Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9603917a86 | ||
|
|
42eb242767 | ||
|
|
7780739a1a | ||
|
|
9365645f72 |
+27
-7
@@ -19,18 +19,38 @@ AUTH_URL=http://localhost:3000
|
|||||||
# Hinter einem Reverse Proxy auf true setzen.
|
# Hinter einem Reverse Proxy auf true setzen.
|
||||||
AUTH_TRUST_HOST=true
|
AUTH_TRUST_HOST=true
|
||||||
|
|
||||||
# --- Erreichbarkeit ---------------------------------------------------------
|
# --- Erreichbarkeit: HIER MUSST DU DICH ENTSCHEIDEN -------------------------
|
||||||
# Auf welcher Adresse und welchem Port horcht die Anwendung?
|
|
||||||
#
|
#
|
||||||
# Proxy auf DEMSELBEN Server: 127.0.0.1 (Standard, nichts zu tun)
|
# Auf welcher Adresse soll die Anwendung erreichbar sein? Das haengt davon
|
||||||
# Proxy auf einem ANDEREN: die Adresse des privaten Netzes bzw. des
|
# ab, WO dein Reverse Proxy laeuft. Waehl genau eine Zeile aus:
|
||||||
# WireGuard-Tunnels, z.B. 10.0.0.5
|
|
||||||
#
|
#
|
||||||
# 0.0.0.0 nur, wenn eine Firewall den Port ausschliesslich fuer die IP des
|
# Proxy auf DEMSELBEN Server
|
||||||
# Proxy-Servers oeffnet. Die Anwendung beendet kein TLS.
|
# APP_BIND=127.0.0.1
|
||||||
|
# Von aussen kommt niemand heran, nur der Proxy nebenan. Sicherste
|
||||||
|
# Variante - aber ein Proxy auf einem anderen Rechner erreicht die
|
||||||
|
# Anwendung damit NICHT.
|
||||||
|
#
|
||||||
|
# Proxy auf einem ANDEREN Server, privates Netz oder WireGuard vorhanden
|
||||||
|
# APP_BIND=10.0.0.5 <- die Adresse DIESES Servers in dem Netz
|
||||||
|
# Erste Wahl, wenn der Proxy woanders steht.
|
||||||
|
#
|
||||||
|
# Proxy auf einem ANDEREN Server, kein privates Netz
|
||||||
|
# APP_BIND=0.0.0.0
|
||||||
|
# Nur zusammen mit einer Firewallregel, die den Port ausschliesslich
|
||||||
|
# fuer die IP des Proxy-Servers oeffnet:
|
||||||
|
# ufw allow from <IP-des-Proxys> to any port 3000 proto tcp
|
||||||
|
# ufw deny 3000/tcp
|
||||||
|
#
|
||||||
|
# Zwischen Proxy und Anwendung laeuft unverschluesseltes HTTP - die
|
||||||
|
# Anwendung beendet kein TLS. Dieser Weg darf nie durchs offene Netz gehen.
|
||||||
|
# Einzelheiten: docs/INSTALLATION.md, Schritt 7.
|
||||||
APP_BIND=127.0.0.1
|
APP_BIND=127.0.0.1
|
||||||
APP_PORT=3000
|
APP_PORT=3000
|
||||||
|
|
||||||
|
# Die Konsole des mitgelieferten Objektspeichers. Nur noetig, wenn auf dem
|
||||||
|
# Server schon etwas auf 9001 laeuft.
|
||||||
|
MINIO_KONSOLE_PORT=9001
|
||||||
|
|
||||||
# --- Anwendungsseitige Feldverschluesselung ---------------------------------
|
# --- Anwendungsseitige Feldverschluesselung ---------------------------------
|
||||||
# AES-256-GCM. 32 Bytes, base64. OHNE DIESEN SCHLUESSEL SIND DIE VERSCHLUESSELTEN
|
# AES-256-GCM. 32 Bytes, base64. OHNE DIESEN SCHLUESSEL SIND DIE VERSCHLUESSELTEN
|
||||||
# FELDER UNWIEDERBRINGLICH VERLOREN - unbedingt getrennt vom Backup sichern!
|
# FELDER UNWIEDERBRINGLICH VERLOREN - unbedingt getrennt vom Backup sichern!
|
||||||
|
|||||||
+132
-1
@@ -26,6 +26,134 @@ Wiederherstellung*.
|
|||||||
|
|
||||||
_Noch nichts._
|
_Noch nichts._
|
||||||
|
|
||||||
|
## [1.0.5] – 2026-08-22
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- **`scripts/update.sh`** — aktualisiert die Anwendung mit einem Befehl.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/update.sh # auf die neueste Fassung
|
||||||
|
./scripts/update.sh --pruefen # nur nachsehen, nichts ändern
|
||||||
|
./scripts/update.sh v1.2.0 # auf eine bestimmte Fassung
|
||||||
|
```
|
||||||
|
|
||||||
|
Es nimmt die Schritte ab, die man von Hand vergisst: Sicherung
|
||||||
|
anstoßen und bei Misserfolg **abbrechen**, den Changelog zwischen altem
|
||||||
|
und neuem Stand anzeigen, umschalten, neu bauen, warten bis die
|
||||||
|
Anwendung antwortet, **die beiden SQL-Skripte ausführen** und zum
|
||||||
|
Schluss prüfen, ob wirklich die neue Fassung läuft.
|
||||||
|
|
||||||
|
Zwei Eigenschaften, die dabei zählen:
|
||||||
|
|
||||||
|
**Bis zur Rückfrage wird nichts verändert.** Bricht etwas vorher ab —
|
||||||
|
Server nicht erreichbar, falsche Versionsangabe — findest du alles so
|
||||||
|
vor wie zuvor. Kein Halbzustand.
|
||||||
|
|
||||||
|
**Eigene Änderungen an versionierten Dateien blockieren nicht.** Im
|
||||||
|
Betrieb passt man die `docker-compose.yml` an, bis etwas läuft; genau
|
||||||
|
diese Anpassungen sind später oft schon eingearbeitet. Das Skript legt
|
||||||
|
sie vor dem Umschalten mit `git stash` beiseite und sagt, wie man sie
|
||||||
|
zurückholt. Mit `--verwerfen` werden sie stattdessen weggeworfen. Die
|
||||||
|
`.env` bleibt immer unangetastet — sie ist nicht versioniert.
|
||||||
|
|
||||||
|
Schlägt der Bau fehl, nennt das Skript den Weg zurück auf den
|
||||||
|
vorherigen Stand.
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
|
||||||
|
- **Der Port der MinIO-Konsole ließ sich nicht ändern.** Er stand fest auf
|
||||||
|
`9001`; lief dort schon etwas, startete der ganze Stapel nicht
|
||||||
|
(`port is already allocated`). Jetzt über `MINIO_KONSOLE_PORT` in der
|
||||||
|
`.env` einstellbar — dieselbe Möglichkeit, die `APP_PORT` schon hatte.
|
||||||
|
|
||||||
|
## [1.0.4] – 2026-08-22
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
|
||||||
|
- **Die Ersteinrichtung ließ sich nicht abschließen.** Schritt 3 meldete
|
||||||
|
„Bitte prüfe die markierten Felder" — markierte aber keines. Man kam an
|
||||||
|
dieser Stelle nicht weiter.
|
||||||
|
|
||||||
|
Ursache: Das kurze Formular der Ersteinrichtung schickt neun Felder, das
|
||||||
|
vollständige unter *Stammdaten* alle. Beide gehen durch dasselbe Schema,
|
||||||
|
und die optionalen Felder waren zwar `nullable`, aber nicht `optional` —
|
||||||
|
ein Feld, das gar nicht abgeschickt wurde, ist `undefined`, nicht
|
||||||
|
`null`. Beanstandet wurden also zwei Dutzend Felder, die auf dem
|
||||||
|
Bildschirm nie standen.
|
||||||
|
|
||||||
|
- **Eine Meldung ohne zugehöriges Feld kann nicht mehr unsichtbar
|
||||||
|
bleiben.** Beanstandet die Prüfung ein Feld, das das Formular gar nicht
|
||||||
|
gesendet hat, sagt die Anwendung das jetzt ausdrücklich und schreibt es
|
||||||
|
ins Serverlog — statt es als Eingabefehler auszugeben, den niemand
|
||||||
|
finden kann. Das ist ein Fehler im Programm, und er soll auch so
|
||||||
|
aussehen.
|
||||||
|
|
||||||
|
- **Eine betreute Person ließ sich nicht endgültig löschen.** Beim Löschen
|
||||||
|
setzt Postgres den Verweis im Audit-Log auf NULL (`ON DELETE SET NULL`),
|
||||||
|
und genau dieses UPDATE hat der Unveränderlichkeits-Trigger abgewiesen.
|
||||||
|
Betroffen war damit auch das Leeren des Papierkorbs nach 30 Tagen und
|
||||||
|
das Entfernen des Demofalls.
|
||||||
|
|
||||||
|
Der Trigger erlaubt jetzt genau eine Ausnahme: das Nullen der beiden
|
||||||
|
Fremdschlüssel `betreuteId` und `userId`. Jede andere Änderung bleibt
|
||||||
|
verboten, auch das Setzen eines Fremdschlüssels auf einen **anderen**
|
||||||
|
Wert. Der Nachweis bleibt vollständig — Zeitpunkt, Aktion, Bezeichnung,
|
||||||
|
die verschlüsselte Änderung und die Momentaufnahmen von Name und
|
||||||
|
E-Mail sind unberührt. Es verschwindet nur der Verweis auf einen
|
||||||
|
Datensatz, den es nicht mehr gibt.
|
||||||
|
|
||||||
|
**Beim Update nötig:** das SQL-Skript erneut ausführen, sonst bleibt der
|
||||||
|
alte Trigger stehen.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec app node node_modules/prisma/build/index.js db execute --schema prisma/schema.prisma --file prisma/sql/audit-schutz.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geprüft
|
||||||
|
|
||||||
|
- Sieben neue Tests (`tests/stammdaten.test.ts`) prüfen genau die
|
||||||
|
Formularinhalte der Ersteinrichtung, dazu das vollständige Formular mit
|
||||||
|
leeren Feldern und die weiterhin verlangten Pflichtangaben.
|
||||||
|
- Der Ablauf ist zusätzlich im Browser durchgespielt worden: Anmeldung,
|
||||||
|
zweiter Faktor, Schritt 3, Landung auf dem Dashboard — und die daraus
|
||||||
|
abgeleiteten Pflichtfristen samt Ausschlussfrist für die
|
||||||
|
Aufwandspauschale.
|
||||||
|
- Der geänderte Trigger ist gegen eine laufende Datenbank geprüft:
|
||||||
|
Löschen gelingt, Fälschen und Löschen von Einträgen scheitert weiterhin.
|
||||||
|
|
||||||
|
## [1.0.3] – 2026-08-22
|
||||||
|
|
||||||
|
Keine Änderung am Programm — nur an der Konfigurationsvorlage und der
|
||||||
|
Anleitung. Ein Neubau des Abbilds ist nicht nötig.
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
|
||||||
|
- **`.env.example` band die Anwendung stillschweigend an `127.0.0.1`.**
|
||||||
|
Der Wert stand dort als fertig ausgefüllte Zeile zwischen lauter anderen
|
||||||
|
fertigen Zeilen. Wer die Datei kopiert hat und seinen Reverse Proxy auf
|
||||||
|
einem **anderen** Server betreibt, kam nicht an die Anwendung heran —
|
||||||
|
und der Fehler sah aus, als läge es am Proxy.
|
||||||
|
|
||||||
|
Jetzt steht dort eine Entscheidung mit drei benannten Fällen statt eines
|
||||||
|
Vorgabewerts. Der sichere Standard bleibt `127.0.0.1`; er wird nur nicht
|
||||||
|
mehr als „schon erledigt" präsentiert.
|
||||||
|
|
||||||
|
Für bestehende Installationen genügt eine Zeile in der `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APP_BIND=10.0.0.5 # Adresse, über die der Proxy diesen Server erreicht
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
|
||||||
|
- **`docs/INSTALLATION.md`** nennt `APP_BIND` jetzt schon in Schritt 3, wo
|
||||||
|
die `.env` ausgefüllt wird — nicht erst in Schritt 7, wo der Proxy
|
||||||
|
drankommt. Dazu ein eigener Abschnitt in der Fehlersuche: „Der Proxy
|
||||||
|
erreicht die Anwendung nicht", mit den zwei `curl`-Proben, die den
|
||||||
|
Unterschied zwischen falscher Bindeadresse und Firewall zeigen.
|
||||||
|
|
||||||
## [1.0.2] – 2026-08-22
|
## [1.0.2] – 2026-08-22
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
@@ -264,7 +392,10 @@ Was davon wann angegangen wird, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.2...HEAD
|
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.5...HEAD
|
||||||
|
[1.0.5]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.4...v1.0.5
|
||||||
|
[1.0.4]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.3...v1.0.4
|
||||||
|
[1.0.3]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.2...v1.0.3
|
||||||
[1.0.2]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.1...v1.0.2
|
[1.0.2]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.1...v1.0.2
|
||||||
[1.0.1]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.0...v1.0.1
|
[1.0.1]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.0...v1.0.1
|
||||||
[1.0.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/releases/tag/v1.0.0
|
[1.0.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/releases/tag/v1.0.0
|
||||||
|
|||||||
@@ -177,12 +177,18 @@ existiert.
|
|||||||
### Aktualisieren
|
### Aktualisieren
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull
|
./scripts/update.sh
|
||||||
docker compose up -d --build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Die Migrationen laufen beim Start des Anwendungsbehälters mit. **Vorher ein
|
Das Skript sichert zuerst, zeigt den Changelog zwischen altem und neuem
|
||||||
Backup anstoßen** (siehe unten) — eine Migration lässt sich nicht zurückrollen.
|
Stand, fragt nach, schaltet um, baut neu und führt danach die beiden
|
||||||
|
SQL-Skripte aus. Schlägt die Sicherung fehl, wird nicht aktualisiert.
|
||||||
|
|
||||||
|
`--pruefen` sieht nur nach, ohne etwas zu ändern. Eigene Änderungen an
|
||||||
|
versionierten Dateien werden vor dem Umschalten beiseite gelegt
|
||||||
|
(`git stash`), die `.env` bleibt unangetastet.
|
||||||
|
|
||||||
|
Einzelheiten und der Weg von Hand: [`docs/INSTALLATION.md`](docs/INSTALLATION.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -66,7 +66,10 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
# Nur an die Loopback-Adresse gebunden. Die Konsole erreichst du
|
# Nur an die Loopback-Adresse gebunden. Die Konsole erreichst du
|
||||||
# ueber einen SSH-Tunnel oder ueber deinen vorhandenen Proxy.
|
# ueber einen SSH-Tunnel oder ueber deinen vorhandenen Proxy.
|
||||||
- '127.0.0.1:9001:9001'
|
#
|
||||||
|
# Laeuft auf dem Server schon etwas auf 9001, weich in der .env aus:
|
||||||
|
# MINIO_KONSOLE_PORT=9011
|
||||||
|
- '${MINIO_KONSOLE_BIND:-127.0.0.1}:${MINIO_KONSOLE_PORT:-9001}:9001'
|
||||||
networks: [intern]
|
networks: [intern]
|
||||||
|
|
||||||
app:
|
app:
|
||||||
@@ -93,7 +96,7 @@ services:
|
|||||||
ENC_KEY_V1: ${ENC_KEY_V1:?ENC_KEY_V1 fehlt in der .env - ohne ihn sind die verschluesselten Felder nicht lesbar}
|
ENC_KEY_V1: ${ENC_KEY_V1:?ENC_KEY_V1 fehlt in der .env - ohne ihn sind die verschluesselten Felder nicht lesbar}
|
||||||
# Bleibt leer, solange nicht rotiert wird. Die Anwendung ueberspringt
|
# Bleibt leer, solange nicht rotiert wird. Die Anwendung ueberspringt
|
||||||
# leere Schluesselvariablen - siehe src/lib/env.ts.
|
# leere Schluesselvariablen - siehe src/lib/env.ts.
|
||||||
ENC_KEY_V2: ${ENC_KEY_V2:-}
|
# ENC_KEY_V2: ${ENC_KEY_V2:-}
|
||||||
ENC_KEY_AKTIV: ${ENC_KEY_AKTIV:-1}
|
ENC_KEY_AKTIV: ${ENC_KEY_AKTIV:-1}
|
||||||
INDEX_KEY: ${INDEX_KEY:?INDEX_KEY fehlt in der .env}
|
INDEX_KEY: ${INDEX_KEY:?INDEX_KEY fehlt in der .env}
|
||||||
SESSION_MAX_AGE: ${SESSION_MAX_AGE:-1800}
|
SESSION_MAX_AGE: ${SESSION_MAX_AGE:-1800}
|
||||||
|
|||||||
+119
-9
@@ -205,6 +205,14 @@ S3_SECRET_ACCESS_KEY=<aus Schritt 2>
|
|||||||
|
|
||||||
# --- Sicherung ---
|
# --- Sicherung ---
|
||||||
SICHERUNG_PASSWORT=<aus Schritt 2>
|
SICHERUNG_PASSWORT=<aus Schritt 2>
|
||||||
|
|
||||||
|
# --- Erreichbarkeit ---
|
||||||
|
# Steht dein Proxy auf einem ANDEREN Server, muss hier die Adresse hinein,
|
||||||
|
# ueber die er diesen Server erreicht. Bleibt es bei 127.0.0.1, kommt er
|
||||||
|
# NICHT heran - und der Fehler sieht aus, als sei der Proxy schuld.
|
||||||
|
# Welcher Wert der richtige ist, steht in Schritt 7a.
|
||||||
|
APP_BIND=127.0.0.1
|
||||||
|
APP_PORT=3000
|
||||||
```
|
```
|
||||||
|
|
||||||
> **`AUTH_URL` muss exakt der Adresse entsprechen**, unter der du die
|
> **`AUTH_URL` muss exakt der Adresse entsprechen**, unter der du die
|
||||||
@@ -824,27 +832,80 @@ Denk an die letzten Pflichten:
|
|||||||
|
|
||||||
## Aktualisieren
|
## Aktualisieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/betreuungs-cockpit
|
||||||
|
./scripts/update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Das war es. Das Skript nimmt dir die Schritte ab, die man von Hand
|
||||||
|
vergisst:
|
||||||
|
|
||||||
|
1. Prüft Verzeichnis, git, docker und `.env`
|
||||||
|
2. Holt die Fassungen und zeigt, was zwischen deinem Stand und dem neuen
|
||||||
|
im Changelog steht
|
||||||
|
3. Fragt nach
|
||||||
|
4. **Stößt eine Sicherung an** — und bricht ab, wenn sie nicht klappt
|
||||||
|
5. Legt eigene Änderungen an versionierten Dateien beiseite, schaltet um,
|
||||||
|
baut neu
|
||||||
|
6. Wartet, bis die Anwendung antwortet
|
||||||
|
7. Führt die **beiden SQL-Skripte** aus (Volltextsuche, Audit-Schutz)
|
||||||
|
8. Prüft, ob wirklich die neue Fassung läuft
|
||||||
|
|
||||||
|
**Bis Schritt 3 wird nichts verändert.** Bricht etwas vorher ab, findest du
|
||||||
|
alles so vor wie zuvor.
|
||||||
|
|
||||||
|
### Aufrufmöglichkeiten
|
||||||
|
|
||||||
|
| Aufruf | Wirkung |
|
||||||
|
|---|---|
|
||||||
|
| `./scripts/update.sh` | auf die neueste veröffentlichte Fassung |
|
||||||
|
| `./scripts/update.sh v1.2.0` | auf eine bestimmte Fassung |
|
||||||
|
| `./scripts/update.sh --pruefen` | nur nachsehen, nichts ändern |
|
||||||
|
| `./scripts/update.sh --ja` | ohne Rückfrage (für cron) |
|
||||||
|
| `./scripts/update.sh --verwerfen` | eigene Änderungen wegwerfen statt beiseitelegen |
|
||||||
|
| `./scripts/update.sh --ohne-sicherung` | Sicherung überspringen — sehr gut überlegen |
|
||||||
|
|
||||||
|
### Eigene Änderungen an der docker-compose.yml
|
||||||
|
|
||||||
|
Das ist der Normalfall: Man passt etwas an, bis es läuft. Das Skript legt
|
||||||
|
solche Änderungen vor dem Umschalten mit `git stash` beiseite und sagt,
|
||||||
|
wie man sie zurückholt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git stash list # was liegt da
|
||||||
|
git stash pop # zurückholen
|
||||||
|
git stash drop # wegwerfen
|
||||||
|
```
|
||||||
|
|
||||||
|
Sind deine Anpassungen inzwischen im Projekt eingearbeitet — wie
|
||||||
|
`APP_BIND` oder `MINIO_KONSOLE_PORT` —, brauchst du sie nicht
|
||||||
|
zurückzuholen. Dann setz die Werte einfach in der `.env`.
|
||||||
|
|
||||||
|
> **Die `.env` wird nie angefasst.** Sie ist nicht versioniert; weder
|
||||||
|
> `git checkout` noch das Skript rühren sie an.
|
||||||
|
|
||||||
|
### Von Hand, wenn du lieber selbst steuerst
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/betreuungs-cockpit
|
cd /opt/betreuungs-cockpit
|
||||||
|
|
||||||
# 1. IMMER zuerst sichern - eine Migration lässt sich nicht zurückrollen
|
# 1. IMMER zuerst sichern - eine Migration lässt sich nicht zurückrollen
|
||||||
docker compose exec sicherung /bin/sh /sicherung.sh
|
docker compose exec sicherung /bin/sh /sicherung.sh
|
||||||
|
|
||||||
# 2. Was ändert sich? Lies den Changelog, bevor du ziehst
|
# 2. Was ändert sich?
|
||||||
git fetch --tags
|
git fetch --tags
|
||||||
git log --oneline HEAD..origin/main
|
git log --oneline HEAD..origin/main
|
||||||
|
|
||||||
# 3. Auf die neue Fassung wechseln
|
# 3. Umschalten
|
||||||
git checkout v1.1.0
|
git checkout v1.1.0
|
||||||
|
|
||||||
# 4. Neu bauen und starten
|
# 4. Neu bauen und starten
|
||||||
GIT_COMMIT=$(git rev-parse --short HEAD) \
|
GIT_COMMIT=$(git rev-parse --short HEAD) BUILD_ZEIT=$(date -Iseconds) docker compose up -d --build
|
||||||
BUILD_ZEIT=$(date -Iseconds) \
|
|
||||||
docker compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
Die Migrationen laufen beim Start mit. **Sagt der Changelog, dass sich ein
|
# 5. Die beiden SQL-Skripte - sie laufen NICHT mit den Migrationen mit
|
||||||
SQL-Skript geändert hat, führ die beiden Befehle aus Schritt 5 erneut aus.**
|
docker compose exec app node node_modules/prisma/build/index.js db execute --schema prisma/schema.prisma --file prisma/sql/volltextsuche.sql
|
||||||
|
docker compose exec app node node_modules/prisma/build/index.js db execute --schema prisma/schema.prisma --file prisma/sql/audit-schutz.sql
|
||||||
|
```
|
||||||
|
|
||||||
### Kontrolle
|
### Kontrolle
|
||||||
|
|
||||||
@@ -854,7 +915,21 @@ curl -s https://cockpit.deine-domain.de/api/gesundheit
|
|||||||
```
|
```
|
||||||
|
|
||||||
Steht dort noch die alte Version, läuft der alte Behälter weiter — dann
|
Steht dort noch die alte Version, läuft der alte Behälter weiter — dann
|
||||||
hat der Bau nicht durchgezogen. Sieh in `docker compose logs app`.
|
hat der Bau nicht durchgezogen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build --force-recreate app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zurück auf den vorherigen Stand
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout v1.0.4
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Hat die neue Fassung die Datenbank migriert, reicht das nicht — dann muss
|
||||||
|
auch die Sicherung zurück (siehe Schritt 9).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -875,6 +950,41 @@ Fehlt eine Umgebungsvariable, sagt die Anwendung beim Start, welche.
|
|||||||
Die `.env` liegt nicht neben der `docker-compose.yml`, oder die Zeile ist
|
Die `.env` liegt nicht neben der `docker-compose.yml`, oder die Zeile ist
|
||||||
leer. Prüf mit dem Befehl aus Schritt 3.
|
leer. Prüf mit dem Befehl aus Schritt 3.
|
||||||
|
|
||||||
|
### Der Proxy erreicht die Anwendung nicht („502", „Connection refused")
|
||||||
|
|
||||||
|
Fast immer steht in der `.env` noch `APP_BIND=127.0.0.1`, während der
|
||||||
|
Proxy auf einem anderen Server läuft. Dann horcht die Anwendung nur auf
|
||||||
|
der Loopback-Adresse — erreichbar ausschließlich vom Anwendungsserver
|
||||||
|
selbst.
|
||||||
|
|
||||||
|
Prüf, worauf sie tatsächlich horcht:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
# unter PORTS muss die Adresse stehen, die der Proxy erreicht,
|
||||||
|
# NICHT 127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Und ob sie überhaupt antwortet — erst lokal, dann vom Proxy-Server aus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# auf dem Anwendungsserver
|
||||||
|
curl -s http://127.0.0.1:3000/api/gesundheit
|
||||||
|
|
||||||
|
# auf dem Proxy-Server
|
||||||
|
curl -s http://<APP_BIND-Adresse>:3000/api/gesundheit
|
||||||
|
```
|
||||||
|
|
||||||
|
Antwortet der erste, aber nicht der zweite, ist `APP_BIND` falsch oder
|
||||||
|
eine Firewall dazwischen. Setz den richtigen Wert (Schritt 7a) und
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein Neubau des Abbilds ist dafür **nicht** nötig — es ist nur eine
|
||||||
|
Umgebungsänderung, der Behälter wird neu erzeugt.
|
||||||
|
|
||||||
### Port 3000 ist schon belegt
|
### Port 3000 ist schon belegt
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "betreuungs-cockpit",
|
"name": "betreuungs-cockpit",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung",
|
"description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -25,6 +25,42 @@ BEGIN
|
|||||||
RETURN OLD;
|
RETURN OLD;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Eine einzige Ausnahme beim UPDATE: das Nullen der beiden
|
||||||
|
* Fremdschluessel.
|
||||||
|
*
|
||||||
|
* Wird eine betreute Person oder ein Benutzer endgueltig geloescht
|
||||||
|
* (Papierkorb nach 30 Tagen), setzt Postgres wegen ON DELETE SET NULL
|
||||||
|
* die Spalten betreuteId bzw. userId auf NULL. Ohne diese Ausnahme
|
||||||
|
* scheitert das Loeschen - und der Papierkorb liesse sich nie leeren.
|
||||||
|
*
|
||||||
|
* Der Nachweis bleibt dabei vollstaendig: Zeitpunkt, Aktion, Entitaet,
|
||||||
|
* Bezeichnung und die verschluesselte Aenderung sind unberuehrt, ebenso
|
||||||
|
* die Momentaufnahmen userEmail und userName. Es verschwindet nur der
|
||||||
|
* Verweis auf einen Datensatz, den es nicht mehr gibt.
|
||||||
|
*
|
||||||
|
* Jede andere Aenderung wird abgewiesen - auch das Zurueckschreiben
|
||||||
|
* eines Fremdschluessels auf einen anderen Wert.
|
||||||
|
*/
|
||||||
|
IF TG_OP = 'UPDATE'
|
||||||
|
AND NEW.id = OLD.id
|
||||||
|
AND NEW.zeitpunkt = OLD.zeitpunkt
|
||||||
|
AND NEW.aktion = OLD.aktion
|
||||||
|
AND NEW.entitaet = OLD.entitaet
|
||||||
|
AND NEW."entitaetId" IS NOT DISTINCT FROM OLD."entitaetId"
|
||||||
|
AND NEW.bezeichnung IS NOT DISTINCT FROM OLD.bezeichnung
|
||||||
|
AND NEW."userEmail" IS NOT DISTINCT FROM OLD."userEmail"
|
||||||
|
AND NEW."userName" IS NOT DISTINCT FROM OLD."userName"
|
||||||
|
AND NEW."aenderungVerschluesselt" IS NOT DISTINCT FROM OLD."aenderungVerschluesselt"
|
||||||
|
AND NEW."ipHash" IS NOT DISTINCT FROM OLD."ipHash"
|
||||||
|
AND NEW."userAgent" IS NOT DISTINCT FROM OLD."userAgent"
|
||||||
|
-- Die beiden Fremdschluessel duerfen sich aendern, aber NUR nach NULL.
|
||||||
|
AND (NEW."betreuteId" IS NULL OR NEW."betreuteId" IS NOT DISTINCT FROM OLD."betreuteId")
|
||||||
|
AND (NEW."userId" IS NULL OR NEW."userId" IS NOT DISTINCT FROM OLD."userId")
|
||||||
|
THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
RAISE EXCEPTION
|
RAISE EXCEPTION
|
||||||
'Das Audit-Log ist unveraenderlich. % auf audit_log ist nicht zulaessig.',
|
'Das Audit-Log ist unveraenderlich. % auf audit_log ist nicht zulaessig.',
|
||||||
TG_OP
|
TG_OP
|
||||||
|
|||||||
@@ -0,0 +1,385 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Betreuungs-Cockpit aktualisieren.
|
||||||
|
#
|
||||||
|
# ./scripts/update.sh auf die neueste veroeffentlichte Fassung
|
||||||
|
# ./scripts/update.sh v1.2.0 auf eine bestimmte Fassung
|
||||||
|
# ./scripts/update.sh --ja ohne Rueckfrage (fuer cron)
|
||||||
|
# ./scripts/update.sh --pruefen nur nachsehen, nichts aendern
|
||||||
|
# ./scripts/update.sh --verwerfen eigene Aenderungen an versionierten
|
||||||
|
# Dateien wegwerfen statt beiseitelegen
|
||||||
|
#
|
||||||
|
# Was das Skript tut, in dieser Reihenfolge:
|
||||||
|
#
|
||||||
|
# 1. Voraussetzungen pruefen (Verzeichnis, git, docker, .env)
|
||||||
|
# 2. Fassungen holen und vergleichen
|
||||||
|
# 3. Zeigen, was sich aendert (Changelog-Abschnitte dazwischen)
|
||||||
|
# 4. Nachfragen
|
||||||
|
# 5. SICHERUNG anstossen - und abbrechen, wenn sie nicht klappt
|
||||||
|
# 6. Eigene Aenderungen beiseitelegen, umschalten, neu bauen, starten
|
||||||
|
# 7. Die beiden SQL-Skripte ausfuehren (sie sind idempotent)
|
||||||
|
# 8. Pruefen, ob die neue Fassung wirklich laeuft
|
||||||
|
#
|
||||||
|
# Die Reihenfolge ist Absicht: Bis einschliesslich Schritt 4 wird nichts
|
||||||
|
# veraendert. Wer vorher abbricht - oder wobei etwas schiefgeht -, findet
|
||||||
|
# alles so vor wie zuvor. Erst danach wird angefasst.
|
||||||
|
#
|
||||||
|
# Schritt 5 ist der wichtigste: Eine Datenbank-Migration laesst sich nicht
|
||||||
|
# zurueckrollen. Ohne Sicherung wird hier nichts aktualisiert.
|
||||||
|
#
|
||||||
|
# Schritt 7 ebenfalls: Die SQL-Skripte laufen NICHT mit den Migrationen
|
||||||
|
# mit. Wer sie vergisst, hat einen veralteten Trigger und merkt es erst,
|
||||||
|
# wenn etwas nicht geht.
|
||||||
|
#
|
||||||
|
# Die .env wird nie angefasst - sie ist nicht versioniert.
|
||||||
|
|
||||||
|
# Die geschweiften Klammern sorgen dafuer, dass bash die ganze Datei liest,
|
||||||
|
# bevor sie irgendetwas ausfuehrt. Ohne sie koennte der git-checkout weiter
|
||||||
|
# unten das Skript unter den eigenen Fuessen austauschen.
|
||||||
|
{
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Ausgabe -----------------------------------------------------------------
|
||||||
|
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
ROT=$'\033[31m'; GRUEN=$'\033[32m'; GELB=$'\033[33m'; FETT=$'\033[1m'; AUS=$'\033[0m'
|
||||||
|
else
|
||||||
|
ROT=''; GRUEN=''; GELB=''; FETT=''; AUS=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
schritt() { printf '\n%s==> %s%s\n' "$FETT" "$1" "$AUS"; }
|
||||||
|
gut() { printf ' %s%s%s\n' "$GRUEN" "$1" "$AUS"; }
|
||||||
|
warnung() { printf ' %s%s%s\n' "$GELB" "$1" "$AUS"; }
|
||||||
|
abbruch() { printf '\n%sAbbruch: %s%s\n\n' "$ROT" "$1" "$AUS" >&2; exit 1; }
|
||||||
|
|
||||||
|
# --- Aufrufparameter ---------------------------------------------------------
|
||||||
|
|
||||||
|
ZIEL=""
|
||||||
|
OHNE_RUECKFRAGE=0
|
||||||
|
NUR_PRUEFEN=0
|
||||||
|
OHNE_SICHERUNG=0
|
||||||
|
VERWERFEN=0
|
||||||
|
ENTSCHEIDUNG=""
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--ja|-j) OHNE_RUECKFRAGE=1 ;;
|
||||||
|
--pruefen|-p) NUR_PRUEFEN=1 ;;
|
||||||
|
--ohne-sicherung) OHNE_SICHERUNG=1 ;;
|
||||||
|
--verwerfen) VERWERFEN=1 ;;
|
||||||
|
--hilfe|-h)
|
||||||
|
sed -n '2,34p' "$0" | sed 's/^#\s\?//'
|
||||||
|
exit 0 ;;
|
||||||
|
-*) abbruch "Unbekannte Angabe: $arg (--hilfe zeigt die Moeglichkeiten)" ;;
|
||||||
|
*) ZIEL="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- 1. Voraussetzungen ------------------------------------------------------
|
||||||
|
|
||||||
|
schritt "Voraussetzungen"
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.." || abbruch "Das Projektverzeichnis ist nicht erreichbar."
|
||||||
|
PROJEKT="$(pwd)"
|
||||||
|
|
||||||
|
[ -f docker-compose.yml ] || abbruch "Hier liegt keine docker-compose.yml: $PROJEKT"
|
||||||
|
[ -f .env ] || abbruch "Es gibt keine .env. Ohne sie startet die Anwendung nicht."
|
||||||
|
command -v git >/dev/null || abbruch "git ist nicht installiert."
|
||||||
|
docker compose version >/dev/null 2>&1 || abbruch "docker compose (v2) ist nicht verfuegbar."
|
||||||
|
|
||||||
|
gut "Verzeichnis: $PROJEKT"
|
||||||
|
|
||||||
|
# Nur nachsehen - angefasst wird die Arbeitskopie erst kurz vor dem
|
||||||
|
# Umschalten, wenn Sicherung und Rueckfrage durch sind. Ein Abbruch
|
||||||
|
# dazwischen darf keinen Halbzustand hinterlassen.
|
||||||
|
SCHMUTZIG=""
|
||||||
|
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
|
||||||
|
SCHMUTZIG="ja"
|
||||||
|
warnung "Es gibt eigene Aenderungen an versionierten Dateien."
|
||||||
|
git status --short --untracked-files=no | sed 's/^/ /'
|
||||||
|
printf ' Sie werden vor dem Umschalten beiseite gelegt.\n'
|
||||||
|
else
|
||||||
|
gut "Arbeitskopie ist sauber"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 2. Was ist da, was kommt ------------------------------------------------
|
||||||
|
|
||||||
|
schritt "Fassungen"
|
||||||
|
|
||||||
|
if ! git fetch --tags --quiet origin; then
|
||||||
|
abbruch "Konnte die Fassungen nicht vom Server holen.
|
||||||
|
Erreichst du ihn? Stimmen die Zugangsdaten?
|
||||||
|
Es wurde nichts geaendert."
|
||||||
|
fi
|
||||||
|
|
||||||
|
JETZT="$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD)"
|
||||||
|
NEUESTE="$(git tag --list 'v*' --sort=-v:refname | head -1)"
|
||||||
|
|
||||||
|
[ -n "$NEUESTE" ] || abbruch "Es gibt keine veroeffentlichte Fassung (kein Tag)."
|
||||||
|
|
||||||
|
if [ -z "$ZIEL" ]; then
|
||||||
|
ZIEL="$NEUESTE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git rev-parse --verify --quiet "$ZIEL^{commit}" >/dev/null \
|
||||||
|
|| abbruch "Die Fassung '$ZIEL' gibt es nicht. Vorhanden:
|
||||||
|
$(git tag --list 'v*' --sort=-v:refname | head -5 | sed 's/^/ /')"
|
||||||
|
|
||||||
|
printf ' Jetzt: %s\n' "$JETZT"
|
||||||
|
printf ' Ziel: %s%s%s\n' "$FETT" "$ZIEL" "$AUS"
|
||||||
|
|
||||||
|
if [ "$(git rev-parse HEAD)" = "$(git rev-parse "$ZIEL^{commit}")" ]; then
|
||||||
|
gut "Bereits auf dem gewuenschten Stand - nichts zu tun."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 3. Was aendert sich -----------------------------------------------------
|
||||||
|
|
||||||
|
schritt "Was sich aendert"
|
||||||
|
|
||||||
|
# Der Changelog muss aus der ZIELfassung kommen: Der laufende Stand kennt
|
||||||
|
# die neuen Abschnitte ja noch gar nicht.
|
||||||
|
CHANGELOG="$(git show "$ZIEL:CHANGELOG.md" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [ -n "$CHANGELOG" ]; then
|
||||||
|
# Alle Abschnitte zwischen dem laufenden und dem gewuenschten Stand.
|
||||||
|
JETZT_NR="${JETZT#v}"
|
||||||
|
printf '%s
|
||||||
|
' "$CHANGELOG" | awk -v jetzt="$JETZT_NR" '
|
||||||
|
/^## \[/ {
|
||||||
|
nummer = $0
|
||||||
|
sub(/^## \[/, "", nummer); sub(/\].*/, "", nummer)
|
||||||
|
if (nummer == jetzt) exit
|
||||||
|
drucken = (nummer != "Unveröffentlicht")
|
||||||
|
}
|
||||||
|
drucken { print " " $0 }
|
||||||
|
' | head -60
|
||||||
|
else
|
||||||
|
warnung "Kein CHANGELOG.md in $ZIEL gefunden."
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
git log --oneline --no-decorate "HEAD..$ZIEL" | head -15 | sed 's/^/ /'
|
||||||
|
|
||||||
|
if [ "$NUR_PRUEFEN" = "1" ]; then
|
||||||
|
printf '\n'
|
||||||
|
gut "Nur geprueft - es wurde nichts geaendert."
|
||||||
|
[ -n "$SCHMUTZIG" ] && warnung "Beim echten Lauf wuerden deine Aenderungen beiseite gelegt."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 4. Nachfragen -----------------------------------------------------------
|
||||||
|
|
||||||
|
if [ "$OHNE_RUECKFRAGE" != "1" ]; then
|
||||||
|
printf '\n%sJetzt aktualisieren auf %s? [j/N] %s' "$FETT" "$ZIEL" "$AUS"
|
||||||
|
read -r antwort </dev/tty || antwort=""
|
||||||
|
case "$antwort" in
|
||||||
|
[jJ]|[jJ][aA]) ;;
|
||||||
|
*) printf '\n'; warnung "Abgebrochen, nichts geaendert."; exit 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 5. Sicherung ------------------------------------------------------------
|
||||||
|
|
||||||
|
if [ "$OHNE_SICHERUNG" = "1" ]; then
|
||||||
|
schritt "Sicherung wird uebersprungen"
|
||||||
|
warnung "Auf eigene Gefahr - eine Migration laesst sich nicht zurueckrollen."
|
||||||
|
else
|
||||||
|
schritt "Sicherung"
|
||||||
|
|
||||||
|
if docker compose ps --status running --services 2>/dev/null | grep -qx sicherung; then
|
||||||
|
if docker compose exec -T sicherung /bin/sh /sicherung.sh; then
|
||||||
|
gut "Sicherung angelegt und selbst geprueft."
|
||||||
|
else
|
||||||
|
abbruch "Die Sicherung ist fehlgeschlagen. Es wird nichts aktualisiert.
|
||||||
|
Mit --ohne-sicherung laesst sich das uebergehen - aber ueberleg
|
||||||
|
dir das zweimal."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warnung "Der Dienst 'sicherung' laeuft nicht - es wurde nichts gesichert."
|
||||||
|
if [ "$OHNE_RUECKFRAGE" != "1" ]; then
|
||||||
|
printf '\n %sTrotzdem weitermachen? [j/N] %s' "$FETT" "$AUS"
|
||||||
|
read -r weiter </dev/tty || weiter=""
|
||||||
|
case "$weiter" in [jJ]|[jJ][aA]) ;; *) abbruch "Abgebrochen." ;; esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 6. Umschalten und bauen -------------------------------------------------
|
||||||
|
|
||||||
|
# Aenderungen an versionierten Dateien blockieren das Umschalten.
|
||||||
|
#
|
||||||
|
# Im Betrieb ist das der Normalfall: Man passt die docker-compose.yml an,
|
||||||
|
# bis etwas laeuft. Genau diese Anpassungen sind spaeter oft schon
|
||||||
|
# eingearbeitet - das Skript soll sie also nicht zum Hindernis machen.
|
||||||
|
#
|
||||||
|
# Verworfen wird trotzdem nichts stillschweigend. Standard ist "beiseite
|
||||||
|
# legen": git stash macht es rueckholbar. Wer sie wirklich weghaben will,
|
||||||
|
# nimmt --verwerfen.
|
||||||
|
#
|
||||||
|
# Die .env ist nie betroffen - sie ist nicht versioniert.
|
||||||
|
schritt "Eigene Aenderungen"
|
||||||
|
|
||||||
|
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
|
||||||
|
printf '\n Diese versionierten Dateien sind veraendert:\n\n'
|
||||||
|
git status --short --untracked-files=no | sed 's/^/ /'
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [ "$VERWERFEN" = "1" ]; then
|
||||||
|
ENTSCHEIDUNG="verwerfen"
|
||||||
|
elif [ "$OHNE_RUECKFRAGE" = "1" ]; then
|
||||||
|
# Ohne jemanden zum Fragen: die sichere Variante.
|
||||||
|
ENTSCHEIDUNG="beiseite"
|
||||||
|
else
|
||||||
|
printf ' %s[b]%s eiseite legen (git stash, spaeter rueckholbar)\n' "$FETT" "$AUS"
|
||||||
|
printf ' %s[v]%s erwerfen (endgueltig weg)\n' "$FETT" "$AUS"
|
||||||
|
printf ' %s[a]%s bbrechen\n\n' "$FETT" "$AUS"
|
||||||
|
printf ' Was soll damit geschehen? [b/v/a] '
|
||||||
|
read -r wahl </dev/tty || wahl=""
|
||||||
|
case "$wahl" in
|
||||||
|
[bB]|'') ENTSCHEIDUNG="beiseite" ;;
|
||||||
|
[vV]) ENTSCHEIDUNG="verwerfen" ;;
|
||||||
|
*) abbruch "Abgebrochen, nichts geaendert." ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$ENTSCHEIDUNG" = "verwerfen" ]; then
|
||||||
|
git checkout -- .
|
||||||
|
warnung "Aenderungen verworfen."
|
||||||
|
else
|
||||||
|
STASH_NAME="vor Update $(date +%Y-%m-%d_%H%M)"
|
||||||
|
git stash push --quiet --message "$STASH_NAME" -- . \
|
||||||
|
|| abbruch "Konnte die Aenderungen nicht beiseite legen."
|
||||||
|
gut "Beiseite gelegt als: $STASH_NAME"
|
||||||
|
printf ' Ansehen: git stash list\n'
|
||||||
|
printf ' Zurueck: git stash pop\n'
|
||||||
|
printf ' Wegwerfen: git stash drop\n'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
gut "Arbeitskopie ist sauber"
|
||||||
|
fi
|
||||||
|
|
||||||
|
schritt "Umschalten auf $ZIEL"
|
||||||
|
|
||||||
|
# Eine unversionierte Datei kann im Weg stehen, wenn die Zielfassung sie
|
||||||
|
# fuehrt. Das passiert beim Update auf die Fassung, die dieses Skript
|
||||||
|
# eingefuehrt hat: Man hat es sich von Hand geholt, jetzt gehoert es dazu.
|
||||||
|
# Solche Dateien werden nicht weggeworfen, sondern beiseite geschoben.
|
||||||
|
if ! git checkout --quiet "$ZIEL" 2>/dev/null; then
|
||||||
|
IM_WEG="$(git checkout "$ZIEL" 2>&1 | sed -n '/would be overwritten/,/^Aborting/p' | grep -E '^\s' | tr -d ' ' | grep -v '^$' || true)"
|
||||||
|
|
||||||
|
if [ -n "$IM_WEG" ]; then
|
||||||
|
ZEIT="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
printf ' Diese unversionierten Dateien stehen im Weg:
|
||||||
|
'
|
||||||
|
for datei in $IM_WEG; do
|
||||||
|
[ -e "$datei" ] || continue
|
||||||
|
mv "$datei" "${datei}.vor-update-${ZEIT}"
|
||||||
|
printf ' %s -> %s.vor-update-%s
|
||||||
|
' "$datei" "$datei" "$ZEIT"
|
||||||
|
done
|
||||||
|
git checkout --quiet "$ZIEL" || abbruch "Das Umschalten auf $ZIEL ist fehlgeschlagen.
|
||||||
|
Deine Aenderungen liegen im Stash: git stash list"
|
||||||
|
else
|
||||||
|
abbruch "Das Umschalten auf $ZIEL ist fehlgeschlagen.
|
||||||
|
$([ -n "$SCHMUTZIG" ] && printf ' Deine Aenderungen liegen im Stash: git stash list')"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
gut "$(git describe --tags --always)"
|
||||||
|
|
||||||
|
schritt "Bauen und starten"
|
||||||
|
printf ' Das dauert beim ersten Mal einige Minuten.\n\n'
|
||||||
|
|
||||||
|
if ! GIT_COMMIT="$(git rev-parse --short HEAD)" \
|
||||||
|
BUILD_ZEIT="$(date -Iseconds)" \
|
||||||
|
docker compose up -d --build; then
|
||||||
|
printf '\n'
|
||||||
|
abbruch "Bauen oder Starten ist fehlgeschlagen.
|
||||||
|
Die Quellen stehen jetzt auf $ZIEL, die Anwendung laeuft aber
|
||||||
|
noch mit dem alten Abbild - oder gar nicht.
|
||||||
|
|
||||||
|
Zurueck auf den vorherigen Stand:
|
||||||
|
git checkout $JETZT && docker compose up -d --build
|
||||||
|
$([ -n "$SCHMUTZIG" ] && printf '\n Deine Aenderungen liegen im Stash: git stash list')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 7. Warten, bis die Anwendung antwortet ----------------------------------
|
||||||
|
|
||||||
|
schritt "Warten auf die Anwendung"
|
||||||
|
|
||||||
|
# Adresse und Port stehen in der .env - Standard ist die Loopback-Adresse.
|
||||||
|
ADRESSE="$(grep -E '^APP_BIND=' .env | tail -1 | cut -d= -f2- | tr -d '"' || true)"
|
||||||
|
PORT="$(grep -E '^APP_PORT=' .env | tail -1 | cut -d= -f2- | tr -d '"' || true)"
|
||||||
|
[ -n "$ADRESSE" ] || ADRESSE="127.0.0.1"
|
||||||
|
[ -n "$PORT" ] || PORT="3000"
|
||||||
|
# Auf 0.0.0.0 kann man nicht zugreifen - dann ueber die Loopback-Adresse.
|
||||||
|
[ "$ADRESSE" = "0.0.0.0" ] && ADRESSE="127.0.0.1"
|
||||||
|
|
||||||
|
GESUND="http://${ADRESSE}:${PORT}/api/gesundheit"
|
||||||
|
ANTWORT=""
|
||||||
|
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
ANTWORT="$(curl -fsS --max-time 5 "$GESUND" 2>/dev/null || true)"
|
||||||
|
case "$ANTWORT" in *'"bereit"'*) break ;; esac
|
||||||
|
printf '.'
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
case "$ANTWORT" in
|
||||||
|
*'"bereit"'*) gut "Die Anwendung antwortet." ;;
|
||||||
|
*)
|
||||||
|
printf '\n'
|
||||||
|
docker compose logs app --tail 40 | sed 's/^/ /'
|
||||||
|
abbruch "Die Anwendung antwortet nicht unter $GESUND.
|
||||||
|
Die letzten Zeilen aus dem Log stehen oben.
|
||||||
|
Zurueck geht es mit: git checkout $JETZT && docker compose up -d --build"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- 8. SQL-Skripte ----------------------------------------------------------
|
||||||
|
|
||||||
|
schritt "Volltextsuche und Audit-Schutz"
|
||||||
|
printf ' Diese beiden laufen nicht mit den Migrationen mit.\n'
|
||||||
|
printf ' Sie sind idempotent - mehrfaches Ausfuehren schadet nicht.\n\n'
|
||||||
|
|
||||||
|
PRISMA="node node_modules/prisma/build/index.js"
|
||||||
|
|
||||||
|
for skript in volltextsuche audit-schutz; do
|
||||||
|
if docker compose exec -T app $PRISMA db execute \
|
||||||
|
--schema prisma/schema.prisma --file "prisma/sql/${skript}.sql" >/dev/null 2>&1; then
|
||||||
|
gut "${skript}.sql ausgefuehrt"
|
||||||
|
else
|
||||||
|
warnung "${skript}.sql ist fehlgeschlagen - bitte von Hand nachholen:"
|
||||||
|
printf ' docker compose exec app %s db execute \\\n' "$PRISMA"
|
||||||
|
printf ' --schema prisma/schema.prisma --file prisma/sql/%s.sql\n' "$skript"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- 9. Nachschau ------------------------------------------------------------
|
||||||
|
|
||||||
|
schritt "Ergebnis"
|
||||||
|
|
||||||
|
LAEUFT="$(printf '%s' "$ANTWORT" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')"
|
||||||
|
ERWARTET="${ZIEL#v}"
|
||||||
|
|
||||||
|
printf ' Erwartet: %s\n' "$ERWARTET"
|
||||||
|
printf ' Laeuft: %s\n' "${LAEUFT:-unbekannt}"
|
||||||
|
|
||||||
|
if [ "$LAEUFT" = "$ERWARTET" ]; then
|
||||||
|
printf '\n'
|
||||||
|
gut "Fertig. Version $ERWARTET laeuft."
|
||||||
|
else
|
||||||
|
printf '\n'
|
||||||
|
warnung "Die laufende Fassung stimmt nicht mit der erwarteten ueberein."
|
||||||
|
warnung "Vermutlich hat der Bau den alten Behaelter stehen lassen:"
|
||||||
|
printf ' docker compose up -d --build --force-recreate app\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n Zustand: docker compose ps\n'
|
||||||
|
printf ' Log: docker compose logs app --tail 50\n'
|
||||||
|
printf ' Zurueck: git checkout %s && docker compose up -d --build\n\n' "$JETZT"
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
@@ -8,21 +8,36 @@ import { z } from 'zod'
|
|||||||
* ist, nicht was falsch war.
|
* ist, nicht was falsch war.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Leere Zeichenketten aus einem Formular sollen zu null werden, nicht zu "". */
|
/**
|
||||||
|
* Optionales Textfeld.
|
||||||
|
*
|
||||||
|
* Drei Eingaben muessen zu null werden:
|
||||||
|
* "" das Feld war da, blieb aber leer
|
||||||
|
* null ausdruecklich geleert
|
||||||
|
* undefined das Feld war im Formular ueberhaupt nicht dabei
|
||||||
|
*
|
||||||
|
* Der letzte Fall ist der wichtige: Das kurze Formular der Ersteinrichtung
|
||||||
|
* schickt nur eine Handvoll Felder, das vollstaendige unter Stammdaten
|
||||||
|
* alle. Beide gehen durch dasselbe Schema. Ohne .optional() wuerde die
|
||||||
|
* Ersteinrichtung an zwei Dutzend Feldern scheitern, die gar nicht auf dem
|
||||||
|
* Bildschirm stehen - und die Meldung liesse sich keinem Feld zuordnen.
|
||||||
|
*/
|
||||||
const optionalerText = (max = 200) =>
|
const optionalerText = (max = 200) =>
|
||||||
z
|
z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.max(max, `Bitte höchstens ${max} Zeichen.`)
|
.max(max, `Bitte höchstens ${max} Zeichen.`)
|
||||||
.transform((wert) => (wert === '' ? null : wert))
|
.optional()
|
||||||
.nullable()
|
.nullable()
|
||||||
|
.transform((wert) => (wert === undefined || wert === '' ? null : wert))
|
||||||
|
|
||||||
/** HTML-Datumsfelder liefern "" oder "2026-01-15". */
|
/** HTML-Datumsfelder liefern "" oder "2026-01-15" - oder gar nichts. */
|
||||||
const optionalesDatum = z
|
const optionalesDatum = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.transform((wert) => (wert === '' ? null : wert))
|
.optional()
|
||||||
.nullable()
|
.nullable()
|
||||||
|
.transform((wert) => (wert === undefined || wert === '' ? null : wert))
|
||||||
.refine((wert) => wert === null || /^\d{4}-\d{2}-\d{2}$/.test(wert), {
|
.refine((wert) => wert === null || /^\d{4}-\d{2}-\d{2}$/.test(wert), {
|
||||||
message: 'Bitte ein Datum im Format TT.MM.JJJJ wählen.',
|
message: 'Bitte ein Datum im Format TT.MM.JJJJ wählen.',
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/** Muss zur "version" in der package.json passen. */
|
/** Muss zur "version" in der package.json passen. */
|
||||||
export const VERSION = '1.0.2'
|
export const VERSION = '1.0.5'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kurzer Git-Stand, beim Bauen hineingereicht:
|
* Kurzer Git-Stand, beim Bauen hineingereicht:
|
||||||
|
|||||||
@@ -53,6 +53,49 @@ function felderAusFehler(fehler: z.ZodError): Record<string, string> {
|
|||||||
return ergebnis
|
return ergebnis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut die Antwort auf einen Pruefungsfehler.
|
||||||
|
*
|
||||||
|
* Der wichtige Teil ist die Unterscheidung: Beanstandet die Pruefung ein
|
||||||
|
* Feld, das gar nicht abgeschickt wurde, dann passt das Formular nicht zum
|
||||||
|
* Schema - und die Meldung liesse sich auf dem Bildschirm keinem Feld
|
||||||
|
* zuordnen. Der Nutzer saehe "Bitte pruefe die markierten Felder", ohne
|
||||||
|
* dass irgendetwas markiert waere, und kaeme nie weiter.
|
||||||
|
*
|
||||||
|
* Das ist ein Fehler im Programm, keiner in der Eingabe. Er wird deshalb
|
||||||
|
* benannt und ins Serverlog geschrieben, statt ihn dem Nutzer als
|
||||||
|
* Eingabefehler unterzuschieben.
|
||||||
|
*/
|
||||||
|
function antwortAufFehler(fehler: z.ZodError, formular: FormData): BetreuteZustand {
|
||||||
|
const felder = felderAusFehler(fehler)
|
||||||
|
const gesendet = new Set(formular.keys())
|
||||||
|
const nichtZuordenbar = Object.entries(felder).filter(([feld]) => !gesendet.has(feld))
|
||||||
|
|
||||||
|
if (nichtZuordenbar.length > 0) {
|
||||||
|
const liste = nichtZuordenbar.map(([feld, meldung]) => `${feld}: ${meldung}`).join('; ')
|
||||||
|
console.error(
|
||||||
|
`[stammdaten] Das Formular passt nicht zum Schema. Beanstandet wurden ` +
|
||||||
|
`Felder, die es gar nicht abgeschickt hat: ${liste}`,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
fehler:
|
||||||
|
'Das Formular passt nicht zur Prüfung — betroffen sind Felder, die ' +
|
||||||
|
'hier gar nicht stehen: ' +
|
||||||
|
nichtZuordenbar.map(([feld]) => feld).join(', ') +
|
||||||
|
'. Das ist ein Fehler in der Anwendung, nicht in deiner Eingabe. ' +
|
||||||
|
'Einzelheiten stehen im Serverlog.',
|
||||||
|
felder,
|
||||||
|
werte: werteMerken(formular),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fehler: 'Bitte prüfe die markierten Felder.',
|
||||||
|
felder,
|
||||||
|
werte: werteMerken(formular),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function werteMerken(formular: FormData): Record<string, string> {
|
function werteMerken(formular: FormData): Record<string, string> {
|
||||||
const werte: Record<string, string> = {}
|
const werte: Record<string, string> = {}
|
||||||
for (const [name, wert] of formular.entries()) {
|
for (const [name, wert] of formular.entries()) {
|
||||||
@@ -77,13 +120,7 @@ export async function betreuteAnlegen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gelesen = BetreuteSchema.safeParse(formularLesen(formular))
|
const gelesen = BetreuteSchema.safeParse(formularLesen(formular))
|
||||||
if (!gelesen.success) {
|
if (!gelesen.success) return antwortAufFehler(gelesen.error, formular)
|
||||||
return {
|
|
||||||
fehler: 'Bitte prüfe die markierten Felder.',
|
|
||||||
felder: felderAusFehler(gelesen.error),
|
|
||||||
werte: werteMerken(formular),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { offen, verschluesselt } = aufteilen(gelesen.data)
|
const { offen, verschluesselt } = aufteilen(gelesen.data)
|
||||||
|
|
||||||
@@ -134,13 +171,7 @@ export async function betreuteAendern(
|
|||||||
const { benutzer, betreute } = await erfordereZugang(betreuteId, 'STAMMDATEN', 'schreiben')
|
const { benutzer, betreute } = await erfordereZugang(betreuteId, 'STAMMDATEN', 'schreiben')
|
||||||
|
|
||||||
const gelesen = BetreuteSchema.safeParse(formularLesen(formular))
|
const gelesen = BetreuteSchema.safeParse(formularLesen(formular))
|
||||||
if (!gelesen.success) {
|
if (!gelesen.success) return antwortAufFehler(gelesen.error, formular)
|
||||||
return {
|
|
||||||
fehler: 'Bitte prüfe die markierten Felder.',
|
|
||||||
felder: felderAusFehler(gelesen.error),
|
|
||||||
werte: werteMerken(formular),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { offen, verschluesselt } = aufteilen(gelesen.data)
|
const { offen, verschluesselt } = aufteilen(gelesen.data)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { BetreuteSchema, formularLesen } from '@/lib/validierung/betreute'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eingabepruefung der Stammdaten.
|
||||||
|
*
|
||||||
|
* Anlass für diese Datei war ein Fehler im Betrieb: Die Ersteinrichtung
|
||||||
|
* ließ sich nicht abschließen. Das Formular meldete „Bitte prüfe die
|
||||||
|
* markierten Felder", markierte aber keines — weil die Meldungen zu
|
||||||
|
* Feldern gehörten, die dort gar nicht stehen.
|
||||||
|
*
|
||||||
|
* Ursache: Das kurze Formular der Ersteinrichtung schickt neun Felder,
|
||||||
|
* das vollständige unter Stammdaten alle. Beide gehen durch dasselbe
|
||||||
|
* Schema, und die optionalen Felder waren zwar `nullable`, aber nicht
|
||||||
|
* `optional` — ein fehlendes Feld ist `undefined`, nicht `null`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Die Felder, die das kurze Formular der Ersteinrichtung tatsächlich schickt. */
|
||||||
|
function ersteinrichtungsFormular(): FormData {
|
||||||
|
const f = new FormData()
|
||||||
|
f.set('vorname', 'Franziska')
|
||||||
|
f.set('nachname', 'Wilde')
|
||||||
|
f.set('geburtsdatum', '1963-09-23')
|
||||||
|
f.set('betreuungsgericht', 'Lichtenfels')
|
||||||
|
f.set('aktenzeichenGericht', 'XVII 355/25')
|
||||||
|
f.set('bestellungAm', '2025-11-07')
|
||||||
|
f.append('aufgabenkreise', 'VERMOEGENSSORGE')
|
||||||
|
f.append('aufgabenkreise', 'GESUNDHEITSSORGE')
|
||||||
|
f.set('befreiterBetreuer', 'on')
|
||||||
|
f.set('ehrenamtlich', 'on')
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Ersteinrichtung', () => {
|
||||||
|
it('nimmt das kurze Formular an, obwohl es die meisten Felder nicht schickt', () => {
|
||||||
|
const gelesen = BetreuteSchema.safeParse(formularLesen(ersteinrichtungsFormular()))
|
||||||
|
|
||||||
|
if (!gelesen.success) {
|
||||||
|
const felder = Object.keys(gelesen.error.flatten().fieldErrors).join(', ')
|
||||||
|
throw new Error(`Abgelehnt wegen: ${felder}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(gelesen.data.vorname).toBe('Franziska')
|
||||||
|
expect(gelesen.data.nachname).toBe('Wilde')
|
||||||
|
expect(gelesen.data.geburtsdatum.toISOString()).toBe('1963-09-23T00:00:00.000Z')
|
||||||
|
expect(gelesen.data.bestellungAm?.toISOString()).toBe('2025-11-07T00:00:00.000Z')
|
||||||
|
expect(gelesen.data.aufgabenkreise).toEqual(['VERMOEGENSSORGE', 'GESUNDHEITSSORGE'])
|
||||||
|
expect(gelesen.data.befreiterBetreuer).toBe(true)
|
||||||
|
expect(gelesen.data.ehrenamtlich).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('macht aus nicht gesendeten Feldern null, nicht undefined', () => {
|
||||||
|
const gelesen = BetreuteSchema.parse(formularLesen(ersteinrichtungsFormular()))
|
||||||
|
|
||||||
|
// Diese Felder standen nie auf dem Bildschirm. Sie muessen als null in
|
||||||
|
// der Datenbank landen, sonst schreibt Prisma sie gar nicht.
|
||||||
|
expect(gelesen.heimName).toBeNull()
|
||||||
|
expect(gelesen.diagnosen).toBeNull()
|
||||||
|
expect(gelesen.geburtsname).toBeNull()
|
||||||
|
expect(gelesen.heimEinzugAm).toBeNull()
|
||||||
|
expect(gelesen.bestellungBis).toBeNull()
|
||||||
|
expect(gelesen.aktenzeichenBezirk).toBeNull()
|
||||||
|
expect(gelesen.pflegegrad).toBeNull()
|
||||||
|
expect(gelesen.schwerbehinderungGdB).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('haelt auch das vollstaendige Formular aus', () => {
|
||||||
|
const f = ersteinrichtungsFormular()
|
||||||
|
// So kommt es aus dem Bearbeitungsformular: alle Felder da, viele leer.
|
||||||
|
for (const feld of [
|
||||||
|
'anrede', 'geburtsname', 'geburtsort',
|
||||||
|
'heimName', 'heimStrasse', 'heimPlz', 'heimOrt', 'heimZimmer', 'heimTelefon',
|
||||||
|
'fruehereStrasse', 'frueherePlz', 'fruehererOrt',
|
||||||
|
'krankenkasse', 'krankenversichertennummer', 'pflegekasse',
|
||||||
|
'schwerbehinderungMerkzeichen', 'diagnosen', 'ermittelterWille',
|
||||||
|
'aufgabenkreiseFreitext', 'aktenzeichenBezirk', 'aktenzeichenSozialamt',
|
||||||
|
'rentenversicherungsnummer', 'steuerId', 'notizen',
|
||||||
|
]) {
|
||||||
|
f.set(feld, '')
|
||||||
|
}
|
||||||
|
for (const feld of ['heimEinzugAm', 'pflegegradSeit', 'pflegegradBescheidVom', 'bestellungBis']) {
|
||||||
|
f.set(feld, '')
|
||||||
|
}
|
||||||
|
f.set('pflegegrad', '')
|
||||||
|
f.set('schwerbehinderungGdB', '')
|
||||||
|
|
||||||
|
const gelesen = BetreuteSchema.safeParse(formularLesen(f))
|
||||||
|
expect(gelesen.success, 'Leere Felder duerfen nicht abgelehnt werden').toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('nimmt ausgefuellte Angaben unveraendert an', () => {
|
||||||
|
const f = ersteinrichtungsFormular()
|
||||||
|
f.set('heimName', ' Seniorenzentrum St. Katharina ')
|
||||||
|
f.set('pflegegrad', '3')
|
||||||
|
f.set('diagnosen', 'COPD GOLD III')
|
||||||
|
|
||||||
|
const gelesen = BetreuteSchema.parse(formularLesen(f))
|
||||||
|
// Fuehrende und nachlaufende Leerzeichen fliegen raus.
|
||||||
|
expect(gelesen.heimName).toBe('Seniorenzentrum St. Katharina')
|
||||||
|
expect(gelesen.pflegegrad).toBe(3)
|
||||||
|
expect(gelesen.diagnosen).toBe('COPD GOLD III')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('besteht weiterhin auf Vorname, Nachname und Geburtsdatum', () => {
|
||||||
|
const f = new FormData()
|
||||||
|
f.set('vorname', '')
|
||||||
|
f.set('nachname', '')
|
||||||
|
f.set('geburtsdatum', '')
|
||||||
|
|
||||||
|
const gelesen = BetreuteSchema.safeParse(formularLesen(f))
|
||||||
|
expect(gelesen.success).toBe(false)
|
||||||
|
|
||||||
|
if (!gelesen.success) {
|
||||||
|
const felder = gelesen.error.flatten().fieldErrors
|
||||||
|
expect(Object.keys(felder).sort()).toEqual(['geburtsdatum', 'nachname', 'vorname'])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('weist ein Geburtsdatum in der Zukunft ab', () => {
|
||||||
|
const f = ersteinrichtungsFormular()
|
||||||
|
f.set('geburtsdatum', '2099-01-01')
|
||||||
|
|
||||||
|
const gelesen = BetreuteSchema.safeParse(formularLesen(f))
|
||||||
|
expect(gelesen.success).toBe(false)
|
||||||
|
if (!gelesen.success) {
|
||||||
|
expect(gelesen.error.flatten().fieldErrors.geburtsdatum?.[0]).toMatch(/Zukunft/)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('weist ein unfertiges Datum ab, statt es stillschweigend zu verwerfen', () => {
|
||||||
|
const f = ersteinrichtungsFormular()
|
||||||
|
f.set('bestellungAm', '2025-11')
|
||||||
|
|
||||||
|
const gelesen = BetreuteSchema.safeParse(formularLesen(f))
|
||||||
|
expect(gelesen.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user