feat(260929-if2): Erinnerungen anlegen und zur Faelligkeit benachrichtigen (Tracer)

- Reminder-Tabelle mit Zeilenschutz (Mandant+Benutzer, Systemlesen fuer den E-Mail-Planer), API reminders (Liste, Anlegen)
- Kachel "Erinnerungen", globaler Melder im Portalrahmen (Browser und Desktop, je Faelligkeit einmal)
- Desktop: Laufzeit-Berechtigung fuer Benachrichtigungen nur fuer die gespeicherte Server-Adresse
- Zugriffsklassifikation nachgemessen

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 13:54:07 +02:00
parent cd1f8f6cda
commit 325c5ddbf2
31 changed files with 1969 additions and 11 deletions
@@ -0,0 +1,73 @@
-- 260929-if2 — Erinnerungen: persoenliche, einmalige Erinnerungen je Benutzer.
--
-- Zweck: die Tabelle "Reminder" traegt die Erinnerungen des Dashboard-Widgets
-- „Erinnerungen“ (Titel, Beschreibung, Faelligkeit, optional E-Mail). Es gibt
-- keine Wiederholung (D-01) und keine Historie: „Erledigt“ loescht die Zeile.
--
-- Besitz: eine Erinnerung gehoert genau einem Benutzer (gleicher Mandant UND
-- gleicher Benutzer, D-05). Faellt der Benutzer weg, fallen seine Erinnerungen
-- mit (ON DELETE CASCADE). Die Anwendung antwortet fuer fremde Kennungen mit
-- 404 (nie 403).
--
-- Spuren des E-Mail-Planers: "emailSentAt" ist der ANSPRUCH auf den Versand
-- (wird vor dem Senden gesetzt, damit mehrere API-Instanzen nicht doppelt
-- senden), "emailAttempts" zaehlt die Versuche (hoechstens 3). Ein Verschieben
-- der Faelligkeit setzt beide zurueck.
--
-- Zeilenschutz, zwei Regeln:
-- tenant_isolation_policy — Mandant UND Benutzer (Form aus DashboardImage,
-- 20260921120000_dashboard_image): ohne gesetzten Benutzer (Hintergrund-
-- dienst, der je Mandant gebunden schreibt) gilt nur der Mandant, mit
-- Benutzer zusaetzlich "userId".
-- system_read_policy — NUR FOR SELECT, Form aus 20260914120000_rls_system_
-- context_read. Sie bedient allein die Kandidatenabfrage des E-Mail-
-- Planers (reminder-mail.scheduler.ts), der einmal ueber ALLE Mandanten
-- liest und dann je Zeile gebunden anspricht. Schreiben bleibt der
-- Mandantenregel vorbehalten.
--
-- Rechte fuer die Anwendungsrolle tessera_app: kommen ueber ALTER DEFAULT
-- PRIVILEGES aus 20260909130000_rls_app_role automatisch — hier nichts zu tun.
--
-- WICHTIG: wie alle RLS-Regeln dieses Schemas wirken diese erst, wenn die
-- Anwendung als Rolle ohne Umgehungsrecht verbindet (Schalter heute AUS, siehe
-- docs/mandantentrennung-datenbankrolle.md). Bis dahin tragen die
-- Anwendungspruefungen im Dienst den Schutz allein.
-- CreateTable
CREATE TABLE "Reminder" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"dueAt" TIMESTAMP(3) NOT NULL,
"emailEnabled" BOOLEAN NOT NULL DEFAULT false,
"emailSentAt" TIMESTAMP(3),
"emailAttempts" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Reminder_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Reminder_tenantId_userId_dueAt_idx" ON "Reminder"("tenantId", "userId", "dueAt");
-- CreateIndex
CREATE INDEX "Reminder_dueAt_idx" ON "Reminder"("dueAt");
-- AddForeignKey
ALTER TABLE "Reminder" ADD CONSTRAINT "Reminder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Zeilenschutz: Mandant UND Benutzer (Muster 20260921120000)
ALTER TABLE "Reminder" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Reminder" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "Reminder"
USING (
"tenantId" = current_tenant_id()
AND (current_user_id() IS NULL OR "userId" = current_user_id())
);
-- Systemkontext: nur Lesen, fuer die Kandidatenabfrage des E-Mail-Planers
CREATE POLICY system_read_policy ON "Reminder"
FOR SELECT USING (is_system_context());