92c78a92ff92db2e8a9402b2fb1f010d94045f82
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
92c78a92ff |
Speicherfehler erklaeren und die Ablage von selbst pruefen
Beim Ablegen eines Schreibens stand in der Oberflaeche "Das Schreiben konnte nicht abgelegt werden." Bei einer Fehlersuche am 31.08.2026 lagen hinter genau diesem Satz nacheinander DREI verschiedene Ursachen: ein fehlender Ablagebereich, ein Endpunkt, der auf einen fremden Anbieter zeigte, und eine Verschluesselung, die der Speicher nicht anbietet. Ohne Zugang zum Serverlog waere die Suche aussichtslos gewesen. Jetzt uebersetzt src/lib/speicher/fehler.ts die bekannten Faelle in Saetze, die sagen, was zu tun ist - fehlender Ablagebereich mit Verweis auf Schritt 6 der Installation, abgelehnte Zugangsdaten mit den Namen der beiden Variablen, nicht unterstuetzte Verschluesselung mit dem Hinweis auf S3_SSE, und nicht erreichbarer Speicher mit der Zusicherung, dass die Eingaben bleiben. Durchgereicht wird dabei nichts. Fremde Fehlertexte tragen Hostnamen, Eimernamen und Anfragekennungen - das gehoert ins Log, nicht auf den Bildschirm eines Betreuers. Uebersetzt werden nur die bekannten Faelle; alles andere bleibt bei der allgemeinen Meldung. Dazu eine Bereitschaftspruefung: Sie schreibt eine winzige Datei, liest sie zurueck und raeumt sie weg - und beantwortet damit erreichbar, Ablagebereich vorhanden und Schreibrecht auf einmal. Das Ergebnis steht in /api/gesundheit und als Zeile im Log. ## Warum nicht instrumentation.ts Der naheliegende Ort waere der Starthaken von Next. Der wird aber auch fuer die Edge-Laufzeit gebuendelt, und ueber die Treiber kaeme node:path in ein Buendel, in dem es das nicht gibt - der Build schlaegt fehl. Das war ausprobiert, nicht vermutet. Die Pruefung haengt deshalb an der Gesundheitsabfrage. Der Healthcheck ruft sie ab 40 Sekunden nach dem Start alle 30 Sekunden auf; die erste Pruefung passiert also von selbst kurz nach dem Hochfahren. Eine Schonfrist von fuenf Minuten verhindert, dass dabei alle 30 Sekunden eine Probedatei in den Objektspeicher geht, und gemeldet wird nur bei einer Aenderung. Ein gestoerter Speicher fuehrt ausdruecklich NICHT zu 503. Sonst startete der Healthcheck den Behaelter neu und machte die ganze Akte unerreichbar - wegen eines Teils, den ein Neustart nicht repariert. Gegen drei Mutationen geprueft: NotImplemented ohne Textpruefung, alte Reihenfolge bei der Codeermittlung, Erklaerung nicht benutzt - jede wird rot. Refs #44 |
||
|
|
2bed78915f |
Zweiten Faktor zur Grenze machen, nicht nur zur Anzeige
"Der zweite Faktor ist Pflicht" stand im Kopf von auth.ts, durchgesetzt wurde es aber nur im App-Layout. Ein Layout schuetzt, was durch das Layout gerendert wird - Server Actions und Route Handler laufen daran vorbei. Wer eine Passwort-Sitzung hatte, die TOTP-Einrichtung aber nie abgeschlossen hat, kam an die Daten. Die Pruefung steckt jetzt in erfordereBenutzer() selbst und damit ueber erfordereZugang() automatisch in jeder Server Action. Massgeblich ist der Datenbankstand, nicht das Feld im Sitzungstoken - das laesst sich ueber session.update() vom Client beeinflussen. Fuer Route Handler gibt es benutzerFuerRoute(): 401 bzw. 403 statt einer Umleitung. Eine Anmeldeseite als Inhalt einer Word-Datei waere die falsche Antwort. Dabei aufgefallen: Der Dokumentenabruf unter /api/dokumente/datei fragte nur nach der Anmeldung. Auch geschlossen. Ausnahmen sind aufgezaehlt und werden von tests/mfa-grenze.test.ts festgehalten - der Test liest den Quelltext und schlaegt an, sobald eine weitere dazukommt. Refs #38 |
||
|
|
b2ac13e128 |
feat: Versionierung mit Changelog, Version 1.0.0
Bis jetzt stand nirgends, welcher Stand laeuft. Beim Aktualisieren auf einem Server ist das die erste Frage. CHANGELOG.md nach Keep a Changelog, Versionsnummern nach SemVer. Der Changelog sagt, was sich beim Arbeiten aendert - nicht, welche Datei angefasst wurde. Dazu eine Tabelle, was Major, Minor und Patch fuer den Betreiber praktisch bedeuten, und der Hinweis, dass sich eine Datenbank-Migration nicht zurueckrollen laesst. Die Version steht bewusst an zwei Stellen: package.json und src/lib/version.ts. Ein Import der package.json waere im Standalone-Build eine andere Datei als die des Projekts - die Zahl haenge dann an der Laufzeit. Ein Test haelt beide zusammen, und "npm run version" setzt sie gemeinsam samt Changelog-Abschnitt. Committet und getaggt wird nicht automatisch. Ein Changelog-Eintrag, den niemand gegengelesen hat, ist keinen Deut besser als keiner. Sichtbar wird die Version unten in der Seitenleiste und unter /api/gesundheit - dort mit Git-Stand, den der Docker-Build hineinreicht. Auf der Anmeldeseite steht sie nicht: wer nicht angemeldet ist, muss nicht wissen, welche Fassung hier laeuft. Etappe: 8 Phase: Haertung |
||
|
|
1a91f62ced |
feat: Erinnerungen, Wartungslauf und Playwright-Tests der Anmeldung
Etappe 8, erster Teil. Erinnerungsmails sind bewusst eine Zugabe: Ist kein SMTP konfiguriert, wird nichts gesendet und nichts geworfen. Die Fristen stehen ohnehin im Dashboard und im abonnierten Kalender - eine ausgefallene Mail darf nicht dazu fuehren, dass der Wartungslauf abbricht und danach auch das Aufraeumen liegen bleibt. In der Mail steht, DASS etwas faellig ist und wo man nachsieht - nicht, worum es geht. Kein Name der betreuten Person, keine Inhalte. E-Mail ist unterwegs unverschluesselt und landet auf fremden Servern. Je Frist geht hoechstens eine Mail pro Vorwarnstufe raus. Taeglich waere nach einer Woche Rauschen, das man wegklickt - und genau dann verpasst man die eine, auf die es ankam. Der Wartungslauf faengt jeden Schritt einzeln ab: Papierkorb leeren, Anmeldeversuche und altes Audit-Log aufraeumen laufen auch dann, wenn der SMTP-Server nicht antwortet. Angestossen wird er ueber /api/wartung mit einem HMAC aus AUTH_SECRET - ohne gueltigen Schluessel antwortet die Route mit 404 statt 401. Ein echter Fund aus den Tests: Auth.js reicht den Fehlercode aus authorize() nicht an die Server Action zurueck. Bei gesperrtem Zugang sah man deshalb nur die allgemeine Meldung "stimmen nicht" und haette ratlos weiterprobiert, obwohl die Sperre bereits lief. Die Sperre wird jetzt zusaetzlich in der Server Action geprueft und benannt; die Pruefung in authorize() bleibt als eigentliche Sperre bestehen. Sechs Playwright-Tests der Anmeldung, darunter zwei, die Sicherheitszusagen nachweisen: dass zwei Fehlversuche mit unterschiedlicher Ursache dieselbe Meldung liefern (sonst liesse sich ablesen, welche Adressen existieren), und dass die Anmeldeseite keine einzige Anfrage an einen fremden Host stellt. Etappe: 8 Phase: Haertung |
||
|
|
1234a57fa6 |
fix: Typisierung von oeffnen(), CSP im Entwicklungsmodus, Nonce-Abweichung
Drei Fehler, die sich beim Durchsehen der Typpruefung und der
Browserkonsole gezeigt haben.
1. oeffnen() lieferte Record<string, string | null>. Zusammen mit
noUncheckedIndexedAccess wurde daraus bei jedem Zugriff
string | null | undefined - und jede aufrufende Seite haette das
wegkaesen muessen, obwohl die Felder immer da sind. Jetzt leitet ein
Mapped Type die Klartextnamen aus VERSCHLUESSELTE_FELDER ab:
oeffnen('Betreute', zeile).diagnosen ist sauber string | null.
Das hat auf einen Schlag 17 Folgefehler in fremden Modulen erledigt.
2. Die CSP blockierte im Entwicklungsmodus die eigenen Skripte.
'strict-dynamic' schaltet die Freigabe nach Herkunft ab, also auch
'self' - und Hot-Reload und Fehleranzeige laden ohne Nonce. Man sah
eine tote Seite statt der Fehlermeldung. Im Produktivbetrieb bleibt
es bei Nonce plus 'strict-dynamic', im Entwicklungsmodus gilt die
lockere Fassung.
3. Das Theme-Skript im Layout meldete bei jedem Seitenaufruf eine
Hydrations-Abweichung: React gibt das nonce-Attribut absichtlich
nicht an den Browser weiter, serverseitig steht es aber im HTML.
suppressHydrationWarning am Skript selbst - das html-Element allein
deckt das nicht ab.
Nebenbei: Algorithm.Argon2id ist ein const enum und laesst sich mit
isolatedModules nicht importieren. Die Angabe entfaellt, @node-rs/argon2
benutzt von sich aus Argon2id.
Die Anmeldeseite laedt jetzt ohne einen einzigen Konsolenfehler.
Etappe: 8
Phase: Haertung
|
||
|
|
3833a9dd8f |
docs: README, Datenschutztexte, Dockerfile und Sicherungsskripte
Damit ist Etappe 1 abgeschlossen und betriebsfertig dokumentiert. README mit beiden Betriebswegen. Docker Compose auf eigenem Server ist der Hauptweg; Vercel und Neon sind vollstaendig beschrieben, aber mit einer klaren Begruendung, warum ich davon abrate: beide Anbieter sind US-Unternehmen und unterliegen auch bei EU-Region potenziell dem CLOUD-Act. Bei Gesundheitsdaten einer nicht entscheidungsfaehigen Person ist das eine Abwaegung, die man bewusst treffen muss. Reverse-Proxy-Beispiele fuer Caddy, nginx und Traefik - der Proxy selbst ist nicht Teil des Compose-Setups, weil er auf dem Zielserver schon steht. Die Datenbank hat keine Portfreigabe. Sicherung: pg_dump schreibt direkt in die Verschluesselung, der Klartext landet nie auf der Platte. Danach eine Selbstpruefung - laesst sich die Datei nicht wieder entschluesseln, endet der Lauf mit Fehler statt mit einem stillen Scheinerfolg. Der jeweils erste Stand eines Monats bleibt dauerhaft stehen. Die README sagt ausdruecklich, was das Backup NICHT enthaelt: den Schluessel ENC_KEY_V1 und die Dateien im Objektspeicher. Dazu eine Anleitung, die Wiederherstellung einmal im Jahr zu ueben - mit dem letzten Schritt, in der Anwendung nachzusehen, ob die Diagnosen im Klartext erscheinen. Erst dann stimmt nicht nur die Datenbank, sondern auch der Schluessel. Verarbeitungsverzeichnis und Datenschutzhinweise als ausfuellbare Textbausteine. Die Hinweise sind in einfacher Sprache geschrieben - sie sollen der betreuten Person vorgelesen werden koennen. Die Seite /datenschutz liest denselben Text aus docs/, damit es nicht zwei Fassungen gibt, die auseinanderlaufen. Dockerfile in drei Stufen mit Tesseract, deutschen Sprachdaten und poppler-utils. Zeitzone Europe/Berlin ist gesetzt - ohne sie rechnet der Server in UTC und verschiebt Fristen um einen Tag. Etappe: 1 Phase: Fundament |
||
|
|
22b3a9b9e9 |
feat: Stammdaten, Ersteinrichtung, Zweitfaktor und Protokollansicht
Damit ist Etappe 1 fachlich vollstaendig. Ersteinrichtung in drei Schritten, bewusst ausserhalb des Anwendungsrahmens: der setzt Benutzer, zweiten Faktor UND eine betreute Person voraus - beim allerersten Start gibt es nichts davon. Die Aktion fuer den ersten Zugang sperrt sich selbst, sobald ein Benutzer existiert. Zweiter Faktor: Das Geheimnis wird zuerst nur abgelegt, ohne totpAktiviertAm. Erst ein echter Code aus der App schaltet ihn scharf. Wer sich beim Abtippen vertut oder die falsche App nimmt, wuerde sich sonst dauerhaft aussperren. Erst danach gibt es die Wiederherstellungscodes - einmal angezeigt, danach nur noch als Hash. Betreuungsjahr (src/lib/stammdaten/betreuungsjahr.ts): laeuft ab dem Tag der Bestellung, nicht ab dem Kalenderjahr. Schrittweise vorwaerts gerechnet statt ueber eine Differenz, damit Schaltjahre und der 29. Februar von selbst stimmen. Daran haengen Jahresbericht, Rechnungslegung und die Ausschlussfrist der Aufwandspauschale. Aendert sich das Bestellungsdatum, werden die Pflichtfristen neu abgeleitet - ueber einen dynamischen Import, damit die Stammdaten auch dann speicherbar bleiben, wenn das Fristenmodul fehlt. Protokollansicht zeigt je Eintrag die geaenderten Felder als alt -> neu. Die Aenderungen liegen verschluesselt, weil dort Diagnosen und Kontostaende stehen koennen. Seed legt bewusst keine echten Personendaten an, nur den ersten Zugang. Mit SEED_DEMO=true kommt ein vollstaendiger Beispielfall dazu - Erika Musterfrau, COPD GOLD III, Pflegegrad 3, Heimeinzug Januar 2026, leerstehendes Haus, zwei Versicherungen, laufender Antrag beim Bezirk mit acht Nachforderungen. Nur ausserhalb der Produktion. Docker Compose ohne Reverse Proxy - der ist auf dem Zielserver vorhanden. Die Datenbank hat keine Portfreigabe und ist nur ueber das interne Netz erreichbar; die Anwendung horcht auf 127.0.0.1. Etappe: 1 Phase: Fundament |
||
|
|
bd1c7b801f |
feat: Anmeldung mit Pflicht-2FA, Rollen und Audit-Log
Auth.js mit Zugangsdaten-Provider: E-Mail, Passwort und TOTP-Code werden in einem Formular abgefragt. Ein zweistufiger Ablauf muesste zwischen den Schritten einen Zwischenzustand halten - genau das ist die uebliche Schwachstelle, und am Handy ist ein Formular ohnehin schneller. Anmeldung: - Sperre wird VOR dem Passwortvergleich geprueft, damit ein gesperrter Zugang nicht einmal Argon2-Rechenzeit kostet. - Bei unbekannter Adresse wird gegen einen Dummy-Hash gerechnet und die Antwort auf eine Mindestdauer gestreckt. Sonst verraet die Antwortzeit, welche Adressen existieren. - Wiederherstellungscodes werden nach Gebrauch entfernt. - Veraltete Argon2-Parameter werden beim erfolgreichen Login nachgezogen. Rate-Limiting in der Datenbank statt Redis - ein Server, wenige Benutzer, kein Grund fuer eine weitere Komponente. Gezaehlt wird nach E-Mail und nach Anschluss, damit weder viele Anschluesse noch viele Adressen helfen. Zugriffsschutz (sitzung.ts): - Die Sitzung allein entscheidet nichts. Bei jedem Aufruf wird der Benutzer frisch geladen und die Sitzungsgeneration verglichen - so lassen sich alle Anmeldungen auf einen Schlag beenden. - erfordereZugang() prueft immer gegen die betreuteId, nie nur im UI. - Erneute Bestaetigung vor sensiblen Aktionen ueber ein signiertes, kurzlebiges Cookie statt ueber Serverzustand. Audit-Log: aenderungErmitteln() protokolliert nur die tatsaechlich geaenderten Felder; Passwoerter und TOTP-Geheimnisse werden ersetzt, nie gespeichert. Ein Fehler im Protokoll bricht die Fachaktion nicht ab - ausser die Aktion laeuft in einer Transaktion, dann faellt beides. CSP mit Nonce pro Anfrage in der Middleware: keine Fremdquellen, kein CDN, keine Fonts von aussen. Fachseiten werden nicht zwischengespeichert. Etappe: 1 Phase: Fundament |