import 'server-only' import { NextResponse } from 'next/server' import { FEHLER, darf, tokenAusKopfzeilen, type Recht } from '@/lib/api' import { auditSchreiben } from '@/server/audit' import { tokenPruefen, type Aufrufer } from '@/server/api/token' /** * Die Tür zur öffentlichen Schnittstelle. * * Jede Route geht hier durch, und jede Route protokolliert. Eine * Schnittstelle, bei der sich hinterher nicht sagen lässt, wer was geändert * hat, ist in einer Anwendung mit fremden Zugängen nicht zu verantworten. */ export type Wachergebnis = { ok: true; aufrufer: Aufrufer } | { ok: false; antwort: NextResponse } function fehler(art: keyof typeof FEHLER): NextResponse { const { status, fehler: text } = FEHLER[art] return NextResponse.json( { fehler: text }, { status, headers: { 'Cache-Control': 'no-store', // Sagt Werkzeugen, wie sie sich ausweisen sollen. ...(status === 401 ? { 'WWW-Authenticate': 'Bearer' } : {}), }, }, ) } /** Prüft Token und Recht in einem Zug. */ export async function wache(anfrage: Request, gebraucht: Recht): Promise { const wert = tokenAusKopfzeilen( anfrage.headers.get('Authorization'), anfrage.headers.get('X-Planer-Token'), ) const aufrufer = await tokenPruefen(wert) if (!aufrufer) return { ok: false, antwort: fehler('nichtAngemeldet') } if (!darf(aufrufer.rechte, gebraucht)) { // Das Fehlen eines Rechts wird protokolliert: Ein Dienst, der immer // wieder an dieselbe Wand läuft, ist entweder falsch eingerichtet oder // versucht etwas. await auditSchreiben({ akteur: { id: aufrufer.benutzerId, email: aufrufer.email }, aktion: 'api.abgewiesen', objektTyp: 'ApiToken', objektId: aufrufer.tokenId, details: { gebraucht, hat: aufrufer.rechte.join(',') }, }) return { ok: false, antwort: fehler('nichtErlaubt') } } return { ok: true, aufrufer } } /** Antwort mit Daten. Nie zwischenspeichern — die Liste ändert sich ständig. */ export function daten(inhalt: unknown, status = 200): NextResponse { return NextResponse.json(inhalt, { status, headers: { 'Cache-Control': 'no-store' }, }) } export function abgelehnt(art: keyof typeof FEHLER): NextResponse { return fehler(art) } /** * Liest den Rumpf als JSON, ohne bei Unsinn zu stolpern. * * Gibt `undefined` zurueck, wenn sich nichts lesen liess — das ist von * einem gelesenen `null` zu unterscheiden. */ export async function rumpfLesen(anfrage: Request): Promise { try { return (await anfrage.json()) as unknown } catch { return undefined } } /** Schreibt einen Protokolleintrag für eine Nutzung der Schnittstelle. */ export async function apiProtokoll( aufrufer: Aufrufer, aktion: string, objektId: string | null, details?: Record, ): Promise { await auditSchreiben({ akteur: { id: aufrufer.benutzerId, email: aufrufer.email }, aktion, objektTyp: 'Aufgabe', objektId, // Der Name des Tokens gehört dazu: Sonst steht im Protokoll nur, dass // "der Benutzer" es war — und das war er ja gerade nicht selbst. details: { ueber: 'api', token: aufrufer.tokenId, ...details }, }) }