- internal/services/mail_service.go: SMTP/TLS sender (implicit TLS on 465), html/template rendering, branded base layout + content templates, silent no-op when SMTP_* unset. - internal/services/reminder_service.go: hourly scanner for Fristen that are overdue / due tomorrow / due within the week (Monday digest). Dedup via paliad.reminder_log (24h window). - internal/services/invite_service.go: POST /api/invite flow with domain whitelist, in-memory 10/day/user rate limit, audit row in paliad.invitations. - internal/handlers/invite.go: POST + GET /api/invite handlers. - Sidebar "Kolleg:in einladen" button + modal on every page. - migration 016: paliad.reminder_log, paliad.invitations, users.lang column. - docker-compose: SMTP_* + PALIAD_BASE_URL env vars. - docs/feature-roadmap.md: documented Supabase auth-SMTP routing as open question; current pilot keeps identity mails on Supabase default sender. Rationale: get Paliad off Supabase's best-effort outbound for the inbox-facing stuff (reminders, invitations) and move deadline nudges from passive dashboard to active email. Custom Supabase auth SMTP is blocked on the shared ydb.youpc.org instance — deferred until Paliad has its own project or GoTrue webhook relay.
62 lines
3.0 KiB
SQL
62 lines
3.0 KiB
SQL
-- Phase M (t-paliad-021): email service tables.
|
|
--
|
|
-- Two tables in this migration:
|
|
-- * reminder_log — dedup for hourly deadline-reminder emails. One row per
|
|
-- (frist_id, reminder_type, day). The service refuses to re-send when a
|
|
-- row younger than 24h exists; storing the timestamp rather than a bare
|
|
-- (frist_id, type) PK lets us re-send after the dedup window without
|
|
-- garbage-collecting.
|
|
-- * invitations — append-only audit of colleague invites sent via POST
|
|
-- /api/invite. Lets us show per-user history and, later, mark a row
|
|
-- accepted_at when the invitee completes register.
|
|
--
|
|
-- Optional paliad.users.lang column lets per-user language preference override
|
|
-- the DE default when rendering reminders/invitations. Unset today (the
|
|
-- onboarding form doesn't collect it yet); the MailService falls back to 'de'
|
|
-- whenever the field is NULL.
|
|
|
|
ALTER TABLE paliad.users
|
|
ADD COLUMN IF NOT EXISTS lang text;
|
|
|
|
-- reminder_log: one row per (frist_id, reminder_type) and day. The type
|
|
-- column takes 'overdue' | 'tomorrow' | 'weekly'. Weekly rows use the Monday
|
|
-- key date; per-Frist rows use the due date.
|
|
CREATE TABLE paliad.reminder_log (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
frist_id uuid REFERENCES paliad.fristen(id) ON DELETE CASCADE,
|
|
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
reminder_type text NOT NULL CHECK (reminder_type IN ('overdue', 'tomorrow', 'weekly')),
|
|
sent_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Dedup index: fast lookup of "did we send this reminder to this user for
|
|
-- this frist recently?". For weekly summaries, frist_id is NULL and the
|
|
-- uniqueness is enforced by the service (one weekly per user per Monday).
|
|
CREATE INDEX reminder_log_dedup_idx
|
|
ON paliad.reminder_log (user_id, reminder_type, frist_id, sent_at DESC);
|
|
|
|
-- invitations: record of every /api/invite call. Rate-limited per user at
|
|
-- the handler layer (10/day), not via a DB constraint — the handler keeps
|
|
-- an in-memory counter, mirroring the AI-extraction rate limiter pattern.
|
|
CREATE TABLE paliad.invitations (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
from_user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
to_email text NOT NULL,
|
|
message text NOT NULL DEFAULT '',
|
|
sent_at timestamptz NOT NULL DEFAULT now(),
|
|
accepted_at timestamptz
|
|
);
|
|
|
|
CREATE INDEX invitations_from_user_sent_idx
|
|
ON paliad.invitations (from_user_id, sent_at DESC);
|
|
|
|
CREATE INDEX invitations_to_email_idx
|
|
ON paliad.invitations (lower(to_email));
|
|
|
|
-- RLS: service-layer only for now (no client-facing endpoints read these
|
|
-- tables). Enabling RLS with no policies denies all direct Supabase PostgREST
|
|
-- access — the Go server bypasses RLS via its direct DB pool anyway, matching
|
|
-- every other paliad.* write-table.
|
|
ALTER TABLE paliad.reminder_log ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE paliad.invitations ENABLE ROW LEVEL SECURITY;
|