Compare commits
3 Commits
c099b3acd9
...
f2578cedab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2578cedab | ||
|
|
a8d07ba2ab | ||
|
|
2e56a92b70 |
@@ -17,6 +17,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
|
||||
|
||||
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
|
||||
|
||||
149
Makefile
149
Makefile
@@ -1,38 +1,145 @@
|
||||
# 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 traefik # einmalig
|
||||
# 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 traefik
|
||||
.PHONY: build up down logs ps deploy 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:
|
||||
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 (Standard-Deploy; migrate läuft via Entrypoint automatisch)
|
||||
|
||||
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"
|
||||
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).
|
||||
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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
51
src/auth.ts
51
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,19 @@ 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;
|
||||
const groups = extractGroups(profile);
|
||||
if (!isAdminGroupMember(groups, env.AUTHENTIK_ADMIN_GROUP)) 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;
|
||||
},
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user