Aggiunge il ruolo allenatore al database (M20, M21)
L'allenatore è uno slot di giocatori_squadra con tipo = 'allenatore' (DD-034): - M20 aggiunge il valore allenatore ad app_role, da solo perché Postgres non lascia usare un valore di enum nella transazione che lo crea; - M21 aggiunge la colonna tipo, rende numero nullabile solo per gli allenatori, blocca tipo nel trigger sugli slot e lascia all'allenatore nome e cognome del proprio slot; un trigger tiene allineato il ruolo in user_roles al collegamento; - eventi_app scrivibile da admin e allenatori; presenze, cacche e voti chiusi all'allenatore; i promemoria evento lo includono sempre. Compatibile con la 0.9.2: senza allenatori registrati non cambia nulla. Già applicata in produzione con supabase db push; g18 «Beta Tester» è allenatore. Il test permessi-allenatore verifica le regole contro il database locale. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
dda4aff746
commit
15a010855e
6 files changed
+601
-42
No files matched your search
@@ -0,0 +1,6 @@
|
||||
-- M20 — Nuovo valore `allenatore` per `app_role` (DD-034).
|
||||
--
|
||||
-- Sta da solo perché Postgres non permette di usare un valore di enum nella stessa
|
||||
-- transazione che lo aggiunge: le policy e le funzioni che lo citano stanno in M21.
|
||||
|
||||
ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'allenatore';
|
||||
@@ -0,0 +1,206 @@
|
||||
-- M21 — Ruolo allenatore (DD-034, docs/modules/allenatore.md).
|
||||
--
|
||||
-- L'allenatore è una riga di giocatori_squadra con `tipo = 'allenatore'`: si collega per
|
||||
-- email come i giocatori (DD-018) e riusa profilo, avatar e compleanno. Non gioca, quindi
|
||||
-- non ha numero di maglia né ruolo in campo, non risponde alle presenze, non dichiara
|
||||
-- cacche, non vota e non viene votato. Gestisce gli eventi come un admin.
|
||||
--
|
||||
-- Il permesso vive in user_roles (DD-011) come valore `allenatore` di app_role (M20). Non
|
||||
-- lo scrive nessuno a mano: lo tiene allineato un trigger su giocatori_squadra, quindi è
|
||||
-- l'admin a concederlo registrando lo slot, e l'allenatore non può auto-promuoversi perché
|
||||
-- `tipo` lo cambia solo un admin.
|
||||
|
||||
-- --- Colonna tipo --------------------------------------------------------------------------
|
||||
|
||||
ALTER TABLE public.giocatori_squadra
|
||||
ADD COLUMN tipo text NOT NULL DEFAULT 'giocatore'
|
||||
CONSTRAINT giocatori_squadra_tipo_valido CHECK (tipo IN ('giocatore', 'allenatore'));
|
||||
|
||||
COMMENT ON COLUMN public.giocatori_squadra.tipo IS
|
||||
'giocatore (in rosa, convocabile, votabile) oppure allenatore (gestisce gli eventi, fuori '
|
||||
'dalla rosa di gioco). Scrivibile solo da un admin (DD-034).';
|
||||
|
||||
-- L'allenatore non ha numero di maglia: resta obbligatorio solo per i giocatori.
|
||||
ALTER TABLE public.giocatori_squadra ALTER COLUMN numero DROP NOT NULL;
|
||||
ALTER TABLE public.giocatori_squadra
|
||||
ADD CONSTRAINT giocatori_squadra_numero_giocatori
|
||||
CHECK (tipo = 'allenatore' OR numero IS NOT NULL);
|
||||
|
||||
-- --- Trigger sugli slot: tipo solo admin, nome e cognome anche all'allenatore ---------------
|
||||
--
|
||||
-- Stessi due rami di M18 (reclamo dello slot libero, sync della nascita sul proprio slot),
|
||||
-- con `tipo` tra i campi bloccati. Il secondo ramo lascia cambiare nome e cognome solo se
|
||||
-- lo slot è di un allenatore: per un giocatore restano dati squadra gestiti dall'admin.
|
||||
CREATE OR REPLACE FUNCTION public.enforce_giocatori_squadra_update()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
IF public.has_role(auth.uid(), 'admin'::public.app_role) THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF OLD.auth_user_id IS NULL
|
||||
AND NEW.auth_user_id = auth.uid()
|
||||
AND OLD.email IS NOT NULL
|
||||
AND lower(OLD.email) = lower(coalesce(auth.jwt() ->> 'email', ''))
|
||||
AND NEW.id IS NOT DISTINCT FROM OLD.id
|
||||
AND NEW.nome IS NOT DISTINCT FROM OLD.nome
|
||||
AND NEW.cognome IS NOT DISTINCT FROM OLD.cognome
|
||||
AND NEW.numero IS NOT DISTINCT FROM OLD.numero
|
||||
AND NEW.ruolo IS NOT DISTINCT FROM OLD.ruolo
|
||||
AND NEW.attivo IS NOT DISTINCT FROM OLD.attivo
|
||||
AND NEW.email IS NOT DISTINCT FROM OLD.email
|
||||
AND NEW.numero_tessera IS NOT DISTINCT FROM OLD.numero_tessera
|
||||
AND NEW.data_tessera IS NOT DISTINCT FROM OLD.data_tessera
|
||||
AND NEW.tipo IS NOT DISTINCT FROM OLD.tipo
|
||||
AND NEW.creato_il IS NOT DISTINCT FROM OLD.creato_il THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- Slot già collegato al chiamante: cambia `nascita` (sync di M18) e, se è un allenatore,
|
||||
-- nome e cognome.
|
||||
IF OLD.auth_user_id IS NOT NULL
|
||||
AND OLD.auth_user_id = auth.uid()
|
||||
AND NEW.auth_user_id IS NOT DISTINCT FROM OLD.auth_user_id
|
||||
AND NEW.id IS NOT DISTINCT FROM OLD.id
|
||||
AND (OLD.tipo = 'allenatore' OR NEW.nome IS NOT DISTINCT FROM OLD.nome)
|
||||
AND (OLD.tipo = 'allenatore' OR NEW.cognome IS NOT DISTINCT FROM OLD.cognome)
|
||||
AND NEW.numero IS NOT DISTINCT FROM OLD.numero
|
||||
AND NEW.ruolo IS NOT DISTINCT FROM OLD.ruolo
|
||||
AND NEW.attivo IS NOT DISTINCT FROM OLD.attivo
|
||||
AND NEW.email IS NOT DISTINCT FROM OLD.email
|
||||
AND NEW.numero_tessera IS NOT DISTINCT FROM OLD.numero_tessera
|
||||
AND NEW.data_tessera IS NOT DISTINCT FROM OLD.data_tessera
|
||||
AND NEW.tipo IS NOT DISTINCT FROM OLD.tipo
|
||||
AND NEW.creato_il IS NOT DISTINCT FROM OLD.creato_il THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
RAISE EXCEPTION 'Aggiornamento non autorizzato su giocatori_squadra';
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Fino a qui un giocatore non poteva aggiornare una riga già collegata (solo il reclamo di
|
||||
-- uno slot libero, M1): l'allenatore ora può farlo sulla propria, per nome e cognome. Il
|
||||
-- trigger sopra decide quali campi.
|
||||
CREATE POLICY "L'allenatore aggiorna il proprio slot" ON public.giocatori_squadra
|
||||
FOR UPDATE TO authenticated
|
||||
USING (auth_user_id = auth.uid() AND tipo = 'allenatore')
|
||||
WITH CHECK (auth_user_id = auth.uid() AND tipo = 'allenatore');
|
||||
|
||||
-- --- Ruolo in user_roles allineato allo slot ---------------------------------------------
|
||||
--
|
||||
-- Un account ha il ruolo `allenatore` finché è collegato a uno slot attivo di tipo
|
||||
-- allenatore. Scollegare, disattivare o cambiare tipo lo toglie.
|
||||
CREATE OR REPLACE FUNCTION public.sincronizza_ruolo_allenatore()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE' AND OLD.auth_user_id IS NOT NULL
|
||||
AND (NEW.auth_user_id IS DISTINCT FROM OLD.auth_user_id
|
||||
OR NEW.tipo <> 'allenatore' OR NOT NEW.attivo) THEN
|
||||
DELETE FROM public.user_roles
|
||||
WHERE user_id = OLD.auth_user_id AND role = 'allenatore'::public.app_role;
|
||||
END IF;
|
||||
|
||||
IF NEW.auth_user_id IS NOT NULL AND NEW.tipo = 'allenatore' AND NEW.attivo THEN
|
||||
INSERT INTO public.user_roles (user_id, role)
|
||||
VALUES (NEW.auth_user_id, 'allenatore'::public.app_role)
|
||||
ON CONFLICT (user_id, role) DO NOTHING;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER sincronizza_ruolo_allenatore
|
||||
AFTER INSERT OR UPDATE OF auth_user_id, tipo, attivo ON public.giocatori_squadra
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.sincronizza_ruolo_allenatore();
|
||||
|
||||
-- --- Eventi: admin o allenatore -----------------------------------------------------------
|
||||
DROP POLICY "Gli admin gestiscono gli eventi" ON public.eventi_app;
|
||||
|
||||
CREATE POLICY "Admin e allenatori gestiscono gli eventi" ON public.eventi_app
|
||||
FOR ALL TO authenticated
|
||||
USING (
|
||||
public.has_role(auth.uid(), 'admin'::public.app_role)
|
||||
OR public.has_role(auth.uid(), 'allenatore'::public.app_role)
|
||||
)
|
||||
WITH CHECK (
|
||||
public.has_role(auth.uid(), 'admin'::public.app_role)
|
||||
OR public.has_role(auth.uid(), 'allenatore'::public.app_role)
|
||||
);
|
||||
|
||||
-- --- Presenze e cacche: solo i giocatori rispondono per sé --------------------------------
|
||||
DROP POLICY "Ognuno gestisce la propria presenza" ON public.risposte_presenze;
|
||||
|
||||
CREATE POLICY "Ognuno gestisce la propria presenza" ON public.risposte_presenze
|
||||
FOR ALL TO authenticated
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.giocatori_squadra g
|
||||
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid() AND g.tipo = 'giocatore'
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.giocatori_squadra g
|
||||
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid() AND g.tipo = 'giocatore'
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY "Ognuno dichiara le proprie cacche" ON public.cacche_partita;
|
||||
|
||||
CREATE POLICY "Ognuno dichiara le proprie cacche" ON public.cacche_partita
|
||||
FOR ALL TO authenticated
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.giocatori_squadra g
|
||||
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid() AND g.tipo = 'giocatore'
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.giocatori_squadra g
|
||||
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid() AND g.tipo = 'giocatore'
|
||||
)
|
||||
);
|
||||
|
||||
-- --- Voti: l'allenatore non vota e non viene votato ---------------------------------------
|
||||
--
|
||||
-- Stessa firma di M13, quindi le policy di pagelle, MVP e badge social la usano già.
|
||||
CREATE OR REPLACE FUNCTION public.evento_permette_voto(
|
||||
p_match_id text, p_votante_id text, p_votato_id text, p_richiede_pagelle_aperte boolean
|
||||
) RETURNS boolean
|
||||
LANGUAGE sql STABLE AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.eventi_app e
|
||||
WHERE e.id = p_match_id
|
||||
AND (cardinality(e.convocati) = 0 OR p_votante_id = ANY(e.convocati))
|
||||
AND (cardinality(e.convocati) = 0 OR p_votato_id = ANY(e.convocati))
|
||||
AND (NOT p_richiede_pagelle_aperte OR NOT e.pagelle_chiuse)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.giocatori_squadra g
|
||||
WHERE g.id IN (p_votante_id, p_votato_id) AND g.tipo = 'allenatore'
|
||||
);
|
||||
$$;
|
||||
|
||||
-- --- Promemoria evento: anche agli allenatori, convocati o no -----------------------------
|
||||
CREATE OR REPLACE FUNCTION public.giocatori_destinatari_evento(p_evento_id text)
|
||||
RETURNS SETOF text
|
||||
LANGUAGE sql STABLE AS $$
|
||||
SELECT g.id
|
||||
FROM public.eventi_app e
|
||||
JOIN public.giocatori_squadra g
|
||||
ON g.attivo = true
|
||||
AND (g.tipo = 'allenatore' OR cardinality(e.convocati) = 0 OR g.id = ANY(e.convocati))
|
||||
WHERE e.id = p_evento_id;
|
||||
$$;
|
||||
Reference in new issue
Block a user