[DOCS] Design definitivo portale fornitori + questionari (OTP/magic-link, categorie preset, scadenze configurabili, stesso dominio)

Decisioni utente: auth OTP/magic-link (no password, tabella supplier_users separata),
categorie predefinite+personalizzabili, scadenze configurabili, portale su supplier-portal.html
(stesso dominio). Schema DB, API (/api/supplier-portal/*), flusso OTP, 4 fasi, sicurezza
(isolamento totale da users interni, no IDOR), domande aperte per approvazione. NO codice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile 2026-05-31 08:55:56 +02:00
parent 7a23789b0f
commit 16a4027ac0

View File

@ -1,159 +1,154 @@
# Design — Modulo Questionari Fornitori configurabile # Design — Modulo Questionari Fornitori + Portale Fornitore
> Stato: **PROPOSTA DI DESIGN** (nessun codice scritto). Da approvare prima dell'implementazione. > Stato: **DESIGN DEFINITIVO da approvare** (nessun codice scritto). Da rivedere prima dell'implementazione.
> Autore: sessione 2026-05-31. Origine: richiesta utente (Cristiano Benassati). > Autore: sessione 2026-05-31. Origine: richiesta utente (Cristiano Benassati).
> Riferimenti prodotto attuali: `SupplyChainController` (self-assessment a link-token già esistente, P3), `supplier-assessment.html`, tabella `supplier_questionnaires` (migrazione 027), `acn_requirements` (migrazione 031). > **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 ## 1. Obiettivo
Evolvere l'attuale self-assessment fornitori (link-token usa-e-getta, 8 domande fisse) in un **modulo questionari configurabile e ricorrente**, che copra 4 capacità richieste: 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**.
1. **Anagrafica fornitore** con possibilità per il fornitore di compilare **online**. 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. **Questionari diversi per categoria fornitore** (es. cloud provider, MSP/MSSP, manutentore hardware, consulenza, logistica…).
3. **Domande configurabili ed estensibili** dall'azienda — non solo NIS2, ma anche per **procedure interne** (qualità, privacy, 231, ESG…).
4. **Gestione scadenze e ricorrenze**, con accesso del fornitore **in base al profilo registrato** e gestione degli **aggiornamenti** nel tempo.
Valore: trasforma il modulo Supply Chain da "valutazione una tantum" a **due-diligence continua dei fornitori** — coerente con Art. 21.2(d) NIS2 e con le best practice (ENISA supply chain, ISO 28000).
--- ---
## 2. Cosa esiste già (base di partenza) ## 2. Cosa esiste già (riuso)
| Componente esistente | Riuso | | Esistente | Riuso nel nuovo modulo |
|---|---| |---|---|
| `SupplyChainController::sendQuestionnaire` (link-token + scadenza 30gg) | base per le "campagne" | | `SupplyChainController::sendQuestionnaire` (link-token sha256 + scadenza) | base per generare l'accesso OTP/campagna |
| `publicQuestionnaire` / `submitPublicQuestionnaire` (no-auth via token) | base per la compilazione online senza account | | `publicQuestionnaire` / `submitPublicQuestionnaire` (no-auth via token) | base compilazione online |
| `supplier_questionnaires` (token_hash, status, score, expires_at) | da estendere con template_id e recurrence | | `supplier-assessment.html` (pagina pubblica) | diventa il **portale fornitore** dinamico |
| `supplier-assessment.html` (pagina pubblica) | da rendere dinamica (render del template) | | relay email `/api/emails/send-raw` + `X-Internal-Key` | invio OTP e notifiche scadenza |
| costante `QUESTIONNAIRE` (8 domande hardcoded) | da migrare in DB configurabile | | 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 |
**Gap da colmare:** le 8 domande sono hardcoded nel controller; non c'è categoria fornitore, né editor domande, né ricorrenza/scadenze gestite, né profilo fornitore persistente.
--- ---
## 3. Decisione architetturale chiave — accesso fornitore (ibrido) ## 3. Autenticazione fornitore — MAGIC-LINK / OTP (scelta utente)
Richiesta utente: valutare **ibrido**. Proposta: **Principio**: nessuna password fornitore (zero credenziali da custodire/rubare). Accesso a ogni sessione tramite codice/temporale via email.
### Modello consigliato: **token-per-campagna come default + account fornitore opzionale ("upgrade")** ### 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.
| Aspetto | Link-token (default) | Account fornitore (opzionale) | ### Sicurezza (vincolante)
|---|---|---| - Tabella **separata** `supplier_users` — MAI in `users` interni. Nessun ruolo interno, nessun accesso a dati di altri fornitori o dell'organizzazione cliente.
| Onboarding fornitore | zero attrito: clic sul link email | registrazione/credenziali | - OTP: hash in DB (mai in chiaro), scadenza 15 min, max 5 tentativi, rate-limit per email/IP, invalidazione dopo uso.
| Sicurezza | token sha256 + scadenza, single-purpose | superficie auth nuova da proteggere | - Sessione fornitore: JWT dedicato con claim `supplier_user_id` + `supplier_id` (NON `user_id`); scope whitelisted ai soli endpoint `/api/supplier-portal/*`.
| Storico per il fornitore | no (lo vede solo l'azienda) | sì: portale con questionari passati/scadenze | - Enumerazione: risposta sempre "se l'email è registrata riceverai il codice" (opaca).
| Ricorrenza/aggiornamenti | nuovo link a ogni campagna | il fornitore rientra e aggiorna | - Audit: login OTP, apertura/compilazione questionario loggati (`logAudit`).
| Costo implementativo | basso (estende l'esistente) | alto (auth esterna, sessioni, recovery) | - GDPR: l'email del referente è dato personale → retention + cancellazione su richiesta.
**Raccomandazione:** partire **token-per-campagna esteso** (Fase 1-2), e introdurre l'**account fornitore** solo in Fase 4, quando il valore lo giustifica. Un fornitore può "rivendicare" il proprio profilo (claim) partendo da un token valido → diventa account. Questo è l'ibrido: **token-first, account-on-demand**, senza costruire subito un IdP esterno.
> Sicurezza account fornitore (se/quando attivato): utenti in tabella separata `supplier_users` (MAI in `users` interni), password Argon2id, rate-limit, scope limitato alla sola anagrafica/questionari del proprio fornitore, nessun accesso ai dati dell'organizzazione cliente. Valutare riuso del pattern token effimero invece di password.
--- ---
## 4. Schema dati proposto (migrazioni nuove, additive) ## 4. Schema dati (migrazioni nuove, additive)
``` ```
supplier_categories supplier_categories
id, organization_id (NULL = globale/sistema), name, slug, description, active 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 questionnaire_templates
id, organization_id, category_id (FK supplier_categories, NULL = tutte), id, organization_id, category_id (FK, NULL = tutte le categorie),
name, version, scope ENUM('nis2','privacy','quality','231','esg','custom'), name, version, scope ENUM('nis2','privacy','quality','231','esg','custom'),
recurrence_months (NULL = una tantum), is_active, created_by, created_at recurrence_months (NULL = una tantum, configurabile),
-- versioning: come policy_versions, snapshot a ogni pubblicazione due_days_default (NULL = nessun default), is_active, created_by, created_at
-- versioning come policy_versions: snapshot a ogni pubblicazione
questionnaire_questions questionnaire_questions
id, template_id, order_index, code, text, id, template_id, order_index, code, text,
type ENUM('yes_no_partial','scale_1_5','single_choice','multi_choice','text','number','file'), type ENUM('yes_no_partial','scale_1_5','single_choice','multi_choice','text','number','file'),
options JSON (per choice), weight, required, help_text options JSON, weight, required, help_text
-- estensibili: l'azienda aggiunge domande per procedure interne
questionnaire_campaigns questionnaire_campaigns
id, organization_id, supplier_id, template_id, template_version, id, organization_id, supplier_id, template_id, template_version,
status ENUM('draft','sent','in_progress','completed','expired','overdue'), status ENUM('draft','sent','in_progress','completed','expired','overdue'),
token_hash, sent_to_email, sent_at, due_at, completed_at, access_token_hash, sent_to_email, sent_at,
reminder_count, next_reminder_at, score, risk_level 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 questionnaire_answers
id, campaign_id, question_id, answer_value, answer_text, file_ref, answered_at id, campaign_id, question_id, answer_value, answer_text, file_ref, answered_at
-- Fase 4 (opzionale): supplier_users -- account fornitore (OTP, no password)
supplier_users id, supplier_id, email, display_name, status, last_login_at, created_at
id, supplier_id, email, password_hash, status, last_login_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), `default_contact_email` già presente come `contact_email`. `suppliers` esistente: aggiungere `category_id` (FK)`contact_email` già presente.
--- ---
## 5. API proposte ## 5. API
**Lato azienda (JWT):** ### Lato azienda (JWT interno)
- `GET/POST /api/supply-chain/categories` — CRUD categorie fornitore - `GET/POST/PUT/DELETE /api/supply-chain/categories`
- `GET/POST/PUT /api/supply-chain/templates` — CRUD template + versioning - `GET/POST/PUT /api/supply-chain/templates` (+ versioning)
- `GET/POST/PUT/DELETE /api/supply-chain/templates/{id}/questions` — editor domande - `GET/POST/PUT/DELETE /api/supply-chain/templates/{id}/questions`
- `POST /api/supply-chain/{supplierId}/campaigns` — avvia campagna (sceglie template per categoria, imposta scadenza/ricorrenza) - `POST /api/supply-chain/{supplierId}/campaigns` (scegli template per categoria, due_at, ricorrenza, reminder)
- `GET /api/supply-chain/campaigns` — cruscotto campagne con scadenze/stato - `GET /api/supply-chain/campaigns` (cruscotto stato/scadenze)
**Lato fornitore (no-auth via token, evoluzione dell'esistente):** ### Lato fornitore (portale, sessione OTP — namespace dedicato `/api/supplier-portal/*`)
- `GET /api/supply-chain/public-questionnaire?token=` — ritorna il **template dinamico** (domande dal DB, non più hardcoded) - `POST /api/supplier-portal/request-otp` (body: email) → invia OTP/magic-link (risposta opaca)
- `POST /api/supply-chain/submit-public-questionnaire` — salva risposte multi-tipo + calcola score pesato - `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
**Scadenze (cron):** ### Cron scadenze (`scripts/supplier-questionnaire-cron.sh`, TZ Europe/Rome)
- `scripts/supplier-questionnaire-cron.sh` (Europe/Rome): invia reminder, marca `overdue`, apre la campagna ricorrente alla scadenza `recurrence_months`. - invia reminder agli offset configurati, marca `overdue`, apre la ricorrenza alla scadenza.
--- ---
## 6. UI proposta ## 6. UI
- **supply-chain.html**: tab "Categorie" + "Template questionari" (editor domande drag&drop o lista ordinabile); sulla scheda fornitore, dropdown categoria + "Avvia campagna" (sceglie template, scadenza, ricorrenza). - **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-assessment.html**: reso **dinamico** — renderizza qualsiasi template/tipi di domanda dal token. - **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.
- **Cruscotto scadenze**: lista campagne con semaforo (in regola / in scadenza / scaduta), reminder inviati. - **supplier-assessment.html**: confluisce nel portale o resta come fallback link-token diretto.
- (Fase 4) **portale fornitore**: login separato, "i miei questionari", storico, prossime scadenze.
--- ---
## 7. Fasi incrementali (proposta) ## 7. Fasi incrementali
| Fase | Contenuto | Rischio | Valore | | Fase | Contenuto | Rischio |
|---|---|---|---| |---|---|---|
| **1** | Categorie fornitore + template/domande configurabili in DB (migra le 8 domande hardcoded) + editor base lato azienda. `supplier-assessment.html` dinamico. | Basso (additivo) | Questionari per categoria + estensibili | | **1** | Categorie (seed + CRUD) + template/domande configurabili in DB (migra le 8 domande) + render dinamico. Riusa il link-token attuale. | Basso |
| **2** | Campagne con **scadenza + reminder** (cron) + cruscotto stato. Ricorrenza (`recurrence_months`). | Medio (cron) | Gestione scadenze/aggiornamenti | | **2** | Campagne con scadenze/ricorrenze configurabili + reminder (cron) + cruscotto. | Medio |
| **3** | Tipi domanda avanzati (allegati, scale, multi-choice) + scoring configurabile per peso. | Medio | Flessibilità procedure interne | | **3** | **Portale fornitore + auth OTP/magic-link** (`supplier_users`, `supplier_otp`, `/api/supplier-portal/*`, supplier-portal.html). | Alto (auth esterna) |
| **4** | **Account fornitore** (portale persistente, claim da token). | Alto (auth esterna) | Self-service completo | | **4** | Tipi domanda avanzati (allegati/scale/multi) + scoring configurabile per peso. | Medio |
Ogni fase è rilasciabile da sola. Le Fasi 1-2 coprono già l'80% della richiesta. Fasi 1-2 danno già categorie + questionari configurabili + scadenze. Fase 3 aggiunge il portale OTP persistente.
--- ---
## 8. Sicurezza / conformità (da rispettare) ## 8. Punti di sicurezza non negoziabili
- `supplier_users`/sessione fornitore totalmente isolati da `users` interni (no IDOR cross-fornitore, no accesso dati org).
- Token: `sq_` + 20 byte random, hash SHA-256 in DB, scadenza, single-use sul completamento (già fatto, da mantenere). - OTP hashed, scadenza breve, rate-limit, risposta opaca.
- Endpoint pubblici: nessun dato di altre org accessibile dal token (no IDOR — già verificato sul modulo attuale). - Segreti/eventuali integrazioni nel vault, non in DB/Git.
- Dati fornitore = dati personali del referente → rispetto GDPR (minimizzazione, retention, diritto cancellazione). - Tutti gli endpoint `/api/supplier-portal/*` validano che il `campaignId`/risorsa appartenga al `supplier_id` della sessione.
- Account fornitore (Fase 4): tabella separata, mai mischiare con `users`; secret nel vault se servono integrazioni.
- Audit: ogni invio/compilazione loggato (riuso `logAudit`).
--- ---
## 9. Domande aperte per l'utente (prima di implementare) ## 9. Stima (multi-sessione)
- Fase 1: ~1 sessione · Fase 2: ~1 sessione · Fase 3 (portale+OTP): ~1.5-2 sessioni · Fase 4: ~0.5.
1. Le **categorie fornitore** sono libere (definite dall'azienda) o vuoi un set predefinito di partenza (cloud/MSP/hardware/consulenza/logistica)? Tutto additivo; nessuna modifica distruttiva ai moduli esistenti.
2. La **ricorrenza** tipica: annuale? configurabile per template?
3. I **reminder**: quanti e a che intervallo prima/dopo la scadenza?
4. Lo **scoring**: soglia "requisiti soddisfatti" come oggi (≥70) o configurabile per template?
5. Fase 4 account fornitore: serve davvero o il link-token ricorrente basta?
--- ---
## 10. Stima (indicativa, multi-sessione) ## 10. Da approvare prima di implementare
1. Set categorie predefinite OK? (Cloud/IaaS, MSP/MSSP, Software/SaaS, Hardware/Manutenzione, Consulenza, Logistica, Telecomunicazioni, Altro)
- Fase 1: ~1 sessione (migrazione + CRUD template/domande + render dinamico). 2. OTP **a 6 cifre + magic-link** entrambi, o solo uno dei due?
- Fase 2: ~1 sessione (campagne + cron scadenze + cruscotto). 3. Durata sessione fornitore (proposta: 2h) e validità OTP (proposta: 15 min)?
- Fase 3: ~0.5 sessione. 4. Partiamo dalla **Fase 1** dopo l'approvazione?
- Fase 4: ~1-2 sessioni (auth fornitore + portale, la più delicata).
Nessuna di queste tocca i moduli esistenti in modo distruttivo: tutto **additivo**.