5 Commits
Author SHA1 Message Date
davideandClaude Opus 5 1036eb860d DD-011: make Google login the only way in
Remove the free player selection from /benvenuto: without a Supabase session
no screen renders, and the VITE_AUTH_OBBLIGATORIA bridge flag is gone.
Admin rights now come only from user_roles, so the hardcoded name list in
crapp-data.ts is deleted along with its tests.

Add migration m4_solo_autenticati, which revokes anon access to the v1.0
tables. Apply it only once the whole team has linked an account.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 18:21:34 +02:00
ivancacciari1995-a11y f325c0485c Merge pull request #2 from ivancacciari1995-a11y/fix/migration-history-cleanup
Clean up superseded profile migrations
2026-08-31 17:26:36 +02:00
Ivan Cacciari 5a6aaad885 Clean up superseded profile migrations 2026-08-31 17:15:58 +02:00
Ivan Cacciari e4f963d170 Update AI and collaboration workflow 2026-08-31 15:31:10 +02:00
Ivan CacciariandCursor f424b3a9f7 Add M2 migration for player profiles and private storage.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 15:25:29 +02:00
18 changed files with 812 additions and 248 deletions
+414 -105
View File
@@ -1,141 +1,450 @@
# AGENTS.md — CrAPP
# CrAPP - AI Development Guide
Regole che qualsiasi assistente AI (Claude Code, Codex, Cursor, ChatGPT o altri) deve seguire
quando lavora su questo progetto. **È l'unica fonte delle regole**: `CLAUDE.md` e
`.cursor/rules/` rimandano qui, non ripetono nulla.
Questo documento definisce le regole che qualsiasi assistente AI (Cursor, Claude Code, Codex, ChatGPT o altri) deve seguire quando lavora su questo progetto.
> **Qualsiasi cosa venga aggiunta o modificata — una regola, una funzionalità, una decisione,
> una tabella — va registrata nei file di riferimento del progetto prima di considerare il
> lavoro finito**, indipendentemente dall'assistente con cui è stata fatta. Vedi
> [Fine lavoro](#fine-lavoro-cosa-aggiornare-sempre): non è un passaggio opzionale.
---
CrAPP è una Progressive Web App per la gestione di una squadra di pallavolo amatoriale (CRAP
Volley). Obiettivi e principi in [docs/VISION.md](docs/VISION.md), architettura in
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
# Obiettivo del progetto
**Codice, commenti, nomi di variabili e documentazione sono in italiano**: mantieni questa
convenzione.
CrAPP è una Progressive Web App sviluppata per digitalizzare completamente la gestione di una squadra di pallavolo.
## Prima di modificare il codice
L'obiettivo principale è:
Leggere sempre, nell'ordine:
- ridurre il lavoro amministrativo degli amministratori;
1. [docs/README.md](docs/README.md) — indice della documentazione
2. [docs/VISION.md](docs/VISION.md)
3. [docs/ROADMAP.md](docs/ROADMAP.md)
4. [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
5. [docs/DATABASE.md](docs/DATABASE.md)
6. [docs/DESIGN_DECISIONS.md](docs/DESIGN_DECISIONS.md)
7. [docs/TODO.md](docs/TODO.md)
8. il documento del modulo interessato in [docs/modules/](docs/modules/)
- aumentare il coinvolgimento dei giocatori;
**Non implementare funzionalità non documentate** (DD-002).
- centralizzare tutte le informazioni della squadra;
## Workflow
- utilizzare l'intelligenza artificiale solo quando porta un reale beneficio.
```
idea → progettazione → documento in docs/modules/ → database → implementazione su develop
→ test → merge su main → deploy automatico Vercel
```
---
- `main` = produzione, sempre funzionante: **non si modifica direttamente**. Qualsiasi
modifica deve mantenere l'app perfettamente funzionante.
- `develop` = sviluppo e preview; tutte le nuove implementazioni nascono qui.
# Prima di modificare il codice
### Commit
Prima di implementare qualsiasi modifica leggere sempre:
- **Si committa solo quando l'utente lo chiede**, mai di propria iniziativa. Lo stesso vale
per il push, che su `develop` fa partire un deploy di preview.
- Quando l'utente lo chiede, **il messaggio lo scrive l'assistente in autonomia**, senza
farlo approvare prima.
- **Il messaggio è in inglese**, all'imperativo presente (`Add medical certificate expiry`),
riga di riepilogo sotto i 72 caratteri. È l'unica eccezione all'italiano: codice, commenti
e documentazione restano in italiano. I commit precedenti sono in italiano e non vanno
riscritti.
- Se il lavoro attua una decisione registrata, il messaggio la cita: `DD-017: ...`.
- Nel commit entrano insieme codice e documentazione: la checklist
[Fine lavoro](#fine-lavoro-cosa-aggiornare-sempre) va eseguita prima, non in un commit a parte.
1. docs/[README.md](http://README.md)
## Vincoli tecnici da non violare
2. docs/[VISION.md](http://VISION.md)
- `src/routeTree.gen.ts` e `src/integrations/supabase/{client.ts,types.ts}` sono **generati**:
non modificarli a mano.
- **Non ri-aggiungere** i plugin Vite (devtools, tanstackStart, viteReact, tailwind,
tsconfig-paths, nitro) già inclusi da `@lovable.dev/vite-tanstack-config` in
`vite.config.ts`: l'app si rompe.
- Nessun accesso al database dai componenti: solo tramite i moduli in `src/lib/`, così il
backend resta sostituibile in un solo punto (DD-013).
- Efficienza cloud: niente polling, cache lunga, e dopo una mutazione `setQueryData` invece di
`invalidateQueries`. Regole complete in [docs/EFFICIENZA_CLOUD.md](docs/EFFICIENZA_CLOUD.md).
- Badge e statistiche si calcolano a runtime, non si persistono (DD-007); la gamification
resta equa tra ruoli (DD-008): niente metriche che favoriscano attaccanti o liberi.
- L'app deve poter girare su Node.js + PostgreSQL standard: niente servizi esclusivi
Lovable/Vercel (DD-001, DD-013, [docs/PORTABILITA.md](docs/PORTABILITA.md)).
3. docs/[ROADMAP.md](http://ROADMAP.md)
## Database
4. docs/[ARCHITECTURE.md](http://ARCHITECTURE.md)
- Non eliminare tabelle esistenti.
- Non modificare lo schema senza creare una migration in `supabase/migrations/`.
- Preferire una nuova tabella all'aggiunta di molte colonne, quando il modulo è indipendente.
- Preferire strutture scalabili; evitare duplicazione dei dati.
- Riferimento: [docs/DATABASE.md](docs/DATABASE.md), da aggiornare nella stessa modifica che
cambia lo schema.
5. docs/[DATABASE.md](http://DATABASE.md)
## Codice e componenti
6. docs/DESIGN_[DECISIONS.md](http://DECISIONS.md)
- TypeScript, funzioni piccole, nomi descrittivi.
- Componenti piccoli, riutilizzabili, a responsabilità singola.
- Nessuna duplicazione: riusare sempre i componenti e i moduli esistenti.
- Commentare solo il codice realmente complesso.
- Nessuna nuova dipendenza senza reale necessità; mantenere la struttura esistente.
- Le dipendenze si installano con **bun**; `bunfig.toml` impone `minimumReleaseAge = 24h` come
guardia supply-chain: aggiungere un pacchetto a `minimumReleaseAgeExcludes` richiede
conferma esplicita dell'utente.
7. docs/[TODO.md](http://TODO.md)
## Interfaccia
8. il documento interessato in docs/modules/
Stile coerente con l'esistente: semplice, moderna, pulita, veloce, ottimizzata per
smartphone, poche schermate e pochi click (DD-005). Riusare i componenti in
`src/components/crapp/` (`ui-bits.tsx` per `PageHeader`, `Section`, `StatTile`) e le primitive
shadcn in `src/components/ui/`.
Inoltre, prima di iniziare una nuova attività:
## Fine lavoro: cosa aggiornare sempre
- verificare lo stato attuale del repository;
Il lavoro **non è finito** finché non è registrato dove va. Vale per tutti gli assistenti allo
stesso modo: chi fa la modifica aggiorna i file, chiunque la stia facendo e da qualunque
strumento. Un cambiamento che vive solo nel codice o solo nella chat è un cambiamento perso.
- controllare le modifiche e i commit recenti;
| Cosa hai aggiunto o cambiato | Dove va registrato |
|---|---|
| Una **regola** per gli assistenti (convenzione, divieto, vincolo di lavoro) | **Questo file, e solo questo.** Mai in `CLAUDE.md` o `.cursor/rules/`: rimandano qui, e una regola scritta lì la vedrebbe un assistente solo |
| Una **funzionalità** | `docs/modules/<modulo>.md` (**prima** di scrivere il codice), poi `docs/ROADMAP.md` e `docs/CHANGELOG.md` |
| Una **decisione** architetturale o di prodotto | `docs/DESIGN_DECISIONS.md`, formato `DD-XXX` (copiare `docs/_template-dd.md`) e aggiungerla all'indice in cima |
| Una **modifica allo schema** del database | una migration in `supabase/migrations/` **e** `docs/DATABASE.md`, nella stessa modifica |
| Lavoro iniziato, sospeso o concluso | `docs/TODO.md` e `PROJECT_STATE.md` |
| Un **comando** o uno script nuovo | `docs/ARCHITECTURE.md` (sezione Comandi) e `CLAUDE.md` |
- verificare eventuali modifiche introdotte da altri sviluppatori o assistenti AI;
Quale informazione vive in quale file — e perché non va duplicata altrove — è spiegato in
[docs/README.md](docs/README.md).
- leggere la documentazione aggiornata relativa alla funzionalità interessata.
## Cosa l'AI non deve fare
Non implementare funzionalità non documentate.
Non presumere che il progetto sia nello stesso stato dell'ultima sessione o conversazione.
---
# Workflow di sviluppo
Ogni nuova funzionalità segue sempre questo processo.
Idea
Progettazione
Documentazione
Database
Implementazione
Test
Pull Request
Merge su develop
Verifica
Merge su main
Deploy
Le funzionalità possono essere sviluppate in parallelo da persone diverse, ciascuna sul proprio branch.
---
# Git
Il repository utilizza due branch principali.
## main
Versione stabile.
Qualsiasi modifica deve mantenere l'app perfettamente funzionante.
`main` rappresenta la versione destinata alla produzione.
## develop
Branch di integrazione e test.
Le nuove funzionalità vengono integrate in `develop` prima di arrivare in `main`.
Non lavorare direttamente su `main`.
Evitare modifiche dirette a `develop`, salvo attività esplicitamente concordate.
---
# Branch di sviluppo
Ogni sviluppatore deve lavorare su un branch dedicato creato a partire da `develop`.
Esempi:
- `feature/profilo-giocatore`
- `feature/integrazione-csi`
- `fix/presenze`
- `refactor/supabase-client`
Non utilizzare lo stesso branch contemporaneamente per attività indipendenti.
Prima di iniziare un'attività verificare che il branch sia aggiornato rispetto a `develop`.
---
# Integrazione delle modifiche
Le modifiche significative devono essere integrate tramite Pull Request verso `develop`.
Una Pull Request dovrebbe permettere di capire:
- cosa è stato modificato;
- perché è stato modificato;
- quali file o moduli sono coinvolti;
- se il database è stato modificato;
- quali test sono stati eseguiti;
- eventuali rischi o conseguenze.
Prima del merge verificare eventuali conflitti con il lavoro sviluppato nel frattempo dagli altri collaboratori.
---
# Tracciabilità delle modifiche
Ogni modifica significativa deve lasciare una traccia nel progetto.
Devono essere utilizzati:
- commit con messaggi descrittivi;
- Pull Request per l'integrazione;
- [CHANGELOG.md](http://CHANGELOG.md) quando una modifica deve essere registrata nella cronologia del progetto;
- PROJECT_[STATE.md](http://STATE.md) quando cambia lo stato generale del progetto;
- DESIGN_[DECISIONS.md](http://DECISIONS.md) per decisioni architetturali significative.
La documentazione deve permettere a uno sviluppatore o a un assistente AI di ricostruire cosa è successo senza dipendere dalla cronologia delle conversazioni.
---
# Aggiornamento del contesto dopo la sincronizzazione
Quando vengono scaricate modifiche da GitHub, l'assistente AI deve considerare il repository come fonte di verità.
Prima di iniziare una nuova attività deve:
1. verificare i nuovi commit;
2. identificare le modifiche rilevanti;
3. leggere la documentazione modificata;
4. verificare eventuali modifiche al database;
5. tenere conto delle nuove decisioni architetturali.
Non ignorare modifiche introdotte da altri collaboratori.
Non sovrascrivere modifiche esistenti senza averne compreso lo scopo.
---
# Architettura
Frontend
- React
- TypeScript
- TanStack Start
- Tailwind CSS
Backend
- Supabase
Hosting
- Vercel
Repository
- GitHub
---
# Database
Il database utilizza Supabase.
Regole:
- non eliminare tabelle esistenti;
- non modificare lo schema senza creare una migration;
- preferire strutture scalabili;
- evitare duplicazione dei dati;
- non modificare migration già applicate;
- ogni modifica allo schema deve essere rappresentata da una nuova migration.
Fare sempre riferimento a:
docs/[DATABASE.md](http://DATABASE.md)
---
# Componenti
Preferire:
- componenti piccoli;
- componenti riutilizzabili;
- responsabilità singola;
- codice semplice da mantenere.
Evitare duplicazioni.
Prima di creare un nuovo componente verificare se esiste già un componente riutilizzabile.
---
# Interfaccia
Lo stile dell'app deve rimanere coerente.
Principi:
- semplice;
- moderna;
- pulita;
- veloce;
- ottimizzata per smartphone;
- poche schermate;
- pochi click.
---
# Documentazione
Ogni nuova funzionalità deve essere documentata prima dello sviluppo.
La documentazione dei moduli si trova in:
docs/modules/
Aggiornare sempre, quando necessario:
- [ROADMAP.md](http://ROADMAP.md)
- [CHANGELOG.md](http://CHANGELOG.md)
- [TODO.md](http://TODO.md)
- [DATABASE.md](http://DATABASE.md) (se il database cambia)
- DESIGN_[DECISIONS.md](http://DECISIONS.md) (se si prende una decisione architetturale importante)
- PROJECT_[STATE.md](http://STATE.md) (se cambia lo stato generale del progetto)
---
# Struttura della documentazione
La cartella `docs/` rappresenta la documentazione ufficiale del progetto.
## Documenti principali
- [README.md](http://README.md) → panoramica del progetto
- [VISION.md](http://VISION.md) → obiettivi e filosofia
- [ROADMAP.md](http://ROADMAP.md) → evoluzione prevista
- [ARCHITECTURE.md](http://ARCHITECTURE.md) → architettura tecnica
- [DATABASE.md](http://DATABASE.md) → struttura del database
- DESIGN_[DECISIONS.md](http://DECISIONS.md) → registro delle decisioni di progetto
- [CHANGELOG.md](http://CHANGELOG.md) → cronologia delle modifiche
- [TODO.md](http://TODO.md) → attività pianificate
- PROJECT_[STATE.md](http://STATE.md) → stato attuale del progetto
## Moduli
La cartella `docs/modules/` contiene una specifica funzionale per ogni modulo dell'applicazione.
Ogni nuovo modulo deve essere progettato e documentato prima dell'implementazione.
---
# Regola anti-regressione
Le nuove versioni devono principalmente aggiungere funzionalità.
Non riscrivere o modificare profondamente moduli già funzionanti senza una motivazione esplicita e una verifica degli impatti.
Evitare refactoring trasversali durante lo sviluppo di nuove funzionalità, salvo quando sono necessari per la funzionalità stessa.
Prima di modificare un modulo esistente verificare quali altre parti dell'app lo utilizzano.
---
# Regole per il database e le migration
Le migration già applicate sono parte della storia del database e non devono essere riscritte.
Per modificare il database:
1. progettare la modifica;
2. documentarla quando necessario;
3. creare una nuova migration;
4. testarla;
5. applicarla all'ambiente di sviluppo;
6. verificare l'assenza di regressioni;
7. solo successivamente applicarla all'ambiente di produzione.
---
# Regole
L'AI non deve:
- introdurre librerie senza necessità;
- modificare il database senza motivazione;
- eliminare funzionalità esistenti;
- modificare il comportamento dell'app senza richiesta esplicita.
## Cosa l'AI deve fare
- modificare il database senza motivazione;
- eliminare funzionalità esistenti;
- modificare il comportamento dell'app senza richiesta esplicita;
- sovrascrivere modifiche di altri collaboratori senza comprenderle;
- riscrivere migration già applicate;
- lavorare direttamente su `main`;
- assumere che il repository sia invariato rispetto all'ultima sessione.
L'AI deve:
- spiegare le modifiche importanti;
- mantenere compatibilità con il codice esistente;
- privilegiare la semplicità;
- riutilizzare i componenti esistenti.
## Filosofia
- riutilizzare i componenti esistenti;
Prima di scrivere codice, chiedersi sempre:
- controllare il lavoro recente degli altri collaboratori;
- mantenere aggiornata la documentazione quando necessario;
- segnalare conflitti, rischi e possibili regressioni prima di modificare parti sensibili.
---
# Filosofia del progetto
Prima di scrivere codice chiedersi sempre:
Questa modifica rende CrAPP più semplice?
Riduce il lavoro degli amministratori?
Migliora l'esperienza dei giocatori?
È coerente con la documentazione?
Riduce oppure aumenta la complessità futura?
Se almeno una risposta è negativa, rivalutare la soluzione proposta.
- questa modifica rende CrAPP più semplice?
- riduce il lavoro degli amministratori?
- migliora l'esperienza dei giocatori?
- è coerente con la documentazione?
+10 -10
View File
@@ -55,19 +55,19 @@ admin) implementati su `develop`, da attivare in produzione seguendo i passaggi
## Autenticazione e dashboard amministratore
Implementate su `develop`, **non ancora attive in produzione**. Il codice è additivo: finché
i passaggi qui sotto non sono fatti, l'app si comporta esattamente come prima.
Implementate su `develop`. **Il login è l'unica via d'accesso** (31/08/2026): la selezione
libera del giocatore non esiste più, senza sessione Google si resta su `/benvenuto`, e i
permessi di amministrazione arrivano solo da `user_roles`.
**Stato in sviluppo (30/08/2026):** il codice c'è ed è completo, ma il login non funziona
ancora. Con `npm run dev`, «Accedi con Google» risponde:
**Attenzione all'ordine:** finché il provider Google è spento in Supabase, «Accedi con
Google» risponde
```
{"code":400,"error_code":"validation_failed","msg":"Unsupported provider: provider is not enabled"}
```
Non è un difetto dell'app: l'errore arriva da Supabase, dove il provider Google è spento.
È il passo 1 qui sotto, ancora da fare. Tutto il resto dell'app in dev funziona normalmente,
perché l'accesso avviene ancora scegliendo il proprio nome.
e **nessuno entra nell'app**, né in dev né sulla preview di `develop`. Il passo 1 qui sotto
va fatto prima di mandare questa versione in produzione.
Passaggi in ordine, nessuno dei quali è reversibile a metà:
@@ -88,9 +88,9 @@ Passaggi in ordine, nessuno dei quali è reversibile a metà:
`INSERT INTO public.user_roles (user_id, role) SELECT id, 'admin' FROM auth.users WHERE email = '<mail>';`
4. **Collegamento dei 17 account**: ciascuno accede con Google e sceglie il proprio nome una
volta sola. Uno slot già collegato può essere liberato solo da un admin.
5. **Solo a squadra collegata**: `VITE_AUTH_OBBLIGATORIA=true` su Vercel (fa sparire la
selezione libera del giocatore), poi la migration che rimuove le policy `anon` dalle
tabelle v1.0. È l'unico passo che cambia il comportamento per tutti.
5. **Solo a squadra collegata**: migration `m4_solo_autenticati`, che toglie al ruolo `anon`
l'accesso alle tabelle v1.0. Da lì in poi i dati sono raggiungibili solo con una sessione;
le route in `src/routes/api/public/` usano la service role e continuano a funzionare.
Attenzione: dev e produzione condividono lo stesso progetto Supabase. Un account di prova che
collega uno slot lo occupa anche in produzione, e va liberato da un admin.
+4 -4
View File
@@ -46,10 +46,10 @@ docs/ documentazione ufficiale
generato) e `client.server.ts` (`supabaseAdmin`, solo server). `types.ts` è generato dallo
schema; finché non viene rigenerato, le tabelle introdotte da M1/M2 si usano tramite
`client-nuove-tabelle.ts`, con i tipi di riga dichiarati nei moduli di `src/lib/`.
- **Autenticazione**: login Google via Supabase Auth (`src/lib/auth.ts`, DD-011); i permessi
di amministrazione arrivano da `user_roles` (`src/lib/ruoli.ts`). La variabile
`VITE_AUTH_OBBLIGATORIA` decide se `/benvenuto` accetta ancora la selezione libera del
giocatore: finché è spenta, login e vecchio accesso convivono.
- **Autenticazione**: login Google via Supabase Auth (`src/lib/auth.ts`, DD-011). È l'unica
strada di accesso: `__root.tsx` rimanda a `/benvenuto` chi non ha sessione, e l'identità
del giocatore è lo slot di `giocatori_squadra` collegato all'account. I permessi di
amministrazione arrivano solo da `user_roles` (`src/lib/ruoli.ts`).
## Livello dati
+9 -3
View File
@@ -11,9 +11,15 @@ qui: sta in [ROADMAP.md](ROADMAP.md).
- Login con Google tramite Supabase Auth (DD-011). Al primo accesso l'account si collega a
un giocatore di `giocatori_squadra`, e il collegamento non è più modificabile dal
giocatore stesso (DD-016 regola 2).
- I permessi di amministrazione arrivano da `user_roles` (`src/lib/ruoli.ts`) e non più
dalla lista di nomi in `crapp-data.ts`, che resta solo come ponte finché
`VITE_AUTH_OBBLIGATORIA` non viene acceso.
- La selezione libera del giocatore è stata rimossa: `/benvenuto` offre solo l'accesso con
Google e senza sessione non si entra in nessuna schermata. Sparita anche la variabile
`VITE_AUTH_OBBLIGATORIA` (non serve più) e il pulsante «Cambia giocatore» in `/profilo`.
- I permessi di amministrazione arrivano solo da `user_roles` (`src/lib/ruoli.ts`): la lista
di nomi in `crapp-data.ts` è stata eliminata, altrimenti bastava scegliere il nome giusto
per amministrare.
- Migration `m4_solo_autenticati`: toglie al ruolo `anon` l'accesso alle tabelle v1.0.
**Da applicare solo a squadra collegata**, altrimenti chi non ha ancora fatto login vede
l'app vuota.
- Profilo giocatore: da `/profilo` ognuno compila i propri dati anagrafici e carica
documento, certificato medico e foto tessera con le relative scadenze
([modules/profilo-giocatore.md](modules/profilo-giocatore.md)).
+5 -7
View File
@@ -6,13 +6,11 @@ Solo il lavoro in corso o imminente. L'elenco completo delle funzionalità previ
## In corso
- Documentazione tecnica del progetto.
- Autenticazione Google e dashboard amministratore: implementate su `develop`, ma in dev il
login risponde ancora `provider is not enabled` perché il provider Google non è stato
acceso in Supabase. Restano da
fare, in quest'ordine: configurazione del provider Google in Supabase, applicazione delle
migration M2/M3, inserimento del primo admin in `user_roles`, collegamento dei 17 account,
e solo alla fine `VITE_AUTH_OBBLIGATORIA=true` + rimozione delle policy `anon`.
Stato di dettaglio in [PROJECT_STATE.md](../PROJECT_STATE.md).
- Autenticazione Google e dashboard amministratore: il codice è completo su `develop` e il
login è ora l'unica via d'accesso. Restano i passaggi di configurazione, in quest'ordine:
provider Google in Supabase (senza, nessuno entra), migration M2/M3, primo admin in
`user_roles`, collegamento dei 17 account, e infine la migration M4 che chiude gli accessi
`anon`. Stato di dettaglio in [PROJECT_STATE.md](../PROJECT_STATE.md).
## Prossimo
+3 -8
View File
@@ -3,15 +3,10 @@ import type { Session } from "@supabase/supabase-js";
import { supabase } from "@/integrations/supabase/client";
/**
* Autenticazione reale con Google (DD-011). Il login non ha ancora sostituito la
* selezione del giocatore: finché `VITE_AUTH_OBBLIGATORIA` non è `true`, `/benvenuto`
* offre entrambe le strade, così la produzione continua a funzionare mentre la squadra
* collega gli account.
* Autenticazione reale con Google (DD-011). Il login ha sostituito la selezione del
* giocatore: senza sessione non si entra, e i permessi di amministrazione arrivano solo
* da `user_roles` (vedi `ruoli.ts`).
*/
export function authObbligatoria(): boolean {
return import.meta.env["VITE_AUTH_OBBLIGATORIA"] === "true";
}
export function useSessione() {
const [sessione, setSessione] = useState<Session | null>(null);
const [pronta, setPronta] = useState(false);
+2 -7
View File
@@ -169,10 +169,5 @@ export function formatData(iso: string) {
return d.toLocaleDateString("it-IT", { weekday: "short", day: "2-digit", month: "long" });
}
/** Referenti che possono gestire eventi e sollecitare le risposte. */
export const adminNomi = ["Ivan Cacciari", "Iacopo Ricci", "Cristina Titone"];
export function isAdmin(giocatoreId: string) {
const g = giocatori.find((x) => x.id === giocatoreId);
return Boolean(g && adminNomi.includes(g.nome));
}
/* I permessi di amministrazione stanno in `user_roles` (DD-011), non in una lista di nomi:
vedi `src/lib/ruoli.ts`. */
+3 -13
View File
@@ -1,22 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { isAdmin as nomeInListaAdmin } from "./crapp-data";
import { useSessione } from "./auth";
import { useGiocatoreBase } from "./user-store";
export const RUOLI_KEY = ["ruolo-admin"] as const;
/**
* Permessi di amministrazione. La fonte è `user_roles` nel database (DD-011): la lista di
* nomi in `crapp-data.ts` resta solo come ponte per chi non ha ancora collegato l'account,
* e sparisce quando `VITE_AUTH_OBBLIGATORIA` viene acceso in produzione.
*
* ponytail: doppia fonte temporanea, si riduce a `ruoloDb` appena l'auth è obbligatoria.
* Permessi di amministrazione: unica fonte è `user_roles` nel database (DD-011).
* Nessuna lista di nomi, altrimenti basterebbe scegliere il nome giusto per amministrare.
*/
export function risolviAdmin(ruoloDb: boolean | null, giocatoreId: string | null): boolean {
if (ruoloDb !== null) return ruoloDb;
return giocatoreId ? nomeInListaAdmin(giocatoreId) : false;
}
/** `null` = nessuna sessione, quindi il database non ha una risposta da dare. */
async function fetchRuoloAdmin(utenteId: string | null): Promise<boolean | null> {
@@ -33,12 +24,11 @@ async function fetchRuoloAdmin(utenteId: string | null): Promise<boolean | null>
export function useIsAdmin(): boolean {
const { utenteId } = useSessione();
const io = useGiocatoreBase();
// Il ruolo cambia solo quando un admin lo assegna: una lettura per sessione basta.
const query = useQuery({
queryKey: [...RUOLI_KEY, utenteId],
queryFn: () => fetchRuoloAdmin(utenteId),
staleTime: 30 * 60_000,
});
return risolviAdmin(query.data ?? null, io?.id ?? null);
return query.data === true;
}
+8 -3
View File
@@ -18,6 +18,7 @@ import { CelebrazioneBadge } from "../components/crapp/CelebrazioneBadge";
import { Toaster } from "../components/ui/sonner";
import { TeamLogo } from "../components/crapp/ui-bits";
import { useGiocatoreBase } from "../lib/user-store";
import { useSessione } from "../lib/auth";
function NotFoundComponent() {
return (
@@ -145,17 +146,21 @@ function RootComponent() {
const navigate = useNavigate();
const location = useLocation();
const giocatore = useGiocatoreBase();
const { pronta, utenteId } = useSessione();
const [mounted, setMounted] = useState(false);
const isBenvenuto = location.pathname === "/benvenuto";
// Senza sessione Google non si entra: l'identità la dà il login, non la scelta del nome
// (DD-011). Si aspetta `pronta`, altrimenti il primo render sloggato rimbalzerebbe fuori
// chi ha già la sessione in localStorage.
useEffect(() => {
setMounted(true);
if (!giocatore && !isBenvenuto) {
if (pronta && (!giocatore || !utenteId) && !isBenvenuto) {
navigate({ to: "/benvenuto" });
}
}, [giocatore, isBenvenuto, navigate]);
}, [giocatore, utenteId, pronta, isBenvenuto, navigate]);
if (!mounted) {
if (!mounted || !pronta) {
return (
<div className="grid min-h-screen place-items-center bg-background">
<TeamLogo className="h-16 w-16 animate-pulse" />
+9 -28
View File
@@ -3,8 +3,7 @@ import { useEffect, useState } from "react";
import { LogIn } from "lucide-react";
import { toast } from "sonner";
import { TeamLogo } from "@/components/crapp/ui-bits";
import { giocatori } from "@/lib/crapp-data";
import { accediConGoogle, authObbligatoria, useSessione } from "@/lib/auth";
import { accediConGoogle, useSessione } from "@/lib/auth";
import {
nomeCompleto,
slotDi,
@@ -13,7 +12,7 @@ import {
useGiocatoriSquadra,
type GiocatoreSquadra,
} from "@/lib/giocatori-squadra";
import { impostaGiocatore, useGiocatoreCorrente } from "@/lib/user-store";
import { impostaGiocatore, resetGiocatore, useGiocatoreCorrente } from "@/lib/user-store";
export const Route = createFileRoute("/benvenuto")({
head: () => ({
@@ -65,23 +64,24 @@ function Benvenuto() {
const navigate = useNavigate();
const giocatore = useGiocatoreCorrente();
const { pronta, utenteId } = useSessione();
const { righe } = useGiocatoriSquadra();
const { righe, daDatabase } = useGiocatoriSquadra();
const collega = useCollegaGiocatore();
const [inCorso, setInCorso] = useState(false);
const mioSlot = slotDi(righe, utenteId);
// Con l'auth obbligatoria si entra solo da loggati; finché non lo è, la selezione
// diretta resta come ponte per chi non ha ancora collegato l'account (DD-011).
const puoEntrare = !!giocatore && (!!utenteId || !authObbligatoria());
// Si entra solo da loggati e con uno slot collegato (DD-011).
const puoEntrare = !!giocatore && !!utenteId;
useEffect(() => {
if (puoEntrare) navigate({ to: "/" });
}, [puoEntrare, navigate]);
// L'account è già collegato a uno slot: nessuna scelta da fare.
// Chi sei lo dice lo slot collegato all'account, non quello che c'è in localStorage:
// senza slot la scelta salvata dalla vecchia selezione libera va buttata.
useEffect(() => {
if (mioSlot) impostaGiocatore(mioSlot.id);
}, [mioSlot]);
else if (utenteId && daDatabase) resetGiocatore();
}, [mioSlot, utenteId, daDatabase]);
async function accedi() {
setInCorso(true);
@@ -125,25 +125,6 @@ function Benvenuto() {
>
<LogIn className="h-4 w-4" /> Accedi con Google
</button>
{authObbligatoria() ? null : (
<div className="mt-10 w-full max-w-sm">
<p className="mb-3 text-center text-xs text-muted-foreground">
Oppure entra scegliendo il tuo nome, come prima.
</p>
<div className="space-y-2">
{giocatori.map((g) => (
<Scheda
key={g.id}
titolo={g.nome}
sottotitolo={`#${g.numero} · ${g.ruolo}`}
iniziali={g.iniziali}
onClick={() => impostaGiocatore(g.id)}
/>
))}
</div>
</div>
)}
</>
) : (
<>
+5 -16
View File
@@ -1,7 +1,7 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { Flame, Camera, Users, Trash2, Bell, LogOut, ShieldCheck } from "lucide-react";
import { Flame, Camera, Trash2, Bell, LogOut, ShieldCheck } from "lucide-react";
import { cn } from "@/lib/utils";
import { PageHeader, Section, StatTile } from "@/components/crapp/ui-bits";
import { Avatar } from "@/components/crapp/Avatar";
@@ -19,7 +19,7 @@ import {
statoNotifiche,
} from "@/lib/push-client";
import { resetGiocatore } from "@/lib/user-store";
import { esci, useSessione } from "@/lib/auth";
import { esci } from "@/lib/auth";
import { useIsAdmin } from "@/lib/ruoli";
import { Reveal } from "@/components/motion/Reveal";
@@ -42,7 +42,6 @@ function Profilo() {
const votiSocial = useVotiSocial();
const g = useIo();
const admin = useIsAdmin();
const { sessione } = useSessione();
const ultimoMese = usePresenzeUltimoMese(g?.id);
const inputRef = useRef<HTMLInputElement>(null);
const foto = useAvatar(g?.id);
@@ -230,22 +229,12 @@ function Profilo() {
) : null}
<button
type="button"
onClick={() => resetGiocatore()}
onClick={logout}
className="flex w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5"
>
<span className="min-w-0 truncate">Cambia giocatore</span>
<Users className="h-4 w-4 text-muted-foreground" />
<span className="min-w-0 truncate">Esci</span>
<LogOut className="h-4 w-4 text-muted-foreground" />
</button>
{sessione ? (
<button
type="button"
onClick={logout}
className="flex w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5"
>
<span className="min-w-0 truncate">Esci</span>
<LogOut className="h-4 w-4 text-muted-foreground" />
</button>
) : null}
</div>
</Section>
</>
@@ -0,0 +1,185 @@
-- M2 — Profilo giocatore + Storage privato (DD-016)
-- Tabella e bucket additive: non modifica alcuna tabella v1.0 esistente.
CREATE TYPE public.doc_identita_tipo AS ENUM (
'carta_identita',
'passaporto',
'patente'
);
CREATE OR REPLACE FUNCTION public.mio_giocatore_id()
RETURNS text
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT id
FROM public.giocatori_squadra
WHERE auth_user_id = auth.uid()
LIMIT 1;
$$;
REVOKE ALL ON FUNCTION public.mio_giocatore_id() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.mio_giocatore_id() TO authenticated;
CREATE TABLE public.profili_giocatore (
giocatore_id text PRIMARY KEY
REFERENCES public.giocatori_squadra(id) ON DELETE CASCADE,
data_nascita date,
luogo_nascita text,
indirizzo text,
telefono text,
email text,
doc_tipo public.doc_identita_tipo,
doc_numero text,
doc_rilasciato_da text,
doc_data_emissione date,
doc_data_scadenza date,
doc_fronte_path text,
doc_retro_path text,
cert_scadenza date,
cert_file_path text,
foto_tessera_path text,
avatar_path text,
creato_il timestamptz NOT NULL DEFAULT now(),
aggiornato_il timestamptz NOT NULL DEFAULT now(),
CONSTRAINT profilo_date_doc_valide CHECK (
doc_data_emissione IS NULL
OR doc_data_scadenza IS NULL
OR doc_data_emissione <= doc_data_scadenza
)
);
COMMENT ON TABLE public.profili_giocatore IS
'Dati personali e path documenti. File binari nel bucket privato profili-giocatore.';
CREATE OR REPLACE FUNCTION public.enforce_profili_giocatore_pk()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
IF NEW.giocatore_id IS DISTINCT FROM OLD.giocatore_id THEN
RAISE EXCEPTION 'giocatore_id non modificabile';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER enforce_profili_giocatore_pk
BEFORE UPDATE ON public.profili_giocatore
FOR EACH ROW
EXECUTE FUNCTION public.enforce_profili_giocatore_pk();
CREATE TRIGGER update_profili_giocatore_aggiornato_il
BEFORE UPDATE ON public.profili_giocatore
FOR EACH ROW
EXECUTE FUNCTION public.update_aggiornato_il();
GRANT SELECT, INSERT, UPDATE ON public.profili_giocatore TO authenticated;
GRANT ALL ON public.profili_giocatore TO service_role;
ALTER TABLE public.profili_giocatore ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Players can view own profile"
ON public.profili_giocatore
FOR SELECT
TO authenticated
USING (
giocatore_id = public.mio_giocatore_id()
OR public.has_role(auth.uid(), 'admin'::public.app_role)
);
CREATE POLICY "Players can insert own profile"
ON public.profili_giocatore
FOR INSERT
TO authenticated
WITH CHECK (
giocatore_id = public.mio_giocatore_id()
OR public.has_role(auth.uid(), 'admin'::public.app_role)
);
CREATE POLICY "Players can update own profile"
ON public.profili_giocatore
FOR UPDATE
TO authenticated
USING (
giocatore_id = public.mio_giocatore_id()
OR public.has_role(auth.uid(), 'admin'::public.app_role)
)
WITH CHECK (
giocatore_id = public.mio_giocatore_id()
OR public.has_role(auth.uid(), 'admin'::public.app_role)
);
CREATE POLICY "Admins can delete profiles"
ON public.profili_giocatore
FOR DELETE
TO authenticated
USING (public.has_role(auth.uid(), 'admin'::public.app_role));
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'profili-giocatore',
'profili-giocatore',
false,
10485760,
ARRAY['image/jpeg', 'image/png', 'image/webp', 'application/pdf']
);
CREATE POLICY "Players can read own profile files"
ON storage.objects
FOR SELECT
TO authenticated
USING (
bucket_id = 'profili-giocatore'
AND (
(storage.foldername(name))[1] = public.mio_giocatore_id()
OR public.has_role(auth.uid(), 'admin'::public.app_role)
)
);
CREATE POLICY "Players can upload own profile files"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'profili-giocatore'
AND (
(storage.foldername(name))[1] = public.mio_giocatore_id()
OR public.has_role(auth.uid(), 'admin'::public.app_role)
)
);
CREATE POLICY "Players can overwrite own profile files"
ON storage.objects
FOR UPDATE
TO authenticated
USING (
bucket_id = 'profili-giocatore'
AND (
(storage.foldername(name))[1] = public.mio_giocatore_id()
OR public.has_role(auth.uid(), 'admin'::public.app_role)
)
)
WITH CHECK (
bucket_id = 'profili-giocatore'
AND (
(storage.foldername(name))[1] = public.mio_giocatore_id()
OR public.has_role(auth.uid(), 'admin'::public.app_role)
)
);
CREATE POLICY "Players can delete own profile files"
ON storage.objects
FOR DELETE
TO authenticated
USING (
bucket_id = 'profili-giocatore'
AND (
(storage.foldername(name))[1] = public.mio_giocatore_id()
OR public.has_role(auth.uid(), 'admin'::public.app_role)
)
);
@@ -0,0 +1,19 @@
-- M4 — Chiusura degli accessi anonimi alle tabelle v1.0 (DD-011).
--
-- Da applicare SOLO quando tutti hanno collegato l'account Google: da qui in poi il
-- ruolo `anon` non legge né scrive più nulla, quindi chi non ha fatto login vede l'app
-- vuota. Le route in `src/routes/api/public/` usano la service role e non sono toccate.
--
-- Le policy restano dichiarate `TO anon, authenticated`: senza GRANT il ruolo anon non
-- arriva comunque alla tabella, e le policy continuano a valere per gli autenticati.
REVOKE ALL ON public.turni_palloni FROM anon;
REVOKE ALL ON public.push_subscriptions FROM anon;
REVOKE ALL ON public.badge_social_voti FROM anon;
REVOKE ALL ON public.scout_sessioni FROM anon;
REVOKE ALL ON public.scout_live FROM anon;
REVOKE ALL ON public.mvp_voti FROM anon;
REVOKE ALL ON public.risposte_presenze FROM anon;
REVOKE ALL ON public.eventi_app FROM anon;
REVOKE ALL ON public.pagelle_voti FROM anon;
REVOKE ALL ON public.cacche_partita FROM anon;
@@ -0,0 +1,135 @@
-- Correzione schema profili_giocatore (post 20260830123000)
--
-- Prerequisito: la migration 20260830123000 deve essere già applicata (schema doc_*,
-- enum doc_identita_tipo, funzione mio_giocatore_id, policy inglesi).
-- Porta il database allo schema atteso dal codice di develop (20260830120000 + 20100).
-- Non modifica giocatori_squadra né le tabelle v1.0.
-- Supabase avvolge ogni migration in una transazione: non serve BEGIN/COMMIT esplicito
-- (inserirli nel file può interferire con db push).
-- ── 1. Policy tabella: rimuovi quelle che dipendono da mio_giocatore_id() ───
DROP POLICY "Players can view own profile" ON public.profili_giocatore;
DROP POLICY "Players can insert own profile" ON public.profili_giocatore;
DROP POLICY "Players can update own profile" ON public.profili_giocatore;
-- Policy admin separata: va sostituita dal modello Davide (FOR ALL).
DROP POLICY "Admins can delete profiles" ON public.profili_giocatore;
-- ── 2. Policy Storage: tutte usano mio_giocatore_id() ──────────────────────
DROP POLICY "Players can read own profile files" ON storage.objects;
DROP POLICY "Players can upload own profile files" ON storage.objects;
DROP POLICY "Players can overwrite own profile files" ON storage.objects;
DROP POLICY "Players can delete own profile files" ON storage.objects;
-- ── 3. Trigger e funzione PK enforcement (assenti nello schema Davide) ─────
DROP TRIGGER enforce_profili_giocatore_pk ON public.profili_giocatore;
DROP FUNCTION public.enforce_profili_giocatore_pk();
-- ── 4. Vincolo sulle date documento (prima delle rinomine di colonna) ────────
ALTER TABLE public.profili_giocatore
DROP CONSTRAINT profilo_date_doc_valide;
-- ── 5. doc_tipo enum → documento_tipo text (valori UI del codice) ──────────
ALTER TABLE public.profili_giocatore
ALTER COLUMN doc_tipo TYPE text
USING (
CASE doc_tipo::text
WHEN 'carta_identita' THEN 'Carta d''identità'
WHEN 'patente' THEN 'Patente'
WHEN 'passaporto' THEN 'Passaporto'
ELSE doc_tipo::text
END
);
ALTER TABLE public.profili_giocatore
RENAME COLUMN doc_tipo TO documento_tipo;
-- ── 6. Rinomina colonne allo schema atteso da profili-core.ts ───────────────
ALTER TABLE public.profili_giocatore RENAME COLUMN doc_numero TO documento_numero;
ALTER TABLE public.profili_giocatore RENAME COLUMN doc_rilasciato_da TO documento_rilasciato_da;
ALTER TABLE public.profili_giocatore RENAME COLUMN doc_data_emissione TO documento_emissione;
ALTER TABLE public.profili_giocatore RENAME COLUMN doc_data_scadenza TO documento_scadenza;
ALTER TABLE public.profili_giocatore RENAME COLUMN doc_fronte_path TO documento_fronte_path;
ALTER TABLE public.profili_giocatore RENAME COLUMN doc_retro_path TO documento_retro_path;
ALTER TABLE public.profili_giocatore RENAME COLUMN cert_scadenza TO certificato_scadenza;
ALTER TABLE public.profili_giocatore RENAME COLUMN cert_file_path TO certificato_path;
ALTER TABLE public.profili_giocatore RENAME COLUMN foto_tessera_path TO foto_path;
-- ── 7. Colonna non usata dal codice ─────────────────────────────────────────
ALTER TABLE public.profili_giocatore DROP COLUMN avatar_path;
-- ── 8. Oggetti della migration 123000 non più necessari ─────────────────────
DROP FUNCTION public.mio_giocatore_id();
DROP TYPE public.doc_identita_tipo;
COMMENT ON TABLE public.profili_giocatore IS
'Dati personali, documento e certificato di ciascun giocatore, 1:1 con giocatori_squadra. Vedi DD-016.';
-- ── 9. RLS tabella (modello Davide, DD-017) ─────────────────────────────────
CREATE POLICY "Il giocatore legge il proprio profilo" ON public.profili_giocatore
FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.giocatori_squadra g
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid()
)
);
CREATE POLICY "Il giocatore crea il proprio profilo" ON public.profili_giocatore
FOR INSERT TO authenticated
WITH CHECK (
EXISTS (
SELECT 1 FROM public.giocatori_squadra g
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid()
)
);
CREATE POLICY "Il giocatore aggiorna il proprio profilo" ON public.profili_giocatore
FOR UPDATE TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.giocatori_squadra g
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid()
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM public.giocatori_squadra g
WHERE g.id = giocatore_id AND g.auth_user_id = auth.uid()
)
);
CREATE POLICY "Gli admin gestiscono tutti i profili" ON public.profili_giocatore
FOR ALL TO authenticated
USING (public.has_role(auth.uid(), 'admin'::public.app_role))
WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role));
-- ── 10. Policy Storage (DD-017: admin solo lettura) ─────────────────────────
CREATE POLICY "Il giocatore gestisce i propri file" ON storage.objects
FOR ALL TO authenticated
USING (
bucket_id = 'profili-giocatore'
AND EXISTS (
SELECT 1 FROM public.giocatori_squadra g
WHERE g.id = (storage.foldername(name))[1] AND g.auth_user_id = auth.uid()
)
)
WITH CHECK (
bucket_id = 'profili-giocatore'
AND EXISTS (
SELECT 1 FROM public.giocatori_squadra g
WHERE g.id = (storage.foldername(name))[1] AND g.auth_user_id = auth.uid()
)
);
CREATE POLICY "Gli admin scaricano tutti i file dei profili" ON storage.objects
FOR SELECT TO authenticated
USING (
bucket_id = 'profili-giocatore'
AND public.has_role(auth.uid(), 'admin'::public.app_role)
);
-- ── 11. Bucket: resta privato; limiti MIME/dimensione invariati ─────────────
UPDATE storage.buckets
SET public = false
WHERE id = 'profili-giocatore';
+1 -20
View File
@@ -1,14 +1,6 @@
/** Check dei dati di base della rosa: `bun test/unit/crapp-data.test.ts`. */
import assert from "node:assert/strict";
import {
adminNomi,
classifica,
formatData,
giocatori,
isAdmin,
statoMeta,
storicoMatch,
} from "@/lib/crapp-data";
import { classifica, formatData, giocatori, statoMeta, storicoMatch } from "@/lib/crapp-data";
// --- rosa --------------------------------------------------------------------
assert.ok(giocatori.length > 0, "la rosa non è vuota");
@@ -32,17 +24,6 @@ for (const g of giocatori) {
assert.ok(g.presenze <= g.totaliEventi, `${g.id}: presenze mai oltre gli eventi totali`);
}
// --- amministratori ----------------------------------------------------------
for (const nome of adminNomi) {
const admin = giocatori.find((g) => g.nome === nome);
assert.ok(admin, `l'amministratore ${nome} è in rosa`);
assert.equal(isAdmin(admin.id), true);
}
const nonAdmin = giocatori.find((g) => !adminNomi.includes(g.nome))!;
assert.equal(isAdmin(nonAdmin.id), false);
assert.equal(isAdmin("g999"), false, "un id inesistente non è amministratore");
assert.equal(isAdmin(""), false);
// --- formatData --------------------------------------------------------------
assert.equal(formatData("2026-09-01"), "mar 01 settembre");
assert.equal(formatData("2026-01-31"), "sab 31 gennaio");
-24
View File
@@ -1,24 +0,0 @@
/** Check dei permessi di amministrazione: `bun test/unit/ruoli.test.ts`. */
import assert from "node:assert/strict";
import { risolviAdmin } from "@/lib/ruoli";
import { adminNomi, giocatori } from "@/lib/crapp-data";
const referente = giocatori.find((g) => adminNomi.includes(g.nome))!;
const chiunque = giocatori.find((g) => !adminNomi.includes(g.nome))!;
// Con una sessione attiva decide il database, e basta: la lista di nomi non conta più.
assert.equal(risolviAdmin(true, chiunque.id), true, "il ruolo nel database concede");
assert.equal(
risolviAdmin(false, referente.id),
false,
"il ruolo nel database nega anche a chi è nella lista dei nomi",
);
assert.equal(risolviAdmin(true, null), true);
// Senza sessione (null) resta il ponte temporaneo sulla lista di nomi.
assert.equal(risolviAdmin(null, referente.id), true);
assert.equal(risolviAdmin(null, chiunque.id), false);
assert.equal(risolviAdmin(null, null), false, "nessuna identità, nessun permesso");
assert.equal(risolviAdmin(null, "gXX"), false, "un id sconosciuto non concede nulla");
console.log("ruoli: ok");