aggiunti i file di validazione del frontend
All checks were successful
Deploy to VPS / build (push) Successful in 18s
Deploy to VPS / deploy (push) Successful in 14s

This commit is contained in:
AV77web 2026-06-24 15:56:25 +02:00
parent 10f87d21d8
commit ad00749aa5
5 changed files with 885 additions and 0 deletions

244
src/validation/cliente.ts Normal file
View file

@ -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<string, unknown>): 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<string, unknown>
): ValidationResult<ClienteInput> {
const data = normalizeClienteInput(raw);
const errors: FieldErrors = {};
validateClienteFields(data, errors, { requireAll: true });
return buildResult(data, errors);
}
export function validateClienteUpdate(
raw: Record<string, unknown>
): ValidationResult<Partial<ClienteInput>> {
const errors: FieldErrors = {};
const data: Partial<ClienteInput> = {};
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);
}

184
src/validation/consegna.ts Normal file
View file

@ -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<string, unknown>): 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<string, unknown>,
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<string, unknown>
): ValidationResult<ConsegnaInput> {
const data = normalizeConsegnaInput(raw);
const errors: FieldErrors = {};
validateConsegnaFields(raw, data, errors, { requireAll: true });
return buildResult(data, errors);
}
export function validateConsegnaUpdate(
raw: Record<string, unknown>
): ValidationResult<Partial<ConsegnaInput>> {
const errors: FieldErrors = {};
const data: Partial<ConsegnaInput> = {};
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);
}

View file

@ -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;

View file

@ -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<string, string>;
export type ValidationResult<T> =
| { 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<T>(data: T, errors: FieldErrors): ValidationResult<T> {
return Object.keys(errors).length > 0
? { success: false, errors }
: { success: true, data };
}

220
src/validation/utente.ts Normal file
View file

@ -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<UtenteCreateInput>;
export function normalizeLoginInput(raw: Record<string, unknown>): LoginInput {
return {
email: normalizeEmail(raw.email),
password: raw.password?.toString() ?? "",
};
}
export function validateLoginInput(
raw: Record<string, unknown>
): ValidationResult<LoginInput> {
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<string, unknown>
): ValidationResult<UtenteCreateInput> {
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<string, unknown>
): ValidationResult<UtenteUpdateInput> {
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);
}