Compare commits
5
Commits
990c2bbd4c
...
1036eb860d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1036eb860d | ||
|
|
f325c0485c | ||
|
|
5a6aaad885 | ||
|
|
e4f963d170 | ||
|
|
f424b3a9f7 |
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
@@ -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,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");
|
||||
|
||||
@@ -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");
|
||||
Reference in New Issue
Block a user