Ajoute les suggestions de questions des Citoyens et les emails sur Admin
Build and deploy / deploy (push) Successful in 35s

Chaque Citoyen peut proposer une question pour Le Char depuis /profile
(upsert via submit_chariot_question, figée dès la date du Tribunal
comme les scores d'Icare). Les Archontes modèrent ces suggestions
directement sur /char/questions (ajouter à la banque ou rejeter).

/admin affiche maintenant l'email de chaque membre via une nouvelle
RPC admin_list_members(), seule façon d'exposer auth.users.email sans
passer par la service_role key côté client.

Corrige au passage deux bugs découverts pendant les tests : la
policy interne de admin_list_members() référençait id/role sans les
qualifier, ambigus avec les colonnes du RETURNS TABLE (erreur
Postgres 42702) ; et l'auteur d'une suggestion s'affichait toujours
comme "un Citoyen" car le embed profiles(...) avait été mal retypé en
tableau alors qu'il est retourné en objet à l'exécution.
This commit is contained in:
Valentin ROBIN
2026-08-19 01:22:36 +02:00
parent 0c4814f4d6
commit 76fa8cd451
8 changed files with 411 additions and 56 deletions
+124
View File
@@ -861,3 +861,127 @@ exception
when others then
raise notice 'pg_cron indisponible : active l''extension via le Dashboard Supabase (Database → Extensions → pg_cron), puis ré-exécute ce script pour planifier le reroll automatique du Gardien.';
end $$;
-- 13. Suggestions de questions par les Citoyens + emails sur Admin
-- -----------------------------------------------------------------
-- Une proposition par Citoyen (une ligne par user_id, comme icarus_scores).
-- Verrouillée en écriture comme points_log/icarus_scores : seule la RPC
-- submit_chariot_question() (SECURITY DEFINER) peut insert/update ; les
-- juges gardent un accès direct en lecture (modération) et en suppression
-- (rejet), sans passer par une RPC pour ce cas simple.
create table if not exists public.chariot_submissions (
user_id uuid primary key references public.profiles (id) on delete cascade,
text text not null,
updated_at timestamptz not null default now()
);
alter table public.chariot_submissions enable row level security;
revoke insert, update on public.chariot_submissions from authenticated, anon;
grant select, delete on public.chariot_submissions to authenticated;
drop policy if exists "chariot_submissions readable by owner or judges" on public.chariot_submissions;
create policy "chariot_submissions readable by owner or judges"
on public.chariot_submissions for select to authenticated
using (
user_id = auth.uid()
or exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge')
);
drop policy if exists "chariot_submissions delete by judges" on public.chariot_submissions;
create policy "chariot_submissions delete by judges" on public.chariot_submissions for delete to authenticated
using (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'));
do $$
begin
alter publication supabase_realtime add table public.chariot_submissions;
exception
when duplicate_object then null;
end $$;
-- Seul point d'entrée pour proposer/modifier sa question. Réutilise
-- settings.tribunal_date comme déclencheur de gel — même philosophie que
-- submit_icarus_score() : une fois l'Agora commencée, no-op silencieux
-- (retourne la ligne existante sans la modifier) plutôt qu'une erreur.
create or replace function public.submit_chariot_question(p_text text)
returns public.chariot_submissions
language plpgsql
security definer
set search_path = public
as $$
declare
v_caller_role text;
v_tribunal_date timestamptz;
v_row public.chariot_submissions;
begin
if auth.uid() is null then
raise exception 'authentication required';
end if;
select role into v_caller_role from public.profiles where id = auth.uid();
if v_caller_role = 'judge' then
raise exception 'only citizens can submit a chariot question';
end if;
if p_text is null or length(trim(p_text)) = 0 then
raise exception 'question vide';
end if;
if length(p_text) > 300 then
raise exception 'question trop longue';
end if;
select tribunal_date into v_tribunal_date from public.settings where id = true;
if v_tribunal_date is not null and now() >= v_tribunal_date then
select * into v_row from public.chariot_submissions where user_id = auth.uid();
return v_row;
end if;
insert into public.chariot_submissions (user_id, text, updated_at)
values (auth.uid(), trim(p_text), now())
on conflict (user_id) do update
set text = excluded.text,
updated_at = now();
select * into v_row from public.chariot_submissions where user_id = auth.uid();
return v_row;
end;
$$;
grant execute on function public.submit_chariot_question(text) to authenticated;
-- Émails des membres pour /admin : jamais stockés dans profiles (voir §4 du
-- doc — le pseudo est le seul nom montré aux autres), donc invisibles côté
-- client sans passer par auth.users. Plutôt que d'exposer la service_role
-- key côté client, une RPC SECURITY DEFINER fait la jointure, gardée par la
-- même vérification de rôle que les autres RPC juge-only du projet.
create or replace function public.admin_list_members()
returns table (
id uuid,
pseudo text,
avatar_url text,
role text,
pseudo_locked boolean,
email text
)
language plpgsql
security definer
set search_path = public
as $$
begin
-- Colonnes de sortie qualifiées par un alias partout : returns table(...)
-- déclare id/role comme variables PL/pgSQL, qui masqueraient sinon les
-- colonnes de même nom dans profiles (erreur 42702 "ambiguous").
if not exists (select 1 from public.profiles pr where pr.id = auth.uid() and pr.role = 'judge') then
raise exception 'only judges can list members';
end if;
return query
select p.id, p.pseudo, p.avatar_url, p.role, p.pseudo_locked, u.email::text
from public.profiles p
join auth.users u on u.id = p.id
order by p.pseudo asc;
end;
$$;
grant execute on function public.admin_list_members() to authenticated;