echos-ocr/src/lib/auth.ts

85 lines
2.4 KiB
TypeScript
Raw Normal View History

import { prisma } from "./db";
export type AppUser = {
id: string;
authentikUid: string;
username: string;
displayName: string;
email: string;
avatarUrl: string;
role: "admin" | "reviewer" | "viewer";
};
type CacheEntry = { user: AppUser; ts: number };
const userCache = new Map<string, CacheEntry>();
const CACHE_TTL_MS = 60_000;
function mapGroupsToRole(groups: string[]): "admin" | "reviewer" | "viewer" {
const lower = groups.map((g) => g.toLowerCase());
if (lower.some((g) => g.includes("admin"))) return "admin";
if (lower.some((g) => g.includes("reviewer") || g.includes("review"))) return "reviewer";
return "viewer";
}
export async function getOrCreateUser(headers: Headers): Promise<AppUser | null> {
const uid = headers.get("x-authentik-uid") ?? "";
const username = headers.get("x-authentik-username") ?? "";
const email = headers.get("x-authentik-email") ?? "";
if (!uid && !username && !email) return null;
const cacheKey = uid || username || email;
const cached = userCache.get(cacheKey);
if (cached && Date.now() - cached.ts < CACHE_TTL_MS) {
return cached.user;
}
const name = headers.get("x-authentik-name") ?? "";
const groupsRaw = headers.get("x-authentik-groups") ?? "";
const groups = groupsRaw ? groupsRaw.split("|") : [];
const role = mapGroupsToRole(groups);
const dbUser = await prisma.user.upsert({
where: { authentikUid: uid || `fallback-${username || email}` },
update: {
username: username || undefined,
displayName: name || username || undefined,
email: email || undefined,
role,
},
create: {
authentikUid: uid || `fallback-${username || email}`,
username: username || email,
displayName: name || username || email,
email,
role,
},
});
const user: AppUser = {
id: dbUser.id,
authentikUid: dbUser.authentikUid,
username: dbUser.username,
displayName: dbUser.displayName,
email: dbUser.email,
avatarUrl: dbUser.avatarUrl,
role: dbUser.role as AppUser["role"],
};
userCache.set(cacheKey, { user, ts: Date.now() });
return user;
}
export function requireRole(user: AppUser | null, ...roles: AppUser["role"][]): void {
if (!user || !roles.includes(user.role)) {
throw new RoleError("Insufficient permissions");
}
}
export class RoleError extends Error {
constructor(message: string) {
super(message);
this.name = "RoleError";
}
}