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>
76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
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;
|
|
}
|