emilsoftware-utilities 2.0.0-dev.41 → 2.0.0-dev.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/accessi-module/AccessiModule.d.ts +1 -0
  2. package/dist/accessi-module/AccessiModule.js +2 -0
  3. package/dist/accessi-module/AccessiModule.js.map +1 -1
  4. package/dist/accessi-module/Console/console.js +407 -30
  5. package/dist/accessi-module/Console/console.js.map +1 -1
  6. package/dist/accessi-module/Console/generated/accessiApi.d.ts +41 -1
  7. package/dist/accessi-module/Console/generated/accessiApi.js +34 -2
  8. package/dist/accessi-module/Console/generated/accessiApi.js.map +1 -1
  9. package/dist/accessi-module/Console/generated/model/confirmResetPasswordRequest.d.ts +1 -1
  10. package/dist/accessi-module/Console/generated/model/confirmResetPasswordRequest.js.map +1 -1
  11. package/dist/accessi-module/Console/generated/model/consoleEntryExchangeRequest.d.ts +10 -0
  12. package/dist/accessi-module/Console/generated/model/consoleEntryExchangeRequest.js +9 -0
  13. package/dist/accessi-module/Console/generated/model/consoleEntryExchangeRequest.js.map +1 -0
  14. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResponse.d.ts +14 -0
  15. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResponse.js +3 -0
  16. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResponse.js.map +1 -0
  17. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResult.d.ts +10 -0
  18. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResult.js +3 -0
  19. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResult.js.map +1 -0
  20. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResponse.d.ts +14 -0
  21. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResponse.js +3 -0
  22. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResponse.js.map +1 -0
  23. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResult.d.ts +12 -0
  24. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResult.js +9 -0
  25. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResult.js.map +1 -0
  26. package/dist/accessi-module/Console/generated/model/consoleEntryTokenResult.d.ts +12 -0
  27. package/dist/accessi-module/Console/generated/model/consoleEntryTokenResult.js +9 -0
  28. package/dist/accessi-module/Console/generated/model/consoleEntryTokenResult.js.map +1 -0
  29. package/dist/accessi-module/Console/generated/model/index.d.ts +7 -0
  30. package/dist/accessi-module/Console/generated/model/index.js +7 -0
  31. package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
  32. package/dist/accessi-module/Console/generated/model/setUserPasswordRequest.d.ts +14 -0
  33. package/dist/accessi-module/Console/generated/model/setUserPasswordRequest.js +9 -0
  34. package/dist/accessi-module/Console/generated/model/setUserPasswordRequest.js.map +1 -0
  35. package/dist/accessi-module/Console/help.js +116 -0
  36. package/dist/accessi-module/Console/help.js.map +1 -1
  37. package/dist/accessi-module/Console/wiki.js +14 -3
  38. package/dist/accessi-module/Console/wiki.js.map +1 -1
  39. package/dist/accessi-module/Controllers/ConsoleEntryController.d.ts +14 -0
  40. package/dist/accessi-module/Controllers/ConsoleEntryController.js +128 -0
  41. package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -0
  42. package/dist/accessi-module/Controllers/FiltriController.d.ts +8 -1
  43. package/dist/accessi-module/Controllers/FiltriController.js +21 -2
  44. package/dist/accessi-module/Controllers/FiltriController.js.map +1 -1
  45. package/dist/accessi-module/Controllers/PermissionController.d.ts +8 -1
  46. package/dist/accessi-module/Controllers/PermissionController.js +24 -4
  47. package/dist/accessi-module/Controllers/PermissionController.js.map +1 -1
  48. package/dist/accessi-module/Controllers/UserController.d.ts +12 -1
  49. package/dist/accessi-module/Controllers/UserController.js +175 -9
  50. package/dist/accessi-module/Controllers/UserController.js.map +1 -1
  51. package/dist/accessi-module/Dtos/ConfirmResetPasswordRequest.js +1 -1
  52. package/dist/accessi-module/Dtos/ConfirmResetPasswordRequest.js.map +1 -1
  53. package/dist/accessi-module/Dtos/ConsoleEntryDtos.d.ts +24 -0
  54. package/dist/accessi-module/Dtos/ConsoleEntryDtos.js +77 -0
  55. package/dist/accessi-module/Dtos/ConsoleEntryDtos.js.map +1 -0
  56. package/dist/accessi-module/Dtos/SetUserPasswordRequest.d.ts +4 -0
  57. package/dist/accessi-module/Dtos/SetUserPasswordRequest.js +32 -0
  58. package/dist/accessi-module/Dtos/SetUserPasswordRequest.js.map +1 -0
  59. package/dist/accessi-module/Services/AuthService/AuthService.d.ts +8 -2
  60. package/dist/accessi-module/Services/AuthService/AuthService.js +13 -9
  61. package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
  62. package/dist/accessi-module/Services/UserService/UserService.d.ts +15 -0
  63. package/dist/accessi-module/Services/UserService/UserService.js +126 -55
  64. package/dist/accessi-module/Services/UserService/UserService.js.map +1 -1
  65. package/dist/accessi-module/Views/reset-password.html +23 -7
  66. package/dist/accessi-module/federated-auth/FederatedAuthController.d.ts +10 -1
  67. package/dist/accessi-module/federated-auth/FederatedAuthController.js +50 -10
  68. package/dist/accessi-module/federated-auth/FederatedAuthController.js.map +1 -1
  69. package/dist/accessi-module/federated-auth/FederatedAuthService.d.ts +2 -0
  70. package/dist/accessi-module/federated-auth/FederatedAuthService.js +10 -0
  71. package/dist/accessi-module/federated-auth/FederatedAuthService.js.map +1 -1
  72. package/dist/accessi-module/security/accessControl.d.ts +26 -0
  73. package/dist/accessi-module/security/accessControl.js +37 -0
  74. package/dist/accessi-module/security/accessControl.js.map +1 -1
  75. package/dist/accessi-module/security/consoleEntry.d.ts +16 -0
  76. package/dist/accessi-module/security/consoleEntry.js +50 -0
  77. package/dist/accessi-module/security/consoleEntry.js.map +1 -0
  78. package/dist/accessi-module/security/passwordPolicy.d.ts +21 -0
  79. package/dist/accessi-module/security/passwordPolicy.js +71 -0
  80. package/dist/accessi-module/security/passwordPolicy.js.map +1 -0
  81. package/dist/accessi-module/security/publicAuthRateLimit.d.ts +1 -1
  82. package/dist/accessi-module/security/publicAuthRateLimit.js +1 -0
  83. package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
  84. package/dist/emilsoftware-utilities-2.0.0-dev.43.tgz +0 -0
  85. package/package.json +1 -1
  86. package/dist/emilsoftware-utilities-2.0.0-dev.41.tgz +0 -0
@@ -417,6 +417,45 @@ var deleteUser = async (codiceUtente, options) => {
417
417
  }
418
418
  );
419
419
  };
420
+ var getDeleteUserPermanentlyUrl = (codiceUtente) => {
421
+ return `/api/accessi/user/delete-user-permanent/${codiceUtente}`;
422
+ };
423
+ var deleteUserPermanently = async (codiceUtente, options) => {
424
+ return accessiFetch(
425
+ getDeleteUserPermanentlyUrl(codiceUtente),
426
+ {
427
+ ...options,
428
+ method: "DELETE"
429
+ }
430
+ );
431
+ };
432
+ var getForcePasswordResetLegacyUrl = () => {
433
+ return `/api/accessi/user/force-password-reset-legacy`;
434
+ };
435
+ var forcePasswordResetLegacy = async (options) => {
436
+ return accessiFetch(
437
+ getForcePasswordResetLegacyUrl(),
438
+ {
439
+ ...options,
440
+ method: "POST"
441
+ }
442
+ );
443
+ };
444
+ var getSetUserPasswordUrl = (codiceUtente) => {
445
+ return `/api/accessi/user/set-password/${codiceUtente}`;
446
+ };
447
+ var setUserPassword = async (codiceUtente, setUserPasswordRequest, options) => {
448
+ return accessiFetch(
449
+ getSetUserPasswordUrl(codiceUtente),
450
+ {
451
+ ...options,
452
+ method: "POST",
453
+ body: JSON.stringify(
454
+ setUserPasswordRequest
455
+ )
456
+ }
457
+ );
458
+ };
420
459
  var getSetStatoRegistrazioneUrl = () => {
421
460
  return `/api/accessi/user/set-stato`;
422
461
  };
@@ -462,6 +501,18 @@ var updateUtente = async (codiceUtente, userDto, options) => {
462
501
  }
463
502
  );
464
503
  };
504
+ var getSetGdprUrl = (codiceUtente) => {
505
+ return `/api/accessi/user/set-gdpr/${codiceUtente}`;
506
+ };
507
+ var setGdpr = async (codiceUtente, options) => {
508
+ return accessiFetch(
509
+ getSetGdprUrl(codiceUtente),
510
+ {
511
+ ...options,
512
+ method: "PATCH"
513
+ }
514
+ );
515
+ };
465
516
  var getGetTipiFiltroUrl = () => {
466
517
  return `/api/accessi/filtri/tipi`;
467
518
  };
@@ -1105,6 +1156,7 @@ POST /api/accessi/auth/two-factor/resend
1105
1156
  POST /api/accessi/auth/confirm-reset-password/{token}
1106
1157
  { "newPassword": "NuovaSegreta123!" }`, "Reset in due passi"),
1107
1158
  p(`L'invio non rivela se l'email esiste (anti-enumerazione). Il token di reset \xE8 monouso ed \xE8 firmato con una chiave dedicata.`),
1159
+ note("warn", `Policy password (solo quando si imposta)`, `Reset, set-password admin e bootstrap richiedono almeno 8 caratteri con maiuscola, minuscola, cifra e carattere speciale, senza spazi e non comuni. Se non conforme l'API risponde 400 con code ACCESSI_WEAK_PASSWORD e l'elenco dei requisiti in details. La policy NON si applica al login: le password preesistenti eventualmente deboli continuano ad autenticare e vengono migrate senza blocco.`),
1108
1160
  note("info", `Pagina personalizzata`, `Imposta customResetPage per usare la tua pagina di reset invece di quella inclusa. confirmationEmailReturnUrl e confirmationEmailPrefix controllano il ritorno.`)
1109
1161
  ]
1110
1162
  },
@@ -1201,12 +1253,18 @@ POST /api/accessi/auth/confirm-reset-password/{token}
1201
1253
  ["GET", "/api/accessi/user/get-users", "Elenco con paginazione (limit e offset, header X-Total-Count)."],
1202
1254
  ["POST", "/api/accessi/user/create-managed-user", "Crea un utente locale e invia il reset password."],
1203
1255
  ["PUT", "/api/accessi/user/update-user/:codiceUtente", "Aggiorna profilo e policy (campi privilegiati solo superutente)."],
1204
- ["DELETE", "/api/accessi/user/delete-user/:codiceUtente", "Soft delete: STAREG=DELETE."],
1256
+ ["DELETE", "/api/accessi/user/delete-user/:codiceUtente", "Soft delete: STAREG=DELETE. Admin o superutente."],
1257
+ ["DELETE", "/api/accessi/user/delete-user-permanent/:codiceUtente", "Eliminazione definitiva: rimuove utente e record collegati. Solo admin."],
1258
+ ["POST", "/api/accessi/user/set-password/:codiceUtente", "Imposta direttamente la password: hash lato backend e scadenza secondo policy, nessuna email."],
1205
1259
  ["POST", "/api/accessi/user/set-stato", "Imposta lo stato di registrazione."],
1206
1260
  ["PATCH", "/api/accessi/user/set-gdpr/:codiceUtente", "Registra il consenso GDPR."],
1207
1261
  ["POST", "/api/accessi/user/register", "Registrazione pubblica, solo se publicRegistration.enabled."]
1208
1262
  ]
1209
1263
  ),
1264
+ note("warn", `Eliminazione: soft o definitiva`, `Il soft delete (STAREG=DELETE) \xE8 disponibile per admin e superutenti ed \xE8 reversibile. L'eliminazione definitiva rimuove utente e record collegati in transazione ed \xE8 riservata agli admin; il superutente senza flag Admin pu\xF2 solo flaggare l'utente come eliminato. Se l'utente ha dati collegati in altre tabelle (per esempio campi estesi del backend) l'eliminazione definitiva fallisce con code ACCESSI_USER_DELETE_HAS_REFERENCES e messaggio esplicito: usa allora il soft delete.`),
1265
+ note("ok", `Creazione con invito atomica`, `La creazione utente con invio email \xE8 atomica: se SMTP non \xE8 configurato si fallisce prima di scrivere, e se l'invio fallisce l'utente creato viene annullato (codice ACCESSI_USER_INVITE_EMAIL_FAILED). Non restano utenti senza password. In alternativa un admin crea l'utente e imposta la password dal form dedicato.`),
1266
+ note("ok", `Admin: controllo completo del profilo`, `Dalla scheda utente un admin modifica tutti i campi Accessi: anagrafica, cellulare, lingua, avatar, pagina di default, metadata JSON, 2FA/passwordless/login con password, flag, GDPR, stato registrazione, scadenza password e filtri (tabella FILTRI). La password si imposta direttamente con set-password. Le scritture sono idempotenti (UPDATE OR INSERT su UTENTI_CONFIG) e verificate dal backend; non serve accedere al database.`),
1267
+ note("info", `Consenso GDPR`, `Il pulsante "Registra consenso GDPR" completa il processo: imposta FLGGDPR e DATGDPR e scrive la riga storica in UTENTI_GDPR (endpoint set-gdpr). Anche impostare flagGdpr via update-user completa il processo con la stessa riga storica.`),
1210
1268
  good("ts", [
1211
1269
  "await fetch('/api/accessi/user/create-managed-user', {",
1212
1270
  " method: 'POST',",
@@ -1413,9 +1471,13 @@ POST /api/accessi/auth/confirm-reset-password/{token}
1413
1471
  `SQL: esporta il DDL delle entit\xE0 Accessi (tabelle, generatori, FK, indici, check, trigger) per replicare il database.`
1414
1472
  ]),
1415
1473
  note("info", `Superutente (FLGSUPER)`, `Riceve TUTTE le abilitazioni al livello massimo (30) dopo il login. In console vede solo la sezione Utenti e gestisce utenti, ruoli e grant.`),
1416
- note("info", `Admin (FLGADMIN)`, `Accede a tutta la console (catalogo, token, SSO, utenti) ma NON conferisce abilitazioni: i menu restano quelli assegnati via ABILITAZIONI/RUOLI_MNU.`),
1474
+ note("info", `Admin (FLGADMIN)`, `Accede a tutta la console (catalogo, token, SSO, utenti) ma NON conferisce abilitazioni: i menu restano quelli assegnati via ABILITAZIONI/RUOLI_MNU. Gerarchia: ADMIN > SUPER > utente.`),
1475
+ note("info", `Gerarchia dei ruoli`, `Vale ADMIN (2) > SUPER (1) > utente comune (0). Un attore pu\xF2 gestire solo utenti di rango pari o inferiore: un superutente pu\xF2 amministrare totalmente utenti comuni e superutenti (profilo, password, flag, ruoli, grant, stato, eliminazione, filtri e identit\xE0/utenti SSO) ma NON pu\xF2 toccare un admin. Un admin pu\xF2 gestire chiunque. La regola \xE8 imposta dal backend (ensureCanManageTargetUser): un tentativo non consentito restituisce 403.`),
1476
+ note("info", `Gestione dei flag`, `Dalla scheda utente i flag sono mostrati in base al ruolo di chi \xE8 connesso: un admin pu\xF2 assegnare o togliere sia il flag Admin sia il flag Superutente; un superutente senza flag Admin pu\xF2 gestire solo il flag Superutente. I flag si impostano anche in creazione (utente locale o SSO).`),
1477
+ note("warn", `Ultimo admin`, `Per non restare chiusi fuori dalla console, l'ultimo admin attivo non pu\xF2 essere disattivato, bloccato, eliminato n\xE9 privato del flag Admin: l'operazione restituisce 400 finch\xE9 non viene promosso un altro admin.`),
1417
1478
  note("info", `Accesso`, `Apri /api/accessi/console e accedi con un utente con FLGSUPER o FLGADMIN. Le sezioni sono raggiungibili anche tramite deep link (per esempio /api/accessi/console/menu-e-gruppi/menu).`),
1418
- note("info", `Bootstrap admin`, `Con adminBootstrap.enabled il backend stampa a ogni avvio un token casuale (monouso, valido fino al riavvio). Nella schermata di login della console trovi "Crea utente admin": incolla il token e crea il primo admin senza email di conferma. La password \xE8 opzionale: se assente viene generata e mostrata una sola volta.`)
1479
+ note("info", `Bootstrap admin`, `Con adminBootstrap.enabled il backend stampa a ogni avvio un token casuale (monouso, valido fino al riavvio). Nella schermata di login della console trovi "Crea utente admin": incolla il token e crea il primo admin senza email di conferma. La password \xE8 opzionale: se assente viene generata e mostrata una sola volta.`),
1480
+ note("ok", `Auto-ingresso dal frontend`, `Se un frontend ha gi\xE0 una sessione Accessi valida e l'utente \xE8 abilitato alla console, pu\xF2 aprire la console senza rifare il login: \`POST /api/accessi/console/entry\` con \`Authorization: Bearer <jwt>\` restituisce un ticket monouso; apri poi \`/api/accessi/console#entry=<ticket>\`. Il JWT non finisce mai nell'URL; il ticket dura 60 secondi ed \xE8 utilizzabile una sola volta (endpoint di scambio protetto da rate limit).`)
1419
1481
  ]
1420
1482
  },
1421
1483
  // -------------------------------------------------------------------------
@@ -1464,7 +1526,7 @@ POST /api/accessi/auth/confirm-reset-password/{token}
1464
1526
  [
1465
1527
  ["Auth", "POST auth/login, auth/get-user-by-token, auth/confirm-reset-password/:token, auth/two-factor/verify, auth/two-factor/resend"],
1466
1528
  ["Email", "POST email/send-reset-password-email, GET email/reset-password-page/:token"],
1467
- ["Utenti", "GET user/get-users, POST user/create-managed-user, PUT user/update-user/:codiceUtente, DELETE user/delete-user/:codiceUtente, POST user/set-stato, PATCH user/set-gdpr/:codiceUtente, POST user/register"],
1529
+ ["Utenti", "GET user/get-users, POST user/create-managed-user, PUT user/update-user/:codiceUtente, DELETE user/delete-user/:codiceUtente, DELETE user/delete-user-permanent/:codiceUtente, POST user/set-password/:codiceUtente, POST user/set-stato, PATCH user/set-gdpr/:codiceUtente, POST user/register"],
1468
1530
  ["Permessi", "GET permission/roles, POST permission/create-role, PUT permission/update-role/:codiceRuolo, DELETE permission/delete-role/:codiceRuolo, POST permission/assign-roles/:codiceUtente, POST permission/assign-permissions/:codiceUtente, GET permission/grants/:codiceUtente, GET permission/menus, GET permission/menu-types, GET permission/groups-with-menus"],
1469
1531
  ["Catalogo", "POST/PUT/DELETE permission/menus[/:codiceMenu], permission/menu-groups[/:codiceGruppo], permission/menu-types[/:codiceTipo]"],
1470
1532
  ["Configurator", "PATCH configurator/menus/:codiceMenu/enabled, PATCH configurator/groups/:codiceGruppo/enabled"],
@@ -1556,7 +1618,7 @@ function buildAiDigest() {
1556
1618
  }
1557
1619
 
1558
1620
  // src/accessi-module/Console/changelog.md
1559
- var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\n\n## 2.0.0-dev.33\n\n### Cambiamenti\n- Rimosse le logiche applicative residue dallo schema Accessi:\n - `MENU_GRP.NOMECAMPO`\n - `UTENTI.ENABLEIA`\n - `UTENTI_CONFIG.PAGDEF`\n - `UTENTI.CELLUTE`\n- Telefono canonico: `UTENTI_CONFIG.CELLULARE` (allargato a `VARCHAR(30)`).\n- Migrazione automatica: copia `UTENTI.CELLUTE` \u2192 `UTENTI_CONFIG.CELLULARE`, poi `DROP` delle colonne obsolete.\n- Ruoli chiariti:\n - `FLGSUPER`: superutente, riceve **tutte** le abilitazioni al livello massimo (30) dopo il login; in console vede **solo** la sezione **Utenti**.\n - `FLGADMIN`: accede a **tutta** la console (catalogo, token, SSO, utenti) ma **non** influisce sulle abilitazioni.\n- Aggiunta la sezione **Changelog** in console (sotto la Wiki).\n\n### Note\n- `FILTRI` e `FILTRI_TIPO` restano invariati (piano di migrazione futuro).\n- I backend che usavano i campi rimossi devono gestirli in tabelle proprie.\n\n## 2.0.0-dev.32\n\n### Cambiamenti\n- Rimosso il supporto ai flag applicativi in `UTENTI_CONFIG`: `FLGMOP`, `FLGPIANA`, `FLGADDETTI`, `FLGOSPITI`, `FLGPIANARFID`, `FLGCONTA`, `FLGCUBI`, `FLGCICLPASS`, `FLGDIPENDENTI`, `FLGINVENTARI`, `CAUMOV`, `NUMMAC`, `RAGSOCCLI`.\n- Rinominata la colonna `FLGADMINCONFIG` in `FLGADMIN` (con copia dei valori e `DROP`).\n\n## 2.0.0-dev.31\n\n### Aggiunte\n- Esposizione dei servizi Accessi all'host Express (`getAccessiModuleServices`) per delegare login, SSO e token senza reimplementarli.\n\n### Correzioni\n- `UserService.getUsers`: filtri estesi (`codiceUtente[]`, `numRep[]`, `codDipendente`, `cellulare`, `tipFil`, `flagSuper`).\n\n## 2.0.0-dev.24\n\n### Cambiamenti (breaking)\n- Chiavi JWT derivate per scopo (`access`, `reset`, `2fa`) e claim `typ` obbligatorio: gli access token precedenti non sono pi\xF9 validi.\n- Endpoint Allegati protetti di default.\n- Pool di connessioni attivo di default (`poolSize` 5).\n\n### Aggiunte\n- Service token macchina-a-macchina (gestione, guard, console).\n- CRUD completo dei cataloghi dalla console Accessi.\n- Supporto Firebird 2.5 nel migrator e nei test di integrazione.\n";
1621
+ var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\n\n## 2.0.0-dev.43\n\n### Cambiamenti\n- Gerarchia dei ruoli formalizzata **ADMIN > SUPER > utente**: un superutente non puo' gestire un admin su `update-user`, `delete-user` (soft e hard), `set-password`, `force-password-reset`, `set-stato`, `assign-roles` e `assign-permissions` (`ensureCanManageTargetUser`, 403 se violato).\n- Guard **ultimo admin attivo**: non puo' essere disattivato, bloccato, eliminato o privato del flag Admin (400), per evitare il lockout della console.\n- Creazione utente (`create-managed-user` e registrazione pubblica) resa **atomica** rispetto all'invio email: fail-fast se SMTP non configurato e rollback dell'utente creato se l'invio fallisce (`ACCESSI_USER_INVITE_EMAIL_FAILED`).\n- Console: form password separato (Invio imposta la password invece di salvare il profilo), scheda utente in sola lettura per target di rango superiore.\n- SSO: anche il superutente puo' creare utenti SSO e gestirne identita/policy (`federated-auth/users/*`) sugli utenti non-admin; il catalogo dei provider SSO resta riservato agli admin.\n- Coerenza: i filtri utente (`saveFiltriUtente`) rispettano la gerarchia; il flag GDPR impostato dal profilo allinea anche `DATGDPR`; la scheda in sola lettura disabilita anche il collegamento SSO.\n- Eliminazione definitiva: se l'utente ha dati collegati (tabelle esterne/FK) risponde 400 con `code: ACCESSI_USER_DELETE_HAS_REFERENCES` e messaggio esplicito, suggerendo il soft delete (la hard delete resta invariata quando non ci sono vincoli).\n- Consenso GDPR completo: `update-user` con `flagGdpr` scrive anche la riga storica `UTENTI_GDPR`; in console il pulsante \"Registra consenso GDPR\" usa `set-gdpr`.\n- Console: aggiunto il pulsante \"Invia reset password legacy\" (super e admin).\n- Console: **auto-ingresso dal frontend** tramite ticket monouso (`POST /api/accessi/console/entry` + apertura di `#entry=<ticket>`), senza JWT nell'URL; il ticket e' legato all'utente, dura 60s, e' monouso e lo scambio e' protetto da rate limit (`publicAuthRateLimit.consoleEntry`).\n\n## 2.0.0-dev.42\n\n### Aggiunte\n- Gestione dei flag di privilegio dalla console Accessi:\n - un admin pu\xF2 assegnare o togliere sia `FLGADMIN` sia `FLGSUPER`;\n - un superutente senza `FLGADMIN` pu\xF2 gestire solo `FLGSUPER`;\n - i flag sono impostabili in creazione (utente locale o SSO) e in modifica profilo.\n- Eliminazione utenti con due modalit\xE0:\n - soft delete (`delete-user`, `STAREG=DELETE`) per admin e superutenti;\n - eliminazione definitiva (`delete-user-permanent`, solo admin) che rimuove in transazione utente e record collegati.\n- Gestione completa dell'utente dalla console (admin): anagrafica estesa (cellulare, lingua, avatar, metadata JSON), GDPR, scadenza password, tutti i filtri (`FILTRI`) e **impostazione diretta della password** (`POST /api/accessi/user/set-password/:codiceUtente`), senza accesso al database.\n- Scritture utente idempotenti: `UTENTI_CONFIG` in `UPDATE OR INSERT ... MATCHING (CODUTE)`; i campi non inviati restano invariati.\n- Policy password moderna quando si imposta una password (reset, set-password admin, bootstrap): minimo 8 caratteri con maiuscola, minuscola, cifra e carattere speciale, senza spazi e non comune. In caso di violazione risposta 400 con `code: ACCESSI_WEAK_PASSWORD` e `details`. La policy **non** si applica al login, cos\xEC le password preesistenti deboli continuano a funzionare.\n\n### Correzioni\n- Il backend verifica i flag in base al ruolo dell'utente connesso (`ensurePrivilegeFlagChanges`): inviare un flag non consentito restituisce 403.\n\n## 2.0.0-dev.33\n\n### Cambiamenti\n- Rimosse le logiche applicative residue dallo schema Accessi:\n - `MENU_GRP.NOMECAMPO`\n - `UTENTI.ENABLEIA`\n - `UTENTI_CONFIG.PAGDEF`\n - `UTENTI.CELLUTE`\n- Telefono canonico: `UTENTI_CONFIG.CELLULARE` (allargato a `VARCHAR(30)`).\n- Migrazione automatica: copia `UTENTI.CELLUTE` \u2192 `UTENTI_CONFIG.CELLULARE`, poi `DROP` delle colonne obsolete.\n- Ruoli chiariti:\n - `FLGSUPER`: superutente, riceve **tutte** le abilitazioni al livello massimo (30) dopo il login; in console vede **solo** la sezione **Utenti**.\n - `FLGADMIN`: accede a **tutta** la console (catalogo, token, SSO, utenti) ma **non** influisce sulle abilitazioni.\n- Aggiunta la sezione **Changelog** in console (sotto la Wiki).\n\n### Note\n- `FILTRI` e `FILTRI_TIPO` restano invariati (piano di migrazione futuro).\n- I backend che usavano i campi rimossi devono gestirli in tabelle proprie.\n\n## 2.0.0-dev.32\n\n### Cambiamenti\n- Rimosso il supporto ai flag applicativi in `UTENTI_CONFIG`: `FLGMOP`, `FLGPIANA`, `FLGADDETTI`, `FLGOSPITI`, `FLGPIANARFID`, `FLGCONTA`, `FLGCUBI`, `FLGCICLPASS`, `FLGDIPENDENTI`, `FLGINVENTARI`, `CAUMOV`, `NUMMAC`, `RAGSOCCLI`.\n- Rinominata la colonna `FLGADMINCONFIG` in `FLGADMIN` (con copia dei valori e `DROP`).\n\n## 2.0.0-dev.31\n\n### Aggiunte\n- Esposizione dei servizi Accessi all'host Express (`getAccessiModuleServices`) per delegare login, SSO e token senza reimplementarli.\n\n### Correzioni\n- `UserService.getUsers`: filtri estesi (`codiceUtente[]`, `numRep[]`, `codDipendente`, `cellulare`, `tipFil`, `flagSuper`).\n\n## 2.0.0-dev.24\n\n### Cambiamenti (breaking)\n- Chiavi JWT derivate per scopo (`access`, `reset`, `2fa`) e claim `typ` obbligatorio: gli access token precedenti non sono pi\xF9 validi.\n- Endpoint Allegati protetti di default.\n- Pool di connessioni attivo di default (`poolSize` 5).\n\n### Aggiunte\n- Service token macchina-a-macchina (gestione, guard, console).\n- CRUD completo dei cataloghi dalla console Accessi.\n- Supporto Firebird 2.5 nel migrator e nei test di integrazione.\n";
1560
1622
 
1561
1623
  // src/accessi-module/Console/help.ts
1562
1624
  var p2 = (text) => ({ kind: "p", text });
@@ -1714,6 +1776,113 @@ var HELP = {
1714
1776
  p2(`E una stringa libera (massimo 50 caratteri): la libreria la conserva e la restituisce nel profilo; l'instradamento effettivo lo decide il frontend.`)
1715
1777
  ]
1716
1778
  },
1779
+ "user-cellulare": {
1780
+ title: "Cellulare",
1781
+ blocks: [
1782
+ p2(`Numero di cellulare dell'utente (UTENTI_CONFIG.CELLULARE). \xC8 il recapito canonico per SMS o notifiche applicative; Accessi non lo invia da solo.`)
1783
+ ]
1784
+ },
1785
+ "user-lingua": {
1786
+ title: "Codice lingua",
1787
+ blocks: [
1788
+ p2(`Lingua preferita dell'utente (UTENTI_CONFIG.CODLINGUA). Stringa breve come it, en, sl; viene restituita nel profilo e il frontend decide come usarla.`)
1789
+ ]
1790
+ },
1791
+ "user-avatar": {
1792
+ title: "Avatar",
1793
+ blocks: [
1794
+ p2(`Riferimento all'avatar (UTENTI_CONFIG.AVATAR, massimo 30 caratteri): tipicamente un nome file o una chiave, non un base64.`)
1795
+ ]
1796
+ },
1797
+ "user-scadenza-password": {
1798
+ title: "Scadenza password",
1799
+ blocks: [
1800
+ p2(`Data di scadenza della password (UTENTI.DATSCAPWD). Lascia vuoto per non forzare una scadenza; usa una data passata per richiedere il cambio al prossimo login, se la policy \xE8 attiva.`)
1801
+ ]
1802
+ },
1803
+ "user-gdpr": {
1804
+ title: "Consenso GDPR",
1805
+ blocks: [
1806
+ p2(`Lo stato mostra se il consenso \xE8 registrato. Il pulsante "Registra consenso GDPR" (endpoint set-gdpr) imposta FLGGDPR e DATGDPR e scrive la riga storica in UTENTI_GDPR.`),
1807
+ note2("info", "Completo e tracciato", `A differenza del flag impostato a mano, il flusso dedicato registra anche la data e lo storico dell'accettazione.`)
1808
+ ]
1809
+ },
1810
+ "user-json": {
1811
+ title: "Metadata JSON",
1812
+ blocks: [
1813
+ p2(`Campo libero JSON (UTENTI_CONFIG.JSON_METADATA) per dati applicativi del backend. Deve essere JSON valido se il backend lo interpreta.`)
1814
+ ]
1815
+ },
1816
+ "user-set-password": {
1817
+ title: "Imposta password",
1818
+ blocks: [
1819
+ p2(`Imposta direttamente una nuova password locale: viene hashata dal backend e la scadenza segue la policy. Nessuna email viene inviata.`),
1820
+ p2(`Requisiti di sicurezza:`),
1821
+ list2([
1822
+ `Almeno 8 caratteri (massimo 100).`,
1823
+ `Almeno una lettera maiuscola e una minuscola.`,
1824
+ `Almeno una cifra e un carattere speciale (es. ! ? @ # $ %).`,
1825
+ `Nessuno spazio e non deve essere una password comune.`
1826
+ ]),
1827
+ note2("danger", "Password non conforme", `Se non rispetta i requisiti l API risponde 400 con codice ACCESSI_WEAK_PASSWORD e l elenco dei requisiti mancanti; la password non viene salvata.`),
1828
+ note2("warn", "Sostituisce la password attuale", `L'utente dovr\xE0 usare subito la nuova password. Per fargliela scegliere, usa "Invia reset password".`)
1829
+ ]
1830
+ },
1831
+ "user-filter-numrep": {
1832
+ title: "Numero reparto (NUMREP)",
1833
+ blocks: [p2(`Reparto associato all'utente nella tabella FILTRI. Vuoto rimuove il valore.`)]
1834
+ },
1835
+ "user-filter-idxpers": {
1836
+ title: "Indice personale (IDXPERS)",
1837
+ blocks: [p2(`Indice del personale nella tabella FILTRI. Vuoto rimuove il valore.`)]
1838
+ },
1839
+ "user-filter-codclisuper": {
1840
+ title: "Cliente padre (CODCLISUPER)",
1841
+ blocks: [p2(`Codice del cliente padre. Vuoto rimuove il valore.`)]
1842
+ },
1843
+ "user-filter-codage": {
1844
+ title: "Agente (CODAGE)",
1845
+ blocks: [p2(`Codice dell'agente associato. Vuoto rimuove il valore.`)]
1846
+ },
1847
+ "user-filter-codclicol": {
1848
+ title: "Cliente collegato (CODCLICOL)",
1849
+ blocks: [p2(`Codice del cliente collegato. Vuoto rimuove il valore.`)]
1850
+ },
1851
+ "user-filter-codclienti": {
1852
+ title: "Clienti (CODCLIENTI)",
1853
+ blocks: [p2(`Elenco di codici cliente separati da virgola (testo libero). Vuoto rimuove il valore.`)]
1854
+ },
1855
+ "user-filter-tipfil": {
1856
+ title: "Tipo filtro (TIPFIL)",
1857
+ blocks: [p2(`Tipo di filtro applicato. Vuoto rimuove il valore.`)]
1858
+ },
1859
+ "user-filter-idxpos": {
1860
+ title: "Postazione (IDXPOS)",
1861
+ blocks: [p2(`Indice della postazione nella tabella ANTENNE_POS. Vuoto rimuove il valore.`)]
1862
+ },
1863
+ "user-filter-coddip": {
1864
+ title: "Dipendente (CODDIP)",
1865
+ blocks: [p2(`Codice del dipendente collegato. Vuoto rimuove il valore.`)]
1866
+ },
1867
+ "user-filter-codvet": {
1868
+ title: "Vettore (CODVET)",
1869
+ blocks: [p2(`Codice del vettore collegato. Vuoto rimuove il valore.`)]
1870
+ },
1871
+ "user-super": {
1872
+ title: "Superutente (FLGSUPER)",
1873
+ blocks: [
1874
+ p2(`Assegna il flag superutente. Un superutente riceve TUTTE le abilitazioni al livello massimo (30) dopo il login e in console vede solo la sezione Utenti, dove gestisce utenti, ruoli e grant.`),
1875
+ note2("warn", "Non \xE8 un ruolo", `Il flag non sostituisce i ruoli o i grant: li amplia assegnando ogni menu attivo al livello massimo. Usalo solo per gli amministratori di Accessi.`),
1876
+ note2("info", "Chi lo modifica", `Un admin pu\xF2 assegnare o togliere sia Admin sia Superutente; un superutente senza Admin pu\xF2 gestire solo il flag Superutente. La verifica \xE8 ripetuta dal backend.`)
1877
+ ]
1878
+ },
1879
+ "user-admin": {
1880
+ title: "Admin (FLGADMIN)",
1881
+ blocks: [
1882
+ p2(`Assegna il flag admin. Un admin accede a tutta la console (catalogo, token di servizio, SSO e utenti) ma NON conferisce abilitazioni: i menu restano quelli assegnati via ABILITAZIONI e RUOLI_MNU.`),
1883
+ note2("info", "Pu\xF2 gestire i flag", `Un admin pu\xF2 assegnare o togliere sia il flag Admin sia il flag Superutente. Un superutente senza Admin non pu\xF2 modificare il flag Admin.`)
1884
+ ]
1885
+ },
1717
1886
  "user-state": {
1718
1887
  title: "Stato di registrazione",
1719
1888
  blocks: [
@@ -1892,7 +2061,16 @@ var HELP = {
1892
2061
  title: "Imposta stato eliminato",
1893
2062
  blocks: [
1894
2063
  p2(`Esegue un soft delete: imposta lo stato di registrazione a "Eliminato" (50). L'utente non pu\xF2 pi\xF9 accedere, ma i dati restano nel database.`),
1895
- note2("info", "Reversibile", `Non \xE8 una cancellazione fisica: puoi ripristinare l'utente cambiando lo stato di registrazione.`)
2064
+ note2("info", "Reversibile", `Non \xE8 una cancellazione fisica: puoi ripristinare l'utente cambiando lo stato di registrazione.`),
2065
+ note2("info", "Chi pu\xF2 farlo", `Sia admin sia superutente possono eseguire il soft delete.`)
2066
+ ]
2067
+ },
2068
+ "user-delete-permanent": {
2069
+ title: "Elimina definitivamente",
2070
+ blocks: [
2071
+ p2(`Cancella l'utente e tutti i record collegati (configurazione, password, ruoli, abilitazioni, filtri, identit\xE0 SSO, challenge 2FA e GDPR). Operazione irreversibile.`),
2072
+ note2("danger", "Non reversibile", `I dati vengono rimossi fisicamente dal database; non \xE8 possibile ripristinare l'utente.`),
2073
+ note2("info", "Solo admin", `Il pulsante \xE8 mostrato e accettato solo per il flag Admin. Un superutente senza Admin pu\xF2 solo flaggare l'utente come eliminato.`)
1896
2074
  ]
1897
2075
  },
1898
2076
  // ----------------------------------------------------- selezioni di menu
@@ -2400,19 +2578,48 @@ async function bootstrap() {
2400
2578
  byId("login-error").textContent = error instanceof Error ? error.message : "Sessione non valida.";
2401
2579
  }
2402
2580
  }
2581
+ async function exchangeConsoleEntry(ticket) {
2582
+ try {
2583
+ const response = await fetch(`${consoleBasePath}/entry/exchange`, {
2584
+ method: "POST",
2585
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2586
+ body: JSON.stringify({ ticket })
2587
+ });
2588
+ const payload = await response.json().catch(() => void 0);
2589
+ const value = payload?.Result?.token?.value;
2590
+ if (!response.ok || typeof value !== "string" || value === "") {
2591
+ throw new Error(payload?.message ?? "Apertura automatica della console non riuscita.");
2592
+ }
2593
+ token = value;
2594
+ sessionStorage.setItem("accessi-console-token", value);
2595
+ setAccessiConsoleToken(value);
2596
+ await bootstrap();
2597
+ } catch (error) {
2598
+ token = null;
2599
+ setAccessiConsoleToken(null);
2600
+ sessionStorage.removeItem("accessi-console-token");
2601
+ finishBoot("login");
2602
+ byId("login-error").textContent = error instanceof Error ? error.message : "Apertura automatica della console non riuscita.";
2603
+ }
2604
+ }
2403
2605
  async function showUsers() {
2404
2606
  const { users, total } = await loadUsersPage();
2405
2607
  usersTotal = total;
2406
2608
  const totalPages = Math.max(1, Math.ceil(total / USERS_PAGE_SIZE));
2407
2609
  if (usersPage >= totalPages) usersPage = totalPages - 1;
2408
- render(`<div class="toolbar"><button id="new-local">Nuovo utente locale</button>${federatedAuthenticationAvailable ? '<button id="new-sso">Nuovo utente SSO</button>' : ""}<button id="reload-users" class="secondary">Aggiorna</button></div>
2610
+ render(`<div class="toolbar"><button id="new-local">Nuovo utente locale</button>${federatedAuthenticationAvailable ? '<button id="new-sso">Nuovo utente SSO</button>' : ""}<button id="reload-users" class="secondary">Aggiorna</button><button id="reset-legacy" class="secondary">Invia reset password legacy</button></div>
2409
2611
  <div class="pager-row pager-row-top">${pagerMarkup(totalPages)}</div>
2410
2612
  <table><thead><tr><th>Utente</th><th>Stato</th><th>Accesso</th><th>2FA</th><th>Ruoli</th><th></th></tr></thead><tbody>
2411
- ${users.map(({ utente, userGrants }) => `<tr><td>${escapeHtml(utente.email)}<br><span class="muted">${escapeHtml(utente.nome)} ${escapeHtml(utente.cognome)}</span></td><td>${escapeHtml(utente.statoRegistrazione)}</td><td>${utente.passwordlessLoginEnabled ? "codice email" : utente.passwordLoginEnabled === false ? "solo SSO" : "password"}</td><td>${utente.flagDueFattori ? "Attiva" : "Disattiva"}</td><td class="roles-col">${rolesCell(utente.codiceUtente, userGrants)}</td><td><button data-user="${utente.codiceUtente}">Gestisci</button></td></tr>`).join("") || '<tr><td colspan="6" class="muted">Nessun utente.</td></tr>'}
2613
+ ${users.map(({ utente, userGrants }) => `<tr><td>${escapeHtml(utente.email)}<br><span class="muted">${escapeHtml(utente.nome)} ${escapeHtml(utente.cognome)}</span></td><td>${escapeHtml(utente.statoRegistrazione)}</td><td>${utente.passwordlessLoginEnabled ? "codice email" : utente.passwordLoginEnabled === false ? "solo SSO" : "password"}</td><td>${utente.flagDueFattori ? "Attiva" : "Disattiva"}</td><td class="roles-col">${rolesCell(utente.codiceUtente, userGrants)}</td><td><button data-user="${utente.codiceUtente}"${canManageUser(utente) ? "" : ' disabled title="Non puoi gestire un utente di rango superiore" aria-disabled="true"'}>Gestisci</button></td></tr>`).join("") || '<tr><td colspan="6" class="muted">Nessun utente.</td></tr>'}
2412
2614
  </tbody></table><div class="pager-row pager-row-bottom">${pagerMarkup(totalPages)}</div>`);
2413
2615
  byId("new-local").onclick = () => showLocalUserForm();
2414
2616
  if (federatedAuthenticationAvailable) byId("new-sso").onclick = () => handleAction(showSsoUserForm);
2415
2617
  byId("reload-users").onclick = () => handleAction(() => show("users"));
2618
+ byId("reset-legacy").onclick = () => handleAction(async () => {
2619
+ if (!window.confirm("Inviare l email di reset a tutti gli utenti con password non ancora migrata? Richiede un servizio email configurato.")) return;
2620
+ await forcePasswordResetLegacy();
2621
+ showNotice("Email di reset inviate agli utenti con password legacy.");
2622
+ });
2416
2623
  document.querySelectorAll("[data-goto]").forEach((button) => {
2417
2624
  button.onclick = () => handleAction(async () => {
2418
2625
  const target = button.dataset.goto;
@@ -2435,15 +2642,160 @@ function authenticationPolicyFields(user) {
2435
2642
  <p class="form-help">Accesso senza password (richiede il codice email).</p>
2436
2643
  </fieldset>`;
2437
2644
  }
2645
+ function canManageSuperFlag() {
2646
+ return Boolean(currentUser?.flagSuper || currentUser?.flagAdmin);
2647
+ }
2648
+ function canManageAdminFlag() {
2649
+ return Boolean(currentUser?.flagAdmin);
2650
+ }
2651
+ function consoleUserRank(user) {
2652
+ if (user?.flagAdmin) return 2;
2653
+ if (user?.flagSuper) return 1;
2654
+ return 0;
2655
+ }
2656
+ function canManageUser(user) {
2657
+ if (currentUser && user.codiceUtente === currentUser.codiceUtente) return true;
2658
+ return consoleUserRank(currentUser) >= consoleUserRank(user);
2659
+ }
2660
+ function setUserManagementEnabled(enabled) {
2661
+ for (const id of ["disable-user", "purge-user", "save-state", "set-password", "save-profile", "set-gdpr"]) {
2662
+ const element = document.getElementById(id);
2663
+ if (element) element.disabled = !enabled;
2664
+ }
2665
+ for (const selector of ["#roles button", "#grants button", "#link-sso button"]) {
2666
+ document.querySelectorAll(selector).forEach((button) => {
2667
+ button.disabled = !enabled;
2668
+ });
2669
+ }
2670
+ for (const selector of ["#profile", "#password-form", "#roles", "#grants", "#link-sso"]) {
2671
+ document.querySelectorAll(
2672
+ `${selector} input, ${selector} select, ${selector} textarea`
2673
+ ).forEach((field) => {
2674
+ field.disabled = !enabled;
2675
+ });
2676
+ }
2677
+ document.querySelectorAll("[data-identity-toggle], [data-identity-delete]").forEach((button) => {
2678
+ button.disabled = !enabled;
2679
+ });
2680
+ }
2681
+ function privilegeFields(user) {
2682
+ const manageSuper = canManageSuperFlag();
2683
+ const manageAdmin = canManageAdminFlag();
2684
+ if (!manageSuper && !manageAdmin) return "";
2685
+ return `<fieldset><legend>Privilegi Accessi (opzionale)</legend>
2686
+ ${manageAdmin ? `<label class="check"><input name="flagAdmin" type="checkbox" ${user.flagAdmin ? "checked" : ""}> Admin ${helpDot("user-admin")}</label><p class="form-help">Accede a tutta la console (catalogo, token, SSO, utenti) e puo gestire i flag. Non modifica le abilitazioni.</p>` : ""}
2687
+ ${manageSuper ? `<label class="check"><input name="flagSuper" type="checkbox" ${user.flagSuper ? "checked" : ""}> Superutente ${helpDot("user-super")}</label><p class="form-help">Riceve tutte le abilitazioni al livello massimo e gestisce utenti, ruoli e grant.</p>` : ""}
2688
+ </fieldset>`;
2689
+ }
2690
+ function privilegePayload(form) {
2691
+ const fields = new FormData(form);
2692
+ const payload = {};
2693
+ if (canManageSuperFlag()) payload.flagSuper = fields.has("flagSuper");
2694
+ if (canManageAdminFlag()) payload.flagAdmin = fields.has("flagAdmin");
2695
+ return payload;
2696
+ }
2697
+ function optionalText(values, key) {
2698
+ if (!(key in values)) return void 0;
2699
+ const raw = (values[key] ?? "").trim();
2700
+ return raw === "" ? null : raw;
2701
+ }
2702
+ function optionalNumber(values, key) {
2703
+ if (!(key in values)) return void 0;
2704
+ const raw = (values[key] ?? "").trim();
2705
+ if (raw === "") return null;
2706
+ const parsed = Number(raw);
2707
+ return Number.isFinite(parsed) ? parsed : null;
2708
+ }
2709
+ function passwordPolicyViolations(password) {
2710
+ const violations = [];
2711
+ if (password.length < 8) violations.push("almeno 8 caratteri");
2712
+ if (password.length > 100) violations.push("al massimo 100 caratteri");
2713
+ if (/\s/.test(password)) violations.push("nessuno spazio");
2714
+ if (!/[A-Z]/.test(password)) violations.push("una lettera maiuscola");
2715
+ if (!/[a-z]/.test(password)) violations.push("una lettera minuscola");
2716
+ if (!/\d/.test(password)) violations.push("una cifra");
2717
+ if (!/[^A-Za-z0-9]/.test(password)) violations.push("un carattere speciale");
2718
+ return violations;
2719
+ }
2720
+ function dateInputValue(value) {
2721
+ const text = typeof value === "string" ? value : "";
2722
+ const match = /^(\d{4}-\d{2}-\d{2})/.exec(text);
2723
+ return match?.[1] ?? "";
2724
+ }
2725
+ function extendedProfileFields(user) {
2726
+ return `<fieldset><legend>Anagrafica estesa</legend>
2727
+ <label>Cellulare ${helpDot("user-cellulare")}<input name="cellulare" maxlength="30" value="${escapeHtml(user.cellulare ?? "")}"></label>
2728
+ <label>Codice lingua ${helpDot("user-lingua")}<input name="codiceLingua" maxlength="2" value="${escapeHtml(user.codiceLingua ?? "")}"></label>
2729
+ <label>Avatar ${helpDot("user-avatar")}<input name="avatar" maxlength="30" value="${escapeHtml(user.avatar ?? "")}"></label>
2730
+ <label>Scadenza password ${helpDot("user-scadenza-password")}<input name="dataScadenzaPassword" type="date" value="${dateInputValue(user.dataScadenzaPassword)}"></label>
2731
+ <fieldset class="gdpr-status"><legend>GDPR ${helpDot("user-gdpr")}</legend><p class="form-help">${user.flagGdpr ? `Consenso registrato${dateInputValue(user.dataGdpr) ? ` il ${escapeHtml(dateInputValue(user.dataGdpr))}` : ""}.` : "Consenso non ancora registrato."}</p><button type="button" id="set-gdpr" class="secondary">Registra consenso GDPR</button></fieldset>
2732
+ <label>Metadata JSON ${helpDot("user-json")}<textarea name="jsonMetadata" rows="3">${escapeHtml(user.jsonMetadata ?? "")}</textarea></label>
2733
+ </fieldset>`;
2734
+ }
2735
+ function filterFields(user) {
2736
+ return `<fieldset><legend>Filtri utente (FILTRI)</legend>
2737
+ <label>Numero reparto ${helpDot("user-filter-numrep")}<input name="numRep" type="number" value="${escapeHtml(user.numRep ?? "")}"></label>
2738
+ <label>Indice personale ${helpDot("user-filter-idxpers")}<input name="idxPers" type="number" value="${escapeHtml(user.idxPers ?? "")}"></label>
2739
+ <label>Cliente padre ${helpDot("user-filter-codclisuper")}<input name="codCliSuper" type="number" value="${escapeHtml(user.codCliSuper ?? "")}"></label>
2740
+ <label>Agente ${helpDot("user-filter-codage")}<input name="codAge" type="number" value="${escapeHtml(user.codAge ?? "")}"></label>
2741
+ <label>Cliente collegato ${helpDot("user-filter-codclicol")}<input name="codCliCol" type="number" value="${escapeHtml(user.codCliCol ?? "")}"></label>
2742
+ <label>Clienti (separati da virgola) ${helpDot("user-filter-codclienti")}<input name="codClienti" value="${escapeHtml(user.codClienti ?? "")}"></label>
2743
+ <label>Tipo filtro ${helpDot("user-filter-tipfil")}<input name="tipFil" type="number" value="${escapeHtml(user.tipFil ?? "")}"></label>
2744
+ <label>Postazione ${helpDot("user-filter-idxpos")}<input name="idxPos" type="number" value="${escapeHtml(user.idxPos ?? "")}"></label>
2745
+ <label>Dipendente ${helpDot("user-filter-coddip")}<input name="codDip" type="number" value="${escapeHtml(user.codDip ?? "")}"></label>
2746
+ <label>Vettore ${helpDot("user-filter-codvet")}<input name="codVet" type="number" value="${escapeHtml(user.codVet ?? "")}"></label>
2747
+ </fieldset>`;
2748
+ }
2749
+ function profilePayload(form, codiceUtente) {
2750
+ const values = formValues(form);
2751
+ const fields = new FormData(form);
2752
+ const payload = {
2753
+ codiceUtente,
2754
+ email: values.email ?? "",
2755
+ nome: optionalText(values, "nome"),
2756
+ cognome: optionalText(values, "cognome"),
2757
+ cellulare: optionalText(values, "cellulare"),
2758
+ codiceLingua: optionalText(values, "codiceLingua"),
2759
+ avatar: optionalText(values, "avatar"),
2760
+ paginaDefault: optionalText(values, "paginaDefault"),
2761
+ jsonMetadata: optionalText(values, "jsonMetadata"),
2762
+ dataScadenzaPassword: optionalText(values, "dataScadenzaPassword"),
2763
+ flagDueFattori: fields.has("flagDueFattori"),
2764
+ passwordlessLoginEnabled: fields.has("passwordlessLoginEnabled"),
2765
+ numRep: optionalNumber(values, "numRep"),
2766
+ idxPers: optionalNumber(values, "idxPers"),
2767
+ codCliSuper: optionalNumber(values, "codCliSuper"),
2768
+ codAge: optionalNumber(values, "codAge"),
2769
+ codCliCol: optionalNumber(values, "codCliCol"),
2770
+ codClienti: optionalText(values, "codClienti"),
2771
+ tipFil: optionalNumber(values, "tipFil"),
2772
+ idxPos: optionalNumber(values, "idxPos"),
2773
+ codDip: optionalNumber(values, "codDip"),
2774
+ codVet: optionalNumber(values, "codVet"),
2775
+ ...privilegePayload(form)
2776
+ };
2777
+ if (federatedAuthenticationAvailable) {
2778
+ payload.passwordLoginEnabled = fields.has("passwordLoginEnabled");
2779
+ }
2780
+ return payload;
2781
+ }
2782
+ function passwordFields() {
2783
+ return `<form id="password-form" class="user-editor-section" data-user-panel="profile" role="tabpanel" aria-labelledby="user-tab-profile">
2784
+ <h3>Password</h3>
2785
+ <label>Nuova password ${helpDot("user-set-password")}<input name="newPassword" type="password" autocomplete="new-password" minlength="8" maxlength="100"><span class="form-help">Almeno 8 caratteri con maiuscola, minuscola, cifra e carattere speciale; nessuno spazio. Viene salvata come hash e la scadenza segue la policy; nessuna email inviata.</span></label>
2786
+ <button type="submit" id="set-password" class="secondary">Imposta password</button>
2787
+ </form>`;
2788
+ }
2438
2789
  function showLocalUserForm() {
2439
2790
  render(`<button id="back">Indietro</button><h2>Nuovo utente locale</h2><p class="muted">Viene inviata l'e-mail per impostare la password.</p>
2440
- <form id="local-user"><label>Email ${helpDot("user-email")}<input name="email" type="email" required><span class="form-help">Identificativo di accesso; ricever\xC3\xA0 l'email per la password.</span></label><label>Nome ${helpDot("user-name")}<input name="nome"></label><label>Cognome ${helpDot("user-name")}<input name="cognome"></label><button>Crea utente</button></form>`);
2791
+ <form id="local-user"><label>Email ${helpDot("user-email")}<input name="email" type="email" required><span class="form-help">Identificativo di accesso; ricever\xC3\xA0 l'email per la password.</span></label><label>Nome ${helpDot("user-name")}<input name="nome"></label><label>Cognome ${helpDot("user-name")}<input name="cognome"></label>${privilegeFields({})}<button>Crea utente</button></form>`);
2441
2792
  byId("back").onclick = () => show("users");
2442
2793
  const localUserForm = document.getElementById("local-user");
2443
2794
  if (localUserForm) localUserForm.onsubmit = (event) => {
2444
2795
  event.preventDefault();
2445
2796
  handleAction(async () => {
2446
- await createManagedUser(formValues(eventForm(event)));
2797
+ const form = eventForm(event);
2798
+ await createManagedUser({ ...formValues(form), ...privilegePayload(form) });
2447
2799
  showNotice("Utente creato; inviata la procedura di impostazione password.");
2448
2800
  await show("users");
2449
2801
  });
@@ -2455,7 +2807,7 @@ async function showSsoUserForm() {
2455
2807
  throw new Error("Prima di creare un utente SSO, censisci e abilita almeno un provider SSO.");
2456
2808
  }
2457
2809
  render(`<button id="back">Indietro</button><h2>Nuovo utente SSO</h2><p class="muted">Salva solo collegamenti gi\xC3\xA0 verificati dal backend.</p>
2458
- <form id="sso-user"><label>Email ${helpDot("user-email")}<input name="email" type="email" required><span class="form-help">Email di contatto e identificativo Accessi dell'utente.</span></label><label>Nome ${helpDot("user-name")}<input name="nome"></label><label>Cognome ${helpDot("user-name")}<input name="cognome"></label>${federatedIdentityFields(providers)}<label class="check"><input name="passwordLoginEnabled" type="checkbox"> Abilita anche la password locale ${helpDot("user-password-login")}</label><span class="form-help">Se disattivato, l'utente accede solo via SSO.</span><button>Crea utente SSO</button></form>`);
2810
+ <form id="sso-user"><label>Email ${helpDot("user-email")}<input name="email" type="email" required><span class="form-help">Email di contatto e identificativo Accessi dell'utente.</span></label><label>Nome ${helpDot("user-name")}<input name="nome"></label><label>Cognome ${helpDot("user-name")}<input name="cognome"></label>${federatedIdentityFields(providers)}<label class="check"><input name="passwordLoginEnabled" type="checkbox"> Abilita anche la password locale ${helpDot("user-password-login")}</label><span class="form-help">Se disattivato, l'utente accede solo via SSO.</span>${privilegeFields({})}<button>Crea utente SSO</button></form>`);
2459
2811
  byId("back").onclick = () => show("ssoUsers");
2460
2812
  const ssoUserForm = document.getElementById("sso-user");
2461
2813
  if (ssoUserForm) ssoUserForm.onsubmit = (event) => {
@@ -2468,11 +2820,11 @@ async function showSsoUserForm() {
2468
2820
  subject: values.subject ?? "",
2469
2821
  note: values.note || void 0,
2470
2822
  passwordLoginEnabled: new FormData(form).has("passwordLoginEnabled"),
2471
- user: { email: values.email ?? "", nome: values.nome || void 0, cognome: values.cognome || void 0 }
2823
+ user: { email: values.email ?? "", nome: values.nome || void 0, cognome: values.cognome || void 0, ...privilegePayload(form) }
2472
2824
  };
2473
2825
  await createFederatedUser(request);
2474
2826
  showNotice("Utente SSO creato e collegamento registrato.");
2475
- await show("ssoUsers");
2827
+ await show(currentUser?.flagAdmin ? "ssoUsers" : "users");
2476
2828
  });
2477
2829
  };
2478
2830
  }
@@ -2570,9 +2922,11 @@ async function showUser(rawCode) {
2570
2922
  const linked = federatedAuthenticationAvailable ? result(await getFederatedIdentities(codiceUtente)) : [];
2571
2923
  const identitySection = federatedAuthenticationAvailable ? federatedIdentitySection(linked, providerList) : "";
2572
2924
  const defaultUserTab = federatedAuthenticationAvailable ? "sso" : "profile";
2573
- render(`<div class="user-action-bar"><button id="back" type="button" class="secondary">Indietro</button><label class="inline-field">Stato registrazione ${helpDot("user-state")}<select id="user-state">${registrationStateOptions(user.statoRegistrazione)}</select></label><button id="save-state" type="button" class="secondary">Aggiorna stato</button><button id="disable-user" type="button" class="danger-button">Imposta stato eliminato</button>${helpDot("user-delete")}</div><div class="page-header"><div><p class="eyebrow">Utente ${codiceUtente}</p><h2>${escapeHtml(user.email)}</h2></div><span class="muted">Gestione profilo, ruoli e autorizzazioni</span></div>${userDetailTabs(federatedAuthenticationAvailable)}<div class="user-editor">
2925
+ const manageable = canManageUser(user);
2926
+ render(`${manageable ? "" : '<div class="notice notice-error" role="alert"><span class="notice-icon" aria-hidden="true">!</span><span class="notice-text">Stai visualizzando un utente di rango superiore: scheda in sola lettura.</span></div>'}<div class="user-action-bar"><button id="back" type="button" class="secondary">Indietro</button><label class="inline-field">Stato registrazione ${helpDot("user-state")}<select id="user-state">${registrationStateOptions(user.statoRegistrazione)}</select></label><button id="save-state" type="button" class="secondary">Aggiorna stato</button><button id="disable-user" type="button" class="danger-button">Imposta stato eliminato</button>${helpDot("user-delete")}${currentUser?.flagAdmin ? `<button id="purge-user" type="button" class="danger-button">Elimina definitivamente</button>${helpDot("user-delete-permanent")}` : ""}</div><div class="page-header"><div><p class="eyebrow">Utente ${codiceUtente}</p><h2>${escapeHtml(user.email)}</h2></div><span class="muted">Gestione profilo, ruoli e autorizzazioni</span></div>${userDetailTabs(federatedAuthenticationAvailable)}<div class="user-editor">
2574
2927
  ${identitySection}
2575
- <form id="profile" data-user-panel="profile" role="tabpanel" aria-labelledby="user-tab-profile"><h3>Profilo e accesso</h3><label>Nome ${helpDot("user-name")}<input name="nome" value="${escapeHtml(user.nome)}"></label><label>Cognome ${helpDot("user-name")}<input name="cognome" value="${escapeHtml(user.cognome)}"></label><label>Email ${helpDot("user-email")}<input name="email" type="email" value="${escapeHtml(user.email)}" required><span class="form-help">Identificativo di accesso e recapito.</span></label><label>Pagina di default ${helpDot("user-default-page")}<input name="paginaDefault" maxlength="50" value="${escapeHtml(user.paginaDefault)}"><span class="form-help">Pagina mostrata dopo il login (percorso o codice pagina).</span></label>${federatedAuthenticationAvailable ? `<label class="check"><input name="passwordLoginEnabled" type="checkbox" ${user.passwordLoginEnabled !== false ? "checked" : ""}> Login con password ${helpDot("user-password-login")}</label><span class="form-help">Se disabilitato, il login con email e password restituisce un errore esplicito; restano valide le identit\xC3\xA0 SSO attive.</span>` : ""}${authenticationPolicyFields(user)}<button>Salva</button></form>
2928
+ <form id="profile" data-user-panel="profile" role="tabpanel" aria-labelledby="user-tab-profile"><h3>Profilo e accesso</h3><label>Nome ${helpDot("user-name")}<input name="nome" value="${escapeHtml(user.nome)}"></label><label>Cognome ${helpDot("user-name")}<input name="cognome" value="${escapeHtml(user.cognome)}"></label><label>Email ${helpDot("user-email")}<input name="email" type="email" value="${escapeHtml(user.email)}" required><span class="form-help">Identificativo di accesso e recapito.</span></label><label>Pagina di default ${helpDot("user-default-page")}<input name="paginaDefault" maxlength="50" value="${escapeHtml(user.paginaDefault)}"><span class="form-help">Pagina mostrata dopo il login (percorso o codice pagina).</span></label>${federatedAuthenticationAvailable ? `<label class="check"><input name="passwordLoginEnabled" type="checkbox" ${user.passwordLoginEnabled !== false ? "checked" : ""}> Login con password ${helpDot("user-password-login")}</label><span class="form-help">Se disabilitato, il login con email e password restituisce un errore esplicito; restano valide le identit\xC3\xA0 SSO attive.</span>` : ""}${authenticationPolicyFields(user)}${extendedProfileFields(user)}${privilegeFields(user)}${filterFields(user)}<button id="save-profile" type="submit">Salva</button></form>
2929
+ ${passwordFields()}
2576
2930
  ${roleAssignmentEditor(allRoles, userGrants.ruoli, menuGroups)}
2577
2931
  ${directGrantEditor(menuGroups, userGrants.abilitazioni)}
2578
2932
  </div>`);
@@ -2602,25 +2956,16 @@ async function showUser(rawCode) {
2602
2956
  profileForm.onsubmit = (event) => {
2603
2957
  event.preventDefault();
2604
2958
  handleAction(async () => {
2605
- const values = formValues(eventForm(event));
2606
- const fields = new FormData(eventForm(event));
2607
- await updateUtente(codiceUtente, {
2608
- codiceUtente,
2609
- email: values.email ?? "",
2610
- nome: values.nome || void 0,
2611
- cognome: values.cognome || void 0,
2612
- paginaDefault: values.paginaDefault || void 0,
2613
- flagDueFattori: fields.has("flagDueFattori"),
2614
- passwordlessLoginEnabled: fields.has("passwordlessLoginEnabled"),
2615
- ...federatedAuthenticationAvailable ? { passwordLoginEnabled: fields.has("passwordLoginEnabled") } : {}
2616
- });
2617
- if (codiceUtente === currentUser?.codiceUtente && (fields.has("flagDueFattori") !== Boolean(user.flagDueFattori) || fields.has("passwordlessLoginEnabled") !== Boolean(user.passwordlessLoginEnabled))) {
2959
+ const form = eventForm(event);
2960
+ const fields = new FormData(form);
2961
+ await updateUtente(codiceUtente, profilePayload(form, codiceUtente));
2962
+ if (codiceUtente === currentUser?.codiceUtente && (fields.has("flagDueFattori") !== Boolean(user.flagDueFattori) || fields.has("passwordlessLoginEnabled") !== Boolean(user.passwordlessLoginEnabled) || canManageSuperFlag() && fields.has("flagSuper") !== Boolean(user.flagSuper) || canManageAdminFlag() && fields.has("flagAdmin") !== Boolean(user.flagAdmin))) {
2618
2963
  token = null;
2619
2964
  sessionStorage.removeItem("accessi-console-token");
2620
2965
  setAccessiConsoleToken(null);
2621
2966
  resetLogin();
2622
2967
  await bootstrap();
2623
- byId("login-error").textContent = "Policy aggiornata. Accedi di nuovo con la nuova configurazione.";
2968
+ byId("login-error").textContent = "Privilegi o policy aggiornati. Accedi di nuovo con la nuova configurazione.";
2624
2969
  return;
2625
2970
  }
2626
2971
  await showUser(String(codiceUtente));
@@ -2629,6 +2974,26 @@ async function showUser(rawCode) {
2629
2974
  });
2630
2975
  };
2631
2976
  }
2977
+ const passwordForm = document.getElementById("password-form");
2978
+ if (passwordForm) passwordForm.onsubmit = (event) => {
2979
+ event.preventDefault();
2980
+ handleAction(async () => {
2981
+ const newPassword = (document.querySelector('#password-form input[name="newPassword"]')?.value ?? "").trim();
2982
+ const violations = passwordPolicyViolations(newPassword);
2983
+ if (violations.length > 0) throw new Error(`La password non rispetta i requisiti: richiede ${violations.join(", ")}.`);
2984
+ if (!window.confirm(`Impostare una nuova password per ${user.email}?`)) return;
2985
+ await setUserPassword(codiceUtente, { newPassword });
2986
+ showNotice("Password aggiornata.");
2987
+ await showUser(String(codiceUtente));
2988
+ });
2989
+ };
2990
+ const setGdprButton = document.getElementById("set-gdpr");
2991
+ if (setGdprButton) setGdprButton.onclick = () => handleAction(async () => {
2992
+ if (!window.confirm(`Registrare il consenso GDPR per ${user.email}?`)) return;
2993
+ await setGdpr(codiceUtente);
2994
+ showNotice("Consenso GDPR registrato.");
2995
+ await showUser(String(codiceUtente));
2996
+ });
2632
2997
  const rolesForm = document.getElementById("roles");
2633
2998
  if (rolesForm) rolesForm.onsubmit = (event) => {
2634
2999
  event.preventDefault();
@@ -2688,6 +3053,14 @@ async function showUser(rawCode) {
2688
3053
  showNotice("Utente impostato come eliminato.");
2689
3054
  await show("users");
2690
3055
  });
3056
+ const purgeUser = document.getElementById("purge-user");
3057
+ if (purgeUser) purgeUser.onclick = () => handleAction(async () => {
3058
+ if (!window.confirm(`Eliminare DEFINITIVAMENTE l'utente ${user.email} e tutti i dati collegati? Operazione irreversibile.`)) return;
3059
+ await deleteUserPermanently(codiceUtente);
3060
+ showNotice("Utente eliminato definitivamente.");
3061
+ await show("users");
3062
+ });
3063
+ if (!manageable) setUserManagementEnabled(false);
2691
3064
  }
2692
3065
  async function showRoles() {
2693
3066
  const [rolesResponse, groupsResponse] = await Promise.all([getRoles(), getGroupsWithMenus({ includeDisabled: true })]);
@@ -3399,8 +3772,12 @@ window.addEventListener("keydown", (event) => {
3399
3772
  });
3400
3773
  window.addEventListener("resize", positionHelp);
3401
3774
  window.addEventListener("scroll", positionHelp, true);
3775
+ var entryTicket = /^#entry=([a-f0-9]{64})$/i.exec(window.location.hash)?.[1];
3402
3776
  var ssoChallenge = /^#two-factor=([a-f0-9]{64})$/.exec(window.location.hash)?.[1];
3403
- if (ssoChallenge) {
3777
+ if (entryTicket) {
3778
+ window.history.replaceState(null, "", window.location.pathname);
3779
+ void exchangeConsoleEntry(entryTicket);
3780
+ } else if (ssoChallenge) {
3404
3781
  token = null;
3405
3782
  setAccessiConsoleToken(null);
3406
3783
  sessionStorage.removeItem("accessi-console-token");