//====================================================== // 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;