-- ============================================================================ -- Admin audit log — append-only journal of administrative actions. Writes -- go through an RPC so the insert path is a single choke-point with input -- validation; reads are admin-only via RLS. -- ============================================================================ create table public.admin_audit_log ( id uuid primary key default gen_random_uuid(), actor_id uuid not null references auth.users(id) on delete set null, action text not null, target_type text, target_id text, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now() ); create index admin_audit_log_created_at_idx on public.admin_audit_log (created_at desc); create index admin_audit_log_actor_idx on public.admin_audit_log (actor_id); alter table public.admin_audit_log enable row level security; create policy audit_log_select_admin on public.admin_audit_log for select to authenticated using (public.current_user_is_admin()); -- Insert via RPC only — no direct insert policy, so clients can't forge rows -- with a different actor_id. create or replace function public.admin_log_action( p_action text, p_target_type text default null, p_target_id text default null, p_metadata jsonb default '{}'::jsonb ) returns uuid language plpgsql security definer set search_path = public as $$ declare new_id uuid; begin if not public.current_user_is_admin() then raise exception 'not an admin'; end if; insert into public.admin_audit_log (actor_id, action, target_type, target_id, metadata) values (auth.uid(), p_action, p_target_type, p_target_id, coalesce(p_metadata, '{}'::jsonb)) returning id into new_id; return new_id; end; $$; revoke all on function public.admin_log_action(text, text, text, jsonb) from public; grant execute on function public.admin_log_action(text, text, text, jsonb) to authenticated; -- ============================================================================ -- Admin conversations view — lets admins list every conversation (bypassing -- is_conversation_member). Read-only: no writes flow through the view. -- ============================================================================ create policy conversations_select_admin on public.conversations for select to authenticated using (public.current_user_is_admin()); create policy conversation_members_select_admin on public.conversation_members for select to authenticated using (public.current_user_is_admin());