Files
2026-07-25 11:59:25 +02:00
..
2026-07-25 11:59:25 +02:00
2026-07-25 11:59:25 +02:00
2026-07-25 11:59:25 +02:00
2026-07-25 11:59:25 +02:00
2026-07-25 11:59:25 +02:00
2026-07-25 11:59:25 +02:00
2026-07-25 11:59:25 +02:00
2026-07-25 11:59:25 +02:00
2026-07-25 11:59:25 +02:00
2026-07-25 11:59:25 +02:00
2026-07-25 11:59:25 +02:00
2026-07-25 11:59:25 +02:00

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, viste anno/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 popola req.auth con userId (claim sub), orgId (claim organization, stessa struttura usata da scouthub-magazzino-be/scouthub-home-be), roles (ruoli realm + ruoli sull'organizzazione attiva) e branche (claim groups, path normalizzati senza lo slash iniziale, es. "/Lupetti" -> "Lupetti"). scouthub-attivita-be non 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 claim azp (client_id del chiamante) sia incluso in KEYCLOAK_AUTHORIZED_SERVICE_CLIENTS e popola req.service.clientId. Da usare al posto di (non insieme a) verifyToken, solo sulle route machine-to-machine come POST /eventi/:id/risorse.
  • src/auth/assert-branca-access.ts (assertBrancaAccess(user, brancaId)) lancia un HttpError 403 se l'utente non appartiene alla branca indicata; chi ha ruolo capo-gruppo (sull'organizzazione attiva del token) salta il controllo.
  • src/auth/require-org-id.middleware.ts (requireOrgId) da usare dopo verifyToken su 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 tra brancaId (evento di primo livello) o parentId (evento figlio).
    • Se parentId è presente: il parent deve esistere nella stessa org, non deve avere a sua volta un parentId (max 2 livelli di annidamento, altrimenti 400 "profondità massima superata"), e l'intervallo dataInizio/dataFine del figlio deve essere contenuto in quello del parent (altrimenti 400). orgId/brancaId del figlio vengono presi dal parent, mai dal body.
  • GET /eventi?vista=anno&anno=2026 — vista di sola lettura, trasversale a tutte le branche dell'org (nessuna assertBrancaAccess): per ogni giorno dell'anno con almeno un evento radice (parentId nullo) 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 con brancaId: restituisce tutti gli eventi (radice e figli) la cui dataInizio/dataFine intersecano il mese richiesto, con dettaglio completo (incluso orario). L'intersezione è calcolata come dataInizio <= 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'array figli (eventi di 2° livello) e risorseCollegate.
  • PUT /eventi/:id — aggiorna un evento a cui l'utente ha accesso di branca (assertBrancaAccess). Se si modificano dataInizio/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 (altrimenti 400 in 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 vincolo ON DELETE CASCADE a livello di database, non da logica applicativa.

Machine-to-machine

  • POST /eventi/:id/risorse — protetta da verifyServiceToken (non da verifyToken): 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 la RisorsaCollegata. Nessun endpoint di lettura dedicato su questa tabella: la lettura per l'utente finale passa da GET /eventi/:id (campo risorseCollegate).

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. bypass capo-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.tsunico 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 (vincolo ON DELETE CASCADE della migration). Richiede un Postgres raggiungibile con lo schema già migrato (di default punta al database di sviluppo locale scouthub_eventi); fallisce se il database non è raggiungibile.