Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7780739a1a | ||
|
|
9365645f72 | ||
|
|
232543e2aa |
+23
-7
@@ -19,15 +19,31 @@ AUTH_URL=http://localhost:3000
|
||||
# Hinter einem Reverse Proxy auf true setzen.
|
||||
AUTH_TRUST_HOST=true
|
||||
|
||||
# --- Erreichbarkeit ---------------------------------------------------------
|
||||
# Auf welcher Adresse und welchem Port horcht die Anwendung?
|
||||
# --- Erreichbarkeit: HIER MUSST DU DICH ENTSCHEIDEN -------------------------
|
||||
#
|
||||
# Proxy auf DEMSELBEN Server: 127.0.0.1 (Standard, nichts zu tun)
|
||||
# Proxy auf einem ANDEREN: die Adresse des privaten Netzes bzw. des
|
||||
# WireGuard-Tunnels, z.B. 10.0.0.5
|
||||
# Auf welcher Adresse soll die Anwendung erreichbar sein? Das haengt davon
|
||||
# ab, WO dein Reverse Proxy laeuft. Waehl genau eine Zeile aus:
|
||||
#
|
||||
# 0.0.0.0 nur, wenn eine Firewall den Port ausschliesslich fuer die IP des
|
||||
# Proxy-Servers oeffnet. Die Anwendung beendet kein TLS.
|
||||
# Proxy auf DEMSELBEN Server
|
||||
# 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_PORT=3000
|
||||
|
||||
|
||||
+136
-1
@@ -26,6 +26,138 @@ Wiederherstellung*.
|
||||
|
||||
_Noch nichts._
|
||||
|
||||
## [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
|
||||
|
||||
### Behoben
|
||||
|
||||
- **Die Einrichtung des zweiten Faktors brach mit einem Serverfehler ab**
|
||||
(„Application error: a server-side exception has occurred"). Im
|
||||
Serverlog stand `Umgebungsvariable ENC_KEY_V2 fehlt` — obwohl niemand
|
||||
einen zweiten Schlüssel wollte.
|
||||
|
||||
Ursache: `docker-compose.yml` reicht `ENC_KEY_V2` immer an den Behälter
|
||||
durch, mit **leerem Wert**, wenn in der `.env` keiner steht. Die
|
||||
Anwendung erkannte Schlüssel aber am Variablennamen statt am Inhalt,
|
||||
fand also ein leeres `ENC_KEY_V2` und brach ab. Leere
|
||||
Schlüsselvariablen werden jetzt übersprungen.
|
||||
|
||||
Betroffen war jede Stelle, die verschlüsselte Felder schreibt oder
|
||||
liest — also praktisch alles außer der reinen Anmeldung. Die Anmeldung
|
||||
ging durch, weil sie ohne Verschlüsselung auskommt; der Fehler trat
|
||||
deshalb erst auf der nächsten Seite auf.
|
||||
|
||||
- **Die Meldung beim Schlüsselwechsel** nennt jetzt, welche Versionen
|
||||
tatsächlich vorhanden sind, und die richtige Reihenfolge: erst den
|
||||
neuen Schlüssel eintragen, dann `ENC_KEY_AKTIV` umstellen.
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- **`scripts/anmeldung-pruefen.mjs`** — beantwortet die Frage, warum eine
|
||||
Anmeldung nicht klappt. Das Formular sagt bewusst nicht, woran es lag,
|
||||
sonst ließe 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 — dazu den Zustand des Zugangs, den Stand der
|
||||
Login-Sperre und die eingestellte `AUTH_URL`.
|
||||
|
||||
```bash
|
||||
docker compose exec app node scripts/anmeldung-pruefen.mjs deine@adresse.de
|
||||
```
|
||||
|
||||
Es ändert nichts, es liest nur.
|
||||
|
||||
### Geprüft
|
||||
|
||||
- Sieben neue Tests (`tests/umgebung.test.ts`) decken die
|
||||
Schlüsselauswertung ab, darunter genau die Konstellation aus dem
|
||||
Fehlerbericht: gesetztes `ENC_KEY_V1`, leeres `ENC_KEY_V2`.
|
||||
|
||||
## [1.0.1] – 2026-08-22
|
||||
|
||||
### Behoben
|
||||
@@ -219,6 +351,9 @@ 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.1...HEAD
|
||||
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.4...HEAD
|
||||
[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.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
|
||||
|
||||
@@ -91,6 +91,8 @@ services:
|
||||
AUTH_URL: ${AUTH_URL:?AUTH_URL fehlt in der .env}
|
||||
AUTH_TRUST_HOST: 'true'
|
||||
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
|
||||
# leere Schluesselvariablen - siehe src/lib/env.ts.
|
||||
ENC_KEY_V2: ${ENC_KEY_V2:-}
|
||||
ENC_KEY_AKTIV: ${ENC_KEY_AKTIV:-1}
|
||||
INDEX_KEY: ${INDEX_KEY:?INDEX_KEY fehlt in der .env}
|
||||
|
||||
@@ -205,6 +205,14 @@ S3_SECRET_ACCESS_KEY=<aus Schritt 2>
|
||||
|
||||
# --- Sicherung ---
|
||||
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
|
||||
@@ -875,6 +883,41 @@ Fehlt eine Umgebungsvariable, sagt die Anwendung beim Start, welche.
|
||||
Die `.env` liegt nicht neben der `docker-compose.yml`, oder die Zeile ist
|
||||
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
|
||||
|
||||
```bash
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "betreuungs-cockpit",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.4",
|
||||
"private": true,
|
||||
"description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung",
|
||||
"engines": {
|
||||
|
||||
@@ -25,6 +25,42 @@ BEGIN
|
||||
RETURN OLD;
|
||||
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
|
||||
'Das Audit-Log ist unveraenderlich. % auf audit_log ist nicht zulaessig.',
|
||||
TG_OP
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Warum klappt die Anmeldung nicht?
|
||||
*
|
||||
* node --env-file=.env scripts/anmeldung-pruefen.mjs <e-mail>
|
||||
* docker compose exec app node scripts/anmeldung-pruefen.mjs <e-mail>
|
||||
*
|
||||
* Die Anwendung sagt am Anmeldeformular bewusst nicht, woran es lag - sonst
|
||||
* liesse sich daran ablesen, welche Adressen es gibt. Auf dem Server darf
|
||||
* man es wissen: Jeder Versuch wird mit Grund protokolliert, und dieses
|
||||
* Skript liest ihn vor.
|
||||
*
|
||||
* Es aendert nichts. Es liest nur.
|
||||
*/
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const email = (process.argv[2] ?? '').trim().toLowerCase()
|
||||
|
||||
if (!email) {
|
||||
console.error('Aufruf: node scripts/anmeldung-pruefen.mjs <e-mail>')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const db = new PrismaClient()
|
||||
|
||||
function zeit(d) {
|
||||
return d ? new Date(d).toLocaleString('de-DE') : '—'
|
||||
}
|
||||
|
||||
/** Klartext zu den Gruenden, die src/auth.ts protokolliert. */
|
||||
const GRUND = {
|
||||
unbekannt: 'Diese E-Mail-Adresse gibt es nicht (Tippfehler? andere Adresse?)',
|
||||
'passwort-falsch': 'Das Passwort stimmt nicht',
|
||||
'code-falsch': 'Der Code aus der Authenticator-App stimmt nicht',
|
||||
gesperrt: 'Abgewiesen, weil zu diesem Zeitpunkt bereits gesperrt war',
|
||||
'konto-inaktiv': 'Der Zugang ist deaktiviert oder im Papierkorb',
|
||||
}
|
||||
|
||||
try {
|
||||
const benutzer = await db.user.findUnique({ where: { email } })
|
||||
|
||||
console.log('\n══ Zugang ' + '═'.repeat(50))
|
||||
|
||||
if (!benutzer) {
|
||||
console.log(` Es gibt KEINEN Zugang mit der Adresse ${email}.`)
|
||||
const alle = await db.user.findMany({
|
||||
select: { email: true, aktiv: true, geloeschtAm: true },
|
||||
orderBy: { erstelltAm: 'asc' },
|
||||
})
|
||||
console.log(
|
||||
alle.length
|
||||
? '\n Vorhandene Adressen:\n' +
|
||||
alle.map((u) => ` ${u.email}${u.geloeschtAm ? ' (im Papierkorb)' : u.aktiv ? '' : ' (deaktiviert)'}`).join('\n')
|
||||
: '\n Es gibt ueberhaupt keinen Zugang. Leg ihn im Browser unter /ersteinrichtung an.',
|
||||
)
|
||||
} else {
|
||||
const zweiterFaktor = benutzer.totpAktiviertAm
|
||||
? `eingerichtet am ${zeit(benutzer.totpAktiviertAm)}`
|
||||
: benutzer.totpSecretVerschluesselt
|
||||
? 'vorbereitet, aber NICHT bestaetigt - beim Anmelden wird noch kein Code verlangt'
|
||||
: 'noch nicht eingerichtet - beim Anmelden wird noch kein Code verlangt'
|
||||
|
||||
console.log(` Name: ${benutzer.name}`)
|
||||
console.log(` Rolle: ${benutzer.rolle}`)
|
||||
console.log(` Aktiv: ${benutzer.aktiv ? 'ja' : 'NEIN - Anmeldung nicht moeglich'}`)
|
||||
console.log(` Papierkorb: ${benutzer.geloeschtAm ? 'JA seit ' + zeit(benutzer.geloeschtAm) : 'nein'}`)
|
||||
console.log(` Zweiter Faktor: ${zweiterFaktor}`)
|
||||
console.log(` Wiederherstellung: ${benutzer.wiederherstellungscodes.length} Codes uebrig`)
|
||||
console.log(` Passwort gesetzt: ${zeit(benutzer.passwortGeaendertAm)}`)
|
||||
console.log(` Letzte Anmeldung: ${zeit(benutzer.letzteAnmeldung)}`)
|
||||
|
||||
const zugaenge = await db.betreuungsZugang.count({ where: { userId: benutzer.id } })
|
||||
const betreute = await db.betreute.count({ where: { geloeschtAm: null } })
|
||||
console.log(` Betreute Personen: ${zugaenge} zugeordnet, ${betreute} insgesamt angelegt`)
|
||||
|
||||
if (zugaenge === 0 && betreute === 0) {
|
||||
console.log(
|
||||
'\n Hinweis: Nach dem Anmelden landest du zuerst bei der\n' +
|
||||
' Ersteinrichtung - erst zweiter Faktor, dann betreute Person.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Versuche der letzten Tage -------------------------------------------
|
||||
const versuche = await db.loginVersuch.findMany({
|
||||
where: { email },
|
||||
orderBy: { zeitpunkt: 'desc' },
|
||||
take: 15,
|
||||
})
|
||||
|
||||
console.log('\n══ Letzte Anmeldeversuche ' + '═'.repeat(35))
|
||||
|
||||
if (versuche.length === 0) {
|
||||
console.log(
|
||||
' Keine. Es ist also NIE ein Versuch beim Server angekommen.\n\n' +
|
||||
' Das heisst: Das Formular hat gar nicht abgeschickt, oder es lief\n' +
|
||||
' auf eine andere Adresse. Pruef AUTH_URL - siehe unten.',
|
||||
)
|
||||
} else {
|
||||
for (const v of versuche) {
|
||||
const kennzeichen = v.erfolg ? 'OK ' : 'FEHLER'
|
||||
const grund = v.erfolg ? 'angemeldet' : (GRUND[v.grund] ?? v.grund ?? 'unbekannter Grund')
|
||||
console.log(` ${zeit(v.zeitpunkt).padEnd(20)} ${kennzeichen} ${grund}`)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sperre ---------------------------------------------------------------
|
||||
const fenster = Number.parseInt(process.env.LOGIN_FENSTER_SEKUNDEN ?? '900', 10)
|
||||
const grenze = Number.parseInt(process.env.LOGIN_MAX_VERSUCHE ?? '5', 10)
|
||||
const seit = new Date(Date.now() - fenster * 1000)
|
||||
|
||||
const offen = await db.loginVersuch.count({
|
||||
where: { email, erfolg: false, zeitpunkt: { gte: seit } },
|
||||
})
|
||||
|
||||
console.log('\n══ Sperre ' + '═'.repeat(51))
|
||||
console.log(` ${offen} von ${grenze} Fehlversuchen im laufenden Fenster (${fenster / 60} Minuten)`)
|
||||
|
||||
if (offen >= grenze) {
|
||||
console.log(
|
||||
'\n GESPERRT. Warte das Fenster ab, oder setz die Zaehler zurueck:\n' +
|
||||
' docker compose exec db psql -U cockpit betreuungs_cockpit \\\n' +
|
||||
' -c "DELETE FROM login_versuch WHERE erfolg = false;"',
|
||||
)
|
||||
}
|
||||
|
||||
// --- Umgebung -------------------------------------------------------------
|
||||
console.log('\n══ Umgebung ' + '═'.repeat(49))
|
||||
console.log(` AUTH_URL: ${process.env.AUTH_URL ?? 'NICHT GESETZT'}`)
|
||||
console.log(
|
||||
'\n Diese Adresse muss EXAKT die sein, die du im Browser aufrufst -\n' +
|
||||
' gleiches Schema (https), gleicher Hostname, kein Schraegstrich am\n' +
|
||||
' Ende. Weicht sie ab, wird das Sitzungs-Cookie fuer die falsche\n' +
|
||||
' Herkunft gesetzt: Die Anmeldung landet ohne Fehlermeldung wieder\n' +
|
||||
' auf dem Formular, und oben steht kein einziger Fehlversuch.\n',
|
||||
)
|
||||
} catch (fehler) {
|
||||
// Ein Prisma-Stacktrace hilft hier niemandem weiter.
|
||||
const meldung = fehler instanceof Error ? fehler.message : String(fehler)
|
||||
|
||||
if (/Can't reach database server|ECONNREFUSED/i.test(meldung)) {
|
||||
// Das Passwort in der Verbindungszeichenkette gehoert nicht auf den Schirm.
|
||||
const url = (process.env.DATABASE_URL ?? 'NICHT GESETZT').replace(/:[^:@/]+@/, ':***@')
|
||||
console.error(
|
||||
[
|
||||
'',
|
||||
'Die Datenbank ist nicht erreichbar.',
|
||||
'',
|
||||
` DATABASE_URL: ${url}`,
|
||||
'',
|
||||
' Im Docker-Betrieb gehoert dieses Skript IN den Behaelter:',
|
||||
' docker compose exec app node scripts/anmeldung-pruefen.mjs <e-mail>',
|
||||
'',
|
||||
' Laeuft die Datenbank ueberhaupt?',
|
||||
' docker compose ps',
|
||||
'',
|
||||
].join('\n'),
|
||||
)
|
||||
} else {
|
||||
console.error(`\nFehler: ${meldung}\n`)
|
||||
}
|
||||
process.exitCode = 1
|
||||
} finally {
|
||||
await db.$disconnect()
|
||||
}
|
||||
+19
-5
@@ -58,14 +58,25 @@ function einmal<T>(fn: () => T): () => T {
|
||||
|
||||
const holeVerschluesselungsschluessel = einmal((): Map<number, Buffer> => {
|
||||
const schluessel = new Map<number, Buffer>()
|
||||
|
||||
// ENC_KEY_V1, ENC_KEY_V2, ... - alte Versionen bleiben zum Entschluesseln
|
||||
// stehen, verschluesselt wird immer mit der aktiven Version.
|
||||
for (const [name] of Object.entries(process.env)) {
|
||||
for (const [name, wert] of Object.entries(process.env)) {
|
||||
const treffer = name.match(/^ENC_KEY_V(\d+)$/)
|
||||
if (treffer) {
|
||||
schluessel.set(Number.parseInt(treffer[1]!, 10), schluessel32(name))
|
||||
}
|
||||
if (!treffer) continue
|
||||
|
||||
// Eine LEERE Variable ist kein Schluessel.
|
||||
//
|
||||
// docker compose reicht ENC_KEY_V2 immer durch - mit leerem Wert, wenn
|
||||
// in der .env keiner steht. Wuerde hier allein der Name zaehlen, waere
|
||||
// jede Installation ohne zweiten Schluessel kaputt, und zwar mit der
|
||||
// irrefuehrenden Meldung "ENC_KEY_V2 fehlt" - obwohl niemand einen
|
||||
// zweiten Schluessel wollte.
|
||||
if (!wert || wert.trim() === '') continue
|
||||
|
||||
schluessel.set(Number.parseInt(treffer[1]!, 10), schluessel32(name))
|
||||
}
|
||||
|
||||
if (schluessel.size === 0) {
|
||||
throw new Error(
|
||||
'Kein Verschluesselungsschluessel gefunden. Es muss mindestens ENC_KEY_V1 ' +
|
||||
@@ -78,8 +89,11 @@ const holeVerschluesselungsschluessel = einmal((): Map<number, Buffer> => {
|
||||
const holeAktiveVersion = einmal((): number => {
|
||||
const version = zahl('ENC_KEY_AKTIV', 1)
|
||||
if (!holeVerschluesselungsschluessel().has(version)) {
|
||||
const vorhanden = [...holeVerschluesselungsschluessel().keys()].sort().join(', ')
|
||||
throw new Error(
|
||||
`ENC_KEY_AKTIV=${version}, aber ENC_KEY_V${version} ist nicht gesetzt.`,
|
||||
`ENC_KEY_AKTIV=${version}, aber ENC_KEY_V${version} ist nicht gesetzt oder leer. ` +
|
||||
`Vorhanden sind die Versionen: ${vorhanden}. Beim Rotieren zuerst den ` +
|
||||
`neuen Schluessel eintragen, danach ENC_KEY_AKTIV umstellen.`,
|
||||
)
|
||||
}
|
||||
return version
|
||||
|
||||
@@ -8,21 +8,36 @@ import { z } from 'zod'
|
||||
* 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) =>
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.max(max, `Bitte höchstens ${max} Zeichen.`)
|
||||
.transform((wert) => (wert === '' ? null : wert))
|
||||
.optional()
|
||||
.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
|
||||
.string()
|
||||
.trim()
|
||||
.transform((wert) => (wert === '' ? null : wert))
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((wert) => (wert === undefined || wert === '' ? null : wert))
|
||||
.refine((wert) => wert === null || /^\d{4}-\d{2}-\d{2}$/.test(wert), {
|
||||
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. */
|
||||
export const VERSION = '1.0.1'
|
||||
export const VERSION = '1.0.4'
|
||||
|
||||
/**
|
||||
* Kurzer Git-Stand, beim Bauen hineingereicht:
|
||||
|
||||
@@ -53,6 +53,49 @@ function felderAusFehler(fehler: z.ZodError): Record<string, string> {
|
||||
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> {
|
||||
const werte: Record<string, string> = {}
|
||||
for (const [name, wert] of formular.entries()) {
|
||||
@@ -77,13 +120,7 @@ export async function betreuteAnlegen(
|
||||
}
|
||||
|
||||
const gelesen = BetreuteSchema.safeParse(formularLesen(formular))
|
||||
if (!gelesen.success) {
|
||||
return {
|
||||
fehler: 'Bitte prüfe die markierten Felder.',
|
||||
felder: felderAusFehler(gelesen.error),
|
||||
werte: werteMerken(formular),
|
||||
}
|
||||
}
|
||||
if (!gelesen.success) return antwortAufFehler(gelesen.error, formular)
|
||||
|
||||
const { offen, verschluesselt } = aufteilen(gelesen.data)
|
||||
|
||||
@@ -134,13 +171,7 @@ export async function betreuteAendern(
|
||||
const { benutzer, betreute } = await erfordereZugang(betreuteId, 'STAMMDATEN', 'schreiben')
|
||||
|
||||
const gelesen = BetreuteSchema.safeParse(formularLesen(formular))
|
||||
if (!gelesen.success) {
|
||||
return {
|
||||
fehler: 'Bitte prüfe die markierten Felder.',
|
||||
felder: felderAusFehler(gelesen.error),
|
||||
werte: werteMerken(formular),
|
||||
}
|
||||
}
|
||||
if (!gelesen.success) return antwortAufFehler(gelesen.error, formular)
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Auswertung der Umgebungsvariablen.
|
||||
*
|
||||
* Der Anlass für diese Datei war ein Fehler im Betrieb: docker compose
|
||||
* reicht `ENC_KEY_V2` immer durch — mit leerem Wert, wenn in der .env
|
||||
* keiner steht. Die Schlüsselsammlung erkannte Schlüssel aber am
|
||||
* Variablennamen statt am Inhalt, fand also ein leeres `ENC_KEY_V2` und
|
||||
* brach ab. Die Meldung lautete „ENC_KEY_V2 fehlt", obwohl niemand einen
|
||||
* zweiten Schlüssel wollte.
|
||||
*
|
||||
* Jede Prüfung hier lädt `@/lib/env` frisch, weil die Werte beim ersten
|
||||
* Zugriff gemerkt werden.
|
||||
*/
|
||||
|
||||
const ECHT = Buffer.alloc(32, 7).toString('base64')
|
||||
const ECHT_ZWEI = Buffer.alloc(32, 9).toString('base64')
|
||||
|
||||
let vorher: NodeJS.ProcessEnv
|
||||
|
||||
async function frischesEnv() {
|
||||
// Leert die Modulregistrierung, damit der naechste Import das Modul neu
|
||||
// auswertet - die Schluessel werden sonst nur beim ersten Mal gelesen.
|
||||
vi.resetModules()
|
||||
const { env } = await import('@/lib/env')
|
||||
return env
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vorher = { ...process.env }
|
||||
for (const name of Object.keys(process.env)) {
|
||||
if (/^ENC_KEY_V\d+$/.test(name)) delete process.env[name]
|
||||
}
|
||||
delete process.env.ENC_KEY_AKTIV
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = vorher
|
||||
})
|
||||
|
||||
describe('Verschlüsselungsschlüssel aus der Umgebung', () => {
|
||||
it('nimmt einen einzelnen Schlüssel an', async () => {
|
||||
process.env.ENC_KEY_V1 = ECHT
|
||||
|
||||
const env = await frischesEnv()
|
||||
expect([...env.verschluesselungsschluessel.keys()]).toEqual([1])
|
||||
expect(env.aktiveSchluesselversion).toBe(1)
|
||||
})
|
||||
|
||||
it('überspringt eine leere Schlüsselvariable', async () => {
|
||||
// Genau das setzt docker compose, wenn in der .env kein zweiter
|
||||
// Schlüssel steht: ENC_KEY_V2: ${ENC_KEY_V2:-}
|
||||
process.env.ENC_KEY_V1 = ECHT
|
||||
process.env.ENC_KEY_V2 = ''
|
||||
|
||||
const env = await frischesEnv()
|
||||
expect([...env.verschluesselungsschluessel.keys()]).toEqual([1])
|
||||
expect(env.aktiveSchluesselversion).toBe(1)
|
||||
})
|
||||
|
||||
it('überspringt eine Variable, in der nur Leerzeichen stehen', async () => {
|
||||
process.env.ENC_KEY_V1 = ECHT
|
||||
process.env.ENC_KEY_V2 = ' '
|
||||
|
||||
const env = await frischesEnv()
|
||||
expect([...env.verschluesselungsschluessel.keys()]).toEqual([1])
|
||||
})
|
||||
|
||||
it('nimmt beide Schlüssel an, wenn wirklich rotiert wird', async () => {
|
||||
process.env.ENC_KEY_V1 = ECHT
|
||||
process.env.ENC_KEY_V2 = ECHT_ZWEI
|
||||
process.env.ENC_KEY_AKTIV = '2'
|
||||
|
||||
const env = await frischesEnv()
|
||||
expect([...env.verschluesselungsschluessel.keys()].sort()).toEqual([1, 2])
|
||||
expect(env.aktiveSchluesselversion).toBe(2)
|
||||
})
|
||||
|
||||
it('sagt beim Rotieren deutlich, wenn der neue Schlüssel fehlt', async () => {
|
||||
process.env.ENC_KEY_V1 = ECHT
|
||||
process.env.ENC_KEY_V2 = ''
|
||||
process.env.ENC_KEY_AKTIV = '2'
|
||||
|
||||
const env = await frischesEnv()
|
||||
expect(() => env.aktiveSchluesselversion).toThrow(/ENC_KEY_AKTIV=2/)
|
||||
// Die Meldung nennt, was da ist, und die richtige Reihenfolge.
|
||||
expect(() => env.aktiveSchluesselversion).toThrow(/Vorhanden sind die Versionen: 1/)
|
||||
expect(() => env.aktiveSchluesselversion).toThrow(/zuerst den neuen Schluessel/)
|
||||
})
|
||||
|
||||
it('beschwert sich, wenn gar kein Schlüssel gesetzt ist', async () => {
|
||||
process.env.ENC_KEY_V2 = ''
|
||||
|
||||
const env = await frischesEnv()
|
||||
expect(() => env.verschluesselungsschluessel).toThrow(/mindestens ENC_KEY_V1/)
|
||||
})
|
||||
|
||||
it('weist einen Schlüssel falscher Länge ab', async () => {
|
||||
process.env.ENC_KEY_V1 = Buffer.alloc(16, 1).toString('base64')
|
||||
|
||||
const env = await frischesEnv()
|
||||
expect(() => env.verschluesselungsschluessel).toThrow(/32 Bytes/)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user