diff --git a/.gitignore b/.gitignore index 7df29fe..5613562 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,11 @@ dist-ssr # Optional .vercel # Supabase CLI local state -supabase/.temp/ \ No newline at end of file +supabase/.temp/ + +# Backup locali del database (contengono dati reali degli utenti): +# ignora tutto il contenuto generato, ma tieni in git lo script e il README. +backups/* +!backups/backup.sh +!backups/download-storage.mjs +!backups/README.md \ No newline at end of file diff --git a/backups/README.md b/backups/README.md new file mode 100644 index 0000000..312851b --- /dev/null +++ b/backups/README.md @@ -0,0 +1,99 @@ +# Backup di disaster recovery + +Script per scaricare su questo host un backup completo del progetto Supabase: +schema DB, dati, utenti auth e file storage. Serve per poter ripartire in fretta +se il progetto Supabase (o Vercel) venisse perso. + +## Preparazione, passo per passo + +Da fare una volta sola per host (poi il backup si rilancia semplicemente con +`./backups/backup.sh`). + +1. **Installa/aggiorna la CLI Supabase** (via `npx`, non serve installarla a parte): + ```bash + npx supabase --version + ``` +2. **Accedi con il tuo account Supabase**: + ```bash + npx supabase login + ``` + Si apre il browser per l'OAuth; il token resta salvato in locale. +3. **Collega il repo al progetto Supabase** (il `project_id` è già in + `supabase/config.toml`, quindi in genere basta): + ```bash + npx supabase link --project-ref + ``` + Verifica che sia andato a buon fine con `npx supabase projects list` + (il progetto deve comparire con `"linked":true`). +4. **Crea/controlla il file `.env`** nella root del repo (accanto a + `package.json`, non dentro `backups/`) con almeno queste due chiavi: + ``` + VITE_SUPABASE_URL="https://.supabase.co" + SUPABASE_SERVICE_ROLE_KEY="" + ``` + Le trovi su [supabase.com/dashboard](https://supabase.com/dashboard) → progetto → + Project Settings → API. La **service role key** è quella con permessi pieni + (bypassa le RLS): serve solo per scaricare i file dai bucket, non va mai + esposta lato client né committata. +5. **(Solo se userai `--encrypt`) Verifica che gpg sia installato**: + ```bash + gpg --version + ``` + Su Debian/Ubuntu: `sudo apt install gnupg` se manca. + +A questo punto sei pronto per lanciare il backup vero e proprio (vedi sotto). + +## Uso + +```bash +./backups/backup.sh # crea backups/YYYYMMDD_HHMM/ con tutto dentro +./backups/backup.sh --encrypt # come sopra, poi comprime e cifra in un unico + # backups/YYYYMMDD_HHMM.tar.gz.gpg (chiede una + # passphrase) e cancella la cartella in chiaro +``` + +Ogni esecuzione crea una cartella nuova con timestamp: non sovrascrive backup +precedenti. Non c'è pulizia automatica dei vecchi backup — cancellali a mano +quando non servono più. + +Il backup contiene: + +| File/cartella | Contenuto | +| ------------------- | ---------------------------------------------------------------------------------------------------------- | +| `schema_public.sql` | schema delle tabelle applicative (tabelle, funzioni, RLS) | +| `data_public.sql` | dati delle tabelle applicative | +| `schema_auth.sql` | schema utenti Supabase Auth | +| `data_auth.sql` | utenti reali, incluso l'hash della password | +| `storage/` | tutti i file dei bucket (`avatar-giocatori`, `profili-giocatore`: foto, documenti d'identità, certificati) | + +## Ripristino + +1. Nuovo progetto Supabase (o `supabase start` in locale via Docker). +2. Importa nell'ordine: `schema_public.sql` → `schema_auth.sql` → + `data_auth.sql` → `data_public.sql` (gli utenti auth prima dei dati + pubblici, perché `giocatori.auth_user_id` referenzia `auth.users.id`). +3. Ricrea i bucket `avatar-giocatori` e `profili-giocatore` e ricarica il + contenuto di `storage/`. +4. Reimposta le variabili d'ambiente su Vercel/locale (non incluse in questo + backup — vedi sotto). +5. Redeploy dal repo git (il codice non serve backupparlo qui, è già su + GitHub e Gitea). + +Per un archivio cifrato: `gpg -d backups/AAAAMMGG_hhmm.tar.gz.gpg | tar xz`. + +## Cosa NON è incluso + +- **Variabili d'ambiente Vercel**: richiede `vercel login` interattivo, non + automatizzabile da questo script. Esportale a mano con `vercel env pull`. +- **Codice applicativo**: vive nel repo git, non nel database. + +## Sicurezza + +Questi backup contengono password hash, documenti d'identità e altri dati +personali reali. Per questo: + +- la cartella `backups/` è in `.gitignore` (tranne questo README e gli script: + quelli sì restano in git) — **non committare mai i dati generati**; +- se sposti un backup fuori da questo host, usa sempre `--encrypt` o cifra tu + l'archivio a mano; +- cancella i backup in chiaro quando non ti servono più. diff --git a/backups/backup.sh b/backups/backup.sh new file mode 100755 index 0000000..28dc98d --- /dev/null +++ b/backups/backup.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Backup completo di disaster recovery: schema + dati DB, utenti auth, file storage. +# Uso: +# ./backups/backup.sh crea backups/YYYYMMDD_HHMM/ con dentro tutto +# ./backups/backup.sh --encrypt come sopra, poi comprime e cifra con gpg (chiede una passphrase) +# +# Richiede: essere già collegati al progetto Supabase (supabase login + link, già fatto +# in questo repo) e avere .env con VITE_SUPABASE_URL e SUPABASE_SERVICE_ROLE_KEY. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +STAMP="$(date +%Y%m%d_%H%M)" +RUN_DIR="$SCRIPT_DIR/$STAMP" + +mkdir -p "$RUN_DIR" +echo "==> Backup in $RUN_DIR" + +echo "==> Dump schema pubblico" +npx supabase db dump --linked -f "$RUN_DIR/schema_public.sql" + +echo "==> Dump dati pubblici" +npx supabase db dump --linked --data-only -f "$RUN_DIR/data_public.sql" + +echo "==> Dump schema auth" +npx supabase db dump --linked --schema auth -f "$RUN_DIR/schema_auth.sql" + +echo "==> Dump dati auth (utenti)" +npx supabase db dump --linked --schema auth --data-only -f "$RUN_DIR/data_auth.sql" + +echo "==> Download file storage (bucket)" +node "$SCRIPT_DIR/download-storage.mjs" "$RUN_DIR/storage" + +echo "==> Backup completato: $RUN_DIR" + +if [[ "${1:-}" == "--encrypt" ]]; then + ARCHIVE="$RUN_DIR.tar.gz.gpg" + echo "==> Comprimo e cifro in $ARCHIVE (verrà chiesta una passphrase)" + tar -C "$SCRIPT_DIR" -czf - "$STAMP" | gpg --symmetric --cipher-algo AES256 -o "$ARCHIVE" + rm -rf "$RUN_DIR" + echo "==> Archivio cifrato pronto: $ARCHIVE" + echo " Per estrarlo: gpg -d $ARCHIVE | tar xz" +fi diff --git a/backups/download-storage.mjs b/backups/download-storage.mjs new file mode 100644 index 0000000..0aa271b --- /dev/null +++ b/backups/download-storage.mjs @@ -0,0 +1,49 @@ +// Scarica tutti i file dei bucket Supabase Storage per il backup di disaster recovery. +// Uso: node backups/download-storage.mjs [cartella-output] +import { createClient } from "@supabase/supabase-js"; +import { mkdir, writeFile, readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; + +for (const line of (await readFile(new URL("../.env", import.meta.url), "utf8")).split("\n")) { + const match = /^([\w.]+)=(.*)$/.exec(line.trim()); + if (match) process.env[match[1]] ??= match[2].replace(/^["']|["']$/g, ""); +} + +const BUCKETS = ["avatar-giocatori", "profili-giocatore"]; +const OUT_DIR = process.argv[2] ?? new URL("./storage/", import.meta.url).pathname; + +const supabase = createClient(process.env.VITE_SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY); + +async function listAll(bucket, prefix = "") { + const { data, error } = await supabase.storage.from(bucket).list(prefix, { limit: 1000 }); + if (error) throw error; + let files = []; + for (const entry of data) { + const path = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.id === null) { + // è una "cartella" (nessun id): ricorri + files = files.concat(await listAll(bucket, path)); + } else { + files.push(path); + } + } + return files; +} + +for (const bucket of BUCKETS) { + console.log(`Bucket: ${bucket}`); + const files = await listAll(bucket); + console.log(` ${files.length} file trovati`); + for (const path of files) { + const { data, error } = await supabase.storage.from(bucket).download(path); + if (error) { + console.error(` ERRORE ${path}:`, error.message); + continue; + } + const destPath = join(OUT_DIR, bucket, path); + await mkdir(dirname(destPath), { recursive: true }); + await writeFile(destPath, Buffer.from(await data.arrayBuffer())); + console.log(` scaricato: ${bucket}/${path}`); + } +} +console.log("Fatto."); diff --git a/docs/PORTABILITA.md b/docs/PORTABILITA.md index c696f7a..9b89f3f 100644 --- a/docs/PORTABILITA.md +++ b/docs/PORTABILITA.md @@ -27,6 +27,15 @@ senza dipendere da servizi esclusivi di Lovable Cloud. 5. Job pianificati richiamabili via semplice HTTP POST (es. `/api/public/promemoria-palloni`), così funzionano con cron di sistema, pg_cron o qualsiasi scheduler. +## Backup e disaster recovery + +Lo script [`backups/backup.sh`](../backups/backup.sh) scarica su un host locale schema e +dati del database (`public` e `auth`), utenti Supabase Auth e file dei bucket storage, in +modo da poter ripartire in fretta se il progetto Supabase (o Vercel) venisse perso. +Uso, contenuto del backup e procedura di ripristino sono descritti in +[`backups/README.md`](../backups/README.md). Non ancora automatizzato: l'export delle +variabili d'ambiente Vercel (richiede login interattivo). + ## Migrazione su server proprio (sintesi) 1. `bun run build` (o `npm run build`) → avvio del server Node generato. diff --git a/docs/README.md b/docs/README.md index e1bb1c8..ab11f09 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,16 +6,16 @@ ancora e va **prima documentata** (vedi [DD-002](DESIGN_DECISIONS.md#dd-002--svi ## Dove sta cosa -| Documento | Risponde a | -| ------------------------------------------ | ---------------------------------------------------------------------------- | -| [ROADMAP.md](ROADMAP.md) | Cosa è fatto, cosa è previsto, cosa resta un'idea | -| [ARCHITECTURE.md](ARCHITECTURE.md) | Com'è fatta l'app: stack, struttura del codice, flusso di sviluppo | -| [DATABASE.md](DATABASE.md) | Quali tabelle esistono, a cosa servono, chi le usa | -| [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) | Perché abbiamo scelto così, cosa abbiamo escluso e quando riaprire la scelta | -| [PORTABILITA.md](PORTABILITA.md) | Cosa lega l'app a un fornitore e cosa no, come spostarla su server proprio | -| [EFFICIENZA_CLOUD.md](EFFICIENZA_CLOUD.md) | Come tenere basso il consumo cloud: cache, query, push | -| [CHANGELOG.md](CHANGELOG.md) | Cosa è cambiato e quando | -| [modules/](modules/) | Specifica funzionale di ogni modulo, una per file | +| Documento | Risponde a | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | +| [ROADMAP.md](ROADMAP.md) | Cosa è fatto, cosa è previsto, cosa resta un'idea | +| [ARCHITECTURE.md](ARCHITECTURE.md) | Com'è fatta l'app: stack, struttura del codice, flusso di sviluppo | +| [DATABASE.md](DATABASE.md) | Quali tabelle esistono, a cosa servono, chi le usa | +| [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) | Perché abbiamo scelto così, cosa abbiamo escluso e quando riaprire la scelta | +| [PORTABILITA.md](PORTABILITA.md) | Cosa lega l'app a un fornitore e cosa no, come spostarla su server proprio (backup/disaster recovery incluso) | +| [EFFICIENZA_CLOUD.md](EFFICIENZA_CLOUD.md) | Come tenere basso il consumo cloud: cache, query, push | +| [CHANGELOG.md](CHANGELOG.md) | Cosa è cambiato e quando | +| [modules/](modules/) | Specifica funzionale di ogni modulo, una per file | Le regole vincolanti per gli assistenti AI stanno in [AGENTS.md](../AGENTS.md); lo stato corrente del lavoro in [PROJECT_STATE.md](../PROJECT_STATE.md).