165 lines
7.6 KiB
Markdown
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.
|
|
|