const express = require('express'); const bcrypt = require('bcryptjs'); const { audit, one } = require('../db'); const { publicUser, tokenFor } = require('../middleware/auth'); const { capabilitiesForRole } = require('../services/roles'); const { isLocked, registerFailure, registerSuccess, mustChangePassword } = require('../services/passwordPolicy'); const router = express.Router(); router.get('/public/bootstrap', (req, res) => { res.json({ appName: 'DepreCore', setupComplete: Boolean(one('SELECT id FROM users LIMIT 1')), database: 'sqlite' }); }); router.post('/auth/login', (req, res) => { const { email, password } = req.body; const user = one('SELECT * FROM users WHERE lower(email) = lower(?) AND status = ?', [email || '', 'active']); if (!user) return res.status(401).json({ error: 'Invalid email or password' }); // Locked accounts are refused before the password is even checked. if (isLocked(user)) { const minutes = Math.max(1, Math.ceil((new Date(user.locked_until).getTime() - Date.now()) / 60000)); return res.status(423).json({ error: `Account locked. Try again in ${minutes} minute(s).`, lockedUntil: user.locked_until }); } if (!bcrypt.compareSync(password || '', user.password_hash)) { const { locked, lockedUntil } = registerFailure(user); audit(user.id, 'login_failed', 'user', user.id, null, { email: user.email }); if (locked) { const minutes = Math.max(1, Math.ceil((new Date(lockedUntil).getTime() - Date.now()) / 60000)); return res.status(423).json({ error: `Too many failed attempts. Account locked for ${minutes} minute(s).`, lockedUntil }); } return res.status(401).json({ error: 'Invalid email or password' }); } registerSuccess(user); audit(user.id, 'login', 'user', user.id, null, { email: user.email }); // Token is still issued so the user can call the self-service change endpoint when forced. return res.json({ token: tokenFor(user), user: publicUser(user), capabilities: capabilitiesForRole(user.role), mustChangePassword: mustChangePassword(user) }); }); module.exports = router;