Isolate next dev from production .next, add build-guard/atomic deploy/health watchdog, error boundaries, and fix JWT startup, meetup leaks, media path traversal, SVG/memory uploads, and JSON-LD escaping. Co-authored-by: Cursor <cursoragent@cursor.com>
90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
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<void> {
|
|
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<void> => {
|
|
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' });
|
|
}
|
|
};
|
|
}
|