diff --git a/src/auth/config.ts b/src/auth/config.ts new file mode 100644 index 0000000..aefbb6e --- /dev/null +++ b/src/auth/config.ts @@ -0,0 +1,81 @@ +import type { ExpressAuthConfig } from "@auth/express"; +import Credentials from "@auth/express/providers/credentials"; +import bcrypt from "bcryptjs"; +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 crossOriginCookieOptions = { + httpOnly: true, + sameSite: "none" as const, + secure: true, + path: "/", +}; + +export const authConfig: ExpressAuthConfig = { + basePath: "/api/auth", + trustHost: true, + secret: process.env.AUTH_SECRET, + useSecureCookies: isProduction, + session: { strategy: "jwt" }, + pages: { signIn: "/api/auth/error" }, + cookies: isProduction + ? { + sessionToken: { options: crossOriginCookieOptions }, + csrfToken: { options: { ...crossOriginCookieOptions, httpOnly: false } }, + callbackUrl: { options: crossOriginCookieOptions }, + } + : undefined, + providers: [ + Credentials({ + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + const validation = validateLoginInput(credentials ?? {}); + + if (!validation.success) return null; + + const { email, password } = validation.data; + + const [rows] = await pool.execute( + "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 { + id: String(utente.UtenteID), + email: utente.Email, + name: `${utente.Nome} ${utente.Cognome}`, + ruolo: utente.Ruolo, + }; + }, + }), + ], + callbacks: { + async jwt({ token, user }) { + if (user) { + token.id = user.id; + token.ruolo = user.ruolo; + } + return token; + }, + async session({ session, token }) { + if (session.user) { + session.user.id = token.id as string; + session.user.ruolo = token.ruolo!; + } + return session; + }, + }, +}; diff --git a/src/cors.ts b/src/cors.ts new file mode 100644 index 0000000..5b63d50 --- /dev/null +++ b/src/cors.ts @@ -0,0 +1,28 @@ +import cors from "cors"; + +const allowedOrigins = [ + "http://localhost:5173", + "http://127.0.0.1:5173", + "https://corrierifrontend.andreavillari.it", + process.env.FRONTEND_URL, +].filter(Boolean) as string[]; + +export const corsMiddleware = cors({ + origin(origin, callback) { + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + return; + } + console.warn(`CORS: origin non consentita: ${origin}`); + callback(null, false); + }, + credentials: true, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "Cookie", + "X-Requested-With", + "X-Auth-Return-Redirect", + ], +}); diff --git a/src/db/pool.ts b/src/db/pool.ts new file mode 100644 index 0000000..ae031ad --- /dev/null +++ b/src/db/pool.ts @@ -0,0 +1,13 @@ +import mysql from "mysql2/promise"; + +const pool = mysql.createPool({ + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD ?? "", + waitForConnections: true, + connectionLimit: 10, +}); + +export default pool; diff --git a/src/errors/db.ts b/src/errors/db.ts new file mode 100644 index 0000000..bd63a44 --- /dev/null +++ b/src/errors/db.ts @@ -0,0 +1,90 @@ +export interface DbErrorResponse { + status: number; + error: string; + fields?: Record; +} + +interface MysqlError extends Error { + code?: string; + errno?: number; + sqlMessage?: string; +} + +export function mapDbError(error: unknown): DbErrorResponse | null { + if (!error || typeof error !== "object") return null; + + const err = error as MysqlError; + const message = err.sqlMessage ?? err.message ?? ""; + + if (err.code === "ER_DUP_ENTRY") { + if (message.includes("uk_cliente_email")) { + return { + status: 409, + error: "Email già registrata", + fields: { email: "Email già registrata" }, + }; + } + if (message.includes("uk_utente_email")) { + return { + status: 409, + error: "Email già registrata", + fields: { email: "Email già registrata" }, + }; + } + if (message.includes("uk_chiaveConsegna")) { + return { + status: 409, + error: "Chiave consegna già registrata", + fields: { chiaveConsegna: "Chiave consegna già registrata" }, + }; + } + return { status: 409, error: "Valore duplicato" }; + } + + if (err.code === "ER_SIGNAL_EXCEPTION" || err.errno === 1644) { + if (message.includes("DataRitiro")) { + return { + status: 400, + error: "Data ritiro non valida", + fields: { dataRitiro: "Data ritiro non può essere anteriore alla data odierna" }, + }; + } + if (message.includes("DataConsegna")) { + return { + status: 400, + error: "Data consegna non valida", + fields: { dataConsegna: "Data consegna non può essere anteriore alla data odierna" }, + }; + } + } + + if (err.code === "ER_CHECK_CONSTRAINT_VIOLATED") { + if (message.includes("chk_consegna_almeno_una_data")) { + return { + status: 400, + error: "Inserire almeno una data di ritiro o di consegna", + fields: { _form: "Inserire almeno una data di ritiro o di consegna" }, + }; + } + } + + if (err.code === "ER_ROW_IS_REFERENCED_2") { + return { + status: 409, + error: "Impossibile eliminare: esistono riferimenti collegati", + }; + } + + if (err.code === "ER_NO_REFERENCED_ROW_2") { + if (message.includes("fk_consegna_cliente")) { + return { + status: 400, + error: "Cliente non valido", + fields: { clienteId: "Cliente non trovato" }, + }; + } + return { status: 400, error: "Riferimento non valido" }; + } + + return null; +} diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts new file mode 100644 index 0000000..ab90c7c --- /dev/null +++ b/src/middleware/auth.ts @@ -0,0 +1,40 @@ +import { getSession } from "@auth/express"; +import type { NextFunction, Request, Response } from "express"; +import { authConfig } from "../auth/config.js"; +import type { Ruolo } from "../types/index.js"; + +export async function requireAuth(req: Request, res: Response, next: NextFunction) { + const session = await getSession(req, authConfig); + if (!session?.user?.id) { + res.status(401).json({ error: "Autenticazione richiesta" }); + return; + } + req.user = { + id: Number(session.user.id), + email: session.user.email, + name: session.user.name, + ruolo: session.user.ruolo, + }; + next(); +} + +export function requireAmministratore(req: Request, res: Response, next: NextFunction) { + if (req.user?.ruolo !== "Amministratore") { + res.status(403).json({ error: "Operazione riservata agli amministratori" }); + return; + } + next(); +} + +declare global { + namespace Express { + interface Request { + user?: { + id: number; + email: string; + name: string; + ruolo: Ruolo; + }; + } + } +} diff --git a/src/routes/utenti.ts b/src/routes/utenti.ts new file mode 100644 index 0000000..5ba240f --- /dev/null +++ b/src/routes/utenti.ts @@ -0,0 +1,195 @@ +import bcrypt from "bcryptjs"; +import { Router } from "express"; +import type { ResultSetHeader, RowDataPacket } from "mysql2"; +import pool from "../db/pool.js"; +import { mapDbError } from "../errors/db.js"; +import { requireAmministratore, requireAuth } from "../middleware/auth.js"; +import type { Ruolo } from "../types/index.js"; +import { validateBody } from "../validation/middleware.js"; +import { + validateUtenteCreate, + validateUtenteUpdate, + type UtenteCreateInput, + type UtenteUpdateInput, +} from "../validation/utente.js"; + +const router = Router(); + +interface UtenteRow extends RowDataPacket { + UtenteID: number; + Nome: string; + Cognome: string; + Email: string; + Ruolo: Ruolo; +} + +function toUtenteResponse(row: UtenteRow) { + return { + id: row.UtenteID, + nome: row.Nome, + cognome: row.Cognome, + email: row.Email, + ruolo: row.Ruolo, + }; +} + +async function findUtenteById(id: number): Promise { + const [rows] = await pool.execute( + "SELECT UtenteID, Nome, Cognome, Email, Ruolo FROM utente WHERE UtenteID = ?", + [id] + ); + return rows[0]; +} + +router.use(requireAuth, requireAmministratore); + +router.get("/", async (_req, res) => { + const [rows] = await pool.execute( + "SELECT UtenteID, Nome, Cognome, Email, Ruolo FROM utente ORDER BY Cognome, Nome" + ); + res.json(rows.map(toUtenteResponse)); +}); + +router.get("/:id", async (req, res) => { + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + res.status(400).json({ error: "ID non valido" }); + return; + } + + const utente = await findUtenteById(id); + if (!utente) { + res.status(404).json({ error: "Utente non trovato" }); + return; + } + + res.json(toUtenteResponse(utente)); +}); + +router.post("/", validateBody(validateUtenteCreate), async (_req, res) => { + const body = res.locals.validatedBody as UtenteCreateInput; + + try { + const passwordHash = await bcrypt.hash(body.password, 10); + const [result] = await pool.execute( + "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) => { + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + res.status(400).json({ error: "ID non valido" }); + return; + } + + const existing = await findUtenteById(id); + if (!existing) { + res.status(404).json({ error: "Utente non trovato" }); + return; + } + + 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 { + await pool.execute( + `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)); + } catch (error) { + const mapped = mapDbError(error); + if (mapped) { + res.status(mapped.status).json({ + error: mapped.error, + fields: mapped.fields, + }); + return; + } + throw error; + } +}); + +router.delete("/:id", async (req, res) => { + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + res.status(400).json({ error: "ID non valido" }); + return; + } + + if (req.user?.id === id) { + res.status(400).json({ error: "Non puoi eliminare il tuo account utente" }); + return; + } + + const existing = await findUtenteById(id); + if (!existing) { + res.status(404).json({ error: "Utente non trovato" }); + return; + } + + try { + await pool.execute("DELETE FROM utente WHERE UtenteID = ?", [id]); + res.status(204).send(); + } catch (error) { + const mapped = mapDbError(error); + if (mapped) { + res.status(mapped.status).json({ error: mapped.error }); + return; + } + throw error; + } +}); + +export default router; diff --git a/src/types/auth.d.ts b/src/types/auth.d.ts new file mode 100644 index 0000000..267c8a4 --- /dev/null +++ b/src/types/auth.d.ts @@ -0,0 +1,22 @@ +import type { Ruolo } from "./index.js"; + +declare module "@auth/core/types" { + interface User { + ruolo?: Ruolo; + } + interface Session { + user: { + id: string; + email: string; + name: string; + ruolo: Ruolo; + }; + } +} + +declare module "@auth/core/jwt" { + interface JWT { + id?: string; + ruolo?: Ruolo; + } +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..6fcbb13 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,35 @@ +import { CONSEGNA_CONSTRAINTS, UTENTE_CONSTRAINTS } from "../validation/constraints.js"; + +export type Ruolo = (typeof UTENTE_CONSTRAINTS.ruolo.allowedValues)[number]; + +export type StatoConsegna = (typeof CONSEGNA_CONSTRAINTS.stato.allowedValues)[number]; + +export interface Utente { + UtenteID: number; + Nome: string; + Cognome: string; + Email: string; + PasswordHash: string; + Ruolo: Ruolo; +} + +export interface Cliente { + ClienteID: number; + Nome: string; + Cognome: string; + Via: string; + Comune: string; + Provincia: string; + Telefono: string; + Email: string; + Note: string | null; +} + +export interface Consegna { + ConsegnaID: number; + ClienteID: number; + DataRitiro: Date | null; + DataConsegna: Date | null; + Stato: StatoConsegna; + ChiaveConsegna: string; +} diff --git a/src/validation/constraints.ts b/src/validation/constraints.ts new file mode 100644 index 0000000..bb02fca --- /dev/null +++ b/src/validation/constraints.ts @@ -0,0 +1,40 @@ +//========================================= +// File: constraints.ts +// Vincoli derivati da initdb/01_schema.sql. +// author: "villari.andrea@libero.it" +// version: "1.0.0 2026-06-18" +//========================================= + +export const UTENTE_CONSTRAINTS = { + nome: { maxLength: 100, required: true }, + cognome: { maxLength: 100, required: true }, + email: { maxLength: 255, required: true }, + password: { minLength: 8, maxLength: 72, required: true }, + ruolo: {allowedValues: ["Operatore", "Amministratore"], + required: true, + } + } as const; + + export const CLIENTE_CONSTRAINTS = { + nome: { maxLength: 100, required: true }, + cognome: { maxLength: 100, required: true }, + via: {maxLength: 100, required: true}, + comune: {maxLength: 100, required: true}, + provincia: {maxLength: 2, required: true}, + telefono: { maxLength: 30, required: true }, + email: { maxLength: 255, required: true }, + note: { required: false }, + } as const; + + +export const CONSEGNA_CONSTRAINTS = { + dataRitiro: { required: false }, + dataConsegna: { required: false }, + stato: { + allowedValues: ["da ritirare", "in consegna", "in giacenza", "in deposito"], + required: true, + }, + chiaveConsegna: { maxLength: 8, required: true }, + clienteId: { required: true }, + } as const; + \ No newline at end of file diff --git a/src/validation/middleware.ts b/src/validation/middleware.ts new file mode 100644 index 0000000..94fa95d --- /dev/null +++ b/src/validation/middleware.ts @@ -0,0 +1,21 @@ +import type { RequestHandler } from "express"; +import type { ValidationResult } from "./primitives.js"; + +export function validateBody( + validator: (body: Record) => ValidationResult +): RequestHandler { + return (req, res, next) => { + const result = validator(req.body as Record); + + if (!result.success) { + res.status(400).json({ + error: "Validazione fallita", + fields: result.errors, + }); + return; + } + + res.locals.validatedBody = result.data; + next(); + }; +} diff --git a/src/validation/primitives.ts b/src/validation/primitives.ts new file mode 100644 index 0000000..e69db6a --- /dev/null +++ b/src/validation/primitives.ts @@ -0,0 +1,199 @@ +//========================================== +// File: primitives.ts +// Funzioni di validazione dei dati prmitivi +// author: "villari.andrea@libero.it" +// version: "1.0.0 2026-06-18" +//========================================== + +export type FieldErrors = Record; + +export type ValidationResult = + | { success: true; data: T } + | { success: false; errors: FieldErrors }; + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export function normalizeString(value: unknown): string { + if (value == null) return ""; + return value.toString().trim(); +} + +export function normalizeEmail(value: unknown): string { + return normalizeString(value).toLowerCase(); +} + +export function normalizeBoolean(value: unknown, defaultValue = false): boolean { + if (value === true || value === "true") return true; + if (value === false || value === "false") return false; + return defaultValue; +} + +export function normalizeDecimal(value: unknown): number { + if (typeof value === "number") return value; + const normalized = value?.toString().trim().replace(",", ".") ?? ""; + if (!normalized) return Number.NaN; + return Number(normalized); +} + +export function normalizePositiveInt(value: unknown): number { + if (typeof value === "number") return value; + const parsed = Number(value); + return Number.isInteger(parsed) && parsed > 0 ? parsed : Number.NaN; +} + +export function normalizeTime(value: unknown): string { + const str = normalizeString(value); + if (!str) return ""; + if (/^\d{2}:\d{2}$/.test(str)) return `${str}:00`; + return str; +} + +export function validateRequired( + errors: FieldErrors, + field: string, + value: string, + message: string +): void { + if (!value) errors[field] = message; +} + +export function validateMaxLength( + errors: FieldErrors, + field: string, + value: string, + maxLength: number, + label: string +): void { + if (value.length > maxLength) { + errors[field] = `${label} deve contenere al massimo ${maxLength} caratteri`; + } +} + +export function validateMinLength( + errors: FieldErrors, + field: string, + value: string, + minLength: number, + label: string +): void { + if (value && value.length < minLength) { + errors[field] = `${label} deve contenere almeno ${minLength} caratteri`; + } +} + +export function validateEmailFormat( + errors: FieldErrors, + field: string, + value: string +): void { + if (value && !EMAIL_REGEX.test(value)) { + errors[field] = "Formato email non valido"; + } +} + +export function validateEnum( + errors: FieldErrors, + field: string, + value: string, + allowedValues: readonly string[], + label: string +): void { + if (value && !allowedValues.includes(value)) { + errors[field] = `${label} non valido`; + } +} + +export function validateBoolean( + errors: FieldErrors, + field: string, + value: unknown, + label: string +): void { + if (value === undefined || value === null || value === "") return; + if ( + value !== true && + value !== false && + value !== "true" && + value !== "false" + ) { + errors[field] = `${label} non valido`; + } +} + +const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; +const TIME_REGEX = /^\d{2}:\d{2}:\d{2}$/; + +export function validateDate( + errors: FieldErrors, + field: string, + value: string, + label: string +): void { + if (!value) return; + if (!DATE_REGEX.test(value)) { + errors[field] = `${label} non valida`; + return; + } + const parsed = new Date(`${value}T00:00:00`); + if (Number.isNaN(parsed.getTime())) { + errors[field] = `${label} non valida`; + } +} + +export function validateTimeFormat( + errors: FieldErrors, + field: string, + value: string, + label: string +): void { + if (!value) return; + if (!TIME_REGEX.test(value)) { + errors[field] = `${label} non valida`; + } +} + +export function validatePositiveInt( + errors: FieldErrors, + field: string, + value: number, + label: string, + required = true +): void { + if (Number.isNaN(value) || value <= 0) { + errors[field] = required ? `${label} obbligatorio` : `${label} non valido`; + } +} + +export function validateTimeRange( + errors: FieldErrors, + startField: string, + endField: string, + start: string, + end: string +): void { + if (!start || !end) return; + if (start >= end) { + errors[endField] = + "L'ora di fine deve essere successiva all'ora di inizio"; + } +} + +export function validateDecimalPositive( + errors: FieldErrors, + field: string, + value: unknown, + label: string, + min = 0.01 +): void { + if (value === undefined || value === null || value === "") return; + const num = typeof value === "number" ? value : Number(value); + if (Number.isNaN(num) || num < min) { + errors[field] = `${label} deve essere almeno ${min}`; + } +} + +export function buildResult(data: T, errors: FieldErrors): ValidationResult { + return Object.keys(errors).length > 0 + ? { success: false, errors } + : { success: true, data }; +} diff --git a/src/validation/utente.ts b/src/validation/utente.ts new file mode 100644 index 0000000..4779dc0 --- /dev/null +++ b/src/validation/utente.ts @@ -0,0 +1,220 @@ +import type { Ruolo } from "../types/index.js"; +import { UTENTE_CONSTRAINTS } from "./constraints.js"; +import { + buildResult, + normalizeEmail, + normalizeString, + validateEmailFormat, + validateEnum, + validateMaxLength, + validateMinLength, + validateRequired, + type FieldErrors, + type ValidationResult, +} from "./primitives.js"; + +export interface LoginInput { + email: string; + password: string; +} + +export interface UtenteCreateInput { + nome: string; + cognome: string; + email: string; + password: string; + ruolo: Ruolo; +} + +export type UtenteUpdateInput = Partial; + +export function normalizeLoginInput(raw: Record): LoginInput { + return { + email: normalizeEmail(raw.email), + password: raw.password?.toString() ?? "", + }; +} + +export function validateLoginInput( + raw: Record +): ValidationResult { + const data = normalizeLoginInput(raw); + const errors: FieldErrors = {}; + + validateRequired(errors, "email", data.email, "Email obbligatoria"); + validateMaxLength( + errors, + "email", + data.email, + UTENTE_CONSTRAINTS.email.maxLength, + "Email" + ); + validateEmailFormat(errors, "email", data.email); + validateRequired(errors, "password", data.password, "Password obbligatoria"); + validateMaxLength( + errors, + "password", + data.password, + UTENTE_CONSTRAINTS.password.maxLength, + "Password" + ); + + return buildResult(data, errors); +} + +function validateUtenteProfileFields( + data: UtenteCreateInput, + errors: FieldErrors +): void { + validateRequired(errors, "nome", data.nome, "Nome obbligatorio"); + validateMaxLength( + errors, + "nome", + data.nome, + UTENTE_CONSTRAINTS.nome.maxLength, + "Nome" + ); + validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio"); + validateMaxLength( + errors, + "cognome", + data.cognome, + UTENTE_CONSTRAINTS.cognome.maxLength, + "Cognome" + ); + validateRequired(errors, "email", data.email, "Email obbligatoria"); + validateMaxLength( + errors, + "email", + data.email, + UTENTE_CONSTRAINTS.email.maxLength, + "Email" + ); + validateEmailFormat(errors, "email", data.email); + + validateRequired(errors, "password", data.password, "Password obbligatoria"); + validateMinLength( + errors, + "password", + data.password, + UTENTE_CONSTRAINTS.password.minLength, + "Password" + ); + validateMaxLength( + errors, + "password", + data.password, + UTENTE_CONSTRAINTS.password.maxLength, + "Password" + ); + + validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio"); + validateEnum( + errors, + "ruolo", + data.ruolo, + UTENTE_CONSTRAINTS.ruolo.allowedValues, + "Ruolo" + ); +} + +export function validateUtenteCreate( + raw: Record +): ValidationResult { + const data: UtenteCreateInput = { + nome: normalizeString(raw.nome), + cognome: normalizeString(raw.cognome), + email: normalizeEmail(raw.email), + password: raw.password?.toString() ?? "", + ruolo: normalizeString(raw.ruolo) as Ruolo, + }; + const errors: FieldErrors = {}; + + validateUtenteProfileFields(data, errors); + + return buildResult(data, errors); +} + + +export function validateUtenteUpdate( + raw: Record +): ValidationResult { + const errors: FieldErrors = {}; + const data: UtenteUpdateInput = {}; + + if ("nome" in raw) { + data.nome = normalizeString(raw.nome); + validateRequired(errors, "nome", data.nome, "Nome obbligatorio"); + validateMaxLength( + errors, + "nome", + data.nome, + UTENTE_CONSTRAINTS.nome.maxLength, + "Nome" + ); + } + + if ("cognome" in raw) { + data.cognome = normalizeString(raw.cognome); + validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio"); + validateMaxLength( + errors, + "cognome", + data.cognome, + UTENTE_CONSTRAINTS.cognome.maxLength, + "Cognome" + ); + } + + if ("email" in raw) { + data.email = normalizeEmail(raw.email); + validateRequired(errors, "email", data.email, "Email obbligatoria"); + validateMaxLength( + errors, + "email", + data.email, + UTENTE_CONSTRAINTS.email.maxLength, + "Email" + ); + validateEmailFormat(errors, "email", data.email); + } + + if ("password" in raw) { + const password = raw.password?.toString() ?? ""; + if (password) { + data.password = password; + validateMinLength( + errors, + "password", + password, + UTENTE_CONSTRAINTS.password.minLength, + "Password" + ); + validateMaxLength( + errors, + "password", + password, + UTENTE_CONSTRAINTS.password.maxLength, + "Password" + ); + } + } + + if ("ruolo" in raw) { + data.ruolo = normalizeString(raw.ruolo) as Ruolo; + validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio"); + validateEnum( + errors, + "ruolo", + data.ruolo, + UTENTE_CONSTRAINTS.ruolo.allowedValues, + "Ruolo" + ); + } + + if (Object.keys(data).length === 0) { + errors._form = "Nessun campo da aggiornare"; + } + + return buildResult(data, errors); +} \ No newline at end of file