Zweistufig wie der Werkzeugkatalog: Betreiber pflegt den plattformweiten Standard (/betreiber/email-vorlagen), jeder Mandant kann ihn für sich übersteuern (/verwaltung/email-vorlagen) - ResolveEmailVorlage nutzt die eigene Vorlage falls vorhanden, sonst fällt sie auf den Plattform- Standard zurück. Passwort-Zurücksetzen ist die einzige aktuell existierende E-Mail und nutzt jetzt diese Vorlage statt Hardcoding. Bug beim Live-Verifizieren gefunden: UNIQUE(account_id, typ) verhindert bei NULLABLE account_id keine Duplikate (NULL != NULL in SQL) - jedes Speichern des Plattform-Standards erzeugte eine neue Zeile statt sie zu aktualisieren. Fix: zwei partielle Unique-Indizes statt eines gemeinsamen Constraints, mit Regressionstest abgesichert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
56 lines
2.9 KiB
SQL
56 lines
2.9 KiB
SQL
-- Editierbare E-Mail-Vorlagen, zweistufig wie der Werkzeugkatalog:
|
|
-- account_id NULL = plattformweiter Standard (nur vom Betreiber
|
|
-- editierbar, z. B. künftige E-Mails, die die Plattform selbst an
|
|
-- Mandanten-Admins schickt), account_id gesetzt = mandantenspezifische
|
|
-- Übersteuerung (vom Mandanten-Admin editierbar, z. B. eigener Ton/
|
|
-- Branding für eine E-Mail, die an die eigenen Mitarbeiter geht).
|
|
-- "typ" identifiziert, welche vom System versendete E-Mail gemeint ist
|
|
-- (aktuell nur "passwort_zuruecksetzen" — die einzige E-Mail, die das
|
|
-- System bisher verschickt, siehe internal/mail). Text darf Platzhalter
|
|
-- wie "{{link}}" enthalten, die beim Versand ersetzt werden (siehe
|
|
-- internal/web/email_vorlage_handlers.go).
|
|
CREATE TABLE email_vorlage (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
account_id UUID REFERENCES account(id),
|
|
typ TEXT NOT NULL,
|
|
betreff TEXT NOT NULL,
|
|
text TEXT NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- ACHTUNG: ein einfaches UNIQUE (account_id, typ) würde NICHT reichen —
|
|
-- SQL behandelt NULL nie als gleich zu NULL, ein normaler UNIQUE-
|
|
-- Constraint hätte also beliebig viele Plattform-Standard-Zeilen
|
|
-- (account_id IS NULL) je typ zugelassen. Zwei partielle Unique-Indizes
|
|
-- statt eines gemeinsamen Constraints, dafür braucht UpsertEmailVorlage
|
|
-- zwei unterschiedliche ON-CONFLICT-Ziele (siehe internal/store/email_vorlage.go).
|
|
CREATE UNIQUE INDEX email_vorlage_plattform_uidx ON email_vorlage (typ) WHERE account_id IS NULL;
|
|
CREATE UNIQUE INDEX email_vorlage_mandant_uidx ON email_vorlage (account_id, typ) WHERE account_id IS NOT NULL;
|
|
|
|
-- Plattformweiter Standard für die einzige aktuell existierende
|
|
-- E-Mail — ohne diese Zeile gäbe es nichts, worauf ResolveEmailVorlage
|
|
-- zurückfallen könnte, solange ein Mandant keine eigene Vorlage hat.
|
|
INSERT INTO email_vorlage (account_id, typ, betreff, text) VALUES (
|
|
NULL,
|
|
'passwort_zuruecksetzen',
|
|
'Deklarix — Passwort zurücksetzen',
|
|
'Hallo,' || E'\n\n' ||
|
|
'über diesen Link kannst du dein Deklarix-Passwort zurücksetzen (gültig 1 Stunde):' || E'\n' ||
|
|
'{{link}}' || E'\n\n' ||
|
|
'Falls du das nicht angefordert hast, ignoriere diese E-Mail.'
|
|
);
|
|
|
|
-- Wie werkzeug (account_id NULLABLE): NULL-Zeilen sind für alle lesbar,
|
|
-- aber nur vom Betreiber schreibbar; ein Mandant darf nur seine eigene
|
|
-- account_id-Zeile anlegen/ändern.
|
|
CREATE POLICY tenant_isolation ON email_vorlage FOR ALL USING (
|
|
account_id IS NULL
|
|
OR account_id = NULLIF(current_setting('app.account_id', true), '')::uuid
|
|
OR current_setting('app.is_betreiber', true) = 'true'
|
|
) WITH CHECK (
|
|
(account_id IS NULL AND current_setting('app.is_betreiber', true) = 'true')
|
|
OR account_id = NULLIF(current_setting('app.account_id', true), '')::uuid
|
|
);
|
|
ALTER TABLE email_vorlage ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE email_vorlage FORCE ROW LEVEL SECURITY;
|