Files
myeasycms-v2/apps/web/supabase/migrations/20260416000008_member_communications.sql
T. Zehetbauer 5c5aaabae5
Some checks failed
Workflow / ʦ TypeScript (pull_request) Failing after 5m57s
Workflow / ⚫️ Test (pull_request) Has been skipped
refactor: remove obsolete member management API module
2026-04-03 14:08:31 +02:00

145 lines
4.9 KiB
PL/PgSQL

-- =====================================================
-- Member Communications Tracking
--
-- Records all communications with/about members:
-- emails sent, phone calls, notes, letters, meetings.
-- Communications are append-only for authenticated users.
-- Only service_role (admin) can delete.
-- Integrates with audit log via triggers.
-- =====================================================
CREATE TABLE IF NOT EXISTS public.member_communications (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
member_id uuid NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
type text NOT NULL CHECK (type IN ('email', 'phone', 'letter', 'meeting', 'note', 'sms')),
direction text NOT NULL DEFAULT 'outbound' CHECK (direction IN ('inbound', 'outbound', 'internal')),
subject text CHECK (subject IS NULL OR length(subject) <= 500),
body text CHECK (body IS NULL OR length(body) <= 50000),
-- Email-specific fields
email_to text,
email_cc text,
email_message_id text,
-- Attachment references (Supabase Storage paths)
attachment_paths text[] CHECK (attachment_paths IS NULL OR array_length(attachment_paths, 1) <= 10),
-- Audit
created_by uuid NOT NULL REFERENCES auth.users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE public.member_communications IS
'Communication log per member — emails, calls, notes, letters, meetings. Append-only for regular users.';
CREATE INDEX ix_member_comms_member
ON public.member_communications(member_id, created_at DESC);
CREATE INDEX ix_member_comms_account
ON public.member_communications(account_id, created_at DESC);
CREATE INDEX ix_member_comms_type
ON public.member_communications(account_id, type);
ALTER TABLE public.member_communications ENABLE ROW LEVEL SECURITY;
REVOKE ALL ON public.member_communications FROM authenticated, service_role;
-- Append-only: authenticated users can SELECT + INSERT, not UPDATE/DELETE
GRANT SELECT, INSERT ON public.member_communications TO authenticated;
GRANT ALL ON public.member_communications TO service_role;
-- Read: must have a role on the account
CREATE POLICY member_comms_select
ON public.member_communications FOR SELECT TO authenticated
USING (public.has_role_on_account(account_id));
-- Insert: must have members.write permission
CREATE POLICY member_comms_insert
ON public.member_communications FOR INSERT TO authenticated
WITH CHECK (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions));
-- No UPDATE/DELETE policies for authenticated — communications are immutable
-- service_role can still delete via admin client when necessary
-- Auto-log to audit trail on communication INSERT
CREATE OR REPLACE FUNCTION public.trg_member_comm_audit_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
INSERT INTO public.member_audit_log (
member_id, account_id, user_id, action, metadata
) VALUES (
NEW.member_id,
NEW.account_id,
NEW.created_by,
'communication_logged',
jsonb_build_object(
'communication_id', NEW.id,
'type', NEW.type,
'direction', NEW.direction,
'subject', NEW.subject
)
);
RETURN NEW;
EXCEPTION WHEN OTHERS THEN
-- Audit failure should not block the insert
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_member_comms_audit_insert
AFTER INSERT ON public.member_communications
FOR EACH ROW
EXECUTE FUNCTION public.trg_member_comm_audit_insert();
-- Safe delete function for admin use — logs before deleting
CREATE OR REPLACE FUNCTION public.delete_member_communication(
p_communication_id uuid,
p_account_id uuid
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
v_comm record;
BEGIN
-- Verify caller has access
IF NOT public.has_permission(auth.uid(), p_account_id, 'members.write'::public.app_permissions) THEN
RAISE EXCEPTION 'Access denied' USING ERRCODE = '42501';
END IF;
-- Fetch the communication for audit
SELECT * INTO v_comm
FROM public.member_communications
WHERE id = p_communication_id AND account_id = p_account_id;
IF v_comm IS NULL THEN
RAISE EXCEPTION 'Communication not found' USING ERRCODE = 'P0002';
END IF;
-- Log deletion to audit trail
INSERT INTO public.member_audit_log (
member_id, account_id, user_id, action, metadata
) VALUES (
v_comm.member_id,
v_comm.account_id,
auth.uid(),
'communication_logged',
jsonb_build_object(
'deleted_communication_id', v_comm.id,
'type', v_comm.type,
'direction', v_comm.direction,
'subject', v_comm.subject,
'action_detail', 'deleted'
)
);
-- Delete via service_role context (SECURITY DEFINER bypasses RLS)
DELETE FROM public.member_communications
WHERE id = p_communication_id AND account_id = p_account_id;
END;
$$;
GRANT EXECUTE ON FUNCTION public.delete_member_communication(uuid, uuid)
TO authenticated, service_role;