nis2-agile/docs/sql/034_supplier_portal_auth.sql
DevEnv nis2-agile aa2db4c6c2 [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>
2026-05-31 09:53:07 +02:00

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;