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,496 @@
-- =====================================================
-- Audit Logging for Courses, Events, Bookings
--
-- Full change history for compliance: who changed what
-- field, old value -> new value, when. Mirrors the
-- member_audit_log pattern from 20260416000007.
-- =====================================================
-- -------------------------------------------------------
-- A) Add created_by / updated_by to main tables
-- -------------------------------------------------------
ALTER TABLE public.courses
ADD COLUMN IF NOT EXISTS created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS updated_by uuid REFERENCES auth.users(id) ON DELETE SET NULL;
ALTER TABLE public.events
ADD COLUMN IF NOT EXISTS created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS updated_by uuid REFERENCES auth.users(id) ON DELETE SET NULL;
ALTER TABLE public.bookings
ADD COLUMN IF NOT EXISTS created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS updated_by uuid REFERENCES auth.users(id) ON DELETE SET NULL;
-- -------------------------------------------------------
-- B) Audit log tables
-- -------------------------------------------------------
-- B.1 Course audit log
CREATE TABLE IF NOT EXISTS public.course_audit_log (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
course_id uuid NOT NULL REFERENCES public.courses(id) ON DELETE CASCADE,
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
action text NOT NULL CHECK (action IN (
'created', 'updated', 'status_changed', 'cancelled',
'participant_enrolled', 'participant_cancelled',
'participant_waitlisted', 'participant_promoted',
'session_created', 'session_cancelled',
'attendance_marked', 'instructor_changed', 'location_changed'
)),
changes jsonb NOT NULL DEFAULT '{}',
metadata jsonb NOT NULL DEFAULT '{}',
created_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE public.course_audit_log IS
'Immutable audit trail for all course lifecycle events';
CREATE INDEX IF NOT EXISTS ix_course_audit_course
ON public.course_audit_log(course_id, created_at DESC);
CREATE INDEX IF NOT EXISTS ix_course_audit_account
ON public.course_audit_log(account_id, created_at DESC);
CREATE INDEX IF NOT EXISTS ix_course_audit_action
ON public.course_audit_log(account_id, action);
ALTER TABLE public.course_audit_log ENABLE ROW LEVEL SECURITY;
REVOKE ALL ON public.course_audit_log FROM authenticated, service_role;
GRANT SELECT ON public.course_audit_log TO authenticated;
GRANT INSERT, SELECT ON public.course_audit_log TO service_role;
CREATE POLICY course_audit_log_select
ON public.course_audit_log FOR SELECT TO authenticated
USING (public.has_role_on_account(account_id));
-- B.2 Event audit log
CREATE TABLE IF NOT EXISTS public.event_audit_log (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
event_id uuid NOT NULL REFERENCES public.events(id) ON DELETE CASCADE,
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
action text NOT NULL CHECK (action IN (
'created', 'updated', 'status_changed', 'cancelled',
'registration_confirmed', 'registration_waitlisted',
'registration_cancelled', 'registration_promoted'
)),
changes jsonb NOT NULL DEFAULT '{}',
metadata jsonb NOT NULL DEFAULT '{}',
created_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE public.event_audit_log IS
'Immutable audit trail for all event lifecycle events';
CREATE INDEX IF NOT EXISTS ix_event_audit_event
ON public.event_audit_log(event_id, created_at DESC);
CREATE INDEX IF NOT EXISTS ix_event_audit_account
ON public.event_audit_log(account_id, created_at DESC);
CREATE INDEX IF NOT EXISTS ix_event_audit_action
ON public.event_audit_log(account_id, action);
ALTER TABLE public.event_audit_log ENABLE ROW LEVEL SECURITY;
REVOKE ALL ON public.event_audit_log FROM authenticated, service_role;
GRANT SELECT ON public.event_audit_log TO authenticated;
GRANT INSERT, SELECT ON public.event_audit_log TO service_role;
CREATE POLICY event_audit_log_select
ON public.event_audit_log FOR SELECT TO authenticated
USING (public.has_role_on_account(account_id));
-- B.3 Booking audit log
CREATE TABLE IF NOT EXISTS public.booking_audit_log (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
booking_id uuid NOT NULL REFERENCES public.bookings(id) ON DELETE CASCADE,
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
action text NOT NULL CHECK (action IN (
'created', 'updated', 'status_changed',
'checked_in', 'checked_out', 'cancelled',
'no_show', 'price_changed'
)),
changes jsonb NOT NULL DEFAULT '{}',
metadata jsonb NOT NULL DEFAULT '{}',
created_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE public.booking_audit_log IS
'Immutable audit trail for all booking lifecycle events';
CREATE INDEX IF NOT EXISTS ix_booking_audit_booking
ON public.booking_audit_log(booking_id, created_at DESC);
CREATE INDEX IF NOT EXISTS ix_booking_audit_account
ON public.booking_audit_log(account_id, created_at DESC);
CREATE INDEX IF NOT EXISTS ix_booking_audit_action
ON public.booking_audit_log(account_id, action);
ALTER TABLE public.booking_audit_log ENABLE ROW LEVEL SECURITY;
REVOKE ALL ON public.booking_audit_log FROM authenticated, service_role;
GRANT SELECT ON public.booking_audit_log TO authenticated;
GRANT INSERT, SELECT ON public.booking_audit_log TO service_role;
CREATE POLICY booking_audit_log_select
ON public.booking_audit_log FOR SELECT TO authenticated
USING (public.has_role_on_account(account_id));
-- -------------------------------------------------------
-- C) Auto-audit triggers for UPDATE
-- -------------------------------------------------------
-- C.1 Courses UPDATE trigger
CREATE OR REPLACE FUNCTION public.trg_course_audit_on_update()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
v_changes jsonb := '{}'::jsonb;
v_action text := 'updated';
v_user_id uuid;
BEGIN
-- Build changes diff (field by field)
IF OLD.name IS DISTINCT FROM NEW.name THEN
v_changes := v_changes || jsonb_build_object('name', jsonb_build_object('old', OLD.name, 'new', NEW.name));
END IF;
IF OLD.description IS DISTINCT FROM NEW.description THEN
v_changes := v_changes || jsonb_build_object('description', jsonb_build_object('old', OLD.description, 'new', NEW.description));
END IF;
IF OLD.course_number IS DISTINCT FROM NEW.course_number THEN
v_changes := v_changes || jsonb_build_object('course_number', jsonb_build_object('old', OLD.course_number, 'new', NEW.course_number));
END IF;
IF OLD.category_id IS DISTINCT FROM NEW.category_id THEN
v_changes := v_changes || jsonb_build_object('category_id', jsonb_build_object('old', OLD.category_id, 'new', NEW.category_id));
END IF;
IF OLD.instructor_id IS DISTINCT FROM NEW.instructor_id THEN
v_changes := v_changes || jsonb_build_object('instructor_id', jsonb_build_object('old', OLD.instructor_id, 'new', NEW.instructor_id));
END IF;
IF OLD.location_id IS DISTINCT FROM NEW.location_id THEN
v_changes := v_changes || jsonb_build_object('location_id', jsonb_build_object('old', OLD.location_id, 'new', NEW.location_id));
END IF;
IF OLD.start_date IS DISTINCT FROM NEW.start_date THEN
v_changes := v_changes || jsonb_build_object('start_date', jsonb_build_object('old', OLD.start_date, 'new', NEW.start_date));
END IF;
IF OLD.end_date IS DISTINCT FROM NEW.end_date THEN
v_changes := v_changes || jsonb_build_object('end_date', jsonb_build_object('old', OLD.end_date, 'new', NEW.end_date));
END IF;
IF OLD.fee IS DISTINCT FROM NEW.fee THEN
v_changes := v_changes || jsonb_build_object('fee', jsonb_build_object('old', OLD.fee, 'new', NEW.fee));
END IF;
IF OLD.reduced_fee IS DISTINCT FROM NEW.reduced_fee THEN
v_changes := v_changes || jsonb_build_object('reduced_fee', jsonb_build_object('old', OLD.reduced_fee, 'new', NEW.reduced_fee));
END IF;
IF OLD.capacity IS DISTINCT FROM NEW.capacity THEN
v_changes := v_changes || jsonb_build_object('capacity', jsonb_build_object('old', OLD.capacity, 'new', NEW.capacity));
END IF;
IF OLD.min_participants IS DISTINCT FROM NEW.min_participants THEN
v_changes := v_changes || jsonb_build_object('min_participants', jsonb_build_object('old', OLD.min_participants, 'new', NEW.min_participants));
END IF;
IF OLD.status IS DISTINCT FROM NEW.status THEN
v_changes := v_changes || jsonb_build_object('status', jsonb_build_object('old', OLD.status, 'new', NEW.status));
END IF;
IF OLD.registration_deadline IS DISTINCT FROM NEW.registration_deadline THEN
v_changes := v_changes || jsonb_build_object('registration_deadline', jsonb_build_object('old', OLD.registration_deadline, 'new', NEW.registration_deadline));
END IF;
IF OLD.notes IS DISTINCT FROM NEW.notes THEN
v_changes := v_changes || jsonb_build_object('notes', jsonb_build_object('old', OLD.notes, 'new', NEW.notes));
END IF;
-- Skip if nothing actually changed
IF v_changes = '{}'::jsonb THEN
RETURN NULL;
END IF;
-- Classify the action
IF OLD.status IS DISTINCT FROM NEW.status THEN
v_action := 'status_changed';
END IF;
v_user_id := COALESCE(
NULLIF(current_setting('app.current_user_id', true), '')::uuid,
auth.uid()
);
INSERT INTO public.course_audit_log (course_id, account_id, user_id, action, changes)
VALUES (NEW.id, NEW.account_id, v_user_id, v_action, v_changes);
RETURN NULL;
END;
$$;
CREATE OR REPLACE TRIGGER trg_courses_audit_on_update
AFTER UPDATE ON public.courses
FOR EACH ROW
EXECUTE FUNCTION public.trg_course_audit_on_update();
-- C.2 Events UPDATE trigger
CREATE OR REPLACE FUNCTION public.trg_event_audit_on_update()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
v_changes jsonb := '{}'::jsonb;
v_action text := 'updated';
v_user_id uuid;
BEGIN
-- Build changes diff (field by field)
IF OLD.name IS DISTINCT FROM NEW.name THEN
v_changes := v_changes || jsonb_build_object('name', jsonb_build_object('old', OLD.name, 'new', NEW.name));
END IF;
IF OLD.description IS DISTINCT FROM NEW.description THEN
v_changes := v_changes || jsonb_build_object('description', jsonb_build_object('old', OLD.description, 'new', NEW.description));
END IF;
IF OLD.event_date IS DISTINCT FROM NEW.event_date THEN
v_changes := v_changes || jsonb_build_object('event_date', jsonb_build_object('old', OLD.event_date, 'new', NEW.event_date));
END IF;
IF OLD.event_time IS DISTINCT FROM NEW.event_time THEN
v_changes := v_changes || jsonb_build_object('event_time', jsonb_build_object('old', OLD.event_time, 'new', NEW.event_time));
END IF;
IF OLD.end_date IS DISTINCT FROM NEW.end_date THEN
v_changes := v_changes || jsonb_build_object('end_date', jsonb_build_object('old', OLD.end_date, 'new', NEW.end_date));
END IF;
IF OLD.location IS DISTINCT FROM NEW.location THEN
v_changes := v_changes || jsonb_build_object('location', jsonb_build_object('old', OLD.location, 'new', NEW.location));
END IF;
IF OLD.capacity IS DISTINCT FROM NEW.capacity THEN
v_changes := v_changes || jsonb_build_object('capacity', jsonb_build_object('old', OLD.capacity, 'new', NEW.capacity));
END IF;
IF OLD.min_age IS DISTINCT FROM NEW.min_age THEN
v_changes := v_changes || jsonb_build_object('min_age', jsonb_build_object('old', OLD.min_age, 'new', NEW.min_age));
END IF;
IF OLD.max_age IS DISTINCT FROM NEW.max_age THEN
v_changes := v_changes || jsonb_build_object('max_age', jsonb_build_object('old', OLD.max_age, 'new', NEW.max_age));
END IF;
IF OLD.fee IS DISTINCT FROM NEW.fee THEN
v_changes := v_changes || jsonb_build_object('fee', jsonb_build_object('old', OLD.fee, 'new', NEW.fee));
END IF;
IF OLD.status IS DISTINCT FROM NEW.status THEN
v_changes := v_changes || jsonb_build_object('status', jsonb_build_object('old', OLD.status, 'new', NEW.status));
END IF;
IF OLD.registration_deadline IS DISTINCT FROM NEW.registration_deadline THEN
v_changes := v_changes || jsonb_build_object('registration_deadline', jsonb_build_object('old', OLD.registration_deadline, 'new', NEW.registration_deadline));
END IF;
IF OLD.contact_name IS DISTINCT FROM NEW.contact_name THEN
v_changes := v_changes || jsonb_build_object('contact_name', jsonb_build_object('old', OLD.contact_name, 'new', NEW.contact_name));
END IF;
IF OLD.contact_email IS DISTINCT FROM NEW.contact_email THEN
v_changes := v_changes || jsonb_build_object('contact_email', jsonb_build_object('old', OLD.contact_email, 'new', NEW.contact_email));
END IF;
IF OLD.contact_phone IS DISTINCT FROM NEW.contact_phone THEN
v_changes := v_changes || jsonb_build_object('contact_phone', jsonb_build_object('old', OLD.contact_phone, 'new', NEW.contact_phone));
END IF;
-- Skip if nothing actually changed
IF v_changes = '{}'::jsonb THEN
RETURN NULL;
END IF;
-- Classify the action
IF OLD.status IS DISTINCT FROM NEW.status THEN
v_action := 'status_changed';
END IF;
v_user_id := COALESCE(
NULLIF(current_setting('app.current_user_id', true), '')::uuid,
auth.uid()
);
INSERT INTO public.event_audit_log (event_id, account_id, user_id, action, changes)
VALUES (NEW.id, NEW.account_id, v_user_id, v_action, v_changes);
RETURN NULL;
END;
$$;
CREATE OR REPLACE TRIGGER trg_events_audit_on_update
AFTER UPDATE ON public.events
FOR EACH ROW
EXECUTE FUNCTION public.trg_event_audit_on_update();
-- C.3 Bookings UPDATE trigger
CREATE OR REPLACE FUNCTION public.trg_booking_audit_on_update()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
v_changes jsonb := '{}'::jsonb;
v_action text := 'updated';
v_user_id uuid;
BEGIN
-- Build changes diff (field by field)
IF OLD.room_id IS DISTINCT FROM NEW.room_id THEN
v_changes := v_changes || jsonb_build_object('room_id', jsonb_build_object('old', OLD.room_id, 'new', NEW.room_id));
END IF;
IF OLD.guest_id IS DISTINCT FROM NEW.guest_id THEN
v_changes := v_changes || jsonb_build_object('guest_id', jsonb_build_object('old', OLD.guest_id, 'new', NEW.guest_id));
END IF;
IF OLD.check_in IS DISTINCT FROM NEW.check_in THEN
v_changes := v_changes || jsonb_build_object('check_in', jsonb_build_object('old', OLD.check_in, 'new', NEW.check_in));
END IF;
IF OLD.check_out IS DISTINCT FROM NEW.check_out THEN
v_changes := v_changes || jsonb_build_object('check_out', jsonb_build_object('old', OLD.check_out, 'new', NEW.check_out));
END IF;
IF OLD.adults IS DISTINCT FROM NEW.adults THEN
v_changes := v_changes || jsonb_build_object('adults', jsonb_build_object('old', OLD.adults, 'new', NEW.adults));
END IF;
IF OLD.children IS DISTINCT FROM NEW.children THEN
v_changes := v_changes || jsonb_build_object('children', jsonb_build_object('old', OLD.children, 'new', NEW.children));
END IF;
IF OLD.status IS DISTINCT FROM NEW.status THEN
v_changes := v_changes || jsonb_build_object('status', jsonb_build_object('old', OLD.status, 'new', NEW.status));
END IF;
IF OLD.total_price IS DISTINCT FROM NEW.total_price THEN
v_changes := v_changes || jsonb_build_object('total_price', jsonb_build_object('old', OLD.total_price, 'new', NEW.total_price));
END IF;
IF OLD.notes IS DISTINCT FROM NEW.notes THEN
v_changes := v_changes || jsonb_build_object('notes', jsonb_build_object('old', OLD.notes, 'new', NEW.notes));
END IF;
-- Skip if nothing actually changed
IF v_changes = '{}'::jsonb THEN
RETURN NULL;
END IF;
-- Classify the action
IF OLD.status IS DISTINCT FROM NEW.status THEN
v_action := 'status_changed';
END IF;
v_user_id := COALESCE(
NULLIF(current_setting('app.current_user_id', true), '')::uuid,
auth.uid()
);
INSERT INTO public.booking_audit_log (booking_id, account_id, user_id, action, changes)
VALUES (NEW.id, NEW.account_id, v_user_id, v_action, v_changes);
RETURN NULL;
END;
$$;
CREATE OR REPLACE TRIGGER trg_bookings_audit_on_update
AFTER UPDATE ON public.bookings
FOR EACH ROW
EXECUTE FUNCTION public.trg_booking_audit_on_update();
-- -------------------------------------------------------
-- D) Auto-audit triggers for INSERT
-- -------------------------------------------------------
-- D.1 Courses INSERT trigger
CREATE OR REPLACE FUNCTION public.trg_course_audit_on_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
v_user_id uuid;
BEGIN
v_user_id := COALESCE(
NULLIF(current_setting('app.current_user_id', true), '')::uuid,
NEW.created_by
);
INSERT INTO public.course_audit_log (course_id, account_id, user_id, action, metadata)
VALUES (
NEW.id, NEW.account_id, v_user_id, 'created',
jsonb_build_object(
'course_number', NEW.course_number,
'name', NEW.name,
'status', NEW.status,
'fee', NEW.fee,
'capacity', NEW.capacity,
'start_date', NEW.start_date,
'end_date', NEW.end_date
)
);
RETURN NEW;
END;
$$;
CREATE OR REPLACE TRIGGER trg_courses_audit_on_insert
AFTER INSERT ON public.courses
FOR EACH ROW
EXECUTE FUNCTION public.trg_course_audit_on_insert();
-- D.2 Events INSERT trigger
CREATE OR REPLACE FUNCTION public.trg_event_audit_on_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
v_user_id uuid;
BEGIN
v_user_id := COALESCE(
NULLIF(current_setting('app.current_user_id', true), '')::uuid,
NEW.created_by
);
INSERT INTO public.event_audit_log (event_id, account_id, user_id, action, metadata)
VALUES (
NEW.id, NEW.account_id, v_user_id, 'created',
jsonb_build_object(
'name', NEW.name,
'status', NEW.status,
'event_date', NEW.event_date,
'location', NEW.location,
'capacity', NEW.capacity,
'fee', NEW.fee
)
);
RETURN NEW;
END;
$$;
CREATE OR REPLACE TRIGGER trg_events_audit_on_insert
AFTER INSERT ON public.events
FOR EACH ROW
EXECUTE FUNCTION public.trg_event_audit_on_insert();
-- D.3 Bookings INSERT trigger
CREATE OR REPLACE FUNCTION public.trg_booking_audit_on_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
v_user_id uuid;
BEGIN
v_user_id := COALESCE(
NULLIF(current_setting('app.current_user_id', true), '')::uuid,
NEW.created_by
);
INSERT INTO public.booking_audit_log (booking_id, account_id, user_id, action, metadata)
VALUES (
NEW.id, NEW.account_id, v_user_id, 'created',
jsonb_build_object(
'room_id', NEW.room_id,
'guest_id', NEW.guest_id,
'check_in', NEW.check_in,
'check_out', NEW.check_out,
'status', NEW.status,
'total_price', NEW.total_price,
'adults', NEW.adults,
'children', NEW.children
)
);
RETURN NEW;
END;
$$;
CREATE OR REPLACE TRIGGER trg_bookings_audit_on_insert
AFTER INSERT ON public.bookings
FOR EACH ROW
EXECUTE FUNCTION public.trg_booking_audit_on_insert();