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
+14
-8
@@ -1,6 +1,6 @@
|
||||
# Project State
|
||||
|
||||
Ultimo aggiornamento: 15/09/2026
|
||||
Ultimo aggiornamento: 24/09/2026
|
||||
|
||||
## Stato generale
|
||||
|
||||
@@ -23,10 +23,8 @@ cancellazioni precedenti a M14 sono state bonificate una tantum (M15/M16).
|
||||
- Cursor e Claude Code come ambienti di sviluppo
|
||||
- Vercel configurato; Environment Variables aggiornate al nuovo Supabase (Preview e Production)
|
||||
- Supabase proprietario attivo — Project Ref: `kfkcldwncxqaixetsjes`
|
||||
- 30 migration in `supabase/migrations/`, fino a `m19_backfill_nascita_da_profili_esistenti`
|
||||
(15/09/2026). `m17_notifiche_utente` e `m18_nascita_pubblica_giocatori_squadra` sono state
|
||||
applicate in produzione lo stesso giorno (`supabase db push`); `m19` (fix del backfill di
|
||||
M18, vedi sotto) è ancora solo in locale
|
||||
- 32 migration in `supabase/migrations/`, fino a `m21_ruolo_allenatore` (24/09/2026), tutte
|
||||
applicate in produzione (verificato con `npx supabase migration list` il 24/09/2026)
|
||||
- Sviluppo locale verificato con il nuovo Supabase
|
||||
|
||||
---
|
||||
@@ -54,9 +52,17 @@ cancellazioni precedenti a M14 sono state bonificate una tantum (M15/M16).
|
||||
seed storico. **In produzione** dal 15/09/2026, ma il backfill copriva solo il seed
|
||||
storico: chi aveva già un profilo compilato prima di M18 è rimasto scoperto
|
||||
- Migration `m19_backfill_nascita_da_profili_esistenti` (15/09/2026, DD-031): completa il
|
||||
backfill di M18 per tutti i profili già esistenti — **non ancora applicata in produzione**,
|
||||
va fatta con `supabase db push` appena possibile (M18 da sola in produzione lascia la
|
||||
nascita vuota per chi non è nel seed storico e non risalva il profilo)
|
||||
backfill di M18 per tutti i profili già esistenti — **in produzione** (risulta applicata
|
||||
in `npx supabase migration list` del 24/09/2026)
|
||||
- Migration `m20_ruolo_allenatore_enum` e `m21_ruolo_allenatore` (24/09/2026, DD-034): ruolo
|
||||
allenatore (colonna `giocatori_squadra.tipo`, valore `allenatore` di `app_role`, policy e
|
||||
trigger) — verificate in locale con `npx supabase db reset` e `npm run test:all`, **in
|
||||
produzione** dal 24/09/2026 (`supabase db push`). Compatibili con la 0.9.2: senza allenatori
|
||||
registrati non cambiano nulla. Il codice che le usa non è ancora deployato
|
||||
- Slot `g18` «Beta Tester» impostato come allenatore in produzione il 24/09/2026 (colonna
|
||||
`tipo`, ruolo assegnato dal trigger di M21). Numero 99 e ruolo «Palleggiatore»
|
||||
lasciati apposta finché gira la 0.9.2, che lo mostra ancora come giocatore: da svuotare
|
||||
da `/admin` dopo il deploy del codice nuovo
|
||||
- `public.giocatori_squadra`: rosa iniziale di 17 giocatori (migration `m5_email_giocatori_squadra`)
|
||||
più quelli aggiunti da `/admin` a stagione in corso; da settembre 2026 tutti i giocatori
|
||||
attivi hanno l'email registrata (colonna `email`, DD-018), impostabile da `/admin` senza
|
||||
|
||||
+33
-32
@@ -7,32 +7,33 @@ non in questo file.
|
||||
|
||||
## Permessi di scrittura
|
||||
|
||||
Chi può scrivere cosa, dopo la migration `m11_scritture_per_ruolo` (DD-023). La **lettura**
|
||||
Chi può scrivere cosa, dopo la migration `m11_scritture_per_ruolo` (DD-023) e il ruolo
|
||||
allenatore di `m21_ruolo_allenatore` (DD-034). La **lettura**
|
||||
resta aperta a tutti gli autenticati su ogni tabella di questo elenco; `anon` non arriva a
|
||||
nessuna di esse da M4 (DD-011).
|
||||
|
||||
| Tabella | Chi può scrivere |
|
||||
| ---------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `eventi_app` | solo admin (nell'app li gestisce la rotta `/eventi`, già riservata) |
|
||||
| `risposte_presenze`, `cacche_partita` | il giocatore sulla propria riga (`giocatore_id`), più gli admin |
|
||||
| `pagelle_voti`, `mvp_voti`, `badge_social_voti` | il votante sui propri voti (`votante_id`), se votante e votato sono convocati all'evento (`m13`); solo per le pagelle anche `pagelle_chiuse = false`; gli admin senza questi vincoli |
|
||||
| `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite` | qualsiasi autenticato: nell'interfaccia non hanno gate |
|
||||
| `notifiche_utente` | nessuno scrive da client: le righe nascono da trigger/funzioni `SECURITY DEFINER` o dalla service role (M17); il giocatore può solo segnare come lette le proprie |
|
||||
| `profili_giocatore` | il giocatore sul proprio profilo, admin su tutti (DD-016, DD-017) |
|
||||
| `giocatori_squadra` | admin; il giocatore può solo reclamare uno slot libero (DD-016) |
|
||||
| `user_roles` | solo admin |
|
||||
| Tabella | Chi può scrivere |
|
||||
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `eventi_app` | admin e allenatori (nell'app li gestisce la rotta `/eventi`, riservata a loro) |
|
||||
| `risposte_presenze`, `cacche_partita` | il giocatore sulla propria riga (`giocatore_id`), mai l'allenatore (M21), più gli admin |
|
||||
| `pagelle_voti`, `mvp_voti`, `badge_social_voti` | il votante sui propri voti (`votante_id`), se votante e votato sono convocati all'evento (`m13`) e nessuno dei due è un allenatore (`m21`); solo per le pagelle anche `pagelle_chiuse = false`; gli admin senza questi vincoli |
|
||||
| `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite` | qualsiasi autenticato: nell'interfaccia non hanno gate |
|
||||
| `notifiche_utente` | nessuno scrive da client: le righe nascono da trigger/funzioni `SECURITY DEFINER` o dalla service role (M17); il giocatore può solo segnare come lette le proprie |
|
||||
| `profili_giocatore` | il giocatore sul proprio profilo, admin su tutti (DD-016, DD-017) |
|
||||
| `giocatori_squadra` | admin; il giocatore può solo reclamare uno slot libero (DD-016); l'allenatore anche cambiare nome e cognome del proprio slot (M21) |
|
||||
| `user_roles` | solo admin; la riga `allenatore` la scrive il trigger `sincronizza_ruolo_allenatore` (M21) |
|
||||
|
||||
L'identità del giocatore è lo slot di `giocatori_squadra` con `auth_user_id = auth.uid()`.
|
||||
La tabella è verificata da `test/integration/permessi.test.ts` contro il database locale.
|
||||
|
||||
## Anagrafica e utenti
|
||||
|
||||
| Tabella | Scopo | Note |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `giocatori_squadra` | Anagrafica operativa della squadra, con ID testuali (`g1`…`gN`), dati gestiti dagli admin (nome, cognome, numero, ruolo), collegamento all'account (`auth_user_id`) ed email registrata (`email`). | Introdotta dalla migration `m1_giocatori_squadra`, source of truth della rosa (DD-015): `useRosa()` e gli altri punti che elencano i giocatori la leggono tramite `useGiocatoriSquadra()` (client) o `leggiGiocatoriSquadra()` (server), filtrando `attivo`. `src/lib/crapp-data.ts` resta solo come seed storico e fallback (`rosaFallback()`) quando il database non risponde. Vedi DD-015 e DD-016. La colonna `email` (migration `m5_email_giocatori_squadra`, impostabile anche da `/admin`) è la chiave del collegamento automatico account↔giocatore al primo accesso (DD-018): NULL finché non nota, oggi impostata per tutta la rosa attiva. Le colonne `numero_tessera`/`data_tessera` (migration `m8_tesseramento_csi`) tracciano chi è già tesserato al CSI; come `numero`/`ruolo` le scrive solo un admin, il trigger di M1/M5 le include tra i campi bloccati per chi reclama il proprio slot. La colonna `nascita` (migration `m18_nascita_pubblica_giocatori_squadra`, DD-031) è pubblica a tutta la rosa attiva (stessa policy di lettura delle altre colonne): non si scrive mai da qui, un trigger su `profili_giocatore` (`sincronizza_nascita_pubblica`) la tiene allineata a `data_nascita` — impostata, aggiornata o azzerata dallo stesso form Profilo che scrive quella colonna. Il trigger copre solo le scritture successive a M18: `m19_backfill_nascita_da_profili_esistenti` ha allineato una tantum anche chi aveva già compilato il profilo prima di quella migration. |
|
||||
| `giocatori` | Anagrafica giocatori con UUID. | Presente ma **non usata** dal codice attuale: la convergenza è rinviata (DD-012, DD-014). |
|
||||
| `profili_giocatore` | Dati personali, metadati del documento d'identità, certificato medico e path dei file, in relazione 1:1 con `giocatori_squadra`. | Creata dalla migration `m2_profili_giocatore` (DD-016). Letta da `src/lib/profili.ts`; le policy mostrano al giocatore solo il proprio profilo e all'admin tutti. I file non stanno qui: la tabella conserva i path nel bucket. |
|
||||
| `user_roles` | Ruoli applicativi (es. amministratore, giocatore). | Fonte dei permessi di amministrazione, letta da `src/lib/ruoli.ts` (DD-011). Il primo admin va inserito a mano; vedi [PROJECT_STATE.md](../PROJECT_STATE.md). |
|
||||
| Tabella | Scopo | Note |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `giocatori_squadra` | Anagrafica operativa della squadra, con ID testuali (`g1`…`gN`), dati gestiti dagli admin (nome, cognome, numero, ruolo), collegamento all'account (`auth_user_id`) ed email registrata (`email`). | Introdotta dalla migration `m1_giocatori_squadra`, source of truth della rosa (DD-015): `useRosa()` e gli altri punti che elencano i giocatori la leggono tramite `useGiocatoriSquadra()` (client) o `leggiGiocatoriSquadra()` (server), filtrando `attivo`. `src/lib/crapp-data.ts` resta solo come seed storico e fallback (`rosaFallback()`) quando il database non risponde. Vedi DD-015 e DD-016. La colonna `email` (migration `m5_email_giocatori_squadra`, impostabile anche da `/admin`) è la chiave del collegamento automatico account↔giocatore al primo accesso (DD-018): NULL finché non nota, oggi impostata per tutta la rosa attiva. Le colonne `numero_tessera`/`data_tessera` (migration `m8_tesseramento_csi`) tracciano chi è già tesserato al CSI; come `numero`/`ruolo` le scrive solo un admin, il trigger di M1/M5 le include tra i campi bloccati per chi reclama il proprio slot. La colonna `nascita` (migration `m18_nascita_pubblica_giocatori_squadra`, DD-031) è pubblica a tutta la rosa attiva (stessa policy di lettura delle altre colonne): non si scrive mai da qui, un trigger su `profili_giocatore` (`sincronizza_nascita_pubblica`) la tiene allineata a `data_nascita` — impostata, aggiornata o azzerata dallo stesso form Profilo che scrive quella colonna. Il trigger copre solo le scritture successive a M18: `m19_backfill_nascita_da_profili_esistenti` ha allineato una tantum anche chi aveva già compilato il profilo prima di quella migration. La colonna `tipo` (migration `m21_ruolo_allenatore`, DD-034) distingue `giocatore` da `allenatore`: l'allenatore ha uno slot per collegarsi e per i dati personali, ma è fuori dalla rosa di gioco (`inRosa()` nel codice), non ha numero di maglia (`numero` è NULL, ammesso solo per lui dal vincolo `giocatori_squadra_numero_giocatori`) e ha `ruolo` vuoto. `tipo` lo scrive solo un admin. |
|
||||
| `giocatori` | Anagrafica giocatori con UUID. | Presente ma **non usata** dal codice attuale: la convergenza è rinviata (DD-012, DD-014). |
|
||||
| `profili_giocatore` | Dati personali, metadati del documento d'identità, certificato medico e path dei file, in relazione 1:1 con `giocatori_squadra`. | Creata dalla migration `m2_profili_giocatore` (DD-016). Letta da `src/lib/profili.ts`; le policy mostrano al giocatore solo il proprio profilo e all'admin tutti. I file non stanno qui: la tabella conserva i path nel bucket. |
|
||||
| `user_roles` | Ruoli applicativi (es. amministratore, giocatore). | Fonte dei permessi di amministrazione, letta da `src/lib/ruoli.ts` (DD-011). Il primo admin va inserito a mano; vedi [PROJECT_STATE.md](../PROJECT_STATE.md). Il valore `allenatore` di `app_role` (migration `m20_ruolo_allenatore_enum`) non si inserisce a mano: lo aggiunge e lo toglie il trigger `sincronizza_ruolo_allenatore` quando uno slot con `tipo = 'allenatore'` viene collegato, scollegato, disattivato o cambia tipo (DD-034). |
|
||||
|
||||
`giocatori_squadra` / `giocatori` sono usate da: Squadra, Profili, Presenze, Scout, Badge, Pagelle.
|
||||
|
||||
@@ -45,12 +46,12 @@ La tabella è verificata da `test/integration/permessi.test.ts` contro il databa
|
||||
|
||||
## Eventi e presenze
|
||||
|
||||
| Tabella | Scopo | Note |
|
||||
| ------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `eventi_app` | Eventi gestionali utilizzati dall'app. | Modello in uso dal codice attuale. Cancellare un evento pulisce a cascata, tramite trigger, tutte le tabelle collegate elencate in questa pagina (`risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite`) — migration `m14_pulizia_dati_evento_cancellato`, DD-029. Le righe orfane da cancellazioni precedenti sono state bonificate una tantum da `m15_bonifica_dati_evento_orfani`, senza toccare i vecchi voti MVP/pagelle/badge social legati a id Scout o CSI; la stessa logica resta richiamabile come funzione `bonifica_dati_evento_orfani()` (`m16_funzione_bonifica_dati_evento_orfani`, riservata al service role) se mai servisse di nuovo. |
|
||||
| `risposte_presenze` | Risposte dei giocatori agli eventi. | Modello in uso dal codice attuale. `risposto_il` è l'istante della **prima** risposta (migration `m9_risposte_presenze_risposto_il`): confrontato con `eventi_app.creato_il` dà la serie "Conferme 24h". Un trigger lo rende immutabile, così un ripensamento non fa risultare rapida una risposta lenta — `aggiornato_il` resta l'ultima modifica. |
|
||||
| `eventi` | Calendario generale: allenamenti, partite, eventi della squadra. | Modello "nuovo" con autenticazione e vincoli, non ancora adottato (DD-014). |
|
||||
| `presenze` | Presenze agli eventi. | Come sopra (DD-014). |
|
||||
| Tabella | Scopo | Note |
|
||||
| ------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `eventi_app` | Eventi gestionali utilizzati dall'app. | Modello in uso dal codice attuale. Cancellare un evento pulisce a cascata, tramite trigger, tutte le tabelle collegate elencate in questa pagina (`risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite`) — migration `m14_pulizia_dati_evento_cancellato`, DD-029. Le righe orfane da cancellazioni precedenti sono state bonificate una tantum da `m15_bonifica_dati_evento_orfani`, senza toccare i vecchi voti MVP/pagelle/badge social legati a id Scout o CSI; la stessa logica resta richiamabile come funzione `bonifica_dati_evento_orfani()` (`m16_funzione_bonifica_dati_evento_orfani`, riservata al service role) se mai servisse di nuovo. |
|
||||
| `risposte_presenze` | Risposte dei giocatori agli eventi. | Modello in uso dal codice attuale. `risposto_il` è l'istante della **prima** risposta (migration `m9_risposte_presenze_risposto_il`): confrontato con `eventi_app.creato_il` dà la serie "Conferme 24h". Un trigger lo rende immutabile, così un ripensamento non fa risultare rapida una risposta lenta — `aggiornato_il` resta l'ultima modifica. |
|
||||
| `eventi` | Calendario generale: allenamenti, partite, eventi della squadra. | Modello "nuovo" con autenticazione e vincoli, non ancora adottato (DD-014). |
|
||||
| `presenze` | Presenze agli eventi. | Come sopra (DD-014). |
|
||||
|
||||
## Scout
|
||||
|
||||
@@ -62,20 +63,20 @@ La tabella è verificata da `test/integration/permessi.test.ts` contro il databa
|
||||
|
||||
## Votazioni
|
||||
|
||||
| Tabella | Scopo | Note |
|
||||
| ------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| Tabella | Scopo | Note |
|
||||
| ------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `mvp_voti` | Voti MVP assegnati a fine partita. | Un voto per votante e partita; auto-voto rifiutato (`mvp_no_autovoto`, migration `m12_niente_autovoto`); votante e votato devono essere convocati all'evento (RLS, `m13_convocati_e_pagelle_chiuse`). |
|
||||
| `pagelle_voti` | Voti anonimi assegnati ai giocatori. | Usati per il voto medio. Voto 1-10 e auto-voto rifiutato dai vincoli originari della tabella; votante/votato convocati e `pagelle_chiuse = false` richiesti dalla RLS di `m13_convocati_e_pagelle_chiuse`. |
|
||||
| `pagelle_voti` | Voti anonimi assegnati ai giocatori. | Usati per il voto medio. Voto 1-10 e auto-voto rifiutato dai vincoli originari della tabella; votante/votato convocati e `pagelle_chiuse = false` richiesti dalla RLS di `m13_convocati_e_pagelle_chiuse`. |
|
||||
| `badge_social_voti` | Voti social per i badge. | Un voto per categoria, votante e partita; auto-voto rifiutato (`badge_social_no_autovoto`, `m12_niente_autovoto`); votante e votato devono essere convocati all'evento (RLS, `m13_convocati_e_pagelle_chiuse`). |
|
||||
|
||||
## Turni e notifiche
|
||||
|
||||
| Tabella | Scopo | Note |
|
||||
| -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `turni_palloni` | Gestione dei turni palloni. | Solo turni **confermati**. Gli allenamenti non ricevono proposta automatica (vedi [palloni.md](modules/palloni.md)); M10 azzera i turni salvati su allenamenti da oggi in poi. |
|
||||
| `push_subscriptions` | Dispositivi registrati per le notifiche Push. | |
|
||||
| `promemoria_push` | Non più usata. | Serviva da coda del testo quando la push partiva vuota; dal payload cifrato non la scrive né la legge nessuno. Tabella ancora presente, da eliminare con una migrazione. |
|
||||
| `notifiche_utente` | Centro notifiche in-app, con stato letto/non letto, sull'avatar del profilo. | Introdotta dalla migration `m17_notifiche_utente`. Quattro sorgenti, tutte in parallelo alla push esistente: messaggio libero dell'admin (`notifica-personalizzata.ts`), un promemoria automatico prima di un evento — 24h e 3h prima, due `cron.schedule` (estensione `pg_cron`, abilitata il 31/07/2026 ma usata per la prima volta qui) — il turno palloni (`promemoria-palloni.ts`) e il sollecito presenze (`sollecita-presenze.ts`), questi ultimi due avviati a mano da un admin. Volutamente non c'è una notifica sulla sola creazione di un evento: conta l'avvicinarsi della data, non il momento in cui è stato messo in calendario. I destinatari di un evento seguono la stessa convenzione di `convocatiEvento()`/`evento_permette_voto()` (M13): `convocati` vuoto = tutta la rosa attiva. Letta/segnata come letta dal client con `useNotificheMie()`/`useSegnaLette()` (`src/lib/notifiche-utente.ts`), RLS-only, nessuna route API dedicata per queste due operazioni. |
|
||||
| Tabella | Scopo | Note |
|
||||
| -------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `turni_palloni` | Gestione dei turni palloni. | Solo turni **confermati**. Gli allenamenti non ricevono proposta automatica (vedi [palloni.md](modules/palloni.md)); M10 azzera i turni salvati su allenamenti da oggi in poi. |
|
||||
| `push_subscriptions` | Dispositivi registrati per le notifiche Push. | |
|
||||
| `promemoria_push` | Non più usata. | Serviva da coda del testo quando la push partiva vuota; dal payload cifrato non la scrive né la legge nessuno. Tabella ancora presente, da eliminare con una migrazione. |
|
||||
| `notifiche_utente` | Centro notifiche in-app, con stato letto/non letto, sull'avatar del profilo. | Introdotta dalla migration `m17_notifiche_utente`. Quattro sorgenti, tutte in parallelo alla push esistente: messaggio libero dell'admin (`notifica-personalizzata.ts`), un promemoria automatico prima di un evento — 24h e 3h prima, due `cron.schedule` (estensione `pg_cron`, abilitata il 31/07/2026 ma usata per la prima volta qui) — il turno palloni (`promemoria-palloni.ts`) e il sollecito presenze (`sollecita-presenze.ts`), questi ultimi due avviati a mano da un admin (il sollecito anche da un allenatore, DD-034). Volutamente non c'è una notifica sulla sola creazione di un evento: conta l'avvicinarsi della data, non il momento in cui è stato messo in calendario. I destinatari di un evento seguono la stessa convenzione di `convocatiEvento()`/`evento_permette_voto()` (M13): `convocati` vuoto = tutta la rosa attiva. Gli allenatori attivi sono sempre tra i destinatari dei promemoria evento, convocati o no (`giocatori_destinatari_evento()` riscritta da M21, DD-034); il sollecito presenze (avviato da admin o allenatore) e il turno palloni non arrivano mai a loro. Letta/segnata come letta dal client con `useNotificheMie()`/`useSegnaLette()` (`src/lib/notifiche-utente.ts`), RLS-only, nessuna route API dedicata per queste due operazioni. |
|
||||
|
||||
## Funzioni speciali
|
||||
|
||||
|
||||
@@ -637,7 +637,7 @@ export type Database = {
|
||||
};
|
||||
};
|
||||
Enums: {
|
||||
app_role: "admin" | "user";
|
||||
app_role: "admin" | "user" | "allenatore";
|
||||
};
|
||||
CompositeTypes: {
|
||||
[_ in never]: never;
|
||||
@@ -762,7 +762,7 @@ export const Constants = {
|
||||
},
|
||||
public: {
|
||||
Enums: {
|
||||
app_role: ["admin", "user"],
|
||||
app_role: ["admin", "user", "allenatore"],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -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;
|
||||
$$;
|
||||
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* Permessi del ruolo allenatore (DD-034, migration M21): `bun test/integration/permessi-allenatore.test.ts`.
|
||||
*
|
||||
* Stesso impianto di `permessi.test.ts`: utenti veri, database interrogato *come loro*,
|
||||
* solo contro lo stack locale. L'allenatore è uno slot di `giocatori_squadra` con
|
||||
* `tipo = 'allenatore'`, creato dall'admin e reclamato per email al primo accesso.
|
||||
*
|
||||
* Stato toccato e ripristinato alla fine: gli utenti creati, lo slot allenatore (cancellato),
|
||||
* lo slot g3 reclamato dal giocatore di prova e le righe con il prefisso `test-allenatore`.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
|
||||
if (!locale) {
|
||||
salta("permessi allenatore", "stack locale non attivo (npx supabase start)");
|
||||
riepilogo("permessi-allenatore");
|
||||
} else {
|
||||
const { url: URL_BASE, anon: ANON, servizio: SERVIZIO } = locale;
|
||||
console.log(`permessi allenatore su ${URL_BASE}`);
|
||||
|
||||
const rest = (percorso: string, token: string, init?: RequestInit) =>
|
||||
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||
...init,
|
||||
headers: {
|
||||
apikey: token === SERVIZIO ? SERVIZIO : ANON,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
const righeToccate = async (res: Response): Promise<number> => {
|
||||
if (!res.ok) return 0;
|
||||
const corpo = (await res.json()) as unknown[];
|
||||
return Array.isArray(corpo) ? corpo.length : 0;
|
||||
};
|
||||
|
||||
async function creaUtente(email: string, password: string): Promise<string> {
|
||||
const res = await fetch(`${URL_BASE}/auth/v1/admin/users`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email, password, email_confirm: true }),
|
||||
});
|
||||
const corpo = (await res.json()) as { id?: string };
|
||||
if (!corpo.id) throw new Error(`creazione utente fallita: ${JSON.stringify(corpo)}`);
|
||||
return corpo.id;
|
||||
}
|
||||
|
||||
async function accedi(email: string, password: string): Promise<string> {
|
||||
const res = await fetch(`${URL_BASE}/auth/v1/token?grant_type=password`, {
|
||||
method: "POST",
|
||||
headers: { apikey: ANON, "content-type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
const corpo = (await res.json()) as { access_token?: string };
|
||||
if (!corpo.access_token) throw new Error(`accesso fallito: ${JSON.stringify(corpo)}`);
|
||||
return corpo.access_token;
|
||||
}
|
||||
|
||||
const eliminaUtente = (id: string) =>
|
||||
fetch(`${URL_BASE}/auth/v1/admin/users/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { apikey: SERVIZIO, Authorization: `Bearer ${SERVIZIO}` },
|
||||
});
|
||||
|
||||
const ruoliDi = async (idUtente: string): Promise<string[]> => {
|
||||
const res = await rest(`user_roles?user_id=eq.${idUtente}&select=role`, SERVIZIO);
|
||||
return ((await res.json()) as Array<{ role: string }>).map((r) => r.role);
|
||||
};
|
||||
|
||||
const PASSWORD = "prova-allenatore-123";
|
||||
const PREFISSO = "test-allenatore";
|
||||
const SLOT = "g9001";
|
||||
const GIOCATORE = "g3";
|
||||
const EVENTO = `${PREFISSO}-evento`;
|
||||
const idUtenti: string[] = [];
|
||||
let tokenAdmin = "";
|
||||
|
||||
try {
|
||||
const oggi = Date.now();
|
||||
const emailAllenatore = `test-allenatore-${oggi}@example.test`;
|
||||
const emailAdmin = `test-admin-all-${oggi}@example.test`;
|
||||
const emailGiocatore = `test-giocatore-all-${oggi}@example.test`;
|
||||
const idAllenatore = await creaUtente(emailAllenatore, PASSWORD);
|
||||
const idAdmin = await creaUtente(emailAdmin, PASSWORD);
|
||||
const idGiocatore = await creaUtente(emailGiocatore, PASSWORD);
|
||||
idUtenti.push(idAllenatore, idAdmin, idGiocatore);
|
||||
|
||||
await rest("user_roles", SERVIZIO, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ user_id: idAdmin, role: "admin" }),
|
||||
});
|
||||
tokenAdmin = await accedi(emailAdmin, PASSWORD);
|
||||
const tokenAllenatore = await accedi(emailAllenatore, PASSWORD);
|
||||
const tokenGiocatore = await accedi(emailGiocatore, PASSWORD);
|
||||
|
||||
const collegaGiocatore = await rest(`giocatori_squadra?id=eq.${GIOCATORE}`, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ auth_user_id: idGiocatore }),
|
||||
});
|
||||
assert.equal(await righeToccate(collegaGiocatore), 1, `${GIOCATORE} collegato al test`);
|
||||
|
||||
await prova("l'admin registra un allenatore senza numero né ruolo", async () => {
|
||||
const res = await rest("giocatori_squadra", tokenAdmin, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
id: SLOT,
|
||||
nome: "Carla",
|
||||
cognome: "Neri",
|
||||
numero: null,
|
||||
ruolo: "",
|
||||
email: emailAllenatore,
|
||||
tipo: "allenatore",
|
||||
}),
|
||||
});
|
||||
assert.equal(await righeToccate(res), 1, `slot allenatore creato (${res.status})`);
|
||||
});
|
||||
|
||||
await prova("un giocatore senza numero resta rifiutato", async () => {
|
||||
const res = await rest("giocatori_squadra", tokenAdmin, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id: "g9002", nome: "X", cognome: "Y", numero: null, ruolo: "" }),
|
||||
});
|
||||
assert.ok(!res.ok, `il numero è obbligatorio per chi gioca (${res.status})`);
|
||||
});
|
||||
|
||||
await prova("il collegamento per email dà il ruolo allenatore", async () => {
|
||||
assert.deepEqual(await ruoliDi(idAllenatore), [], "prima del collegamento nessun ruolo");
|
||||
const res = await rest(
|
||||
`giocatori_squadra?id=eq.${SLOT}&auth_user_id=is.null`,
|
||||
tokenAllenatore,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ auth_user_id: idAllenatore }),
|
||||
},
|
||||
);
|
||||
assert.equal(await righeToccate(res), 1, `slot reclamato (${res.status})`);
|
||||
assert.deepEqual(await ruoliDi(idAllenatore), ["allenatore"]);
|
||||
});
|
||||
|
||||
await prova("l'allenatore crea, modifica ed elimina gli eventi", async () => {
|
||||
const crea = await rest("eventi_app", tokenAllenatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
id: EVENTO,
|
||||
tipo: "partita",
|
||||
titolo: "Partita di prova",
|
||||
data: "2026-01-01",
|
||||
ora: "20:00",
|
||||
luogo: "Palestra",
|
||||
convocati: [GIOCATORE],
|
||||
}),
|
||||
});
|
||||
assert.equal(await righeToccate(crea), 1, `evento creato (${crea.status})`);
|
||||
|
||||
const modifica = await rest(`eventi_app?id=eq.${EVENTO}`, tokenAllenatore, {
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ luogo: "Altra palestra" }),
|
||||
});
|
||||
assert.equal(await righeToccate(modifica), 1, "evento modificato");
|
||||
|
||||
const altro = await rest("eventi_app", tokenAllenatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
id: `${PREFISSO}-da-cancellare`,
|
||||
tipo: "allenamento",
|
||||
titolo: "Da cancellare",
|
||||
data: "2026-01-02",
|
||||
ora: "20:00",
|
||||
luogo: "",
|
||||
}),
|
||||
});
|
||||
assert.equal(await righeToccate(altro), 1);
|
||||
const cancella = await rest(`eventi_app?id=eq.${PREFISSO}-da-cancellare`, tokenAllenatore, {
|
||||
method: "DELETE",
|
||||
headers: { Prefer: "return=representation" },
|
||||
});
|
||||
assert.equal(await righeToccate(cancella), 1, "evento eliminato");
|
||||
});
|
||||
|
||||
await prova("il giocatore continua a non gestire gli eventi", async () => {
|
||||
const res = await rest(`eventi_app?id=eq.${EVENTO}`, tokenGiocatore, {
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ luogo: "Abusivo" }),
|
||||
});
|
||||
assert.equal(await righeToccate(res), 0);
|
||||
});
|
||||
|
||||
await prova("l'allenatore cambia il proprio nome e cognome", async () => {
|
||||
const res = await rest(`giocatori_squadra?id=eq.${SLOT}`, tokenAllenatore, {
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ nome: "Carlotta", cognome: "Neri Bianchi" }),
|
||||
});
|
||||
assert.equal(await righeToccate(res), 1, `nome aggiornato (${res.status})`);
|
||||
});
|
||||
|
||||
await prova("l'allenatore non cambia il proprio tipo né gli altri campi admin", async () => {
|
||||
for (const campi of [{ tipo: "giocatore" }, { numero: 5 }, { email: "altra@example.test" }]) {
|
||||
const res = await rest(`giocatori_squadra?id=eq.${SLOT}`, tokenAllenatore, {
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify(campi),
|
||||
});
|
||||
assert.equal(await righeToccate(res), 0, `rifiutato: ${JSON.stringify(campi)}`);
|
||||
}
|
||||
assert.deepEqual(await ruoliDi(idAllenatore), ["allenatore"], "il ruolo resta");
|
||||
});
|
||||
|
||||
await prova("l'allenatore non scrive lo slot di un giocatore", async () => {
|
||||
const res = await rest(`giocatori_squadra?id=eq.${GIOCATORE}`, tokenAllenatore, {
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ nome: "Abusivo" }),
|
||||
});
|
||||
assert.equal(await righeToccate(res), 0);
|
||||
});
|
||||
|
||||
await prova("il giocatore non cambia il proprio nome", async () => {
|
||||
const res = await rest(`giocatori_squadra?id=eq.${GIOCATORE}`, tokenGiocatore, {
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ nome: "Abusivo" }),
|
||||
});
|
||||
assert.equal(await righeToccate(res), 0, "per chi gioca nome e cognome restano all'admin");
|
||||
});
|
||||
|
||||
await prova("l'allenatore non risponde alle presenze né dichiara cacche", async () => {
|
||||
const presenza = await rest("risposte_presenze", tokenAllenatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: SLOT, stato: "presente" }),
|
||||
});
|
||||
assert.ok(!presenza.ok, `presenza rifiutata (${presenza.status})`);
|
||||
|
||||
const cacche = await rest("cacche_partita", tokenAllenatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ evento_id: EVENTO, giocatore_id: SLOT, quantita: 1 }),
|
||||
});
|
||||
assert.ok(!cacche.ok, `cacche rifiutate (${cacche.status})`);
|
||||
});
|
||||
|
||||
await prova("l'allenatore non vota e non viene votato", async () => {
|
||||
// Evento con `convocati` vuoto: tutta la rosa, ma l'allenatore resta fuori comunque.
|
||||
await rest(`eventi_app?id=eq.${EVENTO}`, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ convocati: [] }),
|
||||
});
|
||||
const vota = await rest("mvp_voti", tokenAllenatore, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO,
|
||||
votante_id: SLOT,
|
||||
votato_id: GIOCATORE,
|
||||
votato_nome: "Tre",
|
||||
}),
|
||||
});
|
||||
assert.ok(!vota.ok, `voto dell'allenatore rifiutato (${vota.status})`);
|
||||
|
||||
const votato = await rest("mvp_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO,
|
||||
votante_id: GIOCATORE,
|
||||
votato_id: SLOT,
|
||||
votato_nome: "Allenatore",
|
||||
}),
|
||||
});
|
||||
assert.ok(!votato.ok, `l'allenatore non si vota (${votato.status})`);
|
||||
});
|
||||
|
||||
await prova("i promemoria evento arrivano anche all'allenatore non convocato", async () => {
|
||||
await rest(`eventi_app?id=eq.${EVENTO}`, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ convocati: [GIOCATORE] }),
|
||||
});
|
||||
const res = await rest("rpc/giocatori_destinatari_evento", SERVIZIO, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ p_evento_id: EVENTO }),
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
const destinatari = ((await res.json()) as Array<string | Record<string, string>>).map((d) =>
|
||||
typeof d === "string" ? d : Object.values(d)[0],
|
||||
);
|
||||
assert.deepEqual([...destinatari].sort(), [GIOCATORE, SLOT].sort());
|
||||
});
|
||||
|
||||
await prova("scollegare l'account toglie il ruolo", async () => {
|
||||
const res = await rest(`giocatori_squadra?id=eq.${SLOT}`, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({ auth_user_id: null }),
|
||||
});
|
||||
assert.equal(await righeToccate(res), 1);
|
||||
assert.deepEqual(await ruoliDi(idAllenatore), []);
|
||||
const crea = await rest("eventi_app", tokenAllenatore, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
id: `${PREFISSO}-dopo`,
|
||||
tipo: "allenamento",
|
||||
titolo: "Non deve esistere",
|
||||
data: "2026-01-03",
|
||||
ora: "20:00",
|
||||
luogo: "",
|
||||
}),
|
||||
});
|
||||
assert.ok(!crea.ok, `senza ruolo niente eventi (${crea.status})`);
|
||||
});
|
||||
} finally {
|
||||
await rest(`mvp_voti?match_id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
||||
await rest(`eventi_app?id=like.${PREFISSO}*`, SERVIZIO, { method: "DELETE" });
|
||||
await rest(`giocatori_squadra?id=in.(${SLOT},g9002)`, SERVIZIO, { method: "DELETE" });
|
||||
if (tokenAdmin) {
|
||||
await rest(`giocatori_squadra?id=eq.${GIOCATORE}`, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ auth_user_id: null }),
|
||||
});
|
||||
}
|
||||
for (const id of idUtenti) {
|
||||
await rest(`user_roles?user_id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
||||
await eliminaUtente(id);
|
||||
}
|
||||
riepilogo("permessi-allenatore");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user