implement password resets

This commit is contained in:
Elijah R 2024-04-05 20:05:49 -04:00
parent 29fcfbcc7c
commit 837a34d8cc
6 changed files with 169 additions and 3 deletions

View File

@ -299,6 +299,6 @@ div[data-cvm-node=vm0b0t] {
}
}
#accountDropdownMenuLink, #accountModalError {
#accountDropdownMenuLink, #accountModalError, #accountModalSuccess {
display: none;
}

View File

@ -109,6 +109,10 @@
<span id="accountModalErrorText"></span>
<button type="button" class="btn-close" aria-label="Close" id="accountModalErrorDismiss"></button>
</div>
<div class="alert alert-success alert-dismissible" id="accountModalSuccess" role="alert">
<span id="accountModalSuccessText"></span>
<button type="button" class="btn-close" aria-label="Close" id="accountModalSuccessDismiss"></button>
</div>
<div id="accountLoginSection">
<form id="accountLoginForm">
<label for="accountLoginUsername">Username</label><br/>
@ -116,7 +120,7 @@
<label for="accountLoginPassword">Password</label><br/>
<input id="accountLoginPassword" type="password" class="form-control" placeholder="Password" name="password" required><br>
<div id="accountLoginCaptcha"></div>
<button type="submit" class="btn btn-primary">Login</button>
<button type="submit" class="btn btn-primary">Login</button> <button type="button" class="btn btn-secondary" id="accountForgotPasswordButton">Forgot Password</button>
</form>
</div>
<div id="accountRegisterSection">
@ -141,7 +145,7 @@
<p id="accountVerifyEmailText"></p>
<form id="accountVerifyEmailForm">
<label for="accountVerifyEmailCode">Code</label><br>
<input id="accountVerifyEmailCode" type="text" class="form-control" name="code" required><br>
<input id="accountVerifyEmailCode" type="text" class="form-control" name="code" placeholder="Code" required><br>
<label for="accountVerifyEmailPassword">Your password</label><br>
<input id="accountVerifyEmailPassword" type="password" class="form-control" placeholder="Password" name="password" required/><br/>
<button type="submit" class="btn btn-primary">Verify</button>
@ -162,6 +166,30 @@
<input id="accountSettingsCurrentPassword" type="password" class="form-control" placeholder="Current Password" name="currentpassword" required/><br/>
<button type="submit" class="btn btn-primary">Update</button>
</form>
</div>
<div id="accountResetPasswordSection">
<form id="accountResetPasswordForm">
<label for="accountResetPasswordEmail">E-Mail</label><br>
<input id="accountResetPasswordEmail" type="email" class="form-control" placeholder="E-Mail" name="email" required/><br/>
<label for="accountResetPasswordUsername">Username</label>
<input id="accountResetPasswordUsername" type="text" class="form-control" placeholder="Username" name="username" required/><br/>
<div id="accountResetPasswordCaptcha"></div>
<button type="submit" class="btn btn-primary">Reset</button>
</div>
<div id="accountResetPasswordVerifySection">
<center>
<i class="fa-solid fa-envelope" style="font-size: 12rem"></i>
<p id="accountVerifyPasswordResetText"></p>
<form id="accountResetPasswordVerifyForm">
<label for="accountResetPasswordCode">Code</label><br>
<input id="accountResetPasswordCode" type="text" class="form-control" name="code" placeholder="Code" required><br>
<label for="accountResetPasswordNewPassword">New Password</label><br>
<input id="accountResetPasswordNewPassword" type="password" class="form-control" placeholder="New Password" name="password" required/><br/>
<label for="accountResetPasswordConfirmNewPassword">Confirm New Password</label><br>
<input id="accountResetPasswordConfirmNewPassword" type="password" class="form-control" placeholder="Confirm New Password" name="confirmpassword" required/><br/>
<button type="submit" class="btn btn-primary">Reset</button>
</form>
</center>
</div>
</div>
</div>

View File

@ -153,6 +153,43 @@ export default class AuthManager {
res(json);
});
}
sendPasswordResetEmail(username : string, email : string, captchaToken : string | undefined) {
return new Promise<PasswordResetResult>(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<PasswordResetResult>(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;
}

View File

@ -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',

View File

@ -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 = {

View File

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