-- Audit Logs Migration -- Tracks all admin actions: article edits, user role changes, publish events, deletions -- 1. Create audit_action_type enum DROP TYPE IF EXISTS public.audit_action_type CASCADE; CREATE TYPE public.audit_action_type AS ENUM ( 'article_created', 'article_edited', 'article_published', 'article_unpublished', 'article_deleted', 'article_archived', 'article_status_changed', 'user_role_changed', 'user_activated', 'user_deactivated', 'user_invited', 'user_invitation_revoked', 'import_completed', 'import_failed' ); -- 2. Create audit_logs table CREATE TABLE IF NOT EXISTS public.audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), action public.audit_action_type NOT NULL, performed_by UUID REFERENCES public.user_profiles(id) ON DELETE SET NULL, performed_by_email TEXT, affected_item_id TEXT, affected_item_type TEXT, affected_item_title TEXT, old_value JSONB, new_value JSONB, metadata JSONB, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- 3. Indexes CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON public.audit_logs(created_at DESC); CREATE INDEX IF NOT EXISTS idx_audit_logs_performed_by ON public.audit_logs(performed_by); CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON public.audit_logs(action); CREATE INDEX IF NOT EXISTS idx_audit_logs_affected_item_id ON public.audit_logs(affected_item_id); -- 4. Enable RLS ALTER TABLE public.audit_logs ENABLE ROW LEVEL SECURITY; -- 5. RLS Policies — only admins and editors can read; system inserts via service role DROP POLICY IF EXISTS "admins_read_audit_logs" ON public.audit_logs; CREATE POLICY "admins_read_audit_logs" ON public.audit_logs FOR SELECT TO authenticated USING ( EXISTS ( SELECT 1 FROM public.user_profiles up WHERE up.id = auth.uid() AND up.role IN ('admin', 'editor') ) ); DROP POLICY IF EXISTS "authenticated_insert_audit_logs" ON public.audit_logs; CREATE POLICY "authenticated_insert_audit_logs" ON public.audit_logs FOR INSERT TO authenticated WITH CHECK (true); -- 6. Seed sample audit log data for demonstration DO $$ DECLARE existing_user_id UUID; existing_user_email TEXT; BEGIN IF EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'user_profiles' ) THEN SELECT id, email INTO existing_user_id, existing_user_email FROM public.user_profiles LIMIT 1; IF existing_user_id IS NOT NULL THEN INSERT INTO public.audit_logs (id, action, performed_by, performed_by_email, affected_item_id, affected_item_type, affected_item_title, old_value, new_value, created_at) VALUES (gen_random_uuid(), 'article_published', existing_user_id, existing_user_email, gen_random_uuid()::TEXT, 'article', 'Broadcast Technology Trends 2026', NULL, jsonb_build_object('status', 'published'), NOW() - INTERVAL '2 hours'), (gen_random_uuid(), 'user_role_changed', existing_user_id, existing_user_email, gen_random_uuid()::TEXT, 'user', 'jane.doe@example.com', jsonb_build_object('role', 'reader'), jsonb_build_object('role', 'contributor'), NOW() - INTERVAL '4 hours'), (gen_random_uuid(), 'article_edited', existing_user_id, existing_user_email, gen_random_uuid()::TEXT, 'article', 'IP Video Production Guide', jsonb_build_object('status', 'draft'), jsonb_build_object('status', 'draft', 'title', 'IP Video Production Guide Updated'), NOW() - INTERVAL '6 hours'), (gen_random_uuid(), 'article_deleted', existing_user_id, existing_user_email, gen_random_uuid()::TEXT, 'article', 'Old NAB Show Coverage 2020', jsonb_build_object('status', 'archived'), NULL, NOW() - INTERVAL '1 day'), (gen_random_uuid(), 'user_invited', existing_user_id, existing_user_email, gen_random_uuid()::TEXT, 'user', 'newcontributor@example.com', NULL, jsonb_build_object('role', 'contributor'), NOW() - INTERVAL '1 day 2 hours'), (gen_random_uuid(), 'article_status_changed', existing_user_id, existing_user_email, gen_random_uuid()::TEXT, 'article', 'Cloud Production Workflows', jsonb_build_object('status', 'pending'), jsonb_build_object('status', 'published'), NOW() - INTERVAL '2 days'), (gen_random_uuid(), 'import_completed', existing_user_id, existing_user_email, NULL, 'import', 'WordPress Import Batch', NULL, jsonb_build_object('imported_count', 42, 'failed_count', 2), NOW() - INTERVAL '3 days'), (gen_random_uuid(), 'user_deactivated', existing_user_id, existing_user_email, gen_random_uuid()::TEXT, 'user', 'spammer@example.com', jsonb_build_object('is_active', true), jsonb_build_object('is_active', false), NOW() - INTERVAL '4 days') ON CONFLICT (id) DO NOTHING; END IF; END IF; EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'Audit log seed data failed: %', SQLERRM; END $$;