nis2-agile/docs/CONTEXT_LAST_SESSION.md
DevEnv nis2-agile e4f9e9179e [FEAT] Allineamento NIS2 ↔ TRPG (Fasi 1-5): SSO + Sessions + Reset + Impersonate + Branding
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>
2026-05-29 13:18:35 +02:00

7.9 KiB

Contesto Ultima Sessione

Data: 2026-05-29 Durata: sessione molto lunga — progetto allineamento NIS2↔TRPG completato


Cosa abbiamo fatto

Progetto allineamento NIS2 ↔ TRPG (suite Evix)

Doc canonico: docs/GAP_TRPG_NIS2_ALIGNMENT.md

Analisi gap tra TRPG v1.54.1 e NIS2 v1.0.0 → 18 gap (G01-G18) raggruppati in 5 fasi. 6 decisioni utente confermate (§10 del doc). Tutte le 5 fasi completate in unica sessione. Version 1.0.0 → 1.5.0.

Fase 1 — SSO Federation (v1.1.0)

  • Migration 015_sso_columns.sql: aggiunge users.sso_identity_id, users.password_version
  • Nuovo application/services/SsoHelper.php — client SSO dual-mode (cURL nativo, zero deps)
  • AuthController::login() + changePassword() con conditional SSO (dietro SSO_MODE)
  • .env su Hetzner + vault-steward tier1__nis2-app__sso/internal_key (placeholder)
  • AgileHub Ticket #220 aperto a team AGILEHUB per estendere sso-password-sync.sh
  • SSO_MODE=local di default → comportamento utente invariato

Fase 2 — Multi-device Sessions (v1.2.0)

  • Migration 016_active_sessions.sql: tabella active_sessions (jti tracking) + refresh_tokens.session_jti
  • BaseController::requireAuth() verifica jti + last_activity throttle
  • BaseController::parseDeviceLabel($ua) — parsing UA-friendly
  • login() genera jti + insert active_sessions, logout() revoca selettiva, changePassword() revoca altre sessioni
  • 3 nuovi endpoint: GET/DELETE /auth/sessions[/{id}]
  • UI settings.html tab Sicurezza: card "Sessioni Attive" con device list + revoca

Fase 3 — Password Reset + Context Switch (v1.3.0)

  • Migration 017_password_reset.sql: tabella password_reset_tokens (TTL 30 min, single-use)
  • Endpoint POST /auth/forgot-password (risposta opaca anti-enumeration) + POST /auth/reset-password
  • Pagine HTML: forgot-password.html, reset-password.html (con strength bar)
  • login.html: link "Password dimenticata?" ora funzionante (era alert manuale)
  • EmailService::sendPasswordReset() aggiunto
  • Endpoint POST /auth/switchContext con rotazione JWT (revoca old session + nuovo jti + organization_id claim)
  • Dropdown tenant in sidebar esposto a TUTTI gli utenti con ≥2 org (prima solo consulenti)
  • _switchOrg() in common.js ora chiama switchContext + setTokens

Fase 4 — Impersonate + Preferences + Versioning UI (v1.4.0)

  • Endpoint POST /auth/impersonate (super_admin o consulente stesso firm, TTL 1h, JWT con impersonated_by claim, audit log)
  • Migration 018_user_preferences.sql: users.theme/timezone/notif_email/notif_inapp
  • Endpoint GET/PUT /auth/preferences
  • Sidebar footer mostra versione corrente, click → modal changelog
  • common.js _loadVersionFooter() fetcha /version.json al boot

Fase 5 — Branding + Auth-gate (v1.5.0)

  • Migration 019_firm_branding.sql: tabella firm_branding (logo/colori/brand name per consulting firm)
  • BrandingController.php (NUOVO): GET /branding/current (auth opzionale), PUT /branding (super_admin o consulente)
  • common.js _loadFirmBranding() applica CSS variables al boot
  • public/js/auth-gate.js copiato/adattato da TRPG (gate password client-side per documenti riservati)
  • G15 skip: simulator esistenti coprono demo flows
  • G18 skip: refactor controller rinviato (~5gg investimento, valore tecnico)

Scoperta CRITICA durante Fase 3: topologia DB

Vedi MEMORY.mdproject_db_topology.md per dettagli.

PHP-FPM nel container nis2-app (clear_env=yes + .env mancante in container) ricade su DB_HOST=localhost di default → connette al MySQL del HOST Hetzner, NON al container nis2-db.

Conseguenza: tutte le migration vanno applicate via:

mysql -u$DB_USER -p$DB_PASS -h localhost $DB_NAME < migration.sql

dal HOST Hetzner, NON docker exec nis2-db mysql.

Lo si è scoperto perché Fase 3 ha fatto 500 con "table password_reset_tokens doesn't exist" anche se la migration era stata applicata "con successo" — andava sul DB sbagliato.


SQL (5 migration nuove, tutte applicate al DB host):

  • docs/sql/015_sso_columns.sql
  • docs/sql/016_active_sessions.sql
  • docs/sql/017_password_reset.sql
  • docs/sql/018_user_preferences.sql
  • docs/sql/019_firm_branding.sql

PHP nuovi:

  • application/services/SsoHelper.php
  • application/controllers/BrandingController.php

PHP modificati:

  • application/controllers/AuthController.php (+13 metodi)
  • application/controllers/BaseController.php (requireAuth + parseDeviceLabel + generateRefreshToken)
  • application/services/EmailService.php (+sendPasswordReset)
  • application/config/config.php (+2 costanti)
  • public/index.php (+12 route + branding controller)

HTML/JS:

  • public/login.html (link forgot-password)
  • public/forgot-password.html (NUOVO)
  • public/reset-password.html (NUOVO)
  • public/settings.html (tab Sicurezza con sessions)
  • public/js/common.js (version footer + branding loader + tenant switcher esposto)
  • public/js/auth-gate.js (NUOVO)
  • public/version.json (1.0.0 → 1.5.0)

Doc:

  • docs/GAP_TRPG_NIS2_ALIGNMENT.md (NUOVO, piano completo + stato esecuzione)

Backups creati (tutti con timestamp 20260529-*):

  • /projects/nis2-agile/application/controllers/*.bak.pre-{sso,sessions,fase3,fase4}-*
  • /projects/nis2-agile/public/*.bak.pre-{sessions,fase3,fase4}-*
  • Hetzner /var/www/nis2-agile/.backups/*

File deployati su Hetzner

Tutti via scp -i .ssh-temp/id_ed25519_nis2-agile_8h_*:

  • 4 PHP in /var/www/nis2-agile/application/controllers/
  • 1 PHP in /var/www/nis2-agile/application/services/
  • 1 PHP in /var/www/nis2-agile/application/config/
  • 5 HTML/JSON in /var/www/nis2-agile/public/
  • 2 JS in /var/www/nis2-agile/public/js/

Nessun docker restart eseguito (non necessario, bind mount + PHP-FPM rilegge ad ogni request).


Decisioni utente confermate (§10 doc)

  1. SSO_MODE iniziale: local (switch a dual dopo ≥7gg validazione)
  2. Backfill SSO: lazy on-login (no script massivo)
  3. Sessioni concorrenti: nessun limite
  4. TTL token reset: 30 min
  5. Cadenza version bump: MINOR per fase (eseguito 1.1.0 → 1.5.0)
  6. Branding white-label: mantenuto in Fase 5 (eseguito)

Problemi aperti / dipendenze esterne

  • AgileHub Ticket #220 (priorità per switch a dual): estendere sso-password-sync.sh per includere DB nis2_agile_db. Status: OPEN, dispatchGroup=RESOLVER, dispatchProduct=AGILEHUB.
  • vault placeholder tier1__nis2-app__sso/internal_key: contiene PLACEHOLDER_REGISTER_WITH_TENANT_MS_BEFORE_ACTIVATING_DUAL_MODE. Va sostituito con chiave reale ottenuta dal Tenant MS prima del switch.
  • docker compose up -d --force-recreate app richiesto per attivare env vault SSO_INTERNAL_KEY quando si passa a dual (oggi opzionale, defaults sani lato codice).
  • SSO_MODE=localdual: modifica una sola riga in .env. Da eseguire solo dopo aver risolto le 3 dipendenze sopra. Bumpare version dopo (oltre 1.5.0).

Prossimi passi consigliati

  1. Validare 1.5.0 in produzione con il primo utente reale (test password reset + sessions UI + tenant switcher)
  2. Attendere risoluzione AgileHub Ticket #220
  3. Ottenere SSO_INTERNAL_KEY dal Tenant MS
  4. Replace placeholder vault: docker exec vault-steward node /app/cli/vault-cli.js migrate tier1__nis2-app__sso internal_key '<real>'
  5. Cambiare .env su Hetzner: SSO_MODE=localSSO_MODE=dual
  6. docker compose up -d --force-recreate app per ricaricare env
  7. Smoke test login utente con identità SSO esistente
  8. Bumpare a 1.6.0 con changelog "SSO dual mode attivato"

File chiave da sapere

  • CLAUDE.md — single source of truth governance
  • docs/GAP_TRPG_NIS2_ALIGNMENT.md — piano 5 fasi + stato esecuzione
  • MEMORY.md (auto) → project_db_topology.md — lezione CRITICA su DB host vs container
  • MEMORY.md (auto) → project_alignment_trpg.md — stato progetto allineamento