diff --git a/README.md b/README.md
index 47881fc..36c48a0 100644
--- a/README.md
+++ b/README.md
@@ -18,8 +18,9 @@ CMS per portali captive: gestione di card informative, gallerie, flip-book e con
10. [Factory Preset (developer)](#factory-preset-developer)
11. [Font](#font)
12. [Deploy sotto sotto-percorso (basePath) dietro Apache](#deploy-sotto-sotto-percorso-basepath-dietro-apache)
-13. [Prerequisiti di sistema](#prerequisiti-di-sistema)
-14. [Risoluzione problemi](#risoluzione-problemi)
+13. [Protezione dell'amministrazione (Keycloak) e routing](#protezione-dellamministrazione-keycloak-e-routing)
+14. [Prerequisiti di sistema](#prerequisiti-di-sistema)
+15. [Risoluzione problemi](#risoluzione-problemi)
---
@@ -87,7 +88,7 @@ Per cambiare un qualunque limite: modifica il numero in `lib/config.ts` e ricost
| `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. Visibile nel menu solo se `EXTERNAL_LINK_ENABLED = true`. |
+| `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.
@@ -132,7 +133,7 @@ La transcodifica richiede `ffmpeg`/`ffprobe` sul server — vedi [Prerequisiti](
Ogni campo compilabile dall'admin ha un limite (vedi [`TEXT_LIMITS`](#limiti-testo-text_limits)). L'interfaccia mostra un contatore quando ci si avvicina al limite (rosso se superato) e blocca l'inserimento oltre il massimo. Lato server, una richiesta che sfora viene rifiutata con `400` e l'elenco dei campi fuori limite — nessun troncamento silenzioso.
-Gli URL (`actionUrl`) accettano solo gli schemi `http`, `https`, `mailto`, `tel`. Schemi come `javascript:` vengono rifiutati.
+Gli URL (`actionUrl`) accettano solo gli schemi `http`, `https`, `mailto`, `tel`. Schemi come `javascript:` vengono rifiutati. Per le card **External Link** l'URL è **obbligatorio**: il salvataggio viene rifiutato (UI + server `400`) se il campo è vuoto.
### Caratteri non-ASCII (emoji, cirillico, CJK…)
@@ -315,6 +316,67 @@ Punti chiave:
- Con `BASE_PATH = '/cards'` l'app vive **sempre** sotto `/cards`, anche **senza** proxy: in locale la raggiungi su `http://localhost:3000/cards` (la radice `/` dà 404). Il proxy serve solo a esporla pubblicamente.
- Un singolo build **non** può rispondere contemporaneamente su `/` e su `/cards`: per cambiare percorso modifica `BASE_PATH` e ricompila.
+## Protezione dell'amministrazione (Keycloak) e routing
+
+L'autorizzazione **non** è gestita dall'app Next ma da **Apache `mod_auth_openidc`** (Keycloak), già usato dal captive portal. L'app non contiene codice di auth: si fida del gate del reverse proxy.
+
+### Cosa è protetto
+- **Pagina `/cards/admin`** e **API di scrittura**: accessibili solo all'utente Keycloak con `preferred_username == admin`.
+- Chi non è autenticato o non è admin → **redirect alla home** del portale (nessun errore).
+- **Restano pubbliche** (servono al portale per tutti gli utenti WiFi): le **GET** di `/cards/api/cards` e `/cards/api/portals`, e `/cards/api/files`, `/cards/api/fonts`.
+- Protette su **tutti i metodi**: `/cards/api/upload`, `/cards/api/transcode`, `/cards/api/admin/*` (incluse le GET come backup e stato factory-preset).
+
+### Configurazione Apache
+1. Il cookie di sessione OIDC deve coprire `/cards`: nel vhost imposta `OIDCCookiePath /` (non un sotto-percorso come `/general`), altrimenti `mod_auth_openidc` non vede la sessione su `/cards`.
+2. Aggiungi nel/i vhost (dove l'OIDC è configurato) i blocchi di protezione:
+```apache
+# Pagina admin: solo utente 'admin'; non autenticato/non admin → home
+
+ AuthType openid-connect
+ OIDCUnAuthAction 401
+ Require claim preferred_username:admin
+ ErrorDocument 401 https:///cards/
+ ErrorDocument 403 https:///cards/
+
+
+# API: GET pubblica, scrittura solo admin
+
+
+ AuthType openid-connect
+ OIDCUnAuthAction 401
+ Require claim preferred_username:admin
+
+
+
+
+ AuthType openid-connect
+ OIDCUnAuthAction 401
+ Require claim preferred_username:admin
+
+
+
+# upload / transcode / admin: tutti i metodi solo admin
+
+ AuthType openid-connect
+ OIDCUnAuthAction 401
+ Require claim preferred_username:admin
+
+
+ AuthType openid-connect
+ OIDCUnAuthAction 401
+ Require claim preferred_username:admin
+
+
+ AuthType openid-connect
+ OIDCUnAuthAction 401
+ Require claim preferred_username:admin
+
+```
+Note: `OIDCUnAuthAction 401` fa sì che il non autenticato riceva `401` (poi `ErrorDocument` → home) invece di essere mandato al login. L'`ErrorDocument` di redirect è solo sulla pagina admin: sulle API un accesso non autorizzato riceve `401/403` (corretto per una chiamata API). L'admin, una volta autenticato via Keycloak come utente `admin` (tramite il normale flusso del portale), invia il cookie di sessione con le richieste e può salvare/caricare/fare backup.
+
+### Pagine inesistenti → redirect alla home
+Qualsiasi percorso non corrispondente a una pagina o API reale del progetto **non mostra una schermata d'errore**: viene reindirizzato alla home. È gestito lato Next da una rotta catch-all ([app/[...not_found]/page.tsx](app/%5B...not_found%5D/page.tsx)) che esegue `redirect('/')`. Le rotte reali (`/`, `/admin`, `/api/*`) hanno priorità; solo i path inesistenti finiscono lì. Le API inesistenti restano `404` (il redirect riguarda le pagine).
+
## Prerequisiti di sistema
Sul server servono alcuni binari di sistema (richiamati direttamente, non via npm):