Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Deploy stack su Proxmox / deploy (push) Failing after 3s
Deploy stack su Proxmox / deploy (push) Failing after 3s
Monorepo npm con apps/public (Angular SSR), apps/private (Angular SPA), apps/api (Fastify), packages/shared e packages/styles; Medusa 2.21 in medusa/; infrastruttura Docker (Postgres PostGIS+pgvector, Redis, Caddy, backup) e workflow Gitea Actions per il deploy via SSH a ogni push su master. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jQyT6dR9czzGwqb7FkG6u
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# Prompt Claude Code — ITI TECHNOLOGY
|
||||
|
||||
Esegui i prompt **in ordine**, uno per sessione. Ogni prompt ha un solo obiettivo e criteri di accettazione espliciti.
|
||||
Prima di ogni prompt Claude Code deve leggere `CLAUDE.md`; alla fine di ogni prompt deve aggiornarlo (sezione "Stato implementazione" + eventuali decisioni prese).
|
||||
|
||||
| Fase | Prompt |
|
||||
|---|---|
|
||||
| Fondamenta | 01 monorepo · 02 docker-compose dev |
|
||||
| Medusa | 03 scaffold · 04 regione/Stripe/IVA · 05 installatori price list · 06 route interne · 07 subscriber webhook |
|
||||
| API Fastify | 08 scaffold · 09 schema utenti · 10 schema dominio · 11 auth · 12 client Medusa + registrazione · 13 proxy shop · 14 webhook Medusa · 15 email · 16 storage/documenti · 17 blog · 18 configuratore · 19 preventivi/ticket · 20 installatori · 21 contatti/showroom/consensi |
|
||||
| App pubblica | 22 scaffold SSR · 23 home/soluzioni · 24 blog · 25 catalogo/carrello · 26 checkout · 27 configuratore · 28 contatti/showroom/cookie |
|
||||
| App privata | 29 scaffold + auth · 30 area clienti · 31 area installatori · 32 admin |
|
||||
| Produzione | 33 Caddy + compose prod · 34 hardening e review sicurezza |
|
||||
|
||||
Convenzioni valide per tutti i prompt:
|
||||
- Ambiente di sviluppo Windows/PowerShell: niente script bash, usa script npm o comandi cross-platform.
|
||||
- TypeScript strict ovunque.
|
||||
- Segreti solo in `.env` (mai committati), con `.env.example` aggiornato a ogni nuova variabile.
|
||||
- Dove un'API di Medusa v2 non è certa, **verificare nella documentazione ufficiale prima di implementare** e annotare in `CLAUDE.md` cosa è stato usato.
|
||||
@@ -0,0 +1,20 @@
|
||||
# 01 — Setup monorepo
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Creare la struttura base del monorepo, senza ancora codice applicativo.
|
||||
|
||||
## Cosa fare
|
||||
- npm workspaces con `apps/*` e `packages/*` (Medusa **escluso** dai workspace: resta un progetto indipendente in `medusa/`).
|
||||
- Crea `packages/shared` (TypeScript) per tipi e DTO condivisi tra `api`, `public` e `private`; esporta per ora solo un tipo `UserRole = 'customer' | 'company' | 'installer' | 'admin'`.
|
||||
- `.gitignore` (node_modules, dist, .env, uploads, logs, .angular), `.editorconfig`, `.nvmrc` con Node LTS, `.env.example` vuoto con commento.
|
||||
- `tsconfig.base.json` con strict mode, estesa dai package.
|
||||
- `README.md` con istruzioni di avvio minime.
|
||||
- Inizializza git.
|
||||
|
||||
## Criteri di accettazione
|
||||
- `npm install` dalla root funziona senza errori.
|
||||
- `packages/shared` compila (`npm run build -w packages/shared`).
|
||||
- Nessuna cartella `apps/*` ancora popolata oltre a placeholder `.gitkeep`.
|
||||
- `CLAUDE.md` aggiornato: struttura workspace + `packages/shared` documentato.
|
||||
@@ -0,0 +1,19 @@
|
||||
# 02 — Docker Compose per sviluppo locale
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Infrastruttura locale: Postgres (con PostGIS + pgvector) e Redis.
|
||||
|
||||
## Cosa fare
|
||||
- `infra/postgres/Dockerfile` basato su `postgis/postgis:16-3.4` che installa `postgresql-16-pgvector`.
|
||||
- Script init `infra/postgres/init/01-init.sql` che crea due database: `iti_app` (estensioni `postgis`, `vector`, `pg_trgm`, `unaccent`) e `iti_medusa`, con utenti distinti e password da variabili.
|
||||
- `docker-compose.dev.yml` con servizi `postgres` (volume persistente, porta 5432) e `redis` (porta 6379).
|
||||
- Variabili in `.env.example`: `POSTGRES_*`, `APP_DB_*`, `MEDUSA_DB_*`, `REDIS_URL`.
|
||||
- Script npm root: `infra:up`, `infra:down`, `infra:logs`.
|
||||
|
||||
## Criteri di accettazione
|
||||
- `npm run infra:up` avvia entrambi i container sani (healthcheck configurati).
|
||||
- Connettendosi a `iti_app`, `SELECT postgis_version();` e `CREATE EXTENSION IF NOT EXISTS vector;` funzionano.
|
||||
- Esistono entrambi i database con i rispettivi utenti.
|
||||
- `CLAUDE.md` aggiornato con comandi infra.
|
||||
@@ -0,0 +1,20 @@
|
||||
# 03 — Scaffold Medusa
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Creare il progetto Medusa v2 in `medusa/`, collegato al database `iti_medusa` e a Redis.
|
||||
|
||||
## Cosa fare
|
||||
- Usa `create-medusa-app` (senza storefront Next.js) in `medusa/`.
|
||||
- Configura `medusa-config.ts` con `DATABASE_URL` → `iti_medusa`, `REDIS_URL`, CORS (`STORE_CORS`, `ADMIN_CORS`, `AUTH_CORS`) da env.
|
||||
- Modulo file: storage locale su disco (cartella configurabile via env, che in produzione sarà un volume Docker).
|
||||
- Crea l'utente admin Medusa via CLI e documenta il comando.
|
||||
- Il dashboard admin integrato di Medusa sarà usato per gestire il catalogo nell'MVP (vedi nota in `CLAUDE.md`).
|
||||
|
||||
## Criteri di accettazione
|
||||
- `npm run dev` dentro `medusa/` avvia il server senza errori e le migrazioni sono applicate su `iti_medusa`.
|
||||
- Il dashboard admin è raggiungibile e permette il login.
|
||||
- `GET /health` risponde 200.
|
||||
- `medusa/.env.example` creato.
|
||||
- `CLAUDE.md` aggiornato: come avviare Medusa, dove sta il dashboard, decisione "catalogo gestito dal dashboard Medusa nell'MVP".
|
||||
@@ -0,0 +1,23 @@
|
||||
# 04 — Medusa: regione Italia, EUR, Stripe, IVA
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Configurare Medusa per vendere in Italia con pagamento Stripe.
|
||||
|
||||
## Cosa fare
|
||||
- Script seed idempotente (`medusa/src/scripts/seed-base.ts`) che crea:
|
||||
- store con valuta EUR;
|
||||
- regione "Italia" (paese IT, EUR) con provider di pagamento Stripe;
|
||||
- tax region IT con aliquota di default **22%** (provvisoria, vedi `CLAUDE.md`);
|
||||
- sales channel "Web" e una publishable API key associata;
|
||||
- una shipping option base (es. "Spedizione standard") con stock location di default.
|
||||
- Configura il payment provider Stripe in `medusa-config.ts` (`STRIPE_API_KEY`, `STRIPE_WEBHOOK_SECRET` da env). Verifica nella documentazione Medusa v2 il nome/percorso corretto del provider e l'endpoint webhook Stripe esposto da Medusa.
|
||||
- Crea 3 prodotti di esempio (un addolcitore, un kit fotovoltaico, una pompa di calore) con prezzi EUR.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Lo script si può eseguire più volte senza duplicare dati.
|
||||
- Con chiavi Stripe di **test**, un carrello creato via Store API mostra Stripe tra i payment provider della regione.
|
||||
- I totali del carrello includono IVA 22%.
|
||||
- La publishable key è stampata a console e documentata in `.env.example` di `apps/api` come `MEDUSA_PUBLISHABLE_KEY`.
|
||||
- `CLAUDE.md` aggiornato con l'endpoint webhook Stripe da registrare nella dashboard Stripe.
|
||||
@@ -0,0 +1,16 @@
|
||||
# 05 — Medusa: customer group installatori e listino riservato
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Listino riservato agli installatori tramite customer group + price list nativi.
|
||||
|
||||
## Cosa fare
|
||||
- Estendi lo script seed (o nuovo script idempotente) per creare il customer group `installatori` (con metadata `{ "code": "installer" }`).
|
||||
- Crea una price list di tipo override, attiva, condizionata al customer group `installatori`, con prezzi scontati sui prodotti di esempio.
|
||||
- Documenta come aggiungere un customer al gruppo (servirà a `apps/api` quando un admin approva un installatore).
|
||||
|
||||
## Criteri di accettazione
|
||||
- Un carrello associato a un customer del gruppo `installatori` mostra i prezzi della price list; un carrello guest o di un customer normale mostra i prezzi standard.
|
||||
- Lo script è idempotente.
|
||||
- `CLAUDE.md` aggiornato: id/codice del gruppo e come si usa.
|
||||
@@ -0,0 +1,25 @@
|
||||
# 06 — Medusa: route interne per il gateway Fastify
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Contesto
|
||||
Il frontend non parla mai con Medusa: Fastify è l'unico gateway e i customer Medusa non hanno password propria. Servono quindi alcune route interne, chiamabili **solo** da Fastify.
|
||||
|
||||
## Obiettivo
|
||||
Custom API routes in `medusa/src/api/internal/*` protette da segreto condiviso.
|
||||
|
||||
## Cosa fare
|
||||
- Middleware che accetta la richiesta solo se l'header `x-internal-secret` corrisponde a `INTERNAL_API_SECRET` (confronto a tempo costante); altrimenti 401.
|
||||
- Route:
|
||||
- `POST /internal/customers` → crea (o restituisce se esiste per email) un customer; body `{ email, first_name, last_name, company_name? }`.
|
||||
- `POST /internal/customers/:id/groups` → aggiunge/rimuove il customer dal gruppo `installatori`.
|
||||
- `POST /internal/carts/:id/customer` → associa un carrello a un customer.
|
||||
- `POST /internal/orders/claim` → collega al customer gli ordini guest con la stessa email; body `{ customer_id, email }`.
|
||||
- `GET /internal/customers/:id/orders` → elenco ordini del customer.
|
||||
- Implementa tramite i workflow ufficiali di Medusa v2 dove esistono (verifica nella documentazione quali: es. aggiornamento customer del carrello, trasferimento ordini). Annota in `CLAUDE.md` quali workflow sono stati usati.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Senza header o con segreto errato tutte le route rispondono 401.
|
||||
- Test (anche manuali documentati, es. file `.http`) che dimostrano: creazione customer idempotente, associazione carrello, claim di un ordine guest.
|
||||
- Dopo l'associazione al gruppo `installatori`, il carrello del customer applica la price list.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,17 @@
|
||||
# 07 — Medusa: subscriber che notificano Fastify
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Inviare a Fastify gli eventi rilevanti dell'e-commerce.
|
||||
|
||||
## Cosa fare
|
||||
- Subscriber in `medusa/src/subscribers/` per `order.placed`, `order.canceled`, `order.fulfillment_created` (verifica i nomi esatti degli eventi in Medusa v2).
|
||||
- Ogni subscriber fa `POST {FASTIFY_WEBHOOK_URL}/webhooks/medusa` con body `{ id, event, occurred_at, data }` dove `id` è un UUID univoco dell'evento.
|
||||
- Firma HMAC-SHA256 del body con `WEBHOOK_SECRET` nell'header `x-signature`.
|
||||
- Retry con backoff (max 5 tentativi) se Fastify non risponde 2xx; errori loggati, mai bloccanti per l'ordine.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Piazzando un ordine di test, viene inviata una richiesta firmata all'URL configurato (verificabile con un endpoint di test o log).
|
||||
- Se l'endpoint è giù, l'ordine si completa comunque e i retry sono visibili nei log.
|
||||
- `CLAUDE.md` aggiornato con formato payload e schema di firma.
|
||||
@@ -0,0 +1,22 @@
|
||||
# 08 — Scaffold API Fastify
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Creare `apps/api` con struttura, configurazione e logging, senza logica di dominio.
|
||||
|
||||
## Cosa fare
|
||||
- Fastify + TypeScript, avvio con `tsx watch` in dev, build con `tsc`.
|
||||
- Struttura: `src/routes/{public,private,admin,webhooks}`, `src/plugins`, `src/services`, `src/db`, `src/config`.
|
||||
- Config da env validata con `zod` all'avvio (il server non parte se manca qualcosa).
|
||||
- Logging con pino: console in dev (pino-pretty), file con rotazione in produzione (`LOG_DIR`).
|
||||
- Plugin base: `@fastify/helmet`, `@fastify/cors` (origini da env: dominio pubblico + area privata, `credentials: true`), `@fastify/cookie`, `@fastify/rate-limit` (limite globale ragionevole).
|
||||
- Validazione input/output con schemi (TypeBox o zod via `fastify-type-provider-zod`), scegline uno e documentalo.
|
||||
- Error handler centralizzato con risposte JSON coerenti `{ error: { code, message } }`, senza stack in produzione.
|
||||
- Route `GET /health`.
|
||||
|
||||
## Criteri di accettazione
|
||||
- `npm run dev -w apps/api` avvia il server; `GET /health` → 200.
|
||||
- Avvio con env mancante → errore chiaro che elenca le variabili mancanti.
|
||||
- In modalità produzione i log finiscono su file.
|
||||
- `CLAUDE.md` aggiornato con struttura e convenzioni API.
|
||||
@@ -0,0 +1,22 @@
|
||||
# 09 — API: database e schema utenti
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Drizzle ORM su `iti_app` con le tabelle di identità.
|
||||
|
||||
## Cosa fare
|
||||
- Drizzle ORM + drizzle-kit, migrazioni versionate in `apps/api/src/db/migrations`.
|
||||
- Plugin Fastify `db` che espone l'istanza Drizzle.
|
||||
- Tabelle:
|
||||
- `users`: id uuid, email unique (case-insensitive, salvata lowercase), password_hash, role (enum `customer|company|installer|admin`), first_name, last_name, company_name null, vat_number null, medusa_customer_id null unique, email_verified_at null, created_at, updated_at, disabled_at null.
|
||||
- `refresh_tokens`: id, user_id FK, token_hash, family_id, expires_at, revoked_at null, created_at, user_agent, ip.
|
||||
- `password_reset_tokens`: id, user_id FK, token_hash, expires_at, used_at null.
|
||||
- `email_verification_tokens`: stessa struttura.
|
||||
- Script `db:migrate`, `db:generate`, `db:seed` (seed: un utente admin da env `SEED_ADMIN_EMAIL/PASSWORD`).
|
||||
|
||||
## Criteri di accettazione
|
||||
- `npm run db:migrate -w apps/api` crea le tabelle su un DB vuoto.
|
||||
- Il seed crea l'admin ed è idempotente.
|
||||
- I token sono salvati solo come hash, mai in chiaro.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,22 @@
|
||||
# 10 — API: schema di dominio
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Tabelle di dominio dell'MVP (solo schema + migrazioni, niente route).
|
||||
|
||||
## Cosa fare
|
||||
- `installer_profiles`: user_id PK/FK, business_name, vat_number, phone, address, `location geography(Point,4326)`, `service_radius_km`, certifications text[], status (`pending|approved|rejected`), approved_at, notes. Indice GIST su location. (Drizzle: custom type per geography.)
|
||||
- `quote_requests`: id, user_id null (anche anonimi), email, name, phone, province, configurator_input jsonb, configurator_output jsonb, status (`new|contacted|converted|closed`), created_at.
|
||||
- `quotes`: id, quote_request_id null FK, user_id FK, number (progressivo leggibile), title, status (`draft|sent|accepted|rejected|expired`), amount_cents, pdf_document_id null, valid_until, created_at.
|
||||
- `tickets` + `ticket_messages`: ticket con user_id, subject, status (`open|in_progress|waiting_customer|closed`), priority; messaggi con author_id, body, created_at.
|
||||
- `blog_categories`, `blog_posts`: slug unique, title, excerpt, content_md, cover_path, status (`draft|published`), published_at, seo_title, seo_description, `search_vector tsvector` generata (configurazione `italian`, con `unaccent`) + indice GIN.
|
||||
- `documents`: id, title, category, file_path, mime_type, size_bytes, visibility (`public|customer|installer|admin`), product_ref null, created_at.
|
||||
- `contact_requests`, `showroom_bookings` (data/ora richiesta, stato), `consents` (user_id o email, tipo, versione testo, accepted_at, ip).
|
||||
- `webhook_events`: id (id evento Medusa, PK per idempotenza), event, payload jsonb, processed_at, error.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Migrazione applicata senza errori; indici GIST e GIN presenti.
|
||||
- Una query di prova su `blog_posts.search_vector` con `websearch_to_tsquery('italian', ...)` funziona.
|
||||
- I tipi delle tabelle sono esportati da `apps/api` (e i DTO pubblici in `packages/shared`).
|
||||
- `CLAUDE.md` aggiornato (ERD testuale sintetico).
|
||||
@@ -0,0 +1,21 @@
|
||||
# 11 — API: autenticazione JWT
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Login unico applicativo: registrazione, login, refresh, logout, reset password, verifica email.
|
||||
|
||||
## Cosa fare
|
||||
- Hash password con argon2id.
|
||||
- Access token JWT breve (15 min) restituito nel body; il frontend lo tiene in memoria.
|
||||
- Refresh token opaco (random 256 bit) in cookie `httpOnly`, `Secure`, `SameSite=Lax`, `Domain` = dominio padre (per condividerlo tra sottodomini), path `/auth`. Rotazione a ogni refresh, con rilevamento riuso (se un token revocato viene riusato → revoca l'intera family).
|
||||
- Route `public/auth`: `POST /register`, `POST /login`, `POST /refresh`, `POST /logout`, `POST /forgot-password`, `POST /reset-password`, `POST /verify-email`, `GET /me`.
|
||||
- Registrazione: ruolo di default `customer`; `company` se viene passata ragione sociale + P.IVA; `installer` crea anche un `installer_profiles` in stato `pending`. Mai assegnabile `admin` da questa route.
|
||||
- Decorator `authenticate` e `requireRole(...roles)` riutilizzabili.
|
||||
- Rate limit stretto su login/forgot-password. Messaggi di errore che non rivelano se un'email esiste.
|
||||
- Invio email tramite interfaccia `EmailService` (per ora può essere un adapter che logga a console: l'implementazione arriva nel prompt 15).
|
||||
|
||||
## Criteri di accettazione
|
||||
- Test automatici (vitest + `fastify.inject`) per: registrazione, login ok/ko, refresh con rotazione, riuso di refresh token revocato → family revocata, logout, `requireRole` che blocca ruoli errati.
|
||||
- Nessun token o password in chiaro nel DB o nei log.
|
||||
- `CLAUDE.md` aggiornato con il flusso auth.
|
||||
@@ -0,0 +1,19 @@
|
||||
# 12 — API: client Medusa e sincronizzazione account
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Plugin `medusaClient` e collegamento tra utenti Fastify e customer Medusa.
|
||||
|
||||
## Cosa fare
|
||||
- Plugin `medusa-client` con due client tipizzati: Store API (header publishable key) e route interne (header `x-internal-secret`). Timeout e gestione errori uniforme.
|
||||
- Alla registrazione (prompt 11): dopo aver creato `users`, chiama `POST /internal/customers` e salva `medusa_customer_id`. Se Medusa non risponde, la registrazione **non fallisce**: l'utente viene creato e la sincronizzazione viene ritentata al primo login o operazione shop (funzione `ensureMedusaCustomer(user)`).
|
||||
- Dopo la verifica email: chiama `POST /internal/orders/claim` per collegare gli ordini guest con la stessa email.
|
||||
- Quando un installatore viene approvato (verrà usato nel prompt 20): funzione `syncInstallerGroup(user)`.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Registrando un utente si crea il customer in Medusa e l'id viene salvato.
|
||||
- Con Medusa spento, la registrazione riesce e `ensureMedusaCustomer` recupera dopo.
|
||||
- Un ordine guest fatto prima della registrazione compare collegato dopo la verifica email.
|
||||
- Test con Medusa mockato.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,21 @@
|
||||
# 13 — API: proxy shop (catalogo, carrello, checkout)
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Esporre al frontend le operazioni e-commerce passando sempre da Fastify.
|
||||
|
||||
## Cosa fare
|
||||
- Catalogo (pubblico): `GET /shop/products` (paginazione, categoria, ricerca), `GET /shop/products/:handle`, `GET /shop/categories`. Cache in memoria breve (es. 60s) per le liste.
|
||||
- Prezzi: se la richiesta ha un utente autenticato installatore approvato, il calcolo prezzi deve usare il suo customer (price list riservata); altrimenti prezzi standard.
|
||||
- Carrello: id carrello in cookie `cart_id` (httpOnly). Route `POST /shop/cart`, `GET /shop/cart`, `POST /shop/cart/items`, `PATCH /shop/cart/items/:id`, `DELETE /shop/cart/items/:id`, `POST /shop/cart/address`, `GET /shop/cart/shipping-options`, `POST /shop/cart/shipping`.
|
||||
- Se l'utente è loggato, il carrello viene associato al suo customer (`/internal/carts/:id/customer`); se fa login con un carrello guest già aperto, il carrello viene associato.
|
||||
- Checkout guest: obbligatoria solo l'email.
|
||||
- Pagamento: `POST /shop/cart/payment` inizializza la payment session Stripe e restituisce il `client_secret`; `POST /shop/cart/complete` completa il carrello dopo la conferma Stripe lato client e restituisce l'ordine. Verifica nella documentazione Medusa v2 la sequenza esatta (payment collection → payment session → complete).
|
||||
- Il frontend non riceve mai chiavi o id interni Medusa non necessari.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Flusso completo testabile da file `.http`: crea carrello → aggiungi prodotto → indirizzo → spedizione → pagamento (carta test Stripe) → ordine creato.
|
||||
- Stesso flusso con utente installatore mostra i prezzi riservati.
|
||||
- Carrello guest associato correttamente al login.
|
||||
- `CLAUDE.md` aggiornato con l'elenco route shop.
|
||||
@@ -0,0 +1,21 @@
|
||||
# 14 — API: ricezione webhook Medusa
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Endpoint `POST /webhooks/medusa` sicuro e idempotente.
|
||||
|
||||
## Cosa fare
|
||||
- Verifica firma HMAC (`x-signature`) sul **raw body** con confronto a tempo costante; firma non valida → 401.
|
||||
- Idempotenza: inserisci l'evento in `webhook_events` con l'id ricevuto; se esiste già → 200 senza rielaborare.
|
||||
- Dispatcher per tipo evento:
|
||||
- `order.placed` → email di conferma ordine al cliente (template), notifica interna all'admin.
|
||||
- `order.canceled` → email di annullamento.
|
||||
- `order.fulfillment_created` → email di spedizione.
|
||||
- Errori di elaborazione salvati in `webhook_events.error`; risposta 500 così Medusa ritenta.
|
||||
- Endpoint escluso dal rate limit globale e dal CORS.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Test: firma valida → elaborato; firma errata → 401; stesso evento due volte → elaborato una volta sola.
|
||||
- Ordine di test reale da Medusa → email (anche solo loggata) inviata.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,22 @@
|
||||
# 15 — API: servizio email
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Invio email transazionali tramite provider esterno, con provider sostituibile.
|
||||
|
||||
## Contesto
|
||||
Il provider non è ancora scelto (Brevo o Resend). Implementa l'astrazione e un adapter per ciascuno, selezionabile da env.
|
||||
|
||||
## Cosa fare
|
||||
- Interfaccia `EmailService.send({ to, template, data })`.
|
||||
- Adapter: `console` (dev), `brevo`, `resend`, selezionati da `EMAIL_PROVIDER`.
|
||||
- Template in `src/emails/` (HTML semplice + versione testo) con layout comune brandizzato: verifica email, reset password, conferma ordine, spedizione, annullamento, nuova richiesta preventivo (interna), preventivo inviato, risposta ticket, conferma prenotazione showroom.
|
||||
- Escaping di tutti i dati inseriti nei template.
|
||||
- Invio asincrono: un errore del provider viene loggato e non fa fallire la richiesta HTTP che l'ha generato.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Con `EMAIL_PROVIDER=console` ogni template si renderizza e viene loggato.
|
||||
- Cambiare provider richiede solo variabili d'ambiente.
|
||||
- Test di rendering per ogni template.
|
||||
- `CLAUDE.md` aggiornato (variabili e lista template).
|
||||
@@ -0,0 +1,22 @@
|
||||
# 16 — API: storage su disco e documenti
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Gestione file su disco VPS con visibilità per ruolo.
|
||||
|
||||
## Cosa fare
|
||||
- `StorageService` (interfaccia + implementazione su disco locale, radice da `UPLOAD_DIR`) così in futuro si può passare a S3/MinIO senza toccare le route.
|
||||
- Due aree: `public/` (servita direttamente da Caddy: immagini blog, foto) e `private/` (mai servita da Caddy, solo tramite API).
|
||||
- Upload admin (`@fastify/multipart`): whitelist MIME (immagini, PDF, DWG/DXF, ZIP), limite dimensione da env, nome file rigenerato (uuid) e controllo path traversal.
|
||||
- Route:
|
||||
- `POST /admin/documents` (upload + metadati, visibility), `PATCH`, `DELETE`, `GET /admin/documents`.
|
||||
- `GET /documents` → elenco dei documenti visibili al ruolo dell'utente (pubblici se anonimo).
|
||||
- `GET /documents/:id/download` → stream del file solo se il ruolo lo consente.
|
||||
- `POST /admin/uploads/image` → upload immagine pubblica, restituisce l'URL.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Un installatore scarica documenti `installer`; un customer riceve 403 sugli stessi.
|
||||
- File con estensione/MIME non consentiti rifiutati; tentativi di path traversal falliscono.
|
||||
- I file privati non sono raggiungibili via URL statico.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,19 @@
|
||||
# 17 — API: blog
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Blog con contenuti markdown, ricerca full-text e gestione admin.
|
||||
|
||||
## Cosa fare
|
||||
- Pubblico: `GET /blog/posts` (paginazione, categoria, `q` per ricerca full-text con `websearch_to_tsquery('italian', unaccent(q))` e ordinamento per rank), `GET /blog/posts/:slug`, `GET /blog/categories`. Solo post `published` con `published_at <= now()`.
|
||||
- Il dettaglio restituisce sia `content_md` sia `content_html` renderizzato con `marked` e **sanitizzato** (`sanitize-html` con whitelist).
|
||||
- Admin: CRUD post e categorie, slug generato dal titolo e univoco, cambio stato draft/published, preview (`POST /admin/blog/preview` riceve markdown e restituisce HTML sanitizzato).
|
||||
- `GET /sitemap-data` con slug e date di aggiornamento (servirà al frontend SSR per la sitemap).
|
||||
|
||||
## Criteri di accettazione
|
||||
- La ricerca "pompa calore" trova un post intitolato "Pompe di calore: guida" (stemming italiano + unaccent).
|
||||
- Markdown con `<script>` o `onerror=` viene ripulito.
|
||||
- I draft non sono visibili via API pubblica.
|
||||
- Test per ricerca, sanitizzazione e permessi admin.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,20 @@
|
||||
# 18 — API: configuratore a regole
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Motore di raccomandazione deterministico (nessuna AI) che genera una `quote_request`.
|
||||
|
||||
## Cosa fare
|
||||
- Definizione del wizard in un file di configurazione versionato (`src/configurator/definition.ts` o JSON): step, domande, opzioni, condizioni di visibilità (branching). Percorsi iniziali per esigenza: "acqua calcarea", "bolletta alta / fotovoltaico", "riscaldamento / pompa di calore", "azienda".
|
||||
- Regole in file separato: soglie (es. consumo annuo kWh → taglia impianto kW; persone in casa + durezza acqua → taglia addolcitore), produttività per provincia/zona (kWh/kWp annui, tabella modificabile), prezzo medio energia, stima costo impianto.
|
||||
- Output: soluzione consigliata, taglia, prodotti suggeriti (handle Medusa), risparmio annuo stimato, tempo di rientro, nota incentivi (testo statico), avvertenza "stima indicativa".
|
||||
- Route: `GET /configurator/definition`, `POST /configurator/evaluate` (calcola senza salvare), `POST /configurator/submit` (salva `quote_requests` con input/output + contatti, consenso privacy obbligatorio, email interna all'admin e conferma all'utente).
|
||||
- Funzioni di calcolo pure (input → output), senza accesso al DB.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Test unitari sulle funzioni di calcolo con casi tabellari (almeno 3 per percorso).
|
||||
- Risposte incoerenti o mancanti rifiutate con errore di validazione.
|
||||
- `submit` senza consenso privacy → 400.
|
||||
- Cambiare una soglia richiede solo di modificare il file di regole.
|
||||
- `CLAUDE.md` aggiornato: dove stanno definizione e regole e come modificarle.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 19 — API: preventivi e ticket
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Gestione richieste preventivo, preventivi e assistenza.
|
||||
|
||||
## Cosa fare
|
||||
- Cliente (autenticato): `GET /me/quotes`, `GET /me/quotes/:id`, `POST /me/quotes/:id/accept`, `POST /me/quotes/:id/reject`; `GET /me/orders` (da Medusa tramite `/internal/customers/:id/orders`); `GET/POST /me/tickets`, `GET /me/tickets/:id`, `POST /me/tickets/:id/messages`.
|
||||
- Admin: elenco/filtri `quote_requests` con cambio stato; creazione `quotes` da una richiesta (numero progressivo per anno, es. `2026-0001`), allegato PDF tramite `documents`, invio al cliente (email); gestione ticket (stato, priorità, risposta).
|
||||
- Un utente vede solo le proprie risorse (controllo ownership in ogni route).
|
||||
- Quando un utente si registra con un'email che ha `quote_requests` anonime, queste vengono collegate al suo account.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Test: un cliente non può leggere preventivi/ticket di un altro (404).
|
||||
- Numerazione preventivi progressiva senza duplicati anche con richieste concorrenti.
|
||||
- Risposta admin a un ticket invia email al cliente.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,17 @@
|
||||
# 20 — API: installatori e PostGIS
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Profilo installatore, approvazione e ricerca per zona.
|
||||
|
||||
## Cosa fare
|
||||
- Installatore: `GET/PUT /installer/profile` (indirizzo, coordinate, raggio di servizio, certificazioni). Geocoding: per ora l'utente inserisce l'indirizzo e le coordinate arrivano da un selettore su mappa nel frontend (niente servizio di geocoding a pagamento); valida lat/lng.
|
||||
- Admin: `GET /admin/installers?status=`, `POST /admin/installers/:id/approve` (cambia ruolo/stato, chiama `syncInstallerGroup` per il customer group Medusa, email di conferma), `POST /admin/installers/:id/reject`.
|
||||
- Admin: `GET /admin/installers/nearby?lat=&lng=` → installatori approvati il cui raggio di servizio copre il punto, ordinati per distanza (`ST_DWithin` + `ST_Distance` su geography).
|
||||
- Utility per trovare la provincia di una `quote_request` → installatori vicini (verrà usata dall'admin per assegnare lavori).
|
||||
|
||||
## Criteri di accettazione
|
||||
- Test con 3 installatori a distanze diverse: la query restituisce solo quelli entro raggio, in ordine di distanza.
|
||||
- Un installatore `pending` non accede ai documenti `installer` né ai prezzi riservati; dopo l'approvazione sì.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,19 @@
|
||||
# 21 — API: contatti, showroom, consensi
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Form pubblici e registro consensi GDPR.
|
||||
|
||||
## Cosa fare
|
||||
- `POST /contact` → salva `contact_requests`, email all'admin. Protezione anti-spam senza servizi terzi: honeypot + tempo minimo di compilazione + rate limit per IP.
|
||||
- `GET /showroom/slots` (slot disponibili da configurazione: giorni/orari di apertura, durata visita, capienza) e `POST /showroom/bookings` (verifica disponibilità, email di conferma). Admin: elenco e cambio stato prenotazioni.
|
||||
- Consensi: ogni form con dati personali richiede consenso privacy; consenso marketing/newsletter separato e facoltativo. Salvati in `consents` con versione del testo informativo e timestamp.
|
||||
- `GET /me/consents` e revoca consenso marketing.
|
||||
- `POST /me/delete-account` (richiesta cancellazione: disabilita l'account e notifica l'admin; la cancellazione effettiva resta manuale nell'MVP).
|
||||
|
||||
## Criteri di accettazione
|
||||
- Form senza consenso privacy → 400.
|
||||
- Due prenotazioni sullo stesso slot oltre la capienza → la seconda rifiutata.
|
||||
- Honeypot compilato → richiesta scartata silenziosamente (200, nulla salvato).
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,21 @@
|
||||
# 22 — App pubblica: scaffold Angular SSR
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Creare `apps/public` con SSR, layout e mappa delle route (pagine vuote).
|
||||
|
||||
## Cosa fare
|
||||
- Angular ultima versione stabile, standalone, signals, zoneless, SCSS, `@angular/ssr`.
|
||||
- Design tokens in SCSS (colori, tipografia, spaziature, breakpoint) con palette coerente con acqua/energia/sostenibilità; layout responsive mobile-first.
|
||||
- Layout: header con menu macro-aree (Acqua, Energia, Comfort, Mobilità, Industria), Shop, Blog, Configuratore, Showroom, Contatti, link "Area riservata" verso il sottodominio privato; footer con dati aziendali, privacy, cookie.
|
||||
- Route (lazy) con componenti placeholder: `/`, `/azienda/*`, `/soluzioni/:area`, `/soluzioni/:area/:soluzione`, `/shop`, `/shop/:handle`, `/carrello`, `/checkout`, `/blog`, `/blog/:slug`, `/configuratore`, `/realizzazioni`, `/showroom`, `/contatti`, `/privacy`, `/cookie`, 404.
|
||||
- `ApiService` con base URL da environment, `withFetch()` e transfer state per non ripetere le chiamate lato client.
|
||||
- `SeoService`: title, meta description, canonical, Open Graph, JSON-LD (Organization, LocalBusiness).
|
||||
- Route SSR che genera `sitemap.xml` e `robots.txt` (usa `/sitemap-data` dell'API).
|
||||
|
||||
## Criteri di accettazione
|
||||
- `npm run dev -w apps/public` e build SSR funzionano; `curl` di una pagina restituisce HTML già renderizzato con title e meta corretti.
|
||||
- Nessun errore di hydration in console.
|
||||
- Lighthouse SEO ≥ 95 sulla home placeholder.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,23 @@
|
||||
# 23 — App pubblica: home e pagine soluzioni
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Homepage completa e pagine delle macro-aree/soluzioni.
|
||||
|
||||
## Contesto
|
||||
Nell'MVP i contenuti delle pagine istituzionali e soluzioni sono **file di contenuto versionati** (JSON/TS in `src/content/`), non un CMS. I testi definitivi verranno forniti dall'azienda: usa testi segnaposto realistici e chiaramente marcati.
|
||||
|
||||
## Cosa fare
|
||||
- Home nell'ordine: hero (payoff + CTA "Trova la soluzione giusta" verso il configuratore e "Richiedi preventivo"), settori (Acqua in evidenza), energia rinnovabile, servizi privati, servizi aziende, blocco configuratore, realizzazioni, partner/marchi, recensioni, ultimi articoli blog (da API), contatti.
|
||||
- Componente riutilizzabile "Qual è la tua esigenza?" con le esigenze principali che portano al configuratore con il percorso preselezionato (query param).
|
||||
- Pagina macro-area e pagina soluzione generate dai file di contenuto: descrizione, vantaggi, come funziona, prodotti correlati (handle Medusa → card da API shop), FAQ (con JSON-LD FAQPage), CTA preventivo.
|
||||
- Pagine azienda (chi siamo, certificazioni, partner, lavora con noi) e realizzazioni (galleria da file di contenuto).
|
||||
- Immagini ottimizzate con `NgOptimizedImage`.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Tutte le macro-aree del menu hanno almeno una pagina soluzione navigabile.
|
||||
- Ogni pagina ha meta e canonical propri; le FAQ producono JSON-LD valido.
|
||||
- Aggiungere una nuova soluzione richiede solo un nuovo file di contenuto.
|
||||
- Lighthouse performance ≥ 90 su mobile per la home.
|
||||
- `CLAUDE.md` aggiornato: come si modificano i contenuti.
|
||||
@@ -0,0 +1,17 @@
|
||||
# 24 — App pubblica: blog
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Elenco, ricerca e dettaglio articoli.
|
||||
|
||||
## Cosa fare
|
||||
- `/blog`: lista paginata con filtro categoria e campo ricerca (query param `q`, debounce), stato vuoto.
|
||||
- `/blog/:slug`: render dell'`content_html` già sanitizzato dall'API, immagine copertina, data, categoria, articoli correlati (stessa categoria), CTA verso configuratore.
|
||||
- SEO: meta da `seo_title`/`seo_description`, JSON-LD `Article`, canonical, paginazione con link corretti.
|
||||
- 404 reale (status code 404 lato SSR) per slug inesistente.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Ricerca, filtri e paginazione sono riflessi nell'URL e funzionano con refresh e SSR.
|
||||
- Slug inesistente → risposta HTTP 404.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,19 @@
|
||||
# 25 — App pubblica: catalogo e carrello
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Shop navigabile e carrello, consumando solo le route `/shop/*` di Fastify.
|
||||
|
||||
## Cosa fare
|
||||
- `/shop`: griglia prodotti con filtri categoria, ricerca, paginazione (stato nell'URL).
|
||||
- `/shop/:handle`: galleria, varianti, prezzo IVA inclusa, schede tecniche pubbliche collegate (da `/documents`), aggiungi al carrello, JSON-LD `Product`.
|
||||
- Stato carrello con signals in un `CartStore`; badge nel header; drawer o pagina `/carrello` con modifica quantità e rimozione.
|
||||
- Chiamate con `withCredentials` (cookie `cart_id`). Il carrello si carica solo lato browser, non durante SSR.
|
||||
- Se l'utente è loggato nell'area privata come installatore approvato e ha il token, i prezzi mostrati sono quelli riservati (documenta come l'app pubblica ottiene lo stato di login: chiamata `/auth/refresh` con il cookie condiviso tra sottodomini).
|
||||
|
||||
## Criteri di accettazione
|
||||
- Aggiungi/modifica/rimuovi prodotto aggiornano subito totali e badge.
|
||||
- Il carrello sopravvive al refresh della pagina.
|
||||
- Pagine prodotto renderizzate lato server con prezzo e meta corretti.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,19 @@
|
||||
# 26 — App pubblica: checkout con Stripe
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Checkout guest o autenticato con pagamento Stripe.
|
||||
|
||||
## Cosa fare
|
||||
- `/checkout` in step: contatti (email; opzione "accedi" se ha un account), indirizzo di spedizione/fatturazione (con campi azienda opzionali: ragione sociale, P.IVA, codice SDI/PEC salvati nei metadata), spedizione, pagamento, riepilogo.
|
||||
- Pagamento con Stripe Payment Element (`@stripe/stripe-js`) usando il `client_secret` da `POST /shop/cart/payment`; dopo conferma chiama `POST /shop/cart/complete`.
|
||||
- Gestione 3D Secure e return URL; errori di pagamento mostrati in modo chiaro.
|
||||
- Pagina `/ordine/:id/confermato` con riepilogo e invito a creare un account per seguire l'ordine (gli ordini guest verranno collegati automaticamente alla verifica email).
|
||||
- Checkout solo lato browser (non SSR); consenso privacy obbligatorio.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Pagamento con carta test Stripe → ordine creato in Medusa, email di conferma inviata (via webhook, prompt 14).
|
||||
- Carta test che richiede 3DS → flusso completato correttamente.
|
||||
- Carta rifiutata → errore mostrato, carrello intatto.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,20 @@
|
||||
# 27 — App pubblica: wizard configuratore
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Interfaccia del configuratore a regole.
|
||||
|
||||
## Cosa fare
|
||||
- `/configuratore` legge `GET /configurator/definition` e renderizza gli step dinamicamente (nessuna domanda hardcoded nel frontend).
|
||||
- Supporto percorso preselezionato da query param (`?esigenza=acqua-calcarea`).
|
||||
- Barra di avanzamento, indietro/avanti, validazione per step, stato salvato in `sessionStorage` per non perdere i dati al refresh.
|
||||
- Risultato da `POST /configurator/evaluate`: soluzione consigliata, risparmio annuo, tempo di rientro, incentivi, prodotti suggeriti (card shop), avvertenza "stima indicativa".
|
||||
- Form finale (nome, email, telefono, consenso privacy, consenso marketing facoltativo) → `POST /configurator/submit` → pagina di ringraziamento.
|
||||
- Accessibilità: navigazione da tastiera, label corrette, focus gestito al cambio step.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Aggiungere una domanda nella definizione lato API la fa comparire senza modificare il frontend.
|
||||
- Ogni percorso arriva a un risultato e a una `quote_request` salvata.
|
||||
- Refresh a metà wizard mantiene le risposte.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 28 — App pubblica: contatti, showroom, cookie consent
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Pagine contatto/showroom e conformità cookie.
|
||||
|
||||
## Cosa fare
|
||||
- `/contatti`: form verso `POST /contact` con honeypot e consenso privacy; dati aziendali; mappa con **OpenStreetMap/Leaflet** caricata solo dopo click ("Mostra mappa") per non impostare cookie di terzi.
|
||||
- `/showroom`: foto, descrizione, prenotazione visita con calendario slot da `GET /showroom/slots`.
|
||||
- Banner cookie custom (nessuna libreria SaaS): categorie necessari / statistiche / marketing, rifiuto facile quanto l'accettazione, preferenze salvate in un cookie first-party, riapribile dal footer. Nessuno script non necessario (analytics, embed) prima del consenso: servizio `ConsentService` che gli altri componenti interrogano.
|
||||
- Pagine `/privacy` e `/cookie` con testo segnaposto marcato "DA VALIDARE CON CONSULENTE PRIVACY".
|
||||
|
||||
## Criteri di accettazione
|
||||
- Al primo accesso nessun cookie non tecnico è impostato (verificabile da DevTools).
|
||||
- Rifiuto e accettazione sono a un click; la scelta è persistente e modificabile.
|
||||
- Prenotazione showroom completa con email di conferma.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,20 @@
|
||||
# 29 — App privata: scaffold e autenticazione
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Creare `apps/private` (SPA) con login, registrazione e routing per ruolo.
|
||||
|
||||
## Cosa fare
|
||||
- Angular standalone, signals, zoneless, SCSS, **senza SSR**. Riusa i design tokens dell'app pubblica (spostali in `packages/shared` o in una cartella SCSS condivisa).
|
||||
- `AuthStore` (signals): access token solo in memoria, utente corrente, ruolo. All'avvio tenta `POST /auth/refresh` (cookie) per ripristinare la sessione.
|
||||
- Interceptor: aggiunge il bearer token; su 401 fa un solo refresh (con coda delle richieste concorrenti) e ripete; se fallisce → logout e redirect al login.
|
||||
- Pagine: login, registrazione (privato / azienda / installatore con campi dedicati), password dimenticata, reset password, verifica email.
|
||||
- Guard per ruolo e layout con menu diverso per `customer/company`, `installer`, `admin`. Redirect post-login all'area corretta.
|
||||
- Link "Torna al sito" verso l'app pubblica.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Refresh della pagina mantiene la sessione (tramite cookie refresh) senza salvare token in localStorage.
|
||||
- Un customer che apre una route admin viene reindirizzato.
|
||||
- Token scaduto → refresh trasparente; più richieste in parallelo producono un solo refresh.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,19 @@
|
||||
# 30 — App privata: area clienti
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Dashboard per clienti privati e aziende.
|
||||
|
||||
## Cosa fare
|
||||
- Dashboard: ultimi ordini, preventivi aperti, ticket aperti.
|
||||
- Ordini: elenco e dettaglio (stato, articoli, spedizione) da `/me/orders`.
|
||||
- Preventivi: elenco, dettaglio, download PDF, accetta/rifiuta con conferma.
|
||||
- Ticket: elenco, apertura nuovo ticket, conversazione con messaggi.
|
||||
- Documenti: documenti visibili al ruolo, download.
|
||||
- Profilo: dati anagrafici (campi azienda se `company`), cambio password, consensi (revoca marketing), richiesta cancellazione account.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Tutte le sezioni gestiscono stato di caricamento, vuoto ed errore.
|
||||
- Accetta/rifiuta preventivo aggiorna lo stato senza ricaricare la pagina.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 31 — App privata: area installatori
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Sezione riservata agli installatori.
|
||||
|
||||
## Cosa fare
|
||||
- Se il profilo è `pending`: pagina di attesa con stato della richiesta e possibilità di completare il profilo.
|
||||
- Profilo: dati aziendali, certificazioni, indirizzo e posizione su mappa Leaflet/OpenStreetMap (click per impostare il punto), raggio di servizio con cerchio visualizzato.
|
||||
- Documentazione tecnica: documenti `installer` filtrabili per categoria (manuali, CAD, certificazioni, listini PDF).
|
||||
- Listino riservato: elenco prodotti con prezzo riservato e prezzo di listino a confronto (da `/shop/products` autenticato) e link per ordinare dallo shop.
|
||||
- Le funzioni dell'area clienti (ordini, ticket) restano disponibili anche all'installatore.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Installatore `pending` non vede documenti né listino riservato.
|
||||
- Posizione e raggio salvati correttamente (verificabili con la ricerca admin del prompt 20).
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,22 @@
|
||||
# 32 — App privata: pannello admin
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Pannello di amministrazione per tutto ciò che non è catalogo (il catalogo si gestisce dal dashboard Medusa nell'MVP: inserisci un link diretto).
|
||||
|
||||
## Cosa fare
|
||||
- Dashboard: nuove richieste preventivo, ticket aperti, installatori in attesa, prenotazioni showroom, contatti.
|
||||
- Richieste preventivo: tabella con filtri e stato, dettaglio con input/output del configuratore, installatori vicini (prompt 20), creazione preventivo con upload PDF e invio al cliente.
|
||||
- Ticket: lista, risposta, cambio stato/priorità.
|
||||
- Installatori: lista per stato, dettaglio, approva/rifiuta.
|
||||
- Utenti: ricerca, dettaglio, disabilita/riabilita.
|
||||
- Blog: lista articoli, editor **markdown custom** (textarea con toolbar minima per grassetto/titoli/link/immagine, anteprima live tramite `POST /admin/blog/preview` con debounce, upload immagine che inserisce il markdown), campi SEO, categorie, pubblica/bozza.
|
||||
- Documenti: upload con visibilità per ruolo, modifica, eliminazione.
|
||||
- Contatti e prenotazioni showroom: elenco e gestione stato.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Solo il ruolo `admin` accede (guard + verifica lato API già presente).
|
||||
- Un articolo scritto nell'editor appare sul sito pubblico una volta pubblicato, con anteprima identica al risultato finale.
|
||||
- Approva installatore → il suo account vede listino riservato e documenti.
|
||||
- `CLAUDE.md` aggiornato.
|
||||
@@ -0,0 +1,28 @@
|
||||
# 33 — Produzione: Docker Compose e Caddy
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Deploy completo sulla VPS con sottodomini sullo stesso dominio.
|
||||
|
||||
## Cosa fare
|
||||
- Dockerfile multi-stage per `apps/api`, `apps/public` (server SSR Node), `medusa`; `apps/private` buildata come statico e servita da Caddy.
|
||||
- `docker-compose.prod.yml`: postgres, redis, medusa, api, public, caddy. Rete interna; solo Caddy espone 80/443. Volumi: dati Postgres, uploads (condiviso api/caddy), file Medusa, log.
|
||||
- `Caddyfile` con dominio da variabile (`{$DOMAIN}`):
|
||||
- `{$DOMAIN}` e `www` → `public` (redirect www → root);
|
||||
- `area.{$DOMAIN}` → statico `apps/private` con fallback SPA a `index.html`;
|
||||
- `api.{$DOMAIN}` → `api`;
|
||||
- `shop-admin.{$DOMAIN}` → dashboard Medusa, protetto da basic auth e/o whitelist IP;
|
||||
- `media.{$DOMAIN}` → sola cartella `uploads/public` (mai `private`);
|
||||
- Medusa Store API **non** esposta pubblicamente, tranne l'endpoint webhook Stripe;
|
||||
- header di sicurezza (HSTS, X-Content-Type-Options, Referrer-Policy, CSP di base compatibile con Stripe), compressione, cache lunga per asset con hash.
|
||||
- Script npm `deploy:build`, `deploy:up`, `db:migrate:prod` e procedura di backup giornaliero (dump di entrambi i database + cartella uploads) con rotazione, da integrare con il backup già presente sulla VPS.
|
||||
- Documenta i record DNS necessari.
|
||||
|
||||
## Criteri di accettazione
|
||||
- `docker compose -f docker-compose.prod.yml up -d` su una macchina pulita porta su tutti i servizi con HTTPS automatico.
|
||||
- `api.{$DOMAIN}/health` → 200; area privata ricarica correttamente qualunque route (fallback SPA).
|
||||
- I file in `uploads/private` non sono raggiungibili via URL.
|
||||
- Dashboard Medusa non accessibile senza credenziali/IP consentito.
|
||||
- Ripristino da backup provato e documentato.
|
||||
- `CLAUDE.md` aggiornato con la procedura di deploy.
|
||||
@@ -0,0 +1,24 @@
|
||||
# 34 — Hardening e review di sicurezza
|
||||
|
||||
Leggi `CLAUDE.md`.
|
||||
|
||||
## Obiettivo
|
||||
Revisione finale prima del go-live. **Prima produci un report, poi applica le correzioni una per una.**
|
||||
|
||||
## Checklist da verificare (e correggere)
|
||||
- Ogni route `private/` e `admin/` ha autenticazione e controllo ruolo; ogni risorsa ha controllo ownership.
|
||||
- Tutti gli input validati da schema; nessuna query SQL costruita concatenando stringhe.
|
||||
- Rate limit su auth, form pubblici, configuratore, upload.
|
||||
- CORS limitato alle due origini; cookie `Secure`, `httpOnly`, `SameSite` corretti; nessun token in localStorage.
|
||||
- Upload: whitelist MIME, dimensione, nomi rigenerati, nessun path traversal.
|
||||
- HTML del blog sempre sanitizzato; nessun `innerHTML`/`bypassSecurityTrust*` su contenuti non sanitizzati nei frontend.
|
||||
- Webhook (Medusa e Stripe) con verifica firma e idempotenza.
|
||||
- Segreti: nessuno nel repo o nei log; `npm audit` senza vulnerabilità alte/critiche.
|
||||
- Errori in produzione senza stack trace; log senza password, token, dati carta.
|
||||
- Cookie banner: nessun cookie non tecnico prima del consenso.
|
||||
- Database: utente applicativo senza privilegi di superuser; Medusa e app su utenti distinti.
|
||||
|
||||
## Criteri di accettazione
|
||||
- Report `SECURITY_REVIEW.md` con esito di ogni voce (ok / corretto / da fare) e motivazione.
|
||||
- Tutti i test esistenti passano dopo le correzioni.
|
||||
- `CLAUDE.md` aggiornato con i punti rimasti aperti.
|
||||
Reference in New Issue
Block a user