Sposta avatar e documenti profilo su PocketBase (file field)
Sostituisce i due bucket Supabase Storage con campi file PocketBase:
- avatar-giocatori: nuova collection avatar_giocatori separata da
giocatori_squadra, perché la policy originale ("chiunque autenticato
carica/sostituisce/elimina qualsiasi avatar, nessun controllo
proprietario") è più permissiva delle regole di giocatori_squadra —
mescolarle avrebbe indebolito le une o bloccato l'altra. File pubblico,
come il bucket originale;
- profili-giocatore: campi file su profili_giocatore stesso, con
protected: true (vedi sotto).
Trovato e corretto un problema di sicurezza reale: PocketBase rende i
file pubblici di default (si affida solo alla casualità del nome file),
a meno di impostare esplicitamente protected: true sul campo — i
documenti d'identità e i certificati medici sarebbero stati raggiungibili
senza autenticazione, in violazione di DD-016 regola 4. Verificato che
ora servono un token valido (404 senza, 200 con).
La scrittura dei campi testuali del profilo e il caricamento dei file
sono due operazioni separate (PocketBase rifiuta una stringa dove si
aspetta un file): verificato che caricare un documento non cancella i
dati testuali già salvati.
Rimossi da profili-core.ts RigaProfilo/daRigaProfilo/aRigaProfilo
(shape Postgres non più usata da nessun modulo di produzione) e
rimuoviFile (mai chiamato da nessun componente). Avatar.tsx non usa più
un URL deterministico per giocatore: PocketBase genera nomi file
casuali, quindi legge la mappa avatar tramite una query React Query
condivisa tra tutte le istanze del componente.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -338,19 +338,21 @@ function Documento({
|
||||
label,
|
||||
stato,
|
||||
path,
|
||||
recordId,
|
||||
}: {
|
||||
icona: React.ReactNode;
|
||||
label: string;
|
||||
stato: StatoScadenza | "presente" | "assente";
|
||||
path: string | null;
|
||||
recordId: string | null;
|
||||
}) {
|
||||
const [inCorso, setInCorso] = useState(false);
|
||||
|
||||
async function scarica() {
|
||||
if (!path || inCorso) return;
|
||||
if (!path || !recordId || inCorso) return;
|
||||
setInCorso(true);
|
||||
try {
|
||||
await scaricaFile(path);
|
||||
await scaricaFile(recordId, path);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "Download non riuscito");
|
||||
} finally {
|
||||
@@ -362,7 +364,7 @@ function Documento({
|
||||
<button
|
||||
type="button"
|
||||
onClick={scarica}
|
||||
disabled={!path || inCorso}
|
||||
disabled={!path || !recordId || inCorso}
|
||||
className={cn(
|
||||
"premi flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-bold uppercase disabled:opacity-60",
|
||||
statoClasse[stato],
|
||||
@@ -431,24 +433,28 @@ function SchedaGiocatore({
|
||||
label="Doc fronte"
|
||||
stato={fronte}
|
||||
path={profilo?.documentoFrontePath ?? null}
|
||||
recordId={profilo?.id ?? null}
|
||||
/>
|
||||
<Documento
|
||||
icona={<IdCard className="h-3.5 w-3.5" />}
|
||||
label="Doc retro"
|
||||
stato={retro}
|
||||
path={profilo?.documentoRetroPath ?? null}
|
||||
recordId={profilo?.id ?? null}
|
||||
/>
|
||||
<Documento
|
||||
icona={<FileText className="h-3.5 w-3.5" />}
|
||||
label="Certificato"
|
||||
stato={certificato}
|
||||
path={profilo?.certificatoPath ?? null}
|
||||
recordId={profilo?.id ?? null}
|
||||
/>
|
||||
<Documento
|
||||
icona={<Image className="h-3.5 w-3.5" />}
|
||||
label="Foto"
|
||||
stato={sezioni.foto ? "presente" : "assente"}
|
||||
path={profilo?.fotoPath ?? null}
|
||||
recordId={profilo?.id ?? null}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
|
||||
Reference in New Issue
Block a user