- ApiToken (nur SHA-256), Bearer-Anmeldung, POST /auth/token für proxdeploy login, Verwaltung in den Einstellungen - anfrageErgaenzen: Storage, Bridge, Node, Benutzer und Schlüssel aus Server-Standards, Verbindungstest und Anmeldung – auch beim Template-Bau - Abschlussbox serverseitig (abschluss), Web und CLI zeigen dieselben Zeilen - Antwortschemas als zod, daraus /api/v1/openapi.json und cli/api/openapi.json (pnpm openapi) Refs #013 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
3.0 KiB
TypeScript
69 lines
3.0 KiB
TypeScript
import { cookies } from "next/headers";
|
||
import { redirect } from "next/navigation";
|
||
import type { Benutzer } from "@/generated/prisma/client";
|
||
import type { Quelle } from "@/generated/prisma/enums";
|
||
import { prisma } from "@/lib/db";
|
||
import { SITZUNG_COOKIE, sitzungLesen } from "@/lib/sitzung";
|
||
import { darf, type Recht } from "@/server/auth/rechte";
|
||
import { keinRecht, nichtAngemeldet } from "@/server/fehler";
|
||
import { benutzerAusToken } from "@/server/services/tokens";
|
||
|
||
/**
|
||
* Wer fragt an? Rolle und Aktiv-Status kommen **je Anfrage aus der Datenbank**, nie aus dem
|
||
* Token. Jede Seite und jeder Route Handler prüft selbst – das Layout ist nur das zweite Netz,
|
||
* weil Next es bei Navigation innerhalb desselben Layouts nicht neu rendert.
|
||
*/
|
||
export type Aufrufer = { benutzer: Benutzer; quelle: Quelle };
|
||
|
||
async function benutzerAusSitzung(jwt: string | undefined): Promise<Benutzer | null> {
|
||
const sitzung = await sitzungLesen(jwt);
|
||
if (!sitzung) return null;
|
||
const benutzer = await prisma.benutzer.findUnique({ where: { id: sitzung.benutzerId } });
|
||
return benutzer?.aktiv ? benutzer : null;
|
||
}
|
||
|
||
/** Für Server Components. */
|
||
export async function aktuellerBenutzer(): Promise<Benutzer | null> {
|
||
const speicher = await cookies();
|
||
return await benutzerAusSitzung(speicher.get(SITZUNG_COOKIE)?.value);
|
||
}
|
||
|
||
/** Für Seiten: leitet zur Anmeldung um oder zeigt „kein Recht“. */
|
||
export async function seiteSchuetzen(recht: Recht, pfad: string): Promise<Benutzer> {
|
||
const benutzer = await aktuellerBenutzer();
|
||
if (!benutzer) redirect(`/anmelden?ziel=${encodeURIComponent(pfad)}`);
|
||
if (!darf(benutzer.rolle, recht)) redirect("/kein-recht");
|
||
return benutzer;
|
||
}
|
||
|
||
function cookieAusKopf(kopf: string | null, name: string): string | undefined {
|
||
if (!kopf) return undefined;
|
||
for (const teil of kopf.split(";")) {
|
||
const [schluessel, ...wert] = teil.trim().split("=");
|
||
if (schluessel === name) return decodeURIComponent(wert.join("="));
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
/**
|
||
* Für Route Handler: Bearer-Token (CLI) oder Sitzungscookie (Web) – hier an einer Stelle, damit
|
||
* kein Handler zwei Wege kennen muss. Ein angegebenes, aber ungültiges Token gilt als nicht
|
||
* angemeldet, auch wenn zusätzlich ein Cookie mitkommt.
|
||
*/
|
||
export async function anfrageAufrufer(anfrage: Request): Promise<Aufrufer | null> {
|
||
const kopf = anfrage.headers.get("authorization");
|
||
if (kopf?.startsWith("Bearer ")) {
|
||
const ausToken = await benutzerAusToken(kopf.slice("Bearer ".length).trim());
|
||
return ausToken ? { benutzer: ausToken, quelle: "CLI" } : null;
|
||
}
|
||
const benutzer = await benutzerAusSitzung(cookieAusKopf(anfrage.headers.get("cookie"), SITZUNG_COOKIE));
|
||
return benutzer ? { benutzer, quelle: "WEB" } : null;
|
||
}
|
||
|
||
export async function rechtErforderlich(anfrage: Request, recht: Recht): Promise<Aufrufer> {
|
||
const aufrufer = await anfrageAufrufer(anfrage);
|
||
if (!aufrufer) throw nichtAngemeldet();
|
||
if (!darf(aufrufer.benutzer.rolle, recht)) throw keinRecht();
|
||
return aufrufer;
|
||
}
|