From c1744d4087357175641596c1a906c2ddaac40a0f Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Fri, 11 Sep 2026 14:58:54 +0200 Subject: [PATCH] Sposta avatar e documenti profilo su PocketBase (file field) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/components/crapp/Avatar.tsx | 10 +- .../crapp/ProfiloAmministrativo.tsx | 20 +- src/lib/avatar-store.ts | 79 ++++---- src/lib/profili-core.ts | 79 +------- src/lib/profili.ts | 173 ++++++++++++------ src/routes/admin.tsx | 12 +- src/routes/profilo.tsx | 6 +- test/integration/schema-profili.test.ts | 7 +- test/unit/avatar-store.test.ts | 27 ++- test/unit/profili-core.test.ts | 14 +- 10 files changed, 229 insertions(+), 198 deletions(-) diff --git a/src/components/crapp/Avatar.tsx b/src/components/crapp/Avatar.tsx index 1ab7039..c29ffba 100644 --- a/src/components/crapp/Avatar.tsx +++ b/src/components/crapp/Avatar.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { cn } from "@/lib/utils"; -import { urlAvatar } from "@/lib/avatar-store"; +import { urlAvatarDaRiga, useAvatarMap } from "@/lib/avatar-store"; export function Avatar({ id, @@ -19,8 +19,12 @@ export function Avatar({ const [errore, setErrore] = useState(false); useEffect(() => setErrore(false), [id, bust]); - if (!errore) { - const src = bust ? `${urlAvatar(id)}?v=${bust}` : urlAvatar(id); + const { data: mappa } = useAvatarMap(); + const riga = mappa?.[id]; + + if (riga && !errore) { + const base = urlAvatarDaRiga(riga); + const src = bust ? `${base}?v=${bust}` : base; return ( Promise; + onCaricato: (risultato: { id: string; filename: string }) => Promise; }) { const input = useRef(null); const [inCorso, setInCorso] = useState(false); @@ -53,7 +55,7 @@ function CampoFile({ if (!file) return; setInCorso(true); try { - const nuovo = await caricaFile(giocatoreId, sezione, file, path); + const nuovo = await caricaFile(giocatoreId, sezione, file); await onCaricato(nuovo); toast.success(`${label} caricato`); } catch (errore) { @@ -77,10 +79,10 @@ function CampoFile({ {label} - {path ? ( + {path && recordId ? (