64 lines
2.2 KiB
JavaScript
64 lines
2.2 KiB
JavaScript
/**
|
|
* 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' });
|
|
};
|