Phase 0: Fundament der Feuerwehr-Verwaltungsplattform
Legt den kompletten Unterbau an, auf dem die Fachmodule aufsetzen. Betrieb: - docker-compose.yml mit Traefik, App, Worker, PostgreSQL 17 + PostGIS, Redis, MinIO und Gotenberg; Entwicklungs-Override ohne Traefik, dafuer mit Mailpit und Adminer - Mehrstufiges Dockerfile: eigener Benutzer, tini als PID 1, Healthcheck - .env.example mit einem Kommentar zu jeder Variable Mandantentrennung: - Row Level Security auf allen mandantenpflichtigen Tabellen - Zwei getrennte Datenbankverbindungen: florian_app mit aktiver RLS fuer die Anwendung, Eigentuemer-Zugang nur fuer Migration, Seed und Anmeldung - fuerOrganisation() als einziger Weg zu Fachdaten Nachvollziehbarkeit: - AuditLog append-only per Trigger; UPDATE ist dauerhaft gesperrt, DELETE nur im gekennzeichneten Aufbewahrungslauf - Geheimnisse werden vor dem Schreiben entfernt Anmeldung: - OIDC gegen ein extern betriebenes authentik, Rollen aus Gruppen nur als Vorbelegung beim ersten Login - Lokaler E-Mail/Passwort-Login als Rueckfallweg - Terminal per PIN und RFID mit zwei Schranken (Geraet, dann Person) Weiteres: - lib/authz.ts mit 9 Rollen und 38 Einzelrechten - lib/storage.ts (MinIO), lib/audit.ts, lib/warteschlange.ts (BullMQ) - Integrationen fuer Mail, Telegram und LLM hinter abschaltbaren Interfaces - tRPC v11, Basis-Layout mit Seitenleiste und dunkler Darstellung - Seed mit einer Beispielwehr: 16 Personen, 3 Fahrzeuge, 40 Geraete - 32 Vitest-Tests (davon 8 gegen eine echte Datenbank), 10 Playwright-Tests - README, Anleitung mit Screenshots, Roadmap, Aufwandsschaetzung, Devlog, Versionierungsregeln und Changelog Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
# FLORIAN — Feuerwehr-Verwaltungsplattform
|
||||
|
||||
Selbstgehostete Verwaltungssoftware für Freiwillige Feuerwehren. Läuft im
|
||||
Gerätehaus, nicht in der Cloud. Keine Telemetrie, keine externen CDNs.
|
||||
|
||||
## Schnellstart
|
||||
|
||||
```bash
|
||||
git clone https://git.wilde-server.de/florian.wilde/FireAdministration.git && cd FireAdministration
|
||||
cp .env.example .env # Passwörter und AUTH_SECRET ersetzen
|
||||
npm ci # Abhängigkeiten
|
||||
docker compose up -d # Postgres, Redis, MinIO, Gotenberg
|
||||
npm run db:migrate && npm run db:seed # Schema und Beispielwehr
|
||||
npm run dev # http://localhost:3100
|
||||
```
|
||||
|
||||
Anmelden mit `SEED_ADMIN_EMAIL` und `SEED_ADMIN_PASSWORT` aus der `.env`
|
||||
(Vorgabe: `admin@beispielwehr.de` / `Feuerwehr2026`). Passwort danach ändern.
|
||||
|
||||
---
|
||||
|
||||
## Was drin ist
|
||||
|
||||
| Bereich | Stand |
|
||||
|---|---|
|
||||
| Phase 0 — Fundament | fertig |
|
||||
| Phase 1 — Stammdaten | offen |
|
||||
| Phase 2 — Wartung und Prüfung | offen |
|
||||
| Phase 3 — Einsätze und Übungen | offen |
|
||||
| Phase 4 — Kiosk-PWA | offen |
|
||||
| Phase 5 — Atemschutz (FwDV 7) | offen |
|
||||
| Phase 6 — Kartenmodule | offen |
|
||||
| Phase 7 — Benachrichtigungen und Termine | offen |
|
||||
| Phase 8 — Lokale KI | offen |
|
||||
|
||||
Vollständige Planung mit Aufwänden: [docs/ROADMAP.md](docs/ROADMAP.md) und
|
||||
[docs/AUFWANDSSCHAETZUNG.md](docs/AUFWANDSSCHAETZUNG.md).
|
||||
|
||||
## Weiterführende Dokumentation
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| [docs/ANLEITUNG.md](docs/ANLEITUNG.md) | Bedienung mit Bildschirmfotos |
|
||||
| [docs/ROADMAP.md](docs/ROADMAP.md) | Phasenplan und was jeweils entsteht |
|
||||
| [docs/AUFWANDSSCHAETZUNG.md](docs/AUFWANDSSCHAETZUNG.md) | Aufwand je Phase, Schätzung gegen Ist |
|
||||
| [docs/DEVLOG.md](docs/DEVLOG.md) | Entwicklungstagebuch mit Zeiterfassung |
|
||||
| [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md) | Versions- und Branch-Regeln |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | Was sich wann geändert hat |
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Fundament
|
||||
|
||||
### Aufbau
|
||||
|
||||
```
|
||||
FireAdministration/
|
||||
├─ docker/ Dockerfile, Entrypoint, Postgres-Initialisierung
|
||||
├─ prisma/ Schema, Migrationen, Seed
|
||||
├─ scripts/ Hilfsskripte (Bildschirmfotos)
|
||||
├─ src/
|
||||
│ ├─ app/ Next.js App Router
|
||||
│ │ ├─ (app)/ alles, was Anmeldung verlangt
|
||||
│ │ ├─ anmelden/ Anmeldeseite
|
||||
│ │ └─ api/ Auth.js, tRPC, Health
|
||||
│ ├─ components/ shadcn/ui-Bausteine und Layout
|
||||
│ ├─ lib/ authz, db, storage, audit, env, auth, warteschlange
|
||||
│ ├─ server/ tRPC-Router und Kontext
|
||||
│ └─ worker/ BullMQ-Hintergrundprozess
|
||||
├─ tests/ Vitest (unit) und Playwright (e2e)
|
||||
└─ docs/ Anleitung, Roadmap, Devlog, Aufwände
|
||||
```
|
||||
|
||||
### Mandantentrennung
|
||||
|
||||
Jede Tabelle trägt `organisationId`, und Postgres setzt die Trennung mit Row
|
||||
Level Security durch. Es gibt zwei Datenbankverbindungen:
|
||||
|
||||
- **`florian_app`** (`DATABASE_URL`) — RLS aktiv, `NOBYPASSRLS`. Über diese
|
||||
Verbindung arbeitet die Anwendung. Ohne gesetztes
|
||||
`app.current_organisation` liefert jede Abfrage **null Zeilen**.
|
||||
- **Eigentümer** (`DIRECT_DATABASE_URL`) — umgeht RLS. Nur für Migrationen,
|
||||
Seed, Anmeldung (die E-Mail-Suche muss über alle Wehren gehen) und
|
||||
Wartungsjobs.
|
||||
|
||||
Fachdaten laufen ausschließlich über `fuerOrganisation()` aus
|
||||
[`src/lib/db.ts`](src/lib/db.ts):
|
||||
|
||||
```ts
|
||||
const personen = await fuerOrganisation(orgId, (db) =>
|
||||
db.person.findMany({ where: { aktiv: true } }),
|
||||
);
|
||||
```
|
||||
|
||||
Ein vergessener Mandantenfilter kostet damit im schlimmsten Fall ein leeres
|
||||
Ergebnis — nicht die Daten einer fremden Wehr.
|
||||
`tests/unit/mandantentrennung.test.ts` prüft das gegen eine echte Datenbank.
|
||||
|
||||
### Audit-Log
|
||||
|
||||
Jede schreibende Operation erzeugt einen Eintrag mit Akteur, Zeitpunkt,
|
||||
Entität und Vorher/Nachher als JSONB. Die Tabelle ist append-only:
|
||||
|
||||
- `UPDATE` ist per Trigger **immer** gesperrt.
|
||||
- `DELETE` nur im gekennzeichneten Aufbewahrungslauf
|
||||
(`SET LOCAL app.audit_bereinigung = 'an'`), den ausschließlich der Worker
|
||||
ausführt — nachdem er exportiert hat. Ohne diese Ausnahme ließe sich das
|
||||
Löschkonzept nach DSGVO nicht umsetzen.
|
||||
|
||||
Geheimnisse (Passwort-Hashes, PINs, Token) werden vor dem Schreiben entfernt.
|
||||
|
||||
### Berechtigungen
|
||||
|
||||
Alles in [`src/lib/authz.ts`](src/lib/authz.ts) — keine Rollenprüfung in
|
||||
Komponenten. 9 Rollen, 38 Einzelrechte. Der Seed spiegelt den Katalog in die
|
||||
Datenbank, damit eine Wehr Rechte nachträglich zuschneiden kann, ohne dass
|
||||
jemand Code ändert.
|
||||
|
||||
```ts
|
||||
export const geraetSpeichern = mitRecht("geraet.bearbeiten").mutation(/* … */);
|
||||
```
|
||||
|
||||
### Anmeldung
|
||||
|
||||
Drei Wege, eine Sitzung:
|
||||
|
||||
1. **OIDC gegen authentik** — der Regelweg. authentik wird **separat**
|
||||
betrieben und ist nicht Teil dieses Stacks. Im Provider einzutragen:
|
||||
Redirect-URI `${APP_URL}/api/auth/callback/authentik`, Scopes
|
||||
`openid profile email`.
|
||||
2. **E-Mail und Passwort** — Rückfalloption für den Erststart und für den Fall,
|
||||
dass authentik nicht erreichbar ist. Über `AUTH_LOCAL_ENABLED` abschaltbar.
|
||||
3. **Terminal (PIN oder RFID)** — für das Kiosk-Gerät im Gerätehaus. Zwei
|
||||
Schranken: erst weist sich das *Gerät* mit `KIOSK_DEVICE_SECRET` aus, dann
|
||||
die *Person* mit PIN oder Karte. Die vollständige Oberfläche dazu kommt in
|
||||
Phase 4.
|
||||
|
||||
Rollen aus authentik-Gruppen wirken als **Vorbelegung beim ersten Login**
|
||||
(`AUTH_OIDC_GROUP_MAPPING`). Sobald jemand die Rolle in FLORIAN ändert, ist
|
||||
FLORIAN führend und OIDC überschreibt nicht mehr.
|
||||
|
||||
### Dateien
|
||||
|
||||
Alles liegt in MinIO, nie im Dateisystem und nie in der Datenbank. Zugriff
|
||||
ausschließlich über [`src/lib/storage.ts`](src/lib/storage.ts). Objektschlüssel:
|
||||
|
||||
```
|
||||
<wehr-slug>/<entitaet>/<entitaetId>/<uuid>-<dateiname>
|
||||
```
|
||||
|
||||
Downloads laufen über presignte Links mit begrenzter Gültigkeit
|
||||
(`S3_URL_TTL`, Vorgabe 15 Minuten).
|
||||
|
||||
---
|
||||
|
||||
## Befehle
|
||||
|
||||
```bash
|
||||
npm run dev # Entwicklungsserver auf Port 3100
|
||||
npm run build # Produktionsbuild
|
||||
npm run worker:dev # Hintergrundprozess mit Neuladen
|
||||
npm run test # Vitest
|
||||
npm run test:e2e # Playwright
|
||||
npm run typecheck # TypeScript ohne Ausgabe
|
||||
npm run db:migrate # ausstehende Migrationen anwenden
|
||||
npm run db:seed # Beispielwehr einspielen
|
||||
npm run db:studio # Prisma Studio
|
||||
npm run screenshots # Bilder für docs/ANLEITUNG.md erzeugen
|
||||
```
|
||||
|
||||
## Dienste und Ports (Entwicklung)
|
||||
|
||||
Bewusst nicht die Standardports, damit FLORIAN neben anderen lokalen Stacks
|
||||
laufen kann. Alle Werte stehen in der `.env`.
|
||||
|
||||
| Dienst | Adresse |
|
||||
|---|---|
|
||||
| Anwendung | http://localhost:3100 |
|
||||
| PostgreSQL 17 + PostGIS | `localhost:5442` |
|
||||
| Redis | `localhost:6389` |
|
||||
| MinIO API | http://localhost:9010 |
|
||||
| MinIO Weboberfläche | http://localhost:9011 |
|
||||
| Gotenberg | http://localhost:3101 |
|
||||
| Mailpit (nur Entwicklung) | http://localhost:8125 |
|
||||
| Adminer (nur Entwicklung) | http://localhost:8081 |
|
||||
|
||||
## Produktionsbetrieb
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
Die Angabe von `-f` ist wichtig: ohne sie legt Docker Compose automatisch
|
||||
`docker-compose.override.yml` darüber, und das ist die Entwicklungsvariante
|
||||
ohne Traefik. In Produktion terminiert Traefik TLS und verteilt auf
|
||||
`APP_DOMAIN` und `MINIO_DOMAIN`.
|
||||
|
||||
Der App-Container wendet beim Start ausstehende Migrationen an; der Worker
|
||||
tut das bewusst nicht, damit nicht zwei Prozesse gleichzeitig migrieren.
|
||||
|
||||
## Datenschutz
|
||||
|
||||
- Keine Telemetrie, keine externen CDNs, keine Google Fonts. Es werden nur
|
||||
Schriften verwendet, die auf dem Gerät vorhanden sind.
|
||||
- Aufbewahrungsfristen konfigurierbar (`AUFBEWAHRUNG_JAHRE`,
|
||||
`AUDIT_AUFBEWAHRUNG_TAGE`, `PERSON_ANONYMISIERUNG_TAGE`).
|
||||
- Externe Dienste (Mail, Telegram, LLM) sitzen hinter Schnittstellen in
|
||||
`src/lib/integrations/` und sind einzeln abschaltbar. Ohne Freischaltung
|
||||
wird nur ins Log geschrieben.
|
||||
|
||||
## Sicherheitshinweis
|
||||
|
||||
Atemschutzüberwachung und Alarmmonitor sind sicherheitsrelevant. Solange die
|
||||
Wehr das System nicht ausdrücklich freigegeben hat, bleibt die **analoge
|
||||
Überwachung führend**. Jede entsprechende Ansicht zeigt diesen Hinweis
|
||||
dauerhaft an.
|
||||
Reference in New Issue
Block a user