forgot pass
This commit is contained in:
@@ -44,6 +44,7 @@ const SMTP_USER = process.env.SMTP_USER || '';
|
||||
const SMTP_PASS = process.env.SMTP_PASS || '';
|
||||
const SMTP_FROM = process.env.SMTP_FROM || SMTP_USER || 'no-reply@accmanager.local';
|
||||
const EMAIL_VERIFY_TOKEN_TTL_MINUTES = Number(process.env.EMAIL_VERIFY_TOKEN_TTL_MINUTES || 30);
|
||||
const PASSWORD_RESET_TOKEN_TTL_MINUTES = Number(process.env.PASSWORD_RESET_TOKEN_TTL_MINUTES || 30);
|
||||
|
||||
let mailTransporter;
|
||||
|
||||
@@ -115,6 +116,10 @@ function getEmailVerificationUrl(token) {
|
||||
return `${APP_BASE_URL}/pages/verify-email.html?token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
function getPasswordResetUrl(token) {
|
||||
return `${APP_BASE_URL}/pages/login.html?mode=reset-password&token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
function canSendEmails() {
|
||||
return Boolean(SMTP_HOST && SMTP_USER && SMTP_PASS);
|
||||
}
|
||||
@@ -180,6 +185,51 @@ async function sendVerificationEmail({ email, username, token }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPasswordResetEmail({ email, username, token }) {
|
||||
const resetUrl = getPasswordResetUrl(token);
|
||||
|
||||
if (!canSendEmails()) {
|
||||
console.warn(`SMTP is not configured. Password reset URL for ${email}: ${resetUrl}`);
|
||||
return {
|
||||
sent: false,
|
||||
previewUrl: resetUrl,
|
||||
reason: 'SMTP is not configured'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const transporter = getMailTransporter();
|
||||
await transporter.sendMail({
|
||||
from: SMTP_FROM,
|
||||
to: email,
|
||||
subject: 'AccManager - Reset your password',
|
||||
text: `Hello ${username || 'there'},\n\nA password reset was requested for your account.\nOpen this link to set a new password:\n${resetUrl}\n\nThis link will expire in ${PASSWORD_RESET_TOKEN_TTL_MINUTES} minutes.\n\nIf you did not request this, please ignore this email.`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; line-height: 1.5; color: #1f2937;">
|
||||
<h2 style="margin-bottom: 8px;">Reset your password</h2>
|
||||
<p>Hello ${username || 'there'},</p>
|
||||
<p>We received a password reset request for your account.</p>
|
||||
<p style="margin: 18px 0;">
|
||||
<a href="${resetUrl}" style="background:#2563eb;color:#ffffff;padding:10px 16px;border-radius:8px;text-decoration:none;font-weight:700;display:inline-block;">Reset Password</a>
|
||||
</p>
|
||||
<p>Or copy this URL into your browser:</p>
|
||||
<p><a href="${resetUrl}">${resetUrl}</a></p>
|
||||
<p>This link will expire in ${PASSWORD_RESET_TOKEN_TTL_MINUTES} minutes.</p>
|
||||
<p>If you did not request this reset, you can ignore this message.</p>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
return { sent: true };
|
||||
} catch (err) {
|
||||
console.error('Send password reset email error:', err.message);
|
||||
return {
|
||||
sent: false,
|
||||
reason: err.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getUserIdFromRequest(req) {
|
||||
const rawUserId = req.headers['x-user-id'] || req.query.userId;
|
||||
const userId = Number(rawUserId);
|
||||
@@ -236,6 +286,15 @@ async function syncAssetDepartmentsFromInventory() {
|
||||
`);
|
||||
}
|
||||
|
||||
async function ensurePasswordResetColumns() {
|
||||
if (!pool) {
|
||||
return;
|
||||
}
|
||||
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.Users','PasswordResetToken') IS NULL ALTER TABLE Users ADD PasswordResetToken NVARCHAR(255) NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.Users','PasswordResetTokenExpires') IS NULL ALTER TABLE Users ADD PasswordResetTokenExpires DATETIME NULL;`);
|
||||
}
|
||||
|
||||
async function ensureDepartmentExists(departmentName) {
|
||||
const normalized = normalizeDepartmentName(departmentName);
|
||||
if (!normalized || !pool) {
|
||||
@@ -1624,6 +1683,8 @@ async function createTables() {
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerifiedAt') IS NULL ALTER TABLE Users ADD EmailVerifiedAt DATETIME NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerifyToken') IS NULL ALTER TABLE Users ADD EmailVerifyToken NVARCHAR(255) NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerifyTokenExpires') IS NULL ALTER TABLE Users ADD EmailVerifyTokenExpires DATETIME NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.Users','PasswordResetToken') IS NULL ALTER TABLE Users ADD PasswordResetToken NVARCHAR(255) NULL;`);
|
||||
await pool.request().query(`IF COL_LENGTH('dbo.Users','PasswordResetTokenExpires') IS NULL ALTER TABLE Users ADD PasswordResetTokenExpires DATETIME NULL;`);
|
||||
await pool.request().query(`UPDATE Users SET EmailVerified = 1, EmailVerifiedAt = ISNULL(EmailVerifiedAt, GETDATE()) WHERE LOWER(ISNULL(Role, '')) = 'admin';`);
|
||||
// Backfill Url to empty string to avoid undefined in responses
|
||||
await pool.request().query(`UPDATE Applications SET Url = '' WHERE Url IS NULL;`);
|
||||
@@ -2025,6 +2086,130 @@ app.post('/api/auth/resend-verification', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/forgot-password', async (req, res) => {
|
||||
try {
|
||||
await ensurePasswordResetColumns();
|
||||
|
||||
const username = String(req.body?.username || '').trim();
|
||||
const email = String(req.body?.email || '').trim().toLowerCase();
|
||||
|
||||
if (!username || !email) {
|
||||
return res.status(400).json({ success: false, message: 'Username and email are required' });
|
||||
}
|
||||
|
||||
const result = await pool.request()
|
||||
.input('username', sql.NVarChar, username)
|
||||
.input('email', sql.NVarChar, email)
|
||||
.query(`SELECT TOP 1 UserId, Username, Email, IsActive
|
||||
FROM Users
|
||||
WHERE Username = @username
|
||||
AND LOWER(ISNULL(Email, '')) = @email`);
|
||||
|
||||
if (result.recordset.length === 0 || !result.recordset[0].IsActive) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'If the username and email match, a password reset email has been sent.'
|
||||
});
|
||||
}
|
||||
|
||||
const user = result.recordset[0];
|
||||
const { token, tokenHash } = generateEmailVerificationToken();
|
||||
|
||||
await pool.request()
|
||||
.input('userId', sql.Int, user.UserId)
|
||||
.input('tokenHash', sql.NVarChar, tokenHash)
|
||||
.input('tokenTtlMinutes', sql.Int, PASSWORD_RESET_TOKEN_TTL_MINUTES)
|
||||
.query(`UPDATE Users
|
||||
SET PasswordResetToken = @tokenHash,
|
||||
PasswordResetTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, GETDATE())
|
||||
WHERE UserId = @userId`);
|
||||
|
||||
const emailResult = await sendPasswordResetEmail({
|
||||
email: user.Email,
|
||||
username: user.Username,
|
||||
token
|
||||
});
|
||||
|
||||
const payload = {
|
||||
success: true,
|
||||
message: emailResult.sent
|
||||
? 'Password reset email sent. Please check your inbox.'
|
||||
: 'SMTP is not configured. Use development reset link or configure SMTP.',
|
||||
emailSent: emailResult.sent
|
||||
};
|
||||
|
||||
if (emailResult.previewUrl) {
|
||||
payload.resetPreviewUrl = emailResult.previewUrl;
|
||||
}
|
||||
|
||||
if (emailResult.reason && !emailResult.sent) {
|
||||
payload.emailError = emailResult.reason;
|
||||
}
|
||||
|
||||
res.json(payload);
|
||||
} catch (err) {
|
||||
console.error('Forgot password error:', err.message);
|
||||
res.status(500).json({ success: false, message: 'Cannot process forgot password request right now' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/reset-password', async (req, res) => {
|
||||
try {
|
||||
await ensurePasswordResetColumns();
|
||||
|
||||
const token = String(req.body?.token || '').trim();
|
||||
const newPassword = String(req.body?.newPassword || req.body?.password || '');
|
||||
|
||||
if (!token || !newPassword) {
|
||||
return res.status(400).json({ success: false, message: 'Reset token and new password are required' });
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return res.status(400).json({ success: false, message: 'New password must be at least 6 characters' });
|
||||
}
|
||||
|
||||
const tokenHash = hashVerificationToken(token);
|
||||
const result = await pool.request()
|
||||
.input('token', sql.NVarChar, tokenHash)
|
||||
.query(`SELECT TOP 1 UserId, PasswordResetTokenExpires, IsActive
|
||||
FROM Users
|
||||
WHERE PasswordResetToken = @token`);
|
||||
|
||||
if (result.recordset.length === 0) {
|
||||
return res.status(400).json({ success: false, message: 'Reset token is invalid or already used' });
|
||||
}
|
||||
|
||||
const user = result.recordset[0];
|
||||
if (!user.IsActive) {
|
||||
return res.status(403).json({ success: false, message: 'Account is inactive. Please contact administrator.' });
|
||||
}
|
||||
|
||||
const expiresAt = user.PasswordResetTokenExpires ? new Date(user.PasswordResetTokenExpires) : null;
|
||||
if (!expiresAt || expiresAt.getTime() < Date.now()) {
|
||||
return res.status(400).json({ success: false, message: 'Reset token has expired. Please request a new reset email.' });
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
const viewPassword = encryptPasswordForView(newPassword);
|
||||
|
||||
await pool.request()
|
||||
.input('userId', sql.Int, user.UserId)
|
||||
.input('password', sql.NVarChar, hashedPassword)
|
||||
.input('viewPassword', sql.NVarChar, viewPassword)
|
||||
.query(`UPDATE Users
|
||||
SET Password = @password,
|
||||
ViewPassword = @viewPassword,
|
||||
PasswordResetToken = NULL,
|
||||
PasswordResetTokenExpires = NULL
|
||||
WHERE UserId = @userId`);
|
||||
|
||||
res.json({ success: true, message: 'Password reset successful. You can sign in now.' });
|
||||
} catch (err) {
|
||||
console.error('Reset password error:', err.message);
|
||||
res.status(500).json({ success: false, message: 'Password reset failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Middleware for role-based access control
|
||||
function normalizeRole(value) {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
|
||||
@@ -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,6 +86,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Remember Me Checkbox -->
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -95,6 +96,10 @@
|
||||
/>
|
||||
<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 -->
|
||||
<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');
|
||||
|
||||
Reference in New Issue
Block a user