notifications(P4d): form submission submitter + admin alerts

submitForm now enqueues two kinds of notifications, gated per the design:

  - form_submission_received: ONLY when the submitter is authenticated
    (members or public_auth visibility). Recipient is the verified auth
    email (user.email), never the typed body.email. Anonymous public
    submissions skip the confirmation because confirming to a typed
    address is a victim-spam vector.

  - form_submission_admin: one row per member who holds forms.manage in
    the space (mirrors the forms-guard / forms RLS gate). Dedupe by
    (submission, admin) so a replay is a no-op.

Finding all permission holders without N round-trips needs a new SQL
helper. Migration 047 adds members_with_permission(sid, perm): the
inverted, set-returning form of user_has_permission, with the same
current/late status gate (046) and the same admin shortcut +
space_role_permissions / user_effective_roles fallback. Additive,
introduces no new tables or policies; existing callers unchanged.
Mirrored into schema.sql; types/database.ts updated.

renderFormEmail + formDedupeKey live in notifications-logic; 6 new
render/dedupe unit tests cover both types, fallback labels, escaping.

Suite 565, build clean.
This commit is contained in:
Moheeb Zara
2026-05-19 15:28:21 -07:00
parent 105b62f28a
commit a2ea7675e7
6 changed files with 320 additions and 14 deletions
+23
View File
@@ -1769,6 +1769,29 @@ AS $$
);
$$;
-- Inverted, set-returning form of user_has_permission: which members in this
-- space hold the named permission? Same membership-status gate (current/late,
-- mirrors 046), same admin-shortcut, same space_role_permissions +
-- user_effective_roles fallback. Used by notification fan-outs (e.g. the
-- forms.manage admin alert) where N user_has_permission calls would be N
-- round-trips. Equivalent to scripts/047_members_with_permission.sql.
CREATE OR REPLACE FUNCTION public.members_with_permission(sid uuid, perm text)
RETURNS TABLE(member_id uuid) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public
AS $$
SELECT sm.id
FROM public.space_members sm
WHERE sm.space_id = sid
AND sm.status IN ('current','late')
AND (
sm.role = 'admin'
OR EXISTS (
SELECT 1 FROM public.space_role_permissions p
WHERE p.space_id = sid AND p.permission = perm
AND p.subject IN (SELECT public.user_effective_roles(sm.user_id, sid))
)
);
$$;
ALTER TABLE public.space_role_permissions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ops_acl ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS role_perms_select ON public.space_role_permissions;