aggiunti: in validation i constraints, middleware, operatore, primitive; aggiunti anche db/pool.ts middleware/auth.ts types/index.ts config/cors.ts e auth/config.ts
This commit is contained in:
parent
d90478dd9d
commit
d82234482b
10 changed files with 677 additions and 0 deletions
81
src/auth/config.ts
Normal file
81
src/auth/config.ts
Normal file
|
|
@ -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 { Operatore } from "../types/index.js";
|
||||
import { validateLoginInput } from "../validation/operatore.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<RowDataPacket[]>(
|
||||
"SELECT OperatoreID, Nome, Cognome, Email, PasswordHash, Admin FROM operatore WHERE Email = ?",
|
||||
[email]
|
||||
);
|
||||
|
||||
const operatore = rows[0] as Operatore | undefined;
|
||||
if (!operatore) return null;
|
||||
|
||||
const valid = await bcrypt.compare(password, operatore.PasswordHash);
|
||||
if (!valid) return null;
|
||||
|
||||
return {
|
||||
id: String(operatore.OperatoreID),
|
||||
email: operatore.Email,
|
||||
name: `${operatore.Nome} ${operatore.Cognome}`,
|
||||
admin: operatore.Admin,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
token.admin = user.admin;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id as string;
|
||||
session.user.admin = token.admin as boolean;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
};
|
||||
28
src/config/cors.ts
Normal file
28
src/config/cors.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import cors from "cors";
|
||||
|
||||
const allowedOrigins = [
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
"https://prenotazionifrontend.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",
|
||||
],
|
||||
});
|
||||
13
src/db/pool.ts
Normal file
13
src/db/pool.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import mysql from "mysql2/promise";
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST ?? "127.0.0.1",
|
||||
port: Number(process.env.DB_PORT ?? 3307),
|
||||
database: process.env.DB_NAME ?? "prenotazioni",
|
||||
user: process.env.DB_USER ?? "prenotazioni_user",
|
||||
password: process.env.DB_PASSWORD ?? "",
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
});
|
||||
|
||||
export default pool;
|
||||
39
src/middleware/auth.ts
Normal file
39
src/middleware/auth.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { getSession } from "@auth/express";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import { authConfig } from "../auth/config.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,
|
||||
admin: session.user.admin,
|
||||
};
|
||||
next();
|
||||
}
|
||||
|
||||
export function requireAdmin(req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.user?.admin) {
|
||||
res.status(403).json({ error: "Operazione riservata agli amministratori" });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string;
|
||||
admin: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/types/auth.d.ts
vendored
Normal file
22
src/types/auth.d.ts
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type {} from "./index.js";
|
||||
|
||||
declare module "@auth/core/types" {
|
||||
interface User {
|
||||
admin?: boolean;
|
||||
}
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
admin: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@auth/core/jwt" {
|
||||
interface JWT {
|
||||
id?: string;
|
||||
admin?: boolean;
|
||||
}
|
||||
}
|
||||
8
src/types/index.ts
Normal file
8
src/types/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface Operatore {
|
||||
OperatoreID: number;
|
||||
Nome: string;
|
||||
Cognome: string;
|
||||
Email: string;
|
||||
PasswordHash: string;
|
||||
Admin: boolean;
|
||||
}
|
||||
43
src/validation/constraints.ts
Normal file
43
src/validation/constraints.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//=========================================
|
||||
// File: constraints.ts
|
||||
// Vincoli derivati da initdb/01_schema.sql.
|
||||
// author: "villari.andrea@libero.it"
|
||||
// version: "1.0.0 2026-06-18"
|
||||
//=========================================
|
||||
|
||||
export const OPERATORE_CONSTRAINTS = {
|
||||
nome: { maxLength: 100, required: true },
|
||||
cognome: { maxLength: 100, required: true },
|
||||
email: { maxLength: 255, required: true },
|
||||
password: { minLength: 8, maxLength: 72, required: true },
|
||||
} as const;
|
||||
|
||||
export const CLIENTE_CONSTRAINTS = {
|
||||
nome: { maxLength: 100, required: true },
|
||||
cognome: { maxLength: 100, required: true },
|
||||
telefono: { maxLength: 30, required: true },
|
||||
email: { maxLength: 255, required: true },
|
||||
note: { required: false },
|
||||
} as const;
|
||||
|
||||
export const CAMPO_SPORTIVO_CONSTRAINTS = {
|
||||
nome: { maxLength: 100, required: true },
|
||||
tipo: { allowedValues: ["tennis", "padel", "calcetto"], required: true },
|
||||
coperto: { required: true },
|
||||
prezzoOrario: { required: true, min: 0.01 },
|
||||
attivo: { required: true },
|
||||
} as const;
|
||||
|
||||
export const PRENOTAZIONE_CONSTRAINTS = {
|
||||
data: { required: true },
|
||||
oraInizio: { required: true },
|
||||
oraFine: { required: true },
|
||||
stato: {
|
||||
allowedValues: ["prenotata", "confermata", "annullata", "completata"],
|
||||
required: true,
|
||||
},
|
||||
codicePrenotazione: { maxLength: 8, required: true },
|
||||
clienteId: { required: true },
|
||||
campoId: { required: true },
|
||||
} as const;
|
||||
|
||||
21
src/validation/middleware.ts
Normal file
21
src/validation/middleware.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { RequestHandler } from "express";
|
||||
import type { ValidationResult } from "./primitives.js";
|
||||
|
||||
export function validateBody<T>(
|
||||
validator: (body: Record<string, unknown>) => ValidationResult<T>
|
||||
): RequestHandler {
|
||||
return (req, res, next) => {
|
||||
const result = validator(req.body as Record<string, unknown>);
|
||||
|
||||
if (!result.success) {
|
||||
res.status(400).json({
|
||||
error: "Validazione fallita",
|
||||
fields: result.errors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.locals.validatedBody = result.data;
|
||||
next();
|
||||
};
|
||||
}
|
||||
224
src/validation/operatore.ts
Normal file
224
src/validation/operatore.ts
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
//==============================================
|
||||
// File: operatore.ts
|
||||
// file per la validazione e il login operatore.
|
||||
// author: "villari.andrea@libero.it"
|
||||
// version: "1.0.0 2026-06-18"
|
||||
//==============================================
|
||||
|
||||
import { OPERATORE_CONSTRAINTS } from "./constraints.js";
|
||||
import {
|
||||
buildResult,
|
||||
normalizeBoolean,
|
||||
normalizeEmail,
|
||||
normalizeString,
|
||||
validateBoolean,
|
||||
validateEmailFormat,
|
||||
validateMaxLength,
|
||||
validateMinLength,
|
||||
validateRequired,
|
||||
type FieldErrors,
|
||||
type ValidationResult,
|
||||
} from "./primitives.js";
|
||||
|
||||
export interface LoginInput {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface OperatoreCreateInput {
|
||||
nome: string;
|
||||
cognome: string;
|
||||
email: string;
|
||||
password: string;
|
||||
admin: boolean;
|
||||
}
|
||||
|
||||
export type OperatoreUpdateInput = Partial<OperatoreCreateInput>;
|
||||
|
||||
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,
|
||||
OPERATORE_CONSTRAINTS.email.maxLength,
|
||||
"Email"
|
||||
);
|
||||
validateEmailFormat(errors, "email", data.email);
|
||||
validateRequired(errors, "password", data.password, "Password obbligatoria");
|
||||
validateMaxLength(
|
||||
errors,
|
||||
"password",
|
||||
data.password,
|
||||
OPERATORE_CONSTRAINTS.password.maxLength,
|
||||
"Password"
|
||||
);
|
||||
|
||||
return buildResult(data, errors);
|
||||
}
|
||||
|
||||
function validateOperatoreProfileFields(
|
||||
data: {
|
||||
nome: string;
|
||||
cognome: string;
|
||||
email: string;
|
||||
password?: string;
|
||||
admin?: boolean;
|
||||
},
|
||||
errors: FieldErrors,
|
||||
options: { requirePassword: boolean }
|
||||
): void {
|
||||
validateRequired(errors, "nome", data.nome, "Nome obbligatorio");
|
||||
validateMaxLength(
|
||||
errors,
|
||||
"nome",
|
||||
data.nome,
|
||||
OPERATORE_CONSTRAINTS.nome.maxLength,
|
||||
"Nome"
|
||||
);
|
||||
validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio");
|
||||
validateMaxLength(
|
||||
errors,
|
||||
"cognome",
|
||||
data.cognome,
|
||||
OPERATORE_CONSTRAINTS.cognome.maxLength,
|
||||
"Cognome"
|
||||
);
|
||||
validateRequired(errors, "email", data.email, "Email obbligatoria");
|
||||
validateMaxLength(
|
||||
errors,
|
||||
"email",
|
||||
data.email,
|
||||
OPERATORE_CONSTRAINTS.email.maxLength,
|
||||
"Email"
|
||||
);
|
||||
validateEmailFormat(errors, "email", data.email);
|
||||
|
||||
if (options.requirePassword) {
|
||||
validateRequired(errors, "password", data.password ?? "", "Password obbligatoria");
|
||||
}
|
||||
|
||||
if (data.password) {
|
||||
validateMinLength(
|
||||
errors,
|
||||
"password",
|
||||
data.password,
|
||||
OPERATORE_CONSTRAINTS.password.minLength,
|
||||
"Password"
|
||||
);
|
||||
validateMaxLength(
|
||||
errors,
|
||||
"password",
|
||||
data.password,
|
||||
OPERATORE_CONSTRAINTS.password.maxLength,
|
||||
"Password"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateOperatoreCreate(
|
||||
raw: Record<string, unknown>
|
||||
): ValidationResult<OperatoreCreateInput> {
|
||||
const data: OperatoreCreateInput = {
|
||||
nome: normalizeString(raw.nome),
|
||||
cognome: normalizeString(raw.cognome),
|
||||
email: normalizeEmail(raw.email),
|
||||
password: raw.password?.toString() ?? "",
|
||||
admin: normalizeBoolean(raw.admin, false),
|
||||
};
|
||||
const errors: FieldErrors = {};
|
||||
|
||||
validateOperatoreProfileFields(data, errors, { requirePassword: true });
|
||||
|
||||
return buildResult(data, errors);
|
||||
}
|
||||
|
||||
export function validateOperatoreUpdate(
|
||||
raw: Record<string, unknown>
|
||||
): ValidationResult<OperatoreUpdateInput> {
|
||||
const errors: FieldErrors = {};
|
||||
const data: OperatoreUpdateInput = {};
|
||||
|
||||
if ("nome" in raw) {
|
||||
data.nome = normalizeString(raw.nome);
|
||||
validateRequired(errors, "nome", data.nome, "Nome obbligatorio");
|
||||
validateMaxLength(
|
||||
errors,
|
||||
"nome",
|
||||
data.nome,
|
||||
OPERATORE_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,
|
||||
OPERATORE_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,
|
||||
OPERATORE_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,
|
||||
OPERATORE_CONSTRAINTS.password.minLength,
|
||||
"Password"
|
||||
);
|
||||
validateMaxLength(
|
||||
errors,
|
||||
"password",
|
||||
password,
|
||||
OPERATORE_CONSTRAINTS.password.maxLength,
|
||||
"Password"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ("admin" in raw) {
|
||||
validateBoolean(errors, "admin", raw.admin, "Admin");
|
||||
if (!errors.admin) {
|
||||
data.admin = normalizeBoolean(raw.admin, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(data).length === 0) {
|
||||
errors._form = "Nessun campo da aggiornare";
|
||||
}
|
||||
|
||||
return buildResult(data, errors);
|
||||
}
|
||||
198
src/validation/primitives.ts
Normal file
198
src/validation/primitives.ts
Normal 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 };
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue