modifica login e inserimento creazione nuovo utente
All checks were successful
Deploy to VPS / build (push) Successful in 17s
Deploy to VPS / deploy (push) Successful in 23s

This commit is contained in:
AV77web 2026-07-14 16:03:16 +02:00
parent 79cfa791d2
commit 03835bf426
6 changed files with 304 additions and 112 deletions

View file

@ -1,10 +1,6 @@
import type { ExpressAuthConfig } from "@auth/express"; import type { ExpressAuthConfig } from "@auth/express";
import Credentials from "@auth/express/providers/credentials"; import Credentials from "@auth/express/providers/credentials";
import bcrypt from "bcryptjs"; import { authenticateUser } from "./credentials.js";
import type { RowDataPacket } from "mysql2";
import pool from "../db/pool.js";
import type { Utente } from "../types/index.js";
import { validateLoginInput } from "../validation/utente.js";
const isProduction = process.env.NODE_ENV === "production"; const isProduction = process.env.NODE_ENV === "production";
@ -36,28 +32,18 @@ export const authConfig: ExpressAuthConfig = {
password: { label: "Password", type: "password" }, password: { label: "Password", type: "password" },
}, },
async authorize(credentials) { async authorize(credentials) {
const validation = validateLoginInput(credentials ?? {}); const email = credentials?.email?.toString().trim().toLowerCase() ?? "";
const password = credentials?.password?.toString() ?? "";
if (!email || !password) return null;
if (!validation.success) return null; const user = await authenticateUser(email, password);
if (!user) return null;
const { email, password } = validation.data;
const [rows] = await pool.execute<RowDataPacket[]>(
"SELECT UtenteID, Nome, Cognome, Email, PasswordHash, Ruolo FROM utente WHERE Email = ?",
[email]
);
const utente = rows[0] as Utente | undefined;
if (!utente) return null;
const valid = await bcrypt.compare(password, utente.PasswordHash);
if (!valid) return null;
return { return {
id: String(utente.UtenteID), id: String(user.id),
email: utente.Email, email: user.email,
name: `${utente.Nome} ${utente.Cognome}`, name: `${user.nome} ${user.cognome}`,
ruolo: utente.Ruolo, ruolo: user.ruolo,
}; };
}, },
}), }),

45
src/auth/credentials.ts Normal file
View file

@ -0,0 +1,45 @@
import bcrypt from "bcryptjs";
import type { RowDataPacket } from "mysql2";
import pool from "../db/pool.js";
import type { Ruolo } from "../types/index.js";
interface UtenteAuthRow extends RowDataPacket {
UtenteID: number;
Nome: string;
Cognome: string;
Email: string;
PasswordHash: string;
Ruolo: Ruolo;
}
export interface AuthenticatedUser {
id: number;
nome: string;
cognome: string;
email: string;
ruolo: Ruolo;
}
export async function authenticateUser(
email: string,
password: string
): Promise<AuthenticatedUser | null> {
const [rows] = await pool.execute<UtenteAuthRow[]>(
"SELECT UtenteID, Nome, Cognome, Email, PasswordHash, Ruolo FROM utente WHERE Email = ?",
[email]
);
const utente = rows[0];
if (!utente) return null;
const valid = await bcrypt.compare(password, utente.PasswordHash);
if (!valid) return null;
return {
id: utente.UtenteID,
nome: utente.Nome,
cognome: utente.Cognome,
email: utente.Email,
ruolo: utente.Ruolo,
};
}

37
src/auth/session.ts Normal file
View file

@ -0,0 +1,37 @@
import { encode } from "@auth/core/jwt";
import type { Response } from "express";
import type { AuthenticatedUser } from "./credentials.js";
function getSessionCookieName(): string {
return process.env.NODE_ENV === "production"
? "__Secure-authjs.session-token"
: "authjs.session-token";
}
export async function setAuthSession(
res: Response,
user: AuthenticatedUser
): Promise<void> {
const cookieName = getSessionCookieName();
const isProduction = process.env.NODE_ENV === "production";
const token = await encode({
token: {
name: `${user.nome} ${user.cognome}`,
email: user.email,
sub: String(user.id),
id: String(user.id),
ruolo: user.ruolo,
},
secret: process.env.AUTH_SECRET!,
salt: cookieName,
maxAge: 30 * 24 * 60 * 60,
});
res.cookie(cookieName, token, {
httpOnly: true,
secure: isProduction,
sameSite: isProduction ? "none" : "lax",
path: "/",
});
}

View file

@ -343,6 +343,63 @@ export const openApiSpec = {
}, },
}, },
}, },
"/api/utenti/login": {
post: {
tags: ["Utenti"],
summary: "Login utente",
description:
"Autentica l'utente con email e password. Imposta il cookie di sessione.",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["email", "password"],
properties: {
email: { type: "string", format: "email" },
password: { type: "string" },
},
},
},
},
},
responses: {
200: {
description: "Login riuscito",
content: { "application/json": { schema: utenteSchema } },
},
400: responses.validationError,
401: { description: "Credenziali non valide" },
},
},
},
"/api/utenti/register": {
post: {
tags: ["Utenti"],
summary: "Registra utente",
description:
"Crea un nuovo utente (Dipendente o Referente Academy). Richiede autenticazione Referente Academy.",
security: [{ cookieAuth: [] }],
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/UtenteCreate" },
},
},
},
responses: {
201: {
description: "Utente registrato",
content: { "application/json": { schema: utenteSchema } },
},
400: responses.validationError,
403: responses.forbidden,
409: { description: "Email già registrata" },
},
},
},
"/api/utenti/{id}": { "/api/utenti/{id}": {
get: { get: {
tags: ["Utenti"], tags: ["Utenti"],

View file

@ -7,6 +7,8 @@
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { Router } from "express"; import { Router } from "express";
import type { ResultSetHeader, RowDataPacket } from "mysql2"; import type { ResultSetHeader, RowDataPacket } from "mysql2";
import { authenticateUser } from "../auth/credentials.js";
import { setAuthSession } from "../auth/session.js";
import pool from "../db/pool.js"; import pool from "../db/pool.js";
import { mapDbError } from "../errors/db.js"; import { mapDbError } from "../errors/db.js";
import { requireAuth, requireReferenteAcademy } from "../middleware/auth.js"; import { requireAuth, requireReferenteAcademy } from "../middleware/auth.js";
@ -14,8 +16,10 @@ import type { Ruolo } from "../types/index.js";
import { UTENTE_CONSTRAINTS } from "../validation/constraints.js"; import { UTENTE_CONSTRAINTS } from "../validation/constraints.js";
import { validateBody } from "../validation/middleware.js"; import { validateBody } from "../validation/middleware.js";
import { import {
validateLoginInput,
validateUtenteCreate, validateUtenteCreate,
validateUtenteUpdate, validateUtenteUpdate,
type LoginInput,
type UtenteCreateInput, type UtenteCreateInput,
type UtenteUpdateInput, type UtenteUpdateInput,
} from "../validation/utente.js"; } from "../validation/utente.js";
@ -48,6 +52,127 @@ async function findUtenteById(id: number): Promise<UtenteRow | undefined> {
return rows[0]; return rows[0];
} }
async function insertUtente(body: UtenteCreateInput): Promise<UtenteRow> {
const passwordHash = await bcrypt.hash(body.password, 10);
const [result] = await pool.execute<ResultSetHeader>(
"INSERT INTO utente (Nome, Cognome, Email, PasswordHash, Ruolo) VALUES (?, ?, ?, ?, ?)",
[body.nome, body.cognome, body.email, passwordHash, body.ruolo]
);
const created = await findUtenteById(result.insertId);
if (!created) {
throw new Error("Errore durante la creazione dell'utente");
}
return created;
}
async function applyUtenteUpdate(
id: number,
updates: UtenteUpdateInput
): Promise<UtenteRow> {
const setClauses: string[] = [];
const values: (string | number)[] = [];
if (updates.nome !== undefined) {
setClauses.push("Nome = ?");
values.push(updates.nome);
}
if (updates.cognome !== undefined) {
setClauses.push("Cognome = ?");
values.push(updates.cognome);
}
if (updates.email !== undefined) {
setClauses.push("Email = ?");
values.push(updates.email);
}
if (updates.ruolo !== undefined) {
setClauses.push("Ruolo = ?");
values.push(updates.ruolo);
}
if (updates.password) {
const passwordHash = await bcrypt.hash(updates.password, 10);
setClauses.push("PasswordHash = ?");
values.push(passwordHash);
}
await pool.execute<ResultSetHeader>(
`UPDATE utente SET ${setClauses.join(", ")} WHERE UtenteID = ?`,
[...values, id]
);
const updated = await findUtenteById(id);
if (!updated) {
throw new Error("Errore durante l'aggiornamento dell'utente");
}
return updated;
}
function handleUtenteDbError(
res: import("express").Response,
error: unknown
): boolean {
const mapped = mapDbError(error);
if (mapped) {
res.status(mapped.status).json({
error: mapped.error,
fields: mapped.fields,
});
return true;
}
return false;
}
async function createUtenteHandler(
body: UtenteCreateInput,
res: import("express").Response
): Promise<void> {
try {
const created = await insertUtente(body);
res.status(201).json(toUtenteResponse(created));
} catch (error) {
if (handleUtenteDbError(res, error)) return;
if (error instanceof Error && error.message.includes("creazione")) {
res.status(500).json({ error: error.message });
return;
}
throw error;
}
}
router.post("/login", validateBody(validateLoginInput), async (_req, res) => {
const { email, password } = res.locals.validatedBody as LoginInput;
const user = await authenticateUser(email, password);
if (!user) {
res.status(401).json({ error: "Credenziali non valide" });
return;
}
await setAuthSession(res, user);
res.json({
id: user.id,
nome: user.nome,
cognome: user.cognome,
email: user.email,
ruolo: user.ruolo,
});
});
router.post(
"/register",
requireAuth,
requireReferenteAcademy,
validateBody(validateUtenteCreate),
async (_req, res) => {
await createUtenteHandler(
res.locals.validatedBody as UtenteCreateInput,
res
);
}
);
router.use(requireAuth, requireReferenteAcademy); router.use(requireAuth, requireReferenteAcademy);
router.get("/", async (req, res) => { router.get("/", async (req, res) => {
@ -90,33 +215,10 @@ router.get("/:id", async (req, res) => {
}); });
router.post("/", validateBody(validateUtenteCreate), async (_req, res) => { router.post("/", validateBody(validateUtenteCreate), async (_req, res) => {
const body = res.locals.validatedBody as UtenteCreateInput; await createUtenteHandler(
res.locals.validatedBody as UtenteCreateInput,
try { res
const passwordHash = await bcrypt.hash(body.password, 10);
const [result] = await pool.execute<ResultSetHeader>(
"INSERT INTO utente (Nome, Cognome, Email, PasswordHash, Ruolo) VALUES (?, ?, ?, ?, ?)",
[body.nome, body.cognome, body.email, passwordHash, body.ruolo]
); );
const created = await findUtenteById(result.insertId);
if (!created) {
res.status(500).json({ error: "Errore durante la creazione dell'utente" });
return;
}
res.status(201).json(toUtenteResponse(created));
} catch (error) {
const mapped = mapDbError(error);
if (mapped) {
res.status(mapped.status).json({
error: mapped.error,
fields: mapped.fields,
});
return;
}
throw error;
}
}); });
router.put("/:id", validateBody(validateUtenteUpdate), async (req, res) => { router.put("/:id", validateBody(validateUtenteUpdate), async (req, res) => {
@ -133,51 +235,14 @@ router.put("/:id", validateBody(validateUtenteUpdate), async (req, res) => {
} }
const updates = res.locals.validatedBody as UtenteUpdateInput; const updates = res.locals.validatedBody as UtenteUpdateInput;
const setClauses: string[] = [];
const values: (string | number)[] = [];
if (updates.nome !== undefined) {
setClauses.push("Nome = ?");
values.push(updates.nome);
}
if (updates.cognome !== undefined) {
setClauses.push("Cognome = ?");
values.push(updates.cognome);
}
if (updates.email !== undefined) {
setClauses.push("Email = ?");
values.push(updates.email);
}
if (updates.ruolo !== undefined) {
setClauses.push("Ruolo = ?");
values.push(updates.ruolo);
}
if (updates.password) {
const passwordHash = await bcrypt.hash(updates.password, 10);
setClauses.push("PasswordHash = ?");
values.push(passwordHash);
}
try { try {
await pool.execute<ResultSetHeader>( const updated = await applyUtenteUpdate(id, updates);
`UPDATE utente SET ${setClauses.join(", ")} WHERE UtenteID = ?`,
[...values, id]
);
const updated = await findUtenteById(id);
if (!updated) {
res.status(500).json({ error: "Errore durante l'aggiornamento dell'utente" });
return;
}
res.json(toUtenteResponse(updated)); res.json(toUtenteResponse(updated));
} catch (error) { } catch (error) {
const mapped = mapDbError(error); if (handleUtenteDbError(res, error)) return;
if (mapped) { if (error instanceof Error && error.message.includes("aggiornamento")) {
res.status(mapped.status).json({ res.status(500).json({ error: error.message });
error: mapped.error,
fields: mapped.fields,
});
return; return;
} }
throw error; throw error;

View file

@ -62,8 +62,8 @@ export function validateLoginInput(
return buildResult(data, errors); return buildResult(data, errors);
} }
function validateUtenteProfileFields( function validateNomeCognomeEmail(
data: UtenteCreateInput, data: Pick<UtenteCreateInput, "nome" | "cognome" | "email">,
errors: FieldErrors errors: FieldErrors
): void { ): void {
validateRequired(errors, "nome", data.nome, "Nome obbligatorio"); validateRequired(errors, "nome", data.nome, "Nome obbligatorio");
@ -91,23 +91,39 @@ function validateUtenteProfileFields(
"Email" "Email"
); );
validateEmailFormat(errors, "email", data.email); validateEmailFormat(errors, "email", data.email);
}
validateRequired(errors, "password", data.password, "Password obbligatoria"); function validatePasswordField(
errors: FieldErrors,
password: string,
options: { required: boolean }
): void {
if (options.required) {
validateRequired(errors, "password", password, "Password obbligatoria");
}
if (!password) return;
validateMinLength( validateMinLength(
errors, errors,
"password", "password",
data.password, password,
UTENTE_CONSTRAINTS.password.minLength, UTENTE_CONSTRAINTS.password.minLength,
"Password" "Password"
); );
validateMaxLength( validateMaxLength(
errors, errors,
"password", "password",
data.password, password,
UTENTE_CONSTRAINTS.password.maxLength, UTENTE_CONSTRAINTS.password.maxLength,
"Password" "Password"
); );
}
function validateUtenteProfileFields(
data: UtenteCreateInput,
errors: FieldErrors
): void {
validateNomeCognomeEmail(data, errors);
validatePasswordField(errors, data.password, { required: true });
validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio"); validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio");
validateEnum( validateEnum(
errors, errors,
@ -135,7 +151,6 @@ export function validateUtenteCreate(
return buildResult(data, errors); return buildResult(data, errors);
} }
export function validateUtenteUpdate( export function validateUtenteUpdate(
raw: Record<string, unknown> raw: Record<string, unknown>
): ValidationResult<UtenteUpdateInput> { ): ValidationResult<UtenteUpdateInput> {
@ -183,20 +198,7 @@ export function validateUtenteUpdate(
const password = raw.password?.toString() ?? ""; const password = raw.password?.toString() ?? "";
if (password) { if (password) {
data.password = password; data.password = password;
validateMinLength( validatePasswordField(errors, password, { required: false });
errors,
"password",
password,
UTENTE_CONSTRAINTS.password.minLength,
"Password"
);
validateMaxLength(
errors,
"password",
password,
UTENTE_CONSTRAINTS.password.maxLength,
"Password"
);
} }
} }