41eb722369
- Invite flow: admin can invite users by email with role selection; accept-invite page sets password and creates the account; 72-hour token expiry; E2E tests - User deactivate/reactivate/delete: new tRPC procedures + UI buttons; deactivation revokes all active sessions immediately; delete cascades vacation/broadcast records; isActive field added via migration 20260402000000_user_isactive - Auth: block login for inactive users with audit entry - Favicon: SVG favicon + ICO/PNG fallbacks (16, 32, 180, 192, 512px); manifest updated - Dashboard: GridLayout dynamic-import loading skeleton prevents blank dark area on first login before react-grid-layout chunk is cached - Admin users: remove max-w-5xl constraint so table uses full page width - Dev: docker container restart workflow documented in LEARNINGS.md; Prisma generate must run inside the container after schema changes (named node_modules volume) Co-Authored-By: claude-flow <ruv@ruv.net>
95 lines
4.8 KiB
Markdown
95 lines
4.8 KiB
Markdown
# Plan: App Base URL — configurable via env, no localhost fallback in production
|
|
|
|
Stand: 2026-04-02
|
|
|
|
---
|
|
|
|
## Anforderungsanalyse
|
|
|
|
Email-Links (Invite, Password Reset) werden aktuell mit `process.env["NEXTAUTH_URL"] ?? "http://localhost:3100"` gebaut. Das ist korrekt, wenn `NEXTAUTH_URL` gesetzt ist. Das Problem: wenn der Wert fehlt oder leer ist, enthalten Produktions-E-Mails localhost-Links. Außerdem gibt es keine `.env.example`, die dokumentiert, welche Variablen gesetzt werden müssen.
|
|
|
|
**Was gebaut wird:**
|
|
1. Zentrale `getAppBaseUrl()` Funktion in `packages/api` — liest `NEXTAUTH_URL`, wirft in production einen Fehler wenn nicht gesetzt, fällt in dev auf localhost zurück.
|
|
2. Beide Router (`invite.ts`, `auth.ts`) verwenden diese Funktion statt duplizierter Inline-Logik.
|
|
3. `.env.example` mit allen benötigten Variablen.
|
|
4. Health-Route zeigt ob `NEXTAUTH_URL` konfiguriert ist.
|
|
|
|
**Betroffene Pakete:** `packages/api`, `apps/web`
|
|
|
|
**Audit-Ergebnis — intentionale localhost-Referenzen (NICHT ändern):**
|
|
- `apps/web/e2e/dev-system/` — alle E2E-Helfer, Specs, global-setup.ts
|
|
- `apps/web/playwright.dev.config.ts`
|
|
- `apps/web/src/middleware.test.ts`
|
|
- `.github/workflows/deploy-test.yml`
|
|
|
|
---
|
|
|
|
## Betroffene Pakete & Dateien
|
|
|
|
| Paket | Datei | Art |
|
|
|-------|-------|-----|
|
|
| `packages/api` | `src/lib/app-base-url.ts` | create |
|
|
| `packages/api` | `src/router/invite.ts` | edit |
|
|
| `packages/api` | `src/router/auth.ts` | edit |
|
|
| `apps/web` | `src/app/api/health/route.ts` | edit |
|
|
| root | `.env.example` | create |
|
|
|
|
---
|
|
|
|
## Task-Liste
|
|
|
|
- [ ] **Task 1:** `getAppBaseUrl()` in `packages/api/src/lib/app-base-url.ts` erstellen.
|
|
- Liest `process.env["NEXTAUTH_URL"]` (trimmed).
|
|
- Wenn gesetzt und nicht leer → gibt den Wert zurück (trailing slash entfernen).
|
|
- Wenn leer/fehlend **und** `NODE_ENV === "production"` → wirft `Error("NEXTAUTH_URL must be set in production — email links will be broken")`.
|
|
- Sonst (development/test) → gibt `"http://localhost:3100"` zurück und loggt einmalig eine Warnung.
|
|
- → Datei: `packages/api/src/lib/app-base-url.ts`
|
|
|
|
- [ ] **Task 2:** `invite.ts` auf `getAppBaseUrl()` umstellen.
|
|
- Ersetze `const baseUrl = process.env["NEXTAUTH_URL"] ?? "http://localhost:3100";` durch `const baseUrl = getAppBaseUrl();`
|
|
- → Datei: `packages/api/src/router/invite.ts` Zeile 53
|
|
|
|
- [ ] **Task 3:** `auth.ts` auf `getAppBaseUrl()` umstellen.
|
|
- Ersetze `const baseUrl = process.env["NEXTAUTH_URL"] ?? "http://localhost:3100";` durch `const baseUrl = getAppBaseUrl();`
|
|
- → Datei: `packages/api/src/router/auth.ts` Zeile 50
|
|
|
|
- [ ] **Task 4:** `.env.example` anlegen mit allen required und optionalen Variablen.
|
|
- Sections: App, Auth, Database, Redis, SMTP, AI, Dev-Tools (pgAdmin)
|
|
- Jede Variable: Kommentar (required/optional, Beschreibung), Beispielwert.
|
|
- `NEXTAUTH_URL` als REQUIRED mit Hinweis "must be the public URL (e.g. https://capakraken.example.com) — used in email links; do not use localhost in production"
|
|
- → Datei: `.env.example`
|
|
|
|
- [ ] **Task 5:** Health-Route um `baseUrl`-Check erweitern.
|
|
- Liest `NEXTAUTH_URL` direkt (kein `getAppBaseUrl()` — soll nie werfen, nur reporten).
|
|
- Fügt `"baseUrl": { "configured": bool, "isLocalhost": bool }` zum JSON-Response hinzu.
|
|
- `configured: false` wenn Var fehlt/leer; `isLocalhost: true` wenn Wert mit `http://localhost` beginnt.
|
|
- → Datei: `apps/web/src/app/api/health/route.ts`
|
|
|
|
---
|
|
|
|
## Abhängigkeiten
|
|
|
|
- Task 2 **und** Task 3 benötigen Task 1 (Funktion muss existieren).
|
|
- Task 2 und Task 3 können **parallel** ausgeführt werden (unterschiedliche Dateien).
|
|
- Task 4 und Task 5 sind **unabhängig** von allen anderen und können parallel ausgeführt werden.
|
|
|
|
---
|
|
|
|
## Akzeptanzkriterien
|
|
|
|
- [ ] `pnpm test:unit` läuft grün
|
|
- [ ] `pnpm --filter @capakraken/web exec tsc --noEmit` — keine neuen TS-Errors
|
|
- [ ] `pnpm test:e2e:email` — alle 3 E2E-Tests bestehen weiterhin
|
|
- [ ] Wenn `NEXTAUTH_URL=https://capakraken.hartmut-noerenberg.com`: E-Mail-Links enthalten diese Domain
|
|
- [ ] Wenn `NEXTAUTH_URL` fehlt und `NODE_ENV=production`: `createInvite` / `requestPasswordReset` werfen einen klaren Fehler beim Link-Bau
|
|
- [ ] `GET /api/health` liefert `baseUrl.configured: true/false` und `baseUrl.isLocalhost: bool`
|
|
- [ ] `.env.example` existiert und dokumentiert alle Pflichtfelder
|
|
|
|
---
|
|
|
|
## Risiken & offene Fragen
|
|
|
|
- **Unit-Tests für Router:** Bestehende Tests für `invite.ts` und `auth.ts` müssen `NEXTAUTH_URL` in der Testumgebung gesetzt haben (oder `NODE_ENV=test` → Dev-Fallback greift). Vorhandene Tests prüfen, ob `process.env["NEXTAUTH_URL"]` dort gesetzt wird.
|
|
- **`docker-compose.prod.yml`** delegiert ENV-Vars an `.env.production` (gitignored). Das `.env.example` deckt ab, was dort stehen muss — kein Code-Change nötig.
|
|
- **Trailing Slash:** NEXTAUTH_URL könnte mit oder ohne `/` enden. `getAppBaseUrl()` sollte trailing slashes normalisieren.
|