Drei Befunde aus einer Durchsicht der gespeicherten personenbezogenen Daten:
Der Import legte bisher jede Spalte der hochgeladenen Datei dauerhaft ab,
auch Mailadressen oder Rechnungsdaten, die nie zugeordnet wurden. Nach dem
Buchen oder Verwerfen bleiben jetzt nur noch die gemappten Spalten stehen.
Ein fehlgeschlagener Login protokollierte den eingetippten Benutzernamen.
Wer sein Passwort ins Namensfeld tippt, hatte es damit im Klartext im
Audit-Log. Der Name wird nur noch gespeichert, wenn das Konto existiert.
Das Loeschen eines Spielers liess Spuren in den Import-Zeilen und in den
Details frueherer Audit-Eintraege zurueck. Beides wird jetzt mit abgeraeumt;
die Vorgaenge bleiben als Eintrag bestehen, nur ohne Inhalt.
Dazu eine Aufbewahrungsfrist fuer das Audit-Log (Standard 12 Monate, ueber
AUDIT_RETENTION_DAYS einstellbar). Die Guthaben-Historie ist davon nicht
betroffen - sie bleibt die Quelle der Wahrheit fuer bezahlte Ansprueche.
Ein CC, der nicht redesignt wurde, existiert unveraendert weiter und kommt
deshalb immer gratis zurueck - egal ob der Spieler inaktiv war oder die
Einheit voruebergehend aus dem Spiel genommen wurde. Bisher kostete der Fall
OTHER ein Guthaben, was die Regel unnoetig verkompliziert hat.
Damit der zweite Fall brauchbar wird, haelt inactiveNote den Grund im Klartext
fest ("Einheit bis Maerz entfernt"); die Notiz steht an der CC-Karte und wird
beim Reaktivieren wieder geleert.
Mehrstufiges Image: gebaut wird mit allen Abhaengigkeiten, ausgeliefert wird
nur die Laufzeit. Migrationen laufen beim Containerstart. Der App-Port wird
bewusst nicht nach aussen gemappt - davor gehoert ein Reverse Proxy, der HTTPS
terminiert.
Der Backup-Service schreibt taeglich einen pg_dump und raeumt Dumps aelter als
14 Tage weg.
Die Tests starten Postgres per PGlite und sprechen es ueber einen lokalen Port
an, laufen also ohne Docker und ohne installierte Datenbank.
Abgedeckt sind die Guthaben-Regeln, der CC-Lebenszyklus samt Reaktivierung,
Stornos und Ruecknahmen, Anmeldung mit CSRF- und Rollenpruefung sowie der
Import mit Dedup.
Eingaben werden mit zod validiert, jede aendernde Aktion schreibt ins
Audit-Log. Beim ersten Start entsteht ein Administrator samt Startwerten fuer
die Produkterkennung; solange ein Erstpasswort gilt, bleiben schreibende Wege
bis auf den Passwortwechsel gesperrt.
Nicht-API-Pfade fallen auf die SPA zurueck, damit Deep-Links funktionieren.
Die beiden zentralen Regeln stehen jeweils an genau einer Stelle:
Ein CC-Paket erzeugt immer CC +1 und REDESIGN_PAID +1. Die Zeilen dafuer baut
buildPackageRows, das sowohl die manuelle Eingabe als auch der Import nutzen -
so koennen beide Wege nicht auseinanderlaufen.
Ein CC kommt nur dann gratis zurueck, wenn er wegen Spieler-Inaktivitaet
entfernt wurde. Ein reworkter CC existiert im Original nicht mehr und kostet
deshalb ein Redesign- oder CC-Guthaben.
Ueberzogene Toepfe werden zugelassen statt blockiert; die Aktion meldet das
ueber ein overdrawn-Flag zurueck. Storniert wird per Gegenbuchung, nie durch
Loeschen. Der Import berechnet den Plan vorab und dedupliziert ueber die
Bestell-ID, sowohl gegen bereits gebuchte als auch innerhalb einer Datei.
Passwoerter mit Argon2id, im Cookie steht nur ein Token und in der Datenbank
dessen Hash. Das CSRF-Token wird per HMAC aus dem Sitzungstoken abgeleitet
statt separat gespeichert - ohne das httpOnly-Cookie ist es nicht herstellbar.
TOTP ist optional und wird erst nach einem bestaetigten Code scharf geschaltet,
damit sich niemand bei der Einrichtung aussperrt.
Guthaben werden als Kontobuch gefuehrt: jede Bewegung ist eine Zeile, der
Stand eines Topfes ist deren Summe. Zusammengehoerige Zeilen teilen sich eine
groupId, damit ein CC-Paket als Ganzes storniert werden kann; ein unique
Constraint auf reversalOfId verhindert doppelte Stornos.
CCs bilden ueber lineageId und predecessorId eine Redesign-Kette. Der Grund
einer Deaktivierung entscheidet spaeter ueber die Reaktivierungskosten.
Fastify mit Prisma im Server-Workspace, React mit Vite im Web-Workspace.
Das Frontend baut direkt nach server/public, damit am Ende ein einzelnes
Image API und Oberflaeche ausliefert.