import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; import { authService, type ResolvedAccess } from '../services/auth'; import { looksLikeApiKey, resolveApiKey } from '../services/apiKeys'; const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'; export interface AuthPayload { pubkey: string; role: string; } declare global { namespace Express { interface Request { user?: AuthPayload; access?: ResolvedAccess; isApiKey?: boolean; } } } export async function requireAuth(req: Request, res: Response, next: NextFunction): Promise { const header = req.headers.authorization; if (!header || !header.startsWith('Bearer ')) { res.status(401).json({ error: 'Missing or invalid authorization header' }); return; } const token = header.slice(7); // API keys authenticate programmatic clients. They carry their own scoped // permission set rather than a role, so we pre-resolve access here. Wrapped in // try/catch because Express 4 does not catch rejections from async middleware. if (looksLikeApiKey(token)) { try { const access = await resolveApiKey(token); if (!access) { res.status(401).json({ error: 'Invalid or revoked API key' }); return; } req.user = { pubkey: access.pubkey, role: 'apikey' }; req.access = access; req.isApiKey = true; next(); } catch (err) { console.error('API key resolution error:', err); res.status(500).json({ error: 'Internal server error' }); } return; } try { const payload = jwt.verify(token, JWT_SECRET) as AuthPayload; req.user = payload; next(); } catch { res.status(401).json({ error: 'Invalid or expired token' }); } } // Resolves the requester's effective access (env SuperAdmin list plus database) // and gates the request on a single permission key. SuperAdmin bypasses every // check. The resolved access is attached to req.access for downstream handlers. export function requires(permission: string) { return async (req: Request, res: Response, next: NextFunction): Promise => { if (!req.user) { res.status(401).json({ error: 'Not authenticated' }); return; } try { // API key access is already resolved (and scoped) in requireAuth; for JWT // callers, resolve their live role-based access here. let access = req.access; if (!req.isApiKey || !access) { access = await authService.resolveAccess(req.user.pubkey); req.access = access; } if (access.isSuperAdmin || access.permissions.has(permission)) { next(); return; } res.status(403).json({ error: 'Insufficient permissions' }); } catch (err) { console.error('Permission check error:', err); res.status(500).json({ error: 'Internal server error' }); } }; }