Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions auth.css
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,20 @@ p{
animation: authFadeUp 560ms cubic-bezier(0.22,1,0.36,1) 500ms forwards;
}

.auth-inline-link{
margin-top:10px;
margin-bottom:6px;
font-size:14px;
animation-delay:460ms;
}

.auth-message{
margin-top:10px;
color:#4d8f67;
font-size:14px;
min-height:20px;
}

a{
color:#ff86ba;
font-weight:600;
Expand Down
113 changes: 113 additions & 0 deletions auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ function getMainPagePath(fileName) {

const USER_STORAGE_KEY = "user";
const AUTH_SESSION_KEY = "authSession";
const PASSWORD_RESET_EMAIL_KEY = "passwordResetEmail";
const SESSION_DURATION_MS = 2 * 60 * 60 * 1000;
const HASH_ITERATIONS = 120000;

Expand Down Expand Up @@ -78,6 +79,18 @@ function storeUser(userRecord) {
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(userRecord));
}

function setPasswordResetEmail(email) {
localStorage.setItem(PASSWORD_RESET_EMAIL_KEY, normalizeEmail(email));
}

function getPasswordResetEmail() {
return normalizeEmail(localStorage.getItem(PASSWORD_RESET_EMAIL_KEY));
}

function clearPasswordResetEmail() {
localStorage.removeItem(PASSWORD_RESET_EMAIL_KEY);
}

async function derivePasswordHash(password, saltBytes, iterations) {
const encoder = new TextEncoder();
const passwordKey = await crypto.subtle.importKey(
Expand Down Expand Up @@ -315,6 +328,106 @@ if (signinForm) {
});
}

/* Forgot password */
const forgotPasswordForm = document.getElementById("forgotPasswordForm");

if (forgotPasswordForm) {
forgotPasswordForm.addEventListener("submit", (event) => {
event.preventDefault();

const email = normalizeEmail(document.getElementById("resetEmail").value);
const error = document.getElementById("forgotError");
const success = document.getElementById("forgotSuccess");
const resetLinkArea = document.getElementById("resetLinkArea");
const storedUser = getStoredUser();

error.textContent = "";
success.textContent = "";
resetLinkArea.hidden = true;

if (!isValidEmail(email)) {
error.textContent = "Please enter a valid email address.";
return;
}

if (!storedUser || normalizeEmail(storedUser.email) !== email) {
error.textContent = "Email not found!";
clearPasswordResetEmail();
return;
}

setPasswordResetEmail(email);
success.textContent = "Reset link generated (demo). Click the link below to continue.";
resetLinkArea.hidden = false;
});
}

/* Reset password */
const resetPasswordForm = document.getElementById("resetPasswordForm");

if (resetPasswordForm) {
resetPasswordForm.addEventListener("submit", async (event) => {
event.preventDefault();

const newPassword = document.getElementById("newPassword").value;
const confirmNewPassword = document.getElementById("confirmNewPassword").value;
const error = document.getElementById("resetError");
const success = document.getElementById("resetSuccess");

error.textContent = "";
success.textContent = "";

const resetEmail = getPasswordResetEmail();
if (!resetEmail) {
error.textContent = "Reset session expired. Start from Forgot Password again.";
return;
}

const passwordMessage = validatePasswordStrength(newPassword);
if (passwordMessage) {
error.textContent = passwordMessage;
return;
}

if (newPassword !== confirmNewPassword) {
error.textContent = "Passwords do not match!";
return;
}

const storedUser = getStoredUser();
if (!storedUser || normalizeEmail(storedUser.email) !== resetEmail) {
error.textContent = "No matching user found for password reset.";
clearPasswordResetEmail();
return;
}

try {
const hashed = await hashPassword(newPassword);
const updatedUser = {
...storedUser,
version: 2,
email: resetEmail,
passwordHash: hashed.hash,
salt: hashed.salt,
iterations: hashed.iterations,
updatedAt: new Date().toISOString(),
};
delete updatedUser.password;

storeUser(updatedUser);
clearPasswordResetEmail();
clearSession();
success.textContent = "Password updated successfully. Redirecting to Sign In...";

setTimeout(() => {
window.location.href = getMainPagePath("signin.html");
}, 1200);
} catch (_error) {
error.textContent = "Unable to reset password right now. Please try again.";
}
});
}

/* Session check */
function checkAuth() {
if (!hasActiveSession()) {
Expand Down
36 changes: 36 additions & 0 deletions forgot.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; img-src 'self' data: https:; font-src 'self' data: https://cdnjs.cloudflare.com; media-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests">
<title>Forgot Password</title>
<link rel="stylesheet" href="auth.css">
</head>

<body class="auth-page">
<div class="auth-container">
<form id="forgotPasswordForm">
<h2>Forgot Password</h2>

<div class="input-group">
<input type="email" id="resetEmail" placeholder="Enter your email" required>
</div>

<div class="error" id="forgotError"></div>
<div class="auth-message" id="forgotSuccess"></div>

<button type="submit">Generate Reset Link</button>

<p id="resetLinkArea" class="auth-message" hidden>
Reset link ready:
<a href="reset.html">Reset Password</a>
</p>

<p>Remembered your password? <a href="signin.html">Back to Sign In</a></p>
</form>
</div>

<script src="auth.js"></script>
</body>
</html>
36 changes: 36 additions & 0 deletions reset.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; img-src 'self' data: https:; font-src 'self' data: https://cdnjs.cloudflare.com; media-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests">
<title>Reset Password</title>
<link rel="stylesheet" href="auth.css">
</head>

<body class="auth-page">
<div class="auth-container">
<form id="resetPasswordForm">
<h2>Reset Password</h2>

<div class="input-group">
<input type="password" id="newPassword" placeholder="New Password" required>
<span class="toggle-password" onclick="togglePassword('newPassword', this)">&#128065;</span>
</div>

<div class="input-group">
<input type="password" id="confirmNewPassword" placeholder="Confirm New Password" required>
<span class="toggle-password" onclick="togglePassword('confirmNewPassword', this)">&#128065;</span>
</div>

<div class="error" id="resetError"></div>
<div class="auth-message" id="resetSuccess"></div>

<button type="submit">Update Password</button>
<p><a href="signin.html">Back to Sign In</a></p>
</form>
</div>

<script src="auth.js"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions signin.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ <h2>Login</h2>

<div class="error" id="loginError"></div>

<p class="auth-inline-link"><a href="forgot.html">Forgot Password?</a></p>

<button type="submit">Sign In</button>
<p>Don't have an account? <a href="signup.html">Sign Up</a></p>
</form>
Expand Down
Loading