Lesen, erfassen, aendern — mehr nicht. Status, Bereich und Projekt lassen sich von aussen nicht setzen: Was hereinkommt, landet in der Inbox und wird hier einsortiert, sonst raeumt ein fremder Dienst die eigene Ordnung um. Mit einer Kennung ist das Erfassen wiederholbar: Zweimal dasselbe ergibt eine Aufgabe, nicht zwei. Das ist der Unterschied zwischen einer Schnittstelle, die man gefahrlos wiederholen kann, und einer, die bei jedem Netzhaenger Karteileichen anlegt. Jede schreibende Nutzung steht im Protokoll, mitsamt dem Token. Lesen nicht: Ein Dienst, der im Minutentakt abfragt, wuerde das Protokoll unbrauchbar machen. Alle Fehlschlaege sehen von aussen gleich aus. Ob ein Token unbekannt, abgelaufen oder widerrufen ist, geht niemanden etwas an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
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<Wachergebnis> {
|
|
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<unknown> {
|
|
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<string, unknown>,
|
|
): Promise<void> {
|
|
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 },
|
|
})
|
|
}
|