Add scouthub-eventi-be

This commit is contained in:
Lorenzo Sanesi
2026-07-25 11:59:25 +02:00
parent 9ceb05cda2
commit 7e0544c5ab
36 changed files with 7776 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
# 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
```bash
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
```bash
npm run build
npm start
```
## Healthcheck
```
GET /health
```
Risponde `{ "status": "ok", "database": "up" }` se il servizio e la connessione al database sono
funzionanti.
## Test
```bash
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.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 (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.