Add verification resend timestamp and cooldown handling for email verification
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m13s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m13s
This commit is contained in:
@@ -138,6 +138,7 @@ export async function initDatabase() {
|
|||||||
email_verified INTEGER DEFAULT 0,
|
email_verified INTEGER DEFAULT 0,
|
||||||
verification_token TEXT,
|
verification_token TEXT,
|
||||||
verification_token_expires TIMESTAMP,
|
verification_token_expires TIMESTAMP,
|
||||||
|
verification_resend_at TIMESTAMP DEFAULT NULL,
|
||||||
created_at TIMESTAMP DEFAULT NOW(),
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP DEFAULT NOW()
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
);
|
);
|
||||||
@@ -226,6 +227,7 @@ export async function initDatabase() {
|
|||||||
email_verified INTEGER DEFAULT 0,
|
email_verified INTEGER DEFAULT 0,
|
||||||
verification_token TEXT,
|
verification_token TEXT,
|
||||||
verification_token_expires DATETIME,
|
verification_token_expires DATETIME,
|
||||||
|
verification_resend_at DATETIME DEFAULT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -357,6 +359,13 @@ export async function initDatabase() {
|
|||||||
if (!(await db.columnExists('rooms', 'presentation_name'))) {
|
if (!(await db.columnExists('rooms', 'presentation_name'))) {
|
||||||
await db.exec('ALTER TABLE rooms ADD COLUMN presentation_name TEXT DEFAULT NULL');
|
await db.exec('ALTER TABLE rooms ADD COLUMN presentation_name TEXT DEFAULT NULL');
|
||||||
}
|
}
|
||||||
|
if (!(await db.columnExists('users', 'verification_resend_at'))) {
|
||||||
|
if (isPostgres) {
|
||||||
|
await db.exec('ALTER TABLE users ADD COLUMN verification_resend_at TIMESTAMP DEFAULT NULL');
|
||||||
|
} else {
|
||||||
|
await db.exec('ALTER TABLE users ADD COLUMN verification_resend_at DATETIME DEFAULT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Default admin ───────────────────────────────────────────────────────
|
// ── Default admin ───────────────────────────────────────────────────────
|
||||||
const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com';
|
const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com';
|
||||||
|
|||||||
@@ -145,19 +145,29 @@ router.post('/resend-verification', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const user = await db.get('SELECT id, name, display_name, email_verified FROM users WHERE email = ?', [email.toLowerCase()]);
|
const user = await db.get('SELECT id, name, display_name, email_verified, verification_resend_at FROM users WHERE email = ?', [email.toLowerCase()]);
|
||||||
|
|
||||||
if (!user || user.email_verified) {
|
if (!user || user.email_verified) {
|
||||||
// Don't reveal whether account exists
|
// Don't reveal whether account exists
|
||||||
return res.json({ message: 'If an account exists, a new email has been sent.' });
|
return res.json({ message: 'If an account exists, a new email has been sent.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server-side 60s rate limit
|
||||||
|
if (user.verification_resend_at) {
|
||||||
|
const secondsAgo = (Date.now() - new Date(user.verification_resend_at).getTime()) / 1000;
|
||||||
|
if (secondsAgo < 60) {
|
||||||
|
const waitSeconds = Math.ceil(60 - secondsAgo);
|
||||||
|
return res.status(429).json({ error: `Please wait ${waitSeconds} seconds before requesting another email.`, waitSeconds });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const verificationToken = uuidv4();
|
const verificationToken = uuidv4();
|
||||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
await db.run(
|
await db.run(
|
||||||
'UPDATE users SET verification_token = ?, verification_token_expires = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
'UPDATE users SET verification_token = ?, verification_token_expires = ?, verification_resend_at = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||||
[verificationToken, expires, user.id]
|
[verificationToken, expires, now, user.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
|
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
|
||||||
|
|||||||
@@ -29,8 +29,12 @@ export default function Layout() {
|
|||||||
await api.post('/auth/resend-verification', { email: user.email });
|
await api.post('/auth/resend-verification', { email: user.email });
|
||||||
toast.success(t('auth.emailVerificationResendSuccess'));
|
toast.success(t('auth.emailVerificationResendSuccess'));
|
||||||
setResendCooldown(60);
|
setResendCooldown(60);
|
||||||
} catch {
|
} catch (err) {
|
||||||
toast.error(t('auth.emailVerificationResendFailed'));
|
const wait = err.response?.data?.waitSeconds;
|
||||||
|
if (wait) {
|
||||||
|
setResendCooldown(wait);
|
||||||
|
}
|
||||||
|
toast.error(err.response?.data?.error || t('auth.emailVerificationResendFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
setResending(false);
|
setResending(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ export default function Login() {
|
|||||||
await api.post('/auth/resend-verification', { email });
|
await api.post('/auth/resend-verification', { email });
|
||||||
toast.success(t('auth.emailVerificationResendSuccess'));
|
toast.success(t('auth.emailVerificationResendSuccess'));
|
||||||
setResendCooldown(60);
|
setResendCooldown(60);
|
||||||
} catch {
|
} catch (err) {
|
||||||
toast.error(t('auth.emailVerificationResendFailed'));
|
const wait = err.response?.data?.waitSeconds;
|
||||||
|
if (wait) {
|
||||||
|
setResendCooldown(wait);
|
||||||
|
}
|
||||||
|
toast.error(err.response?.data?.error || t('auth.emailVerificationResendFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
setResending(false);
|
setResending(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user