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>
86 lines
4.7 KiB
SQL
86 lines
4.7 KiB
SQL
-- ============================================================================
|
|
-- 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;
|