Add scouthub-eventi-be
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user