From 837a34d8cc05bfb0f0d937ef8e3bff65fbf64704 Mon Sep 17 00:00:00 2001 From: Elijah R Date: Fri, 5 Apr 2024 20:05:49 -0400 Subject: [PATCH] implement password resets --- src/css/style.css | 2 +- src/html/index.html | 32 ++++++++++++++- src/ts/AuthManager.ts | 42 ++++++++++++++++++++ src/ts/i18n.ts | 3 ++ src/ts/main.ts | 90 ++++++++++++++++++++++++++++++++++++++++++ static/lang/en-us.json | 3 ++ 6 files changed, 169 insertions(+), 3 deletions(-) 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 @@ +

@@ -116,7 +120,7 @@

- +
@@ -141,7 +145,7 @@


-
+


@@ -162,6 +166,30 @@
+
+
+
+
+
+ +
+
+ +
+
+
+ +

+ +
+
+
+
+
+
+ + +
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" }