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:
Florian Wilde
2026-09-03 00:13:53 +02:00
co-authored by Claude Opus 5
commit 845942f36c
84 changed files with 21433 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
import "server-only";
import { env } from "@/lib/env";
/**
* E-Mail-Versand hinter einer Schnittstelle.
*
* Regel aus CLAUDE.md: externe Dienste hinter Interfaces in lib/integrations/,
* damit sie im Test mockbar und im Betrieb abschaltbar sind. Ist MAIL_ENABLED
* nicht gesetzt, landet die Nachricht im Log statt beim Empfänger — für
* Entwicklung und Testbetrieb genau richtig.
*
* Die tatsächliche SMTP-Anbindung kommt in Phase 7 (Benachrichtigungen). Bis
* dahin steht hier bewusst nur der Vertrag.
*/
export interface Nachricht {
an: string | string[];
betreff: string;
text: string;
html?: string;
antwortAn?: string;
anhaenge?: Array<{ dateiname: string; inhalt: Buffer; mimeTyp: string }>;
}
export interface MailVersand {
senden(nachricht: Nachricht): Promise<{ erfolgreich: boolean; kennung?: string; fehler?: string }>;
verfuegbar(): boolean;
}
/** Schreibt statt zu senden. Standard, solange MAIL_ENABLED=false. */
class ProtokollVersand implements MailVersand {
verfuegbar(): boolean {
return false;
}
async senden(nachricht: Nachricht) {
const empfaenger = Array.isArray(nachricht.an) ? nachricht.an.join(", ") : nachricht.an;
console.info(
`[mail] (nicht versendet, MAIL_ENABLED=false) an=${empfaenger} betreff="${nachricht.betreff}"`,
);
return { erfolgreich: true, kennung: "protokoll" };
}
}
/** Platzhalter für den echten SMTP-Versand aus Phase 7. */
class SmtpVersand implements MailVersand {
verfuegbar(): boolean {
return Boolean(env.MAIL_HOST && env.MAIL_FROM);
}
async senden(nachricht: Nachricht) {
const empfaenger = Array.isArray(nachricht.an) ? nachricht.an.join(", ") : nachricht.an;
console.warn(
`[mail] SMTP-Versand ist erst ab Phase 7 umgesetzt. an=${empfaenger} betreff="${nachricht.betreff}"`,
);
return {
erfolgreich: false,
fehler: "SMTP-Versand noch nicht umgesetzt (Phase 7).",
};
}
}
let versandCache: MailVersand | null = null;
export function mail(): MailVersand {
if (!versandCache) {
versandCache = env.MAIL_ENABLED ? new SmtpVersand() : new ProtokollVersand();
}
return versandCache;
}
/** Nur für Tests: eigene Umsetzung unterschieben. */
export function _mailErsetzen(ersatz: MailVersand | null): void {
versandCache = ersatz;
}