# 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.