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:
@@ -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,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");
|
||||
|
||||
Reference in New Issue
Block a user