correzioni finali
All checks were successful
Deploy to VPS / build (push) Successful in 18s
Deploy to VPS / deploy (push) Successful in 20s

This commit is contained in:
AV77web 2026-06-09 15:44:48 +02:00
parent c49757aa13
commit 895860e906
4 changed files with 106 additions and 11 deletions

68
README.md Normal file
View file

@ -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`.

View file

@ -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'),

View file

@ -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');

View file

@ -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" });