Files
paliad/internal/db/migrations/026_email_templates.up.sql
m 0e3411c40b feat(admin): /admin/email-templates editor (t-paliad-072)
DB-backed email-template editor for global_admins, replacing the
"Kommt bald" placeholder. Admins can edit invitation, deadline_digest,
and the shared base wrapper for both DE and EN, preview against sample
data, save with versions, and reset to the embedded default.

Backend:
- Migration 026 adds paliad.email_templates (active row per (key, lang))
  and paliad.email_template_versions (append-only, retained 20 deep).
- EmailTemplateService — GetActive falls through to the embedded per-
  language file when no DB row, Save validates parse + structural
  invariants and writes a version, Reset deletes the active row, Restore
  copies a version back. Mutations require DB; reads work without.
- MailService now consults the service for body and subject and falls
  back to the embedded default if the active row is malformed at parse
  time — a corrupt admin save can never wedge the send path.
- Subjects move from Go (buildDigestSubject + inviteSubject) to
  text/template strings stored in the (key, lang) row. Default subjects
  ship with a {{/* keep this phrasing */}} comment pointing at the
  reminder-redesign doc so the SLO framing rationale survives edits.
- Bilingual templates split into per-language files (invitation.de.html
  + .en.html, deadline_digest.de.html + .en.html, base.de.html + .en.html).
  No more {{if eq .Lang}} branching inside templates.
- Handlers under /api/admin/email-templates/* gated by the existing
  RequireAdminFunc(users) admin middleware, same shape as /admin/team.

Frontend:
- /admin/email-templates list page — three cards (one per template),
  each linking to DE + EN editors with their last-modified status.
- /admin/email-templates/{key}?lang=de three-pane editor — subject + body
  textarea + variable docs + actions on the left, sandboxed iframe
  preview + version log on the right. 500 ms debounced live preview;
  save validates server-side (422 on parse error, surfaced inline).
- admin.tsx flips the Email-Templates card from PLANNED to verfügbar.
- 50 new i18n keys (DE + EN) for the editor surface.

Tests: GetActive fallback path, ValidateTemplate happy + sad paths,
SaveRequiresStore on no-DB service, RenderTemplate body + subject
goldens, full SYSTEMAUSFALL/SYSTEM FAILURE subject matrix.

Smoke (knowledge-platform-only run, no DB/auth):
- GET /admin/email-templates → 302 to /login
- GET /api/admin/email-templates → 401
- go build/vet/test clean, bun run build clean

Design: docs/design-email-templates-2026-04-29.md.
2026-04-29 22:09:39 +02:00

47 lines
2.1 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Admin Email-Templates editor (t-paliad-072).
--
-- Two tables backing the /admin/email-templates UI:
-- * paliad.email_templates — exactly one active row per (key, lang) pair.
-- Absence of a row means "use the embedded default shipped with the
-- binary". Saves UPSERT into this table; resets DELETE the row.
-- * paliad.email_template_versions — append-only history. One row per
-- save (and per reset / restore). The service GCs to the most recent
-- 20 rows per (key, lang) inside the same tx as the save, so this table
-- stays at most 3 templates × 2 languages × 20 = 120 rows steady-state.
--
-- RLS enabled with no policies: same shape as paliad.invitations and
-- paliad.reminder_log. The Go server bypasses RLS via its direct DB pool;
-- this denies all PostgREST access (no PostgREST surface today, but kept
-- closed by default).
--
-- key is one of: 'invitation', 'deadline_digest', 'base'. Not enforced via
-- CHECK because the canonical key set lives in code (internal/services/
-- email_template_service.go) and changing it shouldn't require a migration.
CREATE TABLE paliad.email_templates (
key text NOT NULL,
lang text NOT NULL CHECK (lang IN ('de', 'en')),
subject text NOT NULL DEFAULT '',
body text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
updated_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
PRIMARY KEY (key, lang)
);
CREATE TABLE paliad.email_template_versions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
key text NOT NULL,
lang text NOT NULL CHECK (lang IN ('de', 'en')),
subject text NOT NULL DEFAULT '',
body text NOT NULL,
saved_at timestamptz NOT NULL DEFAULT now(),
saved_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
note text NOT NULL DEFAULT ''
);
CREATE INDEX email_template_versions_key_lang_idx
ON paliad.email_template_versions (key, lang, saved_at DESC);
ALTER TABLE paliad.email_templates ENABLE ROW LEVEL SECURITY;
ALTER TABLE paliad.email_template_versions ENABLE ROW LEVEL SECURITY;