/** * Authentication middleware. * * When AUTH_ENABLED=true in the environment, every protected route requires * either: * - An active session (set by POST /api/v1/auth/login), or * - A valid Bearer token in Authorization header (set by POST /api/v1/tokens) * * When AUTH_ENABLED is unset or any other value, all middleware is a no-op so * the stack can be run without user accounts during development. */ import crypto from 'crypto'; import pool from '../db/pool.js'; export const requireAuth = async (req, res, next) => { if (process.env.AUTH_ENABLED !== 'true') return next(); // ── Session-based auth ──────────────────────────────────────── if (req.session?.userId) { req.user = { id: req.session.userId, username: req.session.username, role: req.session.role, }; return next(); } // ── Bearer token auth ───────────────────────────────────────── const authHeader = req.headers.authorization; if (authHeader?.startsWith('Bearer ')) { const raw = authHeader.slice(7).trim(); const hash = crypto.createHash('sha256').update(raw).digest('hex'); try { const { rows } = await pool.query( `SELECT t.user_id AS id, u.username, u.role, t.bound_hostname FROM api_tokens t JOIN users u ON u.id = t.user_id WHERE t.token_hash = $1 AND (t.expires_at IS NULL OR t.expires_at > NOW())`, [hash] ); if (rows.length > 0) { req.user = rows[0]; req.tokenBoundHostname = rows[0].bound_hostname || null; // Fire-and-forget last_used_at update pool.query( 'UPDATE api_tokens SET last_used_at = NOW() WHERE token_hash = $1', [hash] ).catch(() => {}); return next(); } } catch (err) { return next(err); } } return res.status(401).json({ error: 'Unauthorized' }); }; export const requireAdmin = (req, res, next) => { if (process.env.AUTH_ENABLED !== 'true') return next(); if (req.user?.role === 'admin') return next(); return res.status(403).json({ error: 'Admin access required' }); };