Files
2026-07-25 12:09:12 +02:00

165 lines
7.6 KiB
Markdown

# scouthub-home-be
Backend Node.js/TypeScript/Express per la gestione organizzativa di Scouthub
(gruppi scout, inviti), integrato con Keycloak per l'autenticazione/gestione
delle organizzazioni. Affianca `scouthub-attivita-be` nello stesso ecosistema.
Stato: creazione gruppi, flusso di invito e gestione membri implementati
(vedi sezione Endpoint); l'integrazione reale con le Admin REST API di
Keycloak (`src/keycloak-admin/`) resta invece solo abbozzata (vedi sotto).
## Struttura
```
src/
config/ configurazione da variabili d'ambiente (dotenv)
routes/ definizione degli endpoint Express
controllers/ gestione richieste/risposte HTTP
services/ logica applicativa
keycloak-admin/ client verso le Admin REST API di Keycloak (token client-credentials + operazioni org/utenti/ruoli)
db/ client Prisma
middleware/ middleware Express (autenticazione JWT, autorizzazione per ruolo, error handling)
prisma/
schema.prisma modelli GruppoScout e Invito
```
## Prerequisiti
- Node.js 20+
- Un'istanza PostgreSQL raggiungibile (locale o remota)
- Un realm Keycloak con un client service-account per la gestione organizzazioni
## Variabili d'ambiente
Copiare `.env.example` in `.env` e valorizzare:
- `PORT` — porta di ascolto del server (default 8082 — 8081 è già usata dal
Keycloak del `docker-compose.yml` alla radice del progetto)
- `DATABASE_URL` — connection string PostgreSQL per Prisma
- `FRONTEND_BASE_URL` — URL base del frontend, usato per comporre il link di invito (`{FRONTEND_BASE_URL}/inviti/{token}`)
- `KEYCLOAK_BASE_URL` — URL base dell'istanza Keycloak
- `KEYCLOAK_REALM` — realm Keycloak usato da Scouthub
- `KEYCLOAK_ORG_SERVICE_CLIENT_ID` — client id del service account per la gestione organizzazioni
- `KEYCLOAK_ORG_SERVICE_CLIENT_SECRET` — client secret corrispondente
## Sviluppo locale
```bash
npm install
npx prisma migrate dev --name init
npm run dev
```
Il server si avvia sulla porta definita da `PORT` e risponde su `GET /health`
(verifica anche la connessione al database).
## Build
```bash
npm run build
npm start
```
## Test
```bash
npm test
```
Suite Jest, nessun database richiesto: le chiamate verso Keycloak (endpoint
token e JWKS) sono mockate con `nock`, e i test di integrazione mockano
interamente `src/keycloak-admin` e `src/db/prisma`.
## Endpoint
### `POST /gruppi`
Crea un nuovo gruppo scout: organizzazione Keycloak, un gruppo Keycloak per
ciascun ruolo di default, e la riga corrispondente in `gruppo_scout`.
- Richiede autenticazione + ruolo `admin` (temporaneo, vedi
`src/routes/gruppi.routes.ts`)
- Body: `{ nome: string, regione?: string, ruoliDefault?: string[] }`
(`ruoliDefault` di default `["Capi", "Aiuto capi", "Censiti"]`)
- Risposta `201`: `{ orgId, gruppiCreati: string[] }`
- `409` se Keycloak segnala un nome di organizzazione duplicato (nessuna riga
orfana viene creata in locale)
- In caso di errore a metà sequenza (es. fallisce la creazione di un gruppo
ruolo dopo che l'organizzazione è già stata creata), `src/services/gruppi.service.ts`
logga con `console.error` esattamente a quale step si è fermato e cosa è già
stato creato, per permettere un retry manuale mirato invece di ripartire da zero.
### Flusso di invito
- **`POST /gruppi/:orgId/inviti`** — richiede autenticazione + ruolo
`capo-gruppo`, e che `req.auth.organizationId` coincida con `:orgId` (un
capo gruppo non può invitare in un'altra organizzazione, 403 altrimenti).
Body `{ email: string, ruolo: string }`. Genera un token casuale
(`crypto.randomBytes(32)`), salva l'invito con stato `pending` e scadenza a
7 giorni, e per ora si limita a loggare in console il link
`{FRONTEND_BASE_URL}/inviti/{token}` (nessun servizio email reale).
Risposta `201`: `{ invitoId, scadenza }`.
- **`GET /inviti/:token`** — pubblico. Risponde `404` se il token non esiste,
altrimenti `{ email, nomeGruppo, ruolo, valido }` (`valido: false` se
l'invito è scaduto o già accettato), usato dal frontend per la schermata di
accettazione prima del redirect a Keycloak.
- **`POST /inviti/:token/accetta`** — richiede autenticazione (l'utente deve
essersi già autenticato/registrato su Keycloak). Verifica in ordine: invito
esistente (`404`), non già accettato (`409`), non scaduto (`410`), email del
JWT corrispondente a quella dell'invito (`403` altrimenti — impedisce che un
altro utente autenticato accetti l'invito di qualcun altro). Poi chiama in
sequenza `addMemberToOrganization``assignUserToGroup`
`assignRealmRoleToUser` e marca l'invito come `accettato`. Risposta `200`:
`{ organizationId, ruolo }`.
⚠️ `assignUserToGroup` riceve `invito.ruolo` (un nome, es. `"Capi"`) al
posto di un vero `groupId` di Keycloak: `createOrganizationGroup` non
persiste ancora una mappa ruolo → groupId, quindi questa risoluzione è
solo un placeholder (vedi `TODO` in `src/services/inviti.service.ts`).
### Gestione membri
Tutti e tre gli endpoint richiedono autenticazione + ruolo `capo-gruppo`, e
che `req.auth.organizationId` coincida con `:orgId` (403 altrimenti — un capo
gruppo non può gestire membri di un'altra organizzazione).
- **`GET /gruppi/:orgId/membri`** — Risposta `200` con un array di
`{ userId, email, ruolo, gruppoInterno }` (entrambi `null` se non
risolvibili). Per ciascun membro restituito da `listOrganizationMembers`
interroga `getUserGroupsInOrganization` e `getUserRealmRoles`, prendendo il
primo risultato di ciascuna (il modello attuale assume un solo gruppo/ruolo
scout per membro, coerente con `POST /gruppi/:orgId/inviti`).
- **`PUT /gruppi/:orgId/membri/:userId/ruolo`** — Body `{ ruolo: string }`.
Rimuove tutti i gruppi/ruoli realm correnti del membro all'interno
dell'organizzazione e assegna il nuovo gruppo/ruolo (`removeUserFromGroup`
+ `assignUserToGroup`, `removeRealmRoleFromUser` + `assignRealmRoleToUser`).
Risposta `200`: `{ userId, ruolo }`. `502` se una delle chiamate Keycloak
fallisce (loggato con `console.error`, stato potenzialmente parziale su
Keycloak da verificare manualmente).
- **`DELETE /gruppi/:orgId/membri/:userId`** — Rimuove solo la membership
dell'Organization (`removeMemberFromOrganization`), **non** elimina
l'utente da Keycloak. Risposta `204`.
## Autenticazione e autorizzazione
- `src/middleware/authenticate.ts` — estrae il Bearer token, ne valida la
firma contro le chiavi pubbliche JWKS di Keycloak (`jsonwebtoken` +
`jwks-rsa`, con caching delle chiavi) e popola `req.auth` con
`{ userId, email, organizationId, roles }` (ruoli realm + ruoli
dell'organizzazione attiva). Risponde 401 se il token manca o non è valido.
- `src/middleware/requireRole.ts``requireRole(...ruoli)` verifica che
`req.auth.roles` contenga almeno uno dei ruoli richiesti, altrimenti 403.
Va usato **dopo** `authenticate` nella catena dei middleware.
- `src/keycloak-admin/` — client verso le Admin REST API di Keycloak. Il
recupero/rinnovo del token via client-credentials (`tokenManager.ts`) è già
funzionante; tutte le operazioni usate dagli endpoint applicativi
(`createOrganization`, `createOrganizationGroup`, `addMemberToOrganization`,
`removeMemberFromOrganization`, `listOrganizationMembers`,
`assignUserToGroup`, `removeUserFromGroup`, `getUserGroupsInOrganization`,
`assignRealmRoleToUser`, `removeRealmRoleFromUser`, `getUserRealmRoles`)
sono già collegate ma il loro corpo è ancora solo un `TODO` (`throw new
Error('Not implemented')`) — vanno implementate prima che gli endpoint
funzionino contro un Keycloak reale (nei test sono sempre mockate).
`findUserByEmail` e `createUser` restano firme tipizzate non ancora usate
da nessun endpoint.