Files
accounted/supabase/migrations/20240101000018_audit_triggers.sql
T
Jakob Wennberg cdf1dcc4c8 New Base func
2026-02-19 09:48:02 +01:00

117 lines
4.1 KiB
PL/PgSQL

-- Migration 18: Audit Logging Triggers
-- Generic audit log writer with AFTER triggers on compliance-critical tables
-- =============================================================================
-- 1. Generic write_audit_log() SECURITY DEFINER function
-- Detects action type from TG_OP and state transitions
-- =============================================================================
CREATE OR REPLACE FUNCTION public.write_audit_log()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_user_id uuid;
v_action text;
v_old_state jsonb;
v_new_state jsonb;
v_record_id uuid;
v_desc text;
BEGIN
-- Determine user_id from the record
IF TG_OP = 'DELETE' THEN
v_user_id := OLD.user_id;
v_record_id := OLD.id;
v_old_state := to_jsonb(OLD);
v_new_state := NULL;
v_action := 'DELETE';
v_desc := 'Deleted ' || TG_TABLE_NAME || ' record';
ELSIF TG_OP = 'INSERT' THEN
v_user_id := NEW.user_id;
v_record_id := NEW.id;
v_old_state := NULL;
v_new_state := to_jsonb(NEW);
v_action := 'INSERT';
v_desc := 'Created ' || TG_TABLE_NAME || ' record';
ELSIF TG_OP = 'UPDATE' THEN
v_user_id := COALESCE(NEW.user_id, OLD.user_id);
v_record_id := COALESCE(NEW.id, OLD.id);
v_old_state := to_jsonb(OLD);
v_new_state := to_jsonb(NEW);
v_action := 'UPDATE';
v_desc := 'Updated ' || TG_TABLE_NAME || ' record';
-- Detect specific state transitions for journal_entries
IF TG_TABLE_NAME = 'journal_entries' THEN
IF OLD.status = 'draft' AND NEW.status = 'posted' THEN
v_action := 'COMMIT';
v_desc := 'Committed journal entry ' || NEW.voucher_series || NEW.voucher_number;
ELSIF OLD.status = 'posted' AND NEW.status = 'reversed' THEN
v_action := 'REVERSE';
v_desc := 'Reversed journal entry ' || OLD.voucher_series || OLD.voucher_number;
END IF;
END IF;
-- Detect period lock/close
IF TG_TABLE_NAME = 'fiscal_periods' THEN
IF (OLD.locked_at IS NULL AND NEW.locked_at IS NOT NULL) THEN
v_action := 'LOCK_PERIOD';
v_desc := 'Locked fiscal period "' || NEW.name || '"';
ELSIF (NOT OLD.is_closed AND NEW.is_closed) THEN
v_action := 'CLOSE_PERIOD';
v_desc := 'Closed fiscal period "' || NEW.name || '"';
END IF;
END IF;
END IF;
-- Write to audit log (bypass RLS via SECURITY DEFINER)
INSERT INTO public.audit_log (user_id, action, table_name, record_id, actor_id, old_state, new_state, description)
VALUES (v_user_id, v_action, TG_TABLE_NAME, v_record_id, v_user_id, v_old_state, v_new_state, v_desc);
-- Return appropriate value
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$;
-- =============================================================================
-- 2. AFTER triggers on compliance-critical tables
-- =============================================================================
-- journal_entries
CREATE TRIGGER audit_journal_entries
AFTER INSERT OR UPDATE OR DELETE ON public.journal_entries
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
-- journal_entry_lines
CREATE TRIGGER audit_journal_entry_lines
AFTER INSERT OR UPDATE OR DELETE ON public.journal_entry_lines
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
-- chart_of_accounts
CREATE TRIGGER audit_chart_of_accounts
AFTER INSERT OR UPDATE OR DELETE ON public.chart_of_accounts
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
-- document_attachments
CREATE TRIGGER audit_document_attachments
AFTER INSERT OR UPDATE OR DELETE ON public.document_attachments
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
-- fiscal_periods
CREATE TRIGGER audit_fiscal_periods
AFTER INSERT OR UPDATE OR DELETE ON public.fiscal_periods
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
-- company_settings
CREATE TRIGGER audit_company_settings
AFTER INSERT OR UPDATE OR DELETE ON public.company_settings
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
-- tax_codes
CREATE TRIGGER audit_tax_codes
AFTER INSERT OR UPDATE OR DELETE ON public.tax_codes
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();