forgot pass

This commit is contained in:
2026-04-25 11:58:09 +07:00
parent 4fb7f412bf
commit 8bd67200ce
2 changed files with 509 additions and 27 deletions

View File

@@ -42,7 +42,7 @@
<p class="text-xs uppercase tracking-widest text-on-surface-variant font-bold mt-2">Account Management System</p>
</div>
<div class="flex gap-2 mb-6" role="tablist" aria-label="Auth switcher">
<div id="authTabs" class="flex gap-2 mb-6" role="tablist" aria-label="Auth switcher">
<button id="loginTab" type="button" class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-primary text-on-primary shadow-sm">Đăng nhập</button>
<button id="registerTab" type="button" class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-surface-container-low text-on-surface-variant">Đăng ký</button>
</div>
@@ -86,14 +86,19 @@
</div>
<!-- Remember Me Checkbox -->
<div class="flex items-center">
<input
type="checkbox"
id="remember"
name="remember"
class="w-4 h-4 rounded border-outline-variant/30 text-primary focus:ring-2 focus:ring-primary/50 cursor-pointer"
/>
<label for="remember" class="ml-2.5 text-xs font-medium text-on-surface-variant cursor-pointer">Remember me</label>
<div class="flex items-center justify-between gap-2">
<div class="flex items-center">
<input
type="checkbox"
id="remember"
name="remember"
class="w-4 h-4 rounded border-outline-variant/30 text-primary focus:ring-2 focus:ring-primary/50 cursor-pointer"
/>
<label for="remember" class="ml-2.5 text-xs font-medium text-on-surface-variant cursor-pointer">Remember me</label>
</div>
<button id="forgotPasswordLink" type="button" class="text-xs font-semibold text-primary hover:text-primary-dim transition-colors bg-transparent border-0 p-0">
Forgot password?
</button>
</div>
<!-- Login Button -->
@@ -117,6 +122,115 @@
</div>
</form>
<form id="forgotPasswordForm" class="space-y-5 hidden">
<div class="bg-blue-50 border border-blue-100 rounded-lg px-4 py-3 text-xs text-blue-700">
Enter your username and registered email to receive a password reset link.
</div>
<div>
<label for="forgotUsername" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Username</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">person</span>
</span>
<input
type="text"
id="forgotUsername"
name="forgotUsername"
placeholder="Enter your username"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="forgotEmail" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Registered Email</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">mail</span>
</span>
<input
type="email"
id="forgotEmail"
name="forgotEmail"
placeholder="Enter your registered email"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-4"
>
<span class="material-symbols-outlined text-sm">mark_email_unread</span>
<span>Send reset email</span>
</button>
<button id="forgotBackToLoginBtn" type="button" class="w-full bg-surface-container-low hover:bg-slate-200 text-on-surface-variant font-semibold py-2.5 px-4 rounded-lg transition-colors">
Back to sign in
</button>
<div id="forgotPasswordErrorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
<div id="forgotPasswordSuccessMessage" class="hidden bg-green-50 text-green-800 border border-green-200 rounded-lg px-4 py-3 text-xs font-medium"></div>
</form>
<form id="resetPasswordForm" class="space-y-5 hidden">
<div class="bg-blue-50 border border-blue-100 rounded-lg px-4 py-3 text-xs text-blue-700">
Set your new password below.
</div>
<div>
<label for="resetPassword" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">New Password</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">lock_reset</span>
</span>
<input
type="password"
id="resetPassword"
name="resetPassword"
placeholder="Enter a new password"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="resetConfirmPassword" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Confirm New Password</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">verified_user</span>
</span>
<input
type="password"
id="resetConfirmPassword"
name="resetConfirmPassword"
placeholder="Re-enter new password"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-4"
>
<span class="material-symbols-outlined text-sm">password</span>
<span>Reset password</span>
</button>
<button id="resetBackToLoginBtn" type="button" class="w-full bg-surface-container-low hover:bg-slate-200 text-on-surface-variant font-semibold py-2.5 px-4 rounded-lg transition-colors">
Back to sign in
</button>
<div id="resetPasswordErrorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
</form>
<!-- Register Form -->
<form id="registerForm" class="space-y-5 hidden">
<div>
@@ -211,12 +325,27 @@
</div>
<script>
// Simple login functionality
const authTabs = document.getElementById('authTabs');
const loginForm = document.getElementById('loginForm');
const errorMessage = document.getElementById('errorMessage');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const rememberCheckbox = document.getElementById('remember');
const forgotPasswordLink = document.getElementById('forgotPasswordLink');
const forgotPasswordForm = document.getElementById('forgotPasswordForm');
const forgotUsernameInput = document.getElementById('forgotUsername');
const forgotEmailInput = document.getElementById('forgotEmail');
const forgotBackToLoginBtn = document.getElementById('forgotBackToLoginBtn');
const forgotPasswordErrorMessage = document.getElementById('forgotPasswordErrorMessage');
const forgotPasswordSuccessMessage = document.getElementById('forgotPasswordSuccessMessage');
const resetPasswordForm = document.getElementById('resetPasswordForm');
const resetPasswordInput = document.getElementById('resetPassword');
const resetConfirmPasswordInput = document.getElementById('resetConfirmPassword');
const resetBackToLoginBtn = document.getElementById('resetBackToLoginBtn');
const resetPasswordErrorMessage = document.getElementById('resetPasswordErrorMessage');
const registerForm = document.getElementById('registerForm');
const registerErrorMessage = document.getElementById('registerErrorMessage');
const registerSuccessMessage = document.getElementById('registerSuccessMessage');
@@ -229,19 +358,31 @@
const regPasswordInput = document.getElementById('regPassword');
const loginTab = document.getElementById('loginTab');
const registerTab = document.getElementById('registerTab');
let pendingVerificationIdentifier = '';
let currentMode = 'login';
let resetToken = '';
const setMode = (mode) => {
currentMode = mode;
const isLogin = mode === 'login';
const isRegister = mode === 'register';
const isForgot = mode === 'forgot';
const isReset = mode === 'reset';
loginForm.classList.toggle('hidden', !isLogin);
registerForm.classList.toggle('hidden', isLogin);
registerForm.classList.toggle('hidden', !isRegister);
forgotPasswordForm.classList.toggle('hidden', !isForgot);
resetPasswordForm.classList.toggle('hidden', !isReset);
authTabs.classList.toggle('hidden', isReset);
errorMessage.classList.add('hidden');
registerErrorMessage.classList.add('hidden');
registerSuccessMessage.classList.add('hidden');
verifyNotice.classList.add('hidden');
forgotPasswordErrorMessage.classList.add('hidden');
forgotPasswordSuccessMessage.classList.add('hidden');
resetPasswordErrorMessage.classList.add('hidden');
const activate = (btn, active) => {
btn.classList.toggle('bg-primary', active);
@@ -251,29 +392,88 @@
btn.classList.toggle('text-on-surface-variant', !active);
};
activate(loginTab, isLogin);
activate(registerTab, !isLogin);
activate(loginTab, !isRegister);
activate(registerTab, isRegister);
};
// Check if already logged in
document.addEventListener('DOMContentLoaded', () => {
const currentUser = localStorage.getItem('currentUser');
if (currentUser) {
window.location.href = './index.html';
const clearResetQuery = () => {
const cleanUrl = `${window.location.origin}${window.location.pathname}`;
window.history.replaceState({}, document.title, cleanUrl);
};
const getInitialMode = () => {
const params = new URLSearchParams(window.location.search);
const mode = String(params.get('mode') || '').trim().toLowerCase();
const token = String(params.get('token') || '').trim();
if (mode === 'reset-password') {
resetToken = token;
return token ? 'reset' : 'forgot';
}
setMode('login');
if (mode === 'forgot-password') {
return 'forgot';
}
return 'login';
};
document.addEventListener('DOMContentLoaded', () => {
const initialMode = getInitialMode();
const currentUser = localStorage.getItem('currentUser');
if (currentUser && initialMode !== 'reset') {
window.location.href = './index.html';
return;
}
setMode(initialMode);
if (initialMode === 'forgot') {
forgotUsernameInput.value = usernameInput.value.trim();
}
if (initialMode === 'reset' && !resetToken) {
setMode('forgot');
forgotPasswordErrorMessage.textContent = 'Reset link is invalid. Please request a new password reset email.';
forgotPasswordErrorMessage.classList.remove('hidden');
}
// Restore remembered username
const rememberedUsername = localStorage.getItem('rememberedUsername');
if (rememberedUsername) {
usernameInput.value = rememberedUsername;
rememberCheckbox.checked = true;
if (!forgotUsernameInput.value) {
forgotUsernameInput.value = rememberedUsername;
}
}
});
loginTab.addEventListener('click', () => setMode('login'));
registerTab.addEventListener('click', () => setMode('register'));
loginTab.addEventListener('click', () => {
clearResetQuery();
setMode('login');
});
registerTab.addEventListener('click', () => {
clearResetQuery();
setMode('register');
});
forgotPasswordLink.addEventListener('click', () => {
setMode('forgot');
forgotUsernameInput.value = usernameInput.value.trim();
forgotEmailInput.focus();
});
forgotBackToLoginBtn.addEventListener('click', () => {
clearResetQuery();
setMode('login');
});
resetBackToLoginBtn.addEventListener('click', () => {
resetToken = '';
clearResetQuery();
setMode('login');
});
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
@@ -284,7 +484,6 @@
const password = passwordInput.value;
try {
// Call backend login API
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
@@ -296,24 +495,20 @@
const data = await response.json();
if (data.success && data.user) {
// Store user info from backend
localStorage.setItem('currentUser', JSON.stringify(data.user));
// Handle remember me
if (rememberCheckbox.checked) {
localStorage.setItem('rememberedUsername', username);
} else {
localStorage.removeItem('rememberedUsername');
}
// Redirect to dashboard
window.location.href = './index.html';
} else if (data.requiresEmailVerification) {
pendingVerificationIdentifier = data.username || data.email || username;
verifyNoticeText.textContent = data.message || 'Please confirm your email before signing in';
verifyNotice.classList.remove('hidden');
} else {
// Show error
errorMessage.textContent = data.message || 'Invalid username or password';
errorMessage.classList.remove('hidden');
passwordInput.value = '';
@@ -326,6 +521,108 @@
}
});
forgotPasswordForm.addEventListener('submit', async (e) => {
e.preventDefault();
forgotPasswordErrorMessage.classList.add('hidden');
forgotPasswordSuccessMessage.classList.add('hidden');
const payload = {
username: forgotUsernameInput.value.trim(),
email: forgotEmailInput.value.trim()
};
if (!payload.username || !payload.email) {
forgotPasswordErrorMessage.textContent = 'Username and email are required.';
forgotPasswordErrorMessage.classList.remove('hidden');
return;
}
try {
const response = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok && data?.success) {
const lines = [data.message || 'If the account exists, a password reset email has been sent.'];
if (data.resetPreviewUrl) {
lines.push(`Development reset link: ${data.resetPreviewUrl}`);
}
forgotPasswordSuccessMessage.textContent = lines.join(' ');
forgotPasswordSuccessMessage.classList.remove('hidden');
} else {
forgotPasswordErrorMessage.textContent = data?.message || 'Forgot password request failed.';
forgotPasswordErrorMessage.classList.remove('hidden');
}
} catch (error) {
forgotPasswordErrorMessage.textContent = 'Connection error. Please try again.';
forgotPasswordErrorMessage.classList.remove('hidden');
console.error('Forgot password error:', error);
}
});
resetPasswordForm.addEventListener('submit', async (e) => {
e.preventDefault();
resetPasswordErrorMessage.classList.add('hidden');
const newPassword = resetPasswordInput.value;
const confirmPassword = resetConfirmPasswordInput.value;
if (!resetToken) {
resetPasswordErrorMessage.textContent = 'Reset link is invalid. Please request a new one.';
resetPasswordErrorMessage.classList.remove('hidden');
return;
}
if (!newPassword || newPassword.length < 6) {
resetPasswordErrorMessage.textContent = 'New password must be at least 6 characters.';
resetPasswordErrorMessage.classList.remove('hidden');
return;
}
if (newPassword !== confirmPassword) {
resetPasswordErrorMessage.textContent = 'Confirm password does not match.';
resetPasswordErrorMessage.classList.remove('hidden');
return;
}
try {
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: resetToken,
newPassword
})
});
const data = await response.json();
if (response.ok && data?.success) {
resetToken = '';
resetPasswordInput.value = '';
resetConfirmPasswordInput.value = '';
clearResetQuery();
setMode('login');
verifyNoticeText.textContent = data.message || 'Password reset successful. Please sign in.';
verifyNotice.classList.remove('hidden');
passwordInput.focus();
} else {
resetPasswordErrorMessage.textContent = data?.message || 'Password reset failed.';
resetPasswordErrorMessage.classList.remove('hidden');
}
} catch (error) {
resetPasswordErrorMessage.textContent = 'Connection error. Please try again.';
resetPasswordErrorMessage.classList.remove('hidden');
console.error('Reset password error:', error);
}
});
registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
registerErrorMessage.classList.add('hidden');