import "server-only"; import NextAuth from "next-auth"; import { authConfig } from "@/lib/auth/config"; import { asSystem, sql } from "@/lib/db"; // Die vollständige Anmeldung — die Fassung, die die Datenbank kennt. // // Aufgeteilt ist sie, weil proxy.ts nur den Teil aus lib/auth/config.ts lädt. // Hier kommt das dazu, was einmal pro Anmeldung passieren muss: aus der // Kennung, die Entra ausstellt, eine Kennung machen, die diese Anwendung // versteht. /** * Legt die app_users-Zeile an oder frischt sie auf und liefert die Kennung, * die überall sonst als `userId` durchgereicht wird. * * Die Datenbankfunktion ist SECURITY DEFINER und darf genau dieses eine: * app_users schreiben. Für den einen Schreibvorgang, für den es noch keinen * Sitzungskontext geben kann, ist das der kleinstmögliche Hebel — früher lag * hier ein Dienstschlüssel, der jede Zeile jeder Tabelle lesen konnte. */ async function upsertAppUser(externalId: string, email: string, fullName: string | null): Promise { const row = await asSystem(async (tx) => { const result = await sql<{ id: string }>` select app_upsert_user(${externalId}, ${email}, ${fullName}) as id `.execute(tx); return result.rows[0]; }); if (!row?.id) throw new Error("app_upsert_user() lieferte keine Kennung."); return row.id; } export const { handlers, auth, signIn, signOut } = NextAuth(() => { const base = authConfig(); return { ...base, callbacks: { // Die Rückrufe aus der Basis **behalten**, nicht ersetzen: dort liegt // session(), das die Kennung aus dem Token auf die Sitzung legt. Ein // schlichtes `callbacks: { jwt }` hätte es stillschweigend entfernt. ...base.callbacks, async jwt({ token, profile }) { // `profile` liegt nur beim ersten Durchlauf nach der Rückkehr von Entra // vor. Danach wird das Token nur noch weitergereicht — die Datenbank // wird also einmal pro Anmeldung befragt, nicht einmal pro Aufruf. if (!profile) return token; const externalId = typeof profile.oid === "string" ? profile.oid : null; const email = [profile.email, profile.preferred_username, profile.upn].find( (v): v is string => typeof v === "string" && v.length > 0 ); // Lieber abbrechen als eine Sitzung ohne Kennung ausstellen: die käme // als `null` bei withUser() an, und die Policies gäben dann konsequent // nichts zurück — was sich als „die Anwendung ist leer" zeigt statt als // Anmeldefehler. if (!externalId || !email) throw new Error("Entra lieferte weder oid noch E-Mail-Adresse."); token.uid = await upsertAppUser(externalId, email, typeof profile.name === "string" ? profile.name : null); return token; }, }, }; });