alter table public.profiles add column username text; alter table public.profiles add constraint profiles_username_format check (username is null or username ~ '^[a-z0-9_]{3,32}$'); create unique index profiles_username_unique on public.profiles (username) where username is not null; create table public.moderator_applications ( id uuid primary key default gen_random_uuid(), platform text not null check (platform in ('telegram', 'discord')), platform_user_id text not null, platform_username text, requested_username text not null, profile_id uuid references public.profiles(id) on delete set null, confirmation_hash text not null unique, status text not null default 'confirming' check (status in ('confirming', 'pending', 'approved', 'declined')), expires_at timestamptz not null default (now() + interval '30 minutes'), created_at timestamptz not null default now(), reviewed_at timestamptz, reviewer_discord_id text, admin_notified_at timestamptz, applicant_notified_at timestamptz ); create index moderator_applications_queue on public.moderator_applications (created_at) where status = 'pending'; create unique index moderator_applications_one_open on public.moderator_applications (platform, platform_user_id) where status = 'pending'; alter table public.moderator_applications enable row level security; revoke all on public.moderator_applications from public, anon, authenticated; grant all on public.moderator_applications to service_role; create table public.moderator_bot_admin (id boolean primary key default true check (id), discord_user_id text not null); alter table public.moderator_bot_admin enable row level security; revoke all on public.moderator_bot_admin from public, anon, authenticated; grant all on public.moderator_bot_admin to service_role; create function public.review_moderator_application(p_application uuid, p_decision text, p_reviewer text) returns text language plpgsql security invoker set search_path = '' as $$ declare application public.moderator_applications; current_role text; current_status text; begin if not exists (select 1 from public.moderator_bot_admin where discord_user_id = p_reviewer) then raise exception 'Admin permission required'; end if; if p_decision not in ('approved', 'declined') then raise exception 'Invalid decision'; end if; select * into application from public.moderator_applications where id = p_application for update; if not found or application.status <> 'pending' then raise exception 'Application is no longer pending'; end if; if p_decision = 'approved' then select role, account_status into current_role, current_status from public.profiles where id = application.profile_id for update; if not found or current_status <> 'active' or current_role <> 'contributor' then raise exception 'Account cannot be promoted'; end if; update public.profiles set role = 'moderator' where id = application.profile_id; end if; update public.moderator_applications set status = p_decision, reviewed_at = now(), reviewer_discord_id = p_reviewer where id = p_application; return p_decision; end; $$; revoke all on function public.review_moderator_application(uuid,text,text) from public, anon, authenticated; grant execute on function public.review_moderator_application(uuid,text,text) to service_role;