From ad00749aa5b17c94e35af799aba3df32177f0077 Mon Sep 17 00:00:00 2001 From: AV77web Date: Wed, 24 Jun 2026 15:56:25 +0200 Subject: [PATCH] aggiunti i file di validazione del frontend --- src/validation/cliente.ts | 244 ++++++++++++++++++++++++++++++++++ src/validation/consegna.ts | 184 +++++++++++++++++++++++++ src/validation/constraints.ts | 39 ++++++ src/validation/primitives.ts | 198 +++++++++++++++++++++++++++ src/validation/utente.ts | 220 ++++++++++++++++++++++++++++++ 5 files changed, 885 insertions(+) create mode 100644 src/validation/cliente.ts create mode 100644 src/validation/consegna.ts create mode 100644 src/validation/constraints.ts create mode 100644 src/validation/primitives.ts create mode 100644 src/validation/utente.ts diff --git a/src/validation/cliente.ts b/src/validation/cliente.ts new file mode 100644 index 0000000..27e83fc --- /dev/null +++ b/src/validation/cliente.ts @@ -0,0 +1,244 @@ +import { CLIENTE_CONSTRAINTS } from "./constraints"; +import { + buildResult, + normalizeEmail, + normalizeString, + validateEmailFormat, + validateMaxLength, + validateRequired, + type FieldErrors, + type ValidationResult, +} from "./primitives"; + +export interface ClienteInput { + nome: string; + cognome: string; + via: string; + comune: string; + provincia: string; + telefono: string; + email: string; + note: string | null; +} + +export function normalizeClienteInput(raw: Record): ClienteInput { + const noteRaw = raw.note; + return { + nome: normalizeString(raw.nome), + cognome: normalizeString(raw.cognome), + via: normalizeString(raw.via), + comune: normalizeString(raw.comune), + provincia: normalizeString(raw.provincia).toUpperCase(), + telefono: normalizeString(raw.telefono), + email: normalizeEmail(raw.email), + note: + noteRaw === null || noteRaw === undefined || noteRaw === "" + ? null + : normalizeString(noteRaw), + }; +} + +function validateProvincia(errors: FieldErrors, provincia: string): void { + if (!provincia) return; + if (provincia.length !== CLIENTE_CONSTRAINTS.provincia.maxLength) { + errors.provincia = "Provincia deve essere di 2 caratteri"; + } +} + +function validateClienteFields( + data: ClienteInput, + errors: FieldErrors, + options: { requireAll: boolean } +): void { + if (options.requireAll || data.nome !== undefined) { + validateRequired(errors, "nome", data.nome, "Nome obbligatorio"); + validateMaxLength( + errors, + "nome", + data.nome, + CLIENTE_CONSTRAINTS.nome.maxLength, + "Nome" + ); + } + + if (options.requireAll || data.cognome !== undefined) { + validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio"); + validateMaxLength( + errors, + "cognome", + data.cognome, + CLIENTE_CONSTRAINTS.cognome.maxLength, + "Cognome" + ); + } + + if (options.requireAll || data.via !== undefined) { + validateRequired(errors, "via", data.via, "Via obbligatoria"); + validateMaxLength( + errors, + "via", + data.via, + CLIENTE_CONSTRAINTS.via.maxLength, + "Via" + ); + } + + if (options.requireAll || data.comune !== undefined) { + validateRequired(errors, "comune", data.comune, "Comune obbligatorio"); + validateMaxLength( + errors, + "comune", + data.comune, + CLIENTE_CONSTRAINTS.comune.maxLength, + "Comune" + ); + } + + if (options.requireAll || data.provincia !== undefined) { + validateRequired(errors, "provincia", data.provincia, "Provincia obbligatoria"); + validateMaxLength( + errors, + "provincia", + data.provincia, + CLIENTE_CONSTRAINTS.provincia.maxLength, + "Provincia" + ); + validateProvincia(errors, data.provincia); + } + + if (options.requireAll || data.telefono !== undefined) { + validateRequired(errors, "telefono", data.telefono, "Telefono obbligatorio"); + validateMaxLength( + errors, + "telefono", + data.telefono, + CLIENTE_CONSTRAINTS.telefono.maxLength, + "Telefono" + ); + } + + if (options.requireAll || data.email !== undefined) { + validateRequired(errors, "email", data.email, "Email obbligatoria"); + validateMaxLength( + errors, + "email", + data.email, + CLIENTE_CONSTRAINTS.email.maxLength, + "Email" + ); + validateEmailFormat(errors, "email", data.email); + } +} + +export function validateClienteCreate( + raw: Record +): ValidationResult { + const data = normalizeClienteInput(raw); + const errors: FieldErrors = {}; + validateClienteFields(data, errors, { requireAll: true }); + return buildResult(data, errors); +} + +export function validateClienteUpdate( + raw: Record +): ValidationResult> { + const errors: FieldErrors = {}; + const data: Partial = {}; + + if ("nome" in raw) { + data.nome = normalizeString(raw.nome); + validateRequired(errors, "nome", data.nome, "Nome obbligatorio"); + validateMaxLength( + errors, + "nome", + data.nome, + CLIENTE_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, + CLIENTE_CONSTRAINTS.cognome.maxLength, + "Cognome" + ); + } + + if ("via" in raw) { + data.via = normalizeString(raw.via); + validateRequired(errors, "via", data.via, "Via obbligatoria"); + validateMaxLength( + errors, + "via", + data.via, + CLIENTE_CONSTRAINTS.via.maxLength, + "Via" + ); + } + + if ("comune" in raw) { + data.comune = normalizeString(raw.comune); + validateRequired(errors, "comune", data.comune, "Comune obbligatorio"); + validateMaxLength( + errors, + "comune", + data.comune, + CLIENTE_CONSTRAINTS.comune.maxLength, + "Comune" + ); + } + + if ("provincia" in raw) { + data.provincia = normalizeString(raw.provincia).toUpperCase(); + validateRequired(errors, "provincia", data.provincia, "Provincia obbligatoria"); + validateMaxLength( + errors, + "provincia", + data.provincia, + CLIENTE_CONSTRAINTS.provincia.maxLength, + "Provincia" + ); + validateProvincia(errors, data.provincia); + } + + if ("telefono" in raw) { + data.telefono = normalizeString(raw.telefono); + validateRequired(errors, "telefono", data.telefono, "Telefono obbligatorio"); + validateMaxLength( + errors, + "telefono", + data.telefono, + CLIENTE_CONSTRAINTS.telefono.maxLength, + "Telefono" + ); + } + + if ("email" in raw) { + data.email = normalizeEmail(raw.email); + validateRequired(errors, "email", data.email, "Email obbligatoria"); + validateMaxLength( + errors, + "email", + data.email, + CLIENTE_CONSTRAINTS.email.maxLength, + "Email" + ); + validateEmailFormat(errors, "email", data.email); + } + + if ("note" in raw) { + data.note = + raw.note === null || raw.note === "" ? null : normalizeString(raw.note); + } + + if (Object.keys(data).length === 0) { + errors._form = "Nessun campo da aggiornare"; + } + + return buildResult(data, errors); +} diff --git a/src/validation/consegna.ts b/src/validation/consegna.ts new file mode 100644 index 0000000..60c03e9 --- /dev/null +++ b/src/validation/consegna.ts @@ -0,0 +1,184 @@ +import { CONSEGNA_CONSTRAINTS } from "./constraints"; +import { + buildResult, + normalizePositiveInt, + normalizeString, + validateDate, + validateEnum, + validateMaxLength, + validatePositiveInt, + validateRequired, + type FieldErrors, + type ValidationResult, +} from "./primitives"; + +export type StatoConsegna = + (typeof CONSEGNA_CONSTRAINTS.stato.allowedValues)[number]; + +export interface ConsegnaInput { + dataRitiro: string; + dataConsegna: string; + stato: StatoConsegna; + chiaveConsegna: string; + clienteId: number; +} + +function todayAtMidnight(): Date { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return today; +} + +function validateDateNotRetroactive( + errors: FieldErrors, + field: string, + value: string, + label: string +): void { + if (!value || errors[field]) return; + + const parsed = new Date(`${value}T00:00:00`); + if (parsed < todayAtMidnight()) { + errors[field] = `${label} non può essere anteriore alla data odierna`; + } +} + +export function normalizeConsegnaInput(raw: Record): ConsegnaInput { + return { + dataRitiro: normalizeString(raw.dataRitiro), + dataConsegna: normalizeString(raw.dataConsegna), + stato: normalizeString(raw.stato || "in deposito") as StatoConsegna, + chiaveConsegna: normalizeString(raw.chiaveConsegna).toUpperCase(), + clienteId: normalizePositiveInt(raw.clienteId), + }; +} + +function validateConsegnaFields( + raw: Record, + data: ConsegnaInput, + errors: FieldErrors, + options: { requireAll: boolean } +): void { + if (options.requireAll || "dataRitiro" in raw) { + validateRequired(errors, "dataRitiro", data.dataRitiro, "Data ritiro obbligatoria"); + validateDate(errors, "dataRitiro", data.dataRitiro, "Data ritiro"); + validateDateNotRetroactive(errors, "dataRitiro", data.dataRitiro, "Data ritiro"); + } + + if (options.requireAll || "dataConsegna" in raw) { + validateRequired( + errors, + "dataConsegna", + data.dataConsegna, + "Data consegna obbligatoria" + ); + validateDate(errors, "dataConsegna", data.dataConsegna, "Data consegna"); + validateDateNotRetroactive(errors, "dataConsegna", data.dataConsegna, "Data consegna"); + } + + if (options.requireAll || "stato" in raw) { + validateRequired(errors, "stato", data.stato, "Stato obbligatorio"); + validateEnum( + errors, + "stato", + data.stato, + CONSEGNA_CONSTRAINTS.stato.allowedValues, + "Stato" + ); + } + + if (options.requireAll || "chiaveConsegna" in raw) { + validateRequired( + errors, + "chiaveConsegna", + data.chiaveConsegna, + "Chiave consegna obbligatoria" + ); + validateMaxLength( + errors, + "chiaveConsegna", + data.chiaveConsegna, + CONSEGNA_CONSTRAINTS.chiaveConsegna.maxLength, + "Chiave consegna" + ); + } + + if (options.requireAll || "clienteId" in raw) { + validatePositiveInt(errors, "clienteId", data.clienteId, "Cliente", options.requireAll); + } +} + +export function validateConsegnaCreate( + raw: Record +): ValidationResult { + const data = normalizeConsegnaInput(raw); + const errors: FieldErrors = {}; + validateConsegnaFields(raw, data, errors, { requireAll: true }); + return buildResult(data, errors); +} + +export function validateConsegnaUpdate( + raw: Record +): ValidationResult> { + const errors: FieldErrors = {}; + const data: Partial = {}; + + if ("dataRitiro" in raw) { + data.dataRitiro = normalizeString(raw.dataRitiro); + validateRequired(errors, "dataRitiro", data.dataRitiro, "Data ritiro obbligatoria"); + validateDate(errors, "dataRitiro", data.dataRitiro, "Data ritiro"); + validateDateNotRetroactive(errors, "dataRitiro", data.dataRitiro, "Data ritiro"); + } + + if ("dataConsegna" in raw) { + data.dataConsegna = normalizeString(raw.dataConsegna); + validateRequired( + errors, + "dataConsegna", + data.dataConsegna, + "Data consegna obbligatoria" + ); + validateDate(errors, "dataConsegna", data.dataConsegna, "Data consegna"); + validateDateNotRetroactive(errors, "dataConsegna", data.dataConsegna, "Data consegna"); + } + + if ("stato" in raw) { + data.stato = normalizeString(raw.stato) as StatoConsegna; + validateRequired(errors, "stato", data.stato, "Stato obbligatorio"); + validateEnum( + errors, + "stato", + data.stato, + CONSEGNA_CONSTRAINTS.stato.allowedValues, + "Stato" + ); + } + + if ("chiaveConsegna" in raw) { + data.chiaveConsegna = normalizeString(raw.chiaveConsegna).toUpperCase(); + validateRequired( + errors, + "chiaveConsegna", + data.chiaveConsegna, + "Chiave consegna obbligatoria" + ); + validateMaxLength( + errors, + "chiaveConsegna", + data.chiaveConsegna, + CONSEGNA_CONSTRAINTS.chiaveConsegna.maxLength, + "Chiave consegna" + ); + } + + if ("clienteId" in raw) { + data.clienteId = normalizePositiveInt(raw.clienteId); + validatePositiveInt(errors, "clienteId", data.clienteId, "Cliente"); + } + + if (Object.keys(data).length === 0) { + errors._form = "Nessun campo da aggiornare"; + } + + return buildResult(data, errors); +} diff --git a/src/validation/constraints.ts b/src/validation/constraints.ts new file mode 100644 index 0000000..6506d67 --- /dev/null +++ b/src/validation/constraints.ts @@ -0,0 +1,39 @@ +//=================================================== +// File: constraints.ts +// Vincoli derivati da initdb/01_schema.sql. +// author: "villari.andrea@libero.it" +// version: "1.0.0 2026-06-24" +//=================================================== + +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 = { + dataconsegna: { required: true }, + dataritiro: { required: true }, + stato: { + allowedValues: ["da ritirare", "in consegna", "in giacenza", "in deposito"], + required: true, + }, + chiaveConsegna: { maxLength: 8, required: true }, + clienteId: { required: true }, +} as const; diff --git a/src/validation/primitives.ts b/src/validation/primitives.ts new file mode 100644 index 0000000..7d8b6d5 --- /dev/null +++ b/src/validation/primitives.ts @@ -0,0 +1,198 @@ +//========================================== +// 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 { + 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..007b3e1 --- /dev/null +++ b/src/validation/utente.ts @@ -0,0 +1,220 @@ +import { UTENTE_CONSTRAINTS } from "./constraints"; +import { + buildResult, + normalizeEmail, + normalizeString, + validateEmailFormat, + validateEnum, + validateMaxLength, + validateMinLength, + validateRequired, + type FieldErrors, + type ValidationResult, +} from "./primitives"; + +export type Ruolo = (typeof UTENTE_CONSTRAINTS.ruolo.allowedValues)[number]; + +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); +}