emilsoftware-utilities 2.0.0-dev.42 → 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 (69) 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 +132 -26
  5. package/dist/accessi-module/Console/console.js.map +1 -1
  6. package/dist/accessi-module/Console/generated/accessiApi.d.ts +21 -1
  7. package/dist/accessi-module/Console/generated/accessiApi.js +17 -1
  8. package/dist/accessi-module/Console/generated/accessiApi.js.map +1 -1
  9. package/dist/accessi-module/Console/generated/model/consoleEntryExchangeRequest.d.ts +10 -0
  10. package/dist/accessi-module/Console/generated/model/consoleEntryExchangeRequest.js +9 -0
  11. package/dist/accessi-module/Console/generated/model/consoleEntryExchangeRequest.js.map +1 -0
  12. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResponse.d.ts +14 -0
  13. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResponse.js +3 -0
  14. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResponse.js.map +1 -0
  15. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResult.d.ts +10 -0
  16. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResult.js +3 -0
  17. package/dist/accessi-module/Console/generated/model/consoleEntrySessionResult.js.map +1 -0
  18. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResponse.d.ts +14 -0
  19. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResponse.js +3 -0
  20. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResponse.js.map +1 -0
  21. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResult.d.ts +12 -0
  22. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResult.js +9 -0
  23. package/dist/accessi-module/Console/generated/model/consoleEntryTicketResult.js.map +1 -0
  24. package/dist/accessi-module/Console/generated/model/consoleEntryTokenResult.d.ts +12 -0
  25. package/dist/accessi-module/Console/generated/model/consoleEntryTokenResult.js +9 -0
  26. package/dist/accessi-module/Console/generated/model/consoleEntryTokenResult.js.map +1 -0
  27. package/dist/accessi-module/Console/generated/model/index.d.ts +6 -0
  28. package/dist/accessi-module/Console/generated/model/index.js +6 -0
  29. package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
  30. package/dist/accessi-module/Console/help.js +3 -2
  31. package/dist/accessi-module/Console/help.js.map +1 -1
  32. package/dist/accessi-module/Console/wiki.js +8 -3
  33. package/dist/accessi-module/Console/wiki.js.map +1 -1
  34. package/dist/accessi-module/Controllers/ConsoleEntryController.d.ts +14 -0
  35. package/dist/accessi-module/Controllers/ConsoleEntryController.js +128 -0
  36. package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -0
  37. package/dist/accessi-module/Controllers/FiltriController.d.ts +8 -1
  38. package/dist/accessi-module/Controllers/FiltriController.js +21 -2
  39. package/dist/accessi-module/Controllers/FiltriController.js.map +1 -1
  40. package/dist/accessi-module/Controllers/PermissionController.d.ts +8 -1
  41. package/dist/accessi-module/Controllers/PermissionController.js +24 -4
  42. package/dist/accessi-module/Controllers/PermissionController.js.map +1 -1
  43. package/dist/accessi-module/Controllers/UserController.d.ts +6 -0
  44. package/dist/accessi-module/Controllers/UserController.js +89 -11
  45. package/dist/accessi-module/Controllers/UserController.js.map +1 -1
  46. package/dist/accessi-module/Dtos/ConsoleEntryDtos.d.ts +24 -0
  47. package/dist/accessi-module/Dtos/ConsoleEntryDtos.js +77 -0
  48. package/dist/accessi-module/Dtos/ConsoleEntryDtos.js.map +1 -0
  49. package/dist/accessi-module/Services/UserService/UserService.d.ts +10 -0
  50. package/dist/accessi-module/Services/UserService/UserService.js +67 -12
  51. package/dist/accessi-module/Services/UserService/UserService.js.map +1 -1
  52. package/dist/accessi-module/federated-auth/FederatedAuthController.d.ts +10 -1
  53. package/dist/accessi-module/federated-auth/FederatedAuthController.js +50 -10
  54. package/dist/accessi-module/federated-auth/FederatedAuthController.js.map +1 -1
  55. package/dist/accessi-module/federated-auth/FederatedAuthService.d.ts +2 -0
  56. package/dist/accessi-module/federated-auth/FederatedAuthService.js +10 -0
  57. package/dist/accessi-module/federated-auth/FederatedAuthService.js.map +1 -1
  58. package/dist/accessi-module/security/accessControl.d.ts +17 -0
  59. package/dist/accessi-module/security/accessControl.js +23 -0
  60. package/dist/accessi-module/security/accessControl.js.map +1 -1
  61. package/dist/accessi-module/security/consoleEntry.d.ts +16 -0
  62. package/dist/accessi-module/security/consoleEntry.js +50 -0
  63. package/dist/accessi-module/security/consoleEntry.js.map +1 -0
  64. package/dist/accessi-module/security/publicAuthRateLimit.d.ts +1 -1
  65. package/dist/accessi-module/security/publicAuthRateLimit.js +1 -0
  66. package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
  67. package/dist/emilsoftware-utilities-2.0.0-dev.43.tgz +0 -0
  68. package/package.json +1 -1
  69. package/dist/emilsoftware-utilities-2.0.0-dev.42.tgz +0 -0
@@ -57,6 +57,7 @@ export interface PublicAuthRateLimitOptions {
57
57
  twoFactorVerify?: PublicAuthRateLimitRuleOptions;
58
58
  twoFactorResend?: PublicAuthRateLimitRuleOptions;
59
59
  adminBootstrap?: PublicAuthRateLimitRuleOptions;
60
+ consoleEntry?: PublicAuthRateLimitRuleOptions;
60
61
  }
61
62
  /**
62
63
  * Bootstrap del primo admin. Se abilitato, Accessi genera un token casuale a ogni avvio del
@@ -28,6 +28,7 @@ const AccessiDatabaseUpdater_1 = require("./database-updates/AccessiDatabaseUpda
28
28
  const FederatedAuthService_1 = require("./federated-auth/FederatedAuthService");
29
29
  const FederatedAuthController_1 = require("./federated-auth/FederatedAuthController");
30
30
  const AccessiConsoleController_1 = require("./Controllers/AccessiConsoleController");
31
+ const ConsoleEntryController_1 = require("./Controllers/ConsoleEntryController");
31
32
  const ServiceTokenController_1 = require("./Controllers/ServiceTokenController");
32
33
  const ServiceTokenService_1 = require("./Services/ServiceTokenService/ServiceTokenService");
33
34
  const serviceTokenGuard_1 = require("./security/serviceTokenGuard");
@@ -97,6 +98,7 @@ exports.AccessiModule = AccessiModule = AccessiModule_1 = __decorate([
97
98
  ConfiguratorController_1.ConfiguratorController,
98
99
  FederatedAuthController_1.FederatedAuthController,
99
100
  AccessiConsoleController_1.AccessiConsoleController,
101
+ ConsoleEntryController_1.ConsoleEntryController,
100
102
  ServiceTokenController_1.ServiceTokenController,
101
103
  SchemaController_1.SchemaController,
102
104
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"AccessiModule.js","sourceRoot":"","sources":["../../src/accessi-module/AccessiModule.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,mFAAgF;AAQhF,2CAA+D;AAC/D,oEAAiE;AACjE,uEAAoE;AACpE,sFAAmF;AACnF,oEAAiE;AACjE,mEAAgE;AAChE,iEAA8D;AAC9D,6EAA0E;AAC1E,iEAA8D;AAC9D,0EAAuE;AACvE,qEAAkE;AAClE,iFAA8E;AAC9E,4FAAyF;AACzF,qDAAoD;AACpD,kEAAsE;AACtE,sFAAmF;AACnF,gFAA6E;AAC7E,sFAAmF;AACnF,qFAAkF;AAClF,iFAA8E;AAC9E,4FAAyF;AACzF,oEAAiE;AACjE,qEAAkE;AAClE,4FAAyF;AACzF,kGAA+F;AAC/F,sEAAsE;AAuM/D,IAAM,aAAa,qBAAnB,MAAM,aAAa;IACxB;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,CAAC,OAAuB;QACpC,yFAAyF;QACzF,IAAA,0CAAqB,EAAC,OAAO,CAAC,CAAC;QAC/B,OAAO;YACL,MAAM,EAAE,eAAa;YACrB,SAAS,EAAE;gBACT;oBACE,OAAO,EAAE,iBAAiB;oBAC1B,QAAQ,EAAE,OAAO;iBAClB;gBACD,yBAAW;gBACX,mCAAgB;gBAChB,yBAAW;gBACX,2BAAY;gBACZ,qCAAiB;gBACjB,6BAAa;gBACb,yCAAmB;gBACnB,6BAAc;gBACd,wCAAsB;gBACtB,+CAAsB;gBACtB,2CAAoB;gBACpB,yCAAmB;gBACnB,qCAAiB,EAAE,yCAAmB,EAAE,6CAAqB;aAAC;YAChE,OAAO,EAAE;gBACP,iBAAiB;gBACjB,yBAAW;gBACX,yBAAW;gBACX,2BAAY;gBACZ,qCAAiB;gBACjB,6BAAa;gBACb,yCAAmB;gBACnB,6BAAc;gBACd,wCAAsB;gBACtB,2CAAoB;gBACpB,yCAAmB;gBACnB,qCAAiB,EAAE,yCAAmB,EAAE,6CAAqB;aAAC;SACjE,CAAC;IACJ,CAAC;CACF,CAAA;AA9CY,sCAAa;wBAAb,aAAa;IAjBzB,IAAA,eAAM,GAAE;IACR,IAAA,eAAM,EAAC;QACN,WAAW,EAAE;YACX,iCAAe;YACf,+BAAc;YACd,2CAAoB;YACpB,+BAAc;YACd,mCAAgB;YAChB,+CAAsB;YACtB,iDAAuB;YACvB,mDAAwB;YACxB,+CAAsB;YACtB,mCAAgB;SACjB;QACD,SAAS,EAAE,CAAC,yBAAW,EAAE,mCAAgB,EAAE,yBAAW,EAAE,2BAAY,EAAE,qCAAiB,EAAE,6BAAa,EAAE,yCAAmB,EAAE,6BAAc,EAAE,wCAAsB,EAAE,+CAAsB,EAAE,2CAAoB,EAAE,yCAAmB,EAAE,qCAAiB,EAAE,yCAAmB,EAAE,6CAAqB,CAAC;QACtS,OAAO,EAAE,CAAC,yBAAW,EAAE,yBAAW,EAAE,2BAAY,EAAE,qCAAiB,EAAE,6BAAa,EAAE,yCAAmB,EAAE,6BAAc,EAAE,wCAAsB,EAAE,2CAAoB,EAAE,yCAAmB,EAAE,qCAAiB,EAAE,yCAAmB,EAAE,6CAAqB,CAAC;KAC3P,CAAC;GACW,aAAa,CA8CzB","sourcesContent":["import { TwoFactorService } from './Services/TwoFactorService/TwoFactorService';\n/**\n * Modulo che gestisce le operazioni di accesso degli utenti, incluse le rotte, il controller e il modello.\n *\n * @module AccessiModule\n * @author mttdev382\n */\nimport { Options } from 'node-firebird';\nimport { DynamicModule, Global, Module } from '@nestjs/common';\nimport { AuthService } from './Services/AuthService/AuthService';\nimport { EmailService } from './Services/EmailService/EmailService';\nimport { PermissionService } from './Services/PermissionService/PermissionService';\nimport { UserService } from './Services/UserService/UserService';\nimport { EmailController } from './Controllers/EmailController';\nimport { AuthController } from './Controllers/AuthController';\nimport { PermissionController } from './Controllers/PermissionController';\nimport { UserController } from './Controllers/UserController';\nimport { FiltriService } from './Services/FiltriService/FiltriService';\nimport { FiltriController } from './Controllers/FiltriController';\nimport { ConfiguratorController } from './Controllers/ConfiguratorController';\nimport { ConfiguratorService } from './Services/ConfiguratorService/ConfiguratorService';\nimport { JwtSimpleGuard } from './jwt/jwt.strategy';\nimport { AuthenticateGenService } from './middleware/authenticateGen';\nimport { AccessiDatabaseUpdater } from './database-updates/AccessiDatabaseUpdater';\nimport { FederatedAuthService } from './federated-auth/FederatedAuthService';\nimport { FederatedAuthController } from './federated-auth/FederatedAuthController';\nimport { AccessiConsoleController } from './Controllers/AccessiConsoleController';\nimport { ServiceTokenController } from './Controllers/ServiceTokenController';\nimport { ServiceTokenService } from './Services/ServiceTokenService/ServiceTokenService';\nimport { ServiceTokenGuard } from './security/serviceTokenGuard';\nimport { SchemaController } from './Controllers/SchemaController';\nimport { SchemaExportService } from './Services/SchemaExportService/SchemaExportService';\nimport { AdminBootstrapService } from './Services/AdminBootstrapService/AdminBootstrapService';\nimport { assertEmailConfigured } from './security/emailConfiguration';\n\n/** JWT emesso da Accessi dopo un login locale o SSO. Non riutilizzare il segreto del provider SSO. */\nexport interface JwtOptions {\n /** Segreto simmetrico per firmare e verificare i JWT Accessi; deve essere unico per ambiente. */\n secret: string;\n /** Durata compatibile con `jsonwebtoken`, per esempio `24h` o `3600s`. */\n expiresIn: string;\n}\n\n/** Parametri SMTP usati esclusivamente per impostazione e reset della password. */\nexport interface EmailOptions {\n host: string;\n port: number;\n secure: boolean;\n requireTLS : boolean;\n tls: {\n rejectUnauthorized: boolean,\n }\n from: string;\n auth: {\n user: string;\n pass: string;\n };\n}\n\n/** Configura campi applicativi aggiuntivi restituiti insieme al profilo Accessi. */\nexport interface ExtensionFieldsOptions {\n /** Connessione al database contenente la tabella estesa. */\n databaseOptions: Options;\n /** Chiave con cui i dati saranno esposti nella risposta. */\n objectKey: string;\n /** Tabella applicativa contenente i dati estesi. */\n tableName: string;\n /** Colonne consentite; non valorizzarle mai con input HTTP. */\n tableFields: string[];\n /** Colonna correlata a `UTENTI.CODUTE`. */\n tableJoinFieldName: string;\n}\n\n/** Limite per una singola categoria di endpoint pubblici. */\nexport interface PublicAuthRateLimitRuleOptions {\n windowMs: number;\n maxAttempts: number;\n}\n\n/** Protezione in-memory degli endpoint pubblici. In cluster aggiungere un limite anche al gateway. */\nexport interface PublicAuthRateLimitOptions {\n enabled?: boolean;\n login?: PublicAuthRateLimitRuleOptions;\n register?: PublicAuthRateLimitRuleOptions;\n passwordResetEmail?: PublicAuthRateLimitRuleOptions;\n passwordResetConfirm?: PublicAuthRateLimitRuleOptions;\n getUserByToken?: PublicAuthRateLimitRuleOptions;\n twoFactorVerify?: PublicAuthRateLimitRuleOptions;\n twoFactorResend?: PublicAuthRateLimitRuleOptions;\n adminBootstrap?: PublicAuthRateLimitRuleOptions;\n}\n\n/**\n * Bootstrap del primo admin. Se abilitato, Accessi genera un token casuale a ogni avvio del\n * processo, lo stampa nel log e lo accetta sull'endpoint pubblico `POST /accessi/auth/bootstrap-admin`.\n * Serve a creare il primo utente admin senza email di conferma e senza un admin gia esistente.\n */\nexport interface AdminBootstrapOptions {\n /** Abilita il bootstrap admin. Default `false`. */\n enabled?: boolean;\n /** Crea l'utente come superutente oltre che admin. Default `true`. */\n superUser?: boolean;\n}\n\n/** Controls the legacy public username/password registration endpoint. Disabled by default. */\nexport interface PublicRegistrationOptions {\n /** Enables POST /accessi/user/register. Applications that used it before must opt in explicitly. */\n enabled?: boolean;\n}\n\n/** Configurazione dei token di servizio (macchina-a-macchina, senza utente). */\nexport interface ServiceTokenOptions {\n /** Abilita gli endpoint amministrativi dei service token. Default `true`. */\n enabled?: boolean;\n /** Durata predefinita in giorni quando non e indicata una scadenza esplicita. Default: nessuna scadenza. */\n defaultTtlDays?: number;\n}\n\n/** Cosa fare quando uno script gia applicato presenta un checksum diverso (file modificato). */\nexport type CatalogScriptChecksumPolicy = 'error' | 'warn' | 'reapply';\n\n/**\n * Catalog migrations applicative (menu, ruoli, tipi, cataloghi). Gli script `.sql` vivono nel\n * backend ospitante (versionati in repository) e vengono eseguiti da Accessi dopo la\n * riconciliazione dello schema. Ogni script e tracciato nella tabella `ACCESSI_CATALOG_SCRIPT`,\n * quindi viene applicato una sola volta per database (idempotenza lato DB).\n */\nexport interface CatalogScriptsOptions {\n /** Abilita l'esecuzione automatica al bootstrap. Default `false`. */\n enabled?: boolean;\n /** Cartella contenente gli script `.sql` (assoluta o relativa alla working directory). */\n folder: string;\n /** Cosa fare se il checksum di uno script applicato e cambiato. Default `error`. */\n onChecksumMismatch?: CatalogScriptChecksumPolicy;\n /** Se `true` (default) un errore interrompe l'esecuzione e non avanza allo script successivo. */\n stopOnError?: boolean;\n /** Globi/estensioni inclusi. Default: tutti i `.sql`. */\n include?: string[];\n /** Numero massimo di script eseguiti in un singolo run (protezione operativa). Default: illimitato. */\n maxScriptsPerRun?: number;\n /**\n * Permette statement DDL/schema negli script. Default `false`: le catalog migrations possono\n * modificare solo righe. Sconsigliato: lo schema e di competenza di `db:update:accessi`.\n */\n allowDdl?: boolean;\n}\n\n/**\n * Enables Accessi's provider-agnostic federated identity capability.\n *\n * Accessi never validates Azure, Google, Apple, SAML, or other provider tokens.\n * The hosting backend must validate its own SSO flow and pass only a verified\n * `{ provider, subject }` identity to FederatedAuthService.\n */\nexport interface FederatedAuthenticationOptions {\n /** Attiva le API SSO generiche; con `false` il comportamento password storico rimane invariato. */\n enabled?: boolean;\n /** @deprecated Schema management is centralized in autoUpdateDatabase. This legacy flag no longer starts separate SSO migrations. */\n autoUpdateSchema?: boolean;\n /** Allows a backend to create a user from a verified external identity without a master user. Default false. */\n allowSelfRegistration?: boolean;\n}\n\n/**\n * Configurazione completa del modulo Accessi, fornita esclusivamente dal backend ospitante.\n * Segreti JWT, SMTP, database e le configurazioni tecniche dei provider SSO non devono mai raggiungere il browser.\n */\nexport interface AccessiOptions {\n /** Connessione Firebird alla base dati Accessi. */\n databaseOptions: Options;\n /**\n * Basepath del sito es: 'http://www.il-mio-sito.it/nome-progetto(se c'è)'\n */\n confirmationEmailUrl: string;\n /**\n * Percorso della pagina di reset personalizzata es. http://localhost:4200/#/admin/reset-password\n * N.B si sostituisce al confirmationMailURl\n */\n customResetPage?: string;\n /** URL di ritorno della pagina standard di reset dopo il completamento. */\n confirmationEmailReturnUrl: string;\n /** Prefisso applicativo facoltativo trasmesso alla pagina standard di reset. */\n confirmationEmailPrefix?: string;\n /** Chiave riservata alla compatibilita e alla migrazione delle password legacy cifrate. */\n encryptionKey: string;\n /** Abilita gli utenti fittizi `admin` e `demo` solo fuori produzione. */\n mockDemoUser: boolean;\n /** Attiva il controllo della scadenza password durante il login locale. */\n passwordExpiration?: boolean;\n /** Giorni di validita della password; quando assente Accessi usa 90 giorni. */\n passwordExpirationDays?: number;\n /** Esegue al bootstrap la conversione non distruttiva delle password legacy. Default `true`. */\n legacyPasswordMigrationOnStartup?: boolean;\n /** Riconcilia lo schema al bootstrap (default `true`). Con `false` verifica comunque la compatibilita in sola lettura e blocca avvii incompatibili. */\n autoUpdateDatabase?: boolean;\n /** Parametri JWT dei token emessi da Accessi. */\n jwtOptions: JwtOptions;\n /** Parametri SMTP per reset e attivazione password. */\n emailOptions: EmailOptions;\n publicAuthRateLimit?: PublicAuthRateLimitOptions;\n /** Public local registration is deliberately opt-in. Existing applications must set enabled: true to retain it. */\n publicRegistration?: PublicRegistrationOptions;\n /** Optional provider-agnostic SSO support. Provider-specific configuration remains in the hosting backend. */\n federatedAuthentication?: FederatedAuthenticationOptions;\n extensionFieldsOptions?: ExtensionFieldsOptions[];\n /** Token di servizio per chiamate tecniche macchina-a-macchina. Abilitati di default. */\n serviceTokens?: ServiceTokenOptions;\n /** Catalog migrations applicative eseguite dopo la riconciliazione dello schema. */\n catalogScripts?: CatalogScriptsOptions;\n /** Bootstrap del primo admin tramite token casuale stampato a ogni avvio. */\n adminBootstrap?: AdminBootstrapOptions;\n /** Base URL pubblica usata solo per loggare gli indirizzi (console, swagger, API). */\n publicBaseUrl?: string;\n}\n\n@Global()\n@Module({\n controllers: [\n EmailController,\n AuthController,\n PermissionController,\n UserController,\n FiltriController,\n ConfiguratorController,\n FederatedAuthController,\n AccessiConsoleController,\n ServiceTokenController,\n SchemaController,\n ],\n providers: [AuthService, TwoFactorService, UserService, EmailService, PermissionService, FiltriService, ConfiguratorService, JwtSimpleGuard, AuthenticateGenService, AccessiDatabaseUpdater, FederatedAuthService, ServiceTokenService, ServiceTokenGuard, SchemaExportService, AdminBootstrapService],\n exports: [AuthService, UserService, EmailService, PermissionService, FiltriService, ConfiguratorService, JwtSimpleGuard, AuthenticateGenService, FederatedAuthService, ServiceTokenService, ServiceTokenGuard, SchemaExportService, AdminBootstrapService],\n})\nexport class AccessiModule {\n /**\n * Registra Accessi nel contenitore Nest del backend ospitante.\n *\n * Usare questo metodo una sola volta nella composizione dell'applicazione. Per un backend\n * Express esistente usare invece `initializeAccessiModule`, che crea e monta l'host Nest interno.\n * La configurazione viene registrata con il token DI `ACCESSI_OPTIONS` ed e condivisa dai servizi.\n */\n static forRoot(options: AccessiOptions): DynamicModule {\n // Il servizio email e obbligatorio: reset password, 2FA e creazione utente ne dipendono.\n assertEmailConfigured(options);\n return {\n module: AccessiModule,\n providers: [\n {\n provide: 'ACCESSI_OPTIONS',\n useValue: options,\n },\n AuthService,\n TwoFactorService,\n UserService,\n EmailService,\n PermissionService,\n FiltriService,\n ConfiguratorService,\n JwtSimpleGuard,\n AuthenticateGenService,\n AccessiDatabaseUpdater,\n FederatedAuthService,\n ServiceTokenService,\n ServiceTokenGuard, SchemaExportService, AdminBootstrapService],\n exports: [\n 'ACCESSI_OPTIONS',\n AuthService,\n UserService,\n EmailService,\n PermissionService,\n FiltriService,\n ConfiguratorService,\n JwtSimpleGuard,\n AuthenticateGenService,\n FederatedAuthService,\n ServiceTokenService,\n ServiceTokenGuard, SchemaExportService, AdminBootstrapService],\n };\n }\n}\n"]}
1
+ {"version":3,"file":"AccessiModule.js","sourceRoot":"","sources":["../../src/accessi-module/AccessiModule.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,mFAAgF;AAQhF,2CAA+D;AAC/D,oEAAiE;AACjE,uEAAoE;AACpE,sFAAmF;AACnF,oEAAiE;AACjE,mEAAgE;AAChE,iEAA8D;AAC9D,6EAA0E;AAC1E,iEAA8D;AAC9D,0EAAuE;AACvE,qEAAkE;AAClE,iFAA8E;AAC9E,4FAAyF;AACzF,qDAAoD;AACpD,kEAAsE;AACtE,sFAAmF;AACnF,gFAA6E;AAC7E,sFAAmF;AACnF,qFAAkF;AAClF,iFAA8E;AAC9E,iFAA8E;AAC9E,4FAAyF;AACzF,oEAAiE;AACjE,qEAAkE;AAClE,4FAAyF;AACzF,kGAA+F;AAC/F,sEAAsE;AAyM/D,IAAM,aAAa,qBAAnB,MAAM,aAAa;IACxB;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,CAAC,OAAuB;QACpC,yFAAyF;QACzF,IAAA,0CAAqB,EAAC,OAAO,CAAC,CAAC;QAC/B,OAAO;YACL,MAAM,EAAE,eAAa;YACrB,SAAS,EAAE;gBACT;oBACE,OAAO,EAAE,iBAAiB;oBAC1B,QAAQ,EAAE,OAAO;iBAClB;gBACD,yBAAW;gBACX,mCAAgB;gBAChB,yBAAW;gBACX,2BAAY;gBACZ,qCAAiB;gBACjB,6BAAa;gBACb,yCAAmB;gBACnB,6BAAc;gBACd,wCAAsB;gBACtB,+CAAsB;gBACtB,2CAAoB;gBACpB,yCAAmB;gBACnB,qCAAiB,EAAE,yCAAmB,EAAE,6CAAqB;aAAC;YAChE,OAAO,EAAE;gBACP,iBAAiB;gBACjB,yBAAW;gBACX,yBAAW;gBACX,2BAAY;gBACZ,qCAAiB;gBACjB,6BAAa;gBACb,yCAAmB;gBACnB,6BAAc;gBACd,wCAAsB;gBACtB,2CAAoB;gBACpB,yCAAmB;gBACnB,qCAAiB,EAAE,yCAAmB,EAAE,6CAAqB;aAAC;SACjE,CAAC;IACJ,CAAC;CACF,CAAA;AA9CY,sCAAa;wBAAb,aAAa;IAlBzB,IAAA,eAAM,GAAE;IACR,IAAA,eAAM,EAAC;QACN,WAAW,EAAE;YACX,iCAAe;YACf,+BAAc;YACd,2CAAoB;YACpB,+BAAc;YACd,mCAAgB;YAChB,+CAAsB;YACtB,iDAAuB;YACvB,mDAAwB;YACxB,+CAAsB;YACtB,+CAAsB;YACtB,mCAAgB;SACjB;QACD,SAAS,EAAE,CAAC,yBAAW,EAAE,mCAAgB,EAAE,yBAAW,EAAE,2BAAY,EAAE,qCAAiB,EAAE,6BAAa,EAAE,yCAAmB,EAAE,6BAAc,EAAE,wCAAsB,EAAE,+CAAsB,EAAE,2CAAoB,EAAE,yCAAmB,EAAE,qCAAiB,EAAE,yCAAmB,EAAE,6CAAqB,CAAC;QACtS,OAAO,EAAE,CAAC,yBAAW,EAAE,yBAAW,EAAE,2BAAY,EAAE,qCAAiB,EAAE,6BAAa,EAAE,yCAAmB,EAAE,6BAAc,EAAE,wCAAsB,EAAE,2CAAoB,EAAE,yCAAmB,EAAE,qCAAiB,EAAE,yCAAmB,EAAE,6CAAqB,CAAC;KAC3P,CAAC;GACW,aAAa,CA8CzB","sourcesContent":["import { TwoFactorService } from './Services/TwoFactorService/TwoFactorService';\n/**\n * Modulo che gestisce le operazioni di accesso degli utenti, incluse le rotte, il controller e il modello.\n *\n * @module AccessiModule\n * @author mttdev382\n */\nimport { Options } from 'node-firebird';\nimport { DynamicModule, Global, Module } from '@nestjs/common';\nimport { AuthService } from './Services/AuthService/AuthService';\nimport { EmailService } from './Services/EmailService/EmailService';\nimport { PermissionService } from './Services/PermissionService/PermissionService';\nimport { UserService } from './Services/UserService/UserService';\nimport { EmailController } from './Controllers/EmailController';\nimport { AuthController } from './Controllers/AuthController';\nimport { PermissionController } from './Controllers/PermissionController';\nimport { UserController } from './Controllers/UserController';\nimport { FiltriService } from './Services/FiltriService/FiltriService';\nimport { FiltriController } from './Controllers/FiltriController';\nimport { ConfiguratorController } from './Controllers/ConfiguratorController';\nimport { ConfiguratorService } from './Services/ConfiguratorService/ConfiguratorService';\nimport { JwtSimpleGuard } from './jwt/jwt.strategy';\nimport { AuthenticateGenService } from './middleware/authenticateGen';\nimport { AccessiDatabaseUpdater } from './database-updates/AccessiDatabaseUpdater';\nimport { FederatedAuthService } from './federated-auth/FederatedAuthService';\nimport { FederatedAuthController } from './federated-auth/FederatedAuthController';\nimport { AccessiConsoleController } from './Controllers/AccessiConsoleController';\nimport { ConsoleEntryController } from './Controllers/ConsoleEntryController';\nimport { ServiceTokenController } from './Controllers/ServiceTokenController';\nimport { ServiceTokenService } from './Services/ServiceTokenService/ServiceTokenService';\nimport { ServiceTokenGuard } from './security/serviceTokenGuard';\nimport { SchemaController } from './Controllers/SchemaController';\nimport { SchemaExportService } from './Services/SchemaExportService/SchemaExportService';\nimport { AdminBootstrapService } from './Services/AdminBootstrapService/AdminBootstrapService';\nimport { assertEmailConfigured } from './security/emailConfiguration';\n\n/** JWT emesso da Accessi dopo un login locale o SSO. Non riutilizzare il segreto del provider SSO. */\nexport interface JwtOptions {\n /** Segreto simmetrico per firmare e verificare i JWT Accessi; deve essere unico per ambiente. */\n secret: string;\n /** Durata compatibile con `jsonwebtoken`, per esempio `24h` o `3600s`. */\n expiresIn: string;\n}\n\n/** Parametri SMTP usati esclusivamente per impostazione e reset della password. */\nexport interface EmailOptions {\n host: string;\n port: number;\n secure: boolean;\n requireTLS : boolean;\n tls: {\n rejectUnauthorized: boolean,\n }\n from: string;\n auth: {\n user: string;\n pass: string;\n };\n}\n\n/** Configura campi applicativi aggiuntivi restituiti insieme al profilo Accessi. */\nexport interface ExtensionFieldsOptions {\n /** Connessione al database contenente la tabella estesa. */\n databaseOptions: Options;\n /** Chiave con cui i dati saranno esposti nella risposta. */\n objectKey: string;\n /** Tabella applicativa contenente i dati estesi. */\n tableName: string;\n /** Colonne consentite; non valorizzarle mai con input HTTP. */\n tableFields: string[];\n /** Colonna correlata a `UTENTI.CODUTE`. */\n tableJoinFieldName: string;\n}\n\n/** Limite per una singola categoria di endpoint pubblici. */\nexport interface PublicAuthRateLimitRuleOptions {\n windowMs: number;\n maxAttempts: number;\n}\n\n/** Protezione in-memory degli endpoint pubblici. In cluster aggiungere un limite anche al gateway. */\nexport interface PublicAuthRateLimitOptions {\n enabled?: boolean;\n login?: PublicAuthRateLimitRuleOptions;\n register?: PublicAuthRateLimitRuleOptions;\n passwordResetEmail?: PublicAuthRateLimitRuleOptions;\n passwordResetConfirm?: PublicAuthRateLimitRuleOptions;\n getUserByToken?: PublicAuthRateLimitRuleOptions;\n twoFactorVerify?: PublicAuthRateLimitRuleOptions;\n twoFactorResend?: PublicAuthRateLimitRuleOptions;\n adminBootstrap?: PublicAuthRateLimitRuleOptions;\n consoleEntry?: PublicAuthRateLimitRuleOptions;\n}\n\n/**\n * Bootstrap del primo admin. Se abilitato, Accessi genera un token casuale a ogni avvio del\n * processo, lo stampa nel log e lo accetta sull'endpoint pubblico `POST /accessi/auth/bootstrap-admin`.\n * Serve a creare il primo utente admin senza email di conferma e senza un admin gia esistente.\n */\nexport interface AdminBootstrapOptions {\n /** Abilita il bootstrap admin. Default `false`. */\n enabled?: boolean;\n /** Crea l'utente come superutente oltre che admin. Default `true`. */\n superUser?: boolean;\n}\n\n/** Controls the legacy public username/password registration endpoint. Disabled by default. */\nexport interface PublicRegistrationOptions {\n /** Enables POST /accessi/user/register. Applications that used it before must opt in explicitly. */\n enabled?: boolean;\n}\n\n/** Configurazione dei token di servizio (macchina-a-macchina, senza utente). */\nexport interface ServiceTokenOptions {\n /** Abilita gli endpoint amministrativi dei service token. Default `true`. */\n enabled?: boolean;\n /** Durata predefinita in giorni quando non e indicata una scadenza esplicita. Default: nessuna scadenza. */\n defaultTtlDays?: number;\n}\n\n/** Cosa fare quando uno script gia applicato presenta un checksum diverso (file modificato). */\nexport type CatalogScriptChecksumPolicy = 'error' | 'warn' | 'reapply';\n\n/**\n * Catalog migrations applicative (menu, ruoli, tipi, cataloghi). Gli script `.sql` vivono nel\n * backend ospitante (versionati in repository) e vengono eseguiti da Accessi dopo la\n * riconciliazione dello schema. Ogni script e tracciato nella tabella `ACCESSI_CATALOG_SCRIPT`,\n * quindi viene applicato una sola volta per database (idempotenza lato DB).\n */\nexport interface CatalogScriptsOptions {\n /** Abilita l'esecuzione automatica al bootstrap. Default `false`. */\n enabled?: boolean;\n /** Cartella contenente gli script `.sql` (assoluta o relativa alla working directory). */\n folder: string;\n /** Cosa fare se il checksum di uno script applicato e cambiato. Default `error`. */\n onChecksumMismatch?: CatalogScriptChecksumPolicy;\n /** Se `true` (default) un errore interrompe l'esecuzione e non avanza allo script successivo. */\n stopOnError?: boolean;\n /** Globi/estensioni inclusi. Default: tutti i `.sql`. */\n include?: string[];\n /** Numero massimo di script eseguiti in un singolo run (protezione operativa). Default: illimitato. */\n maxScriptsPerRun?: number;\n /**\n * Permette statement DDL/schema negli script. Default `false`: le catalog migrations possono\n * modificare solo righe. Sconsigliato: lo schema e di competenza di `db:update:accessi`.\n */\n allowDdl?: boolean;\n}\n\n/**\n * Enables Accessi's provider-agnostic federated identity capability.\n *\n * Accessi never validates Azure, Google, Apple, SAML, or other provider tokens.\n * The hosting backend must validate its own SSO flow and pass only a verified\n * `{ provider, subject }` identity to FederatedAuthService.\n */\nexport interface FederatedAuthenticationOptions {\n /** Attiva le API SSO generiche; con `false` il comportamento password storico rimane invariato. */\n enabled?: boolean;\n /** @deprecated Schema management is centralized in autoUpdateDatabase. This legacy flag no longer starts separate SSO migrations. */\n autoUpdateSchema?: boolean;\n /** Allows a backend to create a user from a verified external identity without a master user. Default false. */\n allowSelfRegistration?: boolean;\n}\n\n/**\n * Configurazione completa del modulo Accessi, fornita esclusivamente dal backend ospitante.\n * Segreti JWT, SMTP, database e le configurazioni tecniche dei provider SSO non devono mai raggiungere il browser.\n */\nexport interface AccessiOptions {\n /** Connessione Firebird alla base dati Accessi. */\n databaseOptions: Options;\n /**\n * Basepath del sito es: 'http://www.il-mio-sito.it/nome-progetto(se c'è)'\n */\n confirmationEmailUrl: string;\n /**\n * Percorso della pagina di reset personalizzata es. http://localhost:4200/#/admin/reset-password\n * N.B si sostituisce al confirmationMailURl\n */\n customResetPage?: string;\n /** URL di ritorno della pagina standard di reset dopo il completamento. */\n confirmationEmailReturnUrl: string;\n /** Prefisso applicativo facoltativo trasmesso alla pagina standard di reset. */\n confirmationEmailPrefix?: string;\n /** Chiave riservata alla compatibilita e alla migrazione delle password legacy cifrate. */\n encryptionKey: string;\n /** Abilita gli utenti fittizi `admin` e `demo` solo fuori produzione. */\n mockDemoUser: boolean;\n /** Attiva il controllo della scadenza password durante il login locale. */\n passwordExpiration?: boolean;\n /** Giorni di validita della password; quando assente Accessi usa 90 giorni. */\n passwordExpirationDays?: number;\n /** Esegue al bootstrap la conversione non distruttiva delle password legacy. Default `true`. */\n legacyPasswordMigrationOnStartup?: boolean;\n /** Riconcilia lo schema al bootstrap (default `true`). Con `false` verifica comunque la compatibilita in sola lettura e blocca avvii incompatibili. */\n autoUpdateDatabase?: boolean;\n /** Parametri JWT dei token emessi da Accessi. */\n jwtOptions: JwtOptions;\n /** Parametri SMTP per reset e attivazione password. */\n emailOptions: EmailOptions;\n publicAuthRateLimit?: PublicAuthRateLimitOptions;\n /** Public local registration is deliberately opt-in. Existing applications must set enabled: true to retain it. */\n publicRegistration?: PublicRegistrationOptions;\n /** Optional provider-agnostic SSO support. Provider-specific configuration remains in the hosting backend. */\n federatedAuthentication?: FederatedAuthenticationOptions;\n extensionFieldsOptions?: ExtensionFieldsOptions[];\n /** Token di servizio per chiamate tecniche macchina-a-macchina. Abilitati di default. */\n serviceTokens?: ServiceTokenOptions;\n /** Catalog migrations applicative eseguite dopo la riconciliazione dello schema. */\n catalogScripts?: CatalogScriptsOptions;\n /** Bootstrap del primo admin tramite token casuale stampato a ogni avvio. */\n adminBootstrap?: AdminBootstrapOptions;\n /** Base URL pubblica usata solo per loggare gli indirizzi (console, swagger, API). */\n publicBaseUrl?: string;\n}\n\n@Global()\n@Module({\n controllers: [\n EmailController,\n AuthController,\n PermissionController,\n UserController,\n FiltriController,\n ConfiguratorController,\n FederatedAuthController,\n AccessiConsoleController,\n ConsoleEntryController,\n ServiceTokenController,\n SchemaController,\n ],\n providers: [AuthService, TwoFactorService, UserService, EmailService, PermissionService, FiltriService, ConfiguratorService, JwtSimpleGuard, AuthenticateGenService, AccessiDatabaseUpdater, FederatedAuthService, ServiceTokenService, ServiceTokenGuard, SchemaExportService, AdminBootstrapService],\n exports: [AuthService, UserService, EmailService, PermissionService, FiltriService, ConfiguratorService, JwtSimpleGuard, AuthenticateGenService, FederatedAuthService, ServiceTokenService, ServiceTokenGuard, SchemaExportService, AdminBootstrapService],\n})\nexport class AccessiModule {\n /**\n * Registra Accessi nel contenitore Nest del backend ospitante.\n *\n * Usare questo metodo una sola volta nella composizione dell'applicazione. Per un backend\n * Express esistente usare invece `initializeAccessiModule`, che crea e monta l'host Nest interno.\n * La configurazione viene registrata con il token DI `ACCESSI_OPTIONS` ed e condivisa dai servizi.\n */\n static forRoot(options: AccessiOptions): DynamicModule {\n // Il servizio email e obbligatorio: reset password, 2FA e creazione utente ne dipendono.\n assertEmailConfigured(options);\n return {\n module: AccessiModule,\n providers: [\n {\n provide: 'ACCESSI_OPTIONS',\n useValue: options,\n },\n AuthService,\n TwoFactorService,\n UserService,\n EmailService,\n PermissionService,\n FiltriService,\n ConfiguratorService,\n JwtSimpleGuard,\n AuthenticateGenService,\n AccessiDatabaseUpdater,\n FederatedAuthService,\n ServiceTokenService,\n ServiceTokenGuard, SchemaExportService, AdminBootstrapService],\n exports: [\n 'ACCESSI_OPTIONS',\n AuthService,\n UserService,\n EmailService,\n PermissionService,\n FiltriService,\n ConfiguratorService,\n JwtSimpleGuard,\n AuthenticateGenService,\n FederatedAuthService,\n ServiceTokenService,\n ServiceTokenGuard, SchemaExportService, AdminBootstrapService],\n };\n }\n}\n"]}
@@ -429,6 +429,18 @@ var deleteUserPermanently = async (codiceUtente, options) => {
429
429
  }
430
430
  );
431
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
+ };
432
444
  var getSetUserPasswordUrl = (codiceUtente) => {
433
445
  return `/api/accessi/user/set-password/${codiceUtente}`;
434
446
  };
@@ -489,6 +501,18 @@ var updateUtente = async (codiceUtente, userDto, options) => {
489
501
  }
490
502
  );
491
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
+ };
492
516
  var getGetTipiFiltroUrl = () => {
493
517
  return `/api/accessi/filtri/tipi`;
494
518
  };
@@ -1237,8 +1261,10 @@ POST /api/accessi/auth/confirm-reset-password/{token}
1237
1261
  ["POST", "/api/accessi/user/register", "Registrazione pubblica, solo se publicRegistration.enabled."]
1238
1262
  ]
1239
1263
  ),
1240
- 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.`),
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.`),
1241
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.`),
1242
1268
  good("ts", [
1243
1269
  "await fetch('/api/accessi/user/create-managed-user', {",
1244
1270
  " method: 'POST',",
@@ -1445,10 +1471,13 @@ POST /api/accessi/auth/confirm-reset-password/{token}
1445
1471
  `SQL: esporta il DDL delle entit\xE0 Accessi (tabelle, generatori, FK, indici, check, trigger) per replicare il database.`
1446
1472
  ]),
1447
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.`),
1448
- 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.`),
1449
- 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). La regola \xE8 imposta anche dal backend: inviare un flag non consentito viene rifiutato.`),
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.`),
1450
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).`),
1451
- 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).`)
1452
1481
  ]
1453
1482
  },
1454
1483
  // -------------------------------------------------------------------------
@@ -1589,7 +1618,7 @@ function buildAiDigest() {
1589
1618
  }
1590
1619
 
1591
1620
  // src/accessi-module/Console/changelog.md
1592
- var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\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";
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";
1593
1622
 
1594
1623
  // src/accessi-module/Console/help.ts
1595
1624
  var p2 = (text) => ({ kind: "p", text });
@@ -1772,9 +1801,10 @@ var HELP = {
1772
1801
  ]
1773
1802
  },
1774
1803
  "user-gdpr": {
1775
- title: "GDPR accettato",
1804
+ title: "Consenso GDPR",
1776
1805
  blocks: [
1777
- p2(`Imposta il flag di accettazione GDPR (UTENTI.FLGGDPR). Il consenso completo con tracciamento si registra dal flusso dedicato (endpoint set-gdpr).`)
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.`)
1778
1808
  ]
1779
1809
  },
1780
1810
  "user-json": {
@@ -2548,19 +2578,48 @@ async function bootstrap() {
2548
2578
  byId("login-error").textContent = error instanceof Error ? error.message : "Sessione non valida.";
2549
2579
  }
2550
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
+ }
2551
2605
  async function showUsers() {
2552
2606
  const { users, total } = await loadUsersPage();
2553
2607
  usersTotal = total;
2554
2608
  const totalPages = Math.max(1, Math.ceil(total / USERS_PAGE_SIZE));
2555
2609
  if (usersPage >= totalPages) usersPage = totalPages - 1;
2556
- 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>
2557
2611
  <div class="pager-row pager-row-top">${pagerMarkup(totalPages)}</div>
2558
2612
  <table><thead><tr><th>Utente</th><th>Stato</th><th>Accesso</th><th>2FA</th><th>Ruoli</th><th></th></tr></thead><tbody>
2559
- ${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>'}
2560
2614
  </tbody></table><div class="pager-row pager-row-bottom">${pagerMarkup(totalPages)}</div>`);
2561
2615
  byId("new-local").onclick = () => showLocalUserForm();
2562
2616
  if (federatedAuthenticationAvailable) byId("new-sso").onclick = () => handleAction(showSsoUserForm);
2563
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
+ });
2564
2623
  document.querySelectorAll("[data-goto]").forEach((button) => {
2565
2624
  button.onclick = () => handleAction(async () => {
2566
2625
  const target = button.dataset.goto;
@@ -2589,6 +2648,36 @@ function canManageSuperFlag() {
2589
2648
  function canManageAdminFlag() {
2590
2649
  return Boolean(currentUser?.flagAdmin);
2591
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
+ }
2592
2681
  function privilegeFields(user) {
2593
2682
  const manageSuper = canManageSuperFlag();
2594
2683
  const manageAdmin = canManageAdminFlag();
@@ -2639,7 +2728,7 @@ function extendedProfileFields(user) {
2639
2728
  <label>Codice lingua ${helpDot("user-lingua")}<input name="codiceLingua" maxlength="2" value="${escapeHtml(user.codiceLingua ?? "")}"></label>
2640
2729
  <label>Avatar ${helpDot("user-avatar")}<input name="avatar" maxlength="30" value="${escapeHtml(user.avatar ?? "")}"></label>
2641
2730
  <label>Scadenza password ${helpDot("user-scadenza-password")}<input name="dataScadenzaPassword" type="date" value="${dateInputValue(user.dataScadenzaPassword)}"></label>
2642
- <label class="check"><input name="flagGdpr" type="checkbox" ${user.flagGdpr ? "checked" : ""}> GDPR accettato ${helpDot("user-gdpr")}</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>
2643
2732
  <label>Metadata JSON ${helpDot("user-json")}<textarea name="jsonMetadata" rows="3">${escapeHtml(user.jsonMetadata ?? "")}</textarea></label>
2644
2733
  </fieldset>`;
2645
2734
  }
@@ -2671,7 +2760,6 @@ function profilePayload(form, codiceUtente) {
2671
2760
  paginaDefault: optionalText(values, "paginaDefault"),
2672
2761
  jsonMetadata: optionalText(values, "jsonMetadata"),
2673
2762
  dataScadenzaPassword: optionalText(values, "dataScadenzaPassword"),
2674
- flagGdpr: fields.has("flagGdpr"),
2675
2763
  flagDueFattori: fields.has("flagDueFattori"),
2676
2764
  passwordlessLoginEnabled: fields.has("passwordlessLoginEnabled"),
2677
2765
  numRep: optionalNumber(values, "numRep"),
@@ -2692,10 +2780,11 @@ function profilePayload(form, codiceUtente) {
2692
2780
  return payload;
2693
2781
  }
2694
2782
  function passwordFields() {
2695
- return `<fieldset><legend>Password</legend>
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>
2696
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>
2697
- <button type="button" id="set-password" class="secondary">Imposta password</button>
2698
- </fieldset>`;
2786
+ <button type="submit" id="set-password" class="secondary">Imposta password</button>
2787
+ </form>`;
2699
2788
  }
2700
2789
  function showLocalUserForm() {
2701
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>
@@ -2735,7 +2824,7 @@ async function showSsoUserForm() {
2735
2824
  };
2736
2825
  await createFederatedUser(request);
2737
2826
  showNotice("Utente SSO creato e collegamento registrato.");
2738
- await show("ssoUsers");
2827
+ await show(currentUser?.flagAdmin ? "ssoUsers" : "users");
2739
2828
  });
2740
2829
  };
2741
2830
  }
@@ -2833,9 +2922,11 @@ async function showUser(rawCode) {
2833
2922
  const linked = federatedAuthenticationAvailable ? result(await getFederatedIdentities(codiceUtente)) : [];
2834
2923
  const identitySection = federatedAuthenticationAvailable ? federatedIdentitySection(linked, providerList) : "";
2835
2924
  const defaultUserTab = federatedAuthenticationAvailable ? "sso" : "profile";
2836
- 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")}${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">
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">
2837
2927
  ${identitySection}
2838
- <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)}${passwordFields()}<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()}
2839
2930
  ${roleAssignmentEditor(allRoles, userGrants.ruoli, menuGroups)}
2840
2931
  ${directGrantEditor(menuGroups, userGrants.abilitazioni)}
2841
2932
  </div>`);
@@ -2883,14 +2974,24 @@ async function showUser(rawCode) {
2883
2974
  });
2884
2975
  };
2885
2976
  }
2886
- const setPasswordButton = document.getElementById("set-password");
2887
- if (setPasswordButton) setPasswordButton.onclick = () => handleAction(async () => {
2888
- const newPassword = (document.querySelector('#profile input[name="newPassword"]')?.value ?? "").trim();
2889
- const violations = passwordPolicyViolations(newPassword);
2890
- if (violations.length > 0) throw new Error(`La password non rispetta i requisiti: richiede ${violations.join(", ")}.`);
2891
- if (!window.confirm(`Impostare una nuova password per ${user.email}?`)) return;
2892
- await setUserPassword(codiceUtente, { newPassword });
2893
- showNotice("Password aggiornata.");
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.");
2894
2995
  await showUser(String(codiceUtente));
2895
2996
  });
2896
2997
  const rolesForm = document.getElementById("roles");
@@ -2959,6 +3060,7 @@ async function showUser(rawCode) {
2959
3060
  showNotice("Utente eliminato definitivamente.");
2960
3061
  await show("users");
2961
3062
  });
3063
+ if (!manageable) setUserManagementEnabled(false);
2962
3064
  }
2963
3065
  async function showRoles() {
2964
3066
  const [rolesResponse, groupsResponse] = await Promise.all([getRoles(), getGroupsWithMenus({ includeDisabled: true })]);
@@ -3670,8 +3772,12 @@ window.addEventListener("keydown", (event) => {
3670
3772
  });
3671
3773
  window.addEventListener("resize", positionHelp);
3672
3774
  window.addEventListener("scroll", positionHelp, true);
3775
+ var entryTicket = /^#entry=([a-f0-9]{64})$/i.exec(window.location.hash)?.[1];
3673
3776
  var ssoChallenge = /^#two-factor=([a-f0-9]{64})$/.exec(window.location.hash)?.[1];
3674
- if (ssoChallenge) {
3777
+ if (entryTicket) {
3778
+ window.history.replaceState(null, "", window.location.pathname);
3779
+ void exchangeConsoleEntry(entryTicket);
3780
+ } else if (ssoChallenge) {
3675
3781
  token = null;
3676
3782
  setAccessiConsoleToken(null);
3677
3783
  sessionStorage.removeItem("accessi-console-token");