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:
2026-09-11 14:58:54 +02:00
co-authored by Claude Sonnet 5
parent d125bdfb6a
commit c1744d4087
10 changed files with 229 additions and 198 deletions
+19 -8
View File
@@ -1,16 +1,27 @@
/**
* Check dell'avatar giocatore: `bun test/unit/avatar-store.test.ts`.
* `urlAvatar` è puro (il bucket è pubblico, nessuna richiesta di rete): qui
* verifichiamo solo la forma dell'URL, non serve un database.
* `urlAvatarDaRiga` è puro (il campo non è protetto, nessuna richiesta di rete): qui
* verifichiamo solo la forma dell'URL a partire da una riga finta, non serve un database.
*/
import assert from "node:assert/strict";
import { urlAvatar } from "@/lib/avatar-store";
import { urlAvatarDaRiga } from "@/lib/avatar-store";
const url = urlAvatar("g1");
const riga = { id: "rec1", giocatore: "g1", foto: "avatar_abc123.jpg" };
assert.ok(url.includes("avatar-giocatori"), "punta al bucket degli avatar");
assert.ok(url.includes("g1/avatar.jpg"), "il percorso è <id>/avatar.jpg");
assert.equal(urlAvatar("g1"), url, "deterministico: nessuna chiamata di rete coinvolta");
assert.notEqual(urlAvatar("g2"), url, "id diversi -> percorsi diversi");
const url = urlAvatarDaRiga(riga);
assert.ok(url.includes("avatar_giocatori"), "punta alla collection degli avatar");
assert.ok(url.includes("rec1"), "contiene l'id del record");
assert.ok(url.includes("avatar_abc123.jpg"), "contiene il nome del file");
assert.equal(
urlAvatarDaRiga(riga),
url,
"deterministico a parità di riga: nessuna chiamata di rete",
);
assert.notEqual(
urlAvatarDaRiga({ ...riga, foto: "altro.jpg" }),
url,
"file diverso -> URL diverso",
);
console.log("avatar-store: ok");
+1 -13
View File
@@ -1,10 +1,8 @@
/** Check dei profili giocatore: `bun test/unit/profili-core.test.ts`. */
import assert from "node:assert/strict";
import {
aRigaProfilo,
completamento,
csvTesseramento,
daRigaProfilo,
sezioniComplete,
statoScadenza,
type Profilo,
@@ -19,6 +17,7 @@ import {
import { dividiNome } from "@/lib/crapp-data";
const vuoto: Profilo = {
id: null,
giocatoreId: "g1",
dataNascita: null,
luogoNascita: null,
@@ -77,17 +76,6 @@ assert.equal(
);
assert.equal(sezioniComplete({ ...completo, documentoFrontePath: null }).documento, false);
// --- aRigaProfilo ------------------------------------------------------------
const riga = aRigaProfilo({ ...completo, luogoNascita: " ", telefono: " 333 " });
assert.equal(riga.luogo_nascita, null, "i campi solo-spazi tornano NULL, non stringa vuota");
assert.equal(riga.telefono, "333", "il resto viene ripulito ai bordi");
assert.equal(riga.documento_fronte_path, "g1/documento-fronte.jpg");
assert.deepEqual(
daRigaProfilo(aRigaProfilo(completo)),
completo,
"modello -> riga -> modello non perde niente",
);
// --- statoScadenza -----------------------------------------------------------
const oggi = "2026-08-30";
assert.equal(statoScadenza(null, null, oggi), "mancante");