diff --git a/src/css/style.css b/src/css/style.css
index 0b1fcd6..c325a47 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -299,6 +299,6 @@ div[data-cvm-node=vm0b0t] {
}
}
-#accountDropdownMenuLink, #accountModalError {
+#accountDropdownMenuLink, #accountModalError, #accountModalSuccess {
display: none;
}
\ No newline at end of file
diff --git a/src/html/index.html b/src/html/index.html
index 20fa843..95fdff7 100644
--- a/src/html/index.html
+++ b/src/html/index.html
@@ -109,6 +109,10 @@
+
+
+
+
+
+
diff --git a/src/ts/AuthManager.ts b/src/ts/AuthManager.ts
index 89c23fc..d23422f 100644
--- a/src/ts/AuthManager.ts
+++ b/src/ts/AuthManager.ts
@@ -153,6 +153,43 @@ export default class AuthManager {
res(json);
});
}
+
+ sendPasswordResetEmail(username : string, email : string, captchaToken : string | undefined) {
+ return new Promise(async res => {
+ if (!this.info) throw new Error("Cannot send password reset email without fetching API information.");
+ if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token.");
+ var data = await fetch(this.apiEndpoint + "/api/v1/sendreset", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ username: username,
+ email: email,
+ captchaToken: captchaToken
+ })
+ });
+ res(await data.json() as PasswordResetResult);
+ });
+ }
+
+ resetPassword(username : string, email : string, code : string, newPassword : string) {
+ return new Promise(async res => {
+ var data = await fetch(this.apiEndpoint + "/api/v1/reset", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ username: username,
+ email: email,
+ code: code,
+ newPassword: newPassword
+ })
+ });
+ res(await data.json() as PasswordResetResult);
+ });
+ }
}
export interface AuthServerInformation {
@@ -211,4 +248,9 @@ export interface UpdateAccountResult {
error : string | undefined;
verificationRequired : boolean | undefined;
sessionExpired : boolean | undefined;
+}
+
+export interface PasswordResetResult {
+ success : boolean;
+ error : string | undefined;
}
\ No newline at end of file
diff --git a/src/ts/i18n.ts b/src/ts/i18n.ts
index 687900f..e99cd5f 100644
--- a/src/ts/i18n.ts
+++ b/src/ts/i18n.ts
@@ -49,8 +49,11 @@ export enum I18nStringKey {
kAccountModal_Register = 'kAccountModal_Register',
kAccountModal_Verify = 'kAccountModal_Verify',
kAccountModal_AccountSettings = 'kAccountModal_AccountSettings',
+ kAccountModal_ResetPassword = 'kAccountModal_ResetPassword',
kAccountModal_VerifyText = 'kAccountModal_VerifyText',
+ kAccountModal_VerifyPasswordResetText = 'kAccountModal_VerifyPasswordResetText',
+ kAccountModal_PasswordResetSuccess = 'kAccountModal_PasswordResetSuccess',
kMissingCaptcha = 'kMissingCaptcha',
kPasswordsMustMatch = 'kPasswordsMustMatch',
diff --git a/src/ts/main.ts b/src/ts/main.ts
index 94fea75..b863f26 100644
--- a/src/ts/main.ts
+++ b/src/ts/main.ts
@@ -79,6 +79,9 @@ const elements = {
accountModalError: document.getElementById("accountModalError") as HTMLDivElement,
accountModalErrorText: document.getElementById("accountModalErrorText") as HTMLSpanElement,
accountModalErrorDismiss: document.getElementById("accountModalErrorDismiss") as HTMLButtonElement,
+ accountModalSuccess: document.getElementById("accountModalSuccess") as HTMLDivElement,
+ accountModalSuccessText: document.getElementById("accountModalSuccessText") as HTMLSpanElement,
+ accountModalSuccessDismiss: document.getElementById("accountModalSuccessDismiss") as HTMLButtonElement,
accountLoginSection: document.getElementById("accountLoginSection") as HTMLDivElement,
accountRegisterSection: document.getElementById("accountRegisterSection") as HTMLDivElement,
accountVerifyEmailSection: document.getElementById("accountVerifyEmailSection") as HTMLDivElement,
@@ -107,6 +110,20 @@ const elements = {
accountSettingsNewPassword: document.getElementById("accountSettingsNewPassword") as HTMLInputElement,
accountSettingsConfirmNewPassword: document.getElementById("accountSettingsConfirmNewPassword") as HTMLInputElement,
accountSettingsCurrentPassword: document.getElementById("accountSettingsCurrentPassword") as HTMLInputElement,
+
+ accountResetPasswordSection: document.getElementById("accountResetPasswordSection") as HTMLDivElement,
+ accountResetPasswordForm: document.getElementById("accountResetPasswordForm") as HTMLFormElement,
+ accountResetPasswordEmail: document.getElementById("accountResetPasswordEmail") as HTMLInputElement,
+ accountResetPasswordUsername: document.getElementById("accountResetPasswordUsername") as HTMLInputElement,
+ accountResetPasswordCaptcha: document.getElementById("accountResetPasswordCaptcha") as HTMLDivElement,
+
+ accountResetPasswordVerifySection: document.getElementById("accountResetPasswordVerifySection") as HTMLDivElement,
+ accountVerifyPasswordResetText: document.getElementById("accountVerifyPasswordResetText") as HTMLParagraphElement,
+ accountResetPasswordVerifyForm: document.getElementById("accountResetPasswordVerifyForm") as HTMLFormElement,
+ accountResetPasswordCode: document.getElementById("accountResetPasswordCode") as HTMLInputElement,
+ accountResetPasswordNewPassword: document.getElementById("accountResetPasswordNewPassword") as HTMLInputElement,
+ accountResetPasswordConfirmNewPassword: document.getElementById("accountResetPasswordConfirmNewPassword") as HTMLInputElement,
+ accountForgotPasswordButton: document.getElementById("accountForgotPasswordButton") as HTMLButtonElement,
};
let auth : AuthManager|null = null;
@@ -879,10 +896,12 @@ async function renderAuth() {
elements.accountLogoutButton.style.display = "none";
elements.accountRegisterCaptcha.innerHTML = "";
elements.accountLoginCaptcha.innerHTML = "";
+ elements.accountResetPasswordCaptcha.innerHTML = "";
if (auth!.info!.hcaptcha.required) {
var hconfig = {sitekey: auth!.info!.hcaptcha.siteKey!};
hcaptcha.render(elements.accountRegisterCaptcha, hconfig);
hcaptcha.render(elements.accountLoginCaptcha, hconfig);
+ hcaptcha.render(elements.accountResetPasswordCaptcha, hconfig);
}
var token = localStorage.getItem("collabvm_session_" + new URL(auth!.apiEndpoint).host);
if (token) {
@@ -905,12 +924,15 @@ function loadAccount() {
}
const accountModal = new bootstrap.Modal(elements.accountModal);
elements.accountModalErrorDismiss.addEventListener('click', () => elements.accountModalError.style.display = "none");
+elements.accountModalSuccessDismiss.addEventListener('click', () => elements.accountModalSuccess.style.display = "none");
elements.accountLoginButton.addEventListener("click", () => {
elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_Login);
elements.accountRegisterSection.style.display = "none";
elements.accountVerifyEmailSection.style.display = "none";
elements.accountLoginSection.style.display = "block";
elements.accountSettingsSection.style.display = "none";
+ elements.accountResetPasswordSection.style.display = "none";
+ elements.accountResetPasswordVerifySection.style.display = "none";
accountModal.show();
});
elements.accountRegisterButton.addEventListener("click", () => {
@@ -919,6 +941,8 @@ elements.accountRegisterButton.addEventListener("click", () => {
elements.accountVerifyEmailSection.style.display = "none";
elements.accountLoginSection.style.display = "none";
elements.accountSettingsSection.style.display = "none";
+ elements.accountResetPasswordSection.style.display = "none";
+ elements.accountResetPasswordVerifySection.style.display = "none";
accountModal.show();
});
elements.accountSettingsButton.addEventListener("click", () => {
@@ -927,6 +951,8 @@ elements.accountSettingsButton.addEventListener("click", () => {
elements.accountVerifyEmailSection.style.display = "none";
elements.accountLoginSection.style.display = "none";
elements.accountSettingsSection.style.display = "block";
+ elements.accountResetPasswordSection.style.display = "none";
+ elements.accountResetPasswordVerifySection.style.display = "none";
// Fill fields
elements.accountSettingsUsername.value = auth!.account!.username;
elements.accountSettingsEmail.value = auth!.account!.email;
@@ -939,6 +965,11 @@ elements.accountLogoutButton.addEventListener('click', async () => {
if (VM) closeVM();
renderAuth();
});
+elements.accountForgotPasswordButton.addEventListener('click', () => {
+ elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_ResetPassword);
+ elements.accountLoginSection.style.display = "none";
+ elements.accountResetPasswordSection.style.display = "block";
+});
// i dont know if theres a better place to put this
let accountBeingVerified;
elements.accountLoginForm.addEventListener('submit', async (e) => {
@@ -1084,6 +1115,65 @@ elements.accountSettingsForm.addEventListener('submit', async e => {
}
return false;
});
+let resetPasswordUsername;
+let resetPasswordEmail;
+elements.accountResetPasswordForm.addEventListener('submit', async e => {
+ e.preventDefault();
+ var hcaptchaToken = undefined;
+ var hcaptchaID = undefined;
+ if (auth!.info!.hcaptcha.required) {
+ hcaptchaID = elements.accountResetPasswordCaptcha.children[0].getAttribute("data-hcaptcha-widget-id")!
+ var response = hcaptcha.getResponse(hcaptchaID);
+ if (response === "") {
+ elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha);
+ elements.accountModalError.style.display = "block";
+ return false;
+ }
+ hcaptchaToken = response;
+ }
+ var username = elements.accountResetPasswordUsername.value;
+ var email = elements.accountResetPasswordEmail.value;
+ var result = await auth!.sendPasswordResetEmail(username, email, hcaptchaToken);
+ if (auth!.info!.hcaptcha.required) hcaptcha.reset(hcaptchaID);
+ if (result.success) {
+ resetPasswordUsername = username;
+ resetPasswordEmail = email;
+ elements.accountResetPasswordUsername.value = "";
+ elements.accountResetPasswordEmail.value = "";
+ elements.accountVerifyPasswordResetText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyPasswordResetText, email);
+ elements.accountResetPasswordSection.style.display = "none";
+ elements.accountResetPasswordVerifySection.style.display = "block";
+ } else {
+ elements.accountModalErrorText.innerHTML = result.error!;
+ elements.accountModalError.style.display = "block";
+ }
+ return false;
+});
+elements.accountResetPasswordVerifyForm.addEventListener('submit', async e => {
+ e.preventDefault();
+ var code = elements.accountResetPasswordCode.value;
+ var password = elements.accountResetPasswordNewPassword.value;
+ if (password !== elements.accountResetPasswordConfirmNewPassword.value) {
+ elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kPasswordsMustMatch);
+ elements.accountModalError.style.display = "block";
+ return false;
+ }
+ var result = await auth!.resetPassword(resetPasswordUsername!, resetPasswordEmail!, code, password);
+ if (result.success) {
+ elements.accountResetPasswordCode.value = "";
+ elements.accountResetPasswordNewPassword.value = "";
+ elements.accountResetPasswordConfirmNewPassword.value = "";
+ elements.accountModalSuccessText.innerHTML = TheI18n.GetString(I18nStringKey.kAccountModal_PasswordResetSuccess);
+ elements.accountModalSuccess.style.display = "block";
+ elements.accountResetPasswordVerifySection.style.display = "none";
+ elements.accountLoginSection.style.display = "block";
+
+ } else {
+ elements.accountModalErrorText.innerHTML = result.error!;
+ elements.accountModalError.style.display = "block";
+ }
+ return false;
+});
// Public API
w.collabvm = {
diff --git a/static/lang/en-us.json b/static/lang/en-us.json
index 33d4507..657542f 100644
--- a/static/lang/en-us.json
+++ b/static/lang/en-us.json
@@ -45,10 +45,13 @@
"kAccountModal_Register": "Register",
"kAccountModal_Verify": "Verify E-Mail",
"kAccountModal_AccountSettings": "Account Settings",
+ "kAccountModal_ResetPassword": "Reset Password",
"kMissingCaptcha": "Please fill out the captcha.",
"kPasswordsMustMatch": "Passwords must match.",
"kAccountModal_VerifyText": "We sent an E-Mail to {0}. To verify your account, please enter the 8-digit code from the E-Mail below.",
+ "kAccountModal_VerifyPasswordResetText": "We sent an E-Mail to {0}. To reset your password, please enter the 8-digit code from the E-Mail below.",
+ "kAccountModal_PasswordResetSuccess": "Your password has been changed successfully. You can now log in with your new password.",
"kNotLoggedIn": "Not Logged in"
}