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";
|
||||
|
||||
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",
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD ?? "",
|
||||
waitForConnections: true,
|
||||
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 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();
|
||||
|
||||
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.options(/.*/, corsMiddleware);
|
||||
|
||||
app.get("/health", (_req, res) => {
|
||||
app.get("/api/health", (_req, res) => {
|
||||
res.json({ status: "ok" });
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server listening on port ${port}`);
|
||||
app.use("/api/verifica-consegna", verificaConsegnaRouter);
|
||||
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 type { NextFunction, Request, Response } from "express";
|
||||
import { authConfig } from "../auth/config.js";
|
||||
import type { Ruolo } from "../types/index.js";
|
||||
|
||||
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const session = await getSession(req, authConfig);
|
||||
|
|
@ -17,8 +18,8 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
|
|||
next();
|
||||
}
|
||||
|
||||
export function requireAdmin(req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.user?.ruolo) {
|
||||
export function requireAmministratore(req: Request, res: Response, next: NextFunction) {
|
||||
if (req.user?.ruolo !== "Amministratore") {
|
||||
res.status(403).json({ error: "Operazione riservata agli amministratori" });
|
||||
return;
|
||||
}
|
||||
|
|
@ -32,7 +33,7 @@ declare global {
|
|||
id: number;
|
||||
email: 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 },
|
||||
cognome: { maxLength: 100, required: true },
|
||||
via: {maxLength: 100, required: true},
|
||||
Comune: {maxLength: 100, required: true},
|
||||
Provincia: {maxLength: 2, required: true},
|
||||
comune: {maxLength: 100, required: true},
|
||||
provincia: {maxLength: 2, required: true},
|
||||
telefono: { maxLength: 30, required: true },
|
||||
email: { maxLength: 255, required: true },
|
||||
note: { required: false },
|
||||
|
|
@ -34,7 +34,7 @@ export const UTENTE_CONSTRAINTS = {
|
|||
allowedValues: ["da ritirare", "in consegna", "in giacenza", "in deposito"],
|
||||
required: true,
|
||||
},
|
||||
chiaveConsegna: { maxLength: 8, required: true },
|
||||
chiaveconsegna: { maxLength: 8, required: true },
|
||||
clienteId: { 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();
|
||||
};
|
||||
}
|
||||
|
|
@ -136,7 +136,7 @@ export function validateUtenteCreate(
|
|||
}
|
||||
|
||||
|
||||
export function validateUTENTEUpdate(
|
||||
export function validateUtenteUpdate(
|
||||
raw: Record<string, unknown>
|
||||
): ValidationResult<UtenteUpdateInput> {
|
||||
const errors: FieldErrors = {};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue