diff --git a/services/mam-api/src/middleware/auth.js b/services/mam-api/src/middleware/auth.js index 4711ddd..0044bb1 100644 --- a/services/mam-api/src/middleware/auth.js +++ b/services/mam-api/src/middleware/auth.js @@ -2,17 +2,62 @@ * Authentication middleware. * * When AUTH_ENABLED=true in the environment, every protected route requires - * an active session (set by POST /api/v1/auth/login). + * 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, the middleware is a no-op - * so the stack can be deployed and tested without setting up users first. - * Set AUTH_ENABLED=true in production after running POST /api/v1/auth/setup - * to create the first admin account. + * 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. */ -export const requireAuth = (req, res, next) => { +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(); - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Unauthorized' }); + + // ── Session-based auth ──────────────────────────────────────── + if (req.session?.userId) { + req.user = { + id: req.session.userId, + username: req.session.username, + role: req.session.role, + }; + return next(); } - 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 + 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]; + // 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' }); };