refactor: remove obsolete member management API module
Some checks failed
Workflow / ʦ TypeScript (pull_request) Failing after 5m57s
Workflow / ⚫️ Test (pull_request) Has been skipped

This commit is contained in:
T. Zehetbauer
2026-04-03 14:08:31 +02:00
parent 124c6a632a
commit 5c5aaabae5
132 changed files with 10107 additions and 3442 deletions

View File

@@ -0,0 +1,170 @@
-- =====================================================
-- GDPR Data Retention Automation
--
-- Configurable retention policies per account.
-- Automatic anonymization of resigned/excluded/deceased
-- members after retention period expires.
-- =====================================================
-- Retention policy configuration per account
CREATE TABLE IF NOT EXISTS public.gdpr_retention_policies (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
policy_name text NOT NULL DEFAULT 'Standard',
retention_days int NOT NULL DEFAULT 1095, -- 3 years
auto_anonymize boolean NOT NULL DEFAULT false,
applies_to_status text[] NOT NULL DEFAULT ARRAY['resigned', 'excluded', 'deceased'],
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(account_id)
);
ALTER TABLE public.gdpr_retention_policies ENABLE ROW LEVEL SECURITY;
REVOKE ALL ON public.gdpr_retention_policies FROM authenticated, service_role;
GRANT SELECT, INSERT, UPDATE ON public.gdpr_retention_policies TO authenticated;
GRANT ALL ON public.gdpr_retention_policies TO service_role;
CREATE POLICY gdpr_retention_select
ON public.gdpr_retention_policies FOR SELECT TO authenticated
USING (public.has_role_on_account(account_id));
CREATE POLICY gdpr_retention_mutate
ON public.gdpr_retention_policies FOR ALL TO authenticated
USING (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions));
-- Anonymize a single member (replaces all PII with placeholder)
CREATE OR REPLACE FUNCTION public.anonymize_member(
p_member_id uuid,
p_performed_by uuid DEFAULT NULL
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
v_member record;
v_user_id uuid;
BEGIN
v_user_id := COALESCE(p_performed_by, auth.uid());
SELECT * INTO v_member FROM public.members WHERE id = p_member_id;
IF v_member IS NULL THEN
RAISE EXCEPTION 'Member not found';
END IF;
-- Verify caller access
IF v_user_id IS NOT NULL AND NOT public.has_permission(v_user_id, v_member.account_id, 'members.write'::public.app_permissions) THEN
RAISE EXCEPTION 'Access denied' USING ERRCODE = '42501';
END IF;
-- Snapshot full record to audit log before anonymization
INSERT INTO public.member_audit_log (member_id, account_id, user_id, action, metadata)
VALUES (
p_member_id, v_member.account_id, v_user_id, 'gdpr_anonymized',
jsonb_build_object(
'original_first_name', v_member.first_name,
'original_last_name', v_member.last_name,
'original_email', v_member.email,
'reason', 'GDPR retention policy'
)
);
-- Replace all PII with anonymized placeholders
UPDATE public.members SET
first_name = 'ANONYMISIERT',
last_name = 'ANONYMISIERT',
email = NULL,
phone = NULL,
mobile = NULL,
phone2 = NULL,
fax = NULL,
street = NULL,
house_number = NULL,
street2 = NULL,
postal_code = NULL,
city = NULL,
date_of_birth = NULL,
birthplace = NULL,
birth_country = NULL,
iban = NULL,
bic = NULL,
account_holder = NULL,
sepa_mandate_reference = NULL,
sepa_mandate_id = NULL,
primary_mandate_id = NULL,
guardian_name = NULL,
guardian_phone = NULL,
guardian_email = NULL,
notes = '[GDPR anonymisiert am ' || to_char(now(), 'YYYY-MM-DD') || ']',
custom_data = '{}'::jsonb,
online_access_key = NULL,
online_access_blocked = true,
gdpr_consent = false,
gdpr_newsletter = false,
gdpr_internet = false,
gdpr_print = false,
gdpr_birthday_info = false,
is_archived = true,
updated_by = v_user_id
WHERE id = p_member_id;
-- Anonymize SEPA mandates (can't DELETE due to ON DELETE RESTRICT from Phase 1)
-- primary_mandate_id already cleared above in the members UPDATE
-- Anonymize SEPA PII fields (keep row for audit, revoke mandate)
UPDATE public.sepa_mandates
SET iban = 'DE00ANON0000000000000', bic = NULL, account_holder = 'ANONYMISIERT',
mandate_reference = 'ANON-' || id::text, status = 'revoked',
notes = '[GDPR anonymisiert]'
WHERE member_id = p_member_id;
-- Remove communications (may contain PII)
BEGIN
DELETE FROM public.member_communications WHERE member_id = p_member_id;
EXCEPTION WHEN undefined_table THEN NULL;
END;
-- Remove portal invitations
DELETE FROM public.member_portal_invitations WHERE member_id = p_member_id;
END;
$$;
GRANT EXECUTE ON FUNCTION public.anonymize_member(uuid, uuid)
TO authenticated, service_role;
-- Batch enforcement: find and anonymize members matching retention criteria
CREATE OR REPLACE FUNCTION public.enforce_gdpr_retention_policies()
RETURNS int
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
v_policy record;
v_member record;
v_count int := 0;
BEGIN
FOR v_policy IN
SELECT * FROM public.gdpr_retention_policies
WHERE auto_anonymize = true
LOOP
FOR v_member IN
SELECT m.id
FROM public.members m
WHERE m.account_id = v_policy.account_id
AND m.status = ANY(v_policy.applies_to_status::public.membership_status[])
AND m.first_name != 'ANONYMISIERT' -- not already anonymized
AND m.exit_date IS NOT NULL -- only retain based on actual exit date
AND m.exit_date + (v_policy.retention_days || ' days')::interval <= current_date
LOOP
PERFORM public.anonymize_member(v_member.id, NULL);
v_count := v_count + 1;
END LOOP;
END LOOP;
RETURN v_count;
END;
$$;
GRANT EXECUTE ON FUNCTION public.enforce_gdpr_retention_policies()
TO service_role;