const express = require('express'); const { z } = require('zod'); const users = require('../services/users'); const mailer = require('../services/mailer'); const { requireUser } = require('../middleware/auth'); const registerSchema = z.object({ email: z.string().email().max(200), display_name: z.string().trim().min(1).max(80), password: z.string().min(8).max(200), }); const loginSchema = z.object({ email: z.string().email().max(200), password: z.string().min(1).max(200), }); module.exports = function authRoutes(csrfProtection) { const router = express.Router(); router.get('/register', (req, res) => { if (req.session.user) return res.redirect('/'); res.render('auth/register', { title: 'Register', values: {}, errors: {} }); }); router.post('/register', csrfProtection, async (req, res, next) => { try { const parsed = registerSchema.safeParse(req.body); if (!parsed.success) { const errors = {}; for (const issue of parsed.error.issues) errors[issue.path[0]] = issue.message; return res.status(400).render('auth/register', { title: 'Register', values: req.body, errors }); } if (users.findByEmail(parsed.data.email)) { return res.status(400).render('auth/register', { title: 'Register', values: req.body, errors: { email: 'Email already registered.' }, }); } const user = await users.createUser({ email: parsed.data.email, displayName: parsed.data.display_name, password: parsed.data.password, }); req.session.user = user; const token = users.createVerificationToken(user.id); const base = process.env.APP_BASE_URL || 'http://localhost:3000'; mailer.sendVerificationEmail(user.email, `${base}/verify-email?token=${token}`) .catch(err => console.error('[mailer] send failed:', err)); req.flash('success', `Welcome, ${user.displayName}! Check your email to verify your account.`); res.redirect('/'); } catch (err) { next(err); } }); router.get('/login', (req, res) => { if (req.session.user) return res.redirect('/'); res.render('auth/login', { title: 'Log in', values: {}, error: null }); }); router.post('/login', csrfProtection, async (req, res, next) => { try { const parsed = loginSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).render('auth/login', { title: 'Log in', values: req.body, error: 'Invalid input.' }); } const user = await users.verifyCredentials(parsed.data.email, parsed.data.password); if (!user) { return res.status(401).render('auth/login', { title: 'Log in', values: req.body, error: 'Invalid email or password.' }); } req.session.user = user; const target = req.session.returnTo || '/'; delete req.session.returnTo; req.flash('success', `Welcome back, ${user.displayName}.`); res.redirect(target); } catch (err) { next(err); } }); router.post('/logout', csrfProtection, (req, res) => { req.session.destroy(() => res.redirect('/')); }); router.get('/verify-email', (req, res, next) => { try { const { token } = req.query; if (!token || typeof token !== 'string') { return res.render('auth/verify-email', { title: 'Verify Email', state: 'invalid' }); } const result = users.verifyEmailToken(token); if (!result.ok) { return res.render('auth/verify-email', { title: 'Verify Email', state: 'invalid' }); } if (req.session.user && req.session.user.id === result.userId) { req.session.user = { ...req.session.user, emailVerified: true }; } res.render('auth/verify-email', { title: 'Verify Email', state: 'success' }); } catch (err) { next(err); } }); router.post('/resend-verification', requireUser, csrfProtection, (req, res, next) => { try { if (req.session.user.emailVerified) { req.flash('success', 'Your email is already verified.'); return res.redirect('/'); } const token = users.createVerificationToken(req.session.user.id); const base = process.env.APP_BASE_URL || 'http://localhost:3000'; mailer.sendVerificationEmail(req.session.user.email, `${base}/verify-email?token=${token}`) .catch(err => console.error('[mailer] resend failed:', err)); req.flash('success', 'Verification email sent. Check your inbox.'); res.redirect('/'); } catch (err) { next(err); } }); return router; };