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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user