const express = require('express'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const pool = require('../db/pool'); const router = express.Router(); const BCRYPT_ROUNDS = 12; // min 10 per spec; 12 for extra safety const JWT_SECRET = process.env.JWT_SECRET; const JWT_EXPIRES_IN = '24h'; const COOKIE_MAX_AGE = 24 * 60 * 60 * 1000; // 24h in ms // ─── Helpers ───────────────────────────────────────────────────────────────── function isValidEmail(email) { return typeof email === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } function setAuthCookie(res, token) { res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: COOKIE_MAX_AGE, }); } // ─── POST /api/auth/register ────────────────────────────────────────────────── router.post('/register', async (req, res) => { const { email, password, display_name, role } = req.body; // 400 — validation const errors = []; if (!email || !isValidEmail(email)) errors.push('Valid email is required'); if (!password || password.length < 8) errors.push('Password must be at least 8 characters'); if (!display_name || display_name.trim().length === 0) errors.push('Display name is required'); if (role && !['organizer', 'vendor'].includes(role)) errors.push('Role must be organizer or vendor'); if (errors.length > 0) { return res.status(400).json({ error: errors.join('; ') }); } const userRole = role || 'organizer'; try { // 409 — duplicate email const existing = await pool.query('SELECT id FROM users WHERE email = $1', [email.toLowerCase()]); if (existing.rows.length > 0) { return res.status(409).json({ error: 'An account with this email already exists' }); } const password_hash = await bcrypt.hash(password, BCRYPT_ROUNDS); const result = await pool.query( `INSERT INTO users (email, password_hash, display_name, role) VALUES ($1, $2, $3, $4) RETURNING id, email, display_name, role, created_at`, [email.toLowerCase(), password_hash, display_name.trim(), userRole] ); const user = result.rows[0]; const token = jwt.sign( { id: user.id, email: user.email, role: user.role }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } ); setAuthCookie(res, token); return res.status(201).json({ user: { id: user.id, email: user.email, display_name: user.display_name, role: user.role }, }); } catch (err) { console.error('Registration error:', err.message); return res.status(500).json({ error: 'Registration failed. Please try again.' }); } }); // ─── POST /api/auth/login ───────────────────────────────────────────────────── router.post('/login', async (req, res) => { const { email, password } = req.body; // 400 — validation if (!email || !password) { return res.status(400).json({ error: 'Email and password are required' }); } try { const result = await pool.query( 'SELECT id, email, password_hash, display_name, role FROM users WHERE email = $1', [email.toLowerCase()] ); // Generic 401 — do not reveal whether email or password was wrong if (result.rows.length === 0) { await bcrypt.compare(password, '$2b$12$fakehashtopreventtimingattacks00000000000000000000000'); // timing safe return res.status(401).json({ error: 'Invalid email or password' }); } const user = result.rows[0]; const valid = await bcrypt.compare(password, user.password_hash); if (!valid) { return res.status(401).json({ error: 'Invalid email or password' }); } const token = jwt.sign( { id: user.id, email: user.email, role: user.role }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } ); setAuthCookie(res, token); return res.json({ user: { id: user.id, email: user.email, display_name: user.display_name, role: user.role }, }); } catch (err) { console.error('Login error:', err.message); return res.status(500).json({ error: 'Login failed. Please try again.' }); } }); // ─── POST /api/auth/logout ──────────────────────────────────────────────────── router.post('/logout', (req, res) => { res.clearCookie('token', { httpOnly: true, sameSite: 'lax' }); return res.json({ message: 'Logged out successfully' }); }); // ─── GET /api/auth/me ───────────────────────────────────────────────────────── // Returns current user from cookie — useful for session restore on page refresh const { authMiddleware } = require('../middleware/auth'); router.get('/me', authMiddleware, async (req, res) => { try { const result = await pool.query( 'SELECT id, email, display_name, role FROM users WHERE id = $1', [req.user.id] ); if (result.rows.length === 0) { return res.status(401).json({ error: 'User not found' }); } return res.json({ user: result.rows[0] }); } catch (err) { console.error('Me endpoint error:', err.message); return res.status(500).json({ error: 'Failed to retrieve user' }); } }); module.exports = router;