completata autenticazione e autorizzazione
All checks were successful
Deploy to VPS / build (push) Successful in 16s
Deploy to VPS / deploy (push) Successful in 20s

This commit is contained in:
AV77web 2026-06-22 17:37:55 +02:00
parent f46a11d762
commit 51afb41128
8 changed files with 580 additions and 8 deletions

View file

@ -24,13 +24,12 @@ INSERT INTO utente (Nome, Cognome, Email, PasswordHash, Ruolo) VALUES
-- ------------------------------------------------------------
-- Clienti
-- ------------------------------------------------------------
INSERT INTO cliente (Nome, Cognome, Comune, Provincia, Telefono, Email, Note) VALUES
('Mario', 'Rossi', 'Milano', 'MI', '3331112233', 'mario.rossi@email.it', 'Consegne preferibilmente al mattino'),
('Laura', 'Verdi', 'Monza', 'MB', '3334445566', 'laura.verdi@email.it', NULL),
('Marco', 'Neri', 'Bergamo', 'BG', '3337778899', 'marco.neri@email.it', 'Citofono non funzionante, chiamare al telefono'),
('Giulia', 'Ferrari', 'Brescia', 'BS', '3332223344', 'giulia.ferrari@email.it', 'Ritiro abituale presso ufficio'),
('Paolo', 'Romano', 'Como', 'CO', '3339988776', 'paolo.romano@email.it', 'Richiede firma del destinatario');
INSERT INTO cliente (Nome, Cognome, Via, Comune, Provincia, Telefono, Email, Note) VALUES
('Mario', 'Rossi', 'Via Roma 12', 'Milano', 'MI', '3331112233', 'mario.rossi@email.it', 'Consegne preferibilmente al mattino'),
('Laura', 'Verdi', 'Corso Italia 45', 'Monza', 'MB', '3334445566', 'laura.verdi@email.it', NULL),
('Marco', 'Neri', 'Via Garibaldi 8', 'Bergamo', 'BG', '3337778899', 'marco.neri@email.it', 'Citofono non funzionante, chiamare al telefono'),
('Giulia', 'Ferrari', 'Viale Venezia 101', 'Brescia', 'BS', '3332223344', 'giulia.ferrari@email.it', 'Ritiro abituale presso ufficio'),
('Paolo', 'Romano', 'Via Manzoni 3', 'Como', 'CO', '3339988776', 'paolo.romano@email.it', 'Richiede firma del destinatario');
-- ------------------------------------------------------------
-- Consegne
-- ClienteID: 1=Mario, 2=Laura, 3=Marco, 4=Giulia, 5=Paolo

81
src/auth/config.ts Normal file
View file

@ -0,0 +1,81 @@
import type { ExpressAuthConfig } from "@auth/express";
import Credentials from "@auth/express/providers/credentials";
import bcrypt from "bcryptjs";
import type { RowDataPacket } from "mysql2";
import pool from "../db/pool.js";
import type { Utente } from "../types/index.js";
import { validateLoginInput } from "../validation/utente.js";
const isProduction = process.env.NODE_ENV === "production";
const crossOriginCookieOptions = {
httpOnly: true,
sameSite: "none" as const,
secure: true,
path: "/",
};
export const authConfig: ExpressAuthConfig = {
basePath: "/api/auth",
trustHost: true,
secret: process.env.AUTH_SECRET,
useSecureCookies: isProduction,
session: { strategy: "jwt" },
pages: { signIn: "/api/auth/error" },
cookies: isProduction
? {
sessionToken: { options: crossOriginCookieOptions },
csrfToken: { options: { ...crossOriginCookieOptions, httpOnly: false } },
callbackUrl: { options: crossOriginCookieOptions },
}
: undefined,
providers: [
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const validation = validateLoginInput(credentials ?? {});
if (!validation.success) return null;
const { email, password } = validation.data;
const [rows] = await pool.execute<RowDataPacket[]>(
"SELECT UtenteID, Nome, Cognome, Email, PasswordHash, Ruolo FROM utente WHERE Email = ?",
[email]
);
const utente = rows[0] as Utente | undefined;
if (!utente) return null;
const valid = await bcrypt.compare(password, utente.PasswordHash);
if (!valid) return null;
return {
id: String(utente.UtenteID),
email: utente.Email,
name: `${utente.Nome} ${utente.Cognome}`,
ruolo: utente.Ruolo,
};
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.ruolo = user.ruolo;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
session.user.ruolo = token.ruolo!;
}
return session;
},
},
};

13
src/db/pool.ts Normal file
View file

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

39
src/middleware/auth.ts Normal file
View file

@ -0,0 +1,39 @@
import { getSession } from "@auth/express";
import type { NextFunction, Request, Response } from "express";
import { authConfig } from "../auth/config.js";
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
const session = await getSession(req, authConfig);
if (!session?.user?.id) {
res.status(401).json({ error: "Autenticazione richiesta" });
return;
}
req.user = {
id: Number(session.user.id),
email: session.user.email,
name: session.user.name,
ruolo: session.user.ruolo,
};
next();
}
export function requireAdmin(req: Request, res: Response, next: NextFunction) {
if (!req.user?.ruolo) {
res.status(403).json({ error: "Operazione riservata agli amministratori" });
return;
}
next();
}
declare global {
namespace Express {
interface Request {
user?: {
id: number;
email: string;
name: string;
ruolo: string;
};
}
}
}

22
src/types/auth.d.ts vendored Normal file
View file

@ -0,0 +1,22 @@
import type { Ruolo } from "./index.js";
declare module "@auth/core/types" {
interface User {
ruolo?: Ruolo;
}
interface Session {
user: {
id: string;
email: string;
name: string;
ruolo: Ruolo;
};
}
}
declare module "@auth/core/jwt" {
interface JWT {
id?: string;
ruolo?: Ruolo;
}
}

View file

@ -16,8 +16,8 @@ export interface Utente {
export interface Cliente {
ClienteID: number;
Nome: string;
Via: string;
Cognome: string;
Via: string;
Comune: string;
Provincia: string;
Telefono: string;

View file

@ -0,0 +1,198 @@
//==========================================
// File: primitives.ts
// Funzioni di validazione dei dati prmitivi
// author: "villari.andrea@libero.it"
// version: "1.0.0 2026-06-18"
//==========================================
export type FieldErrors = Record<string, string>;
export type ValidationResult<T> =
| { success: true; data: T }
| { success: false; errors: FieldErrors };
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export function normalizeString(value: unknown): string {
return value?.toString().trim() ?? "";
}
export function normalizeEmail(value: unknown): string {
return normalizeString(value).toLowerCase();
}
export function normalizeBoolean(value: unknown, defaultValue = false): boolean {
if (value === true || value === "true") return true;
if (value === false || value === "false") return false;
return defaultValue;
}
export function normalizeDecimal(value: unknown): number {
if (typeof value === "number") return value;
const normalized = value?.toString().trim().replace(",", ".") ?? "";
if (!normalized) return Number.NaN;
return Number(normalized);
}
export function normalizePositiveInt(value: unknown): number {
if (typeof value === "number") return value;
const parsed = Number(value);
return Number.isInteger(parsed) && parsed > 0 ? parsed : Number.NaN;
}
export function normalizeTime(value: unknown): string {
const str = normalizeString(value);
if (!str) return "";
if (/^\d{2}:\d{2}$/.test(str)) return `${str}:00`;
return str;
}
export function validateRequired(
errors: FieldErrors,
field: string,
value: string,
message: string
): void {
if (!value) errors[field] = message;
}
export function validateMaxLength(
errors: FieldErrors,
field: string,
value: string,
maxLength: number,
label: string
): void {
if (value.length > maxLength) {
errors[field] = `${label} deve contenere al massimo ${maxLength} caratteri`;
}
}
export function validateMinLength(
errors: FieldErrors,
field: string,
value: string,
minLength: number,
label: string
): void {
if (value && value.length < minLength) {
errors[field] = `${label} deve contenere almeno ${minLength} caratteri`;
}
}
export function validateEmailFormat(
errors: FieldErrors,
field: string,
value: string
): void {
if (value && !EMAIL_REGEX.test(value)) {
errors[field] = "Formato email non valido";
}
}
export function validateEnum(
errors: FieldErrors,
field: string,
value: string,
allowedValues: readonly string[],
label: string
): void {
if (value && !allowedValues.includes(value)) {
errors[field] = `${label} non valido`;
}
}
export function validateBoolean(
errors: FieldErrors,
field: string,
value: unknown,
label: string
): void {
if (value === undefined || value === null || value === "") return;
if (
value !== true &&
value !== false &&
value !== "true" &&
value !== "false"
) {
errors[field] = `${label} non valido`;
}
}
const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
const TIME_REGEX = /^\d{2}:\d{2}:\d{2}$/;
export function validateDate(
errors: FieldErrors,
field: string,
value: string,
label: string
): void {
if (!value) return;
if (!DATE_REGEX.test(value)) {
errors[field] = `${label} non valida`;
return;
}
const parsed = new Date(`${value}T00:00:00`);
if (Number.isNaN(parsed.getTime())) {
errors[field] = `${label} non valida`;
}
}
export function validateTimeFormat(
errors: FieldErrors,
field: string,
value: string,
label: string
): void {
if (!value) return;
if (!TIME_REGEX.test(value)) {
errors[field] = `${label} non valida`;
}
}
export function validatePositiveInt(
errors: FieldErrors,
field: string,
value: number,
label: string,
required = true
): void {
if (Number.isNaN(value) || value <= 0) {
errors[field] = required ? `${label} obbligatorio` : `${label} non valido`;
}
}
export function validateTimeRange(
errors: FieldErrors,
startField: string,
endField: string,
start: string,
end: string
): void {
if (!start || !end) return;
if (start >= end) {
errors[endField] =
"L'ora di fine deve essere successiva all'ora di inizio";
}
}
export function validateDecimalPositive(
errors: FieldErrors,
field: string,
value: unknown,
label: string,
min = 0.01
): void {
if (value === undefined || value === null || value === "") return;
const num = typeof value === "number" ? value : Number(value);
if (Number.isNaN(num) || num < min) {
errors[field] = `${label} deve essere almeno ${min}`;
}
}
export function buildResult<T>(data: T, errors: FieldErrors): ValidationResult<T> {
return Object.keys(errors).length > 0
? { success: false, errors }
: { success: true, data };
}

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

@ -0,0 +1,220 @@
import type { Ruolo } from "../types/index.js";
import { UTENTE_CONSTRAINTS } from "./constraints.js";
import {
buildResult,
normalizeEmail,
normalizeString,
validateEmailFormat,
validateEnum,
validateMaxLength,
validateMinLength,
validateRequired,
type FieldErrors,
type ValidationResult,
} from "./primitives.js";
export interface LoginInput {
email: string;
password: string;
}
export interface UtenteCreateInput {
nome: string;
cognome: string;
email: string;
password: string;
ruolo: Ruolo;
}
export type UtenteUpdateInput = Partial<UtenteCreateInput>;
export function normalizeLoginInput(raw: Record<string, unknown>): LoginInput {
return {
email: normalizeEmail(raw.email),
password: raw.password?.toString() ?? "",
};
}
export function validateLoginInput(
raw: Record<string, unknown>
): ValidationResult<LoginInput> {
const data = normalizeLoginInput(raw);
const errors: FieldErrors = {};
validateRequired(errors, "email", data.email, "Email obbligatoria");
validateMaxLength(
errors,
"email",
data.email,
UTENTE_CONSTRAINTS.email.maxLength,
"Email"
);
validateEmailFormat(errors, "email", data.email);
validateRequired(errors, "password", data.password, "Password obbligatoria");
validateMaxLength(
errors,
"password",
data.password,
UTENTE_CONSTRAINTS.password.maxLength,
"Password"
);
return buildResult(data, errors);
}
function validateUtenteProfileFields(
data: UtenteCreateInput,
errors: FieldErrors
): void {
validateRequired(errors, "nome", data.nome, "Nome obbligatorio");
validateMaxLength(
errors,
"nome",
data.nome,
UTENTE_CONSTRAINTS.nome.maxLength,
"Nome"
);
validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio");
validateMaxLength(
errors,
"cognome",
data.cognome,
UTENTE_CONSTRAINTS.cognome.maxLength,
"Cognome"
);
validateRequired(errors, "email", data.email, "Email obbligatoria");
validateMaxLength(
errors,
"email",
data.email,
UTENTE_CONSTRAINTS.email.maxLength,
"Email"
);
validateEmailFormat(errors, "email", data.email);
validateRequired(errors, "password", data.password, "Password obbligatoria");
validateMinLength(
errors,
"password",
data.password,
UTENTE_CONSTRAINTS.password.minLength,
"Password"
);
validateMaxLength(
errors,
"password",
data.password,
UTENTE_CONSTRAINTS.password.maxLength,
"Password"
);
validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio");
validateEnum(
errors,
"ruolo",
data.ruolo,
UTENTE_CONSTRAINTS.ruolo.allowedValues,
"Ruolo"
);
}
export function validateUtenteCreate(
raw: Record<string, unknown>
): ValidationResult<UtenteCreateInput> {
const data: UtenteCreateInput = {
nome: normalizeString(raw.nome),
cognome: normalizeString(raw.cognome),
email: normalizeEmail(raw.email),
password: raw.password?.toString() ?? "",
ruolo: normalizeString(raw.ruolo) as Ruolo,
};
const errors: FieldErrors = {};
validateUtenteProfileFields(data, errors);
return buildResult(data, errors);
}
export function validateUTENTEUpdate(
raw: Record<string, unknown>
): ValidationResult<UtenteUpdateInput> {
const errors: FieldErrors = {};
const data: UtenteUpdateInput = {};
if ("nome" in raw) {
data.nome = normalizeString(raw.nome);
validateRequired(errors, "nome", data.nome, "Nome obbligatorio");
validateMaxLength(
errors,
"nome",
data.nome,
UTENTE_CONSTRAINTS.nome.maxLength,
"Nome"
);
}
if ("cognome" in raw) {
data.cognome = normalizeString(raw.cognome);
validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio");
validateMaxLength(
errors,
"cognome",
data.cognome,
UTENTE_CONSTRAINTS.cognome.maxLength,
"Cognome"
);
}
if ("email" in raw) {
data.email = normalizeEmail(raw.email);
validateRequired(errors, "email", data.email, "Email obbligatoria");
validateMaxLength(
errors,
"email",
data.email,
UTENTE_CONSTRAINTS.email.maxLength,
"Email"
);
validateEmailFormat(errors, "email", data.email);
}
if ("password" in raw) {
const password = raw.password?.toString() ?? "";
if (password) {
data.password = password;
validateMinLength(
errors,
"password",
password,
UTENTE_CONSTRAINTS.password.minLength,
"Password"
);
validateMaxLength(
errors,
"password",
password,
UTENTE_CONSTRAINTS.password.maxLength,
"Password"
);
}
}
if ("ruolo" in raw) {
data.ruolo = normalizeString(raw.ruolo) as Ruolo;
validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio");
validateEnum(
errors,
"ruolo",
data.ruolo,
UTENTE_CONSTRAINTS.ruolo.allowedValues,
"Ruolo"
);
}
if (Object.keys(data).length === 0) {
errors._form = "Nessun campo da aggiornare";
}
return buildResult(data, errors);
}