Add scouthub-home-be
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
# 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-centrale` (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.
|
||||
|
||||
Reference in New Issue
Block a user