931 lines
37 KiB
Markdown
931 lines
37 KiB
Markdown
# TerManager2
|
|
|
|
Applicazione web per la gestione dell'assegnazione e del rientro dei territori
|
|
(cartoline) di congregazione, con statistiche di percorrenza, campagne speciali
|
|
e registro storico completo.
|
|
|
|
I dati personali dei proclamatori sono **cifrati nel database**, e ogni
|
|
operazione è tracciata in un **audit log** consultabile.
|
|
|
|
---
|
|
|
|
## Indice
|
|
|
|
**Per iniziare**
|
|
- [Deploy su Dokploy](#deploy-su-dokploy) — la via consigliata per la produzione
|
|
- [Deploy su altri sistemi](#deploy-su-altri-sistemi) — Coolify, Portainer, server con Docker
|
|
- [Sviluppo locale](#sviluppo-locale) — compreso come lanciare i [test automatici](#test-automatici)
|
|
|
|
**Gestione quotidiana**
|
|
- [Variabili d'ambiente](#variabili-dambiente)
|
|
- [Impostazioni applicative](#impostazioni-applicative)
|
|
- [Aggiornamenti](#aggiornamenti)
|
|
- [Backup e ripristino](#backup-e-ripristino)
|
|
- [Comandi utili](#comandi-utili)
|
|
- [Troubleshooting](#troubleshooting)
|
|
|
|
**Riferimento**
|
|
- [Funzionalità](#funzionalità)
|
|
- [Ruoli e permessi](#ruoli-e-permessi)
|
|
- [Sicurezza e GDPR](#sicurezza-e-gdpr)
|
|
- [Modello dati](#modello-dati)
|
|
- [Regole di business](#regole-di-business)
|
|
- [Architettura](#architettura)
|
|
|
|
---
|
|
|
|
## Deploy su Dokploy
|
|
|
|
### Come funziona
|
|
|
|
L'immagine Docker è **autosufficiente**: dipendenze PHP e asset frontend sono
|
|
compilati durante il build, non all'avvio. Nginx gira nello stesso container di
|
|
php-fpm. Un deploy quindi è semplicemente: *build → migrazioni → online*.
|
|
|
|
Non serve il codice sorgente sul server, non ci sono bind mount, e non viene
|
|
eseguito né `composer install` né `npm build` all'avvio.
|
|
|
|
| Servizio | Ruolo | Esposto |
|
|
|----------------|------------------------------------------------|---------|
|
|
| `app` | nginx + php-fpm — l'applicazione | via Traefik |
|
|
| `queue-worker` | Job in coda (import PDF massivi) | no |
|
|
| `scheduler` | Task pianificati (pulizia audit log) | no |
|
|
| `mariadb` | Database | no |
|
|
| `redis` | Cache, sessioni, code | no |
|
|
|
|
Solo `app` è raggiungibile dall'esterno. Database e Redis restano su una rete
|
|
interna senza rotta verso Internet.
|
|
|
|
---
|
|
|
|
### Passo 1 — Crea l'applicazione
|
|
|
|
Su Dokploy: **Create Application** → tipo **Compose**.
|
|
|
|
| Campo | Valore |
|
|
|---------------|---------------------|
|
|
| Repository | il tuo repo Git |
|
|
| Branch | `main` |
|
|
| Compose Path | `docker-compose.yml`|
|
|
|
|
### Passo 2 — Genera le credenziali
|
|
|
|
Sul tuo computer, esegui questi comandi e tieni da parte l'output:
|
|
|
|
```bash
|
|
echo "APP_KEY=base64:$(openssl rand -base64 32)"
|
|
echo "DB_PASSWORD=$(openssl rand -base64 24)"
|
|
echo "DB_ROOT_PASSWORD=$(openssl rand -base64 24)"
|
|
echo "REDIS_PASSWORD=$(openssl rand -base64 24)"
|
|
```
|
|
|
|
> **Conserva `APP_KEY` in un password manager.** I dati personali dei
|
|
> proclamatori sono cifrati con questa chiave: se la perdi, non sono più
|
|
> leggibili nemmeno avendo il backup del database.
|
|
|
|
### Passo 3 — Imposta le variabili d'ambiente
|
|
|
|
Nella sezione **Environment** di Dokploy, incolla questo blocco e sostituisci i
|
|
valori:
|
|
|
|
```bash
|
|
# --- Applicazione ---
|
|
APP_NAME=TerManager2
|
|
APP_ENV=production
|
|
APP_DEBUG=false
|
|
APP_TIMEZONE=Europe/Rome
|
|
|
|
# Deve combaciare con l'indirizzo da cui accedi, porta inclusa.
|
|
# per IP:porta -> http://IP-DEL-SERVER:8080
|
|
# con dominio -> https://territori.tuodominio.it
|
|
APP_URL=http://IP-DEL-SERVER:8080
|
|
|
|
# --- Chiavi e password (dal Passo 2) ---
|
|
APP_KEY=base64:...
|
|
DB_PASSWORD=...
|
|
DB_ROOT_PASSWORD=...
|
|
REDIS_PASSWORD=...
|
|
|
|
# --- Database (mariadb e' il nome del servizio, non cambiarlo) ---
|
|
DB_CONNECTION=mysql
|
|
DB_HOST=mariadb
|
|
DB_PORT=3306
|
|
DB_DATABASE=termanager2
|
|
DB_USERNAME=termanager2
|
|
|
|
# --- Redis (idem: redis e' il nome del servizio) ---
|
|
REDIS_HOST=redis
|
|
REDIS_PORT=6379
|
|
CACHE_STORE=redis
|
|
SESSION_DRIVER=redis
|
|
QUEUE_CONNECTION=redis
|
|
|
|
# --- Primo amministratore (creato al primo avvio) ---
|
|
INITIAL_ADMIN_NAME=Nome Cognome
|
|
INITIAL_ADMIN_EMAIL=admin@tuodominio.it
|
|
INITIAL_ADMIN_PASSWORD=UnaPasswordSolida
|
|
|
|
# --- Porte esposte sull'host ---
|
|
APP_BIND=0.0.0.0 # 127.0.0.1 se ci metti davanti un reverse proxy
|
|
APP_PORT=8080
|
|
DB_BIND=127.0.0.1 # database raggiungibile solo via tunnel SSH
|
|
DB_PORT_HOST=3306
|
|
|
|
# --- Sessioni e log ---
|
|
# false con accesso HTTP, true solo in HTTPS: vedi .env.example
|
|
SESSION_SECURE_COOKIE=false
|
|
LOG_CHANNEL=stderr
|
|
LOG_LEVEL=warning
|
|
|
|
# --- Posta (SMTP esterno) ---
|
|
MAIL_MAILER=smtp
|
|
MAIL_HOST=smtp.tuoprovider.it
|
|
MAIL_PORT=587
|
|
MAIL_USERNAME=utente
|
|
MAIL_PASSWORD=password
|
|
MAIL_SCHEME=tls
|
|
MAIL_FROM_ADDRESS=noreply@tuodominio.it
|
|
```
|
|
|
|
> `APP_URL` deve corrispondere **esattamente** al dominio che servirai, schema
|
|
> `https://` incluso. L'applicazione rifiuta richieste con un `Host` diverso: è
|
|
> la protezione contro l'host header injection, ed è la causa più comune di
|
|
> "pagina che non si apre" al primo deploy.
|
|
|
|
### Passo 4 — Assegna il dominio (facoltativo)
|
|
|
|
Con la configurazione qui sopra l'applicazione risponde gia' su
|
|
`http://IP-DEL-SERVER:8080`. Questo passo serve solo se vuoi un dominio con
|
|
HTTPS — **consigliato**, perche' altrimenti password e sessioni viaggiano in
|
|
chiaro.
|
|
|
|
Quando lo fai, cambia anche: `APP_URL=https://tuodominio.it`,
|
|
`SESSION_SECURE_COOKIE=true` e `APP_BIND=127.0.0.1`.
|
|
|
|
**Domains** → **Add Domain**:
|
|
|
|
| Campo | Valore |
|
|
|----------------|-----------------------------|
|
|
| Service Name | `app` |
|
|
| Container Port | `80` |
|
|
| Host | il tuo dominio |
|
|
| HTTPS | attivo, Let's Encrypt |
|
|
|
|
Dokploy genera da sé le label Traefik: non vanno scritte nel compose.
|
|
|
|
### Passo 5 — Deploy
|
|
|
|
Premi **Deploy**. Il primo avvio esegue migrazioni, seed di ruoli e permessi, e
|
|
crea l'amministratore. Richiede in genere uno o due minuti.
|
|
|
|
Nei log dovresti vedere:
|
|
|
|
```
|
|
[✓] Storage directories ready.
|
|
[✓] APP_KEY provided via environment.
|
|
[✓] Database reachable.
|
|
[✓] Migrations applied.
|
|
[✓] Seed completed.
|
|
Initial admin created: admin@tuodominio.it
|
|
[✓] Caches warmed.
|
|
TerManager2 — ready
|
|
```
|
|
|
|
Accedi con le credenziali `INITIAL_ADMIN_*` e **cambia subito la password**.
|
|
|
|
### Se qualcosa non parte
|
|
|
|
Lo stack si rifiuta di avviarsi quando manca una variabile essenziale, con un
|
|
messaggio esplicito:
|
|
|
|
| Messaggio nei log | Cosa fare |
|
|
|------------------------------------------------------|----------------------------------|
|
|
| `required variable DB_PASSWORD is missing a value` | Imposta `DB_PASSWORD` |
|
|
| `required variable REDIS_PASSWORD is missing a value` | Imposta `REDIS_PASSWORD` |
|
|
| `[✗] APP_URL is not set` | Imposta `APP_URL` |
|
|
| `[✗] APP_URL must include the scheme` | Aggiungi `https://` davanti |
|
|
|
|
È voluto: meglio un errore immediato che un database in produzione con la
|
|
password di default.
|
|
|
|
---
|
|
|
|
## Deploy su altri sistemi
|
|
|
|
Lo stack è Docker Compose standard e funziona su **Coolify**, **Portainer**,
|
|
**CapRover** o un semplice server con Docker.
|
|
|
|
Le uniche due particolarità sono:
|
|
|
|
1. **Il file `.env`.** Dokploy lo genera dalle variabili della sua interfaccia.
|
|
Altrove crealo a mano: `cp .env.example .env` e compila i valori.
|
|
|
|
2. **La rete `dokploy-network`.** In fondo a `docker-compose.yml` c'è una rete
|
|
esterna che serve al Traefik di Dokploy per raggiungere il container. Hai
|
|
due possibilità:
|
|
|
|
- **Tenerla**, creandola una volta sola:
|
|
`docker network create dokploy-network`
|
|
- **Rimuoverla**: elimina `- dokploy-network` dalle reti del servizio `app`
|
|
e il blocco `dokploy-network:` in fondo al file.
|
|
|
|
Poi imposta nel `.env` la porta su cui esporre l'applicazione e avvia:
|
|
|
|
```bash
|
|
# ascolta su tutte le interfacce, per metterci davanti un reverse proxy
|
|
echo "APP_PORT=0.0.0.0:8080" >> .env
|
|
|
|
docker compose up -d --build
|
|
docker compose logs -f app
|
|
```
|
|
|
|
Infine metti il tuo reverse proxy (Nginx, Caddy, Traefik) davanti alla porta
|
|
`8080`, con certificato TLS.
|
|
|
|
---
|
|
|
|
## Sviluppo locale
|
|
|
|
Si usa lo stesso `docker-compose.yml` della produzione, con il profilo `dev`
|
|
che aggiunge Mailpit:
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
docker network create dokploy-network # una volta sola
|
|
|
|
# nel .env, per l'uso locale:
|
|
# APP_URL=http://localhost:8080
|
|
# APP_ENV=local
|
|
# MAIL_MAILER=smtp MAIL_HOST=mailpit MAIL_PORT=1025
|
|
# e le tre password a piacere
|
|
# (SESSION_SECURE_COOKIE=false e APP_BIND/APP_PORT vanno gia' bene)
|
|
|
|
docker compose --profile dev up -d --build
|
|
```
|
|
|
|
| Cosa | Dove |
|
|
|---------------------|-------------------------------|
|
|
| Applicazione | http://localhost:8080 |
|
|
| Mailpit (posta test)| http://localhost:8025 |
|
|
| Login | quello che hai messo in `INITIAL_ADMIN_*` |
|
|
|
|
Se la porta 8080 è occupata, cambia `APP_PORT` nel `.env`
|
|
(es. `APP_PORT=127.0.0.1:8081`).
|
|
|
|
### Modifiche al codice senza ricostruire l'immagine
|
|
|
|
L'immagine è autosufficiente: `vendor/` e `public/build` stanno dentro, quindi
|
|
di norma una modifica al codice richiede `docker compose up -d --build`.
|
|
|
|
Per avere invece le modifiche PHP immediate, crea un
|
|
`docker-compose.override.yml` — Docker Compose lo applica da solo, senza
|
|
bisogno di alcun flag:
|
|
|
|
```yaml
|
|
services:
|
|
app:
|
|
environment:
|
|
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 1
|
|
volumes:
|
|
- ./:/var/www/html
|
|
# vendor e build restano quelli dell'immagine, non quelli dell'host
|
|
- /var/www/html/vendor
|
|
- /var/www/html/public/build
|
|
```
|
|
|
|
Il file è già in `.gitignore`, quindi resta solo tuo.
|
|
|
|
Per lavorare sugli asset frontend con hot reload:
|
|
|
|
```bash
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
### Test automatici
|
|
|
|
La suite gira in un container dedicato: l'immagine di produzione installa le
|
|
dipendenze senza quelle di sviluppo, quindi PHPUnit lì dentro non c'è.
|
|
|
|
```bash
|
|
docker compose --profile test run --build --rm test
|
|
|
|
# una sola classe
|
|
docker compose --profile test run --build --rm test --filter BackupArchiveTest
|
|
|
|
# un solo gruppo
|
|
docker compose --profile test run --build --rm test --testsuite Unit
|
|
```
|
|
|
|
Il container esce col codice di PHPUnit, quindi la riga di comando si incatena
|
|
come qualsiasi altro comando.
|
|
|
|
**`--build` non è facoltativo.** Il sorgente non è montato: senza ricostruire,
|
|
si eseguono i test dell'ultima immagine costruita, che possono essere di due
|
|
modifiche fa. Ometterlo ha senso solo per rilanciare la stessa suite subito
|
|
dopo, quando si sa che nulla è cambiato.
|
|
|
|
Cosa serve sapere:
|
|
|
|
- **Non serve nient'altro acceso.** `phpunit.xml` impone SQLite in memoria,
|
|
cache `array` e code `sync`: la suite passa a stack spento e non tocca né il
|
|
database di sviluppo né il volume `storage_data`.
|
|
- **`APP_KEY` di test è fissa e scritta in `phpunit.xml`.** Serve perché nome e
|
|
cognome dei proclamatori sono cifrati: senza chiave nemmeno un test che non
|
|
parla di cifratura riuscirebbe a scrivere una riga. Non ha alcun rapporto con
|
|
la chiave di produzione.
|
|
- **SQLite non è MariaDB.** Resta un punto che usa SQL specifico di MySQL —
|
|
`CAST(... AS UNSIGNED)` nell'ordinamento del Registro — e che quindi va
|
|
provato sullo stack vero, non qui.
|
|
|
|
---
|
|
|
|
## Variabili d'ambiente
|
|
|
|
Il container legge la configurazione dall'ambiente di processo (su Dokploy: le
|
|
variabili della UI, salvate in `.env`). Il file `docker-compose.yml` **non**
|
|
ridichiara le variabili applicative: le prende dal `.env` così com'è.
|
|
|
|
### Obbligatorie
|
|
|
|
| Variabile | Esempio | Note |
|
|
|---------------------|-------------------------------|-----------------------------------------|
|
|
| `APP_URL` | `https://territori.esempio.it`| Deve combaciare col dominio servito |
|
|
| `APP_KEY` | `base64:...` | Cifra i dati personali — **da custodire**|
|
|
| `DB_PASSWORD` | — | Nessun default |
|
|
| `DB_ROOT_PASSWORD` | — | Nessun default |
|
|
| `REDIS_PASSWORD` | — | Nessun default |
|
|
|
|
### Principali facoltative
|
|
|
|
| Variabile | Default | Descrizione |
|
|
|------------------------------|----------------|--------------------------------------|
|
|
| `APP_NAME` | `TerManager2` | Nome mostrato nell'interfaccia |
|
|
| `APP_ENV` | `production` | `local` in sviluppo |
|
|
| `APP_DEBUG` | `false` | **Mai `true` in produzione** |
|
|
| `APP_TIMEZONE` | `Europe/Rome` | Fuso orario |
|
|
| `LOG_LEVEL` | `warning` | `debug` per diagnosticare |
|
|
| `SESSION_SECURE_COOKIE` | `false` | `true` solo servendo in HTTPS |
|
|
| `MAX_UPLOAD_MB` | `2048` | Tetto dei file caricati: governa insieme nginx, PHP e la validazione |
|
|
| `APP_BIND` | `0.0.0.0` | `127.0.0.1` dietro un reverse proxy |
|
|
| `APP_PORT` | `8080` | Porta dell'applicazione sull'host |
|
|
| `DB_BIND` | `127.0.0.1` | Interfaccia della porta MariaDB |
|
|
| `DB_PORT_HOST` | `3306` | Cambiala se la 3306 e' occupata |
|
|
| `INITIAL_ADMIN_EMAIL` | — | Admin creato al primo avvio |
|
|
| `INITIAL_ADMIN_PASSWORD` | — | Idem |
|
|
| `INITIAL_ADMIN_NAME` | `Amministratore`| Idem |
|
|
| `MAIL_MAILER` | `log` | `smtp` per inviare davvero |
|
|
| `RUN_MIGRATIONS` | `true` | Migrazioni automatiche all'avvio |
|
|
| `RUN_DB_SEED_ON_FIRST_START` | `true` | Seed al primo avvio |
|
|
|
|
### Da non cambiare
|
|
|
|
Questi valori sono **nomi di servizi** definiti in `docker-compose.yml`, non
|
|
indirizzi di rete. Se li modifichi il container non trova più il database o
|
|
Redis:
|
|
|
|
```dotenv
|
|
DB_HOST=mariadb
|
|
REDIS_HOST=redis
|
|
```
|
|
|
|
Nemmeno `CACHE_STORE`, `SESSION_DRIVER` e `QUEUE_CONNECTION` andrebbero
|
|
spostati altrove: lo stack fornisce Redis e il queue worker consuma la coda
|
|
Redis, quindi puntarli su un driver diverso interrompe l'elaborazione dei job.
|
|
|
|
---
|
|
|
|
## Impostazioni applicative
|
|
|
|
Configurabili dall'interfaccia (**Impostazioni**, permesso `settings.manage`),
|
|
non da variabili d'ambiente:
|
|
|
|
| Impostazione | Descrizione |
|
|
|--------------------------------|------------------------------------------------------|
|
|
| Nome congregazione | Mostrato nell'intestazione e nei PDF |
|
|
| Logo | Immagine nell'intestazione |
|
|
| URL pubblico base | Base dei link PDF condivisibili |
|
|
| Giorni giacenza "da assegnare" | Soglia oltre cui un territorio è da assegnare |
|
|
| Mesi giacenza "prioritari" | Soglia oltre cui un territorio diventa prioritario |
|
|
| Giorni per "smarrito" | Soglia oltre cui un'assegnazione è da rientrare |
|
|
| Elementi nelle liste della home| Quanti record mostrare nelle tre liste rapide |
|
|
| Validità link PDF (mesi) | Scadenza dei link pubblici alle cartoline |
|
|
| Download PDF visibile | Mostra o nasconde il pulsante di download |
|
|
| Retention audit log (giorni) | Dopo quanti giorni i log vengono cancellati |
|
|
|
|
---
|
|
|
|
## Aggiornamenti
|
|
|
|
Push su `main` → **Deploy** su Dokploy. Ad ogni avvio il container:
|
|
|
|
1. ricrea le directory di storage e risolve `APP_KEY`
|
|
2. attende il database
|
|
3. applica le migrazioni pendenti
|
|
4. salta il seed (già eseguito — il marker è nel volume storage)
|
|
5. rigenera le cache di configurazione, rotte e viste
|
|
|
|
Se le migrazioni falliscono il container **non parte**: preferibile a servire
|
|
traffico su uno schema applicato a metà.
|
|
|
|
Un aggiornamento ordinario non richiede alcun intervento manuale.
|
|
|
|
---
|
|
|
|
## Backup e ripristino
|
|
|
|
### Com'è fatta la pagina *Backup e import*
|
|
|
|
Cinque zone numerate, in ordine di quanto sono definitive. L'indice in cima
|
|
salta su ciascuna evidenziandola:
|
|
|
|
| # | Zona | A cosa serve |
|
|
|---|------|--------------|
|
|
| 1 | Backup completo | Crea e scarica il pacchetto `.zip` |
|
|
| 2 | Ripristina da backup | Ricarica un pacchetto: **sostituisce** dati e file |
|
|
| 3 | Import PDF territori | Associa i PDF ai territori (da archivio ZIP o da cartella) |
|
|
| 4 | Dati in formato XML | Esporta/importa i soli dati, converte i dump della vecchia app |
|
|
| 5 | Zona pericolosa | Azzera l'installazione |
|
|
|
|
In fondo alla finestra c'è **una sola console**, comune a tutte le operazioni:
|
|
si apre da sé quando qualcosa parte, scorre da sola man mano che arrivano le
|
|
righe e riporta sia quello che succede nel browser (file scelto, avanzamento del
|
|
caricamento, codici HTTP, timeout) sia quello che succede sul server, in ordine
|
|
di tempo e con la sorgente accanto a ogni riga. Il pulsante **Copia** serve
|
|
proprio a questo: il primo passo di ogni diagnosi è farsi mandare quel testo.
|
|
|
|
Se torni indietro a leggere, lo scorrimento automatico si sospende e compare
|
|
*Torna in fondo*.
|
|
|
|
### Dall'applicazione: pacchetto `.zip` (consigliato)
|
|
|
|
*Backup e import* → **Crea e scarica backup**. Produce un unico `.zip` con
|
|
tutti i dati gestionali, gli utenti e tutti i file caricati (PDF dei territori,
|
|
miniature, logo). È la via da usare per spostare o rimettere in piedi
|
|
un'installazione senza toccare i container.
|
|
|
|
L'archivio si prepara in background e la console mostra l'avanzamento reale
|
|
(file compressi / file totali); il download parte da solo appena è pronto.
|
|
Stessa cosa per il ripristino: prima la barra di caricamento del pacchetto,
|
|
accanto al pulsante, poi le fasi di lavoro nella console (estrazione, database,
|
|
file).
|
|
|
|
Sulla stessa pagina, **Ripristina backup** ricarica il pacchetto: sostituisce i
|
|
dati gestionali correnti *e* i file caricati.
|
|
|
|
> **Dopo un ripristino tutti gli utenti — amministratore compreso — devono
|
|
> impostare una nuova password.** Il pacchetto contiene gli hash delle password
|
|
> di ogni account: chi lo ha avuto in mano ha potuto tentarne il crack offline,
|
|
> quindi le vecchie credenziali non sono più considerate sicure. Al primo
|
|
> accesso l'applicazione resta bloccata sulla schermata di cambio password.
|
|
|
|
Il pacchetto **non** contiene `APP_KEY`: i nomi dei proclamatori sono cifrati
|
|
con quella chiave, quindi il ripristino va fatto su un'installazione che ha la
|
|
**stessa** `APP_KEY` di quando il backup è stato creato.
|
|
|
|
Il tetto per l'upload lo decide **`MAX_UPLOAD_MB`** nel `.env` (default: 2048,
|
|
cioè 2 GB). Da quel valore l'entrypoint genera `client_max_body_size` per nginx
|
|
e `upload_max_filesize`/`post_max_size` per PHP, e la validazione lo rilegge:
|
|
un solo numero per tutta la catena.
|
|
|
|
> Se un caricamento si pianta a una percentuale bassa **senza messaggi**, è
|
|
> quasi sempre questo il limite. Superato il tetto nginx risponde `413` dopo
|
|
> aver accettato zero byte: il browser si vede chiudere la connessione a metà
|
|
> invio e la barra resta ferma. Dalla versione attuale la pagina controlla la
|
|
> dimensione *prima* di iniziare e lo dice subito, ma se aggiorni un'installazione
|
|
> vecchia il sintomo è quello.
|
|
|
|
Per alzarlo servono un `MAX_UPLOAD_MB` più grande **e** un redeploy (i limiti si
|
|
scrivono all'avvio del container). Alzarlo non costa memoria — nginx accumula il
|
|
corpo su disco e PHP lavora su un file temporaneo — ma serve spazio libero nel
|
|
volume. Oltre qualche giga conviene comunque il backup dei volumi qui sotto: un
|
|
upload lunghissimo da browser è fragile.
|
|
|
|
> **Se il dominio passa da Cloudflare, comanda Cloudflare.** Sul piano free
|
|
> rifiuta i corpi di richiesta oltre **100 MB**, e lo fa in base al
|
|
> `Content-Length`: il `413` arriva dopo appena un megabyte spedito, quindi il
|
|
> pacchetto **non raggiunge mai nginx** e `MAX_UPLOAD_MB` non sposta nulla.
|
|
> C'è anche un tetto di 100 secondi: un caricamento lento può fallire con `524`
|
|
> pur restando sotto il limite di dimensione.
|
|
>
|
|
> Per superarlo: togliere il dominio da Cloudflare (DNS-only), anche solo su un
|
|
> sottodominio d'amministrazione; oppure caricare puntando direttamente all'IP
|
|
> dell'origine; oppure usare il ripristino dai volumi qui sotto. Il piano
|
|
> Business arriva a 200 MB, spesso ancora insufficienti.
|
|
>
|
|
> La **console** in fondo alla finestra mostra il codice HTTP ricevuto: se
|
|
> leggi `413` con un pacchetto sotto `MAX_UPLOAD_MB`, il tetto è a monte.
|
|
|
|
### Zona pericolosa: azzera tutto
|
|
|
|
Zona 5, in fondo alla stessa pagina: **Scarica backup e azzera tutto** riporta
|
|
l'applicazione allo stato di installazione appena eseguita. Va scritto
|
|
`AZZERA TUTTO` per sbloccare il pulsante, e l'operazione procede in due tempi:
|
|
|
|
1. Viene creato e scaricato un backup completo di tutto ciò che c'è.
|
|
2. L'operazione **si ferma e aspetta**, e la richiesta di conferma compare lì
|
|
nella zona 5, sotto il pulsante che l'ha avviata: controlli il file
|
|
scaricato, digiti la **tua password** e confermi con un secondo clic. Fino a
|
|
lì non viene toccato nulla, e puoi annullare. Dopo cinque password sbagliate
|
|
l'azzeramento resta bloccato per 5 minuti (l'accesso all'applicazione no).
|
|
3. Solo dopo la conferma vengono eliminati territori, proclamatori,
|
|
assegnazioni, campagne, zone, tipologie, anni di servizio, il registro
|
|
attività e tutti i file caricati; le impostazioni tornano ai default e i
|
|
ruoli vengono riseminati.
|
|
|
|
> **Tutti gli altri utenti vengono eliminati.** Resta solo l'account
|
|
> dell'amministratore che ha eseguito l'operazione, con la sua password
|
|
> attuale — come un'installazione nuova, che ha un amministratore solo.
|
|
> Cancellarli tutti lascerebbe l'applicazione senza nessuno che possa
|
|
> accedere fino al riavvio del container.
|
|
|
|
Il backup scaricato al punto 1 è l'unico modo per tornare indietro.
|
|
|
|
### Dai volumi: cosa salvare
|
|
|
|
| Volume | Contenuto | Nel backup |
|
|
|----------------|------------------------------------------------------|:----------:|
|
|
| `db_data` | Database MariaDB | ✅ |
|
|
| `storage_data` | PDF territori, miniature, log, `APP_KEY` di fallback | ✅ |
|
|
| `redis_data` | Cache e sessioni — rigenerabili | — |
|
|
|
|
Più il valore di **`APP_KEY`**. Il database da solo non basta: senza la chiave
|
|
i dati personali cifrati non sono leggibili.
|
|
|
|
### Backup
|
|
|
|
```bash
|
|
# Database
|
|
docker exec <container-db> mariadb-dump -u root -p"$DB_ROOT_PASSWORD" \
|
|
termanager2 | gzip > termanager2_$(date +%Y%m%d).sql.gz
|
|
|
|
# File caricati (PDF e miniature)
|
|
docker run --rm -v <progetto>_storage_data:/data -v "$PWD":/backup \
|
|
alpine tar czf /backup/storage_$(date +%Y%m%d).tar.gz -C /data .
|
|
```
|
|
|
|
Automatizzabile con cron:
|
|
|
|
```bash
|
|
0 2 * * * docker exec <container-db> mariadb-dump -u root -p"$DB_ROOT_PASSWORD" termanager2 | gzip > /backups/termanager2_$(date +\%Y\%m\%d).sql.gz
|
|
```
|
|
|
|
### Ripristino
|
|
|
|
```bash
|
|
gunzip -c termanager2_20260718.sql.gz | \
|
|
docker exec -i <container-db> mariadb -u root -p"$DB_ROOT_PASSWORD" termanager2
|
|
|
|
docker exec <container-app> php artisan migrate --force
|
|
docker exec <container-app> php artisan cache:clear
|
|
```
|
|
|
|
Assicurati che `APP_KEY` sia **la stessa** di quando il backup è stato creato.
|
|
|
|
---
|
|
|
|
## Comandi utili
|
|
|
|
Sostituisci `<app>` con il nome del container (lo trovi con `docker ps`).
|
|
|
|
```bash
|
|
# Log in tempo reale
|
|
docker logs -f <app>
|
|
|
|
# Stato delle migrazioni
|
|
docker exec -it <app> php artisan migrate:status
|
|
|
|
# Shell interattiva Laravel
|
|
docker exec -it <app> php artisan tinker
|
|
|
|
# Reimpostare la password di un utente
|
|
docker exec -it <app> php artisan tinker --execute="
|
|
\$u = App\Models\User::where('email','admin@tuodominio.it')->first();
|
|
\$u->password = Hash::make('NuovaPassword');
|
|
\$u->save();
|
|
"
|
|
|
|
# Creare un altro amministratore
|
|
docker exec -it <app> php artisan app:create-initial-admin \
|
|
--name="Nome" --email="mail@dominio.it" --password="Password"
|
|
|
|
# Pulizia manuale dell'audit log
|
|
docker exec -it <app> php artisan audit:cleanup
|
|
|
|
# Pulizia manuale degli archivi di backup residui (default: più vecchi di 24h)
|
|
docker exec -it <app> php artisan backup:cleanup
|
|
docker exec -it <app> php artisan backup:cleanup --ore=0 # tutti
|
|
|
|
# Svuotare le cache
|
|
docker exec -it <app> php artisan optimize:clear
|
|
```
|
|
|
|
Entrambe le pulizie girano già da sole ogni notte (container `scheduler`); i
|
|
comandi qui sopra servono solo per anticiparle.
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Il container `app` resta unhealthy
|
|
|
|
L'health check interroga `/up`. Guarda i log (`docker logs <app>`): quasi
|
|
sempre è il database irraggiungibile o una migrazione fallita.
|
|
|
|
### Errore 400 o pagina bianca sul dominio
|
|
|
|
`APP_URL` non corrisponde al dominio servito. Deve combaciare esattamente,
|
|
schema `https://` incluso. È la protezione contro l'host header injection.
|
|
|
|
### La pagina appare senza stile (CSS mancante)
|
|
|
|
Gli asset sono compilati dentro l'immagine. Se mancano, è fallito lo stage di
|
|
build frontend: cerca `npm run build` nel log di build del deploy.
|
|
|
|
### I job restano in coda
|
|
|
|
Verifica che `queue-worker` sia healthy e che `REDIS_PASSWORD` sia identica per
|
|
tutti i servizi.
|
|
|
|
### L'import PDF non termina
|
|
|
|
L'import ha un timeout di 15 minuti; oltre, il job viene marcato failed e i file
|
|
temporanei rimossi. Controlla i log del worker per individuare il PDF
|
|
responsabile.
|
|
|
|
### I dati cifrati non sono più leggibili
|
|
|
|
`APP_KEY` è cambiata. Ripristina il valore originale: non esiste altro modo di
|
|
recuperare i dati. Se la chiave è persa definitivamente, i proclamatori vanno
|
|
reinseriti (territori, assegnazioni e storico restano intatti — non sono
|
|
cifrati).
|
|
|
|
### Reset completo (solo sviluppo)
|
|
|
|
```bash
|
|
docker compose --profile dev down -v
|
|
docker compose --profile dev up -d --build
|
|
```
|
|
|
|
`-v` cancella i volumi: database, Redis e file caricati vengono persi.
|
|
|
|
---
|
|
|
|
## Funzionalità
|
|
|
|
### Dashboard
|
|
- Anno teocratico corrente (creato automaticamente, settembre → agosto)
|
|
- Media mensile di percorrenza e durata media delle assegnazioni
|
|
- Campagna attiva con barra di progresso
|
|
- Tre liste rapide con azione diretta: **da assegnare**, **prioritari**,
|
|
**da rientrare**
|
|
|
|
### Territori
|
|
- CRUD completo con upload PDF e miniatura generata automaticamente
|
|
- Filtri per zona, tipologia e stato
|
|
- Badge di stato colorati e indicatore di priorità (manuale o automatica)
|
|
- Dettaglio con visualizzatore PDF e storico assegnazioni per anno teocratico
|
|
- Link PDF pubblico condivisibile, a scadenza configurabile
|
|
- Soft delete con cestino (ripristino o eliminazione definitiva)
|
|
- Import massivo di PDF da cartella o archivio ZIP, elaborato in background
|
|
|
|
### Proclamatori
|
|
- CRUD con nome e cognome **cifrati nel database**
|
|
- Conteggio dei territori attualmente assegnati
|
|
- **Anonimizzazione GDPR** irreversibile
|
|
- Soft delete con cestino
|
|
|
|
### Assegnazione e rientro
|
|
- Selezione territorio disponibile e proclamatore attivo
|
|
- Anno teocratico rilevato automaticamente dalla data
|
|
- Validazione: territorio non già assegnato, entrambi attivi
|
|
- Rientro con calcolo automatico dei giorni
|
|
- **Prompt campagna**: se il rientro ricade nel periodo di una campagna, viene
|
|
chiesto se conteggiarlo
|
|
|
|
### Campagne
|
|
- CRUD con date di inizio e fine
|
|
- Stato automatico: attiva, futura o conclusa
|
|
- Percentuale di percorrenza in tempo reale
|
|
- Dettaglio territorio per territorio
|
|
|
|
### Registro assegnazioni
|
|
- Filtri per anno teocratico, zona, tipologia, stato e ricerca testuale
|
|
- Colonne: territorio, proclamatore, date, giorni, campagna
|
|
|
|
### Audit log
|
|
- Tracciamento automatico su tutti i modelli
|
|
- Eventi di flusso espliciti: assegnazione, rientro, login, logout
|
|
- Filtri per utente e tipo evento, con diff prima/dopo
|
|
- Pulizia automatica schedulata secondo la retention configurata
|
|
|
|
### Backup e import
|
|
- **Backup completo `.zip`**: dati gestionali, utenti e file caricati (PDF
|
|
territori, miniature, logo) in un unico pacchetto scaricabile e ripristinabile
|
|
- Barra di avanzamento reale su backup e ripristino (lavoro svolto, non tempo)
|
|
- Ripristino da pacchetto, con cambio password obbligatorio per tutti gli utenti
|
|
- **Zona pericolosa**: azzeramento completo con backup obbligatorio scaricato,
|
|
password dell'amministratore e conferma esplicita prima di procedere
|
|
- Import ed export XML dell'intero set di dati gestionali (senza i file)
|
|
- Conversione di dump SQL legacy in XML compatibile
|
|
- Import massivo dei PDF dei territori da cartella o archivio ZIP
|
|
|
|
### Zone e tipologie
|
|
- Gestione inline: aggiungi, rinomina, attiva/disattiva, elimina
|
|
- Non eliminabili se hanno territori associati
|
|
|
|
---
|
|
|
|
## Ruoli e permessi
|
|
|
|
Tre ruoli con permessi granulari (spatie/laravel-permission):
|
|
|
|
| Permesso | Amministratore | Assistente | Operatore |
|
|
|-----------------------|:--------------:|:----------:|:---------:|
|
|
| `settings.manage` | ✅ | | |
|
|
| `territori.manage` | ✅ | | |
|
|
| `proclamatori.manage` | ✅ | ✅ | |
|
|
| `campagne.manage` | ✅ | ✅ | |
|
|
| `registro.view` | ✅ | ✅ | |
|
|
| `registro.export` | ✅ | | |
|
|
| `territori.assign` | ✅ | ✅ | ✅ |
|
|
| `territori.return` | ✅ | ✅ | ✅ |
|
|
| `audit.view` | ✅ | | |
|
|
| `audit.export` | ✅ | | |
|
|
|
|
L'amministratore ha un bypass completo (`Gate::before` in `AppServiceProvider`).
|
|
Il menu laterale mostra solo le voci per cui l'utente ha i permessi.
|
|
|
|
I permessi sono verificati sia sulle rotte sia sulle richieste Livewire, quindi
|
|
una revoca ha effetto immediato senza bisogno di ricaricare la pagina.
|
|
|
|
---
|
|
|
|
## Sicurezza e GDPR
|
|
|
|
### Sicurezza
|
|
- **Cifratura a riposo**: nome e cognome dei proclamatori cifrati con AES-256-CBC
|
|
tramite `APP_KEY`
|
|
- **Password**: hash bcrypt
|
|
- **RBAC**: middleware `permission:` sulle rotte, riapplicato anche alle
|
|
richieste Livewire
|
|
- **CSRF**: token su tutti i form
|
|
- **Rate limiting**: sul login
|
|
- **Host header**: accettate solo richieste il cui `Host` corrisponde a `APP_URL`
|
|
- **Scheme forwarding**: lo schema è deciso dal server, non dall'header del
|
|
client, per non falsare la validazione dei link firmati
|
|
- **Security headers**: `X-Frame-Options`, `X-Content-Type-Options`,
|
|
`Referrer-Policy`
|
|
- **Sessioni**: in Redis, cookie httpOnly e `Secure` in produzione
|
|
- **Esecuzione PHP**: solo `index.php` è un entrypoint valido; ogni altro `.php`
|
|
restituisce 404
|
|
|
|
### GDPR
|
|
- **Minimizzazione**: dei proclamatori si conservano solo nome, cognome e stato
|
|
- **Cifratura a riposo**: i dati sono illeggibili anche con accesso diretto al DB
|
|
- **Audit trail**: registro completo delle operazioni
|
|
- **Diritto all'oblio**: funzione di anonimizzazione irreversibile
|
|
- **Retention**: cancellazione automatica dei log oltre il periodo configurato
|
|
- **No dati sensibili nei log**: l'audit registra gli identificativi, non i nomi
|
|
in chiaro
|
|
|
|
---
|
|
|
|
## Modello dati
|
|
|
|
| Tabella | Descrizione |
|
|
|-------------------|--------------------------------------------------------------|
|
|
| `users` | Utenti; `password_change_required` blocca l'accesso finché non si sceglie una password nuova |
|
|
| `settings` | Configurazione singleton |
|
|
| `zone` | Zone territoriali |
|
|
| `tipologie` | Tipologie di territorio |
|
|
| `proclamatori` | Assegnatari, con nome/cognome cifrati e soft delete |
|
|
| `territori` | Cartoline con zona, tipologia, PDF, miniatura, soft delete |
|
|
| `anni_teocratici` | Periodi settembre → agosto |
|
|
| `campagne` | Periodi speciali con date e descrizione |
|
|
| `assegnazioni` | Territorio ↔ proclamatore, con date, anno e campagna |
|
|
| `activity_log` | Audit trail |
|
|
|
|
**Vincoli e indici principali**
|
|
|
|
- `territori.numero` e `anni_teocratici.label` — univoci
|
|
- `assegnazioni(territorio_id, returned_at)` — indice composito, è la ricerca
|
|
più frequente dell'applicazione
|
|
- `assegnazioni(proclamatore_id, returned_at)`, `assegnazioni(assigned_at)`
|
|
- `territori(attivo, deleted_at)`, `proclamatori(deleted_at)`
|
|
- Foreign key con `RESTRICT` su territorio e proclamatore, a protezione
|
|
dell'integrità dello storico
|
|
|
|
---
|
|
|
|
## Regole di business
|
|
|
|
### Anno teocratico
|
|
Va da **settembre ad agosto**: da settembre in poi l'anno è `corrente/corrente+1`,
|
|
prima è `corrente-1/corrente`. Viene creato automaticamente quando serve.
|
|
L'assegnazione conta per l'anno determinato dalla **data di assegnazione**.
|
|
|
|
### Stato del territorio
|
|
Calcolato dinamicamente, non memorizzato:
|
|
|
|
| Stato | Condizione |
|
|
|----------------|-----------------------------------------------------|
|
|
| `in_reparto` | Attivo, nessuna assegnazione aperta |
|
|
| `assegnato` | Ha un'assegnazione con `returned_at` nullo |
|
|
| `da_rientrare` | Assegnato da più giorni della soglia configurata |
|
|
| `inattivo` | Contrassegnato come non attivo |
|
|
|
|
### Priorità
|
|
Un territorio è prioritario se lo è per **flag manuale** o perché non viene
|
|
assegnato da più mesi della soglia configurata. La soglia automatica **vince
|
|
sempre** sul flag: l'amministratore può aggiungere priorità, non toglierla
|
|
quando la soglia è scattata.
|
|
|
|
### Percentuale di percorrenza delle campagne
|
|
- **Denominatore**: tutti i territori attivi
|
|
- **Numeratore**: i territori attivi distinti con almeno un'assegnazione
|
|
collegata alla campagna e contrassegnata come conteggiata al rientro (un
|
|
territorio percorso due volte conta una volta sola)
|
|
|
|
Al rientro la campagna viene proposta se il territorio era fuori almeno in
|
|
parte del periodo: assegnato durante la campagna, rientrato durante la
|
|
campagna, oppure fuori per tutta la sua durata. Se sia stato davvero percorso
|
|
lo decide l'operatore con la spunta. Se più campagne corrispondono viene
|
|
proposta la più recente. I territori assegnati durante la campagna e non
|
|
ancora rientrati compaiono nel dettaglio come "in corso".
|
|
|
|
---
|
|
|
|
## Architettura
|
|
|
|
### Stack
|
|
|
|
| Componente | Tecnologia |
|
|
|-------------------|--------------------------------------------|
|
|
| Framework | Laravel 11 |
|
|
| PHP | 8.3 (FPM) |
|
|
| Interfaccia | Livewire 3.5 + TailwindCSS 4 + Alpine.js |
|
|
| Database | MariaDB 11 |
|
|
| Cache/Sessioni/Code| Redis 7 |
|
|
| Web server | Nginx, nello stesso container di php-fpm |
|
|
| RBAC | spatie/laravel-permission |
|
|
| Audit trail | spatie/laravel-activitylog |
|
|
| PDF | barryvdh/laravel-dompdf + poppler-utils |
|
|
| Build asset | Vite 6 (in fase di build immagine) |
|
|
|
|
### Struttura del progetto
|
|
|
|
```
|
|
TerManager2/
|
|
├── app/
|
|
│ ├── Console/Commands/ # audit:cleanup, backup:cleanup, app:create-initial-admin
|
|
│ ├── Http/Controllers/ # PDF pubblici, import, backup, login, cambio password
|
|
│ ├── Http/Middleware/ # RequirePasswordChange
|
|
│ ├── Jobs/ # Import PDF, backup, ripristino, azzeramento (in coda)
|
|
│ ├── Livewire/ # Tutti i componenti di interfaccia
|
|
│ ├── Models/ # 9 modelli Eloquent
|
|
│ ├── Providers/ # AppServiceProvider
|
|
│ └── Services/ # Miniature PDF, stato import, archivio backup
|
|
├── bootstrap/app.php # Middleware, proxy e host fidati
|
|
├── config/ # Configurazione Laravel
|
|
├── database/
|
|
│ ├── migrations/
|
|
│ └── seeders/ # Ruoli, permessi, dati di sviluppo
|
|
├── docker/
|
|
│ ├── nginx/default.conf # Vhost
|
|
│ ├── php/
|
|
│ │ ├── Dockerfile # Build multi-stage (runtime = ultimo stage)
|
|
│ │ ├── entrypoint.sh # Avvio: chiave, migrazioni, cache
|
|
│ │ ├── queue-entrypoint.sh # Avvio worker e scheduler
|
|
│ │ ├── php.ini
|
|
│ │ └── www.conf
|
|
│ └── supervisor/ # nginx + php-fpm nello stesso container
|
|
├── resources/
|
|
│ ├── css/app.css # Sistema di pulsanti, tabelle responsive, console
|
|
│ ├── js/
|
|
│ └── views/
|
|
│ ├── components/btn.blade.php # L'unico pulsante dell'applicazione
|
|
│ └── livewire/ # Una vista per componente
|
|
├── routes/web.php # Rotte con i relativi permessi
|
|
├── tests/ # Suite PHPUnit (profilo compose "test")
|
|
├── phpunit.xml # Configurazione e ambiente dei test
|
|
├── docker-compose.yml # Unico, produzione e sviluppo
|
|
├── .env.example # Tutta la configurazione
|
|
├── HANDOFF.md # Stato del progetto per chi subentra
|
|
└── README.md
|
|
```
|
|
|
|
### Perché nginx e php-fpm nello stesso container
|
|
|
|
Con container separati, nginx avrebbe bisogno di una propria copia di `public/`
|
|
e del volume storage solo per servire i file statici. Per un'applicazione di
|
|
questa scala è complessità senza beneficio: un solo container significa un solo
|
|
health check, un solo target per il proxy e nessun volume condiviso da tenere
|
|
allineato.
|
|
|
|
---
|
|
|
|
## Licenza
|
|
|
|
Proprietaria. Tutti i diritti riservati.
|