First-run setup wizard: API #80
Labels
No Label
area:auth
area:docs
area:export
area:ops
area:storage
area:supply-chain
auth
backend
blocked
collab
deployment
docs
effort:L
effort:M
effort:S
frontend
plugins
qa
vs-nfd
vs-nfd:blocker
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: stwaidele/dorfteich#80
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Kickoff decision: self-hosters configure their instance in the browser instead of editing .env — the wizard runs exactly once against an empty database.
Scope
Implement setup mode: when no Site Admin exists, all routes except
/setup/*return the setup-pending state; endpoints for the wizard steps — create Site Admin account, instance name + default locale, SMTP configuration (with a live 'send test mail' check; secret written to the env-backed secret store per security.md, not a DB row — implement that store), registration mode; completing the wizard locks it permanently; all values pre-seedable via env for automated deploys (Test/Int pipelines keep working unattended).Acceptance criteria
Technical notes
Dependencies
Depends on #13, #19.
Size: ~1.5 days
Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add
deanden), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.Umgesetzt in
f0a82ba+ Bootfix224ae3e(Pipeline grün, alle 8 Kontexte inkl. Smoke auf Test; Test+Int readyz voll ok,secrets-Volumes provisioniert).Setup-Mode & Gate
apps/api/src/setup/-Modul. Solangesetup.completedAt(instance_settings) fehlt, beantwortet ein globalerSetupGuardjede nicht ausgenommene Route mit 503setup_required— auch anonym (das Modul ist bewusst VOR dem AuthModule registriert, damit 503 vor 401 gewinnt). Ausgenommen:/setup/*,healthz/readyz(Deploys/Monitoring) undauth/login|logout|me(Recovery, wenn der Wizard-Admin mitten im Wizard das Cookie verliert).GET /setupbleibt immer lesbar ({status, adminCreated, smtpConfigured}) — darauf routet die #81-UI.Wizard-Schritte (
POST /setup/…)admin: legt den Site Admin an (aktiv + E-Mail verifiziert + Personal Pond wie beim Double-Opt-in) und setzt direkt das Session-Cookie; alle Folgeschritte verlangen diese Site-Admin-Session (kein Hijacking durch einen zweiten Besucher). Zweiter Admin → 409setup_admin_exists. Rate-limitiert.instance(Name + Default-Locale) undregistration(open/closed) schreiben direkt in instance_settings.smtp: Live-Test zuerst (verify + echte Testmail an den Admin, neuessmtpTest-Mail-Template de+en); Fehler → 400smtp_test_failedmit Transport-Fehlertext indetails.smtp(AC „actionable detail"), es wird nichts persistiert. Überspringen erlaubt — Konsequenz: keine Signup-/Reset-Mails, bis SMTP konfiguriert ist.complete: setztsetup.completedAt→ alle Schritte dauerhaft 410setup_locked, auch nach Neustart; der Admin-Settings-PATCH kann den Marker nicht anfassen (interner Key, 400).Env-backed Secret Store (security.md §Secrets) — neu implementiert
apps/api/src/config/secret-store.ts+ Service: mode-600-dotenv-Datei (SECRETS_FILE, Default/data/secrets/secrets.envim Image, neuessecrets-Volume in der Compose), atomarer Write (staging+rename). Der Wizard schreibt die SMTP-Werte dorthin — nie in die DB..envmöglich); Leerstrings zählen als ungesetzt (die Compose reicht${SMTP_HOST:-}als""durch). Zod-Defaults füllen den Rest.SmtpConfigService: Mail-Transport wird lazy aus Env+Store aufgelöst und nach dem Wizard-Save perrefresh()invalidiert → Mails fließen ohne Neustart über das neue Relay.Pre-Seeding & Bestandsinstanzen
SETUP_ADMIN_USERNAME/EMAIL/PASSWORD(+ optionalSETUP_ADMIN_DISPLAY_NAME,SETUP_INSTANCE_NAME,SETUP_DEFAULT_LOCALE,SETUP_REGISTRATION_MODE) in der.envschließen den kompletten Wizard beim Boot ab — automatisierte Deploys sehen ihn nie. In.env.example+ Compose dokumentiert/durchgereicht.20260711100000: Instanzen, die schon einen Site Admin haben (Test/Int!), bekommen den Marker beim erstenmigrate deploy→ kein Wizard, Lock aktiv. Seed + vitest-global-setup setzen den Marker für Fixture-DBs. Pipeline-Beweis auf Test: der CD-Deploy dieses Commits lief unbeaufsichtigt durch, Smoke grün — die Stage hat den Wizard nie gesehen.Tests
secret-store.test.ts(Parse/Serialize/Overlay/atomarer Write/0600).setup.e2e.db.test.ts(10 Tests): provisioniert sich eine eigene frische Datenbank (CREATE DATABASE +prisma migrate deploy— validiert nebenbei die Backfill-Migration auf jungfräulichem Schema) und deckt alle vier ACs ab: fresh DB → Wizard required + 503-Gate; SMTP-Fail blockt mit Detail / Success gegen einen minimalen echten SMTP-Fake-Server (verify + Delivery über echten Socket) und persistiert in den Store (0600); complete → Instanz nutzbar + alle Schritte 410, auch mit frischer App-Instanz (Restart); Env-Preseed → komplett + gesperrt + Login funktioniert.Stage-Provisioning: Stage-Composes auf ONE ersetzt (Backup
*.bak-pre80) — neuessecrets-Volume +SECRETS_FILE+SETUP_*-Passthrough; SMTP-Defaults der Compose sind jetzt leer stattlocalhost(sonst hätte die Container-Env den Store immer überstimmt — Stage-.envs setzen SMTP explizit, Verhalten dort unverändert).Fallen für Folge-Sessions
f0a82ba):main.tsparsteprocess.envroh — die Compose reicht optionale Variablen als""durch (SETUP_DEFAULT_LOCALE: ${…:-}), der Enum-Parse crashte den api-Boot in der Schleife. Fix224ae3e: main.ts und AppConfig teilen sichloadApiEnv()(Store-Overlay + Leerstring-Drop) + Regressionstest. Merke: jede neue optionale Enum-Env-Variable, die die Compose durchreicht, geht durch loadApiEnv, nie durch rohes parseEnv.InstanceSettingsServicecached auchnull-Werte → der Setup-State liest den Marker direkt via Prisma (sonst friert eine Anfrage zwischen api-Boot und Seed den Pending-Zustand ein).Prisma.JsonNullbeim Upsert.app.module.ts.