-- Admin accounts manage settings and enforcement. Moderators review map submissions. alter table public.profiles drop constraint profiles_role_check; alter table public.profiles add constraint profiles_role_check check (role in ('contributor','moderator','admin')); create or replace function public.publish_dataset(p_dataset uuid,p_reviewer uuid,p_decision text,p_reason text default null) returns void language plpgsql security invoker set search_path='' as $$ declare d public.datasets; m jsonb; rev uuid; first jsonb; count integer; begin if not exists(select 1 from public.profiles where id=p_reviewer and role in ('moderator','admin')) then raise exception 'Moderator permission required'; end if; if p_decision='unpublish' and not exists(select 1 from public.profiles where id=p_reviewer and role='admin') then raise exception 'Admin permission required'; end if; select * into d from public.datasets where id=p_dataset and parent_dataset_id is null for update; if not found then raise exception 'Map not found'; end if; if p_decision='approve' then if not(d.state='review' or (d.state='published' and coalesce(d.update_state,'')='review')) then raise exception 'Map is not ready for publication'; end if; select manifest into m from public.dataset_inputs where dataset_id=p_dataset and validated; count:=jsonb_array_length(m->'viewpoints'); first:=m->'viewpoints'->0; if m is null or count<5 or count>24 then raise exception 'Map requires 5–24 validated panoramas'; end if; insert into public.revisions(dataset_id,manifest,asset_root) values(p_dataset,m,p_dataset::text) returning id into rev; update public.datasets set state='published',revision_id=rev,update_state=null,name=m->>'name',description=m->>'description',panorama_count=count, map_path=coalesce(m->>'mapPath',d.map_path), cover_path=coalesce(first->>'assetRoot',id::text)||'/'||coalesce(first->>'assetId',first->>'id')||'/preview.webp',rejection_reason=null,updated_at=now() where id=p_dataset; elsif p_decision='reject' and (d.state='review' or (d.state='published' and d.update_state='review')) then if coalesce(length(trim(p_reason)),0)=0 then raise exception 'Provide a rejection reason'; end if; update public.datasets set state=case when state='published' then state else 'rejected' end,update_state=case when state='published' then 'rejected' end,rejection_reason=p_reason,updated_at=now() where id=p_dataset; elsif p_decision='unpublish' and d.state='published' then update public.datasets set state='unpublished',update_state=null,updated_at=now() where id=p_dataset; else raise exception 'Invalid review transition'; end if; insert into public.review_decisions(dataset_id,reviewer_id,decision,reason) values(p_dataset,p_reviewer,p_decision,p_reason); end; $$; create or replace function public.review_content_report(p_report uuid,p_reviewer uuid,p_decision text,p_reason text) returns void language plpgsql security invoker set search_path='' as $$ declare r public.reports; d public.datasets; map_id uuid; strikes integer; begin if not exists(select 1 from public.profiles where id=p_reviewer and role = 'admin') then raise exception 'Admin permission required'; end if; select dataset_id into map_id from public.reports where id=p_report; select * into d from public.datasets where id=map_id for update; select * into r from public.reports where id=p_report for update; if not found or r.state<>'open' or r.report_cycle<>d.report_cycle then raise exception 'Report is no longer awaiting review'; end if; if p_decision is null or p_decision not in ('confirm','dismiss') or coalesce(length(trim(p_reason)),0) not between 10 and 2000 then raise exception 'Provide a review decision and feedback in 10–2000 characters'; end if; if p_decision='confirm' then if d.banned_at is not null then raise exception 'This map is already banned; dismiss the remaining report or review its appeal'; end if; if p_reviewer=r.owner_id or p_reviewer=d.owner_id then raise exception 'An independent moderator must confirm this report'; end if; -- Distinct reporters provide each strike, including legacy reports. if exists(select 1 from public.reports where dataset_id=d.id and owner_id=r.owner_id and report_cycle=d.report_cycle and state='confirmed') then raise exception 'This reporter already contributed a strike'; end if; strikes:=d.report_strikes+1; update public.datasets set report_strikes=strikes, state=case when strikes>=3 then 'unpublished' else state end, update_state=case when strikes>=3 then null else update_state end, banned_at=case when strikes>=3 then now() end, ban_reason=case when strikes>=3 then trim(p_reason) end,updated_at=now() where id=d.id; end if; update public.reports set state=case when p_decision='confirm' then 'confirmed' else 'dismissed' end, reviewer_id=p_reviewer,reviewed_at=now(),review_reason=trim(p_reason) where id=r.id; end; $$; create or replace function public.review_map_appeal(p_appeal uuid,p_reviewer uuid,p_decision text,p_reason text) returns void language plpgsql security invoker set search_path='' as $$ declare a public.map_appeals; d public.datasets; map_id uuid; begin if not exists(select 1 from public.profiles where id=p_reviewer and role = 'admin') then raise exception 'Admin permission required'; end if; select dataset_id into map_id from public.map_appeals where id=p_appeal; select * into d from public.datasets where id=map_id for update; select * into a from public.map_appeals where id=p_appeal for update; if not found or a.state<>'open' or d.banned_at is null or a.report_cycle<>d.report_cycle then raise exception 'Appeal is no longer awaiting review'; end if; if p_reviewer=d.owner_id then raise exception 'An independent moderator must review your appeal'; end if; if p_decision is null or p_decision not in ('approve','reject') or coalesce(length(trim(p_reason)),0) not between 10 and 2000 then raise exception 'Provide an appeal decision and feedback in 10–2000 characters'; end if; if p_decision='approve' then if d.revision_id is null then raise exception 'Map has no approved version to restore'; end if; update public.datasets set report_strikes=0,report_cycle=report_cycle+1,banned_at=null,ban_reason=null,state='published',updated_at=now() where id=d.id; update public.reports set state='dismissed',reviewer_id=p_reviewer,reviewed_at=now(),review_reason='Closed when the map ban appeal was approved.' where dataset_id=d.id and report_cycle=d.report_cycle and state='open'; end if; update public.map_appeals set state=case when p_decision='approve' then 'approved' else 'rejected' end,reviewer_id=p_reviewer,reviewed_at=now(),review_reason=trim(p_reason) where id=a.id; end; $$; create or replace function public.set_beta_badge(p_reviewer uuid, p_enabled boolean) returns boolean language plpgsql security invoker set search_path = '' as $$ begin if not exists ( select 1 from public.profiles where id = p_reviewer and role = 'admin' ) then raise exception 'Admin permission required'; end if; if p_enabled is null then raise exception 'Beta badge visibility is required'; end if; update public.site_settings set beta_badge_enabled = p_enabled, updated_at = now(), updated_by = p_reviewer where id = true; if not found then raise exception 'Site settings not found'; end if; return p_enabled; end; $$; create or replace function public.set_user_prefix(p_reviewer uuid,p_user uuid,p_prefix text,p_icon text) returns void language plpgsql security invoker set search_path='' as $$ begin if not exists(select 1 from public.profiles where id=p_reviewer and role = 'admin') then raise exception 'Admin permission required'; end if; if p_icon not in ('star','crown','heart','flame','gem','trophy','sparkles') or p_icon is null then raise exception 'Invalid prefix icon'; end if; if p_prefix is not null and (length(btrim(p_prefix)) not between 1 and 24 or p_prefix ~ '[[:cntrl:]]') then raise exception 'Invalid custom prefix'; end if; update public.profiles set custom_prefix=case when p_prefix is null then null else btrim(p_prefix) end,custom_prefix_icon=p_icon where id=p_user; if not found then raise exception 'Player not found'; end if; end; $$; revoke all on function public.publish_dataset(uuid,uuid,text,text) from public,anon,authenticated; grant execute on function public.publish_dataset(uuid,uuid,text,text) to service_role; revoke all on function public.review_content_report(uuid,uuid,text,text) from public,anon,authenticated; grant execute on function public.review_content_report(uuid,uuid,text,text) to service_role; revoke all on function public.review_map_appeal(uuid,uuid,text,text) from public,anon,authenticated; grant execute on function public.review_map_appeal(uuid,uuid,text,text) to service_role; revoke all on function public.set_beta_badge(uuid,boolean) from public,anon,authenticated; grant execute on function public.set_beta_badge(uuid,boolean) to service_role; revoke all on function public.set_user_prefix(uuid,uuid,text,text) from public,anon,authenticated; grant execute on function public.set_user_prefix(uuid,uuid,text,text) to service_role;