emilsoftware-utilities 2.0.0-dev.50 → 2.0.0-dev.51

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.
@@ -525,95 +525,6 @@ var setGdpr = async (codiceUtente, options) => {
525
525
  }
526
526
  );
527
527
  };
528
- var getGetTipiFiltroUrl = () => {
529
- return `/api/accessi/filtri/tipi`;
530
- };
531
- var getTipiFiltro = async (options) => {
532
- return accessiFetch(
533
- getGetTipiFiltroUrl(),
534
- {
535
- ...options,
536
- method: "GET"
537
- }
538
- );
539
- };
540
- var getCreateTipoFiltroUrl = () => {
541
- return `/api/accessi/filtri/tipi`;
542
- };
543
- var createTipoFiltro = async (createFilterTypeRequest, options) => {
544
- return accessiFetch(
545
- getCreateTipoFiltroUrl(),
546
- {
547
- ...options,
548
- method: "POST",
549
- body: JSON.stringify(
550
- createFilterTypeRequest
551
- )
552
- }
553
- );
554
- };
555
- var getUpdateTipoFiltroUrl = (tipFil) => {
556
- return `/api/accessi/filtri/tipi/${tipFil}`;
557
- };
558
- var updateTipoFiltro = async (tipFil, updateFilterTypeRequest, options) => {
559
- return accessiFetch(
560
- getUpdateTipoFiltroUrl(tipFil),
561
- {
562
- ...options,
563
- method: "PUT",
564
- body: JSON.stringify(
565
- updateFilterTypeRequest
566
- )
567
- }
568
- );
569
- };
570
- var getDeleteTipoFiltroUrl = (tipFil) => {
571
- return `/api/accessi/filtri/tipi/${tipFil}`;
572
- };
573
- var deleteTipoFiltro = async (tipFil, options) => {
574
- return accessiFetch(
575
- getDeleteTipoFiltroUrl(tipFil),
576
- {
577
- ...options,
578
- method: "DELETE"
579
- }
580
- );
581
- };
582
- var getGetFiltriUtenteUrl = (params) => {
583
- const normalizedParams = new URLSearchParams();
584
- Object.entries(params || {}).forEach(([key, value]) => {
585
- if (value === null) {
586
- normalizedParams.append(key, "null");
587
- } else if (value !== void 0) {
588
- normalizedParams.append(key, value.toString());
589
- }
590
- });
591
- return `/api/accessi/filtri/utente?${normalizedParams.toString()}`;
592
- };
593
- var getFiltriUtente = async (params, options) => {
594
- return accessiFetch(
595
- getGetFiltriUtenteUrl(params),
596
- {
597
- ...options,
598
- method: "GET"
599
- }
600
- );
601
- };
602
- var getSaveFiltriUtenteUrl = () => {
603
- return `/api/accessi/filtri/utente`;
604
- };
605
- var saveFiltriUtente = async (filtriUtente, options) => {
606
- return accessiFetch(
607
- getSaveFiltriUtenteUrl(),
608
- {
609
- ...options,
610
- method: "POST",
611
- body: JSON.stringify(
612
- filtriUtente
613
- )
614
- }
615
- );
616
- };
617
528
  var getSetMenuEnabledUrl = (codiceMenu) => {
618
529
  return `/api/accessi/configurator/menus/${codiceMenu}/enabled`;
619
530
  };
@@ -767,6 +678,21 @@ var deleteFederatedIdentityPermanently = async (codiceUtente, identityKey, optio
767
678
  }
768
679
  );
769
680
  };
681
+ var getExchangeConsoleEntryTicketUrl = () => {
682
+ return `/api/accessi/console/entry/exchange`;
683
+ };
684
+ var exchangeConsoleEntryTicket = async (consoleEntryExchangeRequest, options) => {
685
+ return accessiFetch(
686
+ getExchangeConsoleEntryTicketUrl(),
687
+ {
688
+ ...options,
689
+ method: "POST",
690
+ body: JSON.stringify(
691
+ consoleEntryExchangeRequest
692
+ )
693
+ }
694
+ );
695
+ };
770
696
  var getCreateServiceTokenUrl = () => {
771
697
  return `/api/accessi/service-token`;
772
698
  };
@@ -859,7 +785,7 @@ var WIKI_SECTIONS = [
859
785
  p(`Accessi \xE8 un modulo Node/NestJS che aggiunge a un backend gi\xE0 esistente: autenticazione JWT, autorizzazione per ruoli e menu, gestione utenti, reset password, codici email (2FA e passwordless), SSO generico, token macchina-a-macchina e una console amministrativa. Il database \xE8 Firebird/InterBase.`),
860
786
  h(`Che cosa possiede il modulo`),
861
787
  list([
862
- `Le rotte sotto /api/accessi/* (login, utenti, ruoli, menu, filtri, SSO, token, console).`,
788
+ `Le rotte sotto /api/accessi/* (login, utenti, ruoli, menu, SSO, token, console).`,
863
789
  `Lo schema database del dominio Accessi, riconciliato automaticamente al bootstrap.`,
864
790
  `Il middleware di autorizzazione authorizeAccessi per le rotte della tua applicazione.`,
865
791
  `La console web su /api/accessi/console.`
@@ -1092,7 +1018,7 @@ ACCESSI_CATALOG_FOLDER=./db/accessi-catalog npm run db:catalog:accessi:check`, "
1092
1018
  Content-Type: application/json
1093
1019
 
1094
1020
  { "email": "utente@example.com", "password": "Segreta123!" }`, "Login con password"),
1095
- p(`La risposta include Result.token (il JWT) e il profilo utente con ruoli, permessi e filtri. Se l'account \xE8 passwordless, ometti la password: il server risponde con un challenge via email (vedi la sezione su 2FA e passwordless).`),
1021
+ p(`La risposta include Result.token (il JWT) e il profilo utente con ruoli e permessi. Se l'account \xE8 passwordless, ometti la password: il server risponde con un challenge via email (vedi la sezione su 2FA e passwordless).`),
1096
1022
  good("ts", [
1097
1023
  "const res = await fetch('/api/accessi/auth/login', {",
1098
1024
  " method: 'POST',",
@@ -1244,7 +1170,7 @@ POST /api/accessi/auth/confirm-reset-password/{token}
1244
1170
  id: "utenti",
1245
1171
  group: "Dominio",
1246
1172
  title: "Utenti",
1247
- summary: "Creazione, aggiornamento, stati, GDPR e filtri.",
1173
+ summary: "Creazione, aggiornamento, stati e GDPR.",
1248
1174
  blocks: [
1249
1175
  h(`Stati di registrazione (STAREG)`),
1250
1176
  table(
@@ -1275,7 +1201,7 @@ POST /api/accessi/auth/confirm-reset-password/{token}
1275
1201
  ),
1276
1202
  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.`),
1277
1203
  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.`),
1278
- 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.`),
1204
+ 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 e scadenza password. 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.`),
1279
1205
  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.`),
1280
1206
  good("ts", [
1281
1207
  "await fetch('/api/accessi/user/create-managed-user', {",
@@ -1378,27 +1304,6 @@ POST /api/accessi/auth/confirm-reset-password/{token}
1378
1304
  ]
1379
1305
  },
1380
1306
  // -------------------------------------------------------------------------
1381
- {
1382
- id: "filtri",
1383
- group: "Dominio",
1384
- title: "Filtri applicativi",
1385
- summary: "Filtri per utente e catalogo dei tipi filtro.",
1386
- blocks: [
1387
- table(
1388
- ["Metodo", "Percorso", "Descrizione"],
1389
- [
1390
- ["GET", "/api/accessi/filtri/tipi", "Catalogo dei tipi filtro."],
1391
- ["POST", "/api/accessi/filtri/tipi", "Crea un tipo filtro (superutente)."],
1392
- ["PUT", "/api/accessi/filtri/tipi/:tipFil", "Aggiorna un tipo filtro."],
1393
- ["DELETE", "/api/accessi/filtri/tipi/:tipFil", "Elimina un tipo filtro (se non \xE8 usato)."],
1394
- ["GET", "/api/accessi/filtri/utente?codUte=123", "Filtri di un utente (self o superutente)."],
1395
- ["POST", "/api/accessi/filtri/utente", "Upsert dei filtri (self o superutente)."]
1396
- ]
1397
- ),
1398
- note("info", `Mapping delle colonne`, `I campi filtro sono mappati su colonne FILTRI in whitelist (FILTRI_UTENTE_DB_MAPPING). I campi non configurati nello schema vengono rifiutati.`)
1399
- ]
1400
- },
1401
- // -------------------------------------------------------------------------
1402
1307
  {
1403
1308
  id: "sso",
1404
1309
  group: "SSO",
@@ -1476,7 +1381,6 @@ POST /api/accessi/auth/confirm-reset-password/{token}
1476
1381
  `Utenti: elenco, creazione locale o SSO, profilo, ruoli, grant diretti, stato, 2FA.`,
1477
1382
  `Ruoli e grant: CRUD dei ruoli con l'albero delle abilitazioni.`,
1478
1383
  `Menu e gruppi: CRUD di gruppi, menu e tipi menu.`,
1479
- `Filtri: filtri utente e tipi filtro.`,
1480
1384
  `Token di servizio: creazione, rotazione, revoca.`,
1481
1385
  `SSO: provider e identit\xE0.`,
1482
1386
  `Wiki e Changelog: documentazione e novit\xE0 della libreria.`,
@@ -1484,7 +1388,7 @@ POST /api/accessi/auth/confirm-reset-password/{token}
1484
1388
  ]),
1485
1389
  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.`),
1486
1390
  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.`),
1487
- 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.`),
1391
+ 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 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.`),
1488
1392
  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).`),
1489
1393
  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.`),
1490
1394
  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).`),
@@ -1542,7 +1446,6 @@ POST /api/accessi/auth/confirm-reset-password/{token}
1542
1446
  ["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"],
1543
1447
  ["Catalogo", "POST/PUT/DELETE permission/menus[/:codiceMenu], permission/menu-groups[/:codiceGruppo], permission/menu-types[/:codiceTipo]"],
1544
1448
  ["Configurator", "PATCH configurator/menus/:codiceMenu/enabled, PATCH configurator/groups/:codiceGruppo/enabled"],
1545
- ["Filtri", "GET filtri/tipi, POST/PUT/DELETE filtri/tipi[/:tipFil], GET/POST filtri/utente"],
1546
1449
  ["SSO", "GET/POST federated-auth/providers, PATCH/DELETE providers/:provider, GET/POST users/:codiceUtente/identities, POST users, PATCH users/:codiceUtente/password-login"],
1547
1450
  ["Token", "POST/GET service-token, DELETE service-token/:tokenId, POST service-token/:tokenId/rotate"],
1548
1451
  ["Schema", "GET schema (DDL completo + entit\xE0 presenti, solo admin)"],
@@ -1630,7 +1533,7 @@ function buildAiDigest() {
1630
1533
  }
1631
1534
 
1632
1535
  // src/accessi-module/Console/changelog.md
1633
- var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\n\n## 2.0.0-dev.44\n\n### Aggiunte\n- Endpoint pubblico `GET /api/accessi/auth/bootstrap-admin/status` (`getAdminBootstrapStatus`): la console mostra la sezione \"Crea utente admin\" **solo** se `adminBootstrap.enabled` \xE8 `true`.\n\n### Correzioni\n- Rimossi i caratteri corrotti (mojibake UTF-8 \u2194 Windows-1252) da DTO, servizi, controller, console, OpenAPI e client generato (es. \"L'email \xE8 gi\xE0 associata a un utente 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";
1536
+ var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\n\n## Non rilasciato\n\n### Cambiamenti\n- Console: rimosse le sezioni **Filtri utente** e **Tipi filtro**. I filtri applicativi sono deprecati e non vengono pi\xF9 letti n\xE9 scritti dalla console; le tabelle restano invariate e la gestione torna manuale sul database.\n- Console: le conferme operative (es. \"Ruoli aggiornati\") ora compaiono come notifiche in alto a destra e scompaiono automaticamente.\n- Console: il salvataggio del profilo non invia pi\xF9 i campi della tabella `FILTRI`, evitando payload rifiutati dal backend. `UserService` tocca `FILTRI` solo se il chiamante invia esplicitamente almeno un campo legacy.\n- Console: scheda utente riorganizzata. \"Indietro\" resta nella barra superiore; stato registrazione, disattivazione ed eliminazione definitiva sono in un form coerente \"Stato e sicurezza\", con icone e tooltip allineati.\n- Console: pagina Changelog a larghezza piena, con intestazioni di versione evidenziate e sezioni orizzontali leggibili.\n- Seed `npm run accessi:seed-admin`: crea o aggiorna un admin completo (`FLGSUPER = 1` e `FLGADMIN = 1`) e corregge il nome colonna usato in precedenza (`FLGADMIN`).\n\n## 2.0.0-dev.50 (2026-10-15)\n\n### Cambiamenti\n- ORM: `Row` e `DbValue` sono i tipi predefiniti di `Orm.query` e `Orm.execute`; `transactionQuery` usa `Row[]`. Gli usi generici non devono pi\xF9 dichiarare `Record<string, unknown>`.\n- Build: il contratto OpenAPI generato (`src/accessi-module/openapi/`) \xE8 ignorato da Git e viene rigenerato da `npm run generate:accessi-openapi`.\n\n## 2.0.0-dev.49 (2026-10-15)\n\n### Cambiamenti\n- Documentazione HTTP rinominata da Swagger a **OpenAPI**: il modulo unificato espone `/accessi/openapi` e `/accessi/openapi.json`; `setupSwagger` usa `/openapi` come default.\n- Il path \xE8 configurabile tramite `EmilsoftwareOptions.swagger` (utile per far convivere pi\xF9 documentazioni).\n- Allineati log di avvio, Wiki e dispatch delle rotte del modulo.\n- L'alias storico `<path>-json` resta supportato.\n\n### Note\n- I consumer che aprivano `/swagger` o `/accessi/swagger` devono aggiornare l'URL alla nuova documentazione OpenAPI.\n\n## 2.0.0-dev.48 (2026-10-13)\n\n### Correzioni\n- `RestUtilities.sendErrorMessage` restituisce status HTTP specifici (400/401/403/4xx/5xx) e codici generici (`VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `INTERNAL_ERROR`, `REQUEST_ERROR`), senza usare codici `ACCESSI_*` nel business helper.\n- Il messaggio restituito \xE8 quello reale dell'eccezione; gli errori testuali di validazione vengono riconosciuti come 400.\n\n## 2.0.0-dev.47 (2026-10-13)\n\n### Correzioni\n- Errore schema database reso generico: codice `DATABASE_SCHEMA_OUTDATED` e messaggio senza riferimenti specifici ad Accessi.\n\n## 2.0.0-dev.46 (2026-10-12)\n\n### Correzioni\n- `sendBaseResponse` resta retro-compatibile quando il terzo argomento \xE8 un tag stringa invece dello status HTTP.\n- Gestita la serializzazione dei `bigint`; gli errori di risposta vengono registrati con stack tramite `sendErrorMessage`.\n- `AccessiHttpExceptionFilter` registra gli errori con stack per facilitare la diagnosi.\n\n## 2.0.0-dev.45 (2026-10-12)\n\n### Correzioni\n- `set-gdpr` e l'aggiornamento di `flagGdpr` sono idempotenti: `UPDATE OR INSERT` su `UTENTI_GDPR` con `MATCHING (CODUTE, DATACC)`.\n- Evitate violazioni di chiave primaria e risposte 500 quando il consenso GDPR viene registrato pi\xF9 volte nello stesso giorno.\n\n## 2.0.0-dev.44\n\n### Aggiunte\n- Endpoint pubblico `GET /api/accessi/auth/bootstrap-admin/status` (`getAdminBootstrapStatus`): la console mostra la sezione \"Crea utente admin\" **solo** se `adminBootstrap.enabled` \xE8 `true`.\n\n### Correzioni\n- Rimossi i caratteri corrotti (mojibake UTF-8 \u2194 Windows-1252) da DTO, servizi, controller, console, OpenAPI e client generato (es. \"L'email \xE8 gi\xE0 associata a un utente 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";
1634
1537
 
1635
1538
  // src/accessi-module/Console/help.ts
1636
1539
  var p2 = (text) => ({ kind: "p", text });
@@ -1840,46 +1743,6 @@ var HELP = {
1840
1743
  note2("warn", "Sostituisce la password attuale", `L'utente dovr\xE0 usare subito la nuova password. Per fargliela scegliere, usa "Invia reset password".`)
1841
1744
  ]
1842
1745
  },
1843
- "user-filter-numrep": {
1844
- title: "Numero reparto (NUMREP)",
1845
- blocks: [p2(`Reparto associato all'utente nella tabella FILTRI. Vuoto rimuove il valore.`)]
1846
- },
1847
- "user-filter-idxpers": {
1848
- title: "Indice personale (IDXPERS)",
1849
- blocks: [p2(`Indice del personale nella tabella FILTRI. Vuoto rimuove il valore.`)]
1850
- },
1851
- "user-filter-codclisuper": {
1852
- title: "Cliente padre (CODCLISUPER)",
1853
- blocks: [p2(`Codice del cliente padre. Vuoto rimuove il valore.`)]
1854
- },
1855
- "user-filter-codage": {
1856
- title: "Agente (CODAGE)",
1857
- blocks: [p2(`Codice dell'agente associato. Vuoto rimuove il valore.`)]
1858
- },
1859
- "user-filter-codclicol": {
1860
- title: "Cliente collegato (CODCLICOL)",
1861
- blocks: [p2(`Codice del cliente collegato. Vuoto rimuove il valore.`)]
1862
- },
1863
- "user-filter-codclienti": {
1864
- title: "Clienti (CODCLIENTI)",
1865
- blocks: [p2(`Elenco di codici cliente separati da virgola (testo libero). Vuoto rimuove il valore.`)]
1866
- },
1867
- "user-filter-tipfil": {
1868
- title: "Tipo filtro (TIPFIL)",
1869
- blocks: [p2(`Tipo di filtro applicato. Vuoto rimuove il valore.`)]
1870
- },
1871
- "user-filter-idxpos": {
1872
- title: "Postazione (IDXPOS)",
1873
- blocks: [p2(`Indice della postazione nella tabella ANTENNE_POS. Vuoto rimuove il valore.`)]
1874
- },
1875
- "user-filter-coddip": {
1876
- title: "Dipendente (CODDIP)",
1877
- blocks: [p2(`Codice del dipendente collegato. Vuoto rimuove il valore.`)]
1878
- },
1879
- "user-filter-codvet": {
1880
- title: "Vettore (CODVET)",
1881
- blocks: [p2(`Codice del vettore collegato. Vuoto rimuove il valore.`)]
1882
- },
1883
1746
  "user-super": {
1884
1747
  title: "Superutente (FLGSUPER)",
1885
1748
  blocks: [
@@ -1999,38 +1862,6 @@ var HELP = {
1999
1862
  p2(`Disattiva l'intero gruppo: i menu al suo interno non sono considerati attivi, ma restano configurabili per una riattivazione futura.`)
2000
1863
  ]
2001
1864
  },
2002
- // ----------------------------------------------------------------- filtri
2003
- "filter-type-code": {
2004
- title: "Codice tipo filtro",
2005
- blocks: [
2006
- p2(`Numero identificativo del tipo filtro. \xC8 la chiave primaria: non si modifica dopo la creazione.`)
2007
- ]
2008
- },
2009
- "filter-type-field": {
2010
- title: "Campo",
2011
- blocks: [
2012
- p2(`Nome della colonna applicativa della tabella FILTRI a cui il tipo \xE8 associato (per esempio CODCLI). Deve esistere nello schema, altrimenti i filtri che lo usano vengono rifiutati.`)
2013
- ]
2014
- },
2015
- "filter-type-enabled": {
2016
- title: "Tipo filtro abilitato",
2017
- blocks: [
2018
- p2(`Se disattivato, il tipo non \xE8 proposto ai client, ma i filtri gi\xE0 salvati restano nel database.`)
2019
- ]
2020
- },
2021
- "filters-json": {
2022
- title: "Filtro JSON",
2023
- blocks: [
2024
- p2(`Rappresentazione JSON dei filtri applicativi dell'utente. Usa "Carica" per partire dalla struttura esistente, modifica i valori e poi "Salva".`),
2025
- note2("info", "Campi ammessi", `Sono accettati solo i campi mappati nello schema (FILTRI_UTENTE_DB_MAPPING). Un campo non configurato viene rifiutato con un errore esplicito.`)
2026
- ]
2027
- },
2028
- "filters-user": {
2029
- title: "Utente",
2030
- blocks: [
2031
- p2(`Utente a cui appartengono i filtri. Selezionalo prima di caricare o salvare: i filtri sono sempre per singolo utente.`)
2032
- ]
2033
- },
2034
1865
  // ------------------------------------------------------------ assegnazioni
2035
1866
  "role-assignment": {
2036
1867
  title: "Assegnazione dei ruoli",
@@ -2080,7 +1911,7 @@ var HELP = {
2080
1911
  "user-delete-permanent": {
2081
1912
  title: "Elimina definitivamente",
2082
1913
  blocks: [
2083
- p2(`Cancella l'utente e tutti i record collegati (configurazione, password, ruoli, abilitazioni, filtri, identit\xE0 SSO, challenge 2FA e GDPR). Operazione irreversibile.`),
1914
+ p2(`Cancella l'utente e tutti i record collegati (configurazione, password, ruoli, abilitazioni, identit\xE0 SSO, challenge 2FA e GDPR). Operazione irreversibile.`),
2084
1915
  note2("danger", "Non reversibile", `I dati vengono rimossi fisicamente dal database; non \xE8 possibile ripristinare l'utente.`),
2085
1916
  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.`)
2086
1917
  ]
@@ -2120,8 +1951,6 @@ var routeSegment = {
2120
1951
  menuGroups: "menu-e-gruppi/gruppi",
2121
1952
  menuItems: "menu-e-gruppi/menu",
2122
1953
  menuTypes: "menu-e-gruppi/tipi-menu",
2123
- filterUser: "filtri/utente",
2124
- filterTypes: "filtri/tipi",
2125
1954
  ssoProviders: "sso/provider",
2126
1955
  ssoUsers: "sso/utenti",
2127
1956
  tokens: "token-di-servizio",
@@ -2132,7 +1961,6 @@ var routeSegment = {
2132
1961
  var routeView = Object.fromEntries(Object.entries(routeSegment).map(([view, segment]) => [segment, view]));
2133
1962
  var parentDefaultView = {
2134
1963
  "menu-e-gruppi": "menuGroups",
2135
- filtri: "filterUser",
2136
1964
  sso: "ssoProviders"
2137
1965
  };
2138
1966
  var consoleMarker = "/accessi/console";
@@ -2157,29 +1985,43 @@ var escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({
2157
1985
  '"': "&quot;",
2158
1986
  "'": "&#39;"
2159
1987
  })[char]);
1988
+ function actionIcon(paths) {
1989
+ return `<svg class="button-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
1990
+ }
2160
1991
  function result(response) {
2161
1992
  const body = response.data;
2162
1993
  if (body.Result === void 0) throw new Error(body.message ?? "Risposta Accessi priva di dati.");
2163
1994
  return body.Result;
2164
1995
  }
2165
- var noticeTimer;
1996
+ var noticeTimers = /* @__PURE__ */ new Set();
2166
1997
  function showNotice(message, isError = false) {
2167
1998
  const container = byId("notice");
2168
- if (noticeTimer !== void 0) {
2169
- window.clearTimeout(noticeTimer);
2170
- noticeTimer = void 0;
2171
- }
2172
1999
  if (!message) {
2000
+ noticeTimers.forEach((timer2) => window.clearTimeout(timer2));
2001
+ noticeTimers.clear();
2173
2002
  container.innerHTML = "";
2174
2003
  return;
2175
2004
  }
2005
+ const toast = document.createElement("div");
2176
2006
  const kind = isError ? "error" : "success";
2177
2007
  const icon = isError ? "!" : "\u2713";
2178
- container.innerHTML = `<div class="notice notice-${kind}${isError ? " error" : ""}" role="alert"><span class="notice-icon" aria-hidden="true">${icon}</span><span class="notice-text">${escapeHtml(message)}</span><button type="button" class="notice-close" aria-label="Chiudi il messaggio">&times;</button></div>`;
2179
- container.querySelector(".notice-close")?.addEventListener("click", () => showNotice(""));
2180
- if (!isError) {
2181
- noticeTimer = window.setTimeout(() => showNotice(""), 6e3);
2182
- }
2008
+ toast.className = `notice notice-${kind}`;
2009
+ toast.setAttribute("role", isError ? "alert" : "status");
2010
+ toast.innerHTML = `<span class="notice-icon" aria-hidden="true">${icon}</span><span class="notice-text">${escapeHtml(message)}</span><button type="button" class="notice-close" aria-label="Chiudi il messaggio">&times;</button>`;
2011
+ container.appendChild(toast);
2012
+ let timer;
2013
+ const dismiss = () => {
2014
+ if (timer !== void 0) {
2015
+ window.clearTimeout(timer);
2016
+ noticeTimers.delete(timer);
2017
+ timer = void 0;
2018
+ }
2019
+ toast.remove();
2020
+ };
2021
+ toast.querySelector(".notice-close")?.addEventListener("click", dismiss);
2022
+ timer = window.setTimeout(dismiss, isError ? 9e3 : 5e3);
2023
+ noticeTimers.add(timer);
2024
+ while (container.childElementCount > 4) container.firstElementChild?.remove();
2183
2025
  }
2184
2026
  function menuMetadata(menu) {
2185
2027
  const details = [
@@ -2602,15 +2444,10 @@ async function bootstrap() {
2602
2444
  }
2603
2445
  async function exchangeConsoleEntry(ticket) {
2604
2446
  try {
2605
- const response = await fetch(`${consoleBasePath}/entry/exchange`, {
2606
- method: "POST",
2607
- headers: { "Content-Type": "application/json", Accept: "application/json" },
2608
- body: JSON.stringify({ ticket })
2609
- });
2610
- const payload = await response.json().catch(() => void 0);
2611
- const value = payload?.Result?.token?.value;
2612
- if (!response.ok || typeof value !== "string" || value === "") {
2613
- throw new Error(payload?.message ?? "Apertura automatica della console non riuscita.");
2447
+ const session = result(await exchangeConsoleEntryTicket({ ticket }));
2448
+ const value = session.token?.value;
2449
+ if (typeof value !== "string" || value === "") {
2450
+ throw new Error("Apertura automatica della console non riuscita.");
2614
2451
  }
2615
2452
  token = value;
2616
2453
  sessionStorage.setItem("accessi-console-token", value);
@@ -2721,13 +2558,6 @@ function optionalText(values, key) {
2721
2558
  const raw = (values[key] ?? "").trim();
2722
2559
  return raw === "" ? null : raw;
2723
2560
  }
2724
- function optionalNumber(values, key) {
2725
- if (!(key in values)) return void 0;
2726
- const raw = (values[key] ?? "").trim();
2727
- if (raw === "") return null;
2728
- const parsed = Number(raw);
2729
- return Number.isFinite(parsed) ? parsed : null;
2730
- }
2731
2561
  function passwordPolicyViolations(password) {
2732
2562
  const violations = [];
2733
2563
  if (password.length < 8) violations.push("almeno 8 caratteri");
@@ -2754,20 +2584,6 @@ function extendedProfileFields(user) {
2754
2584
  <label>Metadata JSON ${helpDot("user-json")}<textarea name="jsonMetadata" rows="3">${escapeHtml(user.jsonMetadata ?? "")}</textarea></label>
2755
2585
  </fieldset>`;
2756
2586
  }
2757
- function filterFields(user) {
2758
- return `<fieldset><legend>Filtri utente (FILTRI)</legend>
2759
- <label>Numero reparto ${helpDot("user-filter-numrep")}<input name="numRep" type="number" value="${escapeHtml(user.numRep ?? "")}"></label>
2760
- <label>Indice personale ${helpDot("user-filter-idxpers")}<input name="idxPers" type="number" value="${escapeHtml(user.idxPers ?? "")}"></label>
2761
- <label>Cliente padre ${helpDot("user-filter-codclisuper")}<input name="codCliSuper" type="number" value="${escapeHtml(user.codCliSuper ?? "")}"></label>
2762
- <label>Agente ${helpDot("user-filter-codage")}<input name="codAge" type="number" value="${escapeHtml(user.codAge ?? "")}"></label>
2763
- <label>Cliente collegato ${helpDot("user-filter-codclicol")}<input name="codCliCol" type="number" value="${escapeHtml(user.codCliCol ?? "")}"></label>
2764
- <label>Clienti (separati da virgola) ${helpDot("user-filter-codclienti")}<input name="codClienti" value="${escapeHtml(user.codClienti ?? "")}"></label>
2765
- <label>Tipo filtro ${helpDot("user-filter-tipfil")}<input name="tipFil" type="number" value="${escapeHtml(user.tipFil ?? "")}"></label>
2766
- <label>Postazione ${helpDot("user-filter-idxpos")}<input name="idxPos" type="number" value="${escapeHtml(user.idxPos ?? "")}"></label>
2767
- <label>Dipendente ${helpDot("user-filter-coddip")}<input name="codDip" type="number" value="${escapeHtml(user.codDip ?? "")}"></label>
2768
- <label>Vettore ${helpDot("user-filter-codvet")}<input name="codVet" type="number" value="${escapeHtml(user.codVet ?? "")}"></label>
2769
- </fieldset>`;
2770
- }
2771
2587
  function profilePayload(form, codiceUtente) {
2772
2588
  const values = formValues(form);
2773
2589
  const fields = new FormData(form);
@@ -2784,16 +2600,6 @@ function profilePayload(form, codiceUtente) {
2784
2600
  dataScadenzaPassword: optionalText(values, "dataScadenzaPassword"),
2785
2601
  flagDueFattori: fields.has("flagDueFattori"),
2786
2602
  passwordlessLoginEnabled: fields.has("passwordlessLoginEnabled"),
2787
- numRep: optionalNumber(values, "numRep"),
2788
- idxPers: optionalNumber(values, "idxPers"),
2789
- codCliSuper: optionalNumber(values, "codCliSuper"),
2790
- codAge: optionalNumber(values, "codAge"),
2791
- codCliCol: optionalNumber(values, "codCliCol"),
2792
- codClienti: optionalText(values, "codClienti"),
2793
- tipFil: optionalNumber(values, "tipFil"),
2794
- idxPos: optionalNumber(values, "idxPos"),
2795
- codDip: optionalNumber(values, "codDip"),
2796
- codVet: optionalNumber(values, "codVet"),
2797
2603
  ...privilegePayload(form)
2798
2604
  };
2799
2605
  if (federatedAuthenticationAvailable) {
@@ -2945,9 +2751,24 @@ async function showUser(rawCode) {
2945
2751
  const identitySection = federatedAuthenticationAvailable ? federatedIdentitySection(linked, providerList) : "";
2946
2752
  const defaultUserTab = federatedAuthenticationAvailable ? "sso" : "profile";
2947
2753
  const manageable = canManageUser(user);
2948
- 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">
2754
+ 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></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>
2755
+ <form id="user-state-form" class="account-panel">
2756
+ <div class="account-panel-head"><div><p class="eyebrow">Amministrazione account</p><h3>Stato e sicurezza</h3><p class="form-help">Aggiorna lo stato di registrazione o disattiva l'account. Le azioni distruttive sono raccolte qui, separate dal profilo.</p></div></div>
2757
+ <div class="account-state-row">
2758
+ <label class="account-state-field">Stato registrazione ${helpDot("user-state")}<select id="user-state">${registrationStateOptions(user.statoRegistrazione)}</select></label>
2759
+ <button id="save-state" type="submit" class="secondary">${actionIcon('<path d="M20 6 9 17l-5-5"/>')}<span>Aggiorna stato</span></button>
2760
+ </div>
2761
+ <div class="account-danger-zone">
2762
+ <div class="account-danger-head"><strong>Azioni distruttive</strong><span class="form-help">La disattivazione conserva i dati; l'eliminazione definitiva \xE8 irreversibile.</span></div>
2763
+ <div class="account-danger-actions">
2764
+ <span class="action-with-help"><button id="disable-user" type="button" class="danger-button">${actionIcon('<path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/>')}<span>Imposta stato eliminato</span></button>${helpDot("user-delete")}</span>
2765
+ ${currentUser?.flagAdmin ? `<span class="action-with-help"><button id="purge-user" type="button" class="danger-button">${actionIcon('<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/>')}<span>Elimina definitivamente</span></button>${helpDot("user-delete-permanent")}</span>` : ""}
2766
+ </div>
2767
+ </div>
2768
+ </form>
2769
+ ${userDetailTabs(federatedAuthenticationAvailable)}<div class="user-editor">
2949
2770
  ${identitySection}
2950
- <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\xE0 SSO attive.</span>` : ""}${authenticationPolicyFields(user)}${extendedProfileFields(user)}${privilegeFields(user)}${filterFields(user)}<button id="save-profile" type="submit">Salva</button></form>
2771
+ <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\xE0 SSO attive.</span>` : ""}${authenticationPolicyFields(user)}${extendedProfileFields(user)}${privilegeFields(user)}<button id="save-profile" type="submit">Salva</button></form>
2951
2772
  ${passwordFields()}
2952
2773
  ${roleAssignmentEditor(allRoles, userGrants.ruoli, menuGroups)}
2953
2774
  ${directGrantEditor(menuGroups, userGrants.abilitazioni)}
@@ -3063,13 +2884,17 @@ async function showUser(rawCode) {
3063
2884
  });
3064
2885
  });
3065
2886
  }
3066
- byId("save-state").onclick = () => handleAction(async () => {
3067
- const stateSelect = document.getElementById("user-state");
3068
- const statoRegistrazione = Number(stateSelect?.value ?? 0);
3069
- await setStatoRegistrazione({ codiceUtente, statoRegistrazione });
3070
- showNotice("Stato registrazione aggiornato.");
3071
- await showUser(String(codiceUtente));
3072
- });
2887
+ const userStateForm = document.getElementById("user-state-form");
2888
+ if (userStateForm) userStateForm.onsubmit = (event) => {
2889
+ event.preventDefault();
2890
+ handleAction(async () => {
2891
+ const stateSelect = document.getElementById("user-state");
2892
+ const statoRegistrazione = Number(stateSelect?.value ?? 0);
2893
+ await setStatoRegistrazione({ codiceUtente, statoRegistrazione });
2894
+ showNotice("Stato registrazione aggiornato.");
2895
+ await showUser(String(codiceUtente));
2896
+ });
2897
+ };
3073
2898
  byId("disable-user").onclick = () => handleAction(async () => {
3074
2899
  await deleteUser(codiceUtente);
3075
2900
  showNotice("Utente impostato come eliminato.");
@@ -3105,7 +2930,9 @@ async function showRoles() {
3105
2930
  handleAction(async () => {
3106
2931
  const form = eventForm(event);
3107
2932
  const data = new FormData(form);
3108
- const request = { descrizioneRuolo: formValues(form).descrizione ?? "", menu: Array.from(data.getAll("menu"), (codiceMenu) => ({ codiceMenu: String(codiceMenu), tipoAbilitazione: Number(data.get(`menu-level:${codiceMenu}`) ?? 10) })) };
2933
+ const menu = Array.from(data.getAll("menu"), (codiceMenu) => ({ codiceMenu: String(codiceMenu), tipoAbilitazione: Number(data.get(`menu-level:${codiceMenu}`) ?? 10) }));
2934
+ if (menu.length === 0) throw new Error("Seleziona almeno un menu per il ruolo.");
2935
+ const request = { descrizioneRuolo: formValues(form).descrizione ?? "", menu };
3109
2936
  if (role?.codiceRuolo) await updateRole(role.codiceRuolo, request);
3110
2937
  else await createRole(request);
3111
2938
  showNotice(role?.codiceRuolo ? "Ruolo aggiornato." : "Ruolo creato.");
@@ -3316,76 +3143,6 @@ function showMenuTypeForm(type) {
3316
3143
  };
3317
3144
  }
3318
3145
  }
3319
- function filterTypeRows(types) {
3320
- return `<div class="menu-table-wrap"><table><thead><tr><th>Codice</th><th>Descrizione</th><th>Campo</th><th>Abilitato</th><th></th></tr></thead><tbody>${types.map((type) => `<tr class="${type.flgEnabled === 0 ? "is-disabled" : ""}"><td><code>${escapeHtml(type.tipFil)}</code></td><td>${escapeHtml(type.desFil || "\u2014")}</td><td>${escapeHtml(type.fldFil || "\u2014")}</td><td>${type.flgEnabled === 0 ? "No" : "Si"}</td><td class="row-actions"><button type="button" data-filter-type-edit="${escapeHtml(type.tipFil)}">Modifica</button><button type="button" class="danger-button" data-filter-type-delete="${escapeHtml(type.tipFil)}">Elimina</button></td></tr>`).join("") || '<tr><td colspan="5" class="muted">Nessun tipo filtro.</td></tr>'}</tbody></table></div>`;
3321
- }
3322
- async function showFilters() {
3323
- const users = await loadUsers();
3324
- render(`<h2>Filtri utente</h2><p class="form-help">Filtri applicativi salvati per singolo utente.</p><form id="filters"><label>Utente ${helpDot("filters-user")}<select name="codUte">${users.map(({ utente }) => `<option value="${utente.codiceUtente}">${escapeHtml(utente.email)}</option>`).join("")}</select><span class="form-help">Salvati per l'utente selezionato.</span></label><label>Filtro JSON ${helpDot("filters-json")}<textarea name="json" rows="12">{}</textarea><span class="form-help">Usa Carica per partire dalla struttura esistente.</span></label><button name="action" value="load">Carica</button><button name="action" value="save">Salva</button></form>`);
3325
- const filtersForm = document.getElementById("filters");
3326
- if (filtersForm) filtersForm.onsubmit = (event) => {
3327
- event.preventDefault();
3328
- const form = eventForm(event);
3329
- const action = event.submitter?.value;
3330
- const code3 = Number(formValues(form).codUte);
3331
- handleAction(async () => {
3332
- if (action === "load") {
3333
- const filters = result(await getFiltriUtente({ codUte: code3 }));
3334
- form.elements.namedItem("json").value = JSON.stringify(filters[0] ?? { codUte: code3 }, null, 2);
3335
- showNotice("Filtri caricati.");
3336
- } else {
3337
- const parsed = JSON.parse(formValues(form).json ?? "{}");
3338
- await saveFiltriUtente({ ...parsed, codUte: code3 });
3339
- showNotice("Filtri salvati.");
3340
- }
3341
- });
3342
- };
3343
- }
3344
- async function showFilterTypes() {
3345
- const filterTypes = result(await getTipiFiltro());
3346
- render(`<div class="toolbar"><button id="new-filter-type">Nuovo tipo filtro</button><button id="reload-filter-types" class="secondary">Aggiorna</button></div>
3347
- <h2>Tipi filtro</h2><p class="form-help">Catalogo dei tipi usati dai filtri utente. Un tipo si elimina solo se nessun filtro lo utilizza.</p>
3348
- ${filterTypeRows(filterTypes)}`);
3349
- byId("new-filter-type").onclick = () => showFilterTypeForm();
3350
- byId("reload-filter-types").onclick = () => handleAction(showFilterTypes);
3351
- document.querySelectorAll("[data-filter-type-edit]").forEach((button) => button.onclick = () => showFilterTypeForm(filterTypes.find((type) => String(type.tipFil) === button.dataset.filterTypeEdit)));
3352
- document.querySelectorAll("[data-filter-type-delete]").forEach((button) => button.onclick = () => handleAction(async () => {
3353
- if (!window.confirm(`Eliminare il tipo filtro ${button.dataset.filterTypeDelete}? Operazione possibile solo se nessun filtro lo utilizza.`)) return;
3354
- await deleteTipoFiltro(Number(button.dataset.filterTypeDelete));
3355
- showNotice("Tipo filtro eliminato.");
3356
- await showFilterTypes();
3357
- }));
3358
- }
3359
- function showFilterTypeForm(type) {
3360
- const isExisting = type !== void 0;
3361
- render(`<button id="back" type="button">Indietro</button><h2>${isExisting ? "Modifica" : "Nuovo"} tipo filtro</h2>
3362
- <form id="filter-type-form">
3363
- <label>Codice ${helpDot("filter-type-code")}<input name="tipFil" type="number" value="${escapeHtml(type?.tipFil)}" ${isExisting ? "readonly" : ""} required><span class="form-help">Identificativo numerico. Immutabile dopo la creazione.</span></label>
3364
- <label>Descrizione<input name="desFil" maxlength="20" value="${escapeHtml(type?.desFil)}"></label>
3365
- <label>Campo ${helpDot("filter-type-field")}<input name="fldFil" maxlength="20" value="${escapeHtml(type?.fldFil)}"><span class="form-help">Nome del campo applicativo associato al filtro.</span></label>
3366
- <label class="check"><input name="flgEnabled" type="checkbox" ${type?.flgEnabled !== 0 ? "checked" : ""}> Abilitato ${helpDot("filter-type-enabled")}</label>
3367
- <button>${isExisting ? "Salva tipo" : "Crea tipo"}</button>
3368
- </form>`);
3369
- byId("back").onclick = () => handleAction(showFilterTypes);
3370
- const form = document.getElementById("filter-type-form");
3371
- if (form) {
3372
- form.onsubmit = (event) => {
3373
- event.preventDefault();
3374
- handleAction(async () => {
3375
- const values = formValues(eventForm(event));
3376
- const payload = {
3377
- desFil: values.desFil || void 0,
3378
- fldFil: values.fldFil || void 0,
3379
- flgEnabled: new FormData(eventForm(event)).has("flgEnabled") ? 1 : 0
3380
- };
3381
- if (isExisting) await updateTipoFiltro(type.tipFil, payload);
3382
- else await createTipoFiltro({ tipFil: Number(values.tipFil), ...payload });
3383
- showNotice(isExisting ? "Tipo filtro aggiornato." : "Tipo filtro creato.");
3384
- await showFilterTypes();
3385
- });
3386
- };
3387
- }
3388
- }
3389
3146
  function scopeList(scopes) {
3390
3147
  return (scopes ?? []).join(", ") || "nessuno";
3391
3148
  }
@@ -3626,10 +3383,10 @@ function renderMarkdown(markdown) {
3626
3383
  return html.join("");
3627
3384
  }
3628
3385
  async function showChangelog() {
3629
- render(`<div class="wiki"><article class="wiki-content markdown-body">
3386
+ render(`<article class="wiki-content markdown-body changelog-page">
3630
3387
  <header class="wiki-section-head"><h2>Changelog</h2><p>Novita e modifiche rilevanti del modulo Accessi.</p></header>
3631
3388
  ${renderMarkdown(changelog_default)}
3632
- </article></div>`);
3389
+ </article>`);
3633
3390
  }
3634
3391
  function downloadTextFile(fileName, content) {
3635
3392
  const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
@@ -3674,8 +3431,6 @@ async function show(view) {
3674
3431
  menuGroups: showMenuGroups,
3675
3432
  menuItems: showMenuItems,
3676
3433
  menuTypes: showMenuTypes,
3677
- filterUser: showFilters,
3678
- filterTypes: showFilterTypes,
3679
3434
  ssoProviders: showSsoProviders,
3680
3435
  ssoUsers: showSsoUsers,
3681
3436
  tokens: showServiceTokens,