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>
This commit is contained in:
2026-08-31 18:21:34 +02:00
co-authored by Claude Opus 5
parent f325c0485c
commit 1036eb860d
13 changed files with 78 additions and 143 deletions
+10 -10
View File
@@ -55,19 +55,19 @@ admin) implementati su `develop`, da attivare in produzione seguendo i passaggi
## Autenticazione e dashboard amministratore ## Autenticazione e dashboard amministratore
Implementate su `develop`, **non ancora attive in produzione**. Il codice è additivo: finché Implementate su `develop`. **Il login è l'unica via d'accesso** (31/08/2026): la selezione
i passaggi qui sotto non sono fatti, l'app si comporta esattamente come prima. 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 **Attenzione all'ordine:** finché il provider Google è spento in Supabase, «Accedi con
ancora. Con `npm run dev`, «Accedi con Google» risponde: Google» risponde
``` ```
{"code":400,"error_code":"validation_failed","msg":"Unsupported provider: provider is not enabled"} {"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. e **nessuno entra nell'app**, né in dev né sulla preview di `develop`. Il passo 1 qui sotto
È il passo 1 qui sotto, ancora da fare. Tutto il resto dell'app in dev funziona normalmente, va fatto prima di mandare questa versione in produzione.
perché l'accesso avviene ancora scegliendo il proprio nome.
Passaggi in ordine, nessuno dei quali è reversibile a metà: 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>';` `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 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. 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 5. **Solo a squadra collegata**: migration `m4_solo_autenticati`, che toglie al ruolo `anon`
selezione libera del giocatore), poi la migration che rimuove le policy `anon` dalle l'accesso alle tabelle v1.0. Da lì in poi i dati sono raggiungibili solo con una sessione;
tabelle v1.0. È l'unico passo che cambia il comportamento per tutti. 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 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. 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 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 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/`. `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 - **Autenticazione**: login Google via Supabase Auth (`src/lib/auth.ts`, DD-011). È l'unica
di amministrazione arrivano da `user_roles` (`src/lib/ruoli.ts`). La variabile strada di accesso: `__root.tsx` rimanda a `/benvenuto` chi non ha sessione, e l'identità
`VITE_AUTH_OBBLIGATORIA` decide se `/benvenuto` accetta ancora la selezione libera del del giocatore è lo slot di `giocatori_squadra` collegato all'account. I permessi di
giocatore: finché è spenta, login e vecchio accesso convivono. amministrazione arrivano solo da `user_roles` (`src/lib/ruoli.ts`).
## Livello dati ## 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 - 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 un giocatore di `giocatori_squadra`, e il collegamento non è più modificabile dal
giocatore stesso (DD-016 regola 2). giocatore stesso (DD-016 regola 2).
- I permessi di amministrazione arrivano da `user_roles` (`src/lib/ruoli.ts`) e non più - La selezione libera del giocatore è stata rimossa: `/benvenuto` offre solo l'accesso con
dalla lista di nomi in `crapp-data.ts`, che resta solo come ponte finché Google e senza sessione non si entra in nessuna schermata. Sparita anche la variabile
`VITE_AUTH_OBBLIGATORIA` non viene acceso. `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 - Profilo giocatore: da `/profilo` ognuno compila i propri dati anagrafici e carica
documento, certificato medico e foto tessera con le relative scadenze documento, certificato medico e foto tessera con le relative scadenze
([modules/profilo-giocatore.md](modules/profilo-giocatore.md)). ([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 ## In corso
- Documentazione tecnica del progetto. - Documentazione tecnica del progetto.
- Autenticazione Google e dashboard amministratore: implementate su `develop`, ma in dev il - Autenticazione Google e dashboard amministratore: il codice è completo su `develop` e il
login risponde ancora `provider is not enabled` perché il provider Google non è stato login è ora l'unica via d'accesso. Restano i passaggi di configurazione, in quest'ordine:
acceso in Supabase. Restano da provider Google in Supabase (senza, nessuno entra), migration M2/M3, primo admin in
fare, in quest'ordine: configurazione del provider Google in Supabase, applicazione delle `user_roles`, collegamento dei 17 account, e infine la migration M4 che chiude gli accessi
migration M2/M3, inserimento del primo admin in `user_roles`, collegamento dei 17 account, `anon`. Stato di dettaglio in [PROJECT_STATE.md](../PROJECT_STATE.md).
e solo alla fine `VITE_AUTH_OBBLIGATORIA=true` + rimozione delle policy `anon`.
Stato di dettaglio in [PROJECT_STATE.md](../PROJECT_STATE.md).
## Prossimo ## Prossimo
+3 -8
View File
@@ -3,15 +3,10 @@ import type { Session } from "@supabase/supabase-js";
import { supabase } from "@/integrations/supabase/client"; import { supabase } from "@/integrations/supabase/client";
/** /**
* Autenticazione reale con Google (DD-011). Il login non ha ancora sostituito la * Autenticazione reale con Google (DD-011). Il login ha sostituito la selezione del
* selezione del giocatore: finché `VITE_AUTH_OBBLIGATORIA` non è `true`, `/benvenuto` * giocatore: senza sessione non si entra, e i permessi di amministrazione arrivano solo
* offre entrambe le strade, così la produzione continua a funzionare mentre la squadra * da `user_roles` (vedi `ruoli.ts`).
* collega gli account.
*/ */
export function authObbligatoria(): boolean {
return import.meta.env["VITE_AUTH_OBBLIGATORIA"] === "true";
}
export function useSessione() { export function useSessione() {
const [sessione, setSessione] = useState<Session | null>(null); const [sessione, setSessione] = useState<Session | null>(null);
const [pronta, setPronta] = useState(false); 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" }); return d.toLocaleDateString("it-IT", { weekday: "short", day: "2-digit", month: "long" });
} }
/** Referenti che possono gestire eventi e sollecitare le risposte. */ /* I permessi di amministrazione stanno in `user_roles` (DD-011), non in una lista di nomi:
export const adminNomi = ["Ivan Cacciari", "Iacopo Ricci", "Cristina Titone"]; vedi `src/lib/ruoli.ts`. */
export function isAdmin(giocatoreId: string) {
const g = giocatori.find((x) => x.id === giocatoreId);
return Boolean(g && adminNomi.includes(g.nome));
}
+3 -13
View File
@@ -1,22 +1,13 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client"; import { supabase } from "@/integrations/supabase/client";
import { isAdmin as nomeInListaAdmin } from "./crapp-data";
import { useSessione } from "./auth"; import { useSessione } from "./auth";
import { useGiocatoreBase } from "./user-store";
export const RUOLI_KEY = ["ruolo-admin"] as const; export const RUOLI_KEY = ["ruolo-admin"] as const;
/** /**
* Permessi di amministrazione. La fonte è `user_roles` nel database (DD-011): la lista di * Permessi di amministrazione: unica fonte è `user_roles` nel database (DD-011).
* nomi in `crapp-data.ts` resta solo come ponte per chi non ha ancora collegato l'account, * Nessuna lista di nomi, altrimenti basterebbe scegliere il nome giusto per amministrare.
* e sparisce quando `VITE_AUTH_OBBLIGATORIA` viene acceso in produzione.
*
* ponytail: doppia fonte temporanea, si riduce a `ruoloDb` appena l'auth è obbligatoria.
*/ */
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. */ /** `null` = nessuna sessione, quindi il database non ha una risposta da dare. */
async function fetchRuoloAdmin(utenteId: string | null): Promise<boolean | null> { 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 { export function useIsAdmin(): boolean {
const { utenteId } = useSessione(); const { utenteId } = useSessione();
const io = useGiocatoreBase();
// Il ruolo cambia solo quando un admin lo assegna: una lettura per sessione basta. // Il ruolo cambia solo quando un admin lo assegna: una lettura per sessione basta.
const query = useQuery({ const query = useQuery({
queryKey: [...RUOLI_KEY, utenteId], queryKey: [...RUOLI_KEY, utenteId],
queryFn: () => fetchRuoloAdmin(utenteId), queryFn: () => fetchRuoloAdmin(utenteId),
staleTime: 30 * 60_000, 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 { Toaster } from "../components/ui/sonner";
import { TeamLogo } from "../components/crapp/ui-bits"; import { TeamLogo } from "../components/crapp/ui-bits";
import { useGiocatoreBase } from "../lib/user-store"; import { useGiocatoreBase } from "../lib/user-store";
import { useSessione } from "../lib/auth";
function NotFoundComponent() { function NotFoundComponent() {
return ( return (
@@ -145,17 +146,21 @@ function RootComponent() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const giocatore = useGiocatoreBase(); const giocatore = useGiocatoreBase();
const { pronta, utenteId } = useSessione();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const isBenvenuto = location.pathname === "/benvenuto"; const isBenvenuto = location.pathname === "/benvenuto";
// Senza sessione Google non si entra: l'identità la 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(() => { useEffect(() => {
setMounted(true); setMounted(true);
if (!giocatore && !isBenvenuto) { if (pronta && (!giocatore || !utenteId) && !isBenvenuto) {
navigate({ to: "/benvenuto" }); navigate({ to: "/benvenuto" });
} }
}, [giocatore, isBenvenuto, navigate]); }, [giocatore, utenteId, pronta, isBenvenuto, navigate]);
if (!mounted) { if (!mounted || !pronta) {
return ( return (
<div className="grid min-h-screen place-items-center bg-background"> <div className="grid min-h-screen place-items-center bg-background">
<TeamLogo className="h-16 w-16 animate-pulse" /> <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 { LogIn } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { TeamLogo } from "@/components/crapp/ui-bits"; import { TeamLogo } from "@/components/crapp/ui-bits";
import { giocatori } from "@/lib/crapp-data"; import { accediConGoogle, useSessione } from "@/lib/auth";
import { accediConGoogle, authObbligatoria, useSessione } from "@/lib/auth";
import { import {
nomeCompleto, nomeCompleto,
slotDi, slotDi,
@@ -13,7 +12,7 @@ import {
useGiocatoriSquadra, useGiocatoriSquadra,
type GiocatoreSquadra, type GiocatoreSquadra,
} from "@/lib/giocatori-squadra"; } from "@/lib/giocatori-squadra";
import { impostaGiocatore, useGiocatoreCorrente } from "@/lib/user-store"; import { impostaGiocatore, resetGiocatore, useGiocatoreCorrente } from "@/lib/user-store";
export const Route = createFileRoute("/benvenuto")({ export const Route = createFileRoute("/benvenuto")({
head: () => ({ head: () => ({
@@ -65,23 +64,24 @@ function Benvenuto() {
const navigate = useNavigate(); const navigate = useNavigate();
const giocatore = useGiocatoreCorrente(); const giocatore = useGiocatoreCorrente();
const { pronta, utenteId } = useSessione(); const { pronta, utenteId } = useSessione();
const { righe } = useGiocatoriSquadra(); const { righe, daDatabase } = useGiocatoriSquadra();
const collega = useCollegaGiocatore(); const collega = useCollegaGiocatore();
const [inCorso, setInCorso] = useState(false); const [inCorso, setInCorso] = useState(false);
const mioSlot = slotDi(righe, utenteId); const mioSlot = slotDi(righe, utenteId);
// Con l'auth obbligatoria si entra solo da loggati; finché non lo è, la selezione // Si entra solo da loggati e con uno slot collegato (DD-011).
// diretta resta come ponte per chi non ha ancora collegato l'account (DD-011). const puoEntrare = !!giocatore && !!utenteId;
const puoEntrare = !!giocatore && (!!utenteId || !authObbligatoria());
useEffect(() => { useEffect(() => {
if (puoEntrare) navigate({ to: "/" }); if (puoEntrare) navigate({ to: "/" });
}, [puoEntrare, navigate]); }, [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(() => { useEffect(() => {
if (mioSlot) impostaGiocatore(mioSlot.id); if (mioSlot) impostaGiocatore(mioSlot.id);
}, [mioSlot]); else if (utenteId && daDatabase) resetGiocatore();
}, [mioSlot, utenteId, daDatabase]);
async function accedi() { async function accedi() {
setInCorso(true); setInCorso(true);
@@ -125,25 +125,6 @@ function Benvenuto() {
> >
<LogIn className="h-4 w-4" /> Accedi con Google <LogIn className="h-4 w-4" /> Accedi con Google
</button> </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 { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { toast } from "sonner"; 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 { cn } from "@/lib/utils";
import { PageHeader, Section, StatTile } from "@/components/crapp/ui-bits"; import { PageHeader, Section, StatTile } from "@/components/crapp/ui-bits";
import { Avatar } from "@/components/crapp/Avatar"; import { Avatar } from "@/components/crapp/Avatar";
@@ -19,7 +19,7 @@ import {
statoNotifiche, statoNotifiche,
} from "@/lib/push-client"; } from "@/lib/push-client";
import { resetGiocatore } from "@/lib/user-store"; import { resetGiocatore } from "@/lib/user-store";
import { esci, useSessione } from "@/lib/auth"; import { esci } from "@/lib/auth";
import { useIsAdmin } from "@/lib/ruoli"; import { useIsAdmin } from "@/lib/ruoli";
import { Reveal } from "@/components/motion/Reveal"; import { Reveal } from "@/components/motion/Reveal";
@@ -42,7 +42,6 @@ function Profilo() {
const votiSocial = useVotiSocial(); const votiSocial = useVotiSocial();
const g = useIo(); const g = useIo();
const admin = useIsAdmin(); const admin = useIsAdmin();
const { sessione } = useSessione();
const ultimoMese = usePresenzeUltimoMese(g?.id); const ultimoMese = usePresenzeUltimoMese(g?.id);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const foto = useAvatar(g?.id); const foto = useAvatar(g?.id);
@@ -230,22 +229,12 @@ function Profilo() {
) : null} ) : null}
<button <button
type="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" 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> <span className="min-w-0 truncate">Esci</span>
<Users className="h-4 w-4 text-muted-foreground" /> <LogOut className="h-4 w-4 text-muted-foreground" />
</button> </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> </div>
</Section> </Section>
</> </>
@@ -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;
+1 -20
View File
@@ -1,14 +1,6 @@
/** Check dei dati di base della rosa: `bun test/unit/crapp-data.test.ts`. */ /** Check dei dati di base della rosa: `bun test/unit/crapp-data.test.ts`. */
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { import { classifica, formatData, giocatori, statoMeta, storicoMatch } from "@/lib/crapp-data";
adminNomi,
classifica,
formatData,
giocatori,
isAdmin,
statoMeta,
storicoMatch,
} from "@/lib/crapp-data";
// --- rosa -------------------------------------------------------------------- // --- rosa --------------------------------------------------------------------
assert.ok(giocatori.length > 0, "la rosa non è vuota"); 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`); 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 -------------------------------------------------------------- // --- formatData --------------------------------------------------------------
assert.equal(formatData("2026-09-01"), "mar 01 settembre"); assert.equal(formatData("2026-09-01"), "mar 01 settembre");
assert.equal(formatData("2026-01-31"), "sab 31 gennaio"); 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");