[DOCS] Design modulo questionari fornitori + portale OTP + AI consulente normativo
Pacchetto di design completo (nessun codice applicato, nessuna migrazione eseguita): - DESIGN aggiornato con 5 review agenti + 3 decisioni utente + pilastro AI consulente (sez. 12-14) - docs/supplier-portal/template-nis2-base.questions.json: 26 domande GV.SC (Allegato 2 ACN) con nis2_ref/vuln_flag e fonti certe verbatim - docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md: corpus normativo aggiornato + persona consulente (modello TRPG) - docs/supplier-portal/UX_MINI_SPEC.md: mini-spec portale fornitore (stati/copy/autosave/mobile/a11y/editor no-code) - docs/sql/032-035: migrazioni idempotenti proposte (modulo, suppliers, portale auth, migrazione 027->campaigns) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
dce53a0d0c
commit
aa2db4c6c2
@@ -0,0 +1,209 @@
|
||||
-- ============================================================================
|
||||
-- Migration 032 - Modulo Questionari Fornitori (supply chain Art.21.2.d)
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- PROPOSTA DI DESIGN (NON ancora applicata). Da eseguire su host MySQL.
|
||||
--
|
||||
-- SOSTITUISCE supplier_questionnaires (mig 027, da qui DEPRECATA/read-only) con
|
||||
-- un modello completo a template versionati + campagne + domande + risposte:
|
||||
--
|
||||
-- supplier_categories = tassonomia fornitori (preset di sistema + per-org)
|
||||
-- questionnaire_templates = modelli di questionario (per-org, opz. per categoria)
|
||||
-- questionnaire_template_versions= snapshot versionato (questions_snapshot JSON, weight)
|
||||
-- questionnaire_questions = domande "live" del template (ordinabili, pesate)
|
||||
-- questionnaire_campaigns = invio di un template a un fornitore (sostituisce 027)
|
||||
-- questionnaire_answers = risposte puntuali (answer_value JSON-ready)
|
||||
--
|
||||
-- Retrocompat: i token sq_ gia inviati (027) restano validi grazie alla
|
||||
-- colonna access_token_hash (stesso SHA-256 di supplier_questionnaires.token_hash),
|
||||
-- migrata da mig 035 (migrazione dati). 027 NON viene droppata: resta read-only.
|
||||
--
|
||||
-- IMPORTANTE (CLAUDE.md / memoria): MySQL 8 Ubuntu NON supporta
|
||||
-- "ADD COLUMN IF NOT EXISTS" / "CREATE INDEX IF NOT EXISTS". Le ALTER usano
|
||||
-- procedure idempotenti su information_schema. CREATE TABLE IF NOT EXISTS ok.
|
||||
-- DELIMITER non funziona via pipe -> usare "source".
|
||||
--
|
||||
-- Eseguire con:
|
||||
-- mysql -h localhost nis2_agile_db -e "source docs/sql/032_supplier_questionnaires_module.sql"
|
||||
--
|
||||
-- Ordine di applicazione: 032 -> 033 -> 034 -> 035 (vedi nota in 035).
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. Categorie fornitori (preset di sistema = organization_id 0)
|
||||
CREATE TABLE IF NOT EXISTS supplier_categories (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
organization_id INT NOT NULL DEFAULT 0 COMMENT '0 = preset di sistema (visibile a tutte le org)',
|
||||
slug VARCHAR(64) NOT NULL COMMENT 'Identificativo stabile, es. cloud_provider',
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
is_system TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 per i preset di sistema',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_supcat_org_slug (organization_id, slug),
|
||||
KEY idx_supcat_org (organization_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Tassonomia fornitori: preset di sistema (org 0) + categorie per-org';
|
||||
|
||||
-- 2. Template questionario (per-org, opz. legato a una categoria)
|
||||
CREATE TABLE IF NOT EXISTS questionnaire_templates (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
organization_id INT NOT NULL,
|
||||
category_id INT NULL COMMENT 'FK supplier_categories: template specifico per categoria',
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
current_version VARCHAR(20) NOT NULL DEFAULT '1.0' COMMENT 'Versione live (vedi questionnaire_template_versions)',
|
||||
status ENUM('draft','active','archived') NOT NULL DEFAULT 'draft',
|
||||
pass_threshold TINYINT UNSIGNED NULL COMMENT 'Soglia score 0-100 per esito positivo',
|
||||
is_default TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Template predefinito per la org/categoria',
|
||||
created_by INT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_qt_org (organization_id),
|
||||
KEY idx_qt_category (category_id),
|
||||
KEY idx_qt_org_status (organization_id, status),
|
||||
CONSTRAINT fk_qt_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_qt_category FOREIGN KEY (category_id) REFERENCES supplier_categories(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_qt_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Modelli di questionario di sicurezza fornitori (per organizzazione)';
|
||||
|
||||
-- 3. Versioni template (pattern policy_versions, snapshot domande+weight)
|
||||
CREATE TABLE IF NOT EXISTS questionnaire_template_versions (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
template_id INT NOT NULL,
|
||||
organization_id INT NOT NULL COMMENT 'Denormalizzato per filtri/authz',
|
||||
version VARCHAR(20) NOT NULL,
|
||||
questions_snapshot JSON NOT NULL COMMENT 'Snapshot completo domande incl. weight, opzioni, tipo',
|
||||
change_note VARCHAR(255) NULL,
|
||||
created_by INT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_qtv_template_version (template_id, version),
|
||||
KEY idx_qtv_template (template_id),
|
||||
KEY idx_qtv_org (organization_id),
|
||||
CONSTRAINT fk_qtv_template FOREIGN KEY (template_id) REFERENCES questionnaire_templates(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_qtv_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_qtv_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Storico versionato dei template questionario (snapshot domande+weight)';
|
||||
|
||||
-- 4. Domande "live" del template (ordinabili, pesate, multi-tipo)
|
||||
CREATE TABLE IF NOT EXISTS questionnaire_questions (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
template_id INT NOT NULL,
|
||||
organization_id INT NOT NULL COMMENT 'Denormalizzato per filtri/authz',
|
||||
question_code VARCHAR(50) NULL COMMENT 'Codice stabile opzionale, es. gvsc07_a_access_level',
|
||||
question_text TEXT NOT NULL,
|
||||
question_type ENUM('yes_no_partial','single_choice','multi_choice','scale_1_5','text','number','file') NOT NULL DEFAULT 'yes_no_partial',
|
||||
options JSON NULL COMMENT 'Opzioni per single_choice/multi_choice/scale',
|
||||
weight DECIMAL(6,2) NOT NULL DEFAULT 1.00 COMMENT 'Peso nello scoring',
|
||||
is_required TINYINT(1) NOT NULL DEFAULT 1,
|
||||
order_index INT NOT NULL DEFAULT 0 COMMENT 'Ordinamento di presentazione',
|
||||
nis2_ref VARCHAR(40) NULL COMMENT 'Riferimento normativo es. GV.SC-07.a',
|
||||
vuln_flag VARCHAR(64) NULL COMMENT 'Flag vulnerabilita acceso da risposta negativa',
|
||||
high_criticality_only TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 = mostra solo a fornitori high/critical',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_qq_template_order (template_id, order_index),
|
||||
KEY idx_qq_org (organization_id),
|
||||
CONSTRAINT fk_qq_template FOREIGN KEY (template_id) REFERENCES questionnaire_templates(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_qq_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Domande del template questionario fornitori (ordinabili e pesate)';
|
||||
|
||||
-- 5. Campagne (SOSTITUISCE supplier_questionnaires di mig 027)
|
||||
CREATE TABLE IF NOT EXISTS questionnaire_campaigns (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
organization_id INT NOT NULL,
|
||||
supplier_id INT NOT NULL,
|
||||
template_id INT NULL COMMENT 'Template usato (NULL se importata da 027 legacy)',
|
||||
template_version VARCHAR(20) NULL COMMENT 'Versione template congelata all''invio',
|
||||
access_token_hash CHAR(64) NOT NULL COMMENT 'SHA-256 del token sq_ inviato (retrocompat 027)',
|
||||
status ENUM('draft','sent','in_progress','completed','expired','cancelled') NOT NULL DEFAULT 'sent',
|
||||
score INT NULL COMMENT 'Punteggio 0-100 calcolato dalle risposte',
|
||||
risk_level ENUM('low','medium','high','critical') NULL,
|
||||
answers JSON NULL COMMENT 'Cache aggregata risposte (la verita e in questionnaire_answers)',
|
||||
show_score_to_supplier TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'F23: default OFF',
|
||||
language ENUM('it','en','auto') NOT NULL DEFAULT 'it' COMMENT 'Lingua portale/email',
|
||||
sent_to_email VARCHAR(255) NULL,
|
||||
sent_at DATETIME NULL,
|
||||
due_at DATETIME NULL COMMENT 'Scadenza compilazione',
|
||||
completed_at DATETIME NULL,
|
||||
expires_at DATETIME NULL,
|
||||
reminder_count INT NOT NULL DEFAULT 0,
|
||||
last_reminder_at DATETIME NULL,
|
||||
next_reminder_at DATETIME NULL COMMENT 'Prossimo invio reminder (cron)',
|
||||
reminder_offsets JSON NULL COMMENT 'Offset reminder, es. [-14,-7,-1]',
|
||||
is_recurring TINYINT(1) NOT NULL DEFAULT 0,
|
||||
recurrence_months TINYINT UNSIGNED NULL COMMENT 'Periodicita in mesi, se ricorrente',
|
||||
next_recurrence_at DATETIME NULL COMMENT 'Prossima riemissione (cron)',
|
||||
created_by INT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_qc_token (access_token_hash),
|
||||
KEY idx_qc_org (organization_id),
|
||||
KEY idx_qc_supplier (supplier_id),
|
||||
KEY idx_qc_template (template_id),
|
||||
KEY idx_qc_status_reminder (status, next_reminder_at),
|
||||
KEY idx_qc_status_due (status, due_at),
|
||||
KEY idx_qc_status_recurrence (status, next_recurrence_at),
|
||||
CONSTRAINT fk_qc_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_qc_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_qc_template FOREIGN KEY (template_id) REFERENCES questionnaire_templates(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_qc_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Campagne questionario inviate ai fornitori (sostituisce supplier_questionnaires/027)';
|
||||
|
||||
-- 6. Risposte puntuali (answer_value JSON-ready per multi_choice/file)
|
||||
CREATE TABLE IF NOT EXISTS questionnaire_answers (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
campaign_id INT NOT NULL,
|
||||
organization_id INT NOT NULL COMMENT 'Denormalizzato NOT NULL per filtri/authz',
|
||||
question_id INT NULL COMMENT 'FK domanda live (NULL se domanda legacy/snapshot)',
|
||||
question_code VARCHAR(50) NULL COMMENT 'Codice domanda dallo snapshot, per storicizzazione',
|
||||
answer_value JSON NULL COMMENT 'Valore risposta JSON-ready (scalare/array per multi_choice/file)',
|
||||
answer_text TEXT NULL COMMENT 'Testo libero',
|
||||
file_ref VARCHAR(255) NULL COMMENT 'Riferimento allegato',
|
||||
score_awarded DECIMAL(6,2) NULL COMMENT 'Punteggio attribuito alla risposta',
|
||||
answered_by INT NULL COMMENT 'supplier_user_id che ha risposto (multi-referente)',
|
||||
answered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_qa_campaign_question (campaign_id, question_id),
|
||||
KEY idx_qa_org (organization_id),
|
||||
KEY idx_qa_question (question_id),
|
||||
CONSTRAINT fk_qa_campaign FOREIGN KEY (campaign_id) REFERENCES questionnaire_campaigns(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_qa_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_qa_question FOREIGN KEY (question_id) REFERENCES questionnaire_questions(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Risposte puntuali alle campagne questionario (answer_value JSON-ready, append-only)';
|
||||
|
||||
-- 7. Seed idempotente preset di sistema (organization_id = 0) - 10 categorie
|
||||
INSERT INTO supplier_categories (organization_id, slug, name, description, is_system)
|
||||
VALUES
|
||||
(0, 'cloud_provider', 'Cloud Provider (IaaS/PaaS/SaaS)', 'Fornitori di servizi cloud e hosting', 1),
|
||||
(0, 'managed_security', 'Managed Security / MSSP', 'SOC, MDR, gestione sicurezza in outsourcing', 1),
|
||||
(0, 'connectivity_telco', 'Connettivita / Telco', 'Operatori di rete, ISP, connettivita', 1),
|
||||
(0, 'hardware_supplier', 'Fornitore Hardware', 'Apparati di rete, server, dispositivi', 1),
|
||||
(0, 'software_vendor', 'Software Vendor / SaaS applicativo', 'Vendor di software applicativo e licenze', 1),
|
||||
(0, 'professional_services', 'Servizi Professionali / Consulenza', 'Consulenza IT, legale, audit, compliance', 1),
|
||||
(0, 'logistics', 'Logistica e Trasporti', 'Fornitori di logistica, magazzino, spedizioni', 1),
|
||||
(0, 'facility_management', 'Facility Management', 'Gestione immobili, energia, impianti, sicurezza fisica',1),
|
||||
(0, 'datacenter_colocation', 'Data center / Colocation', 'Data center, housing, colocation, infrastruttura fisica',1),
|
||||
(0, 'tic_software_dev', 'TIC / Software Development', 'Sviluppo software su commessa, system integration, ICT', 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
description = VALUES(description),
|
||||
is_system = VALUES(is_system),
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
|
||||
-- ROLLBACK (manuale, ordine inverso per le FK):
|
||||
-- DROP TABLE IF EXISTS questionnaire_answers;
|
||||
-- DROP TABLE IF EXISTS questionnaire_campaigns;
|
||||
-- DROP TABLE IF EXISTS questionnaire_questions;
|
||||
-- DROP TABLE IF EXISTS questionnaire_template_versions;
|
||||
-- DROP TABLE IF EXISTS questionnaire_templates;
|
||||
-- DELETE FROM supplier_categories WHERE organization_id = 0 AND is_system = 1;
|
||||
-- DROP TABLE IF EXISTS supplier_categories;
|
||||
@@ -0,0 +1,79 @@
|
||||
-- ============================================================================
|
||||
-- Migration 033 - suppliers: categoria, provenienza import, soft-delete (guard)
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- PROPOSTA DI DESIGN (NON ancora applicata). Da eseguire su host MySQL.
|
||||
--
|
||||
-- Estende suppliers per il nuovo modulo questionari/import:
|
||||
-- - category_id : FK supplier_categories (classificazione fornitore)
|
||||
-- - external_ref : ID nel sistema sorgente (dedup import API/CSV)
|
||||
-- - source : ENUM('manual','csv','api') provenienza del record
|
||||
-- - deleted_at : soft-delete -- GIA PRESENTE da mig 006; aggiunto solo se assente
|
||||
-- + UNIQUE(organization_id, external_ref) per upsert idempotente.
|
||||
--
|
||||
-- NOTA REALE: suppliers.deleted_at e stato aggiunto dalla migration 006.
|
||||
-- Questo script lo riaggiunge SOLO se mancante. Nessuna duplicazione.
|
||||
--
|
||||
-- IMPORTANTE: MySQL 8 Ubuntu NON supporta "ADD COLUMN IF NOT EXISTS".
|
||||
-- Procedura idempotente su information_schema. DELIMITER -> usare "source".
|
||||
-- mysql -h localhost nis2_agile_db -e "source docs/sql/033_suppliers_category_source.sql"
|
||||
-- ============================================================================
|
||||
|
||||
DELIMITER //
|
||||
DROP PROCEDURE IF EXISTS _mig033 //
|
||||
CREATE PROCEDURE _mig033()
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers' AND COLUMN_NAME='category_id') THEN
|
||||
ALTER TABLE suppliers ADD COLUMN category_id INT NULL
|
||||
COMMENT 'FK supplier_categories: classificazione fornitore' AFTER service_type;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers' AND COLUMN_NAME='external_ref') THEN
|
||||
ALTER TABLE suppliers ADD COLUMN external_ref VARCHAR(190) NULL
|
||||
COMMENT 'ID fornitore nel sistema sorgente (dedup import API/CSV)' AFTER vat_number;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers' AND COLUMN_NAME='source') THEN
|
||||
ALTER TABLE suppliers ADD COLUMN source ENUM('manual','csv','api') NOT NULL DEFAULT 'manual'
|
||||
COMMENT 'Provenienza del record fornitore' AFTER external_ref;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers' AND COLUMN_NAME='deleted_at') THEN
|
||||
ALTER TABLE suppliers ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL AFTER updated_at;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers'
|
||||
AND CONSTRAINT_NAME='fk_supplier_category' AND CONSTRAINT_TYPE='FOREIGN KEY') THEN
|
||||
ALTER TABLE suppliers ADD CONSTRAINT fk_supplier_category
|
||||
FOREIGN KEY (category_id) REFERENCES supplier_categories(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers'
|
||||
AND INDEX_NAME='uq_supplier_external_ref') THEN
|
||||
ALTER TABLE suppliers ADD UNIQUE KEY uq_supplier_external_ref (organization_id, external_ref);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers'
|
||||
AND INDEX_NAME='idx_suppliers_not_deleted') THEN
|
||||
ALTER TABLE suppliers ADD INDEX idx_suppliers_not_deleted (organization_id, deleted_at);
|
||||
END IF;
|
||||
END //
|
||||
DELIMITER ;
|
||||
CALL _mig033();
|
||||
DROP PROCEDURE IF EXISTS _mig033;
|
||||
|
||||
-- ROLLBACK (manuale):
|
||||
-- ALTER TABLE suppliers
|
||||
-- DROP FOREIGN KEY fk_supplier_category,
|
||||
-- DROP INDEX uq_supplier_external_ref,
|
||||
-- DROP INDEX idx_suppliers_not_deleted,
|
||||
-- DROP COLUMN category_id,
|
||||
-- DROP COLUMN external_ref,
|
||||
-- DROP COLUMN source;
|
||||
-- -- NB: NON droppare deleted_at (di proprieta della mig 006).
|
||||
@@ -0,0 +1,85 @@
|
||||
-- ============================================================================
|
||||
-- Migration 034 - Portale fornitore: utenti, OTP/magic-link, sessioni revocabili
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- PROPOSTA DI DESIGN (NON ancora applicata). Da eseguire su host MySQL.
|
||||
--
|
||||
-- Login passwordless del fornitore per compilare le campagne questionario:
|
||||
-- supplier_users = referenti del fornitore abilitati al portale
|
||||
-- supplier_otp = codici OTP + magic token (hash), tentativi, scadenza
|
||||
-- supplier_sessions = sessioni con jti, revocabili (revoked_at)
|
||||
--
|
||||
-- Tutto NO-PASSWORD: l'accesso avviene via OTP/magic-link consegnato all'email.
|
||||
-- Sicurezza: lockout persistente su supplier_otp.attempts; sessione revocabile.
|
||||
--
|
||||
-- CREATE TABLE IF NOT EXISTS (idempotente). DELIMITER -> usare "source".
|
||||
-- mysql -h localhost nis2_agile_db -e "source docs/sql/034_supplier_portal_auth.sql"
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. Utenti del fornitore (referenti abilitati al portale)
|
||||
CREATE TABLE IF NOT EXISTS supplier_users (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
supplier_id INT NOT NULL,
|
||||
organization_id INT NOT NULL COMMENT 'Denormalizzato: org committente proprietaria del fornitore',
|
||||
email VARCHAR(255) NOT NULL,
|
||||
full_name VARCHAR(255) NULL,
|
||||
role ENUM('contact','compliance','signer') NOT NULL DEFAULT 'contact',
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
last_login_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_supuser_supplier_email (supplier_id, email),
|
||||
KEY idx_supuser_org (organization_id),
|
||||
KEY idx_supuser_email (email),
|
||||
CONSTRAINT fk_supuser_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_supuser_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Referenti del fornitore abilitati al portale (login passwordless)';
|
||||
|
||||
-- 2. OTP + magic-link (hash, tentativi, scadenza)
|
||||
CREATE TABLE IF NOT EXISTS supplier_otp (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
supplier_user_id INT NOT NULL,
|
||||
otp_hash CHAR(64) NULL COMMENT 'SHA-256 del codice OTP numerico',
|
||||
magic_token_hash CHAR(64) NULL COMMENT 'SHA-256 del magic-link token',
|
||||
purpose ENUM('login','questionnaire') NOT NULL DEFAULT 'login',
|
||||
attempts INT NOT NULL DEFAULT 0 COMMENT 'Tentativi di verifica falliti (lockout persistente)',
|
||||
max_attempts INT NOT NULL DEFAULT 5,
|
||||
locked_until DATETIME NULL COMMENT 'Lockout temporaneo dopo max_attempts',
|
||||
consumed_at DATETIME NULL COMMENT 'Quando usato (single-use)',
|
||||
expires_at DATETIME NOT NULL,
|
||||
ip_created VARCHAR(45) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_sotp_user (supplier_user_id),
|
||||
KEY idx_sotp_expires (expires_at),
|
||||
KEY idx_sotp_magic (magic_token_hash),
|
||||
CONSTRAINT fk_sotp_user FOREIGN KEY (supplier_user_id) REFERENCES supplier_users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='OTP e magic-link per autenticazione passwordless del fornitore';
|
||||
|
||||
-- 3. Sessioni revocabili (jti, scadenza, revoca)
|
||||
CREATE TABLE IF NOT EXISTS supplier_sessions (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
supplier_user_id INT NOT NULL,
|
||||
organization_id INT NOT NULL COMMENT 'Denormalizzato per filtri/audit',
|
||||
jti CHAR(36) NOT NULL COMMENT 'JWT ID (UUID) della sessione',
|
||||
ip_address VARCHAR(45) NULL,
|
||||
user_agent VARCHAR(500) NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
revoked_at DATETIME NULL COMMENT 'NOT NULL => sessione revocata',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_ssess_jti (jti),
|
||||
KEY idx_ssess_user (supplier_user_id),
|
||||
KEY idx_ssess_org (organization_id),
|
||||
KEY idx_ssess_expires (expires_at),
|
||||
CONSTRAINT fk_ssess_user FOREIGN KEY (supplier_user_id) REFERENCES supplier_users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_ssess_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Sessioni portale fornitore, revocabili via revoked_at';
|
||||
|
||||
-- ROLLBACK (manuale, ordine inverso per le FK):
|
||||
-- DROP TABLE IF EXISTS supplier_sessions;
|
||||
-- DROP TABLE IF EXISTS supplier_otp;
|
||||
-- DROP TABLE IF EXISTS supplier_users;
|
||||
@@ -0,0 +1,69 @@
|
||||
-- ============================================================================
|
||||
-- Migration 035 - Migrazione dati supplier_questionnaires (027) -> campaigns
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- PROPOSTA DI DESIGN (NON ancora applicata). Da eseguire PER ULTIMA su host MySQL.
|
||||
--
|
||||
-- Travasa le righe di supplier_questionnaires (mig 027) in
|
||||
-- questionnaire_campaigns (mig 032), preservando i token sq_ gia inviati:
|
||||
-- token_hash (027) -> access_token_hash (032) [stesso SHA-256]
|
||||
-- status (027: sent|completed|expired) -> status (032 ENUM esteso)
|
||||
-- answers/score/risk_level/email/date -> mappatura 1:1
|
||||
--
|
||||
-- Idempotente: INSERT ... SELECT con anti-join su access_token_hash (UNIQUE),
|
||||
-- quindi rilanciabile senza duplicare. template_id resta NULL (legacy).
|
||||
-- 027 NON viene droppata: resta read-only/deprecata per audit storico.
|
||||
--
|
||||
-- DIPENDE da: 032 (questionnaire_campaigns deve esistere) e 027.
|
||||
-- mysql -h localhost nis2_agile_db -e "source docs/sql/035_migrate_027_to_campaigns.sql"
|
||||
--
|
||||
-- Ordine globale: 032 -> 033 -> 034 -> 035.
|
||||
-- ============================================================================
|
||||
|
||||
DELIMITER //
|
||||
DROP PROCEDURE IF EXISTS _mig035 //
|
||||
CREATE PROCEDURE _mig035()
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='supplier_questionnaires')
|
||||
AND EXISTS (SELECT 1 FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='questionnaire_campaigns')
|
||||
THEN
|
||||
INSERT INTO questionnaire_campaigns
|
||||
(organization_id, supplier_id, template_id, template_version,
|
||||
access_token_hash, status, score, risk_level, answers,
|
||||
sent_to_email, sent_at, completed_at, expires_at, created_at)
|
||||
SELECT
|
||||
sq.organization_id,
|
||||
sq.supplier_id,
|
||||
NULL AS template_id,
|
||||
NULL AS template_version,
|
||||
sq.token_hash AS access_token_hash,
|
||||
CASE sq.status
|
||||
WHEN 'sent' THEN 'sent'
|
||||
WHEN 'completed' THEN 'completed'
|
||||
WHEN 'expired' THEN 'expired'
|
||||
ELSE 'sent'
|
||||
END AS status,
|
||||
sq.score,
|
||||
sq.risk_level,
|
||||
sq.answers,
|
||||
sq.sent_to_email,
|
||||
sq.sent_at,
|
||||
sq.completed_at,
|
||||
sq.expires_at,
|
||||
sq.created_at
|
||||
FROM supplier_questionnaires sq
|
||||
LEFT JOIN questionnaire_campaigns qc
|
||||
ON qc.access_token_hash = sq.token_hash
|
||||
WHERE qc.id IS NULL; -- anti-join: salta quelle gia migrate (idempotenza)
|
||||
END IF;
|
||||
END //
|
||||
DELIMITER ;
|
||||
CALL _mig035();
|
||||
DROP PROCEDURE IF EXISTS _mig035;
|
||||
|
||||
-- ROLLBACK (manuale): cancella SOLO le campagne legacy importate (template_id NULL,
|
||||
-- token corrispondente in supplier_questionnaires). NON tocca le campagne native.
|
||||
-- DELETE qc FROM questionnaire_campaigns qc
|
||||
-- JOIN supplier_questionnaires sq ON sq.token_hash = qc.access_token_hash
|
||||
-- WHERE qc.template_id IS NULL;
|
||||
Reference in New Issue
Block a user