76 lines
2.1 KiB
TypeScript
76 lines
2.1 KiB
TypeScript
|
|
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();
|
||
|
|
}
|