163 lines
9.4 KiB
Markdown
163 lines
9.4 KiB
Markdown
# Design — Modulo Questionari Fornitori + Portale Fornitore
|
|
|
|
> Stato: **DESIGN DEFINITIVO da approvare** (nessun codice scritto). Da rivedere prima dell'implementazione.
|
|
> Autore: sessione 2026-05-31. Origine: richiesta utente (Cristiano Benassati).
|
|
> **Decisioni utente confermate**: (1) categorie **predefinite + personalizzabili**; (2) auth fornitore via **magic-link / OTP email** (NO password); (3) scadenze/ricorrenze **configurabili** (nessun default imposto); (4) portale sullo **stesso dominio** `nis2.agile.software` → pagina `supplier-portal.html`.
|
|
> Base esistente riusata: `SupplyChainController` (self-assessment link-token, P3), `supplier-assessment.html`, tabella `supplier_questionnaires` (mig. 027), relay email AgileHub (`/api/emails/send-raw`, `X-Internal-Key`).
|
|
|
|
---
|
|
|
|
## 1. Obiettivo
|
|
|
|
Evolvere il self-assessment fornitori (oggi: 8 domande hardcoded, link usa-e-getta) in un **modulo questionari configurabile** con **portale fornitore** ad accesso OTP, che gestisce **categorie**, **domande estensibili** (anche per procedure interne), **scadenze/ricorrenze** e **aggiornamenti nel tempo**.
|
|
|
|
Valore: due-diligence continua dei fornitori (Art. 21.2(d) NIS2; coerente con ENISA supply chain / NIST 800-161), riutilizzabile anche per procedure interne (privacy, 231, ESG, qualità).
|
|
|
|
---
|
|
|
|
## 2. Cosa esiste già (riuso)
|
|
|
|
| Esistente | Riuso nel nuovo modulo |
|
|
|---|---|
|
|
| `SupplyChainController::sendQuestionnaire` (link-token sha256 + scadenza) | base per generare l'accesso OTP/campagna |
|
|
| `publicQuestionnaire` / `submitPublicQuestionnaire` (no-auth via token) | base compilazione online |
|
|
| `supplier-assessment.html` (pagina pubblica) | diventa il **portale fornitore** dinamico |
|
|
| relay email `/api/emails/send-raw` + `X-Internal-Key` | invio OTP e notifiche scadenza |
|
|
| pattern token `sq_` + sha256 + scadenza + single-use | riusato per OTP e accesso |
|
|
| costante `QUESTIONNAIRE` (8 domande) | migrata in DB come template "NIS2 base" di default |
|
|
|
|
---
|
|
|
|
## 3. Autenticazione fornitore — MAGIC-LINK / OTP (scelta utente)
|
|
|
|
**Principio**: nessuna password fornitore (zero credenziali da custodire/rubare). Accesso a ogni sessione tramite codice/temporale via email.
|
|
|
|
### Flusso
|
|
1. L'azienda registra il fornitore (anagrafica) con **email referente**.
|
|
2. Quando serve accesso (campagna o login spontaneo), il fornitore va su `supplier-portal.html` e inserisce la **propria email** → riceve un **magic-link** (token `sp_` + 32 byte) **e/o un codice OTP a 6 cifre**, validi ~15 minuti, single-use.
|
|
3. Cliccando il link / inserendo l'OTP, si apre una **sessione fornitore** (token di sessione separato, durata breve, es. 2h) limitata al **solo** profilo di quel fornitore.
|
|
4. Nel portale il fornitore vede: anagrafica propria, questionari assegnati (aperti/scaduti/completati), scadenze, storico.
|
|
|
|
### Sicurezza (vincolante)
|
|
- Tabella **separata** `supplier_users` — MAI in `users` interni. Nessun ruolo interno, nessun accesso a dati di altri fornitori o dell'organizzazione cliente.
|
|
- OTP: hash in DB (mai in chiaro), scadenza 15 min, max 5 tentativi, rate-limit per email/IP, invalidazione dopo uso.
|
|
- Sessione fornitore: JWT dedicato con claim `supplier_user_id` + `supplier_id` (NON `user_id`); scope whitelisted ai soli endpoint `/api/supplier-portal/*`.
|
|
- Enumerazione: risposta sempre "se l'email è registrata riceverai il codice" (opaca).
|
|
- Audit: login OTP, apertura/compilazione questionario loggati (`logAudit`).
|
|
- GDPR: l'email del referente è dato personale → retention + cancellazione su richiesta.
|
|
|
|
---
|
|
|
|
## 4. Schema dati (migrazioni nuove, additive)
|
|
|
|
```
|
|
supplier_categories
|
|
id, organization_id (NULL = preset di sistema), name, slug, description, active
|
|
-- SEED predefinito (personalizzabile): Cloud/IaaS, MSP/MSSP, Software/SaaS,
|
|
-- Hardware/Manutenzione, Consulenza, Logistica, Telecomunicazioni, Altro
|
|
|
|
questionnaire_templates
|
|
id, organization_id, category_id (FK, NULL = tutte le categorie),
|
|
name, version, scope ENUM('nis2','privacy','quality','231','esg','custom'),
|
|
recurrence_months (NULL = una tantum, configurabile),
|
|
due_days_default (NULL = nessun default), is_active, created_by, created_at
|
|
-- versioning come policy_versions: snapshot a ogni pubblicazione
|
|
|
|
questionnaire_questions
|
|
id, template_id, order_index, code, text,
|
|
type ENUM('yes_no_partial','scale_1_5','single_choice','multi_choice','text','number','file'),
|
|
options JSON, weight, required, help_text
|
|
|
|
questionnaire_campaigns
|
|
id, organization_id, supplier_id, template_id, template_version,
|
|
status ENUM('draft','sent','in_progress','completed','expired','overdue'),
|
|
access_token_hash, sent_to_email, sent_at,
|
|
due_at (NULL ammesso), recurrence_months, next_recurrence_at,
|
|
reminder_offsets JSON (es. [-14,-7,-1]; configurabile), next_reminder_at, reminder_count,
|
|
score, risk_level, completed_at
|
|
|
|
questionnaire_answers
|
|
id, campaign_id, question_id, answer_value, answer_text, file_ref, answered_at
|
|
|
|
supplier_users -- account fornitore (OTP, no password)
|
|
id, supplier_id, email, display_name, status, last_login_at, created_at
|
|
|
|
supplier_otp
|
|
id, supplier_user_id, otp_hash, magic_token_hash, purpose,
|
|
expires_at, used_at, attempts, ip_created
|
|
```
|
|
|
|
`suppliers` esistente: aggiungere `category_id` (FK) — `contact_email` già presente.
|
|
|
|
---
|
|
|
|
## 5. API
|
|
|
|
### Lato azienda (JWT interno)
|
|
- `GET/POST/PUT/DELETE /api/supply-chain/categories`
|
|
- `GET/POST/PUT /api/supply-chain/templates` (+ versioning)
|
|
- `GET/POST/PUT/DELETE /api/supply-chain/templates/{id}/questions`
|
|
- `POST /api/supply-chain/{supplierId}/campaigns` (scegli template per categoria, due_at, ricorrenza, reminder)
|
|
- `GET /api/supply-chain/campaigns` (cruscotto stato/scadenze)
|
|
|
|
### Lato fornitore (portale, sessione OTP — namespace dedicato `/api/supplier-portal/*`)
|
|
- `POST /api/supplier-portal/request-otp` (body: email) → invia OTP/magic-link (risposta opaca)
|
|
- `POST /api/supplier-portal/verify-otp` (email + codice) → sessione fornitore
|
|
- `GET /api/supplier-portal/me` → anagrafica + questionari assegnati + scadenze
|
|
- `GET /api/supplier-portal/questionnaire/{campaignId}` → template dinamico da compilare
|
|
- `POST /api/supplier-portal/questionnaire/{campaignId}/submit` → salva risposte + scoring
|
|
|
|
### Cron scadenze (`scripts/supplier-questionnaire-cron.sh`, TZ Europe/Rome)
|
|
- invia reminder agli offset configurati, marca `overdue`, apre la ricorrenza alla scadenza.
|
|
|
|
---
|
|
|
|
## 6. UI
|
|
|
|
- **supply-chain.html** (lato azienda): tab "Categorie", "Template questionari" (editor domande ordinabile), "Campagne/scadenze" (cruscotto con semaforo in-regola/in-scadenza/scaduto). Sulla scheda fornitore: dropdown categoria + "Avvia campagna".
|
|
- **supplier-portal.html** (NUOVA, stesso dominio): step OTP (inserisci email → inserisci codice) → dashboard fornitore (questionari assegnati, scadenze, storico) → compilazione dinamica del template. `noindex,nofollow`, nessun link all'app interna.
|
|
- **supplier-assessment.html**: confluisce nel portale o resta come fallback link-token diretto.
|
|
|
|
---
|
|
|
|
## 7. Fasi incrementali
|
|
|
|
| Fase | Contenuto | Rischio |
|
|
|---|---|---|
|
|
| **1** | Categorie (seed + CRUD) + template/domande configurabili in DB (migra le 8 domande) + render dinamico. Riusa il link-token attuale. | Basso |
|
|
| **2** | Campagne con scadenze/ricorrenze configurabili + reminder (cron) + cruscotto. | Medio |
|
|
| **3** | **Portale fornitore + auth OTP/magic-link** (`supplier_users`, `supplier_otp`, `/api/supplier-portal/*`, supplier-portal.html). | Alto (auth esterna) |
|
|
| **4** | Tipi domanda avanzati (allegati/scale/multi) + scoring configurabile per peso. | Medio |
|
|
|
|
Fasi 1-2 danno già categorie + questionari configurabili + scadenze. Fase 3 aggiunge il portale OTP persistente.
|
|
|
|
---
|
|
|
|
## 8. Punti di sicurezza non negoziabili
|
|
- `supplier_users`/sessione fornitore totalmente isolati da `users` interni (no IDOR cross-fornitore, no accesso dati org).
|
|
- OTP hashed, scadenza breve, rate-limit, risposta opaca.
|
|
- Segreti/eventuali integrazioni nel vault, non in DB/Git.
|
|
- Tutti gli endpoint `/api/supplier-portal/*` validano che il `campaignId`/risorsa appartenga al `supplier_id` della sessione.
|
|
|
|
---
|
|
|
|
## 9. Stima (multi-sessione)
|
|
- Fase 1: ~1 sessione · Fase 2: ~1 sessione · Fase 3 (portale+OTP): ~1.5-2 sessioni · Fase 4: ~0.5.
|
|
Tutto additivo; nessuna modifica distruttiva ai moduli esistenti.
|
|
|
|
---
|
|
|
|
## 10. Decisioni APPROVATE (utente, 2026-05-31)
|
|
1. ✅ Set categorie predefinite confermato (Cloud/IaaS, MSP/MSSP, Software/SaaS, Hardware/Manutenzione, Consulenza, Logistica, Telecomunicazioni, Altro) — personalizzabili.
|
|
2. ✅ Auth: **OTP a 6 cifre + magic-link, ENTRAMBI** (il fornitore può usare l'uno o l'altro).
|
|
3. ✅ **Sessione fornitore = 4 ore**; **validità OTP/magic-link = 15 minuti**, single-use, max 5 tentativi, rate-limit.
|
|
4. ✅ Processo: prima **review del design da 5 agenti**, poi implementazione partendo dalla **Fase 1**.
|
|
|
|
## 11. Promemoria post-implementazione (regola progetto — VINCOLANTE per ogni fase)
|
|
Ad ogni fase che cambia funzionalità, aggiornare SEMPRE:
|
|
- **Guida** (`public/guida.html`): nuovo capitolo/sezione "Portale fornitori" + àncora; allineare help.js `_guideAnchor`.
|
|
- **Help online** (`public/js/help.js`): sezione contestuale per supply-chain (questionari/portale) e per la pagina portale.
|
|
- **Traduzioni** (`public/js/i18n.js`): chiavi IT + EN per le nuove UI (categorie, template, campagne, portale, OTP).
|
|
- **KB AI / product_knowledge**: aggiornare la knowledge base AI (RAG + product_knowledge AgileHub) con la nuova funzionalità, così l'assistente la conosce.
|
|
- `version.json` bump, commit per fase, push da host.
|