Backend-Konzepte & Detaillierte Geschäftslogik

Das Spring Boot Backend von SMARTKRIS kapselt die gesamte Domänen- und Geschäftslogik. Die folgende Dokumentation beschreibt die exakten Verhaltensweisen, Kaskadierungsregeln und System-Mechanismen, die für einen Nachbau oder ein tiefgreifendes Verständnis der Services zwingend erforderlich sind.

1. Domänenmodell & Hierarchie-Regeln

Das System basiert auf einer strikten Trennung und Verknüpfung der Hauptentitäten Situation (Lage/Einsatzschwerpunkt), Message (Meldung) und FeedbackMessage (Bürger-Feedback).

Situationen (Lagen & Einsatzschwerpunkte)

Eine Situation repräsentiert das übergeordnete Ereignis. Ist das Feld parent_id null, handelt es sich um eine "Lage". Ist es gesetzt, handelt es sich um einen "Einsatzschwerpunkt". * Hierarchie-Limitierung: Die Verschachtelung ist strikt auf exakt eine Ebene begrenzt. * Eine Situation kann nicht ihr eigener Parent sein (parentId == id wirft Exception). * Eine Situation, die bereits Children hat (also eine Lage ist), darf keinem Parent zugewiesen werden. * Eine Situation, die bereits ein Child ist (Einsatzschwerpunkt), darf nicht Parent für andere Situationen werden.

Lösch- und Änderungsrestriktionen bei Stammdaten

Stammdaten (Category, EscalationLevel) können nicht gelöscht werden (DELETE), solange noch mindestens eine Situation existiert, die diese referenziert (wirft ConflictException). Ein UPDATE des Namens oder Icons ist möglich, gibt jedoch im Response-DTO eine warning zurück, falls die Kategorie in aktiven Lagen verwendet wird.

2. Status-Lebenszyklus & Kaskadierungs-Logik

Lagen und Meldungen durchlaufen die Status PENDING_AUTHORIZATION, ACTIVE und INACTIVE. Kritische Übergänge erfordern den systemweiten AUTHORIZATION_PIN (Umgebungsvariable app.authorization-pin).

Logik bei Erstellung (CREATE)

  • Situation: Wird eine Situation mit dem korrekten PIN übergeben, wird sie direkt ACTIVE, sonst PENDING_AUTHORIZATION.

  • Auto-Message-Generierung: Nach erfolgreichem Speichern der Situation generiert das System immer automatisch eine initiale Message. Der Content setzt sich zusammen aus $title - $description (bzw. nur $title, falls keine Description vorhanden). Diese initiale Meldung erhält alle verfügbaren Kanäle (Stele, Website, App) und erbt Gebiete, Koordinaten und Eskalationsstufe der Situation.

  • Message: Das Ziel-Status-Flag wird anhand des PINs geprüft. Ausnahme: Ist die zugehörige Situation nicht ACTIVE, wird die neue Meldung zwangsweise auf PENDING_AUTHORIZATION gesetzt, selbst wenn der PIN korrekt war.

Logik bei Aktualisierung (UPDATE Kaskaden)

Das System wendet beim Wechsel des Status harte Kaskaden auf den Baum (Parent → Children → Messages) an:

  • Situation wird ACTIVE:

  • Parent-Kaskade: Hat die Situation einen Parent, wird dieser ebenfalls auf ACTIVE gesetzt.

  • Children-Kaskade: Alle untergeordneten Einsatzschwerpunkte (Children) werden sicherheitshalber auf PENDING_AUTHORIZATION zurückgestuft.

  • Message-Kaskade: Die absolut neueste Meldung aus dem gesamten Hierarchie-Baum (Parent + alle Children) wird gesucht und auf PENDING_AUTHORIZATION gesetzt.

  • Situation wird INACTIVE (zuvor ACTIVE):

  • Alle Children werden auf INACTIVE gesetzt.

  • Alle aktuell ACTIVE Meldungen des gesamten Baums (Parent + Children) werden auf INACTIVE gesetzt.

  • Message wird ACTIVE:

  • Ancestry-Kaskade: Die zugehörige Situation wird auf ACTIVE gesetzt. Das System traversiert den Baum weiter nach oben (While-Loop über parent.parent) und setzt alle Vorfahren ebenfalls auf ACTIVE.

3. Geocoding & File Storage

Das System ergänzt fehlende Daten automatisch und verwaltet Uploads restriktiv.

Nominatim (OpenStreetMap) Geocoding

Der GeocodingService fängt Lücken in den Requests ab: * Reverse-Geocoding: Wenn das Frontend bei einem POST/PUT nur Koordinaten (geoPoint / centerPoint) sendet, aber keine Address, führt das Backend einen Request an nominatim.openstreetmap.org/reverse durch. Die JSON-Antwort füllt die Felder Straße, Hausnummer, PLZ, Stadt und Land (Default: "Deutschland") auf. * Forward-Geocoding: Sendet das Frontend nur eine Adresse, baut der Service einen Query-String aus Straße, Hausnummer, PLZ und Stadt, ruft nominatim.openstreetmap.org/search auf und speichert den zurückgegebenen ersten lat/lon Treffer.

File Storage (Icons & Bilder)

Der FileStorageService speichert Uploads im lokalen Verzeichnis /app/uploads. * Validierung: Es werden ausschließlich image/png, image/jpeg, image/webp und image/svg+xml akzeptiert. * Dateinamen-Sanitization: Der originale Dateiname wird von Sonderzeichen bereinigt. Zur Vermeidung von Kollisionen wird der finale Dateiname im Muster ${UUID}_${Timestamp}_${SafeBaseName}${Extension} generiert.

4. Security, Auth & Proxy-Spoofing

Das Spring Boot Backend läuft im internen Netzwerk und gibt keine Ports nach außen frei. Alle API-Calls erfolgen durch das Next.js-Frontend (BFF), welches als Proxy dient.

Authentifizierung (ApiKeyAuthenticationFilter)

Die SecurityFilterChain ist auf STATELESS konfiguriert. Der Filter prüft bei jedem Request: 1. Master-Key: Ist der Header x-api-key identisch mit smartkris.api.admin-key, wird ROLE_ADMIN vergeben. 2. Service-Token: Passt der Key zu einem aktiven Eintrag in der Tabelle api_token, wird ROLE_SERVICE vergeben.

Umgang mit externen Usern"

Da Next.js die Keycloak-Logins (JWTs) der Nutzer verwaltet und Requests als Proxy weiterleitet, muss das Backend wissen, welcher echte Dashboard-Nutzer die Aktion ausgeführt hat (wichtig für Auditing und das createdBy Feld). * Next.js sendet System-Requests mit dem Master-Key, hängt aber optional die Header x-external-user-id, x-external-user-name und x-external-user-email an. * Der ApiKeyAuthenticationFilter fängt diese Header ab und erzeugt on-the-fly ein temporäres JWT (Gültigkeit 1 Stunde, Algorithmus none), füllt die Claims (sub, name, email) mit den externen Werten ab und reicht dieses Fake-JWT an UserService.findOrCreateUser weiter. * Der User wird in der lokalen PostgreSQL-Tabelle user angelegt/gefunden. Anschließend wird ein CustomAuthenticationPrincipal in den Spring Security Context gelegt. Die nachfolgende Geschäftslogik agiert so, als wäre der Nutzer direkt mit seinem echten Keycloak-Token am Backend authentifiziert.

5. Manipulationssicheres Audit-Logging

Alle Änderungen an Kern-Entitäten (Situation, Message, User, Settings, etc.) werden durch den @EntityListeners(AuditListener::class) abgefangen (@PostPersist, @PostUpdate, @PreRemove).

  • JSON-Diffing: Der Listener generiert ein JSON-Objekt (changes), das bei Erstellung das Feld newState, bei Löschung deletedState und bei Updates oldState sowie newState enthält (als komplettes DTO gemappt).

  • Hash-Signatur: Der AuditLogService speichert Logs mit einer Krypto-Signatur, um nachträgliche Manipulationen in der Datenbank zu erkennen. Die Formel zur Generierung des Raw-Strings lautet exakt: {timestampInMillis}|{userId}|{actionType}|{entityType}|{entityId}|{changesJson}|{lastSignature} Aus diesem String wird ein SHA-256 Hash berechnet und als Hex-String in der Spalte signature abgelegt.

  • Beim Auslesen der Audit-Logs prüft das Backend die Signatur jedes Eintrags erneut. Stimmt der berechnete Hash nicht mit der Datenbank überein, wird das Flag isCompromised = true an das Frontend gesendet.

6. RabbitMQ Event-Publisher

Sobald Entitäten sich ändern, pusht das Backend Events an den RabbitMQ Fanout-Exchange crisis-updates-exchange.

  • Trigger-Bedingung: Events werden nur für Message-Entitäten gefeuert, und auch nur dann, wenn der neue Status ACTIVE ist ODER der alte Status ACTIVE war (z.B. bei Löschung oder Deaktivierung einer aktiven Meldung).

  • Payload-Format:

  • Bei Erstellung (CREATE): {"type": "CREATE", "payload": MessageResponseDto}

  • Bei Aktualisierung (UPDATE): {"type": "UPDATE", "payload": MessageResponseDto}

  • Bei Löschung (DELETE): {"type": "DELETE", "id": 123} (Nur die ID, da die Daten bereits in der Datenbank vernichtet wurden).

7. Statistik-Aggregationen

Das Backend bietet performante Aggregationen für das Dashboard. * Situation Stats: Berechnet via COUNT() die Anzahl aktiver, inaktiver und ausstehender Lagen. Das highEscalationCount zählt alle Lagen, deren Eskalationsstufe levelNumber > 3 ist. Die totalAffectedPopulation ist die Summe der Einwohner (affectedPopulationCount) aller Areas, die mit aktiven Lagen verknüpft sind. * Feedback Stats: Da der Status frei als String konfigurierbar ist, erfolgt das Zählen über hartcodiertes Regex/IgnoreCase-Matching der Kategorienamen: NEW / Neu, IN_PROGRESS / In Bearbeitung, RESOLVED / Erledigt. Die averageResponseTimeMinutes berechnet die durchschnittliche Zeitdauer zwischen der Erstellung des Bürger-Feedbacks und der ersten Antwort (FeedbackMessageReply) des Systems.

8. Strukturiertes Logging

Um das System in Cloud-Umgebungen (Kubernetes, Docker) optimal überwachen zu können, verzichtet das Backend vollständig auf klassische Text-Logs in Dateien. Stattdessen nutzt es einen "Cloud Native"-Ansatz, der perfekt auf Tools wie Grafana (Loki), Promtail, Sentry und Matomo abgestimmt ist.

JSON-Logs via stdout

Alle Logs des Spring Boot Backends werden durch den logstash-logback-encoder formatiert und ausschließlich als flache JSON-Objekte auf die Standardausgabe (stdout) geschrieben. * Der Container-Daemon (Docker/Containerd) fängt diese JSON-Strings ab. * Unnötiger "Spam" von Frameworks (Hibernate, Spring AMQP) wird über die logback-spring.xml auf WARN hochgestuft, um das Log-Volumen gering zu halten.

Tracing mittels MDC (Mapped Diagnostic Context)

Um nachzuvollziehen, welcher Nutzer eine bestimmte Aktion ausgelöst hat, ohne die User-ID in jede einzelne Log-Nachricht hardcoden zu müssen, nutzt das Backend den MDC von SLF4J. * Injektion: Sobald der ApiKeyAuthenticationFilter einen Request erfolgreich authentifiziert hat (egal ob über Master-Key oder externes Spoofing-JWT), schreibt er die userId und den username in den Thread-lokalen MDC. * Effekt: Jedes anschließende logger.info() oder logger.error() im gesamten Backend (z.B. in den Services) enthält in seinem JSON-Output automatisch die Felder "userId" und "username". * Sicherheit: Im finally-Block des Filters wird der MDC mit MDC.clear() wieder geleert, damit keine Daten-Leaks bei Thread-Wiederverwendung (Thread-Pooling) entstehen.

Fehlerbehandlung & Sentry-Integration

Das Backend nutzt einen zentralen @ControllerAdvice (GlobalExceptionHandler), um Abstürze und Exceptions zu fangen und saubere HTTP-Responses (z.B. 404, 409, 401) an das BFF (Next.js) zurückzugeben. * Vollständige Stacktraces: Tritt eine unerwartete Exception (HTTP 500) auf, wird diese mit logger.error("message", ex) geloggt. Das Übergeben des Exception-Objekts als letztes Argument ist zwingend erforderlich, da nur so Sentry (via Logback-Appender) den vollständigen Stacktrace extrahieren, gruppieren und als Alert an die Entwickler senden kann.