# Task 15 — Area admin: editor Tiptap, API CRUD, upload immagini Data: 2026-07-02 Commit: `f0ad162f742179e5154dd0e011345b35f093f119` ## Riepilogo Il Task 15 introduce l'area di amministrazione del sito InsanityLab: layout admin dedicato, pagine di login/logout, lista articoli con eliminazione, form di creazione e modifica con editor Tiptap, API REST protette per il CRUD dei post, endpoint di upload immagini con validazione e route di serving dei file caricati con protezione contro il path traversal. La sanitizzazione dell'HTML in ingresso è centralizzata in `src/lib/sanitize.ts` (basata su `sanitize-html`) ed è coperta da test scritti prima dell'implementazione, secondo il flusso TDD richiesto. ## File creati - `tests/sanitize.test.ts` - `src/lib/sanitize.ts` - `src/lib/post-body.ts` - `src/layouts/Admin.astro` - `src/pages/admin/login.astro`, `src/pages/admin/logout.ts`, `src/pages/admin/index.astro`, `src/pages/admin/new.astro`, `src/pages/admin/edit/[id].astro` - `src/components/admin/PostForm.astro` - `src/pages/api/admin/posts/index.ts`, `src/pages/api/admin/posts/[id].ts`, `src/pages/api/admin/upload.ts` - `src/pages/uploads/[...path].ts` ## Esito test (`npm test`) ``` Test Files 6 passed (6) Tests 28 passed (28) ``` 28/28 test superati: 25 preesistenti (`auth`, `contact`, `posts`, `rate-limit`, `slug`) più i 3 nuovi test di `sanitize`. Il ciclo TDD è stato rispettato: il test è stato creato per primo, la sua esecuzione è fallita con `Cannot find module '../src/lib/sanitize'`, quindi è stata scritta l'implementazione e i test sono passati al primo tentativo. ## Verifica end-to-end (dev server su porta 4321) | # | Check | Atteso | Ottenuto | Esito | |---|-------|--------|----------|-------| | 1 | GET `/admin` senza autenticazione | 302 | 302 | ✓ | | 2 | POST `/api/admin/posts` senza autenticazione | 401 | 401 | ✓ | | 3 | Login POST `/admin/login` (utente e2e dedicato) | 302 + cookie | 302, cookie `session` impostato | ✓ | | 4 | POST nuovo articolo | 201 | 201, `{"id":16,"slug":"test-e2e-task-15"}` | ✓ | | 5 | GET `/blog/test-e2e-task-15` | 200 | 200 | ✓ | | 6 | PUT aggiornamento articolo | 200 | 200, `{"ok":true}` | ✓ | | 7 | Upload PNG (10x10, generato via Python) | 200 | 200, `{"url":"/uploads/1782949642171-10ccb41e.png"}` | ✓ | | 8 | GET file caricato | 200 | 200, `Content-Type: image/png` | ✓ | | 9 | Path traversal `/uploads/..%2f..%2fpackage.json` | 404 | 404 | ✓ | | 10 | DELETE articolo | 200 | 200, `{"ok":true}` | ✓ | Nota sul check 3: il dev server risponde 403 ai POST privi di header `Origin` per via della protezione CSRF integrata di Astro (`security.checkOrigin`). Con `Origin: http://localhost:4321` il login funziona correttamente. Un browser reale invia sempre l'header, quindi il comportamento è quello atteso. Nota sul check 9: la variante `--path-as-is /uploads/../package.json` restituiva 200 in modalità dev, ma la risposta proveniva dal middleware statico di Vite (headers `ETag` e `Cache-Control: no-cache`, assenti nella nostra route), che in sviluppo espone i file di progetto dopo la normalizzazione del path. La verifica è stata ripetuta sul server di produzione (`npm run build` + `node --env-file=.env ./dist/server/entry.mjs`): tutte le varianti di traversal (`../`, `%2e%2e/`, `..%2f`) e l'accesso diretto a `/package.json` restituiscono 404. La route `[...path].ts` è quindi sicura; l'esposizione riguarda esclusivamente l'ambiente di sviluppo locale e non il deploy. ## Build `npm run build` completata senza errori: 54 asset generati, sitemap creata, server bundle prodotto in `dist/server`. ## Adattamenti al test sanitize Nessuno. `sanitize-html` 2.17.5 preserva l'HTML consentito esattamente com'è, quindi l'asserzione `toBe(html)` del primo test è rimasta invariata. Le asserzioni su `