Compare commits
8 Commits
f2578cedab
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb4747cfeb | ||
|
|
0634d8c236 | ||
|
|
987b8c9c8f | ||
|
|
5d4afb5936 | ||
|
|
4863eadcce | ||
|
|
f933ecc19e | ||
|
|
38021cbc51 | ||
|
|
f71cf51eb4 |
13
.env.example
13
.env.example
@@ -4,7 +4,10 @@
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
# Datenbank (Postgres)
|
# 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.js / NextAuth
|
||||||
# AUTH_SECRET muss >= 32 Zeichen sein (z. B. `openssl rand -base64 32`)
|
# AUTH_SECRET muss >= 32 Zeichen sein (z. B. `openssl rand -base64 32`)
|
||||||
@@ -31,11 +34,13 @@ HAVERSINE_KMH=50
|
|||||||
# Deployment / externes Traefik
|
# Deployment / externes Traefik
|
||||||
# APP_HOST ist der öffentliche Hostname (Traefik-Routing + AUTH_URL-Basis).
|
# APP_HOST ist der öffentliche Hostname (Traefik-Routing + AUTH_URL-Basis).
|
||||||
# In Produktion: AUTH_URL=https://${APP_HOST} und AUTH_TRUST_HOST=true setzen.
|
# 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-Zertifikatsauflöser (muss in der externen Traefik-Instanz definiert sein).
|
||||||
TRAEFIK_CERTRESOLVER=letsencrypt
|
TRAEFIK_CERTRESOLVER=letsencrypt
|
||||||
# Name des externen, von Traefik verwalteten Docker-Netzes.
|
# Name des externen, von Traefik verwalteten Docker-Netzes
|
||||||
TRAEFIK_NETWORK=traefik
|
# (im feuerwehr_dashboard heißt es "frontend"). Muss existieren:
|
||||||
|
# docker network create frontend
|
||||||
|
TRAEFIK_NETWORK=frontend
|
||||||
# Optionaler Katalog-Seed beim Container-Start (idempotent).
|
# Optionaler Katalog-Seed beim Container-Start (idempotent).
|
||||||
RUN_SEED=false
|
RUN_SEED=false
|
||||||
# Postgres-Zugangsdaten für den Compose-Postgres-Service.
|
# Postgres-Zugangsdaten für den Compose-Postgres-Service.
|
||||||
|
|||||||
25
Dockerfile
25
Dockerfile
@@ -9,14 +9,33 @@ ARG NODE_VERSION=22
|
|||||||
# --- deps: Produktions- und Build-Abhängigkeiten installieren -----------------
|
# --- deps: Produktions- und Build-Abhängigkeiten installieren -----------------
|
||||||
FROM node:${NODE_VERSION}-alpine AS deps
|
FROM node:${NODE_VERSION}-alpine AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# Nur Manifeste kopieren -> Layer-Cache bleibt stabil, solange sich Deps nicht ändern.
|
# node:alpine bündelt npm 10, das bei plattformfremden optionalen Transitiv-Deps
|
||||||
COPY package.json package-lock.json ./
|
# (z. B. @node-rs/argon2 -> *-wasm32-wasi / @emnapi) strenger ist. npm 11 wie im
|
||||||
RUN npm ci
|
# 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 -------------------------------
|
# --- builder: Next.js im Standalone-Modus bauen -------------------------------
|
||||||
FROM node:${NODE_VERSION}-alpine AS builder
|
FROM node:${NODE_VERSION}-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
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 --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
# next.config.ts setzt output:"standalone" -> erzeugt .next/standalone/server.js.
|
# next.config.ts setzt output:"standalone" -> erzeugt .next/standalone/server.js.
|
||||||
|
|||||||
14
Makefile
14
Makefile
@@ -9,7 +9,7 @@
|
|||||||
# make build-app migrate
|
# make build-app migrate
|
||||||
#
|
#
|
||||||
# Voll-Deploy hinter externem Traefik (Docker):
|
# Voll-Deploy hinter externem Traefik (Docker):
|
||||||
# docker network create traefik # einmalig
|
# docker network create frontend # einmalig (externes Traefik-Netz)
|
||||||
# make deploy
|
# make deploy
|
||||||
#
|
#
|
||||||
# `make help` listet alle Ziele.
|
# `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"
|
@echo "✓ Setup fertig. Login-Admin via 'make seed-auth' angelegt. Weiter mit: make dev"
|
||||||
|
|
||||||
# --- Deployment (externes Traefik; braucht Docker) -----------------------
|
# --- Deployment (externes Traefik; braucht Docker) -----------------------
|
||||||
# Externes Netz muss existieren: docker network create traefik
|
# Externes Netz muss existieren: docker network create frontend
|
||||||
.PHONY: build up down logs ps deploy migrate-stack data config
|
.PHONY: build up up-core down logs ps deploy deploy-core migrate-stack data config
|
||||||
build: ## App-Image bauen (Next.js standalone, non-root)
|
build: ## App-Image bauen (Next.js standalone, non-root)
|
||||||
$(COMPOSE) build app
|
$(COMPOSE) build app
|
||||||
|
|
||||||
up: ## Stack starten (App + Postgres + Geo) hinter Traefik
|
up: ## Stack starten (App + Postgres + Geo) hinter Traefik
|
||||||
$(COMPOSE) up -d
|
$(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
|
down: ## Stack stoppen
|
||||||
$(COMPOSE) down
|
$(COMPOSE) down
|
||||||
|
|
||||||
@@ -133,7 +136,10 @@ logs: ## App-Logs folgen
|
|||||||
ps: ## Status der Stack-Container
|
ps: ## Status der Stack-Container
|
||||||
$(COMPOSE) ps
|
$(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)
|
migrate-stack: ## Migrationen im laufenden App-Container ausführen (manuell)
|
||||||
$(COMPOSE) exec app node docker/migrate.mjs
|
$(COMPOSE) exec app node docker/migrate.mjs
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
# FlorianNetz — Basis-Compose hinter EXTERNEM Traefik.
|
# 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
|
# Es gibt bewusst KEINEN eigenen Proxy-/Traefik-Service: Routing/TLS übernimmt
|
||||||
# eine separat betriebene Traefik-Instanz, die am externen Netz "${TRAEFIK_NETWORK}"
|
# die separat betriebene Traefik-Instanz am Netz "${TRAEFIK_NETWORK}" (Default:
|
||||||
# (Default: traefik) lauscht. Dieses Netz muss bereits existieren:
|
# frontend). Dieses Netz muss bereits existieren:
|
||||||
# docker network create traefik
|
# docker network create frontend
|
||||||
#
|
#
|
||||||
# Geo-Dienste (osrm, nominatim) sind hier mit ihren Laufzeit-Verträgen definiert;
|
# Postgres/Geo liegen am internen Bridge-Netz (keine veröffentlichten Ports,
|
||||||
# das schwergewichtige Daten-Preprocessing/Volume kommt aus docker-compose.geo.yml
|
# also nicht öffentlich erreichbar) — der App-Container hat über dieses Netz
|
||||||
# (siehe scripts/prepare-osm-data.sh / infra/geo).
|
# zugleich Egress (z. B. für den Authentik-OIDC-Token-Austausch).
|
||||||
#
|
#
|
||||||
# Start:
|
# Start: docker compose --env-file .env up -d
|
||||||
# docker compose --env-file .env up -d
|
# Lokal: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||||
# Lokal ohne Traefik/TLS:
|
|
||||||
# docker compose -f docker-compose.yml -f docker-compose.override.yml up -d
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
@@ -24,7 +28,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
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.
|
# Forwarded-Header + sichere Cookies hinter Traefik.
|
||||||
AUTH_TRUST_HOST: "true"
|
AUTH_TRUST_HOST: "true"
|
||||||
AUTH_URL: https://${APP_HOST}
|
AUTH_URL: https://${APP_HOST}
|
||||||
@@ -32,13 +36,14 @@ services:
|
|||||||
AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER}
|
AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER}
|
||||||
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID}
|
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID}
|
||||||
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET}
|
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET}
|
||||||
|
AUTHENTIK_ADMIN_GROUP: ${AUTHENTIK_ADMIN_GROUP:-floriannetz-admins}
|
||||||
OSRM_URL: http://osrm:5000
|
OSRM_URL: http://osrm:5000
|
||||||
NOMINATIM_URL: http://nominatim:8080
|
NOMINATIM_URL: http://nominatim:8080
|
||||||
GEO_HTTP_TIMEOUT_MS: ${GEO_HTTP_TIMEOUT_MS:-4000}
|
GEO_HTTP_TIMEOUT_MS: ${GEO_HTTP_TIMEOUT_MS:-4000}
|
||||||
HAVERSINE_KMH: ${HAVERSINE_KMH:-50}
|
HAVERSINE_KMH: ${HAVERSINE_KMH:-50}
|
||||||
RUN_SEED: ${RUN_SEED:-false}
|
RUN_SEED: ${RUN_SEED:-false}
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- frontend
|
||||||
- internal
|
- internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
@@ -51,11 +56,12 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=${TRAEFIK_NETWORK:-traefik}"
|
- "traefik.docker.network=${TRAEFIK_NETWORK:-frontend}"
|
||||||
- "traefik.http.routers.floriannetz.rule=Host(`${APP_HOST}`)"
|
|
||||||
- "traefik.http.routers.floriannetz.entrypoints=websecure"
|
- "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=true"
|
||||||
- "traefik.http.routers.floriannetz.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt}"
|
- "traefik.http.routers.floriannetz.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt}"
|
||||||
|
- "traefik.http.routers.floriannetz.service=floriannetz"
|
||||||
- "traefik.http.services.floriannetz.loadbalancer.server.port=3000"
|
- "traefik.http.services.floriannetz.loadbalancer.server.port=3000"
|
||||||
# Security-Header-Middleware (zusätzlich zu next.config.ts; defense-in-depth).
|
# Security-Header-Middleware (zusätzlich zu next.config.ts; defense-in-depth).
|
||||||
- "traefik.http.routers.floriannetz.middlewares=floriannetz-sechdrs"
|
- "traefik.http.routers.floriannetz.middlewares=floriannetz-sechdrs"
|
||||||
@@ -137,10 +143,12 @@ volumes:
|
|||||||
nominatim-data:
|
nominatim-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
# Externes, von der separaten Traefik-Instanz verwaltetes Netz.
|
# Externes, von der separaten Traefik-Instanz verwaltetes Netz (wie im
|
||||||
traefik:
|
# feuerwehr_dashboard "frontend"). Muss existieren: docker network create frontend
|
||||||
|
frontend:
|
||||||
external: true
|
external: true
|
||||||
name: ${TRAEFIK_NETWORK:-traefik}
|
name: ${TRAEFIK_NETWORK:-frontend}
|
||||||
# Internes Netz: Postgres/Geo sind nur app-intern erreichbar, nicht öffentlich.
|
# Internes Bridge-Netz: Postgres/Geo ohne veröffentlichte Ports (nicht
|
||||||
|
# öffentlich), zugleich Egress für den App-Container (Authentik-OIDC).
|
||||||
internal:
|
internal:
|
||||||
internal: true
|
driver: bridge
|
||||||
|
|||||||
@@ -18,17 +18,20 @@ Der Stack besteht aus genau vier Services (kein Proxy):
|
|||||||
|
|
||||||
Netze:
|
Netze:
|
||||||
|
|
||||||
- **`traefik`** — externes, von Traefik verwaltetes Netz (`external: true`,
|
- **`frontend`** — externes, von Traefik verwaltetes Netz (`external: true`,
|
||||||
Name aus `TRAEFIK_NETWORK`, Default `traefik`). Nur `app` hängt daran.
|
Name aus `TRAEFIK_NETWORK`, Default `frontend` — wie im feuerwehr_dashboard).
|
||||||
- **`internal`** — internes Netz (`internal: true`); Postgres und die Geo-Dienste
|
Nur `app` hängt daran (Proxy↔App).
|
||||||
sind ausschließlich für die App erreichbar, nie öffentlich.
|
- **`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
|
## Voraussetzungen
|
||||||
|
|
||||||
Das externe Traefik-Netz muss existieren, bevor der Stack startet:
|
Das externe Traefik-Netz muss existieren, bevor der Stack startet:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker network create traefik
|
docker network create frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
Die externe Traefik-Instanz muss:
|
Die externe Traefik-Instanz muss:
|
||||||
@@ -36,7 +39,7 @@ Die externe Traefik-Instanz muss:
|
|||||||
- einen Entrypoint `websecure` (Port 443) bereitstellen,
|
- einen Entrypoint `websecure` (Port 443) bereitstellen,
|
||||||
- einen Zertifikatsauflöser anbieten, dessen Name `TRAEFIK_CERTRESOLVER`
|
- einen Zertifikatsauflöser anbieten, dessen Name `TRAEFIK_CERTRESOLVER`
|
||||||
entspricht (Default `letsencrypt`),
|
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}\`)`,
|
Die App-Labels in `docker-compose.yml` setzen Router (`Host(\`${APP_HOST}\`)`,
|
||||||
`entrypoints=websecure`, `tls.certresolver`), Service-Port `3000` und eine
|
`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 |
|
| 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_URL` | `https://${APP_HOST}` — Basis für Callback + Cookies |
|
||||||
| `AUTH_TRUST_HOST` | `true` — Auth.js vertraut den Forwarded-Headern |
|
| `AUTH_TRUST_HOST` | `true` — Auth.js vertraut den Forwarded-Headern |
|
||||||
| `AUTH_SECRET` | >= 32 Zeichen (`openssl rand -base64 32`) |
|
| `AUTH_SECRET` | >= 32 Zeichen (`openssl rand -base64 32`) |
|
||||||
| `AUTHENTIK_ISSUER` | OIDC-Issuer-URL der Authentik-Anwendung |
|
| `AUTHENTIK_ISSUER` | OIDC-Issuer-URL der Authentik-Anwendung |
|
||||||
| `AUTHENTIK_CLIENT_ID` | Client-ID der Authentik-Anwendung |
|
| `AUTHENTIK_CLIENT_ID` | Client-ID der Authentik-Anwendung |
|
||||||
| `AUTHENTIK_CLIENT_SECRET` | Client-Secret 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 |
|
| `DATABASE_URL` | wird in Compose aus `POSTGRES_*` zusammengesetzt |
|
||||||
| `TRAEFIK_CERTRESOLVER` | Name des Traefik-Zertifikatsauflösers |
|
| `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
|
## 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
|
Der Pfad `callback/authentik` entspricht dem NextAuth-Provider-Namen. Bei lokaler
|
||||||
Entwicklung zusätzlich `http://localhost:3000/api/auth/callback/authentik`.
|
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
|
## Health-Check & Middleware-Allowlist
|
||||||
|
|
||||||
`GET /api/health` ist **öffentlich** (anonym `200`, nur Liveness, keine
|
`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
|
```bash
|
||||||
cp .env.example .env # Werte setzen (APP_HOST, AUTH_*, AUTHENTIK_*, POSTGRES_*)
|
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 data # einmalig: OSRM-Geodaten vorbereiten (groß, dauert)
|
||||||
make deploy # build + up
|
make deploy # build + up
|
||||||
```
|
```
|
||||||
|
|||||||
3
public/.gitkeep
Normal file
3
public/.gitkeep
Normal 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.
|
||||||
17
src/auth.ts
17
src/auth.ts
@@ -91,9 +91,22 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
async signIn({ user, account, profile }) {
|
async signIn({ user, account, profile }) {
|
||||||
if (account?.provider === "authentik") {
|
if (account?.provider === "authentik") {
|
||||||
const email = user.email;
|
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);
|
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);
|
const u = await upsertAuthentikAdmin(email, user.name ?? null);
|
||||||
user.id = u.id;
|
user.id = u.id;
|
||||||
user.role = u.rolle;
|
user.role = u.rolle;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
const VALID_ENV = {
|
const VALID_ENV = {
|
||||||
NODE_ENV: "test",
|
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_SECRET: "x".repeat(32),
|
||||||
AUTH_URL: "http://localhost:3000",
|
AUTH_URL: "http://localhost:3000",
|
||||||
AUTHENTIK_ISSUER: "http://localhost:9000/application/o/floriannetz/",
|
AUTHENTIK_ISSUER: "http://localhost:9000/application/o/floriannetz/",
|
||||||
|
|||||||
@@ -2,17 +2,19 @@
|
|||||||
* Sicherheits-Header, eingehängt in next.config.ts.
|
* Sicherheits-Header, eingehängt in next.config.ts.
|
||||||
*
|
*
|
||||||
* Content-Security-Policy ist der zentrale Querschnitts-Schutz (Implementierungs-
|
* Content-Security-Policy ist der zentrale Querschnitts-Schutz (Implementierungs-
|
||||||
* plan Z.1314): in Produktion strikt mit default-src 'self', frame-ancestors 'none'
|
* plan Z.1314): default-src 'self', frame-ancestors 'none', form-action 'self',
|
||||||
* und form-action 'self'. Im Dev-Modus benötigt Next.js (HMR/React-Refresh) eine
|
* object-src 'none'. script-src erlaubt 'unsafe-inline' (KEIN 'unsafe-eval' in
|
||||||
* gelockerte script-src/connect-src-Variante ('unsafe-eval' + ws: für den Dev-Socket).
|
* 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 isProd = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
const CSP = [
|
const CSP = [
|
||||||
"default-src 'self'",
|
"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
|
isProd
|
||||||
? "script-src 'self'"
|
? "script-src 'self' 'unsafe-inline'"
|
||||||
: "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
|
: "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"img-src 'self' data: blob:",
|
"img-src 'self' data: blob:",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Es wird KEINE echte DB-Verbindung geöffnet (Pool ist lazy bis zur Query).
|
// Es wird KEINE echte DB-Verbindung geöffnet (Pool ist lazy bis zur Query).
|
||||||
const TEST_ENV: Record<string, string> = {
|
const TEST_ENV: Record<string, string> = {
|
||||||
NODE_ENV: "test",
|
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_SECRET: "test-secret-mindestens-32-zeichen-lang-xxxx",
|
||||||
AUTH_URL: "http://localhost:3000",
|
AUTH_URL: "http://localhost:3000",
|
||||||
AUTH_TRUST_HOST: "true",
|
AUTH_TRUST_HOST: "true",
|
||||||
|
|||||||
Reference in New Issue
Block a user