Compare commits
11 Commits
c099b3acd9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb4747cfeb | ||
|
|
0634d8c236 | ||
|
|
987b8c9c8f | ||
|
|
5d4afb5936 | ||
|
|
4863eadcce | ||
|
|
f933ecc19e | ||
|
|
38021cbc51 | ||
|
|
f71cf51eb4 | ||
|
|
f2578cedab | ||
|
|
a8d07ba2ab | ||
|
|
2e56a92b70 |
17
.env.example
17
.env.example
@@ -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`)
|
||||
@@ -17,6 +20,10 @@ AUTH_TRUST_HOST=true
|
||||
AUTHENTIK_ISSUER=http://localhost:9000/application/o/floriannetz/
|
||||
AUTHENTIK_CLIENT_ID=floriannetz
|
||||
AUTHENTIK_CLIENT_SECRET=bitte-setzen
|
||||
# Mitglieder dieser Authentik-Gruppe erhalten beim Login automatisch
|
||||
# platform_admin. Wer NICHT in der Gruppe ist, wird vom SSO-Login abgewiesen.
|
||||
# Setup siehe docs/reference/authentik-setup.md.
|
||||
AUTHENTIK_ADMIN_GROUP=floriannetz-admins
|
||||
|
||||
# Geo (interne Dienste; Defaults zeigen auf Docker-Compose-Hostnamen)
|
||||
OSRM_URL=http://osrm:5000
|
||||
@@ -27,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.
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,6 +9,9 @@ tests/e2e/.auth/
|
||||
next-env.d.ts
|
||||
*.tsbuildinfo
|
||||
|
||||
# Coverage-Report (vitest --coverage), generiertes Artefakt.
|
||||
coverage/
|
||||
|
||||
# Generiertes Artefakt: wird im Docker-builder aus src/db/seed gebündelt
|
||||
# (scripts/build-seed-bundle.mjs), nicht eingecheckt.
|
||||
docker/seed.mjs
|
||||
|
||||
25
Dockerfile
25
Dockerfile
@@ -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.
|
||||
|
||||
155
Makefile
155
Makefile
@@ -1,38 +1,151 @@
|
||||
# FlorianNetz — Deployment-Makefile (externes Traefik).
|
||||
# FlorianNetz — Makefile
|
||||
# Lokale Entwicklung, Datenbank (Migrationen/Seeds) und Deployment (externes Traefik).
|
||||
#
|
||||
# Ziele:
|
||||
# make build - baut das App-Image (Next.js standalone, non-root)
|
||||
# make up - startet den Stack (App + Postgres + Geo) hinter Traefik
|
||||
# make down - stoppt den Stack
|
||||
# make logs - folgt den App-Logs
|
||||
# make deploy - build + up (Standard-Deploy)
|
||||
# make data - bereitet die OSRM-Geodaten vor (Download + Preprocessing)
|
||||
# make config - validiert die Compose-Konfiguration
|
||||
# Schnellstart (lokal, Postgres via Docker):
|
||||
# make setup # install + Postgres hoch + migrate + seed-all
|
||||
# make dev # Dev-Server -> http://localhost:3000
|
||||
#
|
||||
# Hinweis: up/data/deploy benötigen Docker (+ Netzzugriff/RAM/Disk) und werden
|
||||
# NICHT in CI/Sandbox ausgeführt. Das externe Traefik-Netz muss existieren:
|
||||
# docker network create traefik
|
||||
# Nur Build + Migrate (z. B. CI / vor Deploy):
|
||||
# make build-app migrate
|
||||
#
|
||||
# Voll-Deploy hinter externem Traefik (Docker):
|
||||
# docker network create frontend # einmalig (externes Traefik-Netz)
|
||||
# make deploy
|
||||
#
|
||||
# `make help` listet alle Ziele.
|
||||
|
||||
COMPOSE = docker compose --env-file .env
|
||||
SHELL := /bin/bash
|
||||
COMPOSE = docker compose --env-file .env
|
||||
# Lokale DB-Ziele binden den Dev-Override ein (veröffentlicht Postgres-Port 5432).
|
||||
COMPOSE_DEV = docker compose --env-file .env -f docker-compose.yml -f docker-compose.dev.yml
|
||||
|
||||
.PHONY: build up down logs deploy data config
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
build:
|
||||
# ---------------------------------------------------------------------------
|
||||
.PHONY: help
|
||||
help: ## Diese Übersicht anzeigen
|
||||
@grep -hE '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
|
||||
| awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
# --- Umgebung -------------------------------------------------------------
|
||||
.PHONY: env install
|
||||
env: ## .env aus .env.example erzeugen (falls noch nicht vorhanden)
|
||||
@if [ ! -f .env ]; then cp .env.example .env && echo "→ .env aus .env.example erstellt (Werte anpassen!)"; else echo "→ .env existiert bereits"; fi
|
||||
|
||||
install: ## npm-Abhängigkeiten installieren
|
||||
npm install
|
||||
|
||||
# --- Lokale Entwicklung & Qualität ---------------------------------------
|
||||
.PHONY: dev build-app lint typecheck test test-cov check
|
||||
dev: ## Next.js Dev-Server (http://localhost:3000)
|
||||
npm run dev
|
||||
|
||||
build-app: ## Next.js Production-Build (ohne Docker)
|
||||
npm run build
|
||||
|
||||
lint: ## ESLint
|
||||
npm run lint
|
||||
|
||||
typecheck: ## TypeScript prüfen (tsc --noEmit)
|
||||
npm run typecheck
|
||||
|
||||
test: ## Unit-Tests (Vitest)
|
||||
npm run test
|
||||
|
||||
test-cov: ## Unit-Tests mit Coverage
|
||||
npm run test:coverage
|
||||
|
||||
check: lint typecheck test ## Lint + Typecheck + Unit-Tests (Offline-DoD)
|
||||
|
||||
# --- E2E (braucht laufenden Server + Browser) ----------------------------
|
||||
.PHONY: e2e-install e2e e2e-gating
|
||||
e2e-install: ## Playwright-Browser installieren (einmalig)
|
||||
npx playwright install
|
||||
|
||||
e2e: ## Komplette Playwright-E2E-Suite
|
||||
npm run test:e2e
|
||||
|
||||
e2e-gating: ## Nur die Default-deny-Gating-Suite
|
||||
npm run test:e2e:gating
|
||||
|
||||
# --- Datenbank (lokal; Postgres via Docker, Port auf Host veröffentlicht) -
|
||||
.PHONY: db-up db-down db-wait generate migrate seed-auth seed seed-all db-check studio db-reset
|
||||
db-up: ## Nur Postgres starten (Docker, Port 5432 lokal)
|
||||
$(COMPOSE_DEV) up -d postgres
|
||||
|
||||
db-down: ## Postgres-Container stoppen
|
||||
$(COMPOSE_DEV) stop postgres
|
||||
|
||||
db-wait: ## Warten bis Postgres bereit ist (max ~60s)
|
||||
@echo "→ Warte auf Postgres…"; \
|
||||
for i in $$(seq 1 30); do \
|
||||
if $(COMPOSE_DEV) exec -T postgres sh -c 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"' >/dev/null 2>&1; then \
|
||||
echo "→ Postgres bereit."; exit 0; \
|
||||
fi; sleep 2; \
|
||||
done; echo "✗ Postgres nicht bereit (Timeout)."; exit 1
|
||||
|
||||
generate: ## Drizzle-Migration aus dem Schema generieren
|
||||
npm run db:generate
|
||||
|
||||
migrate: ## Migrationen anwenden (DATABASE_URL aus .env -> localhost:5432)
|
||||
npm run db:migrate
|
||||
|
||||
seed-auth: ## Ersten Platform-Admin anlegen (idempotent)
|
||||
npm run db:seed-auth
|
||||
|
||||
seed: ## NÖ-Katalog seeden: Merkmale/Vorlagen/Kategorien (idempotent)
|
||||
npm run db:seed
|
||||
|
||||
seed-all: seed-auth seed ## Auth- + Katalog-Seed
|
||||
|
||||
db-check: ## Drizzle-Schema-/Migrationskonsistenz prüfen (offline)
|
||||
npm run db:check
|
||||
|
||||
studio: ## Drizzle Studio öffnen (DB-Browser)
|
||||
npm run db:studio
|
||||
|
||||
db-reset: ## ACHTUNG: Postgres-Volume löschen, neu migrieren + seeden
|
||||
$(COMPOSE_DEV) rm -sf postgres
|
||||
-docker volume rm florian-netz_postgres-data
|
||||
$(MAKE) db-up db-wait migrate seed-all
|
||||
|
||||
# --- Erststart (lokal, von 0) --------------------------------------------
|
||||
.PHONY: setup
|
||||
setup: install env db-up db-wait migrate seed-all ## Komplettes lokales Setup von 0
|
||||
@echo ""
|
||||
@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 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:
|
||||
up: ## Stack starten (App + Postgres + Geo) hinter Traefik
|
||||
$(COMPOSE) up -d
|
||||
|
||||
down:
|
||||
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
|
||||
|
||||
logs:
|
||||
logs: ## App-Logs folgen
|
||||
$(COMPOSE) logs -f app
|
||||
|
||||
deploy: build up
|
||||
ps: ## Status der Stack-Container
|
||||
$(COMPOSE) ps
|
||||
|
||||
data:
|
||||
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
|
||||
|
||||
data: ## OSRM-Geodaten vorbereiten (Download + Preprocessing; viel RAM/Disk)
|
||||
./scripts/prepare-osm-data.sh
|
||||
|
||||
config:
|
||||
config: ## Compose-Konfiguration validieren
|
||||
$(COMPOSE) config --services
|
||||
|
||||
11
docker-compose.dev.yml
Normal file
11
docker-compose.dev.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
# Lokale Entwicklung: veröffentlicht den Postgres-Port auf dem Host (5432),
|
||||
# damit auf dem HOST laufende Befehle (`make migrate`, `make seed`, `npm run db:*`)
|
||||
# die Datenbank über DATABASE_URL (…@localhost:5432/…) erreichen.
|
||||
#
|
||||
# Wird NUR von den lokalen DB-Zielen des Makefiles eingebunden
|
||||
# (docker compose -f docker-compose.yml -f docker-compose.dev.yml …),
|
||||
# NICHT vom Produktiv-Deploy — dort bleibt Postgres app-intern (kein offener Port).
|
||||
services:
|
||||
postgres:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
@@ -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
|
||||
|
||||
53
docs/reference/authentik-setup.md
Normal file
53
docs/reference/authentik-setup.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Authentik-Integration & Admin-Zugang (FlorianNetz)
|
||||
|
||||
FlorianNetz nutzt Authentik als **OIDC-Identitätsanbieter**. Der **Admin-Zugang
|
||||
(platform_admin) wird zentral über eine Authentik-Gruppe** gesteuert — nicht über
|
||||
manuell gesetzte DB-Rollen.
|
||||
|
||||
## Wie es funktioniert
|
||||
|
||||
- Anmeldung über Authentik (OIDC). FlorianNetz fordert die Scopes
|
||||
`openid email profile groups` an.
|
||||
- Im `signIn`-Callback (`src/auth.ts`) wird der `groups`-Claim ausgewertet:
|
||||
- **Mitglied der Admin-Gruppe** (`AUTHENTIK_ADMIN_GROUP`, Standard
|
||||
`floriannetz-admins`) → wird (idempotent) als `platform_admin` in `users`
|
||||
angelegt/aktualisiert und eingeloggt.
|
||||
- **Kein Mitglied** → Login wird **abgewiesen** (`return false`).
|
||||
- Folge: Admins werden **in Authentik** verwaltet (Gruppenmitgliedschaft), nicht
|
||||
per `seed-auth`. Ein erstes manuelles Seeding entfällt (kein Henne-Ei-Problem).
|
||||
- **Wehr-Konten** (wehr_admin/wehr_read) bleiben **lokale** App-Konten
|
||||
(E-Mail+Passwort), die Wehr-Admins selbst anlegen — sie nutzen NICHT Authentik.
|
||||
|
||||
## Einrichtung in Authentik
|
||||
|
||||
1. **Gruppe anlegen:** z. B. `floriannetz-admins`; gewünschte Admin-Benutzer
|
||||
hinzufügen. (Muss exakt `AUTHENTIK_ADMIN_GROUP` entsprechen.)
|
||||
2. **Provider anlegen:** OAuth2/OpenID Provider
|
||||
- Redirect-URI: `https://<APP_HOST>/api/auth/callback/authentik`
|
||||
- Signing Key wie üblich; Client-Typ „Confidential".
|
||||
3. **Scopes/Property-Mappings:** dem Provider die Scope-Mappings
|
||||
`openid`, `email`, `profile` **und** das Gruppen-Mapping zuweisen, das den
|
||||
`groups`-Claim liefert (Authentik-Standard: „authentik default OAuth Mapping:
|
||||
OpenID 'groups'"). Ohne dieses Mapping enthält das Token keine `groups` und
|
||||
**niemand** erhält Admin-Zugang.
|
||||
4. **Application anlegen** und mit dem Provider verknüpfen; Slug muss zum
|
||||
`AUTHENTIK_ISSUER` passen (`…/application/o/<slug>/`).
|
||||
5. **Client-ID/-Secret** aus dem Provider übernehmen.
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
```
|
||||
AUTHENTIK_ISSUER=https://auth.example.at/application/o/floriannetz/
|
||||
AUTHENTIK_CLIENT_ID=…
|
||||
AUTHENTIK_CLIENT_SECRET=…
|
||||
AUTHENTIK_ADMIN_GROUP=floriannetz-admins
|
||||
```
|
||||
|
||||
## Prüfen
|
||||
|
||||
- Mitglied von `floriannetz-admins` meldet sich an → landet als Admin in
|
||||
`/admin`; in `users` existiert eine Zeile `authTyp='authentik'`,
|
||||
`rolle='platform_admin'`.
|
||||
- Nicht-Mitglied meldet sich an → Login abgewiesen (zurück zu `/login`).
|
||||
- `groups`-Claim fehlt (Mapping nicht zugewiesen) → alle SSO-Logins abgewiesen
|
||||
(erwartetes Fail-safe-Verhalten: kein Claim ⇒ kein Admin).
|
||||
@@ -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
|
||||
```
|
||||
|
||||
650
package-lock.json
generated
650
package-lock.json
generated
@@ -34,6 +34,7 @@
|
||||
"@types/pg": "^8.11.11",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitest/coverage-v8": "^3.2.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"drizzle-kit": "^0.30.4",
|
||||
"eslint": "^9.21.0",
|
||||
@@ -59,6 +60,20 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://npm.apple.com/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/core": {
|
||||
"version": "0.41.2",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@auth/core/-/core-0.41.2.tgz",
|
||||
@@ -88,6 +103,64 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://npm.apple.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://npm.apple.com/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://npm.apple.com/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://npm.apple.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
||||
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@drizzle-team/brocli": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
|
||||
@@ -1687,6 +1760,33 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://npm.apple.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/schema": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://npm.apple.com/@istanbuljs/schema/-/schema-0.1.6.tgz",
|
||||
"integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -2211,6 +2311,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://npm.apple.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/@playwright/test/-/test-1.60.0.tgz",
|
||||
@@ -4011,6 +4121,39 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://npm.apple.com/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz",
|
||||
"integrity": "sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"ast-v8-to-istanbul": "^0.3.3",
|
||||
"debug": "^4.4.1",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-lib-source-maps": "^5.0.6",
|
||||
"istanbul-reports": "^3.1.7",
|
||||
"magic-string": "^0.30.17",
|
||||
"magicast": "^0.3.5",
|
||||
"std-env": "^3.9.0",
|
||||
"test-exclude": "^7.0.1",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "3.2.6",
|
||||
"vitest": "3.2.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://npm.apple.com/@vitest/expect/-/expect-3.2.6.tgz",
|
||||
@@ -4160,6 +4303,19 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://npm.apple.com/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://npm.apple.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
@@ -4397,6 +4553,25 @@
|
||||
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul": {
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz",
|
||||
"integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.31",
|
||||
"estree-walker": "^3.0.3",
|
||||
"js-tokens": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://npm.apple.com/js-tokens/-/js-tokens-10.0.0.tgz",
|
||||
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async-function": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://npm.apple.com/async-function/-/async-function-1.0.0.tgz",
|
||||
@@ -5155,6 +5330,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://npm.apple.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.368",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz",
|
||||
@@ -5977,6 +6158,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://npm.apple.com/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://npm.apple.com/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||
@@ -6176,6 +6373,27 @@
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://npm.apple.com/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://npm.apple.com/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -6188,6 +6406,30 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://npm.apple.com/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://npm.apple.com/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://npm.apple.com/globals/-/globals-14.0.0.tgz",
|
||||
@@ -6317,6 +6559,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://npm.apple.com/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://npm.apple.com/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -6541,6 +6790,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://npm.apple.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-generator-function": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://npm.apple.com/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
||||
@@ -6770,6 +7028,58 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://npm.apple.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://npm.apple.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-source-maps": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://npm.apple.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
|
||||
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.23",
|
||||
"debug": "^4.1.1",
|
||||
"istanbul-lib-coverage": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/iterator.prototype": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://npm.apple.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
|
||||
@@ -6787,6 +7097,22 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://npm.apple.com/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "1.21.7",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/jiti/-/jiti-1.21.7.tgz",
|
||||
@@ -6979,6 +7305,12 @@
|
||||
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://npm.apple.com/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -6989,6 +7321,33 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/magicast": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://npm.apple.com/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -7043,6 +7402,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://npm.apple.com/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://npm.apple.com/ms/-/ms-2.1.3.tgz",
|
||||
@@ -7449,6 +7817,13 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://npm.apple.com/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -7485,6 +7860,22 @@
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://npm.apple.com/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://npm.apple.com/pathe/-/pathe-2.0.3.tgz",
|
||||
@@ -8625,6 +9016,18 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://npm.apple.com/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://npm.apple.com/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -8697,6 +9100,68 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://npm.apple.com/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://npm.apple.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://npm.apple.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string.prototype.includes": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://npm.apple.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
||||
@@ -8805,6 +9270,46 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://npm.apple.com/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://npm.apple.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-bom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
@@ -9012,6 +9517,57 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://npm.apple.com/test-exclude/-/test-exclude-7.0.2.tgz",
|
||||
"integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@istanbuljs/schema": "^0.1.2",
|
||||
"glob": "^10.4.1",
|
||||
"minimatch": "^10.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://npm.apple.com/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://npm.apple.com/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://npm.apple.com/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://npm.apple.com/thenify/-/thenify-3.3.1.tgz",
|
||||
@@ -10495,6 +11051,7 @@
|
||||
"resolved": "https://npm.apple.com/vitest/-/vitest-3.2.6.tgz",
|
||||
"integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.6",
|
||||
@@ -10702,6 +11259,99 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://npm.apple.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://npm.apple.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://npm.apple.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://npm.apple.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://artifacts.apple.com/artifactory/api/npm/npm-apple/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"@types/pg": "^8.11.11",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitest/coverage-v8": "^3.2.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"drizzle-kit": "^0.30.4",
|
||||
"eslint": "^9.21.0",
|
||||
|
||||
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.
|
||||
@@ -30,6 +30,9 @@ export const authConfig = {
|
||||
issuer: process.env.AUTHENTIK_ISSUER!,
|
||||
clientId: process.env.AUTHENTIK_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTHENTIK_CLIENT_SECRET!,
|
||||
// `groups`-Claim anfordern, damit der Admin-Zugang über die
|
||||
// Authentik-Gruppenmitgliedschaft gesteuert werden kann (signIn-Callback).
|
||||
authorization: { params: { scope: "openid email profile groups" } },
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
|
||||
66
src/auth.ts
66
src/auth.ts
@@ -7,12 +7,47 @@ import { users } from "@/db/schema";
|
||||
import { authConfig } from "./auth.config";
|
||||
import { verifyPassword } from "@/lib/auth/password";
|
||||
import { checkRateLimit, recordAttempt } from "@/lib/auth/rate-limit";
|
||||
import { extractGroups, isAdminGroupMember } from "@/lib/auth/authentik";
|
||||
import { ROLES } from "@/lib/auth/roles";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
const credSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
/**
|
||||
* Stellt sicher, dass ein Authentik-Admin (Mitglied der Admin-Gruppe) als
|
||||
* platform_admin in `users` existiert — für eine stabile id (Audit/FKs).
|
||||
* Idempotent über die eindeutige E-Mail; benötigt KEIN vorheriges Seeding.
|
||||
*/
|
||||
async function upsertAuthentikAdmin(email: string, name: string | null) {
|
||||
const normalized = email.toLowerCase();
|
||||
const rows = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
email: normalized,
|
||||
name: name ?? normalized,
|
||||
rolle: ROLES.PLATFORM_ADMIN,
|
||||
authTyp: "authentik",
|
||||
aktiv: true,
|
||||
brigadeId: null,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: users.email,
|
||||
set: {
|
||||
rolle: ROLES.PLATFORM_ADMIN,
|
||||
authTyp: "authentik",
|
||||
aktiv: true,
|
||||
...(name ? { name } : {}),
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
const row = rows[0];
|
||||
if (!row) throw new Error("Authentik-Admin-Upsert lieferte keine Zeile");
|
||||
return row;
|
||||
}
|
||||
|
||||
export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
...authConfig,
|
||||
providers: [
|
||||
@@ -50,17 +85,32 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
],
|
||||
callbacks: {
|
||||
...authConfig.callbacks,
|
||||
// Authentik-Login-Gate: nur vorgemerkte, aktive authentik-Konten zulassen.
|
||||
async signIn({ user, account }) {
|
||||
// Authentik-Login = Admin-Zugang, gesteuert über die Authentik-GRUPPE:
|
||||
// Nur Mitglieder von AUTHENTIK_ADMIN_GROUP dürfen rein und werden
|
||||
// (idempotent) als platform_admin angelegt. Alle anderen werden abgewiesen.
|
||||
async signIn({ user, account, profile }) {
|
||||
if (account?.provider === "authentik") {
|
||||
const email = user.email;
|
||||
if (!email) return false;
|
||||
const u = await db.query.users.findFirst({
|
||||
where: eq(users.email, email),
|
||||
});
|
||||
if (!u || !u.aktiv || u.authTyp !== "authentik") 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)) {
|
||||
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;
|
||||
user.brigadeId = u.brigadeId ?? null;
|
||||
user.brigadeId = null;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -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/",
|
||||
|
||||
38
src/lib/auth/__tests__/authentik.test.ts
Normal file
38
src/lib/auth/__tests__/authentik.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { extractGroups, isAdminGroupMember } from "@/lib/auth/authentik";
|
||||
|
||||
describe("isAdminGroupMember", () => {
|
||||
it("true, wenn die Admin-Gruppe enthalten ist", () => {
|
||||
expect(
|
||||
isAdminGroupMember(["wehr-x", "floriannetz-admins"], "floriannetz-admins"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("false, wenn die Admin-Gruppe fehlt", () => {
|
||||
expect(isAdminGroupMember(["wehr-x"], "floriannetz-admins")).toBe(false);
|
||||
});
|
||||
|
||||
it("false bei leerer Gruppenliste", () => {
|
||||
expect(isAdminGroupMember([], "floriannetz-admins")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractGroups", () => {
|
||||
it("liest den groups-Claim", () => {
|
||||
expect(extractGroups({ sub: "1", groups: ["a", "b"] })).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("leeres Array, wenn kein groups-Claim vorhanden ist", () => {
|
||||
expect(extractGroups({ sub: "1" })).toEqual([]);
|
||||
});
|
||||
|
||||
it("leeres Array bei undefined/null", () => {
|
||||
expect(extractGroups(undefined)).toEqual([]);
|
||||
expect(extractGroups(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it("defensiv: ignoriert nicht-string-Arrays", () => {
|
||||
expect(extractGroups({ groups: "kein-array" })).toEqual([]);
|
||||
expect(extractGroups({ groups: [1, 2, 3] })).toEqual([]);
|
||||
});
|
||||
});
|
||||
25
src/lib/auth/authentik.ts
Normal file
25
src/lib/auth/authentik.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Reine Helfer für die Authentik-Gruppensteuerung des Admin-Zugangs.
|
||||
* Bewusst OHNE DB-/Node-Importe, damit sie isoliert unit-testbar sind und
|
||||
* auch im Edge-Pfad unbedenklich wären.
|
||||
*/
|
||||
|
||||
const profileWithGroups = z
|
||||
.object({ groups: z.array(z.string()).optional() })
|
||||
.passthrough();
|
||||
|
||||
/** Extrahiert den `groups`-Claim aus dem Authentik-OIDC-Profil (defensiv). */
|
||||
export function extractGroups(profile: unknown): string[] {
|
||||
const parsed = profileWithGroups.safeParse(profile);
|
||||
return parsed.success && parsed.data.groups ? parsed.data.groups : [];
|
||||
}
|
||||
|
||||
/** Entscheidung: Ist der Benutzer Mitglied der konfigurierten Admin-Gruppe? */
|
||||
export function isAdminGroupMember(
|
||||
groups: readonly string[],
|
||||
adminGroup: string,
|
||||
): boolean {
|
||||
return groups.includes(adminGroup);
|
||||
}
|
||||
@@ -10,6 +10,8 @@ const serverSchema = z.object({
|
||||
AUTHENTIK_ISSUER: z.string().url(),
|
||||
AUTHENTIK_CLIENT_ID: z.string().min(1),
|
||||
AUTHENTIK_CLIENT_SECRET: z.string().min(1),
|
||||
// Authentik-Gruppe, deren Mitglieder automatisch platform_admin werden.
|
||||
AUTHENTIK_ADMIN_GROUP: z.string().min(1).default("floriannetz-admins"),
|
||||
// Geo:
|
||||
OSRM_URL: z.string().url().default("http://osrm:5000"),
|
||||
NOMINATIM_URL: z.string().url().default("http://nominatim:8080"),
|
||||
|
||||
@@ -61,4 +61,46 @@ describe("orderByEintreffzeit", () => {
|
||||
const result = await orderByEintreffzeit(origin, [a, b], etaTable);
|
||||
expect(result.map((r) => r.id)).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("(f) OSRM ohne Route fuer EINEN Kandidaten: einzelner Haversine-Fallback", async () => {
|
||||
// OSRM liefert eine Tabelle, kennt aber fuer 'fern' keine Route (null).
|
||||
// -> nur dieser Treffer faellt auf Haversine zurueck, 'nah' bleibt osrm.
|
||||
const etaTable = vi.fn().mockResolvedValue({
|
||||
durations: [[0, 120, null]], // origin -> [nah=120s, fern=keine Route]
|
||||
distances: [[0, 3000, null]],
|
||||
});
|
||||
const result = await orderByEintreffzeit(origin, [nah, fern], etaTable);
|
||||
const nahRes = result.find((r) => r.id === "nah")!;
|
||||
const fernRes = result.find((r) => r.id === "fern")!;
|
||||
expect(nahRes.eta.mode).toBe("osrm");
|
||||
expect(nahRes.eta.isFallback).toBe(false);
|
||||
expect(nahRes.eta.durationSec).toBe(120);
|
||||
// 'fern' ohne OSRM-Route -> Haversine-Fallback mit gesetzter Dauer.
|
||||
expect(fernRes.eta.mode).toBe("haversine");
|
||||
expect(fernRes.eta.isFallback).toBe(true);
|
||||
expect(fernRes.eta.durationSec).not.toBeNull();
|
||||
});
|
||||
|
||||
it("(g) nur Kandidaten ohne Koordinaten: OSRM wird nicht gerufen, alle ans Ende", async () => {
|
||||
// coords.length === 0 -> computeEtas ruft etaTable gar nicht auf.
|
||||
const ohne2: Hit = { id: "ohne2", brigadeCoords: null };
|
||||
const etaTable = vi.fn().mockResolvedValue({ durations: [[0]], distances: [[0]] });
|
||||
const result = await orderByEintreffzeit(origin, [ohne, ohne2], etaTable);
|
||||
expect(etaTable).not.toHaveBeenCalled();
|
||||
expect(result.map((r) => r.id)).toEqual(["ohne", "ohne2"]);
|
||||
expect(result.every((r) => r.eta.durationSec === null)).toBe(true);
|
||||
});
|
||||
|
||||
it("(h) OSRM-Tabelle ohne distances-Zeile: distanceMeters faellt auf null", async () => {
|
||||
// distances fehlt komplett -> `?? []` greift, distanceMeters wird null,
|
||||
// durationSec aus durations bleibt jedoch erhalten (mode=osrm).
|
||||
const etaTable = vi.fn().mockResolvedValue({
|
||||
durations: [[0, 90]],
|
||||
// kein distances-Feld
|
||||
});
|
||||
const result = await orderByEintreffzeit(origin, [nah], etaTable);
|
||||
expect(result[0]!.eta.mode).toBe("osrm");
|
||||
expect(result[0]!.eta.durationSec).toBe(90);
|
||||
expect(result[0]!.eta.distanceMeters).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -40,8 +40,11 @@ export function filePathToRoute(filePath: string): string {
|
||||
}
|
||||
|
||||
function isPublic(route: string): boolean {
|
||||
// Nur exakter Treffer oder echtes Unterpfad-Segment zählt als öffentlich.
|
||||
// KEIN reiner String-Präfix: sonst wären z. B. "/loginhelp" oder
|
||||
// "/api/healthz" fälschlich anonym erreichbar und entkämen dem Drift-Check.
|
||||
return PUBLIC_ALLOWLIST.some(
|
||||
(p) => route === p || route.startsWith(p + "/") || route.startsWith(p),
|
||||
(p) => route === p || route.startsWith(p + "/"),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,18 @@ describe("findUndeclaredRoutes", () => {
|
||||
expect(findUndeclaredRoutes(discovered, declared)).toEqual([]);
|
||||
});
|
||||
|
||||
it("flaggt Routen mit reinem String-Präfix als ungetestet (kein Pfadsegment)", () => {
|
||||
const declared = new Set<string>();
|
||||
// "/loginhelp" beginnt zwar mit "/login" und "/api/healthz" mit
|
||||
// "/api/health", sind aber KEINE Unterpfade -> müssen gegated werden.
|
||||
const discovered = ["/loginhelp", "/api/healthz", "/api/authentication"];
|
||||
expect(findUndeclaredRoutes(discovered, declared)).toEqual([
|
||||
"/loginhelp",
|
||||
"/api/healthz",
|
||||
"/api/authentication",
|
||||
]);
|
||||
});
|
||||
|
||||
it("PUBLIC_ALLOWLIST enthält /api/health und /login", () => {
|
||||
expect(PUBLIC_ALLOWLIST).toContain("/api/health");
|
||||
expect(PUBLIC_ALLOWLIST).toContain("/login");
|
||||
|
||||
@@ -19,9 +19,31 @@ export default defineConfig({
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
// Querschnitt-Kern muss hoch abgedeckt sein (Definition of Done #7):
|
||||
// src/lib/search und src/lib/geo >= 90 %.
|
||||
// die REINE, offline testbare Logik in src/lib/search und src/lib/geo
|
||||
// >= 90 %. Reine I/O-Wrapper (DB-Queries via Drizzle, HTTP zu
|
||||
// Nominatim/OSRM) sowie reine Typ-Module sind hier ausgenommen: sie
|
||||
// brauchen ein laufendes Postgres bzw. erreichbare Dienste und werden
|
||||
// über Integrations-/E2E-Tests abgesichert (im Sandbox deferred, da
|
||||
// kein Postgres/Server verfügbar).
|
||||
include: ["src/lib/search/**", "src/lib/geo/**"],
|
||||
exclude: [
|
||||
// Reine Typdefinitionen (keine ausführbaren Zeilen).
|
||||
"src/lib/search/types.ts",
|
||||
"src/lib/geo/types.ts",
|
||||
// DB-gebundene Query-Builder (Drizzle gegen Postgres).
|
||||
"src/lib/search/facets.ts",
|
||||
"src/lib/search/query-brigades.ts",
|
||||
"src/lib/search/query-equipment.ts",
|
||||
"src/lib/search/query-vehicles.ts",
|
||||
"src/lib/geo/candidates.ts",
|
||||
// Externe HTTP-Dienste (Nominatim-Geocoding, OSRM-Routing).
|
||||
"src/lib/geo/nominatim.ts",
|
||||
"src/lib/geo/osrm.ts",
|
||||
],
|
||||
// Per-Datei statt global: jede eingeschlossene reine-Logik-Datei muss
|
||||
// die Schwelle selbst erreichen (kein Verwässern über den Durchschnitt).
|
||||
thresholds: {
|
||||
perFile: true,
|
||||
lines: 90,
|
||||
functions: 90,
|
||||
statements: 90,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user