Files
MineGeo/supabase/migrations/20261009123642_admin_permissions.sql

120 lines
9.0 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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;