From b17605ae769467d6790795202c9668028c84e90f Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 26 Oct 2026 09:57:35 +0100 Subject: [PATCH] Migration m37: voto MVP segreto, nome dalla rosa, chiusura a 24 ore Lettura diretta di mvp_voti solo del votante e degli admin, conteggi dalla vista mvp_voti_anonimi; trigger che riempie votato_nome dalla rosa; il voto chiude 24 ore dopo l'apertura. Da applicare su Supabase con db push (non ancora fatto). Co-Authored-By: Claude Sonnet 5.5 --- ...170000_m37_voto_mvp_segreto_e_chiusura.sql | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 supabase/migrations/20261026170000_m37_voto_mvp_segreto_e_chiusura.sql diff --git a/supabase/migrations/20261026170000_m37_voto_mvp_segreto_e_chiusura.sql b/supabase/migrations/20261026170000_m37_voto_mvp_segreto_e_chiusura.sql new file mode 100644 index 0000000..258a593 --- /dev/null +++ b/supabase/migrations/20261026170000_m37_voto_mvp_segreto_e_chiusura.sql @@ -0,0 +1,89 @@ +-- M37 — Voto MVP segreto, nome del votato dal database, votazione che si chiude (DD-052) +-- +-- 1. Segretezza. Finora `mvp_voti` era leggibile da tutti, quindi chiunque vedeva chi aveva votato chi. +-- Ora la lettura diretta della tabella è del solo votante (il proprio voto) e degli admin (policy +-- M11, che servono per correggere un voto: dall'app non vedono chi ha votato chi). Tutti gli altri +-- leggono solo i conteggi, dalla vista `mvp_voti_anonimi`: una riga per voto, senza `votante_id`. +-- 2. Nome. `votato_nome` lo scriveva il client, quindi poteva non corrispondere a `votato_id`. Un trigger +-- lo riempie dalla rosa (`giocatori_squadra`) a partire da `votato_id`, ignorando quello inviato. +-- 3. Chiusura. Il voto si apre 2 ore dopo l'inizio della partita (M36) e si chiude 24 ore dopo +-- l'apertura. I valori sono gli stessi di ORE_ATTESA_MVP e DURATA_VOTO_MVP_ORE in +-- src/lib/mvp-voti.ts. Gli admin restano fuori da questo vincolo. + +-- --- 1. Segretezza ------------------------------------------------------------------------------ +DROP POLICY "Chiunque puo leggere i voti MVP" ON public.mvp_voti; + +CREATE POLICY "Ognuno legge il proprio voto MVP" ON public.mvp_voti + FOR SELECT TO authenticated + USING ( + EXISTS ( + SELECT 1 FROM public.giocatori_squadra g + WHERE g.id = votante_id AND g.auth_user_id = auth.uid() + ) + ); + +-- La vista gira con i permessi del proprietario (security_invoker = false): è ciò che le permette di +-- leggere tutte le righe pur senza esporre il votante. L'ordine è per candidato, non per arrivo, così +-- la sequenza dei voti non rivela nulla. +CREATE VIEW public.mvp_voti_anonimi WITH (security_invoker = false) AS + SELECT match_id, votato_id, votato_nome + FROM public.mvp_voti + ORDER BY match_id, votato_id; + +REVOKE ALL ON public.mvp_voti_anonimi FROM PUBLIC, anon, authenticated; +GRANT SELECT ON public.mvp_voti_anonimi TO authenticated; +GRANT ALL ON public.mvp_voti_anonimi TO service_role; + +-- --- 2. Nome del votato dalla rosa ---------------------------------------------------------------- +-- Se `votato_id` non è in rosa (id di prova) resta il nome ricevuto: non c'è niente da sostituire. +CREATE OR REPLACE FUNCTION public.mvp_voti_nome_dalla_rosa() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = public +AS $$ +DECLARE + nome_rosa text; +BEGIN + SELECT btrim(g.nome || ' ' || g.cognome) INTO nome_rosa + FROM public.giocatori_squadra g + WHERE g.id = NEW.votato_id; + IF FOUND THEN + NEW.votato_nome := nome_rosa; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER mvp_voti_nome_dalla_rosa + BEFORE INSERT OR UPDATE OF votato_id, votato_nome ON public.mvp_voti + FOR EACH ROW + EXECUTE FUNCTION public.mvp_voti_nome_dalla_rosa(); + +-- --- 3. Finestra di voto: apre a +2 ore, chiude 24 ore dopo l'apertura ------------------------------ +CREATE OR REPLACE FUNCTION public.voto_mvp_consentito( + p_match_id text, p_votante_id text, p_votato_id text +) RETURNS boolean +LANGUAGE sql STABLE AS $$ + SELECT public.evento_permette_voto(p_match_id, p_votante_id, p_votato_id, false) + AND EXISTS ( + SELECT 1 + FROM public.eventi_app e + CROSS JOIN LATERAL ( + SELECT ((e.data + CASE WHEN e.ora ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' + THEN e.ora::time ELSE time '00:00' END) + AT TIME ZONE 'Europe/Rome') AS inizio + ) t + WHERE e.id = p_match_id + AND e.tipo = 'partita' + AND t.inizio + interval '2 hours' <= now() + AND now() < t.inizio + interval '2 hours' + interval '24 hours' + ) + -- votante e votato sono diversi (mvp_no_autovoto): due righe = entrambi presenti + AND ( + SELECT count(DISTINCT r.giocatore_id) + FROM public.risposte_presenze r + WHERE r.evento_id = p_match_id + AND r.giocatore_id IN (p_votante_id, p_votato_id) + AND r.stato IN ('presente', 'ritardo') + ) = 2; +$$;