feat(mam-api): add auth middleware with session and Bearer token support

This commit is contained in:
Zac Gaetano 2026-05-18 12:45:15 -04:00
parent 2d8c44c529
commit f57ed1b498

View file

@ -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' });
};