Implementazione completa del progetto allineamento alla suite Evix (TRPG/lg231),
basato sul doc canonico docs/GAP_TRPG_NIS2_ALIGNMENT.md (5 fasi, 18 gap).
Version 1.0.0 → 1.5.0
Fase 1 — SSO Federation (v1.1.0)
- Migration 015_sso_columns: users.sso_identity_id + password_version
- application/services/SsoHelper.php (client SSO dual-mode, cURL nativo, zero deps)
- AuthController::login() + changePassword() conditional SSO (SSO_MODE=local default)
Fase 2 — Multi-device Sessions (v1.2.0)
- Migration 016_active_sessions: tabella + refresh_tokens.session_jti
- BaseController::requireAuth() verifica jti + last_activity throttle + parseDeviceLabel
- login() genera jti, logout/changePassword revoca selettiva
- GET/DELETE /auth/sessions[/{id}]
- UI settings.html tab Sicurezza con lista device + revoca
Fase 3 — Password Reset + Tenant Switcher (v1.3.0)
- Migration 017_password_reset_tokens (TTL 30min, single-use)
- POST /auth/forgot-password (risposta opaca) + reset-password
- Pagine forgot-password.html + reset-password.html (con strength bar)
- EmailService::sendPasswordReset
- POST /auth/switchContext con rotazione JWT + organization_id claim
- Dropdown tenant in sidebar esposto a tutti gli utenti con ≥2 org
Fase 4 — Impersonate + Preferences + Versioning UI (v1.4.0)
- POST /auth/impersonate (super_admin o consulente stesso firm, TTL 1h, audit)
- Migration 018_user_preferences: users.theme/timezone/notif_email/notif_inapp
- GET/PUT /auth/preferences
- Sidebar footer mostra versione + changelog modal su click
Fase 5 — Branding white-label + Auth-gate (v1.5.0)
- Migration 019_firm_branding (logo/colori/brand_name per consulting firm)
- BrandingController GET /branding/current (auth opzionale) + PUT
- common.js auto-applica CSS variables al boot
- public/js/auth-gate.js (gate password client-side per docs riservati, da TRPG)
Skip motivati:
- G15 demo login: simulator esistenti coprono
- G18 refactor controllers: rinviato (~5gg, valore tecnico solo)
Cron sync SSO: AgileHub Ticket #220 aperto a team AGILEHUB per estendere
sso-password-sync.sh al DB nis2_agile_db. Prerequisito per switch SSO_MODE=dual.
Backup files: tutti i file modificati hanno .bak.pre-{fase}-{ts} sia in DEV
sia in /var/www/nis2-agile/.backups/ su Hetzner (rollback ready).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
3.9 KiB
SQL
82 lines
3.9 KiB
SQL
-- Migration 016: Multi-device session tracking
|
|
-- Progetto allineamento NIS2 ↔ TRPG — Fase 2 / G05
|
|
-- Data: 2026-05-29
|
|
-- Mutuata da TRPG /var/www/trpg-agile/sql/078_active_sessions.sql
|
|
--
|
|
-- Crea tabella `active_sessions` per tracciare ogni sessione JWT come riga
|
|
-- distinta (chiave = jti del token), con device label + IP + last activity +
|
|
-- revoke audit. Permette:
|
|
-- - lista sessioni attive per utente (Settings → Sicurezza)
|
|
-- - revoca selettiva ("esci da questo dispositivo")
|
|
-- - revoca cascade su password change
|
|
-- - context switch tra organization (Fase 3) senza rotazione manuale
|
|
--
|
|
-- Differenze rispetto a TRPG:
|
|
-- - aggiunta colonna `organization_id` (NIS2 ha tenant esplicito X-Organization-Id)
|
|
-- - ENUM revoked_reason include già 'context_switch' (TRPG l'ha aggiunta in
|
|
-- migration 093; noi nasciamo con essa per evitare doppia migration)
|
|
--
|
|
-- Anche aggiungiamo `session_jti` a `refresh_tokens` per legare
|
|
-- ogni refresh alla sua sessione (rotazione safe).
|
|
--
|
|
-- Rollback:
|
|
-- ALTER TABLE refresh_tokens DROP INDEX idx_refresh_jti, DROP COLUMN session_jti;
|
|
-- DROP TABLE IF EXISTS active_sessions;
|
|
|
|
SET @tbl := (
|
|
SELECT COUNT(*) FROM information_schema.TABLES
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'active_sessions'
|
|
);
|
|
SET @sql_tbl := IF(@tbl = 0,
|
|
'CREATE TABLE active_sessions (
|
|
id CHAR(32) NOT NULL PRIMARY KEY COMMENT ''jti del JWT (bin2hex(random_bytes(16)))'',
|
|
user_id INT NOT NULL,
|
|
organization_id INT NULL COMMENT ''org attiva al momento del login'',
|
|
ip_address VARCHAR(45) NOT NULL,
|
|
user_agent VARCHAR(512) NULL,
|
|
device_label VARCHAR(120) NULL COMMENT ''Parsing UA-friendly (es. Chrome/Win11)'',
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ''login time'',
|
|
last_activity_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TIMESTAMP NOT NULL COMMENT ''Hard cap = login + refresh TTL'',
|
|
revoked_at TIMESTAMP NULL DEFAULT NULL,
|
|
revoked_reason ENUM(''logout'',''force'',''password_change'',''admin'',''expired_idle'',''context_switch'') NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
INDEX idx_user_active (user_id, revoked_at, last_activity_at),
|
|
INDEX idx_last_activity (last_activity_at),
|
|
INDEX idx_expires (expires_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
COMMENT=''JWT session tracking — solo utenti umani (API key B2B escluse)''',
|
|
'SELECT ''active_sessions già presente — skip'' AS info'
|
|
);
|
|
PREPARE stmt FROM @sql_tbl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
|
|
|
-- Aggiungi session_jti a refresh_tokens
|
|
SET @col_jti := (
|
|
SELECT COUNT(*) FROM information_schema.COLUMNS
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'refresh_tokens' AND COLUMN_NAME = 'session_jti'
|
|
);
|
|
SET @sql_col := IF(@col_jti = 0,
|
|
'ALTER TABLE refresh_tokens ADD COLUMN session_jti CHAR(32) NULL COMMENT ''FK logica → active_sessions.id'' AFTER user_id',
|
|
'SELECT ''refresh_tokens.session_jti già presente — skip'' AS info'
|
|
);
|
|
PREPARE stmt FROM @sql_col; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
|
|
|
SET @idx_jti := (
|
|
SELECT COUNT(*) FROM information_schema.STATISTICS
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'refresh_tokens' AND INDEX_NAME = 'idx_refresh_jti'
|
|
);
|
|
SET @sql_idx := IF(@idx_jti = 0,
|
|
'CREATE INDEX idx_refresh_jti ON refresh_tokens (session_jti)',
|
|
'SELECT ''idx_refresh_jti già presente — skip'' AS info'
|
|
);
|
|
PREPARE stmt FROM @sql_idx; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
|
|
|
-- Verifica finale
|
|
SELECT TABLE_NAME, ENGINE, TABLE_COMMENT
|
|
FROM information_schema.TABLES
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'active_sessions';
|
|
|
|
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_COMMENT
|
|
FROM information_schema.COLUMNS
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'refresh_tokens' AND COLUMN_NAME = 'session_jti';
|