Sistemato home e keycloak
This commit is contained in:
@@ -7,6 +7,7 @@ import { membriRouter } from './routes/membri.routes';
|
||||
import { linkIngressoRouter } from './routes/linkIngresso.routes';
|
||||
import { richiesteIngressoRouter } from './routes/richiesteIngresso.routes';
|
||||
import { richiesteCreazioneGruppoRouter } from './routes/richiesteCreazioneGruppo.routes';
|
||||
import { utenteRouter } from './routes/utente.routes';
|
||||
import { errorHandler } from './middleware/errorHandler';
|
||||
|
||||
export const app = express();
|
||||
@@ -21,6 +22,7 @@ app.use(membriRouter);
|
||||
app.use(linkIngressoRouter);
|
||||
app.use(richiesteIngressoRouter);
|
||||
app.use(richiesteCreazioneGruppoRouter);
|
||||
app.use(utenteRouter);
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ message: 'not found' });
|
||||
|
||||
@@ -34,7 +34,7 @@ function parseBody(body: PostGruppoBody): { nome: string; regione?: string; ruol
|
||||
export async function postGruppo(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const input = parseBody(req.body ?? {});
|
||||
const result = await createGruppo({ ...input, userId: req.auth!.userId });
|
||||
const result = await createGruppo(input);
|
||||
res.status(201).json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { getProfiloUtente, aggiornaProfiloUtente, cambiaPasswordUtente } from '../services/utente.service';
|
||||
import { HttpError } from '../errors';
|
||||
|
||||
interface PutProfiloBody {
|
||||
email?: unknown;
|
||||
nome?: unknown;
|
||||
cognome?: unknown;
|
||||
}
|
||||
|
||||
function parseProfiloBody(body: PutProfiloBody): { email: string; nome: string; cognome: string } {
|
||||
if (typeof body.email !== 'string' || body.email.trim().length === 0) {
|
||||
throw new HttpError(400, "Il campo 'email' è obbligatorio ed è una stringa non vuota");
|
||||
}
|
||||
if (typeof body.nome !== 'string' || body.nome.trim().length === 0) {
|
||||
throw new HttpError(400, "Il campo 'nome' è obbligatorio ed è una stringa non vuota");
|
||||
}
|
||||
if (typeof body.cognome !== 'string' || body.cognome.trim().length === 0) {
|
||||
throw new HttpError(400, "Il campo 'cognome' è obbligatorio ed è una stringa non vuota");
|
||||
}
|
||||
return { email: body.email.trim(), nome: body.nome.trim(), cognome: body.cognome.trim() };
|
||||
}
|
||||
|
||||
export async function getMe(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const profilo = await getProfiloUtente(req.auth!.userId);
|
||||
res.json(profilo);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function putMe(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const input = parseProfiloBody(req.body ?? {});
|
||||
await aggiornaProfiloUtente(req.auth!.userId, input);
|
||||
res.status(200).json(input);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function putMePassword(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const { password } = req.body ?? {};
|
||||
if (typeof password !== 'string' || password.length < 8) {
|
||||
throw new HttpError(400, "Il campo 'password' è obbligatorio e deve avere almeno 8 caratteri");
|
||||
}
|
||||
|
||||
await cambiaPasswordUtente(req.auth!.userId, password);
|
||||
res.status(204).send();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@ export {
|
||||
} from './organizations';
|
||||
export { assignUserToGroup, removeUserFromGroup, getUserGroupsInOrganization } from './groups';
|
||||
export { assignRealmRoleToUser, removeRealmRoleFromUser, getUserRealmRoles } from './roles';
|
||||
export { findUserByEmail } from './users';
|
||||
export { findUserByEmail, getUserById, updateUserProfile, resetUserPassword } from './users';
|
||||
export type { KeycloakUserProfile, UpdateUserProfileInput } from './users';
|
||||
|
||||
@@ -8,3 +8,52 @@ export async function findUserByEmail(email: string): Promise<KeycloakUserSummar
|
||||
const [utente] = response.data;
|
||||
return utente ? { id: utente.id } : null;
|
||||
}
|
||||
|
||||
export interface KeycloakUserProfile {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
|
||||
export async function getUserById(userId: string): Promise<KeycloakUserProfile> {
|
||||
const response = await keycloakAdminHttp.get<{
|
||||
id: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}>(`/users/${userId}`);
|
||||
|
||||
return {
|
||||
id: response.data.id,
|
||||
email: response.data.email ?? '',
|
||||
firstName: response.data.firstName ?? '',
|
||||
lastName: response.data.lastName ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateUserProfileInput {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
|
||||
// Il realm ha `registrationEmailAsUsername: true`, quindi username ed email
|
||||
// devono restare sincronizzati: aggiornare l'email senza lo username
|
||||
// lascerebbe l'utente con un login (username) diverso dalla nuova email.
|
||||
export async function updateUserProfile(userId: string, input: UpdateUserProfileInput): Promise<void> {
|
||||
await keycloakAdminHttp.put(`/users/${userId}`, {
|
||||
email: input.email,
|
||||
username: input.email,
|
||||
firstName: input.firstName,
|
||||
lastName: input.lastName,
|
||||
});
|
||||
}
|
||||
|
||||
export async function resetUserPassword(userId: string, password: string): Promise<void> {
|
||||
await keycloakAdminHttp.put(`/users/${userId}/reset-password`, {
|
||||
type: 'password',
|
||||
value: password,
|
||||
temporary: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '../middleware/authenticate';
|
||||
import { getMe, putMe, putMePassword } from '../controllers/utente.controller';
|
||||
|
||||
export const utenteRouter = Router();
|
||||
|
||||
// Nessun controllo di ruolo/org: l'utente autenticato può leggere e
|
||||
// modificare solo il proprio profilo (userId preso da req.auth, mai da input client).
|
||||
utenteRouter.get('/me', authenticate, getMe);
|
||||
utenteRouter.put('/me', authenticate, putMe);
|
||||
utenteRouter.put('/me/password', authenticate, putMePassword);
|
||||
@@ -1,13 +1,12 @@
|
||||
import axios from 'axios';
|
||||
import { prisma } from '../db/prisma';
|
||||
import { createOrganization, createOrganizationGroup, addMemberToOrganization } from '../keycloak-admin';
|
||||
import { createOrganization, createOrganizationGroup } from '../keycloak-admin';
|
||||
import { HttpError } from '../errors';
|
||||
|
||||
export const RUOLI_DEFAULT = ['Capi', 'Aiuto capi', 'Censiti'];
|
||||
|
||||
export interface CreateGruppoInput {
|
||||
nome: string;
|
||||
userId: string;
|
||||
regione?: string;
|
||||
ruoliDefault?: string[];
|
||||
}
|
||||
@@ -87,31 +86,20 @@ export async function createGruppo(input: CreateGruppoInput): Promise<CreateGrup
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: il creatore diventa membro dell'organizzazione appena creata.
|
||||
// Senza questo passaggio il claim "organization" non comparirebbe mai nel
|
||||
// suo token, e la auth guard del FE lo rimanderebbe sempre su "crea gruppo"
|
||||
// anche a creazione riuscita.
|
||||
try {
|
||||
await addMemberToOrganization(orgId, input.userId);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[gruppi] STEP 3 (addMemberToOrganization) fallito per orgId="${orgId}", userId="${input.userId}". ` +
|
||||
`Organizzazione e gruppi [${gruppiCreati.join(', ')}] già creati su Keycloak: per il retry manuale ` +
|
||||
`non richiamare le API di creazione, ma solo l'aggiunta del membro con orgId="${orgId}".`,
|
||||
err,
|
||||
);
|
||||
throw new HttpError(502, "Impossibile aggiungere l'utente all'organizzazione su Keycloak");
|
||||
}
|
||||
|
||||
// Step 4: riga locale in gruppo_scout.
|
||||
// Step 3: riga locale in gruppo_scout.
|
||||
// Nota: chi chiama createGruppo (admin via POST /gruppi, oppure l'approvazione
|
||||
// di una richiesta di creazione gruppo) NON diventa automaticamente membro
|
||||
// dell'organizzazione qui: l'admin che crea un gruppo per conto di altri non deve
|
||||
// entrarne a far parte, mentre il richiedente che diventa capo-gruppo viene reso
|
||||
// membro esplicitamente dal chiamante (vedi richiesteCreazioneGruppo.service.ts).
|
||||
try {
|
||||
await prisma.gruppoScout.create({
|
||||
data: { orgId, nome: input.nome, regione: input.regione },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[gruppi] STEP 4 (salvataggio locale gruppo_scout) fallito per orgId="${orgId}", nome="${input.nome}". ` +
|
||||
`Organizzazione, gruppi [${gruppiCreati.join(', ')}] e membership già creati su Keycloak: per il ` +
|
||||
`[gruppi] STEP 3 (salvataggio locale gruppo_scout) fallito per orgId="${orgId}", nome="${input.nome}". ` +
|
||||
`Organizzazione e gruppi [${gruppiCreati.join(', ')}] già creati su Keycloak: per il ` +
|
||||
`retry manuale non richiamare le API Keycloak, ma solo il salvataggio locale con orgId="${orgId}".`,
|
||||
err,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { prisma } from '../db/prisma';
|
||||
import { HttpError } from '../errors';
|
||||
import { createGruppo } from './gruppi.service';
|
||||
import { addMemberToOrganization } from '../keycloak-admin';
|
||||
import { assegnaGruppoERuolo } from './assegnazioneGruppoRuolo';
|
||||
|
||||
const STATO_RICHIESTA = {
|
||||
@@ -91,28 +92,31 @@ async function trovaRichiestaPending(id: string) {
|
||||
export async function approvaRichiesta(id: string): Promise<RisolviRichiestaResult> {
|
||||
const richiesta = await trovaRichiestaPending(id);
|
||||
|
||||
// Step 1-4: stessa logica di POST /gruppi (organizzazione, gruppi ruolo,
|
||||
// membership del richiedente, riga locale). In caso di fallimento
|
||||
// createGruppo lancia già il proprio HttpError con il dettaglio dello step,
|
||||
// e la richiesta resta 'PENDING' perché non viene ancora aggiornata qui.
|
||||
// Step 1-3: stessa logica di POST /gruppi (organizzazione, gruppi ruolo,
|
||||
// riga locale). In caso di fallimento createGruppo lancia già il proprio
|
||||
// HttpError con il dettaglio dello step, e la richiesta resta 'PENDING'
|
||||
// perché non viene ancora aggiornata qui. A differenza di POST /gruppi
|
||||
// (dove l'admin crea il gruppo senza entrarne a far parte), qui il
|
||||
// richiedente diventa membro dell'organizzazione e capo-gruppo.
|
||||
const { orgId } = await createGruppo({
|
||||
nome: richiesta.nomeProposto,
|
||||
regione: richiesta.regione ?? undefined,
|
||||
userId: richiesta.userId,
|
||||
});
|
||||
|
||||
// Step 5: il richiedente diventa capo-gruppo del gruppo appena creato.
|
||||
// Step 4: il richiedente diventa membro dell'organizzazione e capo-gruppo
|
||||
// del gruppo appena creato.
|
||||
try {
|
||||
await addMemberToOrganization(orgId, richiesta.userId);
|
||||
await assegnaGruppoERuolo(richiesta.userId, RUOLO_CAPO_GRUPPO);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[richieste-creazione-gruppo] STEP 5 (assegnazione ruolo "${RUOLO_CAPO_GRUPPO}") fallito per ` +
|
||||
`[richieste-creazione-gruppo] STEP 4 (membership e ruolo "${RUOLO_CAPO_GRUPPO}") fallito per ` +
|
||||
`richiestaId="${id}", orgId="${orgId}", userId="${richiesta.userId}". Il gruppo scout è già stato ` +
|
||||
'creato su Keycloak e localmente: per il retry manuale non richiamare createGruppo, ma solo ' +
|
||||
`l'assegnazione del ruolo "${RUOLO_CAPO_GRUPPO}" all'utente userId="${richiesta.userId}".`,
|
||||
`l'aggiunta a membro e l'assegnazione del ruolo "${RUOLO_CAPO_GRUPPO}" all'utente userId="${richiesta.userId}".`,
|
||||
err,
|
||||
);
|
||||
throw new HttpError(502, "Impossibile completare l'assegnazione del ruolo capo-gruppo su Keycloak");
|
||||
throw new HttpError(502, "Impossibile completare l'assegnazione a membro/capo-gruppo su Keycloak");
|
||||
}
|
||||
|
||||
const aggiornata = await prisma.richiestaCreazioneGruppo.update({
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import axios from 'axios';
|
||||
import { getUserById, updateUserProfile, resetUserPassword } from '../keycloak-admin';
|
||||
import { HttpError } from '../errors';
|
||||
|
||||
export interface ProfiloUtente {
|
||||
email: string;
|
||||
nome: string;
|
||||
cognome: string;
|
||||
}
|
||||
|
||||
export async function getProfiloUtente(userId: string): Promise<ProfiloUtente> {
|
||||
const utente = await getUserById(userId);
|
||||
return { email: utente.email, nome: utente.firstName, cognome: utente.lastName };
|
||||
}
|
||||
|
||||
export interface AggiornaProfiloInput {
|
||||
email: string;
|
||||
nome: string;
|
||||
cognome: string;
|
||||
}
|
||||
|
||||
export async function aggiornaProfiloUtente(userId: string, input: AggiornaProfiloInput): Promise<void> {
|
||||
try {
|
||||
await updateUserProfile(userId, { email: input.email, firstName: input.nome, lastName: input.cognome });
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err) && err.response?.status === 409) {
|
||||
throw new HttpError(409, 'Esiste già un utente registrato con questa email');
|
||||
}
|
||||
console.error(`[utente] aggiornamento profilo fallito per userId="${userId}".`, err);
|
||||
throw new HttpError(502, "Impossibile aggiornare il profilo su Keycloak");
|
||||
}
|
||||
}
|
||||
|
||||
export async function cambiaPasswordUtente(userId: string, nuovaPassword: string): Promise<void> {
|
||||
try {
|
||||
await resetUserPassword(userId, nuovaPassword);
|
||||
} catch (err) {
|
||||
console.error(`[utente] cambio password fallito per userId="${userId}".`, err);
|
||||
throw new HttpError(502, 'Impossibile aggiornare la password su Keycloak');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user