Frontend-Architektur: BFF, Auth & Proxy-Logik

Das SMARTKRIS Frontend (Next.js App Router) ist mehr als nur das grafische Dashboard. Es fungiert als vollwertiges Backend-for-Frontend (BFF). Der Browser des Endnutzers und externe Drittsysteme kommunizieren ausschließlich mit der Next.js-API (/api/…​), welche wiederum die Anfragen sicher an das isolierte Spring Boot Backend weiterleitet.

1. Das Backend-for-Frontend (BFF) Pattern

Die Architektur erzwingt einen strikten Kommunikationsfluss:

  1. Client-seitige Interaktion: Eine React Client-Komponente (im Browser) sendet eine Anfrage an eine interne Next.js API-Route (z. B. /api/situations).

  2. Next.js Proxy: Der Next.js-Server nimmt die Anfrage entgegen, validiert die Session des Nutzers und reichert den Request mit den notwendigen Authentifizierungs-Headern (Keycloak Access Token & API-Key) an.

  3. Backend-Aufruf: Der Next.js-Server leitet den modifizierten Request an das isolierte Spring Boot Backend (BACKEND_API_URL) weiter.

  4. Antwort: Die Daten fließen auf demselben Weg zurück zum Client. Caching wird für diese dynamischen Dashboard-Routen strikt deaktiviert (Cache-Control: no-store), um jederzeit Echtzeitdaten zu garantieren.

2. Token-Architektur: Admin-Key vs. Service-Token

Das System unterscheidet streng zwischen zwei Arten von API-Schlüsseln, die den Zugriff auf das Spring Boot Backend regeln. Das Next.js-BFF verwaltet und nutzt diese Tokens für unterschiedliche Zwecke:

Der Admin-Key (SMARTKRIS_API_ADMIN_KEY)

  • Herkunft: Wird statisch über die Umgebungsvariable .env konfiguriert.

  • Berechtigung: Verleiht die Rolle ROLE_ADMIN im Backend.

  • Verwendung: Das Next.js-BFF nutzt ausschließlich diesen Key, um mit dem Backend zu kommunizieren. Wenn ein städtischer Mitarbeiter im Dashboard agiert, sendet Next.js diesen Admin-Key zusammen mit dem individuellen Keycloak-Token des Nutzers. So weiß das Backend, dass die Anfrage aus einer vertrauenswürdigen Quelle (dem BFF) stammt, loggt die Aktion im Audit-Log aber unter dem echten Nutzernamen.

Service-Tokens (ApiToken)

  • Herkunft: Werden dynamisch über das Dashboard generiert (Route: POST /api/tokens). Das Backend erzeugt dabei eine UUID.

  • Berechtigung: Verleihen die Rolle ROLE_SERVICE im Backend. Diese Rolle hat keinen Zugriff auf sicherheitskritische Endpunkte wie Benutzerverwaltung oder Audit-Logs.

  • Verwendung: Diese Tokens sind für externe Drittsysteme gedacht (z. B. den smartkris-ws-service, externe Warn-Apps oder digitale Stelen). Diese Systeme übergeben den Service-Token im Header x-api-key. Da kein Keycloak-Nutzer dranhängt, loggt das Backend Aktionen, die mit diesem Token ausgeführt werden, unter dem Namen des Tokens (z. B. "Service: Stelen-System").

3. Die Next.js API-Routen (Das Gateway)

Der Ordner app/api/ enthält die Route-Handler, die als Proxy-Endpunkte für das Frontend dienen. Sie abstrahieren die Komplexität des Backends.

Standard-Proxy-Routen

Die meisten Dateien (z. B. app/api/areas/route.ts, app/api/escalation-levels/route.ts, app/api/tokens/route.ts) implementieren klassische GET, POST, PUT, DELETE Methoden. Sie rufen validateRequest(req) auf und reichen den Aufruf mit getBackendHeaders(auth) an die korrespondierende Spring Boot URL durch.

Komplexe Logik-Routen

Einige Routen übernehmen echte BFF-Verantwortung, indem sie Logik bündeln oder Daten transformieren:

  • app/api/messages/route.ts (Bündelung): Wenn eine neue Meldung erstellt wird, prüft das BFF, ob eine situationId existiert. Fehlt diese, generiert das BFF zunächst einen autonomen POST-Request an das Backend, um eine neue Lage zu erstellen. Erst mit der neuen situationId wird anschließend die Meldung erstellt. Das Frontend benötigt dafür nur einen einzigen Aufruf.

  • app/api/categories/[id]/route.ts (Multipart-Handling): Bei der Aktualisierung von Kategorien mit neuen Icons (PUT / PATCH) prüft die Route den Content-Type. Handelt es sich um multipart/form-data, extrahiert das BFF das File-Objekt, erstellt ein neues FormData-Objekt für das Backend und entfernt bewusst den Content-Type-Header, damit Node.js die Boundary für den ausgehenden Fetch-Request korrekt berechnen kann.

  • app/api/uploads/[…​path]/route.ts und [id]/image/route.ts (Streaming): Dateien wie Kategorie-Icons oder Feedback-Bilder werden nicht in den Arbeitsspeicher geladen, sondern als Stream (new NextResponse(resp.body)) direkt an den Client durchgereicht. Dabei werden Metadaten wie Content-Length und Content-Type exakt vom Backend übernommen.

  • app/api/audit-logs/export/route.ts (Datenverarbeitung): Der PDF-, CSV- und Excel-Export der Audit-Logs findet komplett im Node.js-Prozess des Next.js-Servers statt. Das BFF lädt via Paginierung bis zu 10.000 Einträge aus dem Backend und rendert das Dokument (z.B. mittels pdf-lib inkl. manuellem Word-Wrapping), um das Spring Boot Backend von dieser rechenintensiven Aufgabe zu entlasten.

4. Authentifizierung mit NextAuth & Keycloak (lib/auth.ts)

Die Authentifizierung der Dashboard-Nutzer wird über next-auth in Kombination mit Keycloak gelöst.

  • Token-Handling: Beim Login erhält NextAuth ein access_token und refresh_token. Das Access Token wird decodiert, um die Keycloak-Rollen (realm_access.roles) in die lokale Session zu schreiben.

  • Auto-Refresh: Das Frontend prüft bei jedem Request, ob das Access Token noch gültig ist. Ist es abgelaufen, ruft die Funktion refreshAccessToken() selbstständig den Keycloak Token-Endpunkt auf und erneuert die Session im Hintergrund. Das Token wird sicher auf dem Server verwaltet und niemals im Browser exponiert.

5. Globaler Application State (DataContext)

Für Stammdaten und die Echtzeit-Synchronisation nutzt das Dashboard das React-Context-Pattern (app/DataContext.tsx), um redundante Netzwerk-Requests zu vermeiden.

In der app/layout.tsx (Server Component) werden beim initialen Seitenaufruf parallel alle relevanten Stammdaten (Kategorien, Eskalationsstufen, aktuelle Lagen, Statistiken) aus dem Backend geladen. Diese werden an den DataProvider übergeben.

Client-Komponenten greifen über useData() auf diesen Cache zu. Modifizierende Operationen (z. B. createMessageInState) führen den API-Call aus, aktualisieren bei Erfolg sofort den lokalen React-State (setAllMessages) und lösen im Hintergrund einen automatischen Refresh der Zähler-Statistiken (refreshStats) aus.