scouthub-eventi-be
Backend Node.js/TypeScript per la gestione degli eventi Scouthub, pensato per affiancare
scouthub-home-be, scouthub-attivita-be e scouthub-magazzino-be nello stesso ecosistema.
Stato attuale: schema Prisma (
Evento/RisorsaCollegata), autenticazione, CRUD eventi (POST/GET/PUT/DELETE /eventi, visteanno/mese) e route machine-to-machine (POST /eventi/:id/risorse) implementati.
Stack
- Node.js ≥ 20, TypeScript
- Express
- Prisma ORM su PostgreSQL (database dedicato
scouthub_eventi) - Autenticazione JWT via Keycloak, stesso realm di
scouthub-home-be/scouthub-attivita-be/scouthub-magazzino-be
Struttura a livelli
src/
routes/ # definizione degli endpoint Express
controllers/ # gestione request/response, delega alla service layer
services/ # business logic
repositories/ # accesso ai dati tramite Prisma
config/ # lettura e validazione delle variabili d'ambiente
db/ # istanza condivisa di PrismaClient
auth/ # verifica JWT Keycloak (utente e machine-to-machine), assertBrancaAccess
middleware/ # error handler
Flusso delle richieste: routes -> controller -> service -> repository (Prisma).
Autenticazione
src/auth/verify-token.middleware.ts(verifyToken) valida il Bearer token utente contro il JWKS del realm Keycloak (${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs) e popolareq.authconuserId(claimsub),orgId(claimorganization, stessa struttura usata dascouthub-magazzino-be/scouthub-home-be),roles(ruoli realm + ruoli sull'organizzazione attiva) ebranche(claimgroups, path normalizzati senza lo slash iniziale, es."/Lupetti"->"Lupetti").scouthub-attivita-benon espone ad oggi un claim branca dedicato: se in futuro venisse introdotto un claim piu' specifico, allineare qui.src/auth/verify-service-token.middleware.ts(verifyServiceToken) valida invece un token ottenuto via client credentials grant (client di servizio, non un utente reale): controlla che il claimazp(client_id del chiamante) sia incluso inKEYCLOAK_AUTHORIZED_SERVICE_CLIENTSe popolareq.service.clientId. Da usare al posto di (non insieme a)verifyToken, solo sulle route machine-to-machine comePOST /eventi/:id/risorse.src/auth/assert-branca-access.ts(assertBrancaAccess(user, brancaId)) lancia unHttpError403 se l'utente non appartiene alla branca indicata; chi ha ruolocapo-gruppo(sull'organizzazione attiva del token) salta il controllo.src/auth/require-org-id.middleware.ts(requireOrgId) da usare dopoverifyTokensu tutte le route utente: rifiuta la richiesta se il token non porta un'organizzazione attiva.
Endpoint
Tutte le route sotto richiedono verifyToken + requireOrgId (Bearer JWT utente con
organizzazione attiva).
POST /eventi— crea un evento per l'org/branca dell'utente corrente (assertBrancaAccess).- Body:
titolo,tipo,dataInizio,dataFine(obbligatori),descrizione,location(opzionali), e uno trabrancaId(evento di primo livello) oparentId(evento figlio). - Se
parentIdè presente: il parent deve esistere nella stessa org, non deve avere a sua volta unparentId(max 2 livelli di annidamento, altrimenti400 "profondità massima superata"), e l'intervallodataInizio/dataFinedel figlio deve essere contenuto in quello del parent (altrimenti400).orgId/brancaIddel figlio vengono presi dal parent, mai dal body.
- Body:
GET /eventi?vista=anno&anno=2026— vista di sola lettura, trasversale a tutte le branche dell'org (nessunaassertBrancaAccess): per ogni giorno dell'anno con almeno un evento radice (parentIdnullo) che lo intersechi, restituisce{ data, conteggio, eventi: [{ titolo, tipo, brancaId }] }. Un evento che copre più giorni compare su ciascuno di essi. Nessuna descrizione/risorsa collegata in questa vista (aggregato leggero per il calendario).GET /eventi?vista=mese&mese=2026-09[&brancaId=...]— vista di sola lettura, trasversale a tutte le branche salvo filtro esplicito conbrancaId: restituisce tutti gli eventi (radice e figli) la cuidataInizio/dataFineintersecano il mese richiesto, con dettaglio completo (incluso orario). L'intersezione è calcolata comedataInizio <= fineMese AND dataFine >= inizioMese, quindi un evento iniziato il mese prima e finito dentro il mese richiesto compare comunque nel risultato.GET /eventi/:id— dettaglio completo dell'evento (isolato per org, trasversale a tutte le branche), con l'arrayfigli(eventi di 2° livello) erisorseCollegate.PUT /eventi/:id— aggiorna un evento a cui l'utente ha accesso di branca (assertBrancaAccess). Se si modificanodataInizio/dataFine: se l'evento è un figlio, il nuovo intervallo deve restare contenuto in quello del parent; se l'evento ha figli, il nuovo intervallo deve continuare a contenerli tutti (altrimenti400in entrambi i casi).DELETE /eventi/:id— elimina un evento a cui l'utente ha accesso di branca (assertBrancaAccess). Figli e risorse collegate vengono rimossi automaticamente dal vincoloON DELETE CASCADEa livello di database, non da logica applicativa.
Machine-to-machine
POST /eventi/:id/risorse— protetta daverifyServiceToken(non daverifyToken): chiamata da altri backend Scouthub con le proprie credenziali di servizio, non da un utente. Body:tipoRisorsa,risorsaId,servizioOrigine(obbligatori),metadata(opzionale, JSON libero). Verifica solo che l'evento esista (404 altrimenti, nessun controllo di org/branca: una chiamata di servizio non è legata a un'organizzazione utente) e crea laRisorsaCollegata. Nessun endpoint di lettura dedicato su questa tabella: la lettura per l'utente finale passa daGET /eventi/:id(camporisorseCollegate).
Variabili d'ambiente
Vedi .env.example. Copiarlo in .env e valorizzare:
| Variabile | Descrizione |
|---|---|
PORT |
Porta HTTP del servizio (default 8084) |
DATABASE_URL |
Connection string Postgres (schema/database scouthub_eventi) |
KEYCLOAK_BASE_URL |
Base URL del server Keycloak |
KEYCLOAK_REALM |
Realm Keycloak (scouthub) |
KEYCLOAK_EVENTI_CLIENT_ID |
Client Keycloak dedicato a questo servizio |
KEYCLOAK_EVENTI_CLIENT_SECRET |
Secret del client sopra |
KEYCLOAK_AUTHORIZED_SERVICE_CLIENTS |
Client_id (claim azp), separati da virgola, autorizzati sulle route machine-to-machine |
Avvio in locale
npm install
npx prisma generate
npx prisma migrate deploy # applica le migration sul database scouthub_eventi
npm run dev # avvia con ts-node-dev su http://localhost:8084
Build e avvio in produzione
npm run build
npm start
Healthcheck
GET /health
Risponde { "status": "ok", "database": "up" } se il servizio e la connessione al database sono
funzionanti.
Test
npm test
tests/integration/eventi.endpoint.test.ts— test end-to-end sull'app Express con Prisma mockato (jest.mock('../../src/db/prisma')) e JWT firmati al volo, verificati contro un JWKS fittizio (nock): profondità massima di annidamento, contenimento date figlio/parent, isolamento branca (incl. bypasscapo-gruppo), intersezione mensile, machine-to-machine (client autorizzato, client non in whitelist, nessun token, token utente su route di servizio), 401/403/404.tests/integration/evento-cascade.db.test.ts— unico test che usa una connessione Prisma reale (non mockata): verifica che eliminare un evento padre elimini a cascata, a livello di database, i suoi figli e le risorse collegate (vincoloON DELETE CASCADEdella migration). Richiede un Postgres raggiungibile con lo schema già migrato (di default punta al database di sviluppo localescouthub_eventi); fallisce se il database non è raggiungibile.