diff --git a/README.md b/README.md new file mode 100644 index 0000000..94f2a20 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Backend – Gestione Rimborsi Spese Aziendali (Prova S5) + +API REST per la gestione delle richieste di rimborso spese aziendali. + +## URL produzione + +| Risorsa | URL | +|---------|-----| +| API | https://provapraticabackend.andreavillari.it | +| Health check | https://provapraticabackend.andreavillari.it/api/health | +| Swagger UI | https://provapraticabackend.andreavillari.it/api/docs | +| Frontend collegato | https://provapraticafrontend.andreavillari.it | + +## Credenziali di test + +Password comune per tutti gli utenti: `Password123!` + +| Ruolo | Email | +|-------|-------| +| Responsabile amministrativo | `admin@azienda.it` | +| Dipendente | `mario.rossi@azienda.it` | +| Dipendente | `marco.bianchi@azienda.it` | +| Dipendente | `anna.verdi@azienda.it` | + +È possibile anche registrare nuovi utenti da `/register` sul frontend, scegliendo il ruolo. + +## Avvio locale + +Configurare il file `.env` (AUTH_SECRET, credenziali DB, URL frontend/backend, ecc.). + +```bash +npm install +npm run dev +``` + +Con Docker (app + MySQL + phpMyAdmin): + +```bash +docker compose up -d --build +``` + +MySQL è esposto su `127.0.0.1:3307`, l'app su `127.0.0.1:3003`. + +Gli script in `initdb/` vengono eseguiti **solo al primo avvio** del volume MySQL (database vuoto). + +## Dati iniziali + +Il seed (`initdb/02_seed.sql`) crea: + +- 4 utenti di test (1 responsabile, 3 dipendenti) +- 5 categorie di spesa +- 10 richieste di rimborso in tutti gli stati (`IN_ATTESA`, `APPROVATA`, `RIFIUTATA`, `LIQUIDATA`) + +Se il database di produzione è stato creato **prima** dell'aggiornamento del seed: + +- ricreare il volume Docker (`docker compose down -v` poi `up`), oppure +- eseguire `scripts/apply-missing-test-users.sql` via phpMyAdmin o CLI MySQL + +## Stack + +- Node.js 20, Express, TypeScript +- MySQL 8 +- Auth.js (sessione JWT via cookie) +- Swagger UI (`/api/docs`) + +## Deploy + +Push su `main` → Forgejo Actions → SSH sul VPS → `docker compose up -d --build` in `/root/provapraticabackend`. diff --git a/initdb/02_seed.sql b/initdb/02_seed.sql index ec12675..649ee4b 100644 --- a/initdb/02_seed.sql +++ b/initdb/02_seed.sql @@ -1,17 +1,19 @@ -- ============================================================ -- Dati iniziali di test – Gestione Rimborsi Spese Aziendali --- Utenti: già presenti in tabella utenti (id 1-4) --- 1 andrea villari – responsabile valutazioni --- 2 mario rossi --- 3 marco bianchi --- 4 anna verdi +-- Password per tutti gli utenti: Password123! +-- 1 Andrea Villari – admin@azienda.it (responsabile) +-- 2 Mario Rossi – mario.rossi@azienda.it (dipendente) +-- 3 Marco Bianchi – marco.bianchi@azienda.it (dipendente) +-- 4 Anna Verdi – anna.verdi@azienda.it (dipendente) -- ============================================================ USE provapratica; --- Ruoli corretti per gli utenti di test (id 1 = responsabile, 2-4 = dipendenti) -UPDATE utenti SET ruolo = 'RESPONSABILE_AMMINISTRATIVO' WHERE utente_id = 1; -UPDATE utenti SET ruolo = 'DIPENDENTE' WHERE utente_id IN (2, 3, 4); +INSERT INTO utenti (nome, cognome, email, password_hash, ruolo) VALUES + ('Andrea', 'Villari', 'admin@azienda.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'RESPONSABILE_AMMINISTRATIVO'), + ('Mario', 'Rossi', 'mario.rossi@azienda.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'DIPENDENTE'), + ('Marco', 'Bianchi', 'marco.bianchi@azienda.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'DIPENDENTE'), + ('Anna', 'Verdi', 'anna.verdi@azienda.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'DIPENDENTE'); INSERT IGNORE INTO categorie_spesa (descrizione) VALUES ('Trasferta'), diff --git a/scripts/apply-missing-test-users.sql b/scripts/apply-missing-test-users.sql new file mode 100644 index 0000000..1aed7c3 --- /dev/null +++ b/scripts/apply-missing-test-users.sql @@ -0,0 +1,14 @@ +-- Eseguire manualmente su un database già esistente (es. produzione) +-- se gli utenti di test non sono presenti. +-- Password per tutti: Password123! + +USE provapratica; + +INSERT IGNORE INTO utenti (nome, cognome, email, password_hash, ruolo) VALUES + ('Andrea', 'Villari', 'admin@azienda.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'RESPONSABILE_AMMINISTRATIVO'), + ('Mario', 'Rossi', 'mario.rossi@azienda.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'DIPENDENTE'), + ('Marco', 'Bianchi', 'marco.bianchi@azienda.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'DIPENDENTE'), + ('Anna', 'Verdi', 'anna.verdi@azienda.it', '$2b$10$7Z80qZcDydZCdLrOcaH/VOa9knKWjQVyyg8MtEgBAvgfNMsiLEZze', 'DIPENDENTE'); + +UPDATE utenti SET ruolo = 'RESPONSABILE_AMMINISTRATIVO' WHERE email = 'admin@azienda.it'; +UPDATE utenti SET ruolo = 'DIPENDENTE' WHERE email IN ('mario.rossi@azienda.it', 'marco.bianchi@azienda.it', 'anna.verdi@azienda.it'); diff --git a/src/routes/rimborsi.ts b/src/routes/rimborsi.ts index 11fef00..16fda99 100644 --- a/src/routes/rimborsi.ts +++ b/src/routes/rimborsi.ts @@ -77,13 +77,18 @@ router.post("/", async (req, res) => { }); router.put("/:id", async (req, res) => { + if (req.user!.ruolo !== "DIPENDENTE") { + res.status(403).json({ error: "Solo i dipendenti possono modificare le proprie richieste" }); + return; + } + const id = Number(req.params.id); const existing = await getRimborsoById(id); if (!existing) { res.status(404).json({ error: "Richiesta non trovata" }); return; } - if (req.user!.ruolo === "DIPENDENTE" && existing.dipendente_id !== req.user!.id) { + if (existing.dipendente_id !== req.user!.id) { res.status(403).json({ error: "Accesso non consentito" }); return; } @@ -108,13 +113,18 @@ router.put("/:id", async (req, res) => { }); router.delete("/:id", async (req, res) => { + if (req.user!.ruolo !== "DIPENDENTE") { + res.status(403).json({ error: "Solo i dipendenti possono eliminare le proprie richieste" }); + return; + } + const id = Number(req.params.id); const existing = await getRimborsoById(id); if (!existing) { res.status(404).json({ error: "Richiesta non trovata" }); return; } - if (req.user!.ruolo === "DIPENDENTE" && existing.dipendente_id !== req.user!.id) { + if (existing.dipendente_id !== req.user!.id) { res.status(403).json({ error: "Accesso non consentito" }); return; } @@ -144,7 +154,8 @@ router.put("/:id/approva", requireRole("RESPONSABILE_AMMINISTRATIVO"), async (re router.put("/:id/rifiuta", requireRole("RESPONSABILE_AMMINISTRATIVO"), async (req, res) => { const id = Number(req.params.id); - const motivazione = req.body.motivazione?.toString().trim() || null; + const raw = req.body.motivazione?.toString().trim(); + const motivazione = raw || "Richiesta non approvata dal responsabile amministrativo"; const ok = await rifiutaRimborso(id, req.user!.id, motivazione); if (!ok) { res.status(400).json({ error: "Impossibile rifiutare la richiesta" });