modifica login e inserimento creazione nuovo utente
This commit is contained in:
parent
79cfa791d2
commit
03835bf426
6 changed files with 304 additions and 112 deletions
|
|
@ -1,10 +1,6 @@
|
|||
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";
|
||||
import { authenticateUser } from "./credentials.js";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
|
|
@ -36,28 +32,18 @@ export const authConfig: ExpressAuthConfig = {
|
|||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
const validation = validateLoginInput(credentials ?? {});
|
||||
const email = credentials?.email?.toString().trim().toLowerCase() ?? "";
|
||||
const password = credentials?.password?.toString() ?? "";
|
||||
if (!email || !password) return null;
|
||||
|
||||
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;
|
||||
const user = await authenticateUser(email, password);
|
||||
if (!user) return null;
|
||||
|
||||
return {
|
||||
id: String(utente.UtenteID),
|
||||
email: utente.Email,
|
||||
name: `${utente.Nome} ${utente.Cognome}`,
|
||||
ruolo: utente.Ruolo,
|
||||
id: String(user.id),
|
||||
email: user.email,
|
||||
name: `${user.nome} ${user.cognome}`,
|
||||
ruolo: user.ruolo,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
45
src/auth/credentials.ts
Normal file
45
src/auth/credentials.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
import pool from "../db/pool.js";
|
||||
import type { Ruolo } from "../types/index.js";
|
||||
|
||||
interface UtenteAuthRow extends RowDataPacket {
|
||||
UtenteID: number;
|
||||
Nome: string;
|
||||
Cognome: string;
|
||||
Email: string;
|
||||
PasswordHash: string;
|
||||
Ruolo: Ruolo;
|
||||
}
|
||||
|
||||
export interface AuthenticatedUser {
|
||||
id: number;
|
||||
nome: string;
|
||||
cognome: string;
|
||||
email: string;
|
||||
ruolo: Ruolo;
|
||||
}
|
||||
|
||||
export async function authenticateUser(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<AuthenticatedUser | null> {
|
||||
const [rows] = await pool.execute<UtenteAuthRow[]>(
|
||||
"SELECT UtenteID, Nome, Cognome, Email, PasswordHash, Ruolo FROM utente WHERE Email = ?",
|
||||
[email]
|
||||
);
|
||||
|
||||
const utente = rows[0];
|
||||
if (!utente) return null;
|
||||
|
||||
const valid = await bcrypt.compare(password, utente.PasswordHash);
|
||||
if (!valid) return null;
|
||||
|
||||
return {
|
||||
id: utente.UtenteID,
|
||||
nome: utente.Nome,
|
||||
cognome: utente.Cognome,
|
||||
email: utente.Email,
|
||||
ruolo: utente.Ruolo,
|
||||
};
|
||||
}
|
||||
37
src/auth/session.ts
Normal file
37
src/auth/session.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { encode } from "@auth/core/jwt";
|
||||
import type { Response } from "express";
|
||||
import type { AuthenticatedUser } from "./credentials.js";
|
||||
|
||||
function getSessionCookieName(): string {
|
||||
return process.env.NODE_ENV === "production"
|
||||
? "__Secure-authjs.session-token"
|
||||
: "authjs.session-token";
|
||||
}
|
||||
|
||||
export async function setAuthSession(
|
||||
res: Response,
|
||||
user: AuthenticatedUser
|
||||
): Promise<void> {
|
||||
const cookieName = getSessionCookieName();
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
const token = await encode({
|
||||
token: {
|
||||
name: `${user.nome} ${user.cognome}`,
|
||||
email: user.email,
|
||||
sub: String(user.id),
|
||||
id: String(user.id),
|
||||
ruolo: user.ruolo,
|
||||
},
|
||||
secret: process.env.AUTH_SECRET!,
|
||||
salt: cookieName,
|
||||
maxAge: 30 * 24 * 60 * 60,
|
||||
});
|
||||
|
||||
res.cookie(cookieName, token, {
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: isProduction ? "none" : "lax",
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
|
@ -343,6 +343,63 @@ export const openApiSpec = {
|
|||
},
|
||||
},
|
||||
},
|
||||
"/api/utenti/login": {
|
||||
post: {
|
||||
tags: ["Utenti"],
|
||||
summary: "Login utente",
|
||||
description:
|
||||
"Autentica l'utente con email e password. Imposta il cookie di sessione.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["email", "password"],
|
||||
properties: {
|
||||
email: { type: "string", format: "email" },
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Login riuscito",
|
||||
content: { "application/json": { schema: utenteSchema } },
|
||||
},
|
||||
400: responses.validationError,
|
||||
401: { description: "Credenziali non valide" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/utenti/register": {
|
||||
post: {
|
||||
tags: ["Utenti"],
|
||||
summary: "Registra utente",
|
||||
description:
|
||||
"Crea un nuovo utente (Dipendente o Referente Academy). Richiede autenticazione Referente Academy.",
|
||||
security: [{ cookieAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/UtenteCreate" },
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
description: "Utente registrato",
|
||||
content: { "application/json": { schema: utenteSchema } },
|
||||
},
|
||||
400: responses.validationError,
|
||||
403: responses.forbidden,
|
||||
409: { description: "Email già registrata" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/utenti/{id}": {
|
||||
get: {
|
||||
tags: ["Utenti"],
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import { Router } from "express";
|
||||
import type { ResultSetHeader, RowDataPacket } from "mysql2";
|
||||
import { authenticateUser } from "../auth/credentials.js";
|
||||
import { setAuthSession } from "../auth/session.js";
|
||||
import pool from "../db/pool.js";
|
||||
import { mapDbError } from "../errors/db.js";
|
||||
import { requireAuth, requireReferenteAcademy } from "../middleware/auth.js";
|
||||
|
|
@ -14,8 +16,10 @@ import type { Ruolo } from "../types/index.js";
|
|||
import { UTENTE_CONSTRAINTS } from "../validation/constraints.js";
|
||||
import { validateBody } from "../validation/middleware.js";
|
||||
import {
|
||||
validateLoginInput,
|
||||
validateUtenteCreate,
|
||||
validateUtenteUpdate,
|
||||
type LoginInput,
|
||||
type UtenteCreateInput,
|
||||
type UtenteUpdateInput,
|
||||
} from "../validation/utente.js";
|
||||
|
|
@ -48,6 +52,127 @@ async function findUtenteById(id: number): Promise<UtenteRow | undefined> {
|
|||
return rows[0];
|
||||
}
|
||||
|
||||
async function insertUtente(body: UtenteCreateInput): Promise<UtenteRow> {
|
||||
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) {
|
||||
throw new Error("Errore durante la creazione dell'utente");
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
async function applyUtenteUpdate(
|
||||
id: number,
|
||||
updates: UtenteUpdateInput
|
||||
): Promise<UtenteRow> {
|
||||
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);
|
||||
}
|
||||
|
||||
await pool.execute<ResultSetHeader>(
|
||||
`UPDATE utente SET ${setClauses.join(", ")} WHERE UtenteID = ?`,
|
||||
[...values, id]
|
||||
);
|
||||
|
||||
const updated = await findUtenteById(id);
|
||||
if (!updated) {
|
||||
throw new Error("Errore durante l'aggiornamento dell'utente");
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
function handleUtenteDbError(
|
||||
res: import("express").Response,
|
||||
error: unknown
|
||||
): boolean {
|
||||
const mapped = mapDbError(error);
|
||||
if (mapped) {
|
||||
res.status(mapped.status).json({
|
||||
error: mapped.error,
|
||||
fields: mapped.fields,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function createUtenteHandler(
|
||||
body: UtenteCreateInput,
|
||||
res: import("express").Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const created = await insertUtente(body);
|
||||
res.status(201).json(toUtenteResponse(created));
|
||||
} catch (error) {
|
||||
if (handleUtenteDbError(res, error)) return;
|
||||
if (error instanceof Error && error.message.includes("creazione")) {
|
||||
res.status(500).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
router.post("/login", validateBody(validateLoginInput), async (_req, res) => {
|
||||
const { email, password } = res.locals.validatedBody as LoginInput;
|
||||
|
||||
const user = await authenticateUser(email, password);
|
||||
if (!user) {
|
||||
res.status(401).json({ error: "Credenziali non valide" });
|
||||
return;
|
||||
}
|
||||
|
||||
await setAuthSession(res, user);
|
||||
res.json({
|
||||
id: user.id,
|
||||
nome: user.nome,
|
||||
cognome: user.cognome,
|
||||
email: user.email,
|
||||
ruolo: user.ruolo,
|
||||
});
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/register",
|
||||
requireAuth,
|
||||
requireReferenteAcademy,
|
||||
validateBody(validateUtenteCreate),
|
||||
async (_req, res) => {
|
||||
await createUtenteHandler(
|
||||
res.locals.validatedBody as UtenteCreateInput,
|
||||
res
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
router.use(requireAuth, requireReferenteAcademy);
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
|
|
@ -90,33 +215,10 @@ router.get("/:id", async (req, res) => {
|
|||
});
|
||||
|
||||
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;
|
||||
}
|
||||
await createUtenteHandler(
|
||||
res.locals.validatedBody as UtenteCreateInput,
|
||||
res
|
||||
);
|
||||
});
|
||||
|
||||
router.put("/:id", validateBody(validateUtenteUpdate), async (req, res) => {
|
||||
|
|
@ -133,51 +235,14 @@ router.put("/:id", validateBody(validateUtenteUpdate), async (req, res) => {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const updated = await applyUtenteUpdate(id, updates);
|
||||
res.json(toUtenteResponse(updated));
|
||||
} catch (error) {
|
||||
const mapped = mapDbError(error);
|
||||
if (mapped) {
|
||||
res.status(mapped.status).json({
|
||||
error: mapped.error,
|
||||
fields: mapped.fields,
|
||||
});
|
||||
if (handleUtenteDbError(res, error)) return;
|
||||
if (error instanceof Error && error.message.includes("aggiornamento")) {
|
||||
res.status(500).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -62,8 +62,8 @@ export function validateLoginInput(
|
|||
return buildResult(data, errors);
|
||||
}
|
||||
|
||||
function validateUtenteProfileFields(
|
||||
data: UtenteCreateInput,
|
||||
function validateNomeCognomeEmail(
|
||||
data: Pick<UtenteCreateInput, "nome" | "cognome" | "email">,
|
||||
errors: FieldErrors
|
||||
): void {
|
||||
validateRequired(errors, "nome", data.nome, "Nome obbligatorio");
|
||||
|
|
@ -91,23 +91,39 @@ function validateUtenteProfileFields(
|
|||
"Email"
|
||||
);
|
||||
validateEmailFormat(errors, "email", data.email);
|
||||
}
|
||||
|
||||
validateRequired(errors, "password", data.password, "Password obbligatoria");
|
||||
function validatePasswordField(
|
||||
errors: FieldErrors,
|
||||
password: string,
|
||||
options: { required: boolean }
|
||||
): void {
|
||||
if (options.required) {
|
||||
validateRequired(errors, "password", password, "Password obbligatoria");
|
||||
}
|
||||
if (!password) return;
|
||||
validateMinLength(
|
||||
errors,
|
||||
"password",
|
||||
data.password,
|
||||
password,
|
||||
UTENTE_CONSTRAINTS.password.minLength,
|
||||
"Password"
|
||||
);
|
||||
validateMaxLength(
|
||||
errors,
|
||||
"password",
|
||||
data.password,
|
||||
password,
|
||||
UTENTE_CONSTRAINTS.password.maxLength,
|
||||
"Password"
|
||||
);
|
||||
}
|
||||
|
||||
function validateUtenteProfileFields(
|
||||
data: UtenteCreateInput,
|
||||
errors: FieldErrors
|
||||
): void {
|
||||
validateNomeCognomeEmail(data, errors);
|
||||
validatePasswordField(errors, data.password, { required: true });
|
||||
validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio");
|
||||
validateEnum(
|
||||
errors,
|
||||
|
|
@ -135,7 +151,6 @@ export function validateUtenteCreate(
|
|||
return buildResult(data, errors);
|
||||
}
|
||||
|
||||
|
||||
export function validateUtenteUpdate(
|
||||
raw: Record<string, unknown>
|
||||
): ValidationResult<UtenteUpdateInput> {
|
||||
|
|
@ -183,20 +198,7 @@ export function validateUtenteUpdate(
|
|||
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"
|
||||
);
|
||||
validatePasswordField(errors, password, { required: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue