aggiunti i file per la validazione, le rotte e la gestione degli errori
This commit is contained in:
parent
51afb41128
commit
2e90ce1f4b
15 changed files with 1333 additions and 17 deletions
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://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",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
@ -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
80
src/errors/db.ts
Normal 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;
|
||||||
|
}
|
||||||
28
src/index.ts
28
src/index.ts
|
|
@ -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}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
219
src/routes/clienti.ts
Normal 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
220
src/routes/consegne.ts
Normal 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
15
src/routes/me.ts
Normal 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
195
src/routes/utenti.ts
Normal 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;
|
||||||
81
src/routes/verifica-consegna.ts
Normal file
81
src/routes/verifica-consegna.ts
Normal 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
251
src/validation/cliente.ts
Normal 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
189
src/validation/consegna.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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 = {};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue