import type { Request, Response, NextFunction } from "express"; import { getAuth, clerkClient } from "@clerk/express"; import { logger } from "../lib/logger"; /** * Admin access is restricted to an allowlist of email addresses configured via * the ADMIN_EMAILS environment variable (comma-separated, case-insensitive). * Signing in alone never grants admin access — the email must be on the list. */ export function getAdminAllowlist(): string[] { return (process.env.ADMIN_EMAILS ?? "") .split(",") .map((e) => e.trim().toLowerCase()) .filter(Boolean); } export type AuthInfo = { userId: string | null; email: string | null; isAdmin: boolean; }; /** * Resolve the caller's authentication state without throwing. Anonymous * visitors short-circuit before any Clerk API call so public endpoints stay * fast; only signed-in users incur a user lookup to read their email. */ export async function resolveAuth(req: Request): Promise { const auth = getAuth(req); const userId = auth?.userId ?? null; if (!userId) return { userId: null, email: null, isAdmin: false }; let email: string | null = null; try { const user = await clerkClient.users.getUser(userId); email = user.primaryEmailAddress?.emailAddress ?? user.emailAddresses[0]?.emailAddress ?? null; } catch (err) { logger.error({ err }, "Clerk-Benutzer konnte nicht geladen werden"); email = null; } const allowlist = getAdminAllowlist(); const isAdmin = !!email && allowlist.includes(email.toLowerCase()); return { userId, email, isAdmin }; } declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Express { interface Request { auth?: AuthInfo; } } } export async function requireAdmin( req: Request, res: Response, next: NextFunction, ): Promise { const info = await resolveAuth(req); if (!info.userId) { res.status(401).json({ error: "Nicht angemeldet." }); return; } if (!info.isAdmin) { res.status(403).json({ error: "Kein Administratorzugriff." }); return; } req.auth = info; next(); }