743 lines
22 KiB
TypeScript
743 lines
22 KiB
TypeScript
//======================================================
|
||
// File: openapi.ts
|
||
// Script per la gestione dello schema di configurazione
|
||
// di swagger.
|
||
// @author: "villari.andrea@libero.it"
|
||
// @version: "1.0.0 2026-07-14"
|
||
//======================================================
|
||
|
||
const errorSchema = {
|
||
type: "object",
|
||
properties: {
|
||
error: { type: "string" },
|
||
fields: {
|
||
type: "object",
|
||
additionalProperties: { type: "string" },
|
||
},
|
||
},
|
||
} as const;
|
||
|
||
const utenteSchema = {
|
||
type: "object",
|
||
properties: {
|
||
id: { type: "integer" },
|
||
nome: { type: "string" },
|
||
cognome: { type: "string" },
|
||
email: { type: "string", format: "email" },
|
||
ruolo: { type: "string", enum: ["Dipendente", "Referente Academy"] },
|
||
},
|
||
} as const;
|
||
|
||
const corsoSchema = {
|
||
type: "object",
|
||
properties: {
|
||
id: { type: "integer" },
|
||
titolo: { type: "string" },
|
||
descrizione: { type: "string", nullable: true },
|
||
categoria: { type: "string" },
|
||
durataOre: { type: "integer", minimum: 1 },
|
||
obbligatorio: { type: "boolean" },
|
||
attivo: { type: "boolean" },
|
||
},
|
||
} as const;
|
||
|
||
const assegnazioneSchema = {
|
||
type: "object",
|
||
properties: {
|
||
id: { type: "integer" },
|
||
corsoId: { type: "integer" },
|
||
dipendenteId: { type: "integer" },
|
||
dataAssegnazione: { type: "string", format: "date" },
|
||
dataScadenza: { type: "string", format: "date" },
|
||
stato: {
|
||
type: "string",
|
||
enum: ["Assegnato", "Completato", "Scaduto", "Annullato"],
|
||
},
|
||
dataCompletamento: { type: "string", format: "date", nullable: true },
|
||
corso: {
|
||
type: "object",
|
||
properties: {
|
||
id: { type: "integer" },
|
||
titolo: { type: "string" },
|
||
categoria: { type: "string" },
|
||
durataOre: { type: "integer" },
|
||
},
|
||
},
|
||
dipendente: {
|
||
type: "object",
|
||
properties: {
|
||
id: { type: "integer" },
|
||
nome: { type: "string" },
|
||
cognome: { type: "string" },
|
||
email: { type: "string", format: "email" },
|
||
},
|
||
},
|
||
},
|
||
} as const;
|
||
|
||
const statisticaSchema = {
|
||
type: "object",
|
||
properties: {
|
||
mese: { type: "string", example: "2026-05" },
|
||
categoria: { type: "string", example: "Sicurezza" },
|
||
numeroAssegnazioni: { type: "integer", example: 18 },
|
||
numeroCompletamenti: { type: "integer", example: 12 },
|
||
percentualeCompletamento: { type: "number", example: 66.67 },
|
||
},
|
||
} as const;
|
||
|
||
const idParam = {
|
||
name: "id",
|
||
in: "path",
|
||
required: true,
|
||
schema: { type: "integer", minimum: 1 },
|
||
} as const;
|
||
|
||
const responses = {
|
||
unauthorized: {
|
||
description: "Autenticazione richiesta",
|
||
content: { "application/json": { schema: errorSchema } },
|
||
},
|
||
forbidden: {
|
||
description: "Operazione non consentita per il ruolo corrente",
|
||
content: { "application/json": { schema: errorSchema } },
|
||
},
|
||
notFound: {
|
||
description: "Risorsa non trovata",
|
||
content: { "application/json": { schema: errorSchema } },
|
||
},
|
||
validationError: {
|
||
description: "Validazione fallita",
|
||
content: { "application/json": { schema: errorSchema } },
|
||
},
|
||
} as const;
|
||
|
||
export const openApiSpec = {
|
||
openapi: "3.0.3",
|
||
info: {
|
||
title: "Esame ITS 24-26 – Academy API",
|
||
version: "1.0.0",
|
||
description: [
|
||
"API REST per la gestione dell'Academy aziendale (corsi, assegnazioni, statistiche).",
|
||
"",
|
||
"### Autenticazione",
|
||
"Le API protette usano sessioni Auth.js tramite cookie HTTP-only.",
|
||
"1. `GET /api/auth/csrf` per ottenere il token CSRF",
|
||
"2. `POST /api/auth/callback/credentials` con `email`, `password`, `csrfToken`",
|
||
"3. Le richieste successive inviano automaticamente il cookie di sessione",
|
||
"",
|
||
"### Utenti di test",
|
||
"| Email | Password | Ruolo |",
|
||
"|-------|----------|-------|",
|
||
"| admin@academy.it | Password123! | Referente Academy |",
|
||
"| maria.bianchi@academy.it | Password123! | Dipendente |",
|
||
].join("\n"),
|
||
},
|
||
servers: [{ url: "/", description: "Server corrente" }],
|
||
tags: [
|
||
{ name: "Sistema", description: "Health check e documentazione" },
|
||
{ name: "Autenticazione", description: "Login e sessione" },
|
||
{ name: "Profilo", description: "Utente autenticato" },
|
||
{ name: "Utenti", description: "Gestione utenti (Referente Academy)" },
|
||
{ name: "Corsi", description: "Catalogo corsi Academy" },
|
||
{ name: "Assegnazioni", description: "Assegnazioni corsi ai dipendenti" },
|
||
{ name: "Statistiche", description: "Riepiloghi formativi" },
|
||
],
|
||
components: {
|
||
securitySchemes: {
|
||
cookieAuth: {
|
||
type: "apiKey",
|
||
in: "cookie",
|
||
name: "authjs.session-token",
|
||
description:
|
||
"Cookie di sessione Auth.js. In produzione può essere __Secure-authjs.session-token.",
|
||
},
|
||
},
|
||
schemas: {
|
||
Error: errorSchema,
|
||
Utente: utenteSchema,
|
||
Corso: corsoSchema,
|
||
AssegnazioneCorso: assegnazioneSchema,
|
||
StatisticaAcademy: statisticaSchema,
|
||
UtenteCreate: {
|
||
type: "object",
|
||
required: ["nome", "cognome", "email", "password", "ruolo"],
|
||
properties: {
|
||
nome: { type: "string" },
|
||
cognome: { type: "string" },
|
||
email: { type: "string", format: "email" },
|
||
password: { type: "string", minLength: 8 },
|
||
ruolo: { type: "string", enum: ["Dipendente", "Referente Academy"] },
|
||
},
|
||
},
|
||
CorsoCreate: {
|
||
type: "object",
|
||
required: ["titolo", "categoria", "durataOre"],
|
||
properties: {
|
||
titolo: { type: "string" },
|
||
descrizione: { type: "string", nullable: true },
|
||
categoria: { type: "string" },
|
||
durataOre: { type: "integer", minimum: 1 },
|
||
obbligatorio: { type: "boolean", default: false },
|
||
attivo: { type: "boolean", default: true },
|
||
},
|
||
},
|
||
AssegnazioneCreate: {
|
||
type: "object",
|
||
required: ["corsoId", "dipendenteId", "dataAssegnazione", "dataScadenza"],
|
||
properties: {
|
||
corsoId: { type: "integer" },
|
||
dipendenteId: { type: "integer" },
|
||
dataAssegnazione: { type: "string", format: "date" },
|
||
dataScadenza: { type: "string", format: "date" },
|
||
},
|
||
},
|
||
AssegnazioneCompleta: {
|
||
type: "object",
|
||
properties: {
|
||
dataCompletamento: {
|
||
type: "string",
|
||
format: "date",
|
||
description: "Se omessa, viene usata la data odierna",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
paths: {
|
||
"/api/health": {
|
||
get: {
|
||
tags: ["Sistema"],
|
||
summary: "Health check",
|
||
responses: {
|
||
200: {
|
||
description: "Servizio attivo",
|
||
content: {
|
||
"application/json": {
|
||
schema: {
|
||
type: "object",
|
||
properties: { status: { type: "string", example: "ok" } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"/api/auth/csrf": {
|
||
get: {
|
||
tags: ["Autenticazione"],
|
||
summary: "Ottieni token CSRF per il login",
|
||
responses: {
|
||
200: {
|
||
description: "Token CSRF",
|
||
content: {
|
||
"application/json": {
|
||
schema: {
|
||
type: "object",
|
||
properties: { csrfToken: { type: "string" } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"/api/auth/callback/credentials": {
|
||
post: {
|
||
tags: ["Autenticazione"],
|
||
summary: "Login con email e password",
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
"application/x-www-form-urlencoded": {
|
||
schema: {
|
||
type: "object",
|
||
required: ["csrfToken", "email", "password"],
|
||
properties: {
|
||
csrfToken: { type: "string" },
|
||
email: { type: "string", format: "email" },
|
||
password: { type: "string" },
|
||
redirect: { type: "string", example: "false" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: { description: "Login riuscito, cookie di sessione impostato" },
|
||
401: { description: "Credenziali non valide" },
|
||
},
|
||
},
|
||
},
|
||
"/api/auth/signout": {
|
||
post: {
|
||
tags: ["Autenticazione"],
|
||
summary: "Logout",
|
||
security: [{ cookieAuth: [] }],
|
||
responses: {
|
||
200: { description: "Logout effettuato" },
|
||
},
|
||
},
|
||
},
|
||
"/api/me": {
|
||
get: {
|
||
tags: ["Profilo"],
|
||
summary: "Profilo utente autenticato",
|
||
security: [{ cookieAuth: [] }],
|
||
responses: {
|
||
200: {
|
||
description: "Profilo corrente",
|
||
content: { "application/json": { schema: utenteSchema } },
|
||
},
|
||
401: responses.unauthorized,
|
||
},
|
||
},
|
||
},
|
||
"/api/utenti": {
|
||
get: {
|
||
tags: ["Utenti"],
|
||
summary: "Elenco utenti",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [
|
||
{
|
||
name: "ruolo",
|
||
in: "query",
|
||
schema: { type: "string", enum: ["Dipendente", "Referente Academy"] },
|
||
description: "Filtra per ruolo (es. Dipendente per l'elenco dipendenti)",
|
||
},
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: "Lista utenti",
|
||
content: {
|
||
"application/json": {
|
||
schema: { type: "array", items: utenteSchema },
|
||
},
|
||
},
|
||
},
|
||
401: responses.unauthorized,
|
||
403: responses.forbidden,
|
||
},
|
||
},
|
||
post: {
|
||
tags: ["Utenti"],
|
||
summary: "Crea utente",
|
||
security: [{ cookieAuth: [] }],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/UtenteCreate" },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
201: {
|
||
description: "Utente creato",
|
||
content: { "application/json": { schema: utenteSchema } },
|
||
},
|
||
400: responses.validationError,
|
||
403: responses.forbidden,
|
||
409: { description: "Email già registrata" },
|
||
},
|
||
},
|
||
},
|
||
"/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"],
|
||
summary: "Dettaglio utente",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
responses: {
|
||
200: {
|
||
description: "Utente trovato",
|
||
content: { "application/json": { schema: utenteSchema } },
|
||
},
|
||
404: responses.notFound,
|
||
},
|
||
},
|
||
put: {
|
||
tags: ["Utenti"],
|
||
summary: "Aggiorna utente",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/UtenteCreate" },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: "Utente aggiornato",
|
||
content: { "application/json": { schema: utenteSchema } },
|
||
},
|
||
400: responses.validationError,
|
||
404: responses.notFound,
|
||
},
|
||
},
|
||
delete: {
|
||
tags: ["Utenti"],
|
||
summary: "Elimina utente",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
responses: {
|
||
204: { description: "Utente eliminato" },
|
||
400: { description: "Impossibile eliminare il proprio account" },
|
||
404: responses.notFound,
|
||
},
|
||
},
|
||
},
|
||
"/api/corsi": {
|
||
get: {
|
||
tags: ["Corsi"],
|
||
summary: "Elenco corsi",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [
|
||
{ name: "categoria", in: "query", schema: { type: "string" } },
|
||
{ name: "attivo", in: "query", schema: { type: "boolean" } },
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: "Lista corsi",
|
||
content: {
|
||
"application/json": {
|
||
schema: { type: "array", items: corsoSchema },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
post: {
|
||
tags: ["Corsi"],
|
||
summary: "Crea corso",
|
||
security: [{ cookieAuth: [] }],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/CorsoCreate" },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
201: {
|
||
description: "Corso creato",
|
||
content: { "application/json": { schema: corsoSchema } },
|
||
},
|
||
403: responses.forbidden,
|
||
},
|
||
},
|
||
},
|
||
"/api/corsi/{id}": {
|
||
get: {
|
||
tags: ["Corsi"],
|
||
summary: "Dettaglio corso",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
responses: {
|
||
200: {
|
||
description: "Corso trovato",
|
||
content: { "application/json": { schema: corsoSchema } },
|
||
},
|
||
404: responses.notFound,
|
||
},
|
||
},
|
||
put: {
|
||
tags: ["Corsi"],
|
||
summary: "Modifica corso",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/CorsoCreate" },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: "Corso aggiornato",
|
||
content: { "application/json": { schema: corsoSchema } },
|
||
},
|
||
403: responses.forbidden,
|
||
},
|
||
},
|
||
delete: {
|
||
tags: ["Corsi"],
|
||
summary: "Elimina corso",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
responses: {
|
||
204: { description: "Corso eliminato" },
|
||
409: { description: "Corso con assegnazioni collegate" },
|
||
},
|
||
},
|
||
},
|
||
"/api/corsi/{id}/disattiva": {
|
||
put: {
|
||
tags: ["Corsi"],
|
||
summary: "Disattiva corso",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
responses: {
|
||
200: {
|
||
description: "Corso disattivato",
|
||
content: { "application/json": { schema: corsoSchema } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"/api/assegnazioni-corsi": {
|
||
get: {
|
||
tags: ["Assegnazioni"],
|
||
summary: "Elenco assegnazioni",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [
|
||
{
|
||
name: "stato",
|
||
in: "query",
|
||
schema: {
|
||
type: "string",
|
||
enum: ["Assegnato", "Completato", "Scaduto", "Annullato"],
|
||
},
|
||
},
|
||
{ name: "categoria", in: "query", schema: { type: "string" } },
|
||
{ name: "corsoId", in: "query", schema: { type: "integer" } },
|
||
{
|
||
name: "dipendenteId",
|
||
in: "query",
|
||
schema: { type: "integer" },
|
||
description: "Solo Referente Academy",
|
||
},
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: "Lista assegnazioni",
|
||
content: {
|
||
"application/json": {
|
||
schema: { type: "array", items: assegnazioneSchema },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
post: {
|
||
tags: ["Assegnazioni"],
|
||
summary: "Assegna corso a dipendente",
|
||
security: [{ cookieAuth: [] }],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/AssegnazioneCreate" },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
201: {
|
||
description: "Assegnazione creata",
|
||
content: { "application/json": { schema: assegnazioneSchema } },
|
||
},
|
||
403: responses.forbidden,
|
||
},
|
||
},
|
||
},
|
||
"/api/assegnazioni-corsi/{id}": {
|
||
get: {
|
||
tags: ["Assegnazioni"],
|
||
summary: "Dettaglio assegnazione",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
responses: {
|
||
200: {
|
||
description: "Assegnazione trovata",
|
||
content: { "application/json": { schema: assegnazioneSchema } },
|
||
},
|
||
403: responses.forbidden,
|
||
404: responses.notFound,
|
||
},
|
||
},
|
||
put: {
|
||
tags: ["Assegnazioni"],
|
||
summary: "Modifica assegnazione",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
"application/json": {
|
||
schema: {
|
||
type: "object",
|
||
properties: {
|
||
corsoId: { type: "integer" },
|
||
dipendenteId: { type: "integer" },
|
||
dataAssegnazione: { type: "string", format: "date" },
|
||
dataScadenza: { type: "string", format: "date" },
|
||
stato: {
|
||
type: "string",
|
||
enum: ["Assegnato", "Completato", "Scaduto", "Annullato"],
|
||
},
|
||
},
|
||
},
|
||
}
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: "Assegnazione aggiornata",
|
||
content: { "application/json": { schema: assegnazioneSchema } },
|
||
},
|
||
},
|
||
},
|
||
delete: {
|
||
tags: ["Assegnazioni"],
|
||
summary: "Elimina assegnazione",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
responses: {
|
||
204: { description: "Assegnazione eliminata" },
|
||
},
|
||
},
|
||
},
|
||
"/api/assegnazioni-corsi/{id}/completa": {
|
||
put: {
|
||
tags: ["Assegnazioni"],
|
||
summary: "Segna assegnazione come completata",
|
||
description: "Solo il dipendente titolare può completare la propria assegnazione.",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
requestBody: {
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/AssegnazioneCompleta" },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: "Assegnazione completata",
|
||
content: { "application/json": { schema: assegnazioneSchema } },
|
||
},
|
||
403: responses.forbidden,
|
||
},
|
||
},
|
||
},
|
||
"/api/assegnazioni-corsi/{id}/annulla": {
|
||
put: {
|
||
tags: ["Assegnazioni"],
|
||
summary: "Annulla assegnazione",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [idParam],
|
||
responses: {
|
||
200: {
|
||
description: "Assegnazione annullata",
|
||
content: { "application/json": { schema: assegnazioneSchema } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"/api/statistiche/academy": {
|
||
get: {
|
||
tags: ["Statistiche"],
|
||
summary: "Riepilogo formazione per mese e categoria",
|
||
security: [{ cookieAuth: [] }],
|
||
parameters: [
|
||
{
|
||
name: "mese",
|
||
in: "query",
|
||
schema: { type: "string", example: "2026-05" },
|
||
description: "Filtra per mese (YYYY-MM)",
|
||
},
|
||
{
|
||
name: "daMese",
|
||
in: "query",
|
||
schema: { type: "string", example: "2026-01" },
|
||
},
|
||
{
|
||
name: "aMese",
|
||
in: "query",
|
||
schema: { type: "string", example: "2026-12" },
|
||
},
|
||
{ name: "categoria", in: "query", schema: { type: "string" } },
|
||
{ name: "dipendenteId", in: "query", schema: { type: "integer" } },
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: "Statistiche aggregate",
|
||
content: {
|
||
"application/json": {
|
||
schema: {
|
||
type: "array",
|
||
items: { $ref: "#/components/schemas/StatisticaAcademy" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
403: responses.forbidden,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
} as const;
|