diff --git a/dist/index.js b/dist/index.js index 79f5c8b..e2e2083 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,8 +1,11 @@ import { ExpressAuth } from "@auth/express"; import dotenv from "dotenv"; import express from "express"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { authConfig } from "./auth/config.js"; import { corsMiddleware } from "./config/cors.js"; +import { swaggerServe, swaggerSetup } from "./config/swagger.js"; import { errorHandler } from "./middleware/errorHandler.js"; import categorieRouter from "./routes/categorie.js"; import rimborsiRouter from "./routes/rimborsi.js"; @@ -18,6 +21,14 @@ app.options(/.*/, corsMiddleware); app.get("/api/health", (_req, res) => { res.json({ status: "ok" }); }); +const swaggerEnabled = process.env.SWAGGER_ENABLED !== "false"; +if (swaggerEnabled) { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + app.use("/api/docs", swaggerServe, swaggerSetup); + app.get("/api/docs/openapi.json", (_req, res) => { + res.sendFile(path.join(__dirname, "openapi/openapi.json")); + }); +} app.use("/api/auth", ExpressAuth(authConfig)); app.use("/api/utenti", utentiRouter); app.use("/api/categorie-spesa", categorieRouter); diff --git a/dist/index.js.map b/dist/index.js.map index 87601f3..fd612e6 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,eAAe,MAAM,uBAAuB,CAAC;AACpD,OAAO,cAAc,MAAM,sBAAsB,CAAC;AAClD,OAAO,iBAAiB,MAAM,yBAAyB,CAAC;AACxD,OAAO,YAAY,MAAM,oBAAoB,CAAC;AAE9C,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AACtB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;AAE9C,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;AAC1B,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AACxB,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AACxB,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;AAElC,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IACnC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAC7B,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC;AAC9C,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;AACrC,GAAG,CAAC,GAAG,CAAC,sBAAsB,EAAE,eAAe,CAAC,CAAC;AACjD,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,cAAc,CAAC,CAAC;AACzC,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,CAAC;AAE/C,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;AAEtB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE;IAC/B,OAAO,CAAC,GAAG,CAAC,kCAAkC,IAAI,EAAE,CAAC,CAAC;AACxD,CAAC,CAAC,CAAC"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,eAAe,MAAM,uBAAuB,CAAC;AACpD,OAAO,cAAc,MAAM,sBAAsB,CAAC;AAClD,OAAO,iBAAiB,MAAM,yBAAyB,CAAC;AACxD,OAAO,YAAY,MAAM,oBAAoB,CAAC;AAE9C,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AACtB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;AAE9C,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;AAC1B,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AACxB,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AACxB,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;AAElC,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IACnC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAC7B,CAAC,CAAC,CAAC;AAEH,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,KAAK,OAAO,CAAC;AAC/D,IAAI,cAAc,EAAE,CAAC;IACnB,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/D,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC;IACjD,GAAG,CAAC,GAAG,CAAC,wBAAwB,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QAC9C,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,sBAAsB,CAAC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC;AAED,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC;AAC9C,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;AACrC,GAAG,CAAC,GAAG,CAAC,sBAAsB,EAAE,eAAe,CAAC,CAAC;AACjD,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,cAAc,CAAC,CAAC;AACzC,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,CAAC;AAE/C,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;AAEtB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE;IAC/B,OAAO,CAAC,GAAG,CAAC,kCAAkC,IAAI,EAAE,CAAC,CAAC;AACxD,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/dockerfile/Dockerfile b/dockerfile/Dockerfile index e51939c..ef80b61 100644 --- a/dockerfile/Dockerfile +++ b/dockerfile/Dockerfile @@ -21,6 +21,7 @@ COPY package*.json ./ RUN npm ci --omit=dev COPY --from=build /app/dist ./dist +COPY --from=build /app/src/openapi ./dist/openapi EXPOSE 3003 diff --git a/package-lock.json b/package-lock.json index a1c6f00..6d13235 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,13 +15,15 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", - "mysql2": "^3.22.5" + "mysql2": "^3.22.5", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^25.9.2", + "@types/swagger-ui-express": "^4.1.8", "nodemon": "^3.1.14", "tsx": "^4.22.4", "typescript": "^6.0.3" @@ -519,6 +521,13 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", @@ -633,6 +642,17 @@ "@types/node": "*" } }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2397,6 +2417,30 @@ "node": ">=4" } }, + "node_modules/swagger-ui-dist": { + "version": "5.32.6", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", + "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index ffde6e9..b5c80ed 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "tsx watch src/index.ts", - "build": "tsc", + "build": "tsc && node --input-type=module -e \"import { cpSync } from 'node:fs'; cpSync('src/openapi', 'dist/openapi', { recursive: true });\"", "start": "node dist/index.js" }, "repository": { @@ -23,15 +23,17 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", - "mysql2": "^3.22.5" + "mysql2": "^3.22.5", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^25.9.2", + "@types/swagger-ui-express": "^4.1.8", "nodemon": "^3.1.14", "tsx": "^4.22.4", "typescript": "^6.0.3" } -} \ No newline at end of file +} diff --git a/src/config/swagger.ts b/src/config/swagger.ts new file mode 100644 index 0000000..dc574d4 --- /dev/null +++ b/src/config/swagger.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import swaggerUi from "swagger-ui-express"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const specPath = path.join(__dirname, "../openapi/openapi.json"); +const openApiDocument = JSON.parse(readFileSync(specPath, "utf-8")); + +export const swaggerServe = swaggerUi.serve; +export const swaggerSetup = swaggerUi.setup(openApiDocument, { + customSiteTitle: "API Rimborsi Spese – Documentazione", + swaggerOptions: { + persistAuthorization: true, + withCredentials: true, + }, +}); + +export { openApiDocument }; diff --git a/src/index.ts b/src/index.ts index 2b16ffe..5e3a287 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,11 @@ import { ExpressAuth } from "@auth/express"; import dotenv from "dotenv"; import express from "express"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { authConfig } from "./auth/config.js"; import { corsMiddleware } from "./config/cors.js"; +import { swaggerServe, swaggerSetup } from "./config/swagger.js"; import { errorHandler } from "./middleware/errorHandler.js"; import categorieRouter from "./routes/categorie.js"; import rimborsiRouter from "./routes/rimborsi.js"; @@ -23,6 +26,15 @@ app.get("/api/health", (_req, res) => { res.json({ status: "ok" }); }); +const swaggerEnabled = process.env.SWAGGER_ENABLED !== "false"; +if (swaggerEnabled) { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + app.use("/api/docs", swaggerServe, swaggerSetup); + app.get("/api/docs/openapi.json", (_req, res) => { + res.sendFile(path.join(__dirname, "openapi/openapi.json")); + }); +} + app.use("/api/auth", ExpressAuth(authConfig)); app.use("/api/utenti", utentiRouter); app.use("/api/categorie-spesa", categorieRouter); diff --git a/src/openapi/openapi.json b/src/openapi/openapi.json new file mode 100644 index 0000000..50e04c8 --- /dev/null +++ b/src/openapi/openapi.json @@ -0,0 +1,679 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "API Gestione Rimborsi Spese Aziendali", + "version": "1.0.0", + "description": "Backend prova pratica S5 – Gestione rimborsi spese aziendali" + }, + "servers": [ + { + "url": "http://localhost:3003", + "description": "Sviluppo locale" + } + ], + "tags": [ + { "name": "Sistema", "description": "Health check" }, + { "name": "Autenticazione", "description": "Login e sessione Auth.js" }, + { "name": "Utenti", "description": "Registrazione e profilo" }, + { "name": "Categorie", "description": "Categorie di spesa" }, + { "name": "Rimborsi", "description": "Richieste di rimborso" }, + { "name": "Statistiche", "description": "Report amministrativi" } + ], + "components": { + "securitySchemes": { + "cookieAuth": { + "type": "apiKey", + "in": "cookie", + "name": "authjs.session-token", + "description": "Cookie di sessione impostato dopo il login. In produzione il nome può essere __Secure-authjs.session-token." + } + }, + "schemas": { + "Error": { + "type": "object", + "properties": { + "error": { "type": "string" } + }, + "required": ["error"] + }, + "Message": { + "type": "object", + "properties": { + "message": { "type": "string" } + }, + "required": ["message"] + }, + "Ruolo": { + "type": "string", + "enum": ["DIPENDENTE", "RESPONSABILE_AMMINISTRATIVO"] + }, + "StatoRichiesta": { + "type": "string", + "enum": ["IN_ATTESA", "APPROVATA", "RIFIUTATA", "LIQUIDATA"] + }, + "Utente": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "nome": { "type": "string" }, + "cognome": { "type": "string" }, + "email": { "type": "string", "format": "email" }, + "ruolo": { "$ref": "#/components/schemas/Ruolo" } + }, + "required": ["id", "nome", "cognome", "email", "ruolo"] + }, + "UtenteSintetico": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "nome": { "type": "string" }, + "cognome": { "type": "string" } + }, + "required": ["id", "nome", "cognome"] + }, + "RegistrazioneInput": { + "type": "object", + "required": ["nome", "cognome", "email", "password", "confermaPassword"], + "properties": { + "nome": { "type": "string", "example": "Mario" }, + "cognome": { "type": "string", "example": "Rossi" }, + "email": { "type": "string", "format": "email", "example": "mario.rossi@azienda.it" }, + "password": { "type": "string", "format": "password" }, + "confermaPassword": { "type": "string", "format": "password" }, + "ruolo": { "$ref": "#/components/schemas/Ruolo" } + } + }, + "RegistrazioneResponse": { + "type": "object", + "properties": { + "message": { "type": "string" }, + "utente": { "$ref": "#/components/schemas/Utente" } + }, + "required": ["message", "utente"] + }, + "CategoriaSpesa": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "descrizione": { "type": "string" } + }, + "required": ["id", "descrizione"] + }, + "Rimborso": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "dataInserimento": { "type": "string", "format": "date-time" }, + "dataSpesa": { "type": "string", "format": "date" }, + "categoriaId": { "type": "integer" }, + "categoria": { "type": "string" }, + "importo": { "type": "number" }, + "descrizione": { "type": "string" }, + "riferimentoGiustificativo": { "type": "string", "nullable": true }, + "stato": { "$ref": "#/components/schemas/StatoRichiesta" }, + "dipendenteId": { "type": "integer" }, + "dipendenteNome": { "type": "string" }, + "dataValutazione": { "type": "string", "format": "date-time", "nullable": true }, + "responsabileValutazioneId": { "type": "integer", "nullable": true }, + "responsabileNome": { "type": "string", "nullable": true }, + "motivazioneRifiuto": { "type": "string", "nullable": true }, + "dataLiquidazione": { "type": "string", "format": "date-time", "nullable": true } + }, + "required": [ + "id", + "dataInserimento", + "dataSpesa", + "categoriaId", + "categoria", + "importo", + "descrizione", + "stato", + "dipendenteId", + "dipendenteNome" + ] + }, + "RimborsoInput": { + "type": "object", + "required": ["dataSpesa", "categoriaId", "importo", "descrizione"], + "properties": { + "dataSpesa": { "type": "string", "format": "date", "example": "2025-06-01" }, + "categoriaId": { "type": "integer", "example": 1 }, + "importo": { "type": "number", "minimum": 0.01, "example": 45.5 }, + "descrizione": { "type": "string", "example": "Pranzo di lavoro con cliente" }, + "riferimentoGiustificativo": { "type": "string", "nullable": true, "example": "FATT-2025-001" } + } + }, + "RifiutaInput": { + "type": "object", + "properties": { + "motivazione": { "type": "string", "nullable": true, "example": "Manca il giustificativo" } + } + }, + "StatisticaRimborso": { + "type": "object", + "properties": { + "mese": { "type": "string", "example": "2025-06" }, + "categoria": { "type": "string" }, + "numeroRichieste": { "type": "integer" }, + "totaleRichiesto": { "type": "number" }, + "totaleApprovato": { "type": "number" }, + "totaleLiquidato": { "type": "number" } + }, + "required": [ + "mese", + "categoria", + "numeroRichieste", + "totaleRichiesto", + "totaleApprovato", + "totaleLiquidato" + ] + }, + "CsrfResponse": { + "type": "object", + "properties": { + "csrfToken": { "type": "string" } + }, + "required": ["csrfToken"] + } + }, + "responses": { + "Unauthorized": { + "description": "Autenticazione richiesta", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + }, + "Forbidden": { + "description": "Operazione non consentita per il ruolo corrente", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + }, + "NotFound": { + "description": "Risorsa non trovata", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + }, + "BadRequest": { + "description": "Richiesta non valida", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + } + } + }, + "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 login/logout", + "responses": { + "200": { + "description": "Token CSRF", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CsrfResponse" } + } + } + } + } + } + }, + "/api/auth/callback/credentials": { + "post": { + "tags": ["Autenticazione"], + "summary": "Login con email e password", + "description": "Invia i dati come application/x-www-form-urlencoded. Imposta il cookie di sessione in caso di successo.", + "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", "format": "password" }, + "redirect": { "type": "string", "example": "false" } + } + } + } + } + }, + "responses": { + "200": { "description": "Login riuscito" }, + "401": { "description": "Credenziali non valide" } + } + } + }, + "/api/auth/signout": { + "post": { + "tags": ["Autenticazione"], + "summary": "Logout", + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "required": ["csrfToken"], + "properties": { + "csrfToken": { "type": "string" } + } + } + } + } + }, + "responses": { + "200": { "description": "Logout effettuato" } + } + } + }, + "/api/utenti/register": { + "post": { + "tags": ["Utenti"], + "summary": "Registrazione nuovo utente", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RegistrazioneInput" } + } + } + }, + "responses": { + "201": { + "description": "Utente registrato", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RegistrazioneResponse" } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "409": { + "description": "Email già registrata", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + } + } + } + }, + "/api/utenti/me": { + "get": { + "tags": ["Utenti"], + "summary": "Profilo utente corrente", + "responses": { + "200": { + "description": "Profilo utente", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Utente" } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/api/utenti/dipendenti": { + "get": { + "tags": ["Utenti"], + "summary": "Elenco dipendenti", + "description": "Solo per RESPONSABILE_AMMINISTRATIVO", + "security": [{ "cookieAuth": [] }], + "responses": { + "200": { + "description": "Lista dipendenti", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/UtenteSintetico" } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { "$ref": "#/components/responses/Forbidden" } + } + } + }, + "/api/categorie-spesa": { + "get": { + "tags": ["Categorie"], + "summary": "Elenco categorie di spesa", + "security": [{ "cookieAuth": [] }], + "responses": { + "200": { + "description": "Lista categorie", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/CategoriaSpesa" } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/api/rimborsi": { + "get": { + "tags": ["Rimborsi"], + "summary": "Elenco richieste di rimborso", + "security": [{ "cookieAuth": [] }], + "parameters": [ + { + "name": "stato", + "in": "query", + "schema": { "$ref": "#/components/schemas/StatoRichiesta" } + }, + { + "name": "categoriaId", + "in": "query", + "schema": { "type": "integer" } + }, + { + "name": "mese", + "in": "query", + "description": "Formato YYYY-MM", + "schema": { "type": "string", "pattern": "^\\d{4}-\\d{2}$", "example": "2025-06" } + }, + { + "name": "dipendenteId", + "in": "query", + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "Lista rimborsi", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/Rimborso" } + } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + }, + "post": { + "tags": ["Rimborsi"], + "summary": "Crea nuova richiesta di rimborso", + "description": "Solo per DIPENDENTE", + "security": [{ "cookieAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RimborsoInput" } + } + } + }, + "responses": { + "201": { + "description": "Richiesta creata", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Rimborso" } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { "$ref": "#/components/responses/Forbidden" } + } + } + }, + "/api/rimborsi/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { "type": "integer" } + } + ], + "get": { + "tags": ["Rimborsi"], + "summary": "Dettaglio richiesta di rimborso", + "security": [{ "cookieAuth": [] }], + "responses": { + "200": { + "description": "Dettaglio rimborso", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Rimborso" } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { "$ref": "#/components/responses/Forbidden" }, + "404": { "$ref": "#/components/responses/NotFound" } + } + }, + "put": { + "tags": ["Rimborsi"], + "summary": "Modifica richiesta (solo stato IN_ATTESA)", + "security": [{ "cookieAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RimborsoInput" } + } + } + }, + "responses": { + "200": { + "description": "Richiesta aggiornata", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Rimborso" } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { "$ref": "#/components/responses/Forbidden" }, + "404": { "$ref": "#/components/responses/NotFound" } + } + }, + "delete": { + "tags": ["Rimborsi"], + "summary": "Elimina richiesta (solo stato IN_ATTESA)", + "security": [{ "cookieAuth": [] }], + "responses": { + "200": { + "description": "Richiesta eliminata", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Message" } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { "$ref": "#/components/responses/Forbidden" }, + "404": { "$ref": "#/components/responses/NotFound" } + } + } + }, + "/api/rimborsi/{id}/approva": { + "put": { + "tags": ["Rimborsi"], + "summary": "Approva richiesta", + "description": "Solo RESPONSABILE_AMMINISTRATIVO. La richiesta deve essere IN_ATTESA.", + "security": [{ "cookieAuth": [] }], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "Richiesta approvata", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Rimborso" } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { "$ref": "#/components/responses/Forbidden" } + } + } + }, + "/api/rimborsi/{id}/rifiuta": { + "put": { + "tags": ["Rimborsi"], + "summary": "Rifiuta richiesta", + "description": "Solo RESPONSABILE_AMMINISTRATIVO. La richiesta deve essere IN_ATTESA.", + "security": [{ "cookieAuth": [] }], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { "type": "integer" } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RifiutaInput" } + } + } + }, + "responses": { + "200": { + "description": "Richiesta rifiutata", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Rimborso" } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { "$ref": "#/components/responses/Forbidden" } + } + } + }, + "/api/rimborsi/{id}/liquida": { + "put": { + "tags": ["Rimborsi"], + "summary": "Liquida richiesta approvata", + "description": "Solo RESPONSABILE_AMMINISTRATIVO. La richiesta deve essere APPROVATA.", + "security": [{ "cookieAuth": [] }], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "Richiesta liquidata", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Rimborso" } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { "$ref": "#/components/responses/Forbidden" } + } + } + }, + "/api/statistiche/rimborsi": { + "get": { + "tags": ["Statistiche"], + "summary": "Statistiche rimborsi aggregate", + "description": "Solo RESPONSABILE_AMMINISTRATIVO", + "security": [{ "cookieAuth": [] }], + "parameters": [ + { + "name": "mese", + "in": "query", + "description": "Formato YYYY-MM", + "schema": { "type": "string", "pattern": "^\\d{4}-\\d{2}$", "example": "2025-06" } + }, + { + "name": "categoriaId", + "in": "query", + "schema": { "type": "integer" } + }, + { + "name": "dipendenteId", + "in": "query", + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "Statistiche aggregate", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/StatisticaRimborso" } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "403": { "$ref": "#/components/responses/Forbidden" } + } + } + } + } +}