Rende pubblica a tutta la squadra la data di nascita inserita dal Profilo
Prima veniva mostrata solo per i 17 giocatori del seed storico: chi la inseriva da sé restava con "Invalid Date" in Squadra, perché il dato finiva solo in profili_giocatore (privato). Un trigger ora lo sincronizza in giocatori_squadra.nascita, colonna pubblica a tutta la rosa. Include il backfill dei profili già compilati prima di M18 (M19), mancante nel push iniziale. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d036f532d
commit
3f2ca30775
14 files changed
+471
-35
No files matched your search
+17
-7
@@ -1,6 +1,6 @@
|
|||||||
# Project State
|
# Project State
|
||||||
|
|
||||||
Ultimo aggiornamento: 09/09/2026
|
Ultimo aggiornamento: 15/09/2026
|
||||||
|
|
||||||
## Stato generale
|
## Stato generale
|
||||||
|
|
||||||
@@ -23,9 +23,10 @@ cancellazioni precedenti a M14 sono state bonificate una tantum (M15/M16).
|
|||||||
- Cursor e Claude Code come ambienti di sviluppo
|
- Cursor e Claude Code come ambienti di sviluppo
|
||||||
- Vercel configurato; Environment Variables aggiornate al nuovo Supabase (Preview e Production)
|
- Vercel configurato; Environment Variables aggiornate al nuovo Supabase (Preview e Production)
|
||||||
- Supabase proprietario attivo — Project Ref: `kfkcldwncxqaixetsjes`
|
- Supabase proprietario attivo — Project Ref: `kfkcldwncxqaixetsjes`
|
||||||
- 28 migration in `supabase/migrations/`, fino a `m17_notifiche_utente` (15/09/2026) —
|
- 30 migration in `supabase/migrations/`, fino a `m19_backfill_nascita_da_profili_esistenti`
|
||||||
quest'ultima testata solo in locale (`npx supabase db reset`), non ancora applicata in
|
(15/09/2026). `m17_notifiche_utente` e `m18_nascita_pubblica_giocatori_squadra` sono state
|
||||||
produzione (`supabase db push`)
|
applicate in produzione lo stesso giorno (`supabase db push`); `m19` (fix del backfill di
|
||||||
|
M18, vedi sotto) è ancora solo in locale
|
||||||
- Sviluppo locale verificato con il nuovo Supabase
|
- Sviluppo locale verificato con il nuovo Supabase
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -44,9 +45,18 @@ cancellazioni precedenti a M14 sono state bonificate una tantum (M15/M16).
|
|||||||
(`m16_funzione_bonifica_dati_evento_orfani` in produzione dal 09/09/2026, verificata con
|
(`m16_funzione_bonifica_dati_evento_orfani` in produzione dal 09/09/2026, verificata con
|
||||||
`npx supabase migration list`)
|
`npx supabase migration list`)
|
||||||
- Migration `m17_notifiche_utente` (15/09/2026, DD-030): nuova tabella `notifiche_utente`
|
- Migration `m17_notifiche_utente` (15/09/2026, DD-030): nuova tabella `notifiche_utente`
|
||||||
per il centro notifiche in-app, testata sullo stack locale — **non ancora applicata in
|
per il centro notifiche in-app — **in produzione** dal 15/09/2026 (è additiva: non tocca
|
||||||
produzione**, va fatta con `supabase db push` quando si decide di rilasciare la
|
schema o comportamento esistenti, vedi DD-030). Il codice che la usa (icona notifiche,
|
||||||
funzionalità (è additiva: non tocca schema o comportamento esistenti, vedi DD-030)
|
route aggiornate) non è ancora deployato: la tabella si riempie già in background
|
||||||
|
- Migration `m18_nascita_pubblica_giocatori_squadra` (15/09/2026, DD-031): nuova colonna
|
||||||
|
`giocatori_squadra.nascita`, sincronizzata da `profili_giocatore.data_nascita` — corregge
|
||||||
|
l'"Invalid Date" in Squadra per chi inserisce la propria nascita dal Profilo invece del
|
||||||
|
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)
|
||||||
- `public.giocatori_squadra`: rosa iniziale di 17 giocatori (migration `m5_email_giocatori_squadra`)
|
- `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
|
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
|
attivi hanno l'email registrata (colonna `email`, DD-018), impostabile da `/admin` senza
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ La tabella è verificata da `test/integration/permessi.test.ts` contro il databa
|
|||||||
|
|
||||||
| Tabella | Scopo | Note |
|
| 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, e come sorgente della data di nascita (non ancora una colonna di questa tabella). 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. |
|
| `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). |
|
| `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. |
|
| `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). |
|
| `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). |
|
||||||
|
|||||||
@@ -1225,3 +1225,87 @@ modo per un giocatore di toglierle di torno per sempre.
|
|||||||
**Riesame**
|
**Riesame**
|
||||||
Se la tabella crescesse in modo sensibile (squadre molto più numerose, o giocatori che non
|
Se la tabella crescesse in modo sensibile (squadre molto più numerose, o giocatori che non
|
||||||
fanno mai swipe), va aggiunta la pulizia automatica scartata sopra.
|
fanno mai swipe), va aggiunta la pulizia automatica scartata sopra.
|
||||||
|
|
||||||
|
### DD-031 — Data di nascita pubblica: una colonna sincronizzata, non una seconda RLS aperta
|
||||||
|
|
||||||
|
**Data:** 15 settembre 2026
|
||||||
|
**Stato:** Accettata
|
||||||
|
|
||||||
|
**Contesto**
|
||||||
|
In "Squadra" la data di nascita mostrava **"Invalid Date"** per chi l'aveva inserita da
|
||||||
|
solo dal proprio Profilo. Causa: due fonti mai collegate. I 17 giocatori del seed storico
|
||||||
|
hanno la nascita in `nascitaPerId`, una mappa statica pubblica bundlata nel client
|
||||||
|
(`src/lib/crapp-data.ts`) — il commento sulla riga la descriveva esplicitamente come
|
||||||
|
stopgap: *"`giocatori_squadra` non ha ancora questa colonna (DD-015 follow-up)"*. Un
|
||||||
|
giocatore aggiunto dopo la migrazione DD-015 inserisce invece la nascita dal form Profilo,
|
||||||
|
che scrive su `profili_giocatore.data_nascita` — tabella con RLS "solo il proprio profilo o
|
||||||
|
admin" (M2), mai letta da `useRosa()`/`useAnagraficaRosa()` (`src/lib/rosa.ts`), e comunque
|
||||||
|
non visibile ai compagni per via della RLS. Per quel giocatore `nascitaPerId[g.id]` era
|
||||||
|
`undefined` → `""` → `formatData("")` in `src/routes/squadra.tsx` produceva `Invalid Date`.
|
||||||
|
|
||||||
|
**Decisione**
|
||||||
|
Migration `m18_nascita_pubblica_giocatori_squadra`: nuova colonna `giocatori_squadra.nascita`
|
||||||
|
(quella prevista da DD-015), pubblica a tutta la rosa attiva con la stessa policy di lettura
|
||||||
|
già in vigore per le altre colonne. Un trigger `SECURITY DEFINER` su `profili_giocatore`
|
||||||
|
(`sincronizza_nascita_pubblica`, AFTER INSERT/UPDATE/DELETE) la tiene allineata a
|
||||||
|
`data_nascita`: un solo input nel form Profilo (`CampiProfilo`), due destinazioni — quella
|
||||||
|
amministrativa esistente (RLS invariata: sé stesso o admin) e questo specchio pubblico.
|
||||||
|
`profili_giocatore` resta l'unica tabella che il client scrive per la nascita; il client non
|
||||||
|
tocca mai `giocatori_squadra.nascita` direttamente.
|
||||||
|
|
||||||
|
Essendo `SECURITY DEFINER` di proprietà dell'owner della tabella, la scrittura del trigger
|
||||||
|
bypassa la RLS di `giocatori_squadra` automaticamente — nessuna nuova policy lì. Serve però
|
||||||
|
un nuovo ramo in `enforce_giocatori_squadra_update()` (il trigger `BEFORE UPDATE` che oggi
|
||||||
|
rifiuta qualunque modifica a uno slot già collegato da parte di chi non è admin): accetta un
|
||||||
|
update dove cambia **solo** `nascita` e il chiamante è il proprietario dello slot
|
||||||
|
(`OLD.auth_user_id = auth.uid()`). Senza quel ramo l'update annidato del trigger di sync
|
||||||
|
fallirebbe con l'eccezione già in vigore.
|
||||||
|
|
||||||
|
Backfill una tantum dei 17 storici con gli stessi valori di `nascitaPerId`, poi rimozione di
|
||||||
|
quell'export (`crapp-data.ts`): era descritto fin dall'inizio come temporaneo, in attesa
|
||||||
|
esattamente di questa colonna.
|
||||||
|
|
||||||
|
**Alternative scartate**
|
||||||
|
|
||||||
|
- Allentare la RLS di `profili_giocatore` per rendere `data_nascita` leggibile a tutta la
|
||||||
|
squadra → scartata: quella tabella porta anche indirizzo, telefono, documento d'identità
|
||||||
|
nello stesso record — aprirne la lettura avrebbe esposto molto più della sola nascita.
|
||||||
|
- Un secondo campo "data di nascita pubblica" nel form Profilo, distinto da quello
|
||||||
|
amministrativo → scartata: due campi per lo stesso dato reale avrebbero richiesto
|
||||||
|
spiegare all'utente perché ne esistono due, con rischio di disallineamento se ne compila
|
||||||
|
uno solo.
|
||||||
|
- Far scrivere `giocatori_squadra.nascita` direttamente dal client (nuova RLS policy per il
|
||||||
|
proprietario dello slot) invece di sincronizzarla da `profili_giocatore` → scartata: la
|
||||||
|
RLS di update per un non-admin resta `USING (auth_user_id IS NULL)` (solo reclamare uno
|
||||||
|
slot libero); aprirla anche a righe già collegate avrebbe ampliato la superficie scrivibile
|
||||||
|
dal client su una tabella con un trigger di sicurezza già delicato (DD-016/DD-023), per un
|
||||||
|
guadagno che il trigger di sync ottiene comunque senza toccare la RLS.
|
||||||
|
|
||||||
|
**Conseguenze**
|
||||||
|
|
||||||
|
- Chiunque compili la propria data di nascita dal Profilo la vede comparire in Squadra (e
|
||||||
|
nei compleanni del Calendario) esattamente come i 17 storici — non più un dato "di serie
|
||||||
|
B".
|
||||||
|
- Cancellare il profilo (da sé o da un admin) azzera anche la nascita pubblica: non resta un
|
||||||
|
valore pubblico orfano di un profilo che non esiste più.
|
||||||
|
- Un giocatore senza nascita nota (profilo mai compilato) non genera più "Invalid Date":
|
||||||
|
`squadra.tsx` mostra la riga della data di nascita solo se `g.nascita` è valorizzata,
|
||||||
|
stesso criterio già usato da `compleanniEventi()` per escludere chi non ha ancora un dato.
|
||||||
|
- `test/integration/nascita-pubblica.test.ts` è la definizione eseguibile del comportamento:
|
||||||
|
sync alla scrittura, visibilità a un giocatore terzo (non solo l'interessato o l'admin),
|
||||||
|
azzeramento alla cancellazione, blocco della scrittura diretta dal client.
|
||||||
|
|
||||||
|
**Riesame**
|
||||||
|
Se in futuro serve differenziare "chi può vedere la nascita" (es. solo squadra, non
|
||||||
|
pubblico) va rivista insieme alla policy di lettura generale di `giocatori_squadra`, non solo
|
||||||
|
per questa colonna.
|
||||||
|
|
||||||
|
**Aggiornamento (15 settembre 2026, stesso giorno)** — dopo aver applicato M18 in produzione
|
||||||
|
(`supabase db push`), la nascita in Squadra compariva solo per i 17 giocatori storici, non
|
||||||
|
per chi aveva già compilato il proprio profilo prima della migration: il trigger di sync
|
||||||
|
copre solo le scritture *future* su `profili_giocatore`, non retroattivamente, e M18
|
||||||
|
backfillava a mano solo il seed storico. Migration `m19_backfill_nascita_da_profili_esistenti`
|
||||||
|
allinea una tantum `giocatori_squadra.nascita` a `profili_giocatore.data_nascita` per tutti i
|
||||||
|
profili già esistenti, con la stessa disattivazione temporanea del trigger di sicurezza usata
|
||||||
|
in M18. Da qui in poi il gap non si può ripresentare: ogni scrittura successiva passa dal
|
||||||
|
trigger di sync, questo backfill serve solo per lo storico antecedente a M18.
|
||||||
@@ -91,7 +91,11 @@ Il profilo viene suddiviso in sette aree.
|
|||||||
|
|
||||||
**Dati personali** — modificabili dal giocatore.
|
**Dati personali** — modificabili dal giocatore.
|
||||||
|
|
||||||
- Data di nascita
|
- Data di nascita — oltre a `profili_giocatore.data_nascita`, un trigger la sincronizza in
|
||||||
|
`giocatori_squadra.nascita` (DD-031): a differenza degli altri campi di questa sezione,
|
||||||
|
visibile a **tutta la squadra**, non solo al giocatore stesso o all'admin (alimenta
|
||||||
|
Squadra e i compleanni nel Calendario, vedi `squadra.md`). Cancellare il profilo azzera
|
||||||
|
anche quel valore pubblico.
|
||||||
- Luogo di nascita
|
- Luogo di nascita
|
||||||
- Indirizzo di residenza
|
- Indirizzo di residenza
|
||||||
- Telefono
|
- Telefono
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ giocatore dalla dashboard admin non aveva alcun effetto sul resto dell'app.
|
|||||||
|
|
||||||
- **`useAnagraficaRosa()`** (`rosa.ts`) — solo id, nome, ruolo, numero, data di nascita dei
|
- **`useAnagraficaRosa()`** (`rosa.ts`) — solo id, nome, ruolo, numero, data di nascita dei
|
||||||
giocatori `attivo`. Serve dove basta sapere chi c'è, es. i compleanni nel Calendario o le
|
giocatori `attivo`. Serve dove basta sapere chi c'è, es. i compleanni nel Calendario o le
|
||||||
liste presenze: non monta gli hook di MVP/pagelle/palloni/infortuni.
|
liste presenze: non monta gli hook di MVP/pagelle/palloni/infortuni. La data di nascita
|
||||||
|
viene da `giocatori_squadra.nascita` (migration `m18_nascita_pubblica_giocatori_squadra`,
|
||||||
|
DD-031): pubblica a tutta la rosa, sincronizzata automaticamente da un trigger quando il
|
||||||
|
giocatore (o un admin per lui) la salva dal Profilo — vedi `profilo-giocatore.md`. `null`
|
||||||
|
finché non è stata inserita da nessuno.
|
||||||
- **`useRosa()`** (`rosa.ts`) — la stessa anagrafica arricchita con tutte le statistiche
|
- **`useRosa()`** (`rosa.ts`) — la stessa anagrafica arricchita con tutte le statistiche
|
||||||
personali calcolate a runtime: presenze, partite giocate, serie (presenze, allenamenti,
|
personali calcolate a runtime: presenze, partite giocate, serie (presenze, allenamenti,
|
||||||
partite, conferme, palloni), MVP vinti, media voto pagelle, palloni, cacche, infortuni,
|
partite, conferme, palloni), MVP vinti, media voto pagelle, palloni, cacche, infortuni,
|
||||||
@@ -77,12 +81,7 @@ presentazione — le rispettive specifiche stanno in `badge.md` e `obiettivi-squ
|
|||||||
|
|
||||||
## Limiti noti
|
## Limiti noti
|
||||||
|
|
||||||
1. **`giocatori_squadra` non ha ancora una colonna per la data di nascita.** Per i
|
1. **`src/lib/crapp-data.ts` resta come fallback**, non più come fonte viva: se il database
|
||||||
giocatori storici (seed iniziale) la nascita viene letta da `crapp-data.ts`
|
|
||||||
(`nascitaPerId`, lookup per id); un giocatore aggiunto dopo la migrazione non ha nascita
|
|
||||||
nota finché la colonna non esiste (DD-015). Effetto visibile: niente compleanno nel
|
|
||||||
Calendario per quei giocatori.
|
|
||||||
2. **`src/lib/crapp-data.ts` resta come fallback**, non più come fonte viva: se il database
|
|
||||||
non risponde o non è ancora popolato, `rosaFallback()` genera una rosa di riserva dai
|
non risponde o non è ancora popolato, `rosaFallback()` genera una rosa di riserva dai
|
||||||
dati statici storici. Un ambiente nuovo senza dati in `giocatori_squadra` mostra quindi
|
dati statici storici. Un ambiente nuovo senza dati in `giocatori_squadra` mostra quindi
|
||||||
comunque una squadra, non una schermata vuota — ma è la rosa 2025/26 hardcoded, non
|
comunque una squadra, non una schermata vuota — ma è la rosa 2025/26 hardcoded, non
|
||||||
|
|||||||
@@ -122,14 +122,6 @@ export const giocatori: Giocatore[] = rosaCSI
|
|||||||
}))
|
}))
|
||||||
.sort((a, b) => dividiNome(a.nome).cognome.localeCompare(dividiNome(b.nome).cognome, "it"));
|
.sort((a, b) => dividiNome(a.nome).cognome.localeCompare(dividiNome(b.nome).cognome, "it"));
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback per la data di nascita: `giocatori_squadra` non ha ancora questa colonna
|
|
||||||
* (DD-015 follow-up). Un giocatore aggiunto dopo la migrazione non ha nascita nota.
|
|
||||||
*/
|
|
||||||
export const nascitaPerId: Record<string, string> = Object.fromEntries(
|
|
||||||
giocatori.map((g) => [g.id, g.nascita]),
|
|
||||||
);
|
|
||||||
|
|
||||||
export type RigaClassifica = {
|
export type RigaClassifica = {
|
||||||
pos: number;
|
pos: number;
|
||||||
squadra: string;
|
squadra: string;
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export type GiocatoreSquadra = {
|
|||||||
email: string | null;
|
email: string | null;
|
||||||
numeroTessera: string | null;
|
numeroTessera: string | null;
|
||||||
dataTessera: string | null;
|
dataTessera: string | null;
|
||||||
|
/** Pubblica a tutta la rosa (M18): sincronizzata da `profili_giocatore.data_nascita`. */
|
||||||
|
nascita: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RigaGiocatoreSquadra = {
|
export type RigaGiocatoreSquadra = {
|
||||||
@@ -31,6 +33,7 @@ export type RigaGiocatoreSquadra = {
|
|||||||
email: string | null;
|
email: string | null;
|
||||||
numero_tessera: string | null;
|
numero_tessera: string | null;
|
||||||
data_tessera: string | null;
|
data_tessera: string | null;
|
||||||
|
nascita: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Ruoli ammessi in campo (pallavolo): usati per il menu a tendina del profilo squadra. */
|
/** Ruoli ammessi in campo (pallavolo): usati per il menu a tendina del profilo squadra. */
|
||||||
@@ -50,6 +53,7 @@ export function rosaFallback(): GiocatoreSquadra[] {
|
|||||||
email: null,
|
email: null,
|
||||||
numeroTessera: null,
|
numeroTessera: null,
|
||||||
dataTessera: null,
|
dataTessera: null,
|
||||||
|
nascita: g.nascita,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +81,7 @@ export function slotPerEmail(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const COLONNE_SQUADRA =
|
export const COLONNE_SQUADRA =
|
||||||
"id, nome, cognome, numero, ruolo, auth_user_id, attivo, email, numero_tessera, data_tessera";
|
"id, nome, cognome, numero, ruolo, auth_user_id, attivo, email, numero_tessera, data_tessera, nascita";
|
||||||
|
|
||||||
/** Conversione riga database -> modello applicativo (riusabile anche lato server). */
|
/** Conversione riga database -> modello applicativo (riusabile anche lato server). */
|
||||||
export function daRigaSquadra(r: RigaGiocatoreSquadra): GiocatoreSquadra {
|
export function daRigaSquadra(r: RigaGiocatoreSquadra): GiocatoreSquadra {
|
||||||
@@ -92,6 +96,7 @@ export function daRigaSquadra(r: RigaGiocatoreSquadra): GiocatoreSquadra {
|
|||||||
email: r.email,
|
email: r.email,
|
||||||
numeroTessera: r.numero_tessera,
|
numeroTessera: r.numero_tessera,
|
||||||
dataTessera: r.data_tessera,
|
dataTessera: r.data_tessera,
|
||||||
|
nascita: r.nascita,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +236,14 @@ export function useAggiungiGiocatore() {
|
|||||||
const { error } = await supabaseNuoveTabelle.from("giocatori_squadra").insert(riga);
|
const { error } = await supabaseNuoveTabelle.from("giocatori_squadra").insert(riga);
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
// Le colonne non inviate hanno i default della tabella (M1): `attivo` true, il resto NULL.
|
// Le colonne non inviate hanno i default della tabella (M1): `attivo` true, il resto NULL.
|
||||||
return { ...riga, authUserId: null, attivo: true, numeroTessera: null, dataTessera: null };
|
return {
|
||||||
|
...riga,
|
||||||
|
authUserId: null,
|
||||||
|
attivo: true,
|
||||||
|
numeroTessera: null,
|
||||||
|
dataTessera: null,
|
||||||
|
nascita: null,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
// Aggiornamento locale della cache: nessuna rilettura, stesso ordine della query
|
// Aggiornamento locale della cache: nessuna rilettura, stesso ordine della query
|
||||||
// (`.order("cognome").order("nome")`).
|
// (`.order("cognome").order("nome")`).
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { nascitaPerId, type Giocatore } from "./crapp-data";
|
import type { Giocatore } from "./crapp-data";
|
||||||
import { nomeCompleto, useGiocatoriSquadra } from "./giocatori-squadra";
|
import { nomeCompleto, useGiocatoriSquadra } from "./giocatori-squadra";
|
||||||
import { mvpVintiPerGiocatore, useVotiMvp } from "./mvp-voti";
|
import { mvpVintiPerGiocatore, useVotiMvp } from "./mvp-voti";
|
||||||
import { mediePagelle, usePagelle } from "./pagelle";
|
import { mediePagelle, usePagelle } from "./pagelle";
|
||||||
@@ -44,7 +44,7 @@ export function useAnagraficaRosa(): Array<
|
|||||||
nome: nomeCompleto(g),
|
nome: nomeCompleto(g),
|
||||||
ruolo: g.ruolo,
|
ruolo: g.ruolo,
|
||||||
numero: g.numero,
|
numero: g.numero,
|
||||||
nascita: nascitaPerId[g.id] ?? "",
|
nascita: g.nascita ?? "",
|
||||||
})),
|
})),
|
||||||
[squadra],
|
[squadra],
|
||||||
);
|
);
|
||||||
@@ -85,7 +85,7 @@ export function useRosa(): Giocatore[] {
|
|||||||
nome: nomeCompleto(g),
|
nome: nomeCompleto(g),
|
||||||
numero: g.numero,
|
numero: g.numero,
|
||||||
ruolo: g.ruolo,
|
ruolo: g.ruolo,
|
||||||
nascita: nascitaPerId[g.id] ?? "",
|
nascita: g.nascita ?? "",
|
||||||
iniziali: iniziali(g.nome, g.cognome),
|
iniziali: iniziali(g.nome, g.cognome),
|
||||||
presenze: contaPresenzeGiocatore(g.id, eventi, mappaPresenze),
|
presenze: contaPresenzeGiocatore(g.id, eventi, mappaPresenze),
|
||||||
totaliEventi: totaliEventiGiocatore(g.id, eventi),
|
totaliEventi: totaliEventiGiocatore(g.id, eventi),
|
||||||
|
|||||||
@@ -169,12 +169,14 @@ function Squadra() {
|
|||||||
|
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<div className="border-t border-border px-4 pb-4 pt-3">
|
<div className="border-t border-border px-4 pb-4 pt-3">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
{g.nascita ? (
|
||||||
<p className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Cake className="h-3.5 w-3.5" /> {formatData(g.nascita)}{" "}
|
<p className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
{g.nascita.slice(0, 4)}
|
<Cake className="h-3.5 w-3.5" /> {formatData(g.nascita)}{" "}
|
||||||
</p>
|
{g.nascita.slice(0, 4)}
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||||
{[
|
{[
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
-- M18 — Data di nascita pubblica a tutta la squadra (DD-031)
|
||||||
|
--
|
||||||
|
-- In "Squadra" la data di nascita mostrava "Invalid Date" per chi l'aveva inserita da solo
|
||||||
|
-- dal proprio Profilo: quel valore finisce in `profili_giocatore.data_nascita`, tabella con
|
||||||
|
-- RLS "solo il proprio profilo o admin" (M2), mai letta da `useRosa()`/`useAnagraficaRosa()`
|
||||||
|
-- (src/lib/rosa.ts). Quelle funzioni leggevano solo la mappa statica `nascitaPerId` del seed
|
||||||
|
-- storico dei 17 giocatori originali (src/lib/crapp-data.ts) — pubblica perché bundlata nel
|
||||||
|
-- client, ma senza voce per chi si è aggiunto dopo.
|
||||||
|
--
|
||||||
|
-- Questa migration aggiunge la colonna pubblica prevista fin da DD-015 ("giocatori_squadra
|
||||||
|
-- non ha ancora questa colonna, DD-015 follow-up") e la tiene sincronizzata con
|
||||||
|
-- `profili_giocatore.data_nascita`: un solo input nel form Profilo, due destinazioni — quella
|
||||||
|
-- amministrativa esistente (RLS invariata) e questo specchio pubblico, leggibile da tutta la
|
||||||
|
-- rosa attiva con la stessa policy di sempre.
|
||||||
|
|
||||||
|
ALTER TABLE public.giocatori_squadra ADD COLUMN nascita date;
|
||||||
|
|
||||||
|
-- Backfill una tantum dei 17 giocatori storici (stessi valori di `rosaCSI` in
|
||||||
|
-- src/lib/crapp-data.ts, stesso ordine g1..g17 del seed di M1). Il trigger
|
||||||
|
-- `enforce_giocatori_squadra_update` (attivo dalla migration M1) rifiuterebbe questi UPDATE:
|
||||||
|
-- gira in un contesto senza `auth.uid()`, quindi non passa né il ramo admin né quello di
|
||||||
|
-- reclamazione. Va disabilitato solo per il backfill e riattivato subito dopo.
|
||||||
|
ALTER TABLE public.giocatori_squadra DISABLE TRIGGER enforce_giocatori_squadra_update;
|
||||||
|
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1997-08-30' WHERE id = 'g1';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1996-12-07' WHERE id = 'g2';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '2000-04-28' WHERE id = 'g3';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1995-05-01' WHERE id = 'g4';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1995-08-13' WHERE id = 'g5';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1996-02-01' WHERE id = 'g6';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1997-07-04' WHERE id = 'g7';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1994-05-28' WHERE id = 'g8';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '2003-04-04' WHERE id = 'g9';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1998-11-30' WHERE id = 'g10';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '2000-02-02' WHERE id = 'g11';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1999-10-03' WHERE id = 'g12';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '2000-09-16' WHERE id = 'g13';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1996-12-13' WHERE id = 'g14';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1993-03-24' WHERE id = 'g15';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '2001-04-18' WHERE id = 'g16';
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = '1994-05-20' WHERE id = 'g17';
|
||||||
|
|
||||||
|
ALTER TABLE public.giocatori_squadra ENABLE TRIGGER enforce_giocatori_squadra_update;
|
||||||
|
|
||||||
|
-- Il trigger di sync (sotto) è SECURITY DEFINER e bypassa la RLS in scrittura su
|
||||||
|
-- giocatori_squadra (di proprietà dell'owner della tabella), ma `enforce_giocatori_squadra_update`
|
||||||
|
-- resta un trigger BEFORE UPDATE che scatta comunque: senza questo ramo, l'update annidato
|
||||||
|
-- fallirebbe con l'eccezione già in vigore per chi non è admin.
|
||||||
|
--
|
||||||
|
-- Non apre una scrittura diretta dal client: la policy di update per un non-admin resta
|
||||||
|
-- "Users can claim unassigned roster slot" (USING auth_user_id IS NULL) — una riga già
|
||||||
|
-- collegata non la supera nemmeno, quindi l'unica strada che raggiunge questo ramo è la
|
||||||
|
-- funzione di sync qui sotto, che gira con privilegi propri e ignora la RLS.
|
||||||
|
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.creato_il IS NOT DISTINCT FROM OLD.creato_il THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Slot già collegato al chiamante: può cambiare solo `nascita` (M18). È la via percorsa
|
||||||
|
-- dal trigger di sync qui sotto, mai da un client (vedi commento sopra).
|
||||||
|
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 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.creato_il IS NOT DISTINCT FROM OLD.creato_il THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RAISE EXCEPTION 'Aggiornamento non autorizzato su giocatori_squadra';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.sincronizza_nascita_pubblica()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF TG_OP = 'DELETE' THEN
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = NULL WHERE id = OLD.giocatore_id;
|
||||||
|
RETURN OLD;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE public.giocatori_squadra SET nascita = NEW.data_nascita WHERE id = NEW.giocatore_id;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER sincronizza_nascita_pubblica
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON public.profili_giocatore
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION public.sincronizza_nascita_pubblica();
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- M19 — Backfill della nascita pubblica per i profili già esistenti (fix di M18)
|
||||||
|
--
|
||||||
|
-- M18 ha aggiunto `giocatori_squadra.nascita` e un trigger che la sincronizza da
|
||||||
|
-- `profili_giocatore.data_nascita`, ma quel trigger scatta solo sulle scritture *future*:
|
||||||
|
-- chi aveva già compilato il proprio profilo (data_nascita) prima che M18 fosse applicata è
|
||||||
|
-- rimasto con `nascita` vuota, mentre M18 aveva backfillato a mano solo i 17 giocatori del
|
||||||
|
-- seed storico. Effetto visibile: in Squadra la data di nascita compariva solo per quei 17,
|
||||||
|
-- non per chi l'aveva inserita dal Profilo prima di oggi.
|
||||||
|
--
|
||||||
|
-- Stesso motivo di M18 per disabilitare temporaneamente `enforce_giocatori_squadra_update`:
|
||||||
|
-- gira in un contesto senza `auth.uid()`, quindi non passerebbe nessun ramo del trigger.
|
||||||
|
|
||||||
|
ALTER TABLE public.giocatori_squadra DISABLE TRIGGER enforce_giocatori_squadra_update;
|
||||||
|
|
||||||
|
-- Ha la precedenza sui valori hardcoded di M18 per chi, tra i 17 storici, ha anche corretto
|
||||||
|
-- la propria data dal Profilo nel frattempo: `profili_giocatore` è la fonte più aggiornata.
|
||||||
|
UPDATE public.giocatori_squadra gs
|
||||||
|
SET nascita = pg.data_nascita
|
||||||
|
FROM public.profili_giocatore pg
|
||||||
|
WHERE pg.giocatore_id = gs.id;
|
||||||
|
|
||||||
|
ALTER TABLE public.giocatori_squadra ENABLE TRIGGER enforce_giocatori_squadra_update;
|
||||||
@@ -5,6 +5,12 @@
|
|||||||
-- I path dei file puntano a oggetti che nel bucket non esistono: i pulsanti di download
|
-- I path dei file puntano a oggetti che nel bucket non esistono: i pulsanti di download
|
||||||
-- falliscono finché non carichi qualcosa dall'app o dallo Studio (http://127.0.0.1:54323).
|
-- falliscono finché non carichi qualcosa dall'app o dallo Studio (http://127.0.0.1:54323).
|
||||||
|
|
||||||
|
-- Il trigger di sync M18 (`sincronizza_nascita_pubblica`) aggiorna `giocatori_squadra.nascita`
|
||||||
|
-- appena questo INSERT tocca `data_nascita`, ma qui gira senza un JWT reale: né il ramo admin
|
||||||
|
-- né quello del proprietario dello slot del trigger di sicurezza passerebbero. Va disabilitato
|
||||||
|
-- solo per questo seed, mai un problema in produzione dove il seed non gira.
|
||||||
|
ALTER TABLE public.giocatori_squadra DISABLE TRIGGER enforce_giocatori_squadra_update;
|
||||||
|
|
||||||
INSERT INTO public.profili_giocatore
|
INSERT INTO public.profili_giocatore
|
||||||
(giocatore_id, data_nascita, luogo_nascita, indirizzo, telefono, email,
|
(giocatore_id, data_nascita, luogo_nascita, indirizzo, telefono, email,
|
||||||
documento_tipo, documento_numero, documento_rilasciato_da,
|
documento_tipo, documento_numero, documento_rilasciato_da,
|
||||||
@@ -29,6 +35,8 @@ VALUES
|
|||||||
'2027-09-15', 'g2/certificato.pdf', NULL)
|
'2027-09-15', 'g2/certificato.pdf', NULL)
|
||||||
ON CONFLICT (giocatore_id) DO NOTHING;
|
ON CONFLICT (giocatore_id) DO NOTHING;
|
||||||
|
|
||||||
|
ALTER TABLE public.giocatori_squadra ENABLE TRIGGER enforce_giocatori_squadra_update;
|
||||||
|
|
||||||
-- Il primo amministratore non si può seminare qui: `user_roles.user_id` punta a un utente
|
-- Il primo amministratore non si può seminare qui: `user_roles.user_id` punta a un utente
|
||||||
-- di `auth.users`, che su un database appena creato non esiste ancora. Dopo il primo login
|
-- di `auth.users`, che su un database appena creato non esiste ancora. Dopo il primo login
|
||||||
-- (in locale come in produzione) basta una riga:
|
-- (in locale come in produzione) basta una riga:
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* Data di nascita pubblica a tutta la squadra (M18): `bun test/integration/nascita-pubblica.test.ts`.
|
||||||
|
*
|
||||||
|
* Prima di questa migration `giocatori_squadra` non aveva una colonna `nascita`: la Squadra
|
||||||
|
* mostrava "Invalid Date" per chi l'aveva inserita da solo dal Profilo, perché quel valore
|
||||||
|
* finiva solo in `profili_giocatore.data_nascita`, tabella con RLS "solo il proprio profilo
|
||||||
|
* o admin" — mai letta dall'anagrafica pubblica. Qui si verifica che il trigger di sync
|
||||||
|
* (`sincronizza_nascita_pubblica`) propaghi il valore a `giocatori_squadra.nascita`, visibile
|
||||||
|
* a **qualsiasi** giocatore (non solo l'interessato o un admin), che lo azzeri quando il
|
||||||
|
* profilo viene cancellato, e che il client non possa scriverlo direttamente.
|
||||||
|
*
|
||||||
|
* Gira solo sullo stack locale (`npx supabase start`): usa lo slot `g6` della rosa seed,
|
||||||
|
* ripristinato alla fine.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { statoLocale } from "../helpers/locale";
|
||||||
|
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||||
|
|
||||||
|
const locale = statoLocale();
|
||||||
|
|
||||||
|
if (!locale) {
|
||||||
|
salta("nascita pubblica", "stack locale non attivo (npx supabase start)");
|
||||||
|
riepilogo("nascita-pubblica");
|
||||||
|
} else {
|
||||||
|
const { url: URL_BASE, anon: ANON, servizio: SERVIZIO } = locale;
|
||||||
|
console.log(`nascita pubblica su ${URL_BASE}`);
|
||||||
|
|
||||||
|
const GIOCATORE = "g6";
|
||||||
|
const PASSWORD = "prova-nascita-pubblica-123";
|
||||||
|
const idUtenti: string[] = [];
|
||||||
|
let tokenAdmin = "";
|
||||||
|
|
||||||
|
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 ?? {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function creaUtente(email: 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: 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): 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: 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}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
async function leggiNascita(token: string): Promise<string | null> {
|
||||||
|
const res = await rest(`giocatori_squadra?id=eq.${GIOCATORE}&select=nascita`, token);
|
||||||
|
const righe = (await res.json()) as Array<{ nascita: string | null }>;
|
||||||
|
return righe[0]?.nascita ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const emailAdmin = `test-nascita-admin-${Date.now()}@example.test`;
|
||||||
|
const emailGiocatore = `test-nascita-giocatore-${Date.now()}@example.test`;
|
||||||
|
const emailAltro = `test-nascita-altro-${Date.now()}@example.test`;
|
||||||
|
const idAdmin = await creaUtente(emailAdmin);
|
||||||
|
const idGiocatore = await creaUtente(emailGiocatore);
|
||||||
|
const idAltro = await creaUtente(emailAltro);
|
||||||
|
idUtenti.push(idAdmin, idGiocatore, idAltro);
|
||||||
|
|
||||||
|
await rest("user_roles", SERVIZIO, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ user_id: idAdmin, role: "admin" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
tokenAdmin = await accedi(emailAdmin);
|
||||||
|
const tokenGiocatore = await accedi(emailGiocatore);
|
||||||
|
const tokenAltro = await accedi(emailAltro);
|
||||||
|
|
||||||
|
const collega = (utente: string | null) =>
|
||||||
|
rest(`giocatori_squadra?id=eq.${GIOCATORE}`, tokenAdmin, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ auth_user_id: utente }),
|
||||||
|
});
|
||||||
|
await collega(idGiocatore);
|
||||||
|
|
||||||
|
await prova(
|
||||||
|
"il giocatore salva la propria nascita e diventa visibile a un altro giocatore",
|
||||||
|
async () => {
|
||||||
|
const res = await rest("profili_giocatore", tokenGiocatore, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ giocatore_id: GIOCATORE, data_nascita: "2002-06-15" }),
|
||||||
|
});
|
||||||
|
assert.ok(res.ok, `salvataggio profilo: ${res.status} ${await res.text()}`);
|
||||||
|
|
||||||
|
assert.equal(await leggiNascita(tokenAltro), "2002-06-15", "visibile a un altro giocatore");
|
||||||
|
assert.equal(await leggiNascita(tokenAdmin), "2002-06-15", "visibile all'admin");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await prova("il client non può scrivere direttamente la nascita pubblica", async () => {
|
||||||
|
await rest(`giocatori_squadra?id=eq.${GIOCATORE}`, tokenGiocatore, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ nascita: "1900-01-01" }),
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
await leggiNascita(tokenAdmin),
|
||||||
|
"2002-06-15",
|
||||||
|
"il valore resta quello scritto dalla sync, non quello del tentativo diretto",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova("cancellare il profilo azzera la nascita pubblica", async () => {
|
||||||
|
const res = await rest(`profili_giocatore?giocatore_id=eq.${GIOCATORE}`, tokenAdmin, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
assert.ok(res.ok, `cancellazione profilo: ${res.status} ${await res.text()}`);
|
||||||
|
assert.equal(await leggiNascita(tokenAdmin), null, "torna NULL");
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova("un admin può impostare la nascita per conto del giocatore", async () => {
|
||||||
|
const res = await rest("profili_giocatore", tokenAdmin, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ giocatore_id: GIOCATORE, data_nascita: "1999-01-01" }),
|
||||||
|
});
|
||||||
|
assert.ok(res.ok, `salvataggio profilo da admin: ${res.status} ${await res.text()}`);
|
||||||
|
assert.equal(await leggiNascita(tokenGiocatore), "1999-01-01");
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// Con la service role questa DELETE fallirebbe: fa scattare il trigger di sync, che
|
||||||
|
// tenta un UPDATE su giocatori_squadra ancora collegato, e `enforce_giocatori_squadra_update`
|
||||||
|
// rifiuta la service role quanto un utente qualsiasi (auth.uid() NULL, nessun ramo passa).
|
||||||
|
// Serve il JWT vero dell'admin, come per la cancellazione dentro il test.
|
||||||
|
if (tokenAdmin) {
|
||||||
|
await rest(`profili_giocatore?giocatore_id=eq.${GIOCATORE}`, tokenAdmin, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await rest(`profili_giocatore?giocatore_id=eq.${GIOCATORE}`, 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("nascita-pubblica");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ const riga = (parziale: Partial<GiocatoreSquadra> = {}): GiocatoreSquadra => ({
|
|||||||
email: null,
|
email: null,
|
||||||
numeroTessera: null,
|
numeroTessera: null,
|
||||||
dataTessera: null,
|
dataTessera: null,
|
||||||
|
nascita: null,
|
||||||
...parziale,
|
...parziale,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,6 +48,11 @@ assert.deepEqual(
|
|||||||
giocatori.map((g) => g.id).sort(),
|
giocatori.map((g) => g.id).sort(),
|
||||||
"gli id combaciano con la rosa reale",
|
"gli id combaciano con la rosa reale",
|
||||||
);
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
Object.fromEntries(fallback.map((g) => [g.id, g.nascita])),
|
||||||
|
Object.fromEntries(giocatori.map((g) => [g.id, g.nascita])),
|
||||||
|
"la nascita del fallback combacia con quella del seed (M18)",
|
||||||
|
);
|
||||||
|
|
||||||
// --- nomeCompleto ----------------------------------------------------------------
|
// --- nomeCompleto ----------------------------------------------------------------
|
||||||
assert.equal(nomeCompleto(riga()), "Mario Rossi");
|
assert.equal(nomeCompleto(riga()), "Mario Rossi");
|
||||||
|
|||||||
Reference in new issue
Block a user