diff --git a/.env.example b/.env.example index ca90ca9..107b97b 100644 --- a/.env.example +++ b/.env.example @@ -103,7 +103,17 @@ VERTRAUE_PROXY=true # --- Objektspeicher (S3-kompatibel, EU!) ------------------------------------ # Wird ab Etappe 3 verwendet. Beispiel: Hetzner Object Storage, Falkenstein. # ACHTUNG: Mit dem Anbieter muss ein AV-Vertrag nach Art. 28 DSGVO bestehen. -S3_ENDPOINT=https://fsn1.your-objectstorage.com +# Bleibt AUSKOMMENTIERT, solange das mitgelieferte MinIO benutzt wird. +# +# Die docker-compose.yml setzt dann von selbst http://minio:9000 - aber +# nur, wenn die Variable leer ist. Steht hier ein Wert, gewinnt er, und +# die Anwendung spricht mit einem fremden Host statt mit dem Speicher +# nebenan. Genau das ist am 31.08.2026 passiert: Jede Ablage scheiterte +# monatelang mit "NoSuchBucket", und die Zugangs-ID ging bei jedem Versuch +# nach draussen. +# +# Nur setzen, wenn ein EXTERNER Objektspeicher benutzt wird, z.B.: +# S3_ENDPOINT=https://fsn1.your-objectstorage.com S3_REGION=eu-central S3_BUCKET=betreuungs-cockpit S3_ACCESS_KEY_ID= @@ -131,5 +141,5 @@ SMTP_FROM="Betreuungs-Cockpit " # --- Erstanmeldung ---------------------------------------------------------- # Wird nur vom Seed benutzt, um den ersten Betreuer-Zugang anzulegen. -SEED_ADMIN_EMAIL=fw@wildeflorian.de -SEED_ADMIN_PASSWORT=bitte-beim-ersten-login-aendern +SEED_ADMIN_EMAIL= +SEED_ADMIN_PASSWORT= diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 9aed948..9bf54fd 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -218,6 +218,9 @@ INDEX_KEY= # --- Objektspeicher --- S3_ACCESS_KEY_ID= S3_SECRET_ACCESS_KEY= +# S3_ENDPOINT bleibt AUSKOMMENTIERT, solange du das mitgelieferte MinIO +# benutzt. Nur dann setzt die docker-compose.yml von selbst +# http://minio:9000 - steht dort ein Wert, gewinnt er. # --- Sicherung --- SICHERUNG_PASSWORT= diff --git a/tests/env-beispiel.test.ts b/tests/env-beispiel.test.ts new file mode 100644 index 0000000..ec78855 --- /dev/null +++ b/tests/env-beispiel.test.ts @@ -0,0 +1,100 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' + +/** + * Die Beispielkonfiguration darf keine Falle enthalten. + * + * `.env.example` wird nach `.env` kopiert und dann Zeile für Zeile + * ausgefüllt. Was schon einen Wert hat, wird dabei leicht überlesen — und + * genau das ist am 31.08.2026 passiert: + * + * S3_ENDPOINT=https://fsn1.your-objectstorage.com + * + * Die Zeile war als Beispiel gedacht. Sie stand aber aktiv in der `.env`, + * und damit sprach die Anwendung monatelang mit einem fremden Anbieter + * statt mit dem Objektspeicher nebenan. Jede Ablage scheiterte mit + * `NoSuchBucket`, und die Zugangs-ID des lokalen Speichers ging bei jedem + * Versuch nach draußen. + * + * Verschärfend: Die `docker-compose.yml` hatte längst einen brauchbaren + * Standard (`${S3_ENDPOINT:-http://minio:9000}`). Der greift aber nur bei + * leerer Variable — die Beispieldatei hebelte ihn aus. Der Mechanismus, + * der den Fehler verhindern sollte, war durch die Beispieldatei + * wirkungslos. + * + * Diese Tests halten fest: Ein Wert steht in der Beispieldatei nur, wenn + * er für die mitgelieferte Zusammenstellung auch richtig ist. + */ + +const beispiel = readFileSync('.env.example', 'utf8') + +/** Alle aktiven Zuweisungen - Kommentarzeilen zählen nicht. */ +function zuweisungen(): [string, string][] { + return beispiel + .split('\n') + .map((zeile) => zeile.trim()) + .filter((zeile) => zeile !== '' && !zeile.startsWith('#')) + .map((zeile) => { + const stelle = zeile.indexOf('=') + return [zeile.slice(0, stelle), zeile.slice(stelle + 1)] as [string, string] + }) +} + +/** + * Rechner, die zur mitgelieferten Zusammenstellung gehören. Alles andere + * ist ein fremder Host und hat in der Beispieldatei nichts verloren. + */ +const EIGENE_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0', 'minio', 'db'] + +describe('Beispielkonfiguration', () => { + it('zeigt mit keinem aktiven Wert auf einen fremden Host', () => { + const fremde = zuweisungen() + .filter(([, wert]) => /https?:\/\//.test(wert)) + .filter(([, wert]) => { + const treffer = wert.match(/https?:\/\/([^:/"']+)/) + return treffer ? !EIGENE_HOSTS.includes(treffer[1]!) : false + }) + .map(([name, wert]) => `${name}=${wert}`) + + expect( + fremde, + 'Diese Zeilen zeigen nach draußen. Wer die Datei kopiert und die ' + + 'Zeile übersieht, schickt seine Daten an einen fremden Rechner.', + ).toEqual([]) + }) + + it('lässt S3_ENDPOINT auskommentiert, damit der Compose-Standard greift', () => { + const aktiv = zuweisungen().some(([name]) => name === 'S3_ENDPOINT') + + expect( + aktiv, + 'Mit gesetztem S3_ENDPOINT greift ${S3_ENDPOINT:-http://minio:9000} ' + + 'aus der docker-compose.yml nicht mehr.', + ).toBe(false) + }) + + it('erklärt trotzdem, wann man S3_ENDPOINT braucht', () => { + // Auskommentieren allein hilft nicht - dann sucht jemand mit externem + // Speicher vergeblich nach der Einstellung. + expect(beispiel).toMatch(/#\s*S3_ENDPOINT=/) + expect(beispiel).toMatch(/EXTERNER Objektspeicher/) + }) + + it('enthält keine persönliche E-Mail-Adresse', () => { + // Stand einmal drin: die Adresse des Betreibers als Vorgabe fuer den + // Seed. In einer mitgelieferten Beispieldatei hat sie nichts zu suchen. + const adressen = zuweisungen() + // Verbindungszeichenfolgen tragen ein "@" vor dem Rechnernamen - + // das ist keine E-Mail-Adresse. + .filter(([, wert]) => !wert.includes('://')) + .filter(([, wert]) => /[\w.+-]+@[\w-]+\.[a-z]{2,}/i.test(wert)) + .filter(([, wert]) => !/example\.(de|com|org)/.test(wert)) + .map(([name, wert]) => `${name}=${wert}`) + + expect(adressen).toEqual([]) + }) + + it('findet überhaupt Zuweisungen - sonst prüft der Test nichts', () => { + expect(zuweisungen().length).toBeGreaterThan(15) + }) +})