# Captive Portal CMS — Casa della Scuola CMS per portali captive: gestione di card informative, gallerie, flip-book e contenuti kiosk a schermo intero, con un'area di amministrazione locale. Stack: Next.js 16 (App Router, Turbopack), React 19, TypeScript, Tailwind v4. Persistenza su file (nessun database). Pensato per girare su **server offline**. --- ## Indice 1. [Prerequisiti di sistema](#prerequisiti-di-sistema) 2. [Avvio](#avvio) 3. [Aggiornamento](#aggiornamento) 4. [Configurazione (`lib/config.ts`)](#configurazione-libconfigts) 5. [Tipi di card](#tipi-di-card) 6. [File consentiti negli upload](#file-consentiti-negli-upload) 7. [Limiti di testo](#limiti-di-testo) 8. [Sicurezza degli input](#sicurezza-degli-input) 9. [Struttura dei dati (`data/`)](#struttura-dei-dati-data) 10. [Stato zero e compatibilità tra versioni](#stato-zero-e-compatibilità-tra-versioni) 11. [Backup e ripristino](#backup-e-ripristino) 12. [Factory Preset (developer)](#factory-preset-developer) 13. [Font](#font) 14. [Deploy sotto sotto-percorso (basePath) dietro Apache](#deploy-sotto-sotto-percorso-basepath-dietro-apache) 15. [Protezione dell'amministrazione (Keycloak) e routing](#protezione-dellamministrazione-keycloak-e-routing) 16. [Risoluzione problemi](#risoluzione-problemi) --- ## Prerequisiti di sistema Sul server servono alcuni binari di sistema (richiamati direttamente, non via npm): | Binario | A cosa serve | Se manca | |---|---|---| | `ffmpeg` | Transcodifica video non compatibili | Upload video che richiedono ricodifica → `503` | | `ffprobe` | Riconoscimento codec video | Come sopra | | `zip` | Creazione backup / preset | Pulsanti "Save backup (ZIP)" e "Save as Factory Preset (dev)" → `503` | | `unzip` | Ripristino backup / factory reset | Pulsanti di ripristino → `503` | Verifica su server: ```bash which ffmpeg ffprobe zip unzip ``` Se mancano, installali con il gestore pacchetti del sistema (i nomi dei pacchetti sono `ffmpeg`, `zip`, `unzip`). --- ## Avvio **Installazione:** ```bash mkdir -p /data/service/captive-portal-cms cd /data/service/captive-portal-cms git clone https://git.afasystems.it/pollutri/captive-portal-cms.git cd captive-portal-cms npm install npm run build ``` **In fase di sviluppo avviarlo con:** ```bash npm run dev ``` Con `BASE_PATH = '/cards'` (default) apri [http://localhost:3000/cards](http://localhost:3000/cards) (portale pubblico) e [http://localhost:3000/cards/admin](http://localhost:3000/cards/admin) (amministrazione). Le URL "nude" `/` e `/admin` reindirizzano automaticamente a quelle prefissate. Con `BASE_PATH = ''` l'app gira sulla radice (`/` e `/admin`). **In produzione avviarlo con:** ```bash /conf/etc/rc.d/rc.custom start ``` **Esempio di rc.custom per CPC:** ```bash . /etc/mnvars . /etc/mnsuper.conf . $MN_rcconfig BIN="xxxxx" return=$rc_done case "$1" in start) cd /data/service/captive-portal-cms/captive-portal-cms && npm start & ;; stop) fuser -k 3000/tcp ;; restart) $0 stop && $0 start || return=$rc_failed ;; *) echo "Usage: $0 {start|stop|restart}" exit 1 esac ``` > **Server offline:** la macchina di produzione non ha accesso a internet. NON eseguire `npm install` lì. Installa le dipendenze su una macchina con internet (stesso OS, Linux), poi copia l'intera cartella `node_modules` sul server insieme al progetto buildato. Sul server basta `npm run build` (se `node_modules` è presente) + `npm start`. ### Prerequisiti per la produzione Per un deploy reale del CPC, oltre a far girare il processo Next servono altri tre setup, ciascuno con la sua sezione dedicata: 1. **Apache reverse proxy** che esponga il portale su `https:///cards/` → vedi [Deploy sotto sotto-percorso (basePath) dietro Apache](#deploy-sotto-sotto-percorso-basepath-dietro-apache). 2. **Autenticazione Keycloak** per `/cards/admin` e le API di scrittura → vedi [Protezione dell'amministrazione (Keycloak) e routing](#protezione-dellamministrazione-keycloak-e-routing). In sintesi: deve esistere in Keycloak un utente con username **`admin`** (lo username è la chiave di accesso). Per più amministratori si aggiungono altri username alla whitelist nei `` Apache. Tutti gli altri utenti (autenticati o no) vengono reindirizzati alla home pubblica. 3. **Binari di sistema** (`ffmpeg`, `ffprobe`, `zip`, `unzip`) per upload video / backup → vedi [Prerequisiti di sistema](#prerequisiti-di-sistema). --- ## Aggiornamento La procedura standard per applicare un nuovo rilascio del CPC in produzione è: 1. **Backup dei contenuti** — dall'admin: Settings → Backup & Restore → **Save backup (ZIP)**. Conserva lo zip per un eventuale rollback. (Stessa cosa via curl: `curl -O http://localhost:3000/cards/api/admin/backup`.) 2. **Aggiorna il codice sul server** — entra nella cartella del progetto e tira la nuova versione: ```bash cd /data/service/captive-portal-cms/captive-portal-cms git pull ``` 3. **Stop → rebuild → start** del demone Next: ```bash /conf/etc/rc.d/rc.custom stop && npm run build && /conf/etc/rc.d/rc.custom start ``` Note operative: - I contenuti in `data/` (card, configurazione del portale, media e font caricati) **non vengono toccati** dal rebuild. Il rebuild compila solo il codice; i dati restano dove sono. - Il bootstrap automatico controlla `data/cards.txt` all'avvio: se è vuoto (es. macchina nuova) e c'è un `factory/preset.zip` nel repo, lo srotola — vedi [Factory Preset → Bootstrap automatico all'avvio](#factory-preset-developer). - Il sito è momentaneamente offline tra lo stop e lo start (`npm run build` può richiedere fino a un minuto). - **Se ci sono dipendenze npm nuove** tra una versione e l'altra: `npm install` deve girare **su una macchina con internet** (la produzione è offline). Su quella macchina lancia `npm install`, poi copi la cartella `node_modules` aggiornata sul server, e solo dopo fai il rebuild. --- ## Configurazione (`lib/config.ts`) Tutte le impostazioni globali sono **flag build-time** nel file [`lib/config.ts`](lib/config.ts). Dopo ogni modifica serve ricostruire: `npm run build`. | Variabile | Default | Descrizione | |---|---|---| | `EXTERNAL_LINK_ENABLED` | `true` | Mostra il tipo di card "External Link" nel menu dell'admin. Le card di quel tipo già esistenti restano comunque visibili e cliccabili anche se messo a `false`. | | `FACTORY_PRESET_SAVE_ENABLED` | `false` | Mostra il bottone "Save as Factory Preset (dev)" nell'admin (funzione developer per ricreare/aggiornare il preset). Il bottone "Factory Reset" e lo stato del preset sono **sempre visibili** indipendentemente da questo flag — vedi [Factory Preset](#factory-preset-developer). L'endpoint API `POST /api/admin/factory-preset` resta attivo a prescindere. | | `DEFAULT_FONT` | `''` | Font di default se il portale non ne ha impostato uno. Stringa vuota = font di sistema (Arial). Altrimenti il nome esatto di un file in `data/fonts/` (es. `"Geist-Variable.woff2"`). | | `TEXT_LIMITS` | vedi sotto | Limiti caratteri di tutti i campi testuali. | | `UPLOAD_LIMITS` | vedi sotto | Dimensioni massime upload per famiglia di file. | ### Limiti testo (`TEXT_LIMITS`) ```ts card: { title: 200, shortDescription: 500, fullContent: 20_000, actionUrl: 2000, }, portal: { title: 200, welcomeText: 1000, }, ``` ### Limiti upload (`UPLOAD_LIMITS`) ```ts image: 25 MB, pdf: 20 MB, video: 1 GB, font: 5 MB, ``` Per cambiare un qualunque limite: modifica il numero in `lib/config.ts` e ricostruisci. Il valore è condiviso tra interfaccia (contatore / `maxLength`), validazione server e check di upload — un solo punto di verità. --- ## Tipi di card | Tipo | Nome in admin | Comportamento | |---|---|---| | `INFO_PAGE` | Info Page | Pagina informativa: solo cover, niente galleria. | | `IMAGE_GALLERY` | Image Gallery | Galleria di immagini/video/PDF sfogliabile a schermo intero. | | `BOOK` | Flip-Book | Sfoglialibro in formato A4 (due pagine affiancate). | | `FULLSCREEN_LOCK` | Fullscreen Lock (kiosk) | **Takeover totale**: se presente, il portale pubblico mostra SOLO il suo contenuto (immagine o video) a tutto schermo, senza hero, griglia o pulsanti di chiusura. Le altre card vengono nascoste. Utile per redirect/segnaletica kiosk. | | `EXTERNAL_LINK` | External Link | Apre un URL esterno. L'**URL è obbligatorio**: il salvataggio è bloccato (lato UI e lato server) se manca. Visibile nel menu solo se `EXTERNAL_LINK_ENABLED = true`. | **Note sulla Fullscreen Lock:** - Se ci sono più card lock, viene usata la prima per ordine di visualizzazione. - `/admin` resta sempre accessibile anche con una lock attiva. - Per tornare al portale normale: elimina (o cambia tipo a) la card lock. --- ## File consentiti negli upload Gli upload passano per tre controlli in cascata. Se uno fallisce, **nessun file viene salvato** e l'admin riceve un messaggio d'errore. ### 1. Whitelist estensioni | Famiglia | Estensioni | Limite | |---|---|---| | Immagini | `png` `jpg` `jpeg` `gif` `webp` | 25 MB | | Video | `mp4` `m4v` `webm` `mov` `ogv` `ogg` | 1 GB | | Documenti | `pdf` | 20 MB | Tutto il resto (es. `heic`, `bmp`, `avi`, `exe`) viene rifiutato. > **Eccezione SVG**: l'estensione `.svg` è ammessa **solo** per l'upload del **logo del portale** (Settings → Logo Image). Tutti gli altri upload (hero, cover card, gallery, ecc.) rifiutano l'SVG. Vedi [SVG: regole speciali per il logo](#svg-regole-speciali-per-il-logo) sotto. ### 2. Controllo "magic-bytes" Il contenuto reale del file viene confrontato con l'estensione dichiarata. Esempi rifiutati: - Un PDF rinominato `.jpg` → rifiutato (contenuto ≠ estensione). - Un eseguibile rinominato `.png` → rifiutato (tipo non riconosciuto). - Un PNG rinominato `.jpg` → rifiutato (mismatch interno alla famiglia immagini). `mov` e `mp4` sono intercambiabili (entrambi container ISO BMFF). ### 3. Transcodifica video automatica Solo i video possono essere ricodificati. Alla ricezione il server sonda i codec: - **Video già compatibile** (H.264 + AAC/MP3) → nessuna ricodifica, salvataggio immediato. - **Video non compatibile** (HEVC iPhone, VP9, AV1, audio Opus/Vorbis…) → messo in coda e ricodificato in background con `ffmpeg` verso **MP4 H.264 + AAC, max 720p**. L'admin vede un badge "Transcoding XX%" sulla miniatura; quando finisce il file diventa riproducibile su tutti i browser. > Le immagini e i PDF **non** vengono mai trasformati né compressi: sono salvati **byte-per-byte identici** al file caricato. Non esiste alcuna conversione AVIF/WebP lato server — un file PNG resta PNG con la stessa dimensione. La transcodifica richiede `ffmpeg`/`ffprobe` sul server — vedi [Prerequisiti](#prerequisiti-di-sistema). Se mancano, gli upload di video che richiedono ricodifica rispondono `503`. ### SVG: regole speciali per il logo L'SVG è un formato vettoriale comodo per i loghi ma può contenere `