Compare commits

8 Commits

Author SHA1 Message Date
Claude
fb4747cfeb auth: Diagnose-Log bei abgelehntem Authentik-Login
Loggt bei AccessDenied die empfangenen Gruppen + erwartete Admin-Gruppe; bei
leerer Gruppenliste Hinweis auf fehlendes 'groups'-Scope-Mapping. Erleichtert
die Diagnose der Authentik-Gruppensteuerung im Betrieb.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:47:09 +02:00
Claude
0634d8c236 fix(csp): Inline-Skripte in Prod erlauben (Next.js-Hydration)
Prod-CSP hatte script-src 'self' ohne nonce/hash -> Next.js' Inline-Bootstrap-/
Hydration-Skripte wurden vom Browser blockiert (Login-Seite ohne JS, 'Connection
closed'). script-src um 'unsafe-inline' ergänzt (KEIN 'unsafe-eval' in Prod);
übrige CSP (default-src 'self', object-src none, frame-ancestors none, base-uri
self, form-action self) bleibt strikt. Stärkere nonce-basierte CSP via Middleware
als Hardening-Option offen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:42:13 +02:00
Claude
987b8c9c8f build: public/ anlegen (.gitkeep) — Dockerfile COPY /app/public
Das Projekt hatte kein public/-Verzeichnis; der Runner-Stage-COPY
(COPY /app/public ./public) brach mit '/app/public: not found' ab.
Leeres, getracktes public/ behebt das (Next.js bedient es problemlos).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:37:20 +02:00
Claude
5d4afb5936 build: Build-Zeit-Platzhalter-Env in builder-Stage (next build)
next build evaluiert beim 'Collecting page data' Server-Routen (u. a.
/api/auth/[...nextauth]); src/lib/env.ts validiert beim Import (Fail-Fast) und
brach ohne gesetzte Variablen ab. Platzhalter-Env (erfüllt das Zod-Schema) nur
für den Build ergänzt — Server-env wird nicht ins Bundle inlined, die builder-
Stage landet nicht im Runtime-Image; echte Werte kommen zur Laufzeit aus Compose.

Lokal verifiziert: next build läuft mit den Platzhaltern sauber durch (alle Routen).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:34:06 +02:00
Claude
4863eadcce build: öffentliche npm-Registry erzwingen (Fix für npm-ci/Apple-Mirror)
Wie im feuerwehr_dashboard:
- .npmrc mit registry=https://registry.npmjs.org/ (committet)
- Dockerfile deps-Stage: npm@11 pinnen + .npmrc kopieren; statt 'npm ci' nun
  'npm install' und den committeten Lockfile NICHT verwenden (er wurde gegen einen
  internen Mirror erzeugt -> apple-Artifactory-URLs -> auf dem Server nicht erreichbar).

Damit baut 'make up-core'/'make deploy' auf dem Server gegen die öffentliche Registry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:16:38 +02:00
Claude
f933ecc19e make: up-core/deploy-core — App + Postgres ohne Geo starten
Ermöglicht Deploy ohne OSRM/Nominatim (deren Preprocessing/Import viel RAM
braucht; Austria-Extrakt OOM-te beim osrm-extract). App läuft mit
Haversine-Fallback; Geo später via 'make data' + 'make up' nachziehen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:07:53 +02:00
Claude
38021cbc51 db: DATABASE_URL-Schema auf postgresql:// (wie feuerwehr_dashboard)
Dashboard nutzt das Format postgresql://USER:PASS@HOST:PORT/DB. Angeglichen in
docker-compose.yml, .env.example (+ Host-Hinweis lokal=localhost / Container=postgres),
vitest.setup.ts und env.test.ts. Funktional identisch (pg/Drizzle akzeptieren beide),
aber konsistent mit dem bestehenden Setup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 12:47:46 +02:00
Claude
f71cf51eb4 deploy: Traefik-Setup an feuerwehr_dashboard angleichen
Abgeglichen mit ~/work/feuerwehr_dashboard/docker-compose.yml:
- externes Traefik-Netz heißt 'frontend' (external: true), nicht 'traefik'
- explizite Router->Service-Bindung (routers.floriannetz.service=floriannetz)
- entrypoints=websecure, tls + certresolver=letsencrypt, port 3000
- traefik.docker.network -> frontend; AUTHENTIK_ADMIN_GROUP an App durchgereicht
- internes Netz als Bridge (statt internal:true): Postgres/Geo ohne Host-Ports,
  aber App hat Egress für Authentik-OIDC
- APP_HOST-Default florian.feuerwehr-rems.at; TRAEFIK_NETWORK-Default frontend
- Doku (deployment-traefik.md) + Makefile-Kommentare angepasst

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 12:39:42 +02:00
11 changed files with 113 additions and 48 deletions

View File

@@ -4,7 +4,10 @@
NODE_ENV=development
# Datenbank (Postgres)
DATABASE_URL=postgres://floriannetz:floriannetz@localhost:5432/floriannetz
# Datenbank (Postgres). Format: postgresql://USER:PASSWORT@HOST:PORT/DB
# Lokal (Host -> Docker-Postgres via docker-compose.dev.yml): HOST=localhost.
# Im Container setzt docker-compose.yml HOST automatisch auf den Service "postgres".
DATABASE_URL=postgresql://floriannetz:floriannetz@localhost:5432/floriannetz
# Auth.js / NextAuth
# AUTH_SECRET muss >= 32 Zeichen sein (z. B. `openssl rand -base64 32`)
@@ -31,11 +34,13 @@ HAVERSINE_KMH=50
# Deployment / externes Traefik
# APP_HOST ist der öffentliche Hostname (Traefik-Routing + AUTH_URL-Basis).
# In Produktion: AUTH_URL=https://${APP_HOST} und AUTH_TRUST_HOST=true setzen.
APP_HOST=floriannetz.example.at
APP_HOST=florian.feuerwehr-rems.at
# Traefik-Zertifikatsauflöser (muss in der externen Traefik-Instanz definiert sein).
TRAEFIK_CERTRESOLVER=letsencrypt
# Name des externen, von Traefik verwalteten Docker-Netzes.
TRAEFIK_NETWORK=traefik
# Name des externen, von Traefik verwalteten Docker-Netzes
# (im feuerwehr_dashboard heißt es "frontend"). Muss existieren:
# docker network create frontend
TRAEFIK_NETWORK=frontend
# Optionaler Katalog-Seed beim Container-Start (idempotent).
RUN_SEED=false
# Postgres-Zugangsdaten für den Compose-Postgres-Service.

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
registry=https://registry.npmjs.org/

View File

@@ -9,14 +9,33 @@ ARG NODE_VERSION=22
# --- deps: Produktions- und Build-Abhängigkeiten installieren -----------------
FROM node:${NODE_VERSION}-alpine AS deps
WORKDIR /app
# Nur Manifeste kopieren -> Layer-Cache bleibt stabil, solange sich Deps nicht ändern.
COPY package.json package-lock.json ./
RUN npm ci
# node:alpine bündelt npm 10, das bei plattformfremden optionalen Transitiv-Deps
# (z. B. @node-rs/argon2 -> *-wasm32-wasi / @emnapi) strenger ist. npm 11 wie im
# feuerwehr_dashboard verwenden.
RUN npm install -g npm@11
# .npmrc erzwingt das ÖFFENTLICHE npm-Registry. Der committete Lockfile wurde
# gegen einen internen Mirror erzeugt (resolved-URLs zeigen dorthin, daher der
# npm-ci-Fehler) und wird im Build bewusst NICHT verwendet — Auflösung frisch aus
# der öffentlichen Registry (gleiches Vorgehen wie feuerwehr_dashboard/frontend).
COPY package.json .npmrc ./
RUN npm install --no-audit --no-fund
# --- builder: Next.js im Standalone-Modus bauen -------------------------------
FROM node:${NODE_VERSION}-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
# Build-Zeit-Platzhalter: src/lib/env.ts validiert beim Import (Fail-Fast).
# `next build` evaluiert beim "Collecting page data" die Server-Routen (u. a.
# /api/auth/[...nextauth]) -> ohne gesetzte Variablen bricht der Import ab.
# Diese Werte sind NUR für den Build (erfüllen das Zod-Schema); Server-env wird
# NICHT ins Bundle inlined und die builder-Stage landet NICHT im Runtime-Image.
# Echte Werte kommen zur Laufzeit aus docker-compose.
ENV DATABASE_URL=postgresql://build:build@localhost:5432/build \
AUTH_SECRET=build_only_placeholder_secret_min_32_chars_long \
AUTH_URL=https://build.invalid \
AUTHENTIK_ISSUER=https://build.invalid/application/o/floriannetz/ \
AUTHENTIK_CLIENT_ID=build \
AUTHENTIK_CLIENT_SECRET=build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# next.config.ts setzt output:"standalone" -> erzeugt .next/standalone/server.js.

View File

@@ -9,7 +9,7 @@
# make build-app migrate
#
# Voll-Deploy hinter externem Traefik (Docker):
# docker network create traefik # einmalig
# docker network create frontend # einmalig (externes Traefik-Netz)
# make deploy
#
# `make help` listet alle Ziele.
@@ -116,14 +116,17 @@ setup: install env db-up db-wait migrate seed-all ## Komplettes lokales Setup vo
@echo "✓ Setup fertig. Login-Admin via 'make seed-auth' angelegt. Weiter mit: make dev"
# --- Deployment (externes Traefik; braucht Docker) -----------------------
# Externes Netz muss existieren: docker network create traefik
.PHONY: build up down logs ps deploy migrate-stack data config
# Externes Netz muss existieren: docker network create frontend
.PHONY: build up up-core down logs ps deploy deploy-core migrate-stack data config
build: ## App-Image bauen (Next.js standalone, non-root)
$(COMPOSE) build app
up: ## Stack starten (App + Postgres + Geo) hinter Traefik
$(COMPOSE) up -d
up-core: ## Nur App + Postgres starten (OHNE Geo/OSRM/Nominatim) — wenig RAM nötig
$(COMPOSE) up -d --build app postgres
down: ## Stack stoppen
$(COMPOSE) down
@@ -133,7 +136,10 @@ logs: ## App-Logs folgen
ps: ## Status der Stack-Container
$(COMPOSE) ps
deploy: build up ## build + up (Standard-Deploy; migrate läuft via Entrypoint automatisch)
deploy: build up ## build + up (voller Stack inkl. Geo; migrate via Entrypoint)
deploy-core: ## build + up-core (App + Postgres, ohne Geo; Geo später per 'make data' + 'make up')
$(MAKE) up-core
migrate-stack: ## Migrationen im laufenden App-Container ausführen (manuell)
$(COMPOSE) exec app node docker/migrate.mjs

View File

@@ -1,18 +1,22 @@
# FlorianNetz — Basis-Compose hinter EXTERNEM Traefik.
#
# Ausgerichtet auf das bestehende Setup von feuerwehr_dashboard:
# - externes, von Traefik verwaltetes Netz heißt "frontend" (external: true)
# - Router: entrypoints=websecure, tls + certresolver=letsencrypt
# - explizite Router->Service-Bindung, loadbalancer.server.port=3000
# - traefik.docker.network = das externe "frontend"-Netz
#
# Es gibt bewusst KEINEN eigenen Proxy-/Traefik-Service: Routing/TLS übernimmt
# eine separat betriebene Traefik-Instanz, die am externen Netz "${TRAEFIK_NETWORK}"
# (Default: traefik) lauscht. Dieses Netz muss bereits existieren:
# docker network create traefik
# die separat betriebene Traefik-Instanz am Netz "${TRAEFIK_NETWORK}" (Default:
# frontend). Dieses Netz muss bereits existieren:
# docker network create frontend
#
# Geo-Dienste (osrm, nominatim) sind hier mit ihren Laufzeit-Verträgen definiert;
# das schwergewichtige Daten-Preprocessing/Volume kommt aus docker-compose.geo.yml
# (siehe scripts/prepare-osm-data.sh / infra/geo).
# Postgres/Geo liegen am internen Bridge-Netz (keine veröffentlichten Ports,
# also nicht öffentlich erreichbar) — der App-Container hat über dieses Netz
# zugleich Egress (z. B. für den Authentik-OIDC-Token-Austausch).
#
# Start:
# docker compose --env-file .env up -d
# Lokal ohne Traefik/TLS:
# docker compose -f docker-compose.yml -f docker-compose.override.yml up -d
# Start: docker compose --env-file .env up -d
# Lokal: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
services:
app:
@@ -24,7 +28,7 @@ services:
condition: service_healthy
environment:
NODE_ENV: production
DATABASE_URL: postgres://${POSTGRES_USER:-floriannetz}:${POSTGRES_PASSWORD:-floriannetz}@postgres:5432/${POSTGRES_DB:-floriannetz}
DATABASE_URL: postgresql://${POSTGRES_USER:-floriannetz}:${POSTGRES_PASSWORD:-floriannetz}@postgres:5432/${POSTGRES_DB:-floriannetz}
# Forwarded-Header + sichere Cookies hinter Traefik.
AUTH_TRUST_HOST: "true"
AUTH_URL: https://${APP_HOST}
@@ -32,13 +36,14 @@ services:
AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER}
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID}
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET}
AUTHENTIK_ADMIN_GROUP: ${AUTHENTIK_ADMIN_GROUP:-floriannetz-admins}
OSRM_URL: http://osrm:5000
NOMINATIM_URL: http://nominatim:8080
GEO_HTTP_TIMEOUT_MS: ${GEO_HTTP_TIMEOUT_MS:-4000}
HAVERSINE_KMH: ${HAVERSINE_KMH:-50}
RUN_SEED: ${RUN_SEED:-false}
networks:
- traefik
- frontend
- internal
healthcheck:
test:
@@ -51,11 +56,12 @@ services:
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.docker.network=${TRAEFIK_NETWORK:-traefik}"
- "traefik.http.routers.floriannetz.rule=Host(`${APP_HOST}`)"
- "traefik.docker.network=${TRAEFIK_NETWORK:-frontend}"
- "traefik.http.routers.floriannetz.entrypoints=websecure"
- "traefik.http.routers.floriannetz.rule=Host(`${APP_HOST}`)"
- "traefik.http.routers.floriannetz.tls=true"
- "traefik.http.routers.floriannetz.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt}"
- "traefik.http.routers.floriannetz.service=floriannetz"
- "traefik.http.services.floriannetz.loadbalancer.server.port=3000"
# Security-Header-Middleware (zusätzlich zu next.config.ts; defense-in-depth).
- "traefik.http.routers.floriannetz.middlewares=floriannetz-sechdrs"
@@ -137,10 +143,12 @@ volumes:
nominatim-data:
networks:
# Externes, von der separaten Traefik-Instanz verwaltetes Netz.
traefik:
# Externes, von der separaten Traefik-Instanz verwaltetes Netz (wie im
# feuerwehr_dashboard "frontend"). Muss existieren: docker network create frontend
frontend:
external: true
name: ${TRAEFIK_NETWORK:-traefik}
# Internes Netz: Postgres/Geo sind nur app-intern erreichbar, nicht öffentlich.
name: ${TRAEFIK_NETWORK:-frontend}
# Internes Bridge-Netz: Postgres/Geo ohne veröffentlichte Ports (nicht
# öffentlich), zugleich Egress für den App-Container (Authentik-OIDC).
internal:
internal: true
driver: bridge

View File

@@ -18,17 +18,20 @@ Der Stack besteht aus genau vier Services (kein Proxy):
Netze:
- **`traefik`** — externes, von Traefik verwaltetes Netz (`external: true`,
Name aus `TRAEFIK_NETWORK`, Default `traefik`). Nur `app` hängt daran.
- **`internal`** — internes Netz (`internal: true`); Postgres und die Geo-Dienste
sind ausschließlich für die App erreichbar, nie öffentlich.
- **`frontend`** — externes, von Traefik verwaltetes Netz (`external: true`,
Name aus `TRAEFIK_NETWORK`, Default `frontend` — wie im feuerwehr_dashboard).
Nur `app` hängt daran (Proxy↔App).
- **`internal`** — internes Bridge-Netz; Postgres und die Geo-Dienste haben
**keine veröffentlichten Ports** (nicht öffentlich erreichbar). Über dieses
Netz hat der App-Container zugleich **Egress** (z. B. für den
Authentik-OIDC-Token-Austausch).
## Voraussetzungen
Das externe Traefik-Netz muss existieren, bevor der Stack startet:
```bash
docker network create traefik
docker network create frontend
```
Die externe Traefik-Instanz muss:
@@ -36,7 +39,7 @@ Die externe Traefik-Instanz muss:
- einen Entrypoint `websecure` (Port 443) bereitstellen,
- einen Zertifikatsauflöser anbieten, dessen Name `TRAEFIK_CERTRESOLVER`
entspricht (Default `letsencrypt`),
- am Netz `traefik` lauschen (`providers.docker` mit `exposedByDefault=false`).
- am Netz `frontend` lauschen (`providers.docker` mit `exposedByDefault=false`).
Die App-Labels in `docker-compose.yml` setzen Router (`Host(\`${APP_HOST}\`)`,
`entrypoints=websecure`, `tls.certresolver`), Service-Port `3000` und eine
@@ -48,16 +51,17 @@ Vollständiger Vertrag in `.env.example`. Für den Betrieb hinter Traefik zwinge
| Variable | Beispiel / Hinweis |
| ------------------------- | ---------------------------------------------------- |
| `APP_HOST` | öffentlicher Hostname, z. B. `floriannetz.example.at` |
| `APP_HOST` | öffentlicher Hostname, z. B. `florian.feuerwehr-rems.at` |
| `AUTH_URL` | `https://${APP_HOST}` — Basis für Callback + Cookies |
| `AUTH_TRUST_HOST` | `true` — Auth.js vertraut den Forwarded-Headern |
| `AUTH_SECRET` | >= 32 Zeichen (`openssl rand -base64 32`) |
| `AUTHENTIK_ISSUER` | OIDC-Issuer-URL der Authentik-Anwendung |
| `AUTHENTIK_CLIENT_ID` | Client-ID der Authentik-Anwendung |
| `AUTHENTIK_CLIENT_SECRET` | Client-Secret der Authentik-Anwendung |
| `AUTHENTIK_ADMIN_GROUP` | Authentik-Gruppe → platform_admin (Default `floriannetz-admins`; s. authentik-setup.md) |
| `DATABASE_URL` | wird in Compose aus `POSTGRES_*` zusammengesetzt |
| `TRAEFIK_CERTRESOLVER` | Name des Traefik-Zertifikatsauflösers |
| `TRAEFIK_NETWORK` | Name des externen Traefik-Netzes (Default `traefik`) |
| `TRAEFIK_NETWORK` | Name des externen Traefik-Netzes (Default `frontend`) |
## Forwarded-Header & sichere Cookies
@@ -86,6 +90,10 @@ https://${APP_HOST}/api/auth/callback/authentik
Der Pfad `callback/authentik` entspricht dem NextAuth-Provider-Namen. Bei lokaler
Entwicklung zusätzlich `http://localhost:3000/api/auth/callback/authentik`.
**Admin-Zugang über Gruppe:** Dem Provider muss das `groups`-Scope-Mapping
zugewiesen sein, und es muss die Gruppe aus `AUTHENTIK_ADMIN_GROUP` existieren —
nur deren Mitglieder werden `platform_admin`. Details: `authentik-setup.md`.
## Health-Check & Middleware-Allowlist
`GET /api/health` ist **öffentlich** (anonym `200`, nur Liveness, keine
@@ -107,7 +115,7 @@ App-Healthcheck pingen `http://127.0.0.1:3000/api/health`.
```bash
cp .env.example .env # Werte setzen (APP_HOST, AUTH_*, AUTHENTIK_*, POSTGRES_*)
docker network create traefik # einmalig, falls nicht vorhanden
docker network create frontend # einmalig, falls nicht vorhanden
make data # einmalig: OSRM-Geodaten vorbereiten (groß, dauert)
make deploy # build + up
```

3
public/.gitkeep Normal file
View File

@@ -0,0 +1,3 @@
# Platzhalter, damit das public/-Verzeichnis existiert und vom Docker-Build
# (COPY /app/public ./public) sowie von Next.js (statische Assets) genutzt werden
# kann. Statische Dateien (z. B. favicon.ico, robots.txt) hier ablegen.

View File

@@ -91,9 +91,22 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
async signIn({ user, account, profile }) {
if (account?.provider === "authentik") {
const email = user.email;
if (!email) return false;
if (!email) {
console.warn("[auth] Authentik-Login ohne E-Mail abgelehnt.");
return false;
}
const groups = extractGroups(profile);
if (!isAdminGroupMember(groups, env.AUTHENTIK_ADMIN_GROUP)) return false;
if (!isAdminGroupMember(groups, env.AUTHENTIK_ADMIN_GROUP)) {
console.warn(
`[auth] Authentik-Login abgelehnt: "${email}" ist nicht in Gruppe ` +
`"${env.AUTHENTIK_ADMIN_GROUP}". Erhaltene Gruppen: ${JSON.stringify(groups)}` +
(groups.length
? ""
: " — leer: vermutlich fehlt das 'groups'-Scope-Mapping im " +
"Authentik-Provider (oder der 'groups'-Scope wird nicht angefragt)."),
);
return false;
}
const u = await upsertAuthentikAdmin(email, user.name ?? null);
user.id = u.id;
user.role = u.rolle;

View File

@@ -8,7 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const VALID_ENV = {
NODE_ENV: "test",
DATABASE_URL: "postgres://user:pass@localhost:5432/floriannetz",
DATABASE_URL: "postgresql://user:pass@localhost:5432/floriannetz",
AUTH_SECRET: "x".repeat(32),
AUTH_URL: "http://localhost:3000",
AUTHENTIK_ISSUER: "http://localhost:9000/application/o/floriannetz/",

View File

@@ -2,17 +2,19 @@
* Sicherheits-Header, eingehängt in next.config.ts.
*
* Content-Security-Policy ist der zentrale Querschnitts-Schutz (Implementierungs-
* plan Z.1314): in Produktion strikt mit default-src 'self', frame-ancestors 'none'
* und form-action 'self'. Im Dev-Modus benötigt Next.js (HMR/React-Refresh) eine
* gelockerte script-src/connect-src-Variante ('unsafe-eval' + ws: für den Dev-Socket).
* plan Z.1314): default-src 'self', frame-ancestors 'none', form-action 'self',
* object-src 'none'. script-src erlaubt 'unsafe-inline' (KEIN 'unsafe-eval' in
* Prod), da Next.js (App Router) Inline-Bootstrap-/Hydration-Skripte ohne Nonce
* ausliefert — eine strikte nonce-basierte CSP ginge nur über die Middleware
* (Hardening-Option). Im Dev zusätzlich 'unsafe-eval' + ws: (HMR/React-Refresh).
*/
const isProd = process.env.NODE_ENV === "production";
const CSP = [
"default-src 'self'",
// Dev braucht eval (React Refresh) + inline; Prod bleibt strikt.
// Next.js braucht Inline-Skripte (Bootstrap/Hydration, ohne Nonce); Dev zusätzlich eval.
isProd
? "script-src 'self'"
? "script-src 'self' 'unsafe-inline'"
: "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob:",

View File

@@ -3,7 +3,7 @@
// Es wird KEINE echte DB-Verbindung geöffnet (Pool ist lazy bis zur Query).
const TEST_ENV: Record<string, string> = {
NODE_ENV: "test",
DATABASE_URL: "postgres://test:test@localhost:5432/test",
DATABASE_URL: "postgresql://test:test@localhost:5432/test",
AUTH_SECRET: "test-secret-mindestens-32-zeichen-lang-xxxx",
AUTH_URL: "http://localhost:3000",
AUTH_TRUST_HOST: "true",