skillguard/artifacts/api-server/src/middlewares/auth.ts

76 lines
2.1 KiB
TypeScript
Raw Normal View History

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<AuthInfo> {
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<void> {
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();
}