aggiunti i file per la validazione, le rotte e la gestione degli errori
Some checks failed
Deploy to VPS / build (push) Failing after 18s
Deploy to VPS / deploy (push) Has been skipped

This commit is contained in:
AV77web 2026-06-24 15:54:46 +02:00
parent 51afb41128
commit 2e90ce1f4b
15 changed files with 1333 additions and 17 deletions

28
src/config/cors.ts Normal file
View file

@ -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",
],
});

View file

@ -1,10 +1,10 @@
import mysql from "mysql2/promise"; import mysql from "mysql2/promise";
const pool = mysql.createPool({ const pool = mysql.createPool({
host: process.env.DB_HOST ?? "127.0.0.1", host: process.env.DB_HOST,
port: Number(process.env.DB_PORT ?? 3307), port: Number(process.env.DB_PORT),
database: process.env.DB_NAME ?? "prenotazioni", database: process.env.DB_NAME,
user: process.env.DB_USER ?? "prenotazioni_user", user: process.env.DB_USER,
password: process.env.DB_PASSWORD ?? "", password: process.env.DB_PASSWORD ?? "",
waitForConnections: true, waitForConnections: true,
connectionLimit: 10, connectionLimit: 10,

80
src/errors/db.ts Normal file
View file

@ -0,0 +1,80 @@
export interface DbErrorResponse {
status: number;
error: string;
fields?: Record<string, string>;
}
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_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;
}

View file

@ -1,19 +1,35 @@
import cors from "cors"; import { ExpressAuth } from "@auth/express";
import dotenv from "dotenv"; import dotenv from "dotenv";
import express from "express"; import express from "express";
import { authConfig } from "./auth/config.js";
import { corsMiddleware } from "./config/cors.js";
import clientiRouter from "./routes/clienti.js";
import consegneRouter from "./routes/consegne.js";
import meRouter from "./routes/me.js";
import utentiRouter from "./routes/utenti.js";
import verificaConsegnaRouter from "./routes/verifica-consegna.js";
dotenv.config(); dotenv.config();
const app = express(); const app = express();
const port = Number(process.env.PORT) || 3005; const port = Number(process.env.PORT ?? 3005);
app.use(cors({ origin: process.env.FRONTEND_URL })); app.set("trust proxy", 1);
app.use(corsMiddleware);
app.use(express.json()); app.use(express.json());
app.options(/.*/, corsMiddleware);
app.get("/health", (_req, res) => { app.get("/api/health", (_req, res) => {
res.json({ status: "ok" }); res.json({ status: "ok" });
}); });
app.listen(port, () => { app.use("/api/verifica-consegna", verificaConsegnaRouter);
console.log(`Server listening on port ${port}`); app.use("/api/auth", ExpressAuth(authConfig));
app.use("/api/me", meRouter);
app.use("/api/utenti", utentiRouter);
app.use("/api/clienti", clientiRouter);
app.use("/api/consegne", consegneRouter);
app.listen(port, "0.0.0.0", () => {
console.log(`Server in ascolto sulla porta ${port}`);
}); });

View file

@ -1,6 +1,7 @@
import { getSession } from "@auth/express"; import { getSession } from "@auth/express";
import type { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from "express";
import { authConfig } from "../auth/config.js"; import { authConfig } from "../auth/config.js";
import type { Ruolo } from "../types/index.js";
export async function requireAuth(req: Request, res: Response, next: NextFunction) { export async function requireAuth(req: Request, res: Response, next: NextFunction) {
const session = await getSession(req, authConfig); const session = await getSession(req, authConfig);
@ -17,8 +18,8 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
next(); next();
} }
export function requireAdmin(req: Request, res: Response, next: NextFunction) { export function requireAmministratore(req: Request, res: Response, next: NextFunction) {
if (!req.user?.ruolo) { if (req.user?.ruolo !== "Amministratore") {
res.status(403).json({ error: "Operazione riservata agli amministratori" }); res.status(403).json({ error: "Operazione riservata agli amministratori" });
return; return;
} }
@ -32,7 +33,7 @@ declare global {
id: number; id: number;
email: string; email: string;
name: string; name: string;
ruolo: string; ruolo: Ruolo;
}; };
} }
} }

219
src/routes/clienti.ts Normal file
View file

@ -0,0 +1,219 @@
import { Router } from "express";
import type { ResultSetHeader, RowDataPacket } from "mysql2";
import pool from "../db/pool.js";
import { mapDbError } from "../errors/db.js";
import { requireAuth } from "../middleware/auth.js";
import {
validateClienteCreate,
validateClienteUpdate,
type ClienteInput,
} from "../validation/cliente.js";
import { validateBody } from "../validation/middleware.js";
const router = Router();
interface ClienteRow extends RowDataPacket {
ClienteID: number;
Nome: string;
Cognome: string;
Via: string;
Comune: string;
Provincia: string;
Telefono: string;
Email: string;
Note: string | null;
}
const SELECT_FIELDS =
"ClienteID, Nome, Cognome, Via, Comune, Provincia, Telefono, Email, Note";
function toClienteResponse(row: ClienteRow) {
return {
id: row.ClienteID,
nome: row.Nome,
cognome: row.Cognome,
via: row.Via,
comune: row.Comune,
provincia: row.Provincia,
telefono: row.Telefono,
email: row.Email,
note: row.Note,
};
}
async function findClienteById(id: number): Promise<ClienteRow | undefined> {
const [rows] = await pool.execute<ClienteRow[]>(
`SELECT ${SELECT_FIELDS} FROM cliente WHERE ClienteID = ?`,
[id]
);
return rows[0];
}
router.use(requireAuth);
router.get("/", async (_req, res) => {
const [rows] = await pool.execute<ClienteRow[]>(
`SELECT ${SELECT_FIELDS} FROM cliente ORDER BY Cognome, Nome`
);
res.json(rows.map(toClienteResponse));
});
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 cliente = await findClienteById(id);
if (!cliente) {
res.status(404).json({ error: "Cliente non trovato" });
return;
}
res.json(toClienteResponse(cliente));
});
router.post("/", validateBody(validateClienteCreate), async (_req, res) => {
const body = res.locals.validatedBody as ClienteInput;
try {
const [result] = await pool.execute<ResultSetHeader>(
`INSERT INTO cliente
(Nome, Cognome, Via, Comune, Provincia, Telefono, Email, Note)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
body.nome,
body.cognome,
body.via,
body.comune,
body.provincia,
body.telefono,
body.email,
body.note,
]
);
const created = await findClienteById(result.insertId);
if (!created) {
res.status(500).json({ error: "Errore durante la creazione del cliente" });
return;
}
res.status(201).json(toClienteResponse(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(validateClienteUpdate), 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 findClienteById(id);
if (!existing) {
res.status(404).json({ error: "Cliente non trovato" });
return;
}
const updates = res.locals.validatedBody as Partial<ClienteInput>;
const setClauses: string[] = [];
const values: (string | null)[] = [];
if (updates.nome !== undefined) {
setClauses.push("Nome = ?");
values.push(updates.nome);
}
if (updates.cognome !== undefined) {
setClauses.push("Cognome = ?");
values.push(updates.cognome);
}
if (updates.via !== undefined) {
setClauses.push("Via = ?");
values.push(updates.via);
}
if (updates.comune !== undefined) {
setClauses.push("Comune = ?");
values.push(updates.comune);
}
if (updates.provincia !== undefined) {
setClauses.push("Provincia = ?");
values.push(updates.provincia);
}
if (updates.telefono !== undefined) {
setClauses.push("Telefono = ?");
values.push(updates.telefono);
}
if (updates.email !== undefined) {
setClauses.push("Email = ?");
values.push(updates.email);
}
if (updates.note !== undefined) {
setClauses.push("Note = ?");
values.push(updates.note);
}
try {
await pool.execute<ResultSetHeader>(
`UPDATE cliente SET ${setClauses.join(", ")} WHERE ClienteID = ?`,
[...values, id]
);
const updated = await findClienteById(id);
if (!updated) {
res.status(500).json({ error: "Errore durante l'aggiornamento del cliente" });
return;
}
res.json(toClienteResponse(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;
}
const existing = await findClienteById(id);
if (!existing) {
res.status(404).json({ error: "Cliente non trovato" });
return;
}
try {
await pool.execute("DELETE FROM cliente WHERE ClienteID = ?", [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;

220
src/routes/consegne.ts Normal file
View file

@ -0,0 +1,220 @@
import { Router } from "express";
import type { ResultSetHeader, RowDataPacket } from "mysql2";
import pool from "../db/pool.js";
import { mapDbError } from "../errors/db.js";
import { requireAuth } from "../middleware/auth.js";
import { validateBody } from "../validation/middleware.js";
import {
validateConsegnaCreate,
validateConsegnaUpdate,
type ConsegnaInput,
} from "../validation/consegna.js";
const router = Router();
interface ConsegnaRow extends RowDataPacket {
ConsegnaID: number;
ClienteID: number;
DataRitiro: Date | string;
DataConsegna: Date | string;
Stato: string;
ChiaveConsegna: string;
ClienteNome: string;
ClienteCognome: string;
}
const SELECT_BASE = `
SELECT
c.ConsegnaID,
c.ClienteID,
c.DataRitiro,
c.DataConsegna,
c.Stato,
c.ChiaveConsegna,
cl.Nome AS ClienteNome,
cl.Cognome AS ClienteCognome
FROM consegna c
INNER JOIN cliente cl ON cl.ClienteID = c.ClienteID
`;
function formatDate(value: Date | string): string {
if (value instanceof Date) {
return value.toISOString().slice(0, 10);
}
return value.toString().slice(0, 10);
}
function toConsegnaResponse(row: ConsegnaRow) {
return {
id: row.ConsegnaID,
clienteId: row.ClienteID,
dataRitiro: formatDate(row.DataRitiro),
dataConsegna: formatDate(row.DataConsegna),
stato: row.Stato,
chiaveConsegna: row.ChiaveConsegna,
clienteNome: row.ClienteNome,
clienteCognome: row.ClienteCognome,
};
}
async function findConsegnaById(id: number): Promise<ConsegnaRow | undefined> {
const [rows] = await pool.execute<ConsegnaRow[]>(
`${SELECT_BASE} WHERE c.ConsegnaID = ?`,
[id]
);
return rows[0];
}
router.use(requireAuth);
router.get("/", async (_req, res) => {
const [rows] = await pool.execute<ConsegnaRow[]>(
`${SELECT_BASE} ORDER BY c.DataConsegna DESC, c.DataRitiro DESC`
);
res.json(rows.map(toConsegnaResponse));
});
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 consegna = await findConsegnaById(id);
if (!consegna) {
res.status(404).json({ error: "Consegna non trovata" });
return;
}
res.json(toConsegnaResponse(consegna));
});
router.post("/", validateBody(validateConsegnaCreate), async (_req, res) => {
const body = res.locals.validatedBody as ConsegnaInput;
try {
const [result] = await pool.execute<ResultSetHeader>(
`INSERT INTO consegna
(ClienteID, DataRitiro, DataConsegna, Stato, ChiaveConsegna)
VALUES (?, ?, ?, ?, ?)`,
[
body.clienteId,
body.dataRitiro,
body.dataConsegna,
body.stato,
body.chiaveConsegna,
]
);
const created = await findConsegnaById(result.insertId);
if (!created) {
res.status(500).json({ error: "Errore durante la creazione della consegna" });
return;
}
res.status(201).json(toConsegnaResponse(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(validateConsegnaUpdate), 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 findConsegnaById(id);
if (!existing) {
res.status(404).json({ error: "Consegna non trovata" });
return;
}
const updates = res.locals.validatedBody as Partial<ConsegnaInput>;
const setClauses: string[] = [];
const values: (string | number)[] = [];
if (updates.dataRitiro !== undefined) {
setClauses.push("DataRitiro = ?");
values.push(updates.dataRitiro);
}
if (updates.dataConsegna !== undefined) {
setClauses.push("DataConsegna = ?");
values.push(updates.dataConsegna);
}
if (updates.stato !== undefined) {
setClauses.push("Stato = ?");
values.push(updates.stato);
}
if (updates.chiaveConsegna !== undefined) {
setClauses.push("ChiaveConsegna = ?");
values.push(updates.chiaveConsegna);
}
if (updates.clienteId !== undefined) {
setClauses.push("ClienteID = ?");
values.push(updates.clienteId);
}
try {
await pool.execute<ResultSetHeader>(
`UPDATE consegna SET ${setClauses.join(", ")} WHERE ConsegnaID = ?`,
[...values, id]
);
const updated = await findConsegnaById(id);
if (!updated) {
res.status(500).json({ error: "Errore durante l'aggiornamento della consegna" });
return;
}
res.json(toConsegnaResponse(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;
}
const existing = await findConsegnaById(id);
if (!existing) {
res.status(404).json({ error: "Consegna non trovata" });
return;
}
try {
await pool.execute("DELETE FROM consegna WHERE ConsegnaID = ?", [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;

15
src/routes/me.ts Normal file
View file

@ -0,0 +1,15 @@
import { Router } from "express";
import { requireAuth } from "../middleware/auth.js";
const router = Router();
router.get("/", requireAuth, (req, res) => {
res.json({
id: req.user!.id,
email: req.user!.email,
name: req.user!.name,
ruolo: req.user!.ruolo,
});
});
export default router;

195
src/routes/utenti.ts Normal file
View file

@ -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<UtenteRow | undefined> {
const [rows] = await pool.execute<UtenteRow[]>(
"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<UtenteRow[]>(
"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<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) => {
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<ResultSetHeader>(
`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;

View file

@ -0,0 +1,81 @@
import { Router } from "express";
import type { RowDataPacket } from "mysql2";
import pool from "../db/pool.js";
import { CONSEGNA_CONSTRAINTS } from "../validation/constraints.js";
import {
normalizeString,
validateMaxLength,
validateRequired,
type FieldErrors,
} from "../validation/primitives.js";
const router = Router();
interface VerificaRow extends RowDataPacket {
DataRitiro: Date | string;
DataConsegna: Date | string;
Stato: string;
ClienteNome: string;
ClienteCognome: string;
Comune: string;
}
function formatDate(value: Date | string): string {
if (value instanceof Date) {
return value.toISOString().slice(0, 10);
}
return value.toString().slice(0, 10);
}
router.get("/", async (req, res) => {
const chiave = normalizeString(req.query.chiave).toUpperCase();
const errors: FieldErrors = {};
validateRequired(errors, "chiave", chiave, "La chiave consegna è obbligatoria");
validateMaxLength(
errors,
"chiave",
chiave,
CONSEGNA_CONSTRAINTS.chiaveConsegna.maxLength,
"Chiave consegna"
);
if (Object.keys(errors).length > 0) {
res.status(400).json({
error: Object.values(errors)[0],
fields: errors,
});
return;
}
const [rows] = await pool.execute<VerificaRow[]>(
`SELECT
c.DataRitiro,
c.DataConsegna,
c.Stato,
cl.Nome AS ClienteNome,
cl.Cognome AS ClienteCognome,
cl.Comune
FROM consegna c
INNER JOIN cliente cl ON cl.ClienteID = c.ClienteID
WHERE c.ChiaveConsegna = ?`,
[chiave]
);
const consegna = rows[0];
if (!consegna) {
res.status(404).json({ error: "Consegna non trovata" });
return;
}
res.json({
dataRitiro: formatDate(consegna.DataRitiro),
dataConsegna: formatDate(consegna.DataConsegna),
stato: consegna.Stato,
clienteNome: consegna.ClienteNome,
clienteCognome: consegna.ClienteCognome,
comune: consegna.Comune,
});
});
export default router;

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

@ -0,0 +1,251 @@
//==============================================
// File: cliente.ts
// Validazione dati cliente.
// author: "villari.andrea@libero.it"
// version: "1.0.0 2026-06-18"
//==============================================
import { CLIENTE_CONSTRAINTS } from "./constraints.js";
import {
buildResult,
normalizeEmail,
normalizeString,
validateEmailFormat,
validateMaxLength,
validateRequired,
type FieldErrors,
type ValidationResult,
} from "./primitives.js";
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);
}

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

@ -0,0 +1,189 @@
//================================================
// File: consegna.ts
// Validazione dati consegna.
// author: "villari.andrea@libero.it"
// version: "1.0.0 2026-06-18"
//================================================
import type { StatoConsegna } from "../types/index.js";
import { CONSEGNA_CONSTRAINTS } from "./constraints.js";
import {
buildResult,
normalizePositiveInt,
normalizeString,
validateDate,
validateEnum,
validateMaxLength,
validatePositiveInt,
validateRequired,
type FieldErrors,
type ValidationResult,
} from "./primitives.js";
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

@ -19,8 +19,8 @@ export const UTENTE_CONSTRAINTS = {
nome: { maxLength: 100, required: true }, nome: { maxLength: 100, required: true },
cognome: { maxLength: 100, required: true }, cognome: { maxLength: 100, required: true },
via: {maxLength: 100, required: true}, via: {maxLength: 100, required: true},
Comune: {maxLength: 100, required: true}, comune: {maxLength: 100, required: true},
Provincia: {maxLength: 2, required: true}, provincia: {maxLength: 2, required: true},
telefono: { maxLength: 30, required: true }, telefono: { maxLength: 30, required: true },
email: { maxLength: 255, required: true }, email: { maxLength: 255, required: true },
note: { required: false }, note: { required: false },
@ -34,7 +34,7 @@ export const UTENTE_CONSTRAINTS = {
allowedValues: ["da ritirare", "in consegna", "in giacenza", "in deposito"], allowedValues: ["da ritirare", "in consegna", "in giacenza", "in deposito"],
required: true, required: true,
}, },
chiaveConsegna: { maxLength: 8, required: true }, chiaveconsegna: { maxLength: 8, required: true },
clienteId: { required: true }, clienteId: { required: true },
} as const; } as const;

View 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();
};
}

View file

@ -136,7 +136,7 @@ export function validateUtenteCreate(
} }
export function validateUTENTEUpdate( export function validateUtenteUpdate(
raw: Record<string, unknown> raw: Record<string, unknown>
): ValidationResult<UtenteUpdateInput> { ): ValidationResult<UtenteUpdateInput> {
const errors: FieldErrors = {}; const errors: FieldErrors = {};