// Authentication Routes const express = require('express'); const bcrypt = require('bcryptjs'); const crypto = require('crypto'); const rateLimit = require('express-rate-limit'); const { requireAuth, requireGroup } = require('../middleware/auth'); const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 20, // 20 attempts per window standardHeaders: true, legacyHeaders: false, message: { error: 'Too many login attempts. Please try again in 15 minutes.' } }); function createAuthRouter(db, logAudit) { const router = express.Router(); /** * POST /api/auth/login * * Authenticates a user with username and password, creates a session, * and sets an httpOnly session cookie. Rate-limited to 20 attempts per 15 minutes. * * @body {string} username - The user's login username * @body {string} password - The user's password * @returns {object} 200 - { message: 'Login successful', user: { id, username, email, group } } * @returns {object} 400 - { error: 'Username and password are required' } * @returns {object} 401 - { error: 'Invalid username or password' } | { error: 'Account is disabled' } * @returns {object} 429 - { error: 'Too many login attempts. Please try again in 15 minutes.' } * @returns {object} 500 - { error: 'Login failed' } */ router.post('/login', loginLimiter, async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Username and password are required' }); } try { // Find user const user = await new Promise((resolve, reject) => { db.get( 'SELECT * FROM users WHERE username = ?', [username], (err, row) => { if (err) reject(err); else resolve(row); } ); }); if (!user) { logAudit(db, { userId: null, username: username, action: 'login_failed', entityType: 'auth', entityId: null, details: { reason: 'user_not_found' }, ipAddress: req.ip }); return res.status(401).json({ error: 'Invalid username or password' }); } if (!user.is_active) { logAudit(db, { userId: user.id, username: username, action: 'login_failed', entityType: 'auth', entityId: null, details: { reason: 'account_disabled' }, ipAddress: req.ip }); return res.status(401).json({ error: 'Account is disabled' }); } // Verify password const validPassword = await bcrypt.compare(password, user.password_hash); if (!validPassword) { logAudit(db, { userId: user.id, username: username, action: 'login_failed', entityType: 'auth', entityId: null, details: { reason: 'invalid_password' }, ipAddress: req.ip }); return res.status(401).json({ error: 'Invalid username or password' }); } // Generate session ID const sessionId = crypto.randomBytes(32).toString('hex'); const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours // Create session await new Promise((resolve, reject) => { db.run( 'INSERT INTO sessions (session_id, user_id, expires_at) VALUES (?, ?, ?)', [sessionId, user.id, expiresAt.toISOString()], (err) => { if (err) reject(err); else resolve(); } ); }); // Update last login await new Promise((resolve, reject) => { db.run( 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', [user.id], (err) => { if (err) reject(err); else resolve(); } ); }); // Set cookie res.cookie('session_id', sessionId, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 // 24 hours }); logAudit(db, { userId: user.id, username: user.username, action: 'login', entityType: 'auth', entityId: null, details: { group: user.user_group }, ipAddress: req.ip }); res.json({ message: 'Login successful', user: { id: user.id, username: user.username, email: user.email, group: user.user_group } }); } catch (err) { console.error('Login error:', err); res.status(500).json({ error: 'Login failed' }); } }); /** * POST /api/auth/logout * * Ends the current user session by deleting it from the database * and clearing the session cookie. * * @returns {object} 200 - { message: 'Logged out successfully' } */ router.post('/logout', async (req, res) => { const sessionId = req.cookies?.session_id; if (sessionId) { // Look up user before deleting session const session = await new Promise((resolve) => { db.get( `SELECT u.id as user_id, u.username FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.session_id = ?`, [sessionId], (err, row) => resolve(row || null) ); }); // Delete session from database await new Promise((resolve) => { db.run( 'DELETE FROM sessions WHERE session_id = ?', [sessionId], () => resolve() ); }); if (session) { logAudit(db, { userId: session.user_id, username: session.username, action: 'logout', entityType: 'auth', entityId: null, details: null, ipAddress: req.ip }); } } // Clear cookie res.clearCookie('session_id'); res.json({ message: 'Logged out successfully' }); }); /** * GET /api/auth/me * * Returns the currently authenticated user based on the session cookie. * Clears the cookie and returns 401 if the session is expired or the account is disabled. * * @returns {object} 200 - { user: { id, username, email, group } } * @returns {object} 401 - { error: 'Not authenticated' } | { error: 'Session expired' } | { error: 'Account is disabled' } * @returns {object} 500 - { error: 'Failed to get user' } */ router.get('/me', async (req, res) => { const sessionId = req.cookies?.session_id; if (!sessionId) { return res.status(401).json({ error: 'Not authenticated' }); } try { const session = await new Promise((resolve, reject) => { db.get( `SELECT s.*, u.id as user_id, u.username, u.email, u.user_group, u.is_active FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.session_id = ? AND s.expires_at > datetime('now')`, [sessionId], (err, row) => { if (err) reject(err); else resolve(row); } ); }); if (!session) { res.clearCookie('session_id'); return res.status(401).json({ error: 'Session expired' }); } if (!session.is_active) { res.clearCookie('session_id'); return res.status(401).json({ error: 'Account is disabled' }); } res.json({ user: { id: session.user_id, username: session.username, email: session.email, group: session.user_group } }); } catch (err) { console.error('Get user error:', err); res.status(500).json({ error: 'Failed to get user' }); } }); /** * POST /api/auth/cleanup-sessions * * Deletes all expired sessions from the database. Requires Admin group. * * @returns {object} 200 - { message: 'Expired sessions cleaned up' } * @returns {object} 401 - { error: 'Authentication required' } * @returns {object} 403 - { error: 'Insufficient permissions', required: ['Admin'], current: '...' } * @returns {object} 500 - { error: 'Cleanup failed' } */ router.post('/cleanup-sessions', requireAuth(db), requireGroup('Admin'), async (req, res) => { try { await new Promise((resolve, reject) => { db.run( "DELETE FROM sessions WHERE expires_at < datetime('now')", (err) => { if (err) reject(err); else resolve(); } ); }); res.json({ message: 'Expired sessions cleaned up' }); } catch (err) { console.error('Session cleanup error:', err); res.status(500).json({ error: 'Cleanup failed' }); } }); return router; } module.exports = createAuthRouter;