4d826ef55f04c575ecaf3b8ba28f121b409b9617
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4d826ef55f |
Dokumente mitsichern - eine Akte ohne ihre Dateien ist wertlos
Der Sicherungsdienst machte taeglich ein pg_dump und sonst nichts. Nach einem Plattenschaden waere die Akte vollstaendig wiederhergestellt worden - mit allen Eintraegen, Verweisen und Pruefsummen, aber ohne eine einzige Datei dahinter. Die Pruefsumme in der Akte haette dann ein Dokument belegt, das es nicht mehr gibt. Gegenueber dem Betreuungsgericht ist das der schlechteste denkbare Zustand. Je Lauf entstehen jetzt zwei verschluesselte Dateien mit demselben Zeitstempel: der Datenbankstand und ein Archiv des Ablageverzeichnisses. Beide mit Pruefsumme, beide in der Aufbewahrung, beide im Monatsstand, beide in der Selbstpruefung. Schlaegt der Dokumententeil fehl, bricht der Lauf ab und raeumt den angefangenen Datenbankstand wieder weg. Eine Sicherung, die nur die halbe Wahrheit enthaelt, aber vollstaendig aussieht, ist schlimmer als gar keine: Man verlaesst sich darauf und merkt es erst im Ernstfall. ## Abweichung von der urspruenglichen Idee Im Issue stand "mc mirror statt rohem Verzeichnis". Beim Umsetzen zeigte sich, dass der MinIO-Client in diesem Behaelter fehlt und bei jedem Lauf aus dem Netz geholt werden muesste. Eine Internetabhaengigkeit im Sicherungspfad ist genau das, was man dort nicht will. Stattdessen wird das Verzeichnis direkt eingepackt. Das ist vertretbar, weil MinIO ein Objekt erst vollstaendig unter .minio.sys/tmp schreibt und dann an seinen Platz umbenennt: Ein laufender Upload liegt nie halbfertig im Archiv, er fehlt hoechstens. Derselbe Weg deckt den Dateitreiber mit ab. ## Nur lesend Der Sicherungsdienst bekommt das Ablageverzeichnis mit ":ro" eingehaengt. Er laeuft unbeaufsichtigt per cron; ein Fehler im Skript oder ein kompromittierter Behaelter wuerde sonst Sicherung und Bestand auf einmal treffen - genau der Fall, gegen den eine Sicherung da ist. Deshalb schreibt die Wiederherstellung die Dateien auch nicht selbst an ihren Platz, sondern packt sie nach ./backups aus und gibt den letzten Handgriff als Einzeiler aus. In ein laufendes MinIO hineinzuschreiben wuerde dessen Datenbestand ohnehin beschaedigen. ## Geprueft Die Kernlogik einmal von Hand durchgespielt: packen, verschluesseln, Selbstpruefung, zurueckholen - der Inhalt kam bitgleich zurueck, der Klartext war im Archiv nicht auffindbar, und die Selbstpruefung schlug sowohl bei einem beschaedigten Archiv als auch bei falschem Passwort an. Dazu elf Pruefungen in tests/skripte.test.ts gegen vier Mutationen: Dokumententeil entfernt, Abbruch entfernt, Aufraeumen halbiert, Schreibrecht erteilt - jede wird rot. Refs #41 |
||
|
|
42eb242767 |
feat: Update-Skript, das die vergessenen Schritte mitnimmt
Bisher war Aktualisieren eine Folge von Befehlen aus der Anleitung, und zwei davon vergisst man zuverlaessig: die Sicherung vorher und die beiden SQL-Skripte hinterher. Genau die hat es hier schon gekostet. ./scripts/update.sh macht das in einem Durchgang - mit zwei Eigenschaften, auf die es ankommt: Bis zur Rueckfrage wird nichts veraendert. Der erste Entwurf hat die eigenen Aenderungen beiseite gelegt und danach am Serverzugriff abgebrochen - Halbzustand, aus dem man erst wieder herausfinden muss. Jetzt laufen Pruefung, Abgleich und Changelog-Anzeige vollstaendig durch, bevor irgendetwas angefasst wird. Eigene Aenderungen an versionierten Dateien blockieren nicht. Im Betrieb passt man die docker-compose.yml an, bis etwas laeuft; genau diese Anpassungen sind spaeter oft schon eingearbeitet. Das Skript legt sie mit git stash beiseite - rueckholbar, nicht weggeworfen - und sagt, wie man sie zurueckbekommt. Wer sie wirklich loswerden will, nimmt --verwerfen. Die .env bleibt immer unangetastet. Schlaegt die Sicherung fehl, wird nicht aktualisiert. Schlaegt der Bau fehl, nennt das Skript den Weg zurueck. Nebenbei behoben: Der Port der MinIO-Konsole stand fest auf 9001. Lief dort schon etwas, startete der ganze Stapel nicht. Jetzt ueber MINIO_KONSOLE_PORT einstellbar - dieselbe Moeglichkeit, die APP_PORT schon hatte. Aufgefallen beim Aufsetzen einer Testumgebung. Etappe: 8 Phase: Haertung |
||
|
|
232543e2aa |
fix: Leere Schluesselvariable legte die Verschluesselung lahm
docker-compose.yml reicht ENC_KEY_V2 immer an den Behaelter durch - mit leerem Wert, wenn in der .env keiner steht. src/lib/env.ts erkannte Schluessel aber am Variablennamen statt am Inhalt, fand also ein leeres ENC_KEY_V2 und brach ab mit "Umgebungsvariable ENC_KEY_V2 fehlt". Die Meldung war doppelt irrefuehrend: Sie nannte einen Schluessel, den niemand wollte, und sie erschien an einer Stelle, die damit nichts zu tun hat - beim Einrichten des zweiten Faktors, als Serverfehler ohne Text. Betroffen war jede Stelle, die verschluesselte Felder schreibt oder liest, also praktisch alles ausser der Anmeldung selbst. Die kam durch, weil sie ohne Verschluesselung auskommt - der Fehler trat deshalb erst auf der naechsten Seite auf, und der Zusammenhang war nicht zu sehen. Leere Schluesselvariablen werden jetzt uebersprungen. Sieben Tests decken die Auswertung ab, darunter genau diese Konstellation. Die Meldung beim Schluesselwechsel nennt jetzt ausserdem, welche Versionen vorhanden sind, und die richtige Reihenfolge: erst den neuen Schluessel eintragen, dann ENC_KEY_AKTIV umstellen. Dazu scripts/anmeldung-pruefen.mjs. Das Anmeldeformular sagt bewusst nicht, woran eine Anmeldung scheiterte - sonst liesse sich daran ablesen, welche Adressen es gibt. Auf dem Server darf man es wissen: Jeder Versuch wird mit Grund protokolliert, und das Skript liest ihn vor, zusammen mit dem Zustand des Zugangs, dem Stand der Sperre und der AUTH_URL. Etappe: 8 Phase: Haertung |
||
|
|
c35fc16133 |
docs: Installationsanleitung fuer vorhandenes Docker und externen Proxy
Die Anleitung setzt jetzt voraus, was ohnehin da ist: ein Server mit laufendem Docker und ein Reverse Proxy, der schon steht - auch auf einem anderen Rechner. Docker-Installation und Betriebssystemhaertung sind raus, dafuer ist der Proxy-Abschnitt der ausfuehrlichste geworden. Damit ein externer Proxy die Anwendung ueberhaupt erreicht, war eine Aenderung noetig: Die Bindeadresse stand fest auf 127.0.0.1. Jetzt gibt es APP_BIND, Standard bleibt die Loopback-Adresse. Der Weg dorthin ist nach Vorzug sortiert - privates Netz, dann WireGuard, dann als Notloesung oeffentliche Adresse mit Firewallregel. Zwischen Proxy und Anwendung laeuft unverschluesseltes HTTP; das gehoert nicht ins offene Netz. Deshalb steht in der Kontrolle eine Gegenprobe, die fehlschlagen MUSS: Antwortet der Anwendungsport von aussen, ist die Firewallregel falsch. Fertige Konfigurationen fuer Caddy, nginx und Traefik, jeweils mit den Werten, die sonst uebersehen werden: - Zeitgrenze 10 Minuten - sonst brechen Belegmappe, Rechnungslegung und Vollexport mitten im Erzeugen ab. Die Standardwerte von nginx liegen bei 60 Sekunden. - Antwort nicht puffern - der Vollexport kommt als Strom. Puffert der Proxy, wartet der Browser, bis alles im Speicher des Proxys liegt, und laeuft dabei in die Zeitgrenze. - Und der Hinweis, dass der Proxy KEINE eigene CSP setzen darf: Sie enthaelt eine Nonce, die pro Anfrage neu ist. Eine feste CSP blockiert die eigenen Skripte - die Seite laedt dann, laesst sich aber nicht bedienen. Der A-Record zeigt auf den Proxy-Server, nicht auf den Anwendungsserver. Stand vorher falsch da. Etappe: 8 Phase: Haertung |
||
|
|
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 |
||
|
|
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 |