const express = require('express'); const { verifyPassword } = require('../lib/auth'); // Per-IP login throttle. In-memory on purpose: it only has to blunt online // guessing, and a counter that resets on restart is the acceptable cost of not // writing a failed-attempt row for every knock on an internet-facing port. const WINDOW_MS = 15 * 60 * 1000; const MAX_ATTEMPTS = 10; const attempts = new Map(); // ip -> { count, resetAt } function tooManyAttempts(ip) { const rec = attempts.get(ip); if (!rec || Date.now() > rec.resetAt) return false; return rec.count >= MAX_ATTEMPTS; } function recordFailure(ip) { const rec = attempts.get(ip); if (!rec || Date.now() > rec.resetAt) { attempts.set(ip, { count: 1, resetAt: Date.now() + WINDOW_MS }); } else { rec.count += 1; } } // Keep the map from growing without bound on a long-lived process. setInterval(() => { const now = Date.now(); for (const [ip, rec] of attempts) if (now > rec.resetAt) attempts.delete(ip); }, WINDOW_MS).unref(); module.exports = function(pool) { const router = express.Router(); router.post('/login', async (req, res) => { const ip = req.ip; if (tooManyAttempts(ip)) { return res.status(429).json({ error: 'Too many failed attempts. Try again later.' }); } const username = String(req.body?.username || '').trim(); const password = String(req.body?.password || ''); if (!username || !password) { return res.status(400).json({ error: 'Username and password are required' }); } try { const result = await pool.query( `SELECT id, username, display_name, pass_hash, is_active FROM pf.app_user WHERE lower(username) = lower($1)`, [username] ); const user = result.rows[0]; // Same message and roughly the same work either way: no unknown // user / wrong password / disabled distinction to enumerate. const ok = user && user.is_active && verifyPassword(password, user.pass_hash); if (!ok) { recordFailure(ip); return res.status(401).json({ error: 'Invalid username or password' }); } // New session id on login — an existing cookie can't be fixated. req.session.regenerate(err => { if (err) { console.error(err); return res.status(500).json({ error: 'Could not start session' }); } req.session.user = { id: user.id, username: user.username, display_name: user.display_name, }; pool.query(`UPDATE pf.app_user SET last_login_at = now() WHERE id = $1`, [user.id]) .catch(e => console.error('last_login_at update failed', e)); req.session.save(err2 => { if (err2) { console.error(err2); return res.status(500).json({ error: 'Could not start session' }); } attempts.delete(ip); res.json({ user: req.session.user }); }); }); } catch (err) { console.error(err); res.status(500).json({ error: err.message }); } }); router.post('/logout', (req, res) => { const name = req.session?.cookie && req.app.get('session cookie name'); req.session.destroy(err => { if (err) console.error(err); res.clearCookie(name || 'pf.sid'); res.json({ ok: true }); }); }); // The UI calls this on load to decide between the login screen and the app. router.get('/me', (req, res) => { if (!req.session?.user) return res.status(401).json({ error: 'Not authenticated' }); res.json({ user: req.session.user }); }); return router; };