emilsoftware-utilities 2.0.0-dev.23 → 2.0.0-dev.25

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 (261) hide show
  1. package/README.MD +147 -0
  2. package/dist/ContractGenerator.d.ts +4 -3
  3. package/dist/ContractGenerator.js +15 -5
  4. package/dist/ContractGenerator.js.map +1 -1
  5. package/dist/DailyFileTransport.d.ts +8 -2
  6. package/dist/DailyFileTransport.js +9 -2
  7. package/dist/DailyFileTransport.js.map +1 -1
  8. package/dist/DatabaseUpdater.js +8 -3
  9. package/dist/DatabaseUpdater.js.map +1 -1
  10. package/dist/ExecutionTimeLogger.d.ts +2 -2
  11. package/dist/ExecutionTimeLogger.js +3 -2
  12. package/dist/ExecutionTimeLogger.js.map +1 -1
  13. package/dist/Logger.d.ts +22 -9
  14. package/dist/Logger.js +131 -54
  15. package/dist/Logger.js.map +1 -1
  16. package/dist/Orm.d.ts +26 -6
  17. package/dist/Orm.js +144 -40
  18. package/dist/Orm.js.map +1 -1
  19. package/dist/Utilities.d.ts +12 -7
  20. package/dist/Utilities.js +57 -22
  21. package/dist/Utilities.js.map +1 -1
  22. package/dist/accessi-module/AccessiModule.d.ts +9 -0
  23. package/dist/accessi-module/AccessiModule.js +12 -4
  24. package/dist/accessi-module/AccessiModule.js.map +1 -1
  25. package/dist/accessi-module/Console/console.css +529 -281
  26. package/dist/accessi-module/Console/console.js +2081 -85
  27. package/dist/accessi-module/Console/console.js.map +1 -1
  28. package/dist/accessi-module/Console/generated/accessiApi.d.ts +167 -1
  29. package/dist/accessi-module/Console/generated/accessiApi.js +156 -2
  30. package/dist/accessi-module/Console/generated/accessiApi.js.map +1 -1
  31. package/dist/accessi-module/Console/generated/model/abilitazioneMenu.d.ts +20 -8
  32. package/dist/accessi-module/Console/generated/model/abilitazioneMenu.js.map +1 -1
  33. package/dist/accessi-module/Console/generated/model/createFilterTypeRequest.d.ts +23 -0
  34. package/dist/accessi-module/Console/generated/model/createFilterTypeRequest.js +3 -0
  35. package/dist/accessi-module/Console/generated/model/createFilterTypeRequest.js.map +1 -0
  36. package/dist/accessi-module/Console/generated/model/createFilterTypeRequestFlgEnabled.d.ts +14 -0
  37. package/dist/accessi-module/Console/generated/model/createFilterTypeRequestFlgEnabled.js +15 -0
  38. package/dist/accessi-module/Console/generated/model/createFilterTypeRequestFlgEnabled.js.map +1 -0
  39. package/dist/accessi-module/Console/generated/model/createMenuGroupRequest.d.ts +27 -0
  40. package/dist/accessi-module/Console/generated/model/createMenuGroupRequest.js +9 -0
  41. package/dist/accessi-module/Console/generated/model/createMenuGroupRequest.js.map +1 -0
  42. package/dist/accessi-module/Console/generated/model/createMenuRequest.d.ts +52 -0
  43. package/dist/accessi-module/Console/generated/model/createMenuRequest.js +9 -0
  44. package/dist/accessi-module/Console/generated/model/createMenuRequest.js.map +1 -0
  45. package/dist/accessi-module/Console/generated/model/createMenuTypeRequest.d.ts +18 -0
  46. package/dist/accessi-module/Console/generated/model/createMenuTypeRequest.js +9 -0
  47. package/dist/accessi-module/Console/generated/model/createMenuTypeRequest.js.map +1 -0
  48. package/dist/accessi-module/Console/generated/model/createServiceTokenRequest.d.ts +16 -0
  49. package/dist/accessi-module/Console/generated/model/createServiceTokenRequest.js +9 -0
  50. package/dist/accessi-module/Console/generated/model/createServiceTokenRequest.js.map +1 -0
  51. package/dist/accessi-module/Console/generated/model/createServiceTokenResponse.d.ts +14 -0
  52. package/dist/accessi-module/Console/generated/model/createServiceTokenResponse.js +3 -0
  53. package/dist/accessi-module/Console/generated/model/createServiceTokenResponse.js.map +1 -0
  54. package/dist/accessi-module/Console/generated/model/getMenuTypesResponse.d.ts +14 -0
  55. package/dist/accessi-module/Console/generated/model/getMenuTypesResponse.js +3 -0
  56. package/dist/accessi-module/Console/generated/model/getMenuTypesResponse.js.map +1 -0
  57. package/dist/accessi-module/Console/generated/model/getServiceTokensParams.d.ts +12 -0
  58. package/dist/accessi-module/Console/generated/model/getServiceTokensParams.js +9 -0
  59. package/dist/accessi-module/Console/generated/model/getServiceTokensParams.js.map +1 -0
  60. package/dist/accessi-module/Console/generated/model/getServiceTokensResponse.d.ts +14 -0
  61. package/dist/accessi-module/Console/generated/model/getServiceTokensResponse.js +3 -0
  62. package/dist/accessi-module/Console/generated/model/getServiceTokensResponse.js.map +1 -0
  63. package/dist/accessi-module/Console/generated/model/getUsersParams.d.ts +8 -0
  64. package/dist/accessi-module/Console/generated/model/getUsersParams.js.map +1 -1
  65. package/dist/accessi-module/Console/generated/model/index.d.ts +18 -0
  66. package/dist/accessi-module/Console/generated/model/index.js +18 -0
  67. package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
  68. package/dist/accessi-module/Console/generated/model/issuedServiceTokenDto.d.ts +30 -0
  69. package/dist/accessi-module/Console/generated/model/issuedServiceTokenDto.js +9 -0
  70. package/dist/accessi-module/Console/generated/model/issuedServiceTokenDto.js.map +1 -0
  71. package/dist/accessi-module/Console/generated/model/menuTypeEntity.d.ts +12 -0
  72. package/dist/accessi-module/Console/generated/model/menuTypeEntity.js +9 -0
  73. package/dist/accessi-module/Console/generated/model/menuTypeEntity.js.map +1 -0
  74. package/dist/accessi-module/Console/generated/model/serviceTokenDto.d.ts +28 -0
  75. package/dist/accessi-module/Console/generated/model/serviceTokenDto.js +9 -0
  76. package/dist/accessi-module/Console/generated/model/serviceTokenDto.js.map +1 -0
  77. package/dist/accessi-module/Console/generated/model/updateFilterTypeRequest.d.ts +21 -0
  78. package/dist/accessi-module/Console/generated/model/updateFilterTypeRequest.js +3 -0
  79. package/dist/accessi-module/Console/generated/model/updateFilterTypeRequest.js.map +1 -0
  80. package/dist/accessi-module/Console/generated/model/updateFilterTypeRequestFlgEnabled.d.ts +14 -0
  81. package/dist/accessi-module/Console/generated/model/updateFilterTypeRequestFlgEnabled.js +15 -0
  82. package/dist/accessi-module/Console/generated/model/updateFilterTypeRequestFlgEnabled.js.map +1 -0
  83. package/dist/accessi-module/Console/generated/model/updateMenuGroupRequest.d.ts +22 -0
  84. package/dist/accessi-module/Console/generated/model/updateMenuGroupRequest.js +9 -0
  85. package/dist/accessi-module/Console/generated/model/updateMenuGroupRequest.js.map +1 -0
  86. package/dist/accessi-module/Console/generated/model/updateMenuRequest.d.ts +47 -0
  87. package/dist/accessi-module/Console/generated/model/updateMenuRequest.js +9 -0
  88. package/dist/accessi-module/Console/generated/model/updateMenuRequest.js.map +1 -0
  89. package/dist/accessi-module/Console/generated/model/updateMenuTypeRequest.d.ts +13 -0
  90. package/dist/accessi-module/Console/generated/model/updateMenuTypeRequest.js +9 -0
  91. package/dist/accessi-module/Console/generated/model/updateMenuTypeRequest.js.map +1 -0
  92. package/dist/accessi-module/Console/help.d.ts +13 -0
  93. package/dist/accessi-module/Console/help.js +348 -0
  94. package/dist/accessi-module/Console/help.js.map +1 -0
  95. package/dist/accessi-module/Console/index.html +89 -21
  96. package/dist/accessi-module/Console/wiki.d.ts +52 -0
  97. package/dist/accessi-module/Console/wiki.js +689 -0
  98. package/dist/accessi-module/Console/wiki.js.map +1 -0
  99. package/dist/accessi-module/Controllers/AccessiConsoleController.d.ts +8 -1
  100. package/dist/accessi-module/Controllers/AccessiConsoleController.js +52 -15
  101. package/dist/accessi-module/Controllers/AccessiConsoleController.js.map +1 -1
  102. package/dist/accessi-module/Controllers/ConfiguratorController.d.ts +1 -3
  103. package/dist/accessi-module/Controllers/ConfiguratorController.js +6 -8
  104. package/dist/accessi-module/Controllers/ConfiguratorController.js.map +1 -1
  105. package/dist/accessi-module/Controllers/EmailController.d.ts +1 -1
  106. package/dist/accessi-module/Controllers/EmailController.js +1 -1
  107. package/dist/accessi-module/Controllers/EmailController.js.map +1 -1
  108. package/dist/accessi-module/Controllers/FiltriController.d.ts +5 -4
  109. package/dist/accessi-module/Controllers/FiltriController.js +92 -4
  110. package/dist/accessi-module/Controllers/FiltriController.js.map +1 -1
  111. package/dist/accessi-module/Controllers/PermissionController.d.ts +11 -0
  112. package/dist/accessi-module/Controllers/PermissionController.js +273 -8
  113. package/dist/accessi-module/Controllers/PermissionController.js.map +1 -1
  114. package/dist/accessi-module/Controllers/ServiceTokenController.d.ts +23 -0
  115. package/dist/accessi-module/Controllers/ServiceTokenController.js +176 -0
  116. package/dist/accessi-module/Controllers/ServiceTokenController.js.map +1 -0
  117. package/dist/accessi-module/Controllers/UserController.d.ts +2 -2
  118. package/dist/accessi-module/Controllers/UserController.js +25 -9
  119. package/dist/accessi-module/Controllers/UserController.js.map +1 -1
  120. package/dist/accessi-module/Dtos/Abilitazione.js.map +1 -1
  121. package/dist/accessi-module/Dtos/AbilitazioneMenu.js +16 -8
  122. package/dist/accessi-module/Dtos/AbilitazioneMenu.js.map +1 -1
  123. package/dist/accessi-module/Dtos/AssignPermissionsToUserRequest.js.map +1 -1
  124. package/dist/accessi-module/Dtos/AssignRolesToUserRequest.js.map +1 -1
  125. package/dist/accessi-module/Dtos/AuthenticatedUserPayloadDto.js.map +1 -1
  126. package/dist/accessi-module/Dtos/BaseResponse.js.map +1 -1
  127. package/dist/accessi-module/Dtos/CatalogDtos.d.ts +66 -0
  128. package/dist/accessi-module/Dtos/CatalogDtos.js +336 -0
  129. package/dist/accessi-module/Dtos/CatalogDtos.js.map +1 -0
  130. package/dist/accessi-module/Dtos/ConfirmResetPasswordRequest.js.map +1 -1
  131. package/dist/accessi-module/Dtos/FiltriUtente.js.map +1 -1
  132. package/dist/accessi-module/Dtos/GetGroupsWithMenusResponse.js.map +1 -1
  133. package/dist/accessi-module/Dtos/GetMenusResponse.js +1 -1
  134. package/dist/accessi-module/Dtos/GetMenusResponse.js.map +1 -1
  135. package/dist/accessi-module/Dtos/GetRolesResponse.js.map +1 -1
  136. package/dist/accessi-module/Dtos/GetUserByTokenRequest.js.map +1 -1
  137. package/dist/accessi-module/Dtos/GetUserByTokenResponse.js.map +1 -1
  138. package/dist/accessi-module/Dtos/GetUsersResponse.d.ts +1 -1
  139. package/dist/accessi-module/Dtos/GetUsersResponse.js.map +1 -1
  140. package/dist/accessi-module/Dtos/LoginRequest.js.map +1 -1
  141. package/dist/accessi-module/Dtos/LoginResponse.d.ts +1 -1
  142. package/dist/accessi-module/Dtos/LoginResponse.js +1 -1
  143. package/dist/accessi-module/Dtos/LoginResponse.js.map +1 -1
  144. package/dist/accessi-module/Dtos/MenuItem.js +1 -0
  145. package/dist/accessi-module/Dtos/Permission.js.map +1 -1
  146. package/dist/accessi-module/Dtos/RegisterRequest.js +2 -1
  147. package/dist/accessi-module/Dtos/RegisterRequest.js.map +1 -1
  148. package/dist/accessi-module/Dtos/RegisterResponse.js.map +1 -1
  149. package/dist/accessi-module/Dtos/Role.js.map +1 -1
  150. package/dist/accessi-module/Dtos/RuoliMenu.js.map +1 -1
  151. package/dist/accessi-module/Dtos/SendResetPasswordEmailRequest.js.map +1 -1
  152. package/dist/accessi-module/Dtos/ServiceToken.d.ts +34 -0
  153. package/dist/accessi-module/Dtos/ServiceToken.js +128 -0
  154. package/dist/accessi-module/Dtos/ServiceToken.js.map +1 -0
  155. package/dist/accessi-module/Dtos/SetStatoRegistrazione.js.map +1 -1
  156. package/dist/accessi-module/Dtos/TipoFiltro.js.map +1 -1
  157. package/dist/accessi-module/Dtos/TokenResult.js.map +1 -1
  158. package/dist/accessi-module/Dtos/TwoFactorDtos.js.map +1 -1
  159. package/dist/accessi-module/Dtos/UpdateEnabledStatusRequest.js.map +1 -1
  160. package/dist/accessi-module/Dtos/UserDto.js +4 -2
  161. package/dist/accessi-module/Dtos/UserDto.js.map +1 -1
  162. package/dist/accessi-module/Dtos/UserGrantsDto.js.map +1 -1
  163. package/dist/accessi-module/Dtos/UserGrantsResponse.js.map +1 -1
  164. package/dist/accessi-module/Dtos/index.d.ts +2 -0
  165. package/dist/accessi-module/Dtos/index.js +2 -0
  166. package/dist/accessi-module/Dtos/index.js.map +1 -1
  167. package/dist/accessi-module/Services/AuthService/AuthService.d.ts +2 -2
  168. package/dist/accessi-module/Services/AuthService/AuthService.js +36 -36
  169. package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
  170. package/dist/accessi-module/Services/EmailService/EmailService.d.ts +2 -1
  171. package/dist/accessi-module/Services/EmailService/EmailService.js +9 -6
  172. package/dist/accessi-module/Services/EmailService/EmailService.js.map +1 -1
  173. package/dist/accessi-module/Services/FiltriService/FiltriService.d.ts +10 -3
  174. package/dist/accessi-module/Services/FiltriService/FiltriService.js +64 -6
  175. package/dist/accessi-module/Services/FiltriService/FiltriService.js.map +1 -1
  176. package/dist/accessi-module/Services/PermissionService/PermissionService.d.ts +44 -1
  177. package/dist/accessi-module/Services/PermissionService/PermissionService.js +418 -110
  178. package/dist/accessi-module/Services/PermissionService/PermissionService.js.map +1 -1
  179. package/dist/accessi-module/Services/ServiceTokenService/ServiceTokenService.d.ts +75 -0
  180. package/dist/accessi-module/Services/ServiceTokenService/ServiceTokenService.js +265 -0
  181. package/dist/accessi-module/Services/ServiceTokenService/ServiceTokenService.js.map +1 -0
  182. package/dist/accessi-module/Services/TwoFactorService/TwoFactorService.js +23 -27
  183. package/dist/accessi-module/Services/TwoFactorService/TwoFactorService.js.map +1 -1
  184. package/dist/accessi-module/Services/UserService/UserService.d.ts +10 -3
  185. package/dist/accessi-module/Services/UserService/UserService.js +170 -84
  186. package/dist/accessi-module/Services/UserService/UserService.js.map +1 -1
  187. package/dist/accessi-module/database-updates/AccessiDatabaseUpdater.d.ts +9 -0
  188. package/dist/accessi-module/database-updates/AccessiDatabaseUpdater.js +76 -28
  189. package/dist/accessi-module/database-updates/AccessiDatabaseUpdater.js.map +1 -1
  190. package/dist/accessi-module/database-updates/accessiSchema.d.ts +1 -1
  191. package/dist/accessi-module/database-updates/accessiSchema.js +9 -1
  192. package/dist/accessi-module/database-updates/accessiSchema.js.map +1 -1
  193. package/dist/accessi-module/database-updates/optionalColumns.d.ts +2 -0
  194. package/dist/accessi-module/database-updates/optionalColumns.js +33 -1
  195. package/dist/accessi-module/database-updates/optionalColumns.js.map +1 -1
  196. package/dist/accessi-module/database-updates/runAccessiDbUpdate.js +7 -2
  197. package/dist/accessi-module/database-updates/runAccessiDbUpdate.js.map +1 -1
  198. package/dist/accessi-module/federated-auth/FederatedAuthController.d.ts +1 -0
  199. package/dist/accessi-module/federated-auth/FederatedAuthController.js +26 -0
  200. package/dist/accessi-module/federated-auth/FederatedAuthController.js.map +1 -1
  201. package/dist/accessi-module/federated-auth/FederatedAuthService.d.ts +6 -0
  202. package/dist/accessi-module/federated-auth/FederatedAuthService.js +28 -1
  203. package/dist/accessi-module/federated-auth/FederatedAuthService.js.map +1 -1
  204. package/dist/accessi-module/index.d.ts +4 -1
  205. package/dist/accessi-module/index.js +8 -2
  206. package/dist/accessi-module/index.js.map +1 -1
  207. package/dist/accessi-module/jwt/jwt.strategy.js +8 -5
  208. package/dist/accessi-module/jwt/jwt.strategy.js.map +1 -1
  209. package/dist/accessi-module/middleware/accessiRequirements.d.ts +1 -1
  210. package/dist/accessi-module/middleware/accessiRequirements.js.map +1 -1
  211. package/dist/accessi-module/middleware/authenticateGen.js +11 -7
  212. package/dist/accessi-module/middleware/authenticateGen.js.map +1 -1
  213. package/dist/accessi-module/security/accessControl.js +8 -3
  214. package/dist/accessi-module/security/accessControl.js.map +1 -1
  215. package/dist/accessi-module/security/authenticatedToken.d.ts +1 -1
  216. package/dist/accessi-module/security/authenticatedToken.js +2 -1
  217. package/dist/accessi-module/security/authenticatedToken.js.map +1 -1
  218. package/dist/accessi-module/security/passwordResetToken.d.ts +9 -3
  219. package/dist/accessi-module/security/passwordResetToken.js +9 -5
  220. package/dist/accessi-module/security/passwordResetToken.js.map +1 -1
  221. package/dist/accessi-module/security/publicAuthRateLimit.d.ts +1 -1
  222. package/dist/accessi-module/security/publicAuthRateLimit.js +28 -8
  223. package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
  224. package/dist/accessi-module/security/serviceTokenGuard.d.ts +25 -0
  225. package/dist/accessi-module/security/serviceTokenGuard.js +85 -0
  226. package/dist/accessi-module/security/serviceTokenGuard.js.map +1 -0
  227. package/dist/accessi-module/security/serviceTokenRateLimit.d.ts +8 -0
  228. package/dist/accessi-module/security/serviceTokenRateLimit.js +49 -0
  229. package/dist/accessi-module/security/serviceTokenRateLimit.js.map +1 -0
  230. package/dist/allegati-module/AllegatiModule.d.ts +11 -0
  231. package/dist/allegati-module/AllegatiModule.js +11 -1
  232. package/dist/allegati-module/AllegatiModule.js.map +1 -1
  233. package/dist/allegati-module/Controllers/AllegatiController.d.ts +2 -1
  234. package/dist/allegati-module/Controllers/AllegatiController.js +23 -15
  235. package/dist/allegati-module/Controllers/AllegatiController.js.map +1 -1
  236. package/dist/allegati-module/Dtos/AllegatoDto.js.map +1 -1
  237. package/dist/allegati-module/Dtos/BaseResponse.js.map +1 -1
  238. package/dist/allegati-module/Dtos/UploadSingleFileRequest.js.map +1 -1
  239. package/dist/allegati-module/Dtos/responses/DownloadAllegatoResponseDto.js.map +1 -1
  240. package/dist/allegati-module/Dtos/responses/GetListResponse.js.map +1 -1
  241. package/dist/allegati-module/Dtos/responses/UploadAllegatoResponseDto.js.map +1 -1
  242. package/dist/allegati-module/Services/AllegatiService/AllegatiService.d.ts +10 -3
  243. package/dist/allegati-module/Services/AllegatiService/AllegatiService.js +101 -52
  244. package/dist/allegati-module/Services/AllegatiService/AllegatiService.js.map +1 -1
  245. package/dist/allegati-module/security/allegatiAuthorizationGuard.d.ts +14 -0
  246. package/dist/allegati-module/security/allegatiAuthorizationGuard.js +70 -0
  247. package/dist/allegati-module/security/allegatiAuthorizationGuard.js.map +1 -0
  248. package/dist/autobind.d.ts +10 -3
  249. package/dist/autobind.js +24 -9
  250. package/dist/autobind.js.map +1 -1
  251. package/dist/emilsoftware-utilities-2.0.0-dev.25.tgz +0 -0
  252. package/dist/firebird-compat.d.ts +13 -0
  253. package/dist/firebird-compat.js +3 -2
  254. package/dist/firebird-compat.js.map +1 -1
  255. package/dist/scripts/generateAccessiWiki.d.ts +1 -0
  256. package/dist/scripts/generateAccessiWiki.js +131 -0
  257. package/dist/scripts/generateAccessiWiki.js.map +1 -0
  258. package/dist/swagger/SwaggerConfig.js +6 -3
  259. package/dist/swagger/SwaggerConfig.js.map +1 -1
  260. package/package.json +19 -8
  261. package/dist/emilsoftware-utilities-2.0.0-dev.23.tgz +0 -0
@@ -167,6 +167,33 @@ var assignPermissionsToUser = async (codiceUtente, assignPermissionsToUserReques
167
167
  }
168
168
  );
169
169
  };
170
+ var getDeleteRoleUrl = (codiceRuolo) => {
171
+ return `/api/accessi/permission/delete-role/${codiceRuolo}`;
172
+ };
173
+ var deleteRole = async (codiceRuolo, options) => {
174
+ return accessiFetch(
175
+ getDeleteRoleUrl(codiceRuolo),
176
+ {
177
+ ...options,
178
+ method: "DELETE"
179
+ }
180
+ );
181
+ };
182
+ var getCreateMenuUrl = () => {
183
+ return `/api/accessi/permission/menus`;
184
+ };
185
+ var createMenu = async (createMenuRequest, options) => {
186
+ return accessiFetch(
187
+ getCreateMenuUrl(),
188
+ {
189
+ ...options,
190
+ method: "POST",
191
+ body: JSON.stringify(
192
+ createMenuRequest
193
+ )
194
+ }
195
+ );
196
+ };
170
197
  var getGetGroupsWithMenusUrl = (params) => {
171
198
  const normalizedParams = new URLSearchParams();
172
199
  Object.entries(params || {}).forEach(([key, value]) => {
@@ -187,6 +214,129 @@ var getGroupsWithMenus = async (params, options) => {
187
214
  }
188
215
  );
189
216
  };
217
+ var getGetMenuTypesUrl = () => {
218
+ return `/api/accessi/permission/menu-types`;
219
+ };
220
+ var getMenuTypes = async (options) => {
221
+ return accessiFetch(
222
+ getGetMenuTypesUrl(),
223
+ {
224
+ ...options,
225
+ method: "GET"
226
+ }
227
+ );
228
+ };
229
+ var getCreateMenuTypeUrl = () => {
230
+ return `/api/accessi/permission/menu-types`;
231
+ };
232
+ var createMenuType = async (createMenuTypeRequest, options) => {
233
+ return accessiFetch(
234
+ getCreateMenuTypeUrl(),
235
+ {
236
+ ...options,
237
+ method: "POST",
238
+ body: JSON.stringify(
239
+ createMenuTypeRequest
240
+ )
241
+ }
242
+ );
243
+ };
244
+ var getUpdateMenuUrl = (codiceMenu) => {
245
+ return `/api/accessi/permission/menus/${codiceMenu}`;
246
+ };
247
+ var updateMenu = async (codiceMenu, updateMenuRequest, options) => {
248
+ return accessiFetch(
249
+ getUpdateMenuUrl(codiceMenu),
250
+ {
251
+ ...options,
252
+ method: "PUT",
253
+ body: JSON.stringify(
254
+ updateMenuRequest
255
+ )
256
+ }
257
+ );
258
+ };
259
+ var getDeleteMenuUrl = (codiceMenu) => {
260
+ return `/api/accessi/permission/menus/${codiceMenu}`;
261
+ };
262
+ var deleteMenu = async (codiceMenu, options) => {
263
+ return accessiFetch(
264
+ getDeleteMenuUrl(codiceMenu),
265
+ {
266
+ ...options,
267
+ method: "DELETE"
268
+ }
269
+ );
270
+ };
271
+ var getCreateMenuGroupUrl = () => {
272
+ return `/api/accessi/permission/menu-groups`;
273
+ };
274
+ var createMenuGroup = async (createMenuGroupRequest, options) => {
275
+ return accessiFetch(
276
+ getCreateMenuGroupUrl(),
277
+ {
278
+ ...options,
279
+ method: "POST",
280
+ body: JSON.stringify(
281
+ createMenuGroupRequest
282
+ )
283
+ }
284
+ );
285
+ };
286
+ var getUpdateMenuGroupUrl = (codiceGruppo) => {
287
+ return `/api/accessi/permission/menu-groups/${codiceGruppo}`;
288
+ };
289
+ var updateMenuGroup = async (codiceGruppo, updateMenuGroupRequest, options) => {
290
+ return accessiFetch(
291
+ getUpdateMenuGroupUrl(codiceGruppo),
292
+ {
293
+ ...options,
294
+ method: "PUT",
295
+ body: JSON.stringify(
296
+ updateMenuGroupRequest
297
+ )
298
+ }
299
+ );
300
+ };
301
+ var getDeleteMenuGroupUrl = (codiceGruppo) => {
302
+ return `/api/accessi/permission/menu-groups/${codiceGruppo}`;
303
+ };
304
+ var deleteMenuGroup = async (codiceGruppo, options) => {
305
+ return accessiFetch(
306
+ getDeleteMenuGroupUrl(codiceGruppo),
307
+ {
308
+ ...options,
309
+ method: "DELETE"
310
+ }
311
+ );
312
+ };
313
+ var getUpdateMenuTypeUrl = (codiceTipo) => {
314
+ return `/api/accessi/permission/menu-types/${codiceTipo}`;
315
+ };
316
+ var updateMenuType = async (codiceTipo, updateMenuTypeRequest, options) => {
317
+ return accessiFetch(
318
+ getUpdateMenuTypeUrl(codiceTipo),
319
+ {
320
+ ...options,
321
+ method: "PUT",
322
+ body: JSON.stringify(
323
+ updateMenuTypeRequest
324
+ )
325
+ }
326
+ );
327
+ };
328
+ var getDeleteMenuTypeUrl = (codiceTipo) => {
329
+ return `/api/accessi/permission/menu-types/${codiceTipo}`;
330
+ };
331
+ var deleteMenuType = async (codiceTipo, options) => {
332
+ return accessiFetch(
333
+ getDeleteMenuTypeUrl(codiceTipo),
334
+ {
335
+ ...options,
336
+ method: "DELETE"
337
+ }
338
+ );
339
+ };
190
340
  var getGetUserRolesAndGrantsUrl = (codiceUtente) => {
191
341
  return `/api/accessi/permission/grants/${codiceUtente}`;
192
342
  };
@@ -231,6 +381,21 @@ var deleteUser = async (codiceUtente, options) => {
231
381
  }
232
382
  );
233
383
  };
384
+ var getSetStatoRegistrazioneUrl = () => {
385
+ return `/api/accessi/user/set-stato`;
386
+ };
387
+ var setStatoRegistrazione = async (setStatoRegistrazioneDto, options) => {
388
+ return accessiFetch(
389
+ getSetStatoRegistrazioneUrl(),
390
+ {
391
+ ...options,
392
+ method: "POST",
393
+ body: JSON.stringify(
394
+ setStatoRegistrazioneDto
395
+ )
396
+ }
397
+ );
398
+ };
234
399
  var getCreateManagedUserUrl = () => {
235
400
  return `/api/accessi/user/create-managed-user`;
236
401
  };
@@ -261,6 +426,60 @@ var updateUtente = async (codiceUtente, userDto, options) => {
261
426
  }
262
427
  );
263
428
  };
429
+ var getGetTipiFiltroUrl = () => {
430
+ return `/api/accessi/filtri/tipi`;
431
+ };
432
+ var getTipiFiltro = async (options) => {
433
+ return accessiFetch(
434
+ getGetTipiFiltroUrl(),
435
+ {
436
+ ...options,
437
+ method: "GET"
438
+ }
439
+ );
440
+ };
441
+ var getCreateTipoFiltroUrl = () => {
442
+ return `/api/accessi/filtri/tipi`;
443
+ };
444
+ var createTipoFiltro = async (createFilterTypeRequest, options) => {
445
+ return accessiFetch(
446
+ getCreateTipoFiltroUrl(),
447
+ {
448
+ ...options,
449
+ method: "POST",
450
+ body: JSON.stringify(
451
+ createFilterTypeRequest
452
+ )
453
+ }
454
+ );
455
+ };
456
+ var getUpdateTipoFiltroUrl = (tipFil) => {
457
+ return `/api/accessi/filtri/tipi/${tipFil}`;
458
+ };
459
+ var updateTipoFiltro = async (tipFil, updateFilterTypeRequest, options) => {
460
+ return accessiFetch(
461
+ getUpdateTipoFiltroUrl(tipFil),
462
+ {
463
+ ...options,
464
+ method: "PUT",
465
+ body: JSON.stringify(
466
+ updateFilterTypeRequest
467
+ )
468
+ }
469
+ );
470
+ };
471
+ var getDeleteTipoFiltroUrl = (tipFil) => {
472
+ return `/api/accessi/filtri/tipi/${tipFil}`;
473
+ };
474
+ var deleteTipoFiltro = async (tipFil, options) => {
475
+ return accessiFetch(
476
+ getDeleteTipoFiltroUrl(tipFil),
477
+ {
478
+ ...options,
479
+ method: "DELETE"
480
+ }
481
+ );
482
+ };
264
483
  var getGetFiltriUtenteUrl = (params) => {
265
484
  const normalizedParams = new URLSearchParams();
266
485
  Object.entries(params || {}).forEach(([key, value]) => {
@@ -368,6 +587,18 @@ var updateFederatedProvider = async (provider, updateFederatedProviderRequest, o
368
587
  }
369
588
  );
370
589
  };
590
+ var getDeleteFederatedProviderUrl = (provider) => {
591
+ return `/api/accessi/federated-auth/providers/${provider}`;
592
+ };
593
+ var deleteFederatedProvider = async (provider, options) => {
594
+ return accessiFetch(
595
+ getDeleteFederatedProviderUrl(provider),
596
+ {
597
+ ...options,
598
+ method: "DELETE"
599
+ }
600
+ );
601
+ };
371
602
  var getGetFederatedIdentitiesUrl = (codiceUtente) => {
372
603
  return `/api/accessi/federated-auth/users/${codiceUtente}/identities`;
373
604
  };
@@ -437,22 +668,1187 @@ var deleteFederatedIdentityPermanently = async (codiceUtente, identityKey, optio
437
668
  }
438
669
  );
439
670
  };
671
+ var getCreateServiceTokenUrl = () => {
672
+ return `/api/accessi/service-token`;
673
+ };
674
+ var createServiceToken = async (createServiceTokenRequest, options) => {
675
+ return accessiFetch(
676
+ getCreateServiceTokenUrl(),
677
+ {
678
+ ...options,
679
+ method: "POST",
680
+ body: JSON.stringify(
681
+ createServiceTokenRequest
682
+ )
683
+ }
684
+ );
685
+ };
686
+ var getGetServiceTokensUrl = (params) => {
687
+ const normalizedParams = new URLSearchParams();
688
+ Object.entries(params || {}).forEach(([key, value]) => {
689
+ if (value === null) {
690
+ normalizedParams.append(key, "null");
691
+ } else if (value !== void 0) {
692
+ normalizedParams.append(key, value.toString());
693
+ }
694
+ });
695
+ return `/api/accessi/service-token?${normalizedParams.toString()}`;
696
+ };
697
+ var getServiceTokens = async (params, options) => {
698
+ return accessiFetch(
699
+ getGetServiceTokensUrl(params),
700
+ {
701
+ ...options,
702
+ method: "GET"
703
+ }
704
+ );
705
+ };
706
+ var getRevokeServiceTokenUrl = (tokenId) => {
707
+ return `/api/accessi/service-token/${tokenId}`;
708
+ };
709
+ var revokeServiceToken = async (tokenId, options) => {
710
+ return accessiFetch(
711
+ getRevokeServiceTokenUrl(tokenId),
712
+ {
713
+ ...options,
714
+ method: "DELETE"
715
+ }
716
+ );
717
+ };
718
+ var getRotateServiceTokenUrl = (tokenId) => {
719
+ return `/api/accessi/service-token/${tokenId}/rotate`;
720
+ };
721
+ var rotateServiceToken = async (tokenId, options) => {
722
+ return accessiFetch(
723
+ getRotateServiceTokenUrl(tokenId),
724
+ {
725
+ ...options,
726
+ method: "POST"
727
+ }
728
+ );
729
+ };
730
+
731
+ // src/accessi-module/Console/wiki.ts
732
+ var p = (text) => ({ kind: "p", text });
733
+ var h = (text) => ({ kind: "h", text });
734
+ var list = (items, ordered = false) => ({ kind: "list", items, ordered });
735
+ var code = (language, codeText, title, variant) => ({ kind: "code", language, title, code: codeText, variant });
736
+ var good = (language, codeText, title) => code(language, codeText, title, "good");
737
+ var bad = (language, codeText, title) => code(language, codeText, title, "bad");
738
+ var note = (tone, title, text) => ({ kind: "callout", tone, title, text });
739
+ var table = (head, rows) => ({ kind: "table", head, rows });
740
+ var WIKI_SECTIONS = [
741
+ // -------------------------------------------------------------------------
742
+ {
743
+ id: "panoramica",
744
+ group: "Per iniziare",
745
+ title: "Panoramica e architettura",
746
+ summary: "Che cosa fa Accessi, come si incastra nel backend e come scorre una richiesta.",
747
+ blocks: [
748
+ 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.`),
749
+ h(`Che cosa possiede il modulo`),
750
+ list([
751
+ `Le rotte sotto /api/accessi/* (login, utenti, ruoli, menu, filtri, SSO, token, console).`,
752
+ `Lo schema database del dominio Accessi, riconciliato automaticamente al bootstrap.`,
753
+ `Il middleware di autorizzazione authorizeAccessi per le rotte della tua applicazione.`,
754
+ `La console web su /api/accessi/console.`
755
+ ]),
756
+ h(`Che cosa resta alla tua applicazione`),
757
+ list([
758
+ `Il proprio frontend e le proprie rotte di business.`,
759
+ `La validazione dei token dei provider SSO esterni (Accessi non li vede).`,
760
+ `I segreti: jwtOptions.secret, SMTP, database ed encryptionKey non raggiungono mai il browser.`
761
+ ]),
762
+ h(`Flusso di una richiesta protetta`),
763
+ list([
764
+ `Il client chiama una rotta della tua app con header Authorization: Bearer <jwt>.`,
765
+ `La tua rotta esegue authorizeAccessi, che verifica firma HS256, scadenza e stato utente corrente.`,
766
+ `Se passi un requisito, il modulo carica ruoli e grant dell'utente e li valuta.`,
767
+ `Su successo req.user, req.data e (se richiesto) req.userGrants sono popolati; altrimenti risponde con il contratto errore Accessi.`
768
+ ], true),
769
+ note("info", `Due modalit\xE0 di bootstrap`, `Express esistente: initializeAccessiModule(app, options). NestJS esistente: AccessiModule.forRoot(options) nei tuoi imports. Non usarle entrambe.`)
770
+ ]
771
+ },
772
+ // -------------------------------------------------------------------------
773
+ {
774
+ id: "installazione",
775
+ group: "Per iniziare",
776
+ title: "Installazione",
777
+ summary: "Dipendenze, peer e build.",
778
+ blocks: [
779
+ code("bash", `npm install emilsoftware-utilities`, "Installazione"),
780
+ p(`La libreria dipende da node-firebird per il database e da Express 5 come peer per il montaggio. \xC8 consigliato Node 18 o superiore.`),
781
+ note("warn", `Un solo modulo per processo`, `AccessiModule \xE8 @Global(): inizializzalo una sola volta per processo. In caso di pi\xF9 istanze, usa un processo per istanza.`)
782
+ ]
783
+ },
784
+ // -------------------------------------------------------------------------
785
+ {
786
+ id: "bootstrap-express",
787
+ group: "Per iniziare",
788
+ title: "Bootstrap su Express esistente",
789
+ summary: "initializeAccessiModule: ordine corretto e attesa prima di listen.",
790
+ blocks: [
791
+ p(`Chiama initializeAccessiModule prima di aprire la porta HTTP. Durante il bootstrap, authorizeAccessi risponde 503 ACCESSI_AUTH_INITIALIZING invece di autorizzare richieste parziali.`),
792
+ good("ts", [
793
+ "import express from 'express';",
794
+ "import { initializeAccessiModule, authorizeAccessi } from 'emilsoftware-utilities';",
795
+ "",
796
+ "const app = express();",
797
+ "app.use(express.json());",
798
+ "",
799
+ "await initializeAccessiModule(app, options); // prima di app.listen",
800
+ "",
801
+ "app.get('/api/ordini', authorizeAccessi, (req, res) => {",
802
+ " res.json({ utente: req.user });",
803
+ "});",
804
+ "",
805
+ "app.get('/api/ordini/scrivi', (req, res, next) =>",
806
+ " authorizeAccessi(req, res, next, {",
807
+ " requirements: [{ menuCode: 'ORDINI', minPermissionLevel: 20 }],",
808
+ " }),",
809
+ "(req, res) => res.json({ ok: true }));",
810
+ "",
811
+ "app.listen(3000);"
812
+ ].join("\n"), "Express + Accessi"),
813
+ bad("ts", [
814
+ "const app = express();",
815
+ "app.listen(3000); // porta aperta prima del bootstrap",
816
+ "await initializeAccessiModule(app, options); // le richieste nel frattempo ricevono 503"
817
+ ].join("\n"), "Non aprire la porta prima del bootstrap"),
818
+ h(`Rotte montate`),
819
+ p(`Accessi intercetta solo /api/accessi/*, /accessi/swagger* e /accessi/swagger.json. Tutte le altre rotte passano intatte al tuo stack Express.`),
820
+ note("ok", `req.user`, `authorizeAccessi popola req.user con il payload utente corrente (utente, codiceUtente, flag) e req.data come alias storico. req.userGrants \xE8 presente solo se hai richiesto dei requisiti.`)
821
+ ]
822
+ },
823
+ // -------------------------------------------------------------------------
824
+ {
825
+ id: "bootstrap-nest",
826
+ group: "Per iniziare",
827
+ title: "Bootstrap su NestJS esistente",
828
+ summary: "AccessiModule.forRoot e uso dei provider esportati.",
829
+ blocks: [
830
+ p(`Se la tua applicazione \xE8 gi\xE0 NestJS, importa AccessiModule.forRoot(options). Il modulo \xE8 @Global e riesporta i servizi (AuthService, UserService, PermissionService, FederatedAuthService, ServiceTokenService, ServiceTokenGuard, AuthenticateGenService).`),
831
+ good("ts", [
832
+ "import { Module } from '@nestjs/common';",
833
+ "import { AccessiModule } from 'emilsoftware-utilities';",
834
+ "",
835
+ "@Module({",
836
+ " imports: [AccessiModule.forRoot(options)],",
837
+ "})",
838
+ "export class AppModule {}"
839
+ ].join("\n"), "app.module.ts"),
840
+ p(`Il global prefix api e le rotte /api/accessi/* sono gestiti dal modulo. Le tue rotte Nest si proteggono con JwtSimpleGuard usando il JWT Accessi.`)
841
+ ]
842
+ },
843
+ // -------------------------------------------------------------------------
844
+ {
845
+ id: "opzioni",
846
+ group: "Configurazione",
847
+ title: "AccessiOptions (riferimento completo)",
848
+ summary: "Ogni campo di configurazione con tipo, obbligo e note.",
849
+ blocks: [
850
+ table(
851
+ ["Campo", "Tipo", "Obbligo", "Note"],
852
+ [
853
+ ["databaseOptions", "Options (node-firebird)", "s\xEC", "Connessione Firebird al database Accessi."],
854
+ ["confirmationEmailUrl", "string", "s\xEC", "Base URL usata per il reset password."],
855
+ ["confirmationEmailReturnUrl", "string", "s\xEC", "URL di ritorno dopo il reset."],
856
+ ["confirmationEmailPrefix", "string", "no", "Prefisso passato alla pagina di reset."],
857
+ ["customResetPage", "string", "no", "Sostituisce la pagina HTML di reset inclusa."],
858
+ ["encryptionKey", "string", "s\xEC", "Compatibilit\xE0 e migrazione delle password legacy."],
859
+ ["mockDemoUser", "boolean", "s\xEC", "Utenti fittizi admin e demo solo fuori produzione."],
860
+ ["passwordExpiration", "boolean", "no", "Attiva il controllo della scadenza password al login."],
861
+ ["passwordExpirationDays", "number", "no", "Valore predefinito: 90 giorni."],
862
+ ["legacyPasswordMigrationOnStartup", "boolean", "no", "Predefinito true: converte le password legacy."],
863
+ ["autoUpdateDatabase", "boolean", "no", "Predefinito true: riconcilia lo schema al boot."],
864
+ ["jwtOptions", "{ secret, expiresIn }", "s\xEC", "Firma HS256 e durata dei JWT Accessi."],
865
+ ["emailOptions", "EmailOptions", "s\xEC", "SMTP per reset e codici email."],
866
+ ["publicAuthRateLimit", "PublicAuthRateLimitOptions", "no", "Limiti in-memory sugli endpoint pubblici."],
867
+ ["publicRegistration", "{ enabled }", "no", "Registrazione pubblica: disattivata per impostazione predefinita."],
868
+ ["federatedAuthentication", "FederatedAuthenticationOptions", "no", `Abilita l'SSO generico.`],
869
+ ["extensionFieldsOptions", "ExtensionFieldsOptions[]", "no", "Tabelle esterne allegate al profilo."],
870
+ ["serviceTokens", "{ enabled, defaultTtlDays }", "no", "Token macchina-a-macchina, abilitati per impostazione predefinita."]
871
+ ]
872
+ ),
873
+ good("ts", [
874
+ "import type { AccessiOptions } from 'emilsoftware-utilities';",
875
+ "",
876
+ "export const options: AccessiOptions = {",
877
+ " databaseOptions: {",
878
+ " host: process.env.ACCESSI_DB_HOST!,",
879
+ " port: Number(process.env.ACCESSI_DB_PORT ?? 3050),",
880
+ " database: process.env.ACCESSI_DB_NAME!,",
881
+ " user: process.env.ACCESSI_DB_USER!,",
882
+ " password: process.env.ACCESSI_DB_PASSWORD!,",
883
+ " },",
884
+ " confirmationEmailUrl: 'https://app.example.com',",
885
+ " confirmationEmailReturnUrl: 'https://app.example.com/login',",
886
+ " encryptionKey: process.env.ACCESSI_ENCRYPTION_KEY!,",
887
+ " mockDemoUser: false,",
888
+ " passwordExpiration: true,",
889
+ " passwordExpirationDays: 90,",
890
+ " jwtOptions: {",
891
+ " secret: process.env.ACCESSI_JWT_SECRET!, // almeno 32 caratteri casuali, unico per ambiente",
892
+ " expiresIn: '24h',",
893
+ " },",
894
+ " emailOptions: {",
895
+ " host: process.env.SMTP_HOST!,",
896
+ " port: Number(process.env.SMTP_PORT ?? 587),",
897
+ " secure: false,",
898
+ " requireTLS: true,",
899
+ " tls: { rejectUnauthorized: true },",
900
+ " from: 'no-reply@example.com',",
901
+ " auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! },",
902
+ " },",
903
+ " publicRegistration: { enabled: false },",
904
+ " serviceTokens: { enabled: true, defaultTtlDays: 365 },",
905
+ "};"
906
+ ].join("\n"), "Configurazione tipica"),
907
+ bad("ts", [
908
+ "jwtOptions: { secret: 'accessi', expiresIn: '24h' }, // segreto debole e condiviso",
909
+ "mockDemoUser: true, // MAI in produzione"
910
+ ].join("\n"), "Errori comuni"),
911
+ note("danger", `Segreti`, `Non versionare i segreti e non passarli al frontend. jwtOptions.secret deve essere identico tra le istanze del backend e diverso da quello dei provider SSO.`)
912
+ ]
913
+ },
914
+ // -------------------------------------------------------------------------
915
+ {
916
+ id: "database",
917
+ group: "Configurazione",
918
+ title: "Schema e aggiornamento del database",
919
+ summary: "Tabelle, riconciliazione automatica e CLI.",
920
+ blocks: [
921
+ p(`Accessi possiede lo schema del dominio. Con autoUpdateDatabase true (predefinito) lo riconcilia a ogni avvio; con false verifica comunque la compatibilit\xE0 in sola lettura e blocca gli avvii incompatibili.`),
922
+ h(`Tabelle principali`),
923
+ list([
924
+ `PARAMETRI, UTENTI, UTENTI_CONFIG, UTENTI_PWD, UTENTI_OLDPWD, UTENTI_GDPR`,
925
+ `RUOLI, MENU_GRP, MENU_TIPI, MENU, ABILITAZIONI, RUOLI_MNU, UTENTI_RUOLI`,
926
+ `FILTRI, FILTRI_TIPO`,
927
+ `SSO_PROVIDER, UTENTI_IDENTITA_EXT`,
928
+ `ACCESSI_2FA, ACCESSI_SERVICE_TOKEN`
929
+ ]),
930
+ code("bash", `npm run db:update:accessi # riconcilia lo schema
931
+ npm run db:check:accessi # verifica senza modificare`, "CLI schema"),
932
+ note("warn", `Versioni`, `La libreria richiede uno schema compatibile con la versione corrente (ACCESSI_SCHEMA_VERSION). Se la verifica fallisce, leggi il codice errore ACCESSI_DATABASE_SCHEMA_OUTDATED ed esegui db:update:accessi.`)
933
+ ]
934
+ },
935
+ // -------------------------------------------------------------------------
936
+ {
937
+ id: "login",
938
+ group: "Autenticazione",
939
+ title: "Login locale e JWT",
940
+ summary: "Endpoint di login, risposta e gestione della password scaduta.",
941
+ blocks: [
942
+ code("http", `POST /api/accessi/auth/login
943
+ Content-Type: application/json
944
+
945
+ { "email": "utente@example.com", "password": "Segreta123!" }`, "Login con password"),
946
+ 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).`),
947
+ good("ts", [
948
+ "const res = await fetch('/api/accessi/auth/login', {",
949
+ " method: 'POST',",
950
+ " headers: { 'Content-Type': 'application/json' },",
951
+ " body: JSON.stringify({ email, password }),",
952
+ "});",
953
+ "const body = await res.json();",
954
+ "if (body.Result?.challenge) { /* richiedi il codice */ }",
955
+ "const token = body.Result?.token;"
956
+ ].join("\n"), "Chiamata dal frontend"),
957
+ note("warn", `Password scaduta`, `Con passwordExpiration attivo, una password scaduta produce una PasswordExpiredResponse con code PASSWORD_EXPIRED. Il client deve avviare il reset della password.`),
958
+ h(`Verifica del token`),
959
+ code("http", `POST /api/accessi/auth/get-user-by-token
960
+ Content-Type: application/json
961
+
962
+ { "token": "<jwt>" }`, "get-user-by-token"),
963
+ p(`Usalo per verificare un JWT e ottenere il profilo corrente (utile al bootstrap di una SPA). Restituisce Result.userData.`)
964
+ ]
965
+ },
966
+ // -------------------------------------------------------------------------
967
+ {
968
+ id: "passwordless-2fa",
969
+ group: "Autenticazione",
970
+ title: "Codici email, 2FA e passwordless",
971
+ summary: "Verifica in due passi, challenge e policy per utente.",
972
+ blocks: [
973
+ p(`La verifica con codice email \xE8 per-utente (flagDueFattori / UTENTI_CONFIG.FLG2FATT) ed \xE8 disattivata per impostazione predefinita. Con passwordlessLoginEnabled (FLGPWDLESS, richiede la 2FA attiva) l'accesso avviene con il solo codice email.`),
974
+ table(
975
+ ["Configurazione", "Accesso locale", "Accesso SSO"],
976
+ [
977
+ ["2FA disattivata", "Password", "Provider"],
978
+ ["2FA attiva", "Password + codice email", "Provider + codice email"],
979
+ ["2FA + passwordless", "Solo codice email", "Provider + codice email"]
980
+ ]
981
+ ),
982
+ code("json", `{
983
+ "Result": {
984
+ "challenge": {
985
+ "twoFactorRequired": true,
986
+ "challengeId": "64-caratteri-esadecimali",
987
+ "method": "email",
988
+ "expiresAt": "2026-09-11T14:00:00.000Z",
989
+ "resendAfterSeconds": 60
990
+ }
991
+ }
992
+ }`, "Risposta con challenge"),
993
+ code("http", `POST /api/accessi/auth/two-factor/verify
994
+ { "challengeId": "...", "code": "012345" }
995
+
996
+ POST /api/accessi/auth/two-factor/resend
997
+ { "challengeId": "..." }`, "Verifica e reinvio"),
998
+ list([
999
+ `Il codice ha 6 cifre, dura 10 minuti, consente 5 tentativi totali e al massimo 3 invii per challenge.`,
1000
+ `Fino alla verifica non sono presenti token, profilo o grant.`,
1001
+ `Il challenge non \xE8 un JWT e non va usato nell'header Authorization.`
1002
+ ]),
1003
+ bad("ts", [
1004
+ "// Sbagliato: usare il challenge come token",
1005
+ "fetch('/api/ordini', { headers: { Authorization: 'Bearer ' + challengeId } }); // 401"
1006
+ ].join("\n"), "Il challenge non \xE8 un token")
1007
+ ]
1008
+ },
1009
+ // -------------------------------------------------------------------------
1010
+ {
1011
+ id: "reset-password",
1012
+ group: "Autenticazione",
1013
+ title: "Reset password",
1014
+ summary: "Invio email e conferma con token monouso.",
1015
+ blocks: [
1016
+ code("http", `POST /api/accessi/email/send-reset-password-email
1017
+ { "email": "utente@example.com" }
1018
+
1019
+ POST /api/accessi/auth/confirm-reset-password/{token}
1020
+ { "newPassword": "NuovaSegreta123!" }`, "Reset in due passi"),
1021
+ p(`L'invio non rivela se l'email esiste (anti-enumerazione). Il token di reset \xE8 monouso ed \xE8 firmato con una chiave dedicata.`),
1022
+ note("info", `Pagina personalizzata`, `Imposta customResetPage per usare la tua pagina di reset invece di quella inclusa. confirmationEmailReturnUrl e confirmationEmailPrefix controllano il ritorno.`)
1023
+ ]
1024
+ },
1025
+ // -------------------------------------------------------------------------
1026
+ {
1027
+ id: "middleware",
1028
+ group: "Autorizzazione",
1029
+ title: "authorizeAccessi (middleware)",
1030
+ summary: "Proteggere le rotte della tua app con il JWT Accessi.",
1031
+ blocks: [
1032
+ p(`authorizeAccessi \xE8 un middleware Express che verifica il JWT, ricarica lo stato utente corrente e applica i requisiti. Su successo prosegue; su errore risponde direttamente con il contratto Accessi (non chiamare un tuo next(error)).`),
1033
+ good("ts", [
1034
+ "app.get('/api/report',",
1035
+ " (req, res, next) => authorizeAccessi(req, res, next, {",
1036
+ " requirements: [{ menuCode: 'REPORT', minPermissionLevel: 10 }],",
1037
+ " }),",
1038
+ " (req, res) => res.json({ user: req.user, grants: req.userGrants }),",
1039
+ ");"
1040
+ ].join("\n"), "Rotta protetta"),
1041
+ h(`Che cosa trovi nella request`),
1042
+ list([
1043
+ `req.user e req.data: payload utente corrente (alias storici).`,
1044
+ `req.userGrants: grant effettivi, presente solo quando hai richiesto dei requisiti.`
1045
+ ]),
1046
+ bad("ts", [
1047
+ "app.get('/api/report', authorizeAccessi, (req, res) => {",
1048
+ " // Sbagliato: assumere che req.userGrants esista senza requisiti",
1049
+ " const grants = req.userGrants.grants; // possibile TypeError",
1050
+ "});"
1051
+ ].join("\n"), "Non dare per scontato req.userGrants"),
1052
+ note("danger", `Lo stato utente \xE8 sempre ricontrollato`, `Un JWT valido non basta: se l'utente \xE8 stato bloccato, eliminato o ha cambiato policy, il middleware risponde 401 AUTH_USER_DISABLED.`)
1053
+ ]
1054
+ },
1055
+ // -------------------------------------------------------------------------
1056
+ {
1057
+ id: "requisiti",
1058
+ group: "Autorizzazione",
1059
+ title: "Requisiti e livelli di abilitazione",
1060
+ summary: "DSL accessiRequirement: permission, and, or, not, custom.",
1061
+ blocks: [
1062
+ p(`I requisiti si compongono con accessiRequirement. La forma storica requirements \xE8 un AND di permessi. Il livello \xE8 TipoAbilitazione: NESSUNA=0, LETTURA=10, SCRITTURA=20, SPECIAL=30; il confronto \xE8 tipoAbilitazione >= soglia.`),
1063
+ good("ts", [
1064
+ "import { authorizeAccessi, accessiRequirement as r } from 'emilsoftware-utilities';",
1065
+ "",
1066
+ "// AND (entrambi i menu)",
1067
+ 'const both = r.and(r.permission("ORDINI", 20), r.permission("CLIENTI", 10));',
1068
+ "",
1069
+ "// OR",
1070
+ 'const either = r.or(r.permission("REPORT", 10), r.permission("EXPORT", 20));',
1071
+ "",
1072
+ "// NOT + custom",
1073
+ "const policy = r.and(",
1074
+ ' r.permission("AREA_RISERVATA", 10),',
1075
+ ' r.not(r.permission("BLOCCO_TOTALE", 0)),',
1076
+ ");",
1077
+ "",
1078
+ 'app.get("/api/riservato", (req, res, next) => authorizeAccessi(req, res, next, {',
1079
+ " requirementTree: policy,",
1080
+ " customRequirementHandlers: {",
1081
+ " stessoReparto: async (ctx) => ctx.userCode === Number(ctx.req.query.reparto),",
1082
+ " },",
1083
+ "}), handler);"
1084
+ ].join("\n"), "Policy composte"),
1085
+ bad("ts", [
1086
+ "// Sbagliato: AND/OR senza figli -> errore di configurazione (500 AUTH_REQUIREMENTS_MISCONFIGURED)",
1087
+ "const empty = r.and();"
1088
+ ].join("\n"), "Configurazione non valida"),
1089
+ note("warn", `custom senza handler`, `Un requisito custom la cui chiave non ha un handler produce 500 AUTH_REQUIREMENTS_MISCONFIGURED: \xE8 un errore di programmazione, non un 403.`)
1090
+ ]
1091
+ },
1092
+ // -------------------------------------------------------------------------
1093
+ {
1094
+ id: "utenti",
1095
+ group: "Dominio",
1096
+ title: "Utenti",
1097
+ summary: "Creazione, aggiornamento, stati, GDPR e filtri.",
1098
+ blocks: [
1099
+ h(`Stati di registrazione (STAREG)`),
1100
+ table(
1101
+ ["Valore", "Nome", "Effetto sul login"],
1102
+ [
1103
+ ["0", "NULL", "Non confermato"],
1104
+ ["5", "INSERT", "Non confermato"],
1105
+ ["10", "INVIO", "Richiede il rinnovo della password"],
1106
+ ["20", "CONF", "Valido"],
1107
+ ["50", "DELETE", "Bloccato (soft delete)"],
1108
+ ["99", "BLOCC", "Bloccato"]
1109
+ ]
1110
+ ),
1111
+ h(`Endpoint principali (superutente salvo diversamente indicato)`),
1112
+ table(
1113
+ ["Metodo", "Percorso", "Descrizione"],
1114
+ [
1115
+ ["GET", "/api/accessi/user/get-users", "Elenco con paginazione (limit e offset, header X-Total-Count)."],
1116
+ ["POST", "/api/accessi/user/create-managed-user", "Crea un utente locale e invia il reset password."],
1117
+ ["PUT", "/api/accessi/user/update-user/:codiceUtente", "Aggiorna profilo e policy (campi privilegiati solo superutente)."],
1118
+ ["DELETE", "/api/accessi/user/delete-user/:codiceUtente", "Soft delete: STAREG=DELETE."],
1119
+ ["POST", "/api/accessi/user/set-stato", "Imposta lo stato di registrazione."],
1120
+ ["PATCH", "/api/accessi/user/set-gdpr/:codiceUtente", "Registra il consenso GDPR."],
1121
+ ["POST", "/api/accessi/user/register", "Registrazione pubblica, solo se publicRegistration.enabled."]
1122
+ ]
1123
+ ),
1124
+ good("ts", [
1125
+ "await fetch('/api/accessi/user/create-managed-user', {",
1126
+ " method: 'POST',",
1127
+ " headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + adminToken },",
1128
+ " body: JSON.stringify({",
1129
+ " email: 'nuovo@example.com',",
1130
+ " nome: 'Mario',",
1131
+ " cognome: 'Rossi',",
1132
+ " roles: [1, 2],",
1133
+ " permissions: [{ codiceMenu: 'REPORT', tipoAbilitazione: 10 }],",
1134
+ " }),",
1135
+ "});"
1136
+ ].join("\n"), "Creazione di un utente gestito"),
1137
+ bad("ts", [
1138
+ "// Sbagliato: inviare flag privilegiati dal frontend pubblico",
1139
+ 'fetch("/api/accessi/user/register", {',
1140
+ ' method: "POST",',
1141
+ " body: JSON.stringify({ email, flagSuper: true }), // ignorato o negato",
1142
+ "});"
1143
+ ].join("\n"), "Privilegi dal client"),
1144
+ note("info", `Campi estesi`, `extensionFieldsOptions allega colonne di tabelle applicative al profilo (per esempio la ragione sociale). Le colonne sono in whitelist: non costruirle mai da input HTTP.`)
1145
+ ]
1146
+ },
1147
+ // -------------------------------------------------------------------------
1148
+ {
1149
+ id: "ruoli",
1150
+ group: "Dominio",
1151
+ title: "Ruoli e permessi",
1152
+ summary: "Assegnazioni sostitutive e composizione dei grant.",
1153
+ blocks: [
1154
+ p(`I ruoli contengono una lista di menu con livello. Le assegnazioni (ruoli e permessi diretti) sono SOSTITUTIVE: invia sempre la lista completa desiderata. I grant diretti prevalgono su quelli dei ruoli; i superutenti vedono tutti i menu attivi.`),
1155
+ table(
1156
+ ["Metodo", "Percorso", "Descrizione"],
1157
+ [
1158
+ ["GET", "/api/accessi/permission/roles", "Ruoli con i menu associati."],
1159
+ ["POST", "/api/accessi/permission/create-role", "Crea un ruolo (i menu sono obbligatori)."],
1160
+ ["PUT", "/api/accessi/permission/update-role/:codiceRuolo", "Aggiorna un ruolo (menu sostitutivi)."],
1161
+ ["DELETE", "/api/accessi/permission/delete-role/:codiceRuolo", "Elimina ruolo, mapping e assegnazioni."],
1162
+ ["POST", "/api/accessi/permission/assign-roles/:codiceUtente", `Sostituisce i ruoli dell'utente.`],
1163
+ ["POST", "/api/accessi/permission/assign-permissions/:codiceUtente", "Sostituisce i permessi diretti."],
1164
+ ["GET", "/api/accessi/permission/grants/:codiceUtente", "Grant effettivi, permessi diretti e ruoli."]
1165
+ ]
1166
+ ),
1167
+ good("ts", [
1168
+ "const role = {",
1169
+ " descrizioneRuolo: 'Gestore ordini',",
1170
+ " menu: [",
1171
+ " { codiceMenu: 'ORDINI', tipoAbilitazione: 20 },",
1172
+ " { codiceMenu: 'CLIENTI', tipoAbilitazione: 10 },",
1173
+ " ],",
1174
+ "};",
1175
+ "await createRole(role); // POST /api/accessi/permission/create-role",
1176
+ "",
1177
+ "// Sostitutivo: questa lista diventa l'intero set di ruoli dell'utente",
1178
+ "await assignRolesToUser(codiceUtente, { roles: [3, 7] });"
1179
+ ].join("\n"), "Creazione di un ruolo e assegnazione"),
1180
+ bad("ts", [
1181
+ "// Sbagliato: pensare che l'assegnazione sia incrementale",
1182
+ "await assignRolesToUser(codiceUtente, { roles: [3] });",
1183
+ "// ora l'utente ha SOLO il ruolo 3, non 3 pi\xF9 quelli di prima"
1184
+ ].join("\n"), "Assegnazione non additiva"),
1185
+ note("warn", `Menu obbligatori`, `create-role e update-role rifiutano un ruolo senza menu. Se ti serve un ruolo senza accessi, non crearlo.`)
1186
+ ]
1187
+ },
1188
+ // -------------------------------------------------------------------------
1189
+ {
1190
+ id: "menu",
1191
+ group: "Dominio",
1192
+ title: "Menu, gruppi e tipi menu",
1193
+ summary: "Catalogo di navigazione e CRUD completo (superutente).",
1194
+ blocks: [
1195
+ table(
1196
+ ["Metodo", "Percorso", "Descrizione"],
1197
+ [
1198
+ ["GET", "/api/accessi/permission/menus", "Menu attivi."],
1199
+ ["GET", "/api/accessi/permission/menu-types", "Catalogo dei tipi menu."],
1200
+ ["GET", "/api/accessi/permission/groups-with-menus?includeDisabled=true", "Gruppi con menu (anche disabilitati)."],
1201
+ ["POST", "/api/accessi/permission/menus", "Crea un menu."],
1202
+ ["PUT", "/api/accessi/permission/menus/:codiceMenu", "Aggiorna un menu."],
1203
+ ["DELETE", "/api/accessi/permission/menus/:codiceMenu", "Elimina un menu (con grant e associazioni ai ruoli)."],
1204
+ ["POST", "/api/accessi/permission/menu-groups", "Crea un gruppo."],
1205
+ ["PUT", "/api/accessi/permission/menu-groups/:codiceGruppo", "Aggiorna un gruppo."],
1206
+ ["DELETE", "/api/accessi/permission/menu-groups/:codiceGruppo", "Elimina un gruppo (solo se vuoto)."],
1207
+ ["POST/PUT/DELETE", "/api/accessi/permission/menu-types[/:codiceTipo]", "CRUD dei tipi menu."]
1208
+ ]
1209
+ ),
1210
+ p(`I codici (CODMNU, CODGRP, CODTIP) sono chiavi immutabili: si indicano in creazione e poi si passano nel percorso. Un gruppo o un tipo si elimina solo se non \xE8 referenziato.`),
1211
+ good("ts", [
1212
+ "await createMenuGroup({ codiceGruppo: 'A', descrizioneGruppo: 'Amministrazione', ordineGruppo: 1, enabled: true });",
1213
+ "await createMenuType({ codiceTipo: 'X', descrizioneTipo: 'Operativo' });",
1214
+ "await createMenu({ codiceMenu: 'ORDINI', descrizioneMenu: 'Ordini', codiceGruppo: 'A', tipo: 'X', ordineMenu: 10, enabled: true });"
1215
+ ].join("\n"), "Catalogo in ordine"),
1216
+ bad("ts", [
1217
+ "// Sbagliato: eliminare un gruppo che contiene menu -> 400 con spiegazione",
1218
+ "await deleteMenuGroup('A'); // contiene N menu"
1219
+ ].join("\n"), "Vincoli di integrit\xE0")
1220
+ ]
1221
+ },
1222
+ // -------------------------------------------------------------------------
1223
+ {
1224
+ id: "filtri",
1225
+ group: "Dominio",
1226
+ title: "Filtri applicativi",
1227
+ summary: "Filtri per utente e catalogo dei tipi filtro.",
1228
+ blocks: [
1229
+ table(
1230
+ ["Metodo", "Percorso", "Descrizione"],
1231
+ [
1232
+ ["GET", "/api/accessi/filtri/tipi", "Catalogo dei tipi filtro."],
1233
+ ["POST", "/api/accessi/filtri/tipi", "Crea un tipo filtro (superutente)."],
1234
+ ["PUT", "/api/accessi/filtri/tipi/:tipFil", "Aggiorna un tipo filtro."],
1235
+ ["DELETE", "/api/accessi/filtri/tipi/:tipFil", "Elimina un tipo filtro (se non \xE8 usato)."],
1236
+ ["GET", "/api/accessi/filtri/utente?codUte=123", "Filtri di un utente (self o superutente)."],
1237
+ ["POST", "/api/accessi/filtri/utente", "Upsert dei filtri (self o superutente)."]
1238
+ ]
1239
+ ),
1240
+ 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.`)
1241
+ ]
1242
+ },
1243
+ // -------------------------------------------------------------------------
1244
+ {
1245
+ id: "sso",
1246
+ group: "SSO",
1247
+ title: "SSO generico (federated authentication)",
1248
+ summary: "Il backend valida il provider, Accessi gestisce identit\xE0 e sessioni.",
1249
+ blocks: [
1250
+ p(`Accessi non valida i token Azure, Google o SAML: \xE8 il TUO backend a validare il flusso SSO e a passare solo { provider, subject } verificati a FederatedAuthService. Cos\xEC issuer, secret e token restano fuori dal modulo.`),
1251
+ good("ts", [
1252
+ "import { FederatedAuthService } from 'emilsoftware-utilities';",
1253
+ "",
1254
+ "// Nel tuo callback SSO, dopo aver validato il token del provider:",
1255
+ 'const verified = { provider: "azure-ad-acme", subject: claims.oid };',
1256
+ "const result = await federatedAuthService.authenticate(verified);",
1257
+ "",
1258
+ "if (result.login.challenge) {",
1259
+ " // 2FA attiva: restituisci il challenge al frontend",
1260
+ " return res.json({ challenge: result.login.challenge });",
1261
+ "}",
1262
+ "return res.json({ login: result.login, token: result.token });"
1263
+ ].join("\n"), "Login SSO"),
1264
+ h(`API amministrative (superutente)`),
1265
+ list([
1266
+ `GET/POST /api/accessi/federated-auth/providers, PATCH/DELETE providers/:provider`,
1267
+ `GET/POST /api/accessi/federated-auth/users/:codiceUtente/identities`,
1268
+ `POST /api/accessi/federated-auth/users (crea un utente SSO-only)`,
1269
+ `PATCH /api/accessi/federated-auth/users/:codiceUtente/password-login`
1270
+ ]),
1271
+ bad("ts", [
1272
+ "// Sbagliato: far validare il token del provider ad Accessi o fidarsi del frontend",
1273
+ "await federatedAuthService.authenticate({ provider, subject: req.body.email }); // subject non verificato"
1274
+ ].join("\n"), `Non fidarti dell'input`),
1275
+ note("warn", `Provider in uso`, `Un provider si elimina solo se nessuna identit\xE0 \xE8 collegata; altrimenti disabilitalo. La chiave del provider deve coincidere con la configurazione del backend.`)
1276
+ ]
1277
+ },
1278
+ // -------------------------------------------------------------------------
1279
+ {
1280
+ id: "service-token",
1281
+ group: "Token di servizio",
1282
+ title: "Token macchina-a-macchina",
1283
+ summary: "Autenticazione tecnica con scope: non \xE8 un superutente.",
1284
+ blocks: [
1285
+ p(`Formato: st_<id>.<segreto> (segreto da 256 bit). Nel database si salva solo lo SHA-256 del segreto; il segreto \xE8 mostrato una sola volta. La verifica restituisce { tokenId, label, scopes }: nessun ruolo o grant.`),
1286
+ note("danger", `Non \xE8 un superutente`, `Un service token non apre le API Accessi: abilita solo le rotte della tua app che proteggi con ServiceTokenGuard.`),
1287
+ good("ts", [
1288
+ "import { ServiceTokenGuard, RequireServiceTokenScopes } from 'emilsoftware-utilities';",
1289
+ "",
1290
+ "@UseGuards(ServiceTokenGuard)",
1291
+ "@RequireServiceTokenScopes('ia')",
1292
+ '@Post("internal/ask")',
1293
+ "ask() { /* req.accessiServiceToken = { tokenId, label, scopes } */ }"
1294
+ ].join("\n"), "Rotta tecnica protetta"),
1295
+ h(`Gestione (superutente)`),
1296
+ list([
1297
+ `POST /api/accessi/service-token (label, scopes, expiresAt o ttlDays) -> segreto mostrato una volta`,
1298
+ `GET /api/accessi/service-token?includeRevoked=true`,
1299
+ `DELETE /api/accessi/service-token/:tokenId (revoca immediata)`,
1300
+ `POST /api/accessi/service-token/:tokenId/rotate`
1301
+ ]),
1302
+ bad("ts", [
1303
+ "// Sbagliato: usare un service token per autenticare un utente umano",
1304
+ "fetch('/api/profilo', { headers: { Authorization: 'Bearer ' + serviceToken } }); // req.user assente"
1305
+ ].join("\n"), "Non \xE8 un utente"),
1306
+ note("warn", `Scadenza`, `Senza expiresAt n\xE9 defaultTtlDays il token non scade: imposta serviceTokens.defaultTtlDays o una scadenza esplicita.`)
1307
+ ]
1308
+ },
1309
+ // -------------------------------------------------------------------------
1310
+ {
1311
+ id: "console",
1312
+ group: "Console",
1313
+ title: "Console amministrativa",
1314
+ summary: "Accesso, sezioni e che cosa si pu\xF2 fare.",
1315
+ blocks: [
1316
+ p(`La console \xE8 servita dal modulo su /api/accessi/console e richiede un superutente Accessi. Non ha stato proprio: ogni azione chiama le API protette con il JWT dell'utente connesso.`),
1317
+ list([
1318
+ `Utenti: elenco, creazione locale o SSO, profilo, ruoli, grant diretti, stato, 2FA.`,
1319
+ `Ruoli e grant: CRUD dei ruoli con l'albero delle abilitazioni.`,
1320
+ `Menu e gruppi: CRUD di gruppi, menu e tipi menu.`,
1321
+ `Filtri: filtri utente e tipi filtro.`,
1322
+ `Token di servizio: creazione, rotazione, revoca.`,
1323
+ `SSO: provider e identit\xE0.`
1324
+ ]),
1325
+ note("info", `Accesso`, `Apri /api/accessi/console e accedi con un utente con flagSuper=true. Le sezioni sono raggiungibili anche tramite deep link (per esempio /api/accessi/console/menu-e-gruppi/menu).`)
1326
+ ]
1327
+ },
1328
+ // -------------------------------------------------------------------------
1329
+ {
1330
+ id: "errori",
1331
+ group: "Riferimenti",
1332
+ title: "Contratto errori e codici",
1333
+ summary: "Formato della risposta e codici stabili ACCESSI_* e AUTH_*.",
1334
+ blocks: [
1335
+ p(`Gli errori HTTP delle API Accessi usano un contratto stabile: severity, status, statusCode, code, error (campo legacy uguale a code), message e details per gli errori di validazione.`),
1336
+ code("json", `{
1337
+ "severity": "error",
1338
+ "status": 400,
1339
+ "statusCode": 2,
1340
+ "code": "ACCESSI_VALIDATION_ERROR",
1341
+ "error": "ACCESSI_VALIDATION_ERROR",
1342
+ "message": "La richiesta contiene dati non validi.",
1343
+ "details": ["email: deve essere una stringa."]
1344
+ }`, "Errore di validazione"),
1345
+ table(
1346
+ ["Codice", "Quando"],
1347
+ [
1348
+ ["ACCESSI_VALIDATION_ERROR", "Body o parametri non validi (400)."],
1349
+ ["ACCESSI_AUTH_INITIALIZING", "Bootstrap in corso (503)."],
1350
+ ["ACCESSI_AUTH_NOT_INITIALIZED", "Modulo non inizializzato (500)."],
1351
+ ["AUTH_HEADER_MISSING / AUTH_TOKEN_MISSING", "Header Authorization assente (401)."],
1352
+ ["AUTH_TOKEN_INVALID", "JWT non valido o scaduto (401)."],
1353
+ ["AUTH_USER_DISABLED", "Utente non pi\xF9 autorizzato (401)."],
1354
+ ["AUTH_INSUFFICIENT_PERMISSIONS", "Requisiti non soddisfatti (403)."],
1355
+ ["AUTH_REQUIREMENTS_MISCONFIGURED", "Policy malformata o handler custom mancante (500)."],
1356
+ ["ACCESSI_DATABASE_SCHEMA_OUTDATED", "Schema del database incompatibile."],
1357
+ ["ACCESSI_SERVICE_TOKEN_RATE_LIMITED", "Troppi tentativi (429)."]
1358
+ ]
1359
+ )
1360
+ ]
1361
+ },
1362
+ // -------------------------------------------------------------------------
1363
+ {
1364
+ id: "endpoint",
1365
+ group: "Riferimenti",
1366
+ title: "Indice degli endpoint",
1367
+ summary: "Tutte le rotte /api/accessi/* in una tabella.",
1368
+ blocks: [
1369
+ table(
1370
+ ["Area", "Endpoint principali"],
1371
+ [
1372
+ ["Auth", "POST auth/login, auth/get-user-by-token, auth/confirm-reset-password/:token, auth/two-factor/verify, auth/two-factor/resend"],
1373
+ ["Email", "POST email/send-reset-password-email, GET email/reset-password-page/:token"],
1374
+ ["Utenti", "GET user/get-users, POST user/create-managed-user, PUT user/update-user/:codiceUtente, DELETE user/delete-user/:codiceUtente, POST user/set-stato, PATCH user/set-gdpr/:codiceUtente, POST user/register"],
1375
+ ["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"],
1376
+ ["Catalogo", "POST/PUT/DELETE permission/menus[/:codiceMenu], permission/menu-groups[/:codiceGruppo], permission/menu-types[/:codiceTipo]"],
1377
+ ["Configurator", "PATCH configurator/menus/:codiceMenu/enabled, PATCH configurator/groups/:codiceGruppo/enabled"],
1378
+ ["Filtri", "GET filtri/tipi, POST/PUT/DELETE filtri/tipi[/:tipFil], GET/POST filtri/utente"],
1379
+ ["SSO", "GET/POST federated-auth/providers, PATCH/DELETE providers/:provider, GET/POST users/:codiceUtente/identities, POST users, PATCH users/:codiceUtente/password-login"],
1380
+ ["Token", "POST/GET service-token, DELETE service-token/:tokenId, POST service-token/:tokenId/rotate"],
1381
+ ["Console", "GET console, console/:view, console/:view/:sub, console/assets/:file"]
1382
+ ]
1383
+ )
1384
+ ]
1385
+ },
1386
+ // -------------------------------------------------------------------------
1387
+ {
1388
+ id: "antipattern",
1389
+ group: "Riferimenti",
1390
+ title: "Antipattern e checklist",
1391
+ summary: "Che cosa non fare, in breve.",
1392
+ blocks: [
1393
+ list([
1394
+ `Non aprire la porta HTTP prima di initializeAccessiModule.`,
1395
+ `Non usare authorizeAccessi come unico strato: valida sempre anche l'input applicativo.`,
1396
+ `Non dare per scontato che req.userGrants esista se non hai richiesto dei requisiti.`,
1397
+ `Non considerare incrementali assignRolesToUser e assignPermissionsToUser: sono sostitutivi.`,
1398
+ `Non far validare i token SSO ad Accessi: passagli solo { provider, subject } verificati.`,
1399
+ `Non usare un service token come identit\xE0 utente.`,
1400
+ `Non versionare segreti; jwtOptions.secret deve essere unico per ambiente.`,
1401
+ `Non eliminare gruppi o tipi in uso: gestisci l'errore 400 e disabilita invece.`,
1402
+ `Non usare il challenge 2FA come JWT.`
1403
+ ]),
1404
+ note("ok", `Checklist di rilascio`, `Schema verificato (db:check), segreti da un secret manager, mockDemoUser=false, rate limit pubblico configurato, SMTP reale testato, serviceTokens.defaultTtlDays impostato.`)
1405
+ ]
1406
+ }
1407
+ ];
1408
+ function wikiGroups() {
1409
+ const groups = [];
1410
+ for (const section of WIKI_SECTIONS) {
1411
+ let bucket = groups.find((item) => item.group === section.group);
1412
+ if (!bucket) {
1413
+ bucket = { group: section.group, sections: [] };
1414
+ groups.push(bucket);
1415
+ }
1416
+ bucket.sections.push(section);
1417
+ }
1418
+ return groups;
1419
+ }
1420
+ function buildAiDigest() {
1421
+ const lines = [
1422
+ `# Accessi (emilsoftware-utilities) - guida compatta per l'integrazione backend`,
1423
+ "",
1424
+ `Modulo NestJS/Express + Firebird per autenticazione JWT, autorizzazione per menu, utenti, 2FA e passwordless, SSO generico, service token e console.`,
1425
+ ""
1426
+ ];
1427
+ for (const { group, sections } of wikiGroups()) {
1428
+ lines.push(`## ${group}`);
1429
+ for (const section of sections) {
1430
+ lines.push(`### ${section.title}`);
1431
+ lines.push(section.summary);
1432
+ for (const block of section.blocks) {
1433
+ switch (block.kind) {
1434
+ case "p":
1435
+ lines.push(block.text);
1436
+ break;
1437
+ case "h":
1438
+ lines.push(`**${block.text}**`);
1439
+ break;
1440
+ case "list":
1441
+ block.items.forEach((item, index) => lines.push(`${block.ordered ? `${index + 1}.` : "-"} ${item}`));
1442
+ break;
1443
+ case "code":
1444
+ lines.push("```" + block.language);
1445
+ lines.push(block.code);
1446
+ lines.push("```");
1447
+ break;
1448
+ case "callout":
1449
+ lines.push(`[${block.tone.toUpperCase()}] ${block.title ? block.title + ": " : ""}${block.text}`);
1450
+ break;
1451
+ case "table":
1452
+ lines.push("| " + block.head.join(" | ") + " |");
1453
+ lines.push("| " + block.head.map(() => "---").join(" | ") + " |");
1454
+ block.rows.forEach((row) => lines.push("| " + row.join(" | ") + " |"));
1455
+ break;
1456
+ }
1457
+ }
1458
+ lines.push("");
1459
+ }
1460
+ }
1461
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
1462
+ }
1463
+
1464
+ // src/accessi-module/Console/help.ts
1465
+ var p2 = (text) => ({ kind: "p", text });
1466
+ var list2 = (items) => ({ kind: "list", items });
1467
+ var code2 = (language, codeText, title) => ({ kind: "code", language, title, code: codeText });
1468
+ var note2 = (tone, title, text) => ({ kind: "callout", tone, title, text });
1469
+ var table2 = (head, rows) => ({ kind: "table", head, rows });
1470
+ var HELP = {
1471
+ // ------------------------------------------------------------------ SSO
1472
+ "sso-provider": {
1473
+ title: "Provider SSO",
1474
+ blocks: [
1475
+ p2(`La chiave del provider censito in Accessi. Deve coincidere esattamente con quella usata dal backend quando valida il login SSO e chiama FederatedAuthService.authenticate({ provider, subject }).`),
1476
+ p2(`Se il backend usa la chiave azure-ad-acme-prod, qui va la stessa stringa: una differenza, anche di un carattere, fa fallire il collegamento.`),
1477
+ note2("info", "Chi crea il provider", `Prima si censisce il provider in "SSO \u2192 Provider" (superutente), poi lo si sceglie qui. Se l'elenco \xE8 vuoto, non ci sono provider attivi.`)
1478
+ ]
1479
+ },
1480
+ "sso-subject": {
1481
+ title: "ID utente del provider (subject)",
1482
+ blocks: [
1483
+ p2(`\xC8 l'identificatore STABILE della persona nel provider, non l'email. Il backend lo ottiene dopo aver validato il token del provider e lo passa ad Accessi; Accessi non vede mai il token esterno.`),
1484
+ p2(`Ecco il claim corretto per i provider pi\xF9 comuni:`),
1485
+ table2(
1486
+ ["Provider", "Claim da usare", "Note"],
1487
+ [
1488
+ ["Azure AD / Entra ID", "oid", "Object ID. Non usare preferred_username n\xE9 email."],
1489
+ ["Google", "sub", "ID numerico di 21 cifre. Non l indirizzo email."],
1490
+ ["Apple", "sub", "Identificatore stabile per la tua app; disponibile solo al primo login, va salvato subito."],
1491
+ ["Okta", "sub", "Claim standard OIDC."],
1492
+ ["Auth0", "sub", "Formato auth0|... oppure google-oauth2|... ."],
1493
+ ["Keycloak", "sub", "Claim standard OIDC."],
1494
+ ["Microsoft personale (MSA)", "sub", "Account outlook/live."],
1495
+ ["SAML 2.0", "NameID", "Identificatore persistente dell utente."],
1496
+ ["OIDC generico", "sub", "Claim standard OIDC."]
1497
+ ]
1498
+ ),
1499
+ note2("danger", "Mai usare l email", `L'email pu\xF2 cambiare e non \xE8 un identificatore stabile: usarla come subject crea duplicati e collegamenti errati.`),
1500
+ code2("ts", [
1501
+ "// Nel backend, dopo aver validato il token del provider:",
1502
+ "// Azure AD / Entra ID",
1503
+ "const subject = claims.oid;",
1504
+ "// Google / Okta / Keycloak / OIDC generico",
1505
+ "const subject = claims.sub;",
1506
+ "// Apple (disponibile al primo login, salvalo subito)",
1507
+ "const subject = claims.sub;",
1508
+ "// SAML",
1509
+ "const subject = assertion.nameId;"
1510
+ ].join("\n"), "Come ricavare il subject")
1511
+ ]
1512
+ },
1513
+ "sso-note": {
1514
+ title: "Nota",
1515
+ blocks: [
1516
+ p2(`Nota interna per gli amministratori, utile a ricordare perch\xE9 il collegamento esiste (per esempio "migrato da AD il 2026-01-10"). Non viene inviata al provider.`)
1517
+ ]
1518
+ },
1519
+ "provider-key": {
1520
+ title: "Chiave provider",
1521
+ blocks: [
1522
+ p2(`Identificativo tecnico del provider, usato dal backend nel payload SSO. Scegli una chiave descrittiva e stabile, per esempio azure-ad-acme-prod o google-workspace-acme.`),
1523
+ note2("warn", "Immutabile", `Dopo il censimento la chiave non si modifica: identit\xE0 e configurazione del backend la referenziano. Se serve cambiarla, disabilita il provider e creane uno nuovo.`)
1524
+ ]
1525
+ },
1526
+ "provider-description": {
1527
+ title: "Descrizione",
1528
+ blocks: [
1529
+ p2(`Nome leggibile mostrato agli amministratori (per esempio "Azure AD \u2013 Acme produzione"). Non \xE8 la chiave tecnica: pu\xF2 contenere spazi.`)
1530
+ ]
1531
+ },
1532
+ "provider-note": {
1533
+ title: "Nota",
1534
+ blocks: [
1535
+ p2(`Nota interna del catalogo. Non inserire segreti, issuer, client id o dati personali: quei valori restano nel backend.`)
1536
+ ]
1537
+ },
1538
+ "provider-active": {
1539
+ title: "Provider attivo",
1540
+ blocks: [
1541
+ p2(`Se disattivato, il provider non accetta nuovi login e nuovi collegamenti; le identit\xE0 e lo storico restano. Riattivalo quando il backend \xE8 pronto.`)
1542
+ ]
1543
+ },
1544
+ // --------------------------------------------------------- service token
1545
+ "token-label": {
1546
+ title: "Descrizione del token",
1547
+ blocks: [
1548
+ p2(`Nome del token, mostrato agli amministratori (per esempio "Integrazione IA \u2013 produzione"). Non \xE8 un segreto.`)
1549
+ ]
1550
+ },
1551
+ "token-scopes": {
1552
+ title: "Scope",
1553
+ blocks: [
1554
+ p2(`Etichette che il backend controlla per autorizzare le chiamate tecniche. Un token non \xE8 un superutente: abilita solo le rotte protette con ServiceTokenGuard e @RequireServiceTokenScopes.`),
1555
+ list2([
1556
+ `Formato: lettere, numeri, ':', '_' o '-', massimo 40 caratteri.`,
1557
+ `Esempi: ia, chat, tools, report:read.`,
1558
+ `Separa gli scope con una virgola.`
1559
+ ]),
1560
+ code2("ts", [
1561
+ "@UseGuards(ServiceTokenGuard)",
1562
+ "@RequireServiceTokenScopes('ia')",
1563
+ '@Post("internal/ask")',
1564
+ "ask() { /* il token deve avere lo scope ia */ }"
1565
+ ].join("\n"), "Come li usa il backend"),
1566
+ note2("warn", "Scope vuoto", `Senza scope il token non soddisfa nessun @RequireServiceTokenScopes, ma passa comunque le rotte protette senza scope richiesto.`)
1567
+ ]
1568
+ },
1569
+ "token-ttl": {
1570
+ title: "Durata (giorni)",
1571
+ blocks: [
1572
+ p2(`Giorni di validit\xE0 del token. Alla scadenza il token smette di funzionare.`),
1573
+ note2("danger", "Nessuna scadenza", `Se lasci vuoto questo campo e non imposti una data, il token non scade mai: sconsigliato in produzione.`)
1574
+ ]
1575
+ },
1576
+ // ----------------------------------------------------------------- utenti
1577
+ "user-email": {
1578
+ title: "Email",
1579
+ blocks: [
1580
+ p2(`Identificativo di accesso dell'utente e recapito per le email di reset o codici. Deve essere univoca.`)
1581
+ ]
1582
+ },
1583
+ "user-name": {
1584
+ title: "Nome e cognome",
1585
+ blocks: [
1586
+ p2(`Dati anagrafici mostrati in console e restituiti nel profilo. Non incidono sull'autenticazione.`)
1587
+ ]
1588
+ },
1589
+ "user-password-login": {
1590
+ title: "Login con password",
1591
+ blocks: [
1592
+ p2(`Se disattivato, il login con email e password viene rifiutato (PASSWORD_LOGIN_DISABLED). Restano validi i login con identit\xE0 SSO attive.`),
1593
+ note2("info", "Utente solo SSO", `\xC8 l'opzione tipica per gli utenti federati: nessuna password locale, accesso solo dal provider.`)
1594
+ ]
1595
+ },
1596
+ "user-2fa": {
1597
+ title: "Codice via email (2FA)",
1598
+ blocks: [
1599
+ p2(`Richiede un codice monouso inviato per email dopo la password o dopo il provider SSO. \xC8 per-utente e disattivata per impostazione predefinita.`),
1600
+ list2([
1601
+ `Codice a 6 cifre, valido 10 minuti, 5 tentativi, massimo 3 invii.`,
1602
+ `Non sono token: fino alla verifica non esiste alcun JWT.`
1603
+ ])
1604
+ ]
1605
+ },
1606
+ "user-passwordless": {
1607
+ title: "Accesso con il solo codice email",
1608
+ blocks: [
1609
+ p2(`Abilita il login senza password: si inserisce solo l'email e si usa il codice ricevuto. Richiede il codice email (2FA) attivo.`),
1610
+ note2("warn", "Non sono due fattori", `L'accesso con il solo codice \xE8 autenticazione tramite possesso della casella email: un solo fattore.`)
1611
+ ]
1612
+ },
1613
+ "user-state": {
1614
+ title: "Stato di registrazione",
1615
+ blocks: [
1616
+ table2(
1617
+ ["Stato", "Significato"],
1618
+ [
1619
+ ["Non definito / Inserito", "Utente non confermato."],
1620
+ ["Invitato (10)", "Richiede il rinnovo della password."],
1621
+ ["Confermato (20)", "Utente valido, pu\xF2 accedere."],
1622
+ ["Eliminato (50)", "Soft delete: login bloccato."],
1623
+ ["Bloccato (99)", "Login bloccato."]
1624
+ ]
1625
+ )
1626
+ ]
1627
+ },
1628
+ // ------------------------------------------------------------------ ruoli
1629
+ "role-description": {
1630
+ title: "Descrizione del ruolo",
1631
+ blocks: [
1632
+ p2(`Nome del ruolo (per esempio "Gestore ordini"). Deve avere almeno un menu selezionato: il salvataggio sostituisce integralmente i menu del ruolo.`)
1633
+ ]
1634
+ },
1635
+ "role-level": {
1636
+ title: "Livello di abilitazione",
1637
+ blocks: [
1638
+ table2(
1639
+ ["Livello", "Valore", "Uso tipico"],
1640
+ [
1641
+ ["Lettura", "10", "Sola consultazione."],
1642
+ ["Scrittura", "20", "Modifica dei dati."],
1643
+ ["Legacy (30)", "30", "Valore storico, mantenuto senza perdita."]
1644
+ ]
1645
+ ),
1646
+ p2(`Il controllo confronta tipoAbilitazione >= soglia: un livello 20 soddisfa anche una richiesta di livello 10.`)
1647
+ ]
1648
+ },
1649
+ // --------------------------------------------------------- catalogo menu
1650
+ "menu-code": {
1651
+ title: "Codice menu",
1652
+ blocks: [
1653
+ p2(`Chiave tecnica del menu, stabile e senza spazi (per esempio ORDINI o MNU001). \xC8 la chiave primaria: non si modifica dopo la creazione.`),
1654
+ note2("warn", "Immutabile", `Il codice \xE8 referenziato da ruoli e grant: per cambiarlo, elimina e ricrea il menu.`)
1655
+ ]
1656
+ },
1657
+ "menu-group-code": {
1658
+ title: "Codice gruppo",
1659
+ blocks: [
1660
+ p2(`Un solo carattere (per esempio A). Raggruppa i menu nella navigazione. Immutabile dopo la creazione.`)
1661
+ ]
1662
+ },
1663
+ "menu-type-code": {
1664
+ title: "Codice tipo menu",
1665
+ blocks: [
1666
+ p2(`Un solo carattere (per esempio X). Classifica i menu (amministrazione, operativo, ...). Immutabile dopo la creazione.`)
1667
+ ]
1668
+ },
1669
+ "menu-group-order": {
1670
+ title: "Ordine del gruppo",
1671
+ blocks: [
1672
+ p2(`Numero che determina l'ordine dei gruppi nella navigazione: valori pi\xF9 bassi compaiono prima.`)
1673
+ ]
1674
+ },
1675
+ "menu-order": {
1676
+ title: "Ordine del menu",
1677
+ blocks: [
1678
+ p2(`Numero che determina l'ordine dei menu all'interno del gruppo: valori pi\xF9 bassi compaiono prima.`)
1679
+ ]
1680
+ },
1681
+ "menu-page": {
1682
+ title: "Pagina",
1683
+ blocks: [
1684
+ p2(`Percorso o nome della pagina del frontend associata al menu (per esempio /ordini). \xC8 informativo: lo usano la UI host e i client.`)
1685
+ ]
1686
+ },
1687
+ "menu-icon": {
1688
+ title: "Icona",
1689
+ blocks: [
1690
+ p2(`Identificatore dell'icona usato dalla UI host (per esempio fa-box o un nome concordato). Accessi non impone un set di icone.`)
1691
+ ]
1692
+ },
1693
+ "menu-rifmenu": {
1694
+ title: "Menu di riferimento",
1695
+ blocks: [
1696
+ p2(`Codice di un altro menu che fa da padre, per costruire gerarchie di menu. Lascia vuoto per un menu di primo livello.`)
1697
+ ]
1698
+ },
1699
+ "menu-note": {
1700
+ title: "Nota",
1701
+ blocks: [
1702
+ p2(`Nota descrittiva mostrata agli amministratori. Non incide sui permessi.`)
1703
+ ]
1704
+ },
1705
+ "menu-enabled": {
1706
+ title: "Menu abilitato",
1707
+ blocks: [
1708
+ p2(`Se disattivato, il menu non compare nel catalogo attivo e non viene considerato nel calcolo dei grant. I collegamenti a ruoli e utenti restano.`)
1709
+ ]
1710
+ },
1711
+ "menu-group-enabled": {
1712
+ title: "Gruppo abilitato",
1713
+ blocks: [
1714
+ p2(`Disattiva l'intero gruppo: i menu al suo interno non sono considerati attivi, ma restano configurabili per una riattivazione futura.`)
1715
+ ]
1716
+ },
1717
+ // ----------------------------------------------------------------- filtri
1718
+ "filter-type-code": {
1719
+ title: "Codice tipo filtro",
1720
+ blocks: [
1721
+ p2(`Numero identificativo del tipo filtro. \xC8 la chiave primaria: non si modifica dopo la creazione.`)
1722
+ ]
1723
+ },
1724
+ "filter-type-field": {
1725
+ title: "Campo",
1726
+ blocks: [
1727
+ 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.`)
1728
+ ]
1729
+ },
1730
+ "filter-type-enabled": {
1731
+ title: "Tipo filtro abilitato",
1732
+ blocks: [
1733
+ p2(`Se disattivato, il tipo non \xE8 proposto ai client, ma i filtri gi\xE0 salvati restano nel database.`)
1734
+ ]
1735
+ },
1736
+ "filters-json": {
1737
+ title: "Filtro JSON",
1738
+ blocks: [
1739
+ p2(`Rappresentazione JSON dei filtri applicativi dell'utente. Usa "Carica" per partire dalla struttura esistente, modifica i valori e poi "Salva".`),
1740
+ 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.`)
1741
+ ]
1742
+ },
1743
+ "filters-user": {
1744
+ title: "Utente",
1745
+ blocks: [
1746
+ p2(`Utente a cui appartengono i filtri. Selezionalo prima di caricare o salvare: i filtri sono sempre per singolo utente.`)
1747
+ ]
1748
+ },
1749
+ // ------------------------------------------------------------ assegnazioni
1750
+ "role-assignment": {
1751
+ title: "Assegnazione dei ruoli",
1752
+ blocks: [
1753
+ p2(`Il salvataggio SOSTITUISCE integralmente i ruoli dell'utente: la lista selezionata diventa l'unico set di ruoli.`),
1754
+ list2([
1755
+ `Per aggiungere un ruolo, selezionalo mantenendo quelli gi\xE0 presenti.`,
1756
+ `Per rimuovere un ruolo, deselezionalo.`,
1757
+ `Non modificare i grant diretti: quelli si gestiscono nella scheda "Grant diretti".`
1758
+ ]),
1759
+ note2("warn", "Non \xE8 incrementale", `Inviare solo il nuovo ruolo rimuove gli altri. Invia sempre la lista completa desiderata.`)
1760
+ ]
1761
+ },
1762
+ "grant-direct": {
1763
+ title: "Grant diretti",
1764
+ blocks: [
1765
+ p2(`Permessi assegnati direttamente all'utente, indipendenti dai ruoli. Hanno precedenza sui permessi ereditati dai ruoli per lo stesso menu.`),
1766
+ list2([
1767
+ `Imposta il livello per concedere l'accesso.`,
1768
+ `Scegli "nessuno" per rimuovere il grant diretto.`,
1769
+ `Il salvataggio sostituisce l'intero set di grant diretti.`
1770
+ ])
1771
+ ]
1772
+ },
1773
+ "grant-level": {
1774
+ title: "Livello di grant",
1775
+ blocks: [
1776
+ table2(
1777
+ ["Voce", "Valore", "Significato"],
1778
+ [
1779
+ ["nessuno", "\u2014", "Nessun grant diretto per quel menu (il permesso pu\xF2 arrivare dai ruoli)."],
1780
+ ["Lettura", "10", "Sola consultazione."],
1781
+ ["Scrittura", "20", "Modifica dei dati."]
1782
+ ]
1783
+ ),
1784
+ p2(`Il confronto \xE8 tipoAbilitazione >= soglia richiesta dalla rotta.`)
1785
+ ]
1786
+ },
1787
+ "user-delete": {
1788
+ title: "Imposta stato eliminato",
1789
+ blocks: [
1790
+ p2(`Esegue un soft delete: imposta lo stato di registrazione a "Eliminato" (50). L'utente non pu\xF2 pi\xF9 accedere, ma i dati restano nel database.`),
1791
+ note2("info", "Reversibile", `Non \xE8 una cancellazione fisica: puoi ripristinare l'utente cambiando lo stato di registrazione.`)
1792
+ ]
1793
+ },
1794
+ // ----------------------------------------------------- selezioni di menu
1795
+ "menu-group-select": {
1796
+ title: "Gruppo",
1797
+ blocks: [
1798
+ p2(`Gruppo di appartenenza del menu. Se l'elenco \xE8 vuoto, crea prima un gruppo in "Menu e gruppi \u2192 Gruppi".`)
1799
+ ]
1800
+ },
1801
+ "menu-type-select": {
1802
+ title: "Tipo menu",
1803
+ blocks: [
1804
+ p2(`Classificazione del menu (amministrazione, operativo, ...). \xC8 facoltativa: se non serve, lascia "Nessun tipo". I tipi si gestiscono in "Menu e gruppi \u2192 Tipi menu".`)
1805
+ ]
1806
+ },
1807
+ // ------------------------------------------------------------------ login
1808
+ "login-email": {
1809
+ title: "Email o utente",
1810
+ blocks: [
1811
+ p2(`Indirizzo email dell'utente Accessi. Se l'account \xE8 abilitato al solo codice, dopo questo passo riceverai un codice via email invece di inserire la password.`)
1812
+ ]
1813
+ },
1814
+ "login-password": {
1815
+ title: "Password",
1816
+ blocks: [
1817
+ p2(`Password locale dell'utente. Non serve per gli account passwordless, che accedono con il solo codice email.`)
1818
+ ]
1819
+ }
1820
+ };
440
1821
 
441
1822
  // src/accessi-module/Console/console.ts
442
1823
  var routeSegment = {
443
1824
  users: "utenti",
444
1825
  roles: "ruoli-e-grant",
445
- menus: "menu-e-gruppi",
446
- filters: "filtri",
447
- sso: "sso"
1826
+ menuGroups: "menu-e-gruppi/gruppi",
1827
+ menuItems: "menu-e-gruppi/menu",
1828
+ menuTypes: "menu-e-gruppi/tipi-menu",
1829
+ filterUser: "filtri/utente",
1830
+ filterTypes: "filtri/tipi",
1831
+ ssoProviders: "sso/provider",
1832
+ ssoUsers: "sso/utenti",
1833
+ tokens: "token-di-servizio",
1834
+ wiki: "wiki"
448
1835
  };
449
1836
  var routeView = Object.fromEntries(Object.entries(routeSegment).map(([view, segment]) => [segment, view]));
1837
+ var parentDefaultView = {
1838
+ "menu-e-gruppi": "menuGroups",
1839
+ filtri: "filterUser",
1840
+ sso: "ssoProviders"
1841
+ };
450
1842
  var consoleMarker = "/accessi/console";
451
1843
  var consoleMarkerIndex = window.location.pathname.indexOf(consoleMarker);
452
1844
  var consoleBasePath = consoleMarkerIndex >= 0 ? window.location.pathname.slice(0, consoleMarkerIndex + consoleMarker.length) : `${window.location.pathname.replace(/\/$/, "")}`;
1845
+ var USERS_PAGE_SIZE = 25;
453
1846
  var token = sessionStorage.getItem("accessi-console-token");
454
1847
  var currentUser = null;
455
1848
  var federatedAuthenticationAvailable = false;
1849
+ var usersPage = 0;
1850
+ var usersTotal = 0;
1851
+ var booting = true;
456
1852
  var pendingNetworkRequests = 0;
457
1853
  var loadingRevealTimer;
458
1854
  var loadingHideTimer;
@@ -471,7 +1867,7 @@ function result(response) {
471
1867
  return body.Result;
472
1868
  }
473
1869
  function showNotice(message, isError = false) {
474
- byId("notice").innerHTML = `<span class="${isError ? "error" : ""}">${escapeHtml(message)}</span>`;
1870
+ byId("notice").innerHTML = message ? `<span class="${isError ? "error" : ""}">${escapeHtml(message)}</span>` : "";
475
1871
  }
476
1872
  function menuMetadata(menu) {
477
1873
  const details = [
@@ -491,9 +1887,9 @@ function groupMetadata(group) {
491
1887
  return `<dl class="entity-metadata"><div><dt>Codice</dt><dd>${escapeHtml(group.codiceGruppo)}</dd></div><div><dt>Ordine</dt><dd>${escapeHtml(group.ordineGruppo)}</dd></div><div><dt>Stato</dt><dd>${group.enabled === false ? "Disabilitato" : "Abilitato"}</dd></div><div><dt>Menu</dt><dd>${group.menus.length}</dd></div></dl>`;
492
1888
  }
493
1889
  function directGrantEditor(groups, grants) {
494
- return `<form id="grants" class="user-editor-section grants-editor" data-user-panel="grants" role="tabpanel" aria-labelledby="user-tab-grants"><h3>Grant diretti</h3><p class="form-help">Abilitazioni assegnate direttamente all'utente, indipendenti dai ruoli. Seleziona <strong>nessuno</strong> per rimuoverle. Le voci disabilitate restano visibili per non perdere grant esistenti al salvataggio.</p><div class="grant-menu-groups">${groups.map((group) => `<fieldset class="grant-menu-group"><legend>${escapeHtml(group.descrizioneGruppo)}</legend>${groupMetadata(group)}<div class="grant-menu-options">${group.menus.map((menu) => {
1890
+ return `<form id="grants" class="user-editor-section grants-editor" data-user-panel="grants" role="tabpanel" aria-labelledby="user-tab-grants"><h3>Grant diretti ${helpDot("grant-direct")}</h3><p class="form-help">Grant diretti, indipendenti dai ruoli. Seleziona <strong>nessuno</strong> per rimuoverli.</p><div class="grant-menu-groups">${groups.map((group) => `<fieldset class="grant-menu-group"><legend>${escapeHtml(group.descrizioneGruppo)}</legend>${groupMetadata(group)}<div class="grant-menu-options">${group.menus.map((menu) => {
495
1891
  const grant = (grants ?? []).find((item) => item.codiceMenu === menu.codiceMenu);
496
- return `<div class="grant-menu-option ${menu.enabled === false ? "is-disabled" : ""}"><div class="grant-menu-info"><strong>${escapeHtml(menu.descrizioneMenu)}</strong>${menuMetadata(menu)}</div><label class="grant-level">Livello di grant<select name="grant:${escapeHtml(menu.codiceMenu)}"><option value="">nessuno</option>${permissionSelectOptions(grant?.tipoAbilitazione)}</select></label></div>`;
1892
+ return `<div class="grant-menu-option ${menu.enabled === false ? "is-disabled" : ""}"><div class="grant-menu-info"><strong>${escapeHtml(menu.descrizioneMenu)}</strong>${menuMetadata(menu)}</div><label class="grant-level">Livello di grant ${helpDot("grant-level")}<select name="grant:${escapeHtml(menu.codiceMenu)}"><option value="">nessuno</option>${permissionSelectOptions(grant?.tipoAbilitazione)}</select></label></div>`;
497
1893
  }).join("") || '<p class="form-help">Nessun menu configurato in questo gruppo.</p>'}</div></fieldset>`).join("")}</div><button>Salva grant</button></form>`;
498
1894
  }
499
1895
  function permissionLevelLabel(value) {
@@ -504,7 +1900,7 @@ function rolePermissionLevel(value) {
504
1900
  }
505
1901
  function roleAssignmentEditor(roles, assignedRoles, groups) {
506
1902
  const catalogMenus = new Map(groups.flatMap((group) => group.menus.map((menu) => [menu.codiceMenu, menu])));
507
- return `<form id="roles" class="user-editor-section roles-editor" data-user-panel="roles" role="tabpanel" aria-labelledby="user-tab-roles"><h3>Ruoli</h3><p class="form-help">Ogni ruolo assegna i menu elencati sotto con il relativo livello. Salvare sostituisce la selezione dei ruoli dell'utente, non i grant diretti.</p><div class="role-assignment-list">${roles.map((role) => {
1903
+ return `<form id="roles" class="user-editor-section roles-editor" data-user-panel="roles" role="tabpanel" aria-labelledby="user-tab-roles"><h3>Ruoli ${helpDot("role-assignment")}</h3><p class="form-help">Salvare sostituisce i ruoli dell'utente (non i grant diretti).</p><div class="role-assignment-list">${roles.map((role) => {
508
1904
  const roleMenus = role.menu ?? [];
509
1905
  const assigned = (assignedRoles ?? []).some((current) => current.codiceRuolo === role.codiceRuolo);
510
1906
  const groupedMenus = groups.map((group) => ({ group, entries: group.menus.map((menu) => ({ menu, assignment: roleMenus.find((item) => item.codiceMenu === menu.codiceMenu) })).filter((entry) => entry.assignment) })).filter((entry) => entry.entries.length > 0);
@@ -514,24 +1910,43 @@ function roleAssignmentEditor(roles, assignedRoles, groups) {
514
1910
  }
515
1911
  function roleOverview(role, groups) {
516
1912
  const roleMenus = role.menu ?? [];
517
- const catalogMenus = new Map(groups.flatMap((group) => group.menus.map((menu) => [menu.codiceMenu, menu])));
518
- const groupedMenus = groups.map((group) => ({ group, entries: group.menus.map((menu) => ({ menu, assignment: roleMenus.find((item) => item.codiceMenu === menu.codiceMenu) })).filter((entry) => entry.assignment) })).filter((entry) => entry.entries.length > 0);
519
- const missingMenus = roleMenus.filter((item) => !catalogMenus.has(item.codiceMenu));
520
- return `<article class="role-assignment-card role-overview-card"><details class="role-overview-details"><summary class="role-assignment-heading"><div class="role-identity"><span class="role-kind">Ruolo</span><strong>${escapeHtml(role.descrizioneRuolo)}</strong><span class="role-code">Codice ruolo: ${escapeHtml(role.codiceRuolo)}</span></div><div class="role-overview-actions"><span class="role-menu-count">${roleMenus.length} menu</span><span class="role-expand-hint"><span class="when-closed">Mostra menu</span><span class="when-open">Nascondi menu</span></span></div></summary><div class="role-permission-groups">${groupedMenus.map(({ group, entries }) => `<section class="role-permission-group"><div class="role-permission-group-heading"><div><span class="group-kind">Gruppo</span><strong>${escapeHtml(group.descrizioneGruppo)}</strong></div>${groupMetadata(group)}</div>${entries.map(({ menu, assignment }) => `<div class="role-permission-entry ${menu.enabled === false ? "is-disabled" : ""}"><div><span class="menu-kind">Menu</span><strong>${escapeHtml(menu.descrizioneMenu)}</strong>${menuMetadata(menu)}</div><span class="permission-level">${escapeHtml(rolePermissionLevel(assignment?.tipoAbilitazione))}</span></div>`).join("")}</section>`).join("") || '<p class="form-help">Questo ruolo non contiene menu attivi nel catalogo.</p>'}${missingMenus.length ? `<section class="role-permission-group missing-role-menus"><strong>Menu non pi\xF9 presenti nel catalogo</strong>${missingMenus.map((menu) => `<div class="role-permission-entry"><div><strong>${escapeHtml(menu.codiceMenu)}</strong><p class="entity-note">Il menu \xE8 ancora associato al ruolo ma non \xE8 disponibile nel catalogo corrente.</p></div><span class="permission-level">${escapeHtml(rolePermissionLevel(menu.tipoAbilitazione))}</span></div>`).join("")}</section>` : ""}</div></details><button type="button" class="role-edit-button" data-role="${escapeHtml(role.codiceRuolo)}">Modifica</button></article>`;
521
- }
522
- function menuGroupTable(group) {
523
- return `<article class="menu-group-card ${group.enabled === false ? "is-disabled" : ""}"><div class="menu-group-header"><label class="check"><input type="checkbox" data-group="${escapeHtml(group.codiceGruppo)}" ${group.enabled !== false ? "checked" : ""}><span><strong>${escapeHtml(group.descrizioneGruppo)}</strong><small>Gruppo di menu</small></span></label>${groupMetadata(group)}</div><div class="menu-table-wrap"><table class="menu-table"><thead><tr><th>Stato</th><th>Menu</th><th>Codice</th><th>Ordine</th><th>Tipo</th><th>Pagina</th><th>Icona</th><th>Nota</th></tr></thead><tbody>${group.menus.map((menu) => `<tr class="${menu.enabled === false ? "is-disabled" : ""}"><td><label class="table-toggle"><input type="checkbox" data-menu="${escapeHtml(menu.codiceMenu)}" ${menu.enabled !== false ? "checked" : ""}><span>${menu.enabled === false ? "Disabilitato" : "Abilitato"}</span></label></td><td><strong>${escapeHtml(menu.descrizioneMenu)}</strong></td><td><code>${escapeHtml(menu.codiceMenu)}</code></td><td>${escapeHtml(menu.ordineMenu)}</td><td>${escapeHtml(menu.tipo || "Non indicato")}</td><td>${menu.pagina ? `<code>${escapeHtml(menu.pagina)}</code>` : '<span class="muted">Non indicata</span>'}</td><td>${menu.icona ? `<code>${escapeHtml(menu.icona)}</code>` : '<span class="muted">Non indicata</span>'}</td><td>${escapeHtml(menu.note || "Nessuna nota")}</td></tr>`).join("") || '<tr><td colspan="8" class="muted">Nessun menu configurato in questo gruppo.</td></tr>'}</tbody></table></div></article>`;
1913
+ const assignments = new Map(roleMenus.map((item) => [item.codiceMenu, item]));
1914
+ const knownMenus = new Set(groups.flatMap((group) => group.menus.map((menu) => menu.codiceMenu)));
1915
+ const tree = groups.map((group) => ({ group, entries: group.menus.filter((menu) => assignments.has(menu.codiceMenu)) })).filter((node) => node.entries.length > 0);
1916
+ const orphans = roleMenus.filter((item) => !knownMenus.has(item.codiceMenu));
1917
+ const groupNodes = tree.map(({ group, entries }) => `<li class="role-tree-group ${group.enabled === false ? "is-disabled" : ""}"><div class="role-tree-group-head"><strong>${escapeHtml(group.descrizioneGruppo)}</strong><span class="role-tree-count">${entries.length}</span></div><ul class="role-tree-menus">${entries.map((menu) => `<li class="role-tree-menu ${menu.enabled === false ? "is-disabled" : ""}"><span class="role-tree-menu-name">${escapeHtml(menu.descrizioneMenu)}</span><span class="permission-level">${escapeHtml(rolePermissionLevel(assignments.get(menu.codiceMenu)?.tipoAbilitazione))}</span></li>`).join("")}</ul></li>`).join("");
1918
+ const orphanNode = orphans.length ? `<li class="role-tree-group is-orphan"><div class="role-tree-group-head"><strong>Menu non pi\xF9 nel catalogo</strong><span class="role-tree-count">${orphans.length}</span></div><ul class="role-tree-menus">${orphans.map((item) => `<li class="role-tree-menu"><span class="role-tree-menu-name"><code>${escapeHtml(item.codiceMenu)}</code></span><span class="permission-level">${escapeHtml(rolePermissionLevel(item.tipoAbilitazione))}</span></li>`).join("")}</ul></li>` : "";
1919
+ return `<article class="role-card">
1920
+ <div class="role-card-head">
1921
+ <div class="role-card-title">
1922
+ <strong>${escapeHtml(role.descrizioneRuolo)}</strong>
1923
+ <span class="role-card-meta">Codice ${escapeHtml(role.codiceRuolo)} &middot; ${roleMenus.length} menu</span>
1924
+ </div>
1925
+ <div class="role-card-actions">
1926
+ <button type="button" class="secondary" data-role="${escapeHtml(role.codiceRuolo)}">Modifica</button>
1927
+ <button type="button" class="danger-button" data-role-delete="${escapeHtml(role.codiceRuolo)}">Elimina</button>
1928
+ </div>
1929
+ </div>
1930
+ ${roleMenus.length ? `<details class="role-card-menus"><summary>Abilitazioni per menu (${roleMenus.length})</summary><ul class="role-tree">${groupNodes}${orphanNode}</ul></details>` : '<p class="role-card-empty">Nessun menu assegnato.</p>'}
1931
+ </article>`;
524
1932
  }
525
1933
  function roleMenuEditorTable(group, role) {
526
1934
  const roleMenus = role?.menu ?? [];
527
- return `<article class="role-menu-group ${group.enabled === false ? "is-disabled" : ""}"><div class="menu-group-header"><div><strong>${escapeHtml(group.descrizioneGruppo)}</strong><small>Gruppo di menu</small></div>${groupMetadata(group)}</div><div class="menu-table-wrap"><table class="menu-table role-menu-table"><thead><tr><th scope="col">Incluso</th><th scope="col">Menu</th><th scope="col">Codice</th><th scope="col">Ordine</th><th scope="col">Tipo</th><th scope="col">Pagina</th><th scope="col">Icona</th><th scope="col">Stato</th><th scope="col">Abilitazione</th><th scope="col">Nota</th></tr></thead><tbody>${group.menus.map((menu) => {
1935
+ const options = group.menus.map((menu) => {
528
1936
  const assignment = roleMenus.find((item) => item.codiceMenu === menu.codiceMenu);
529
1937
  const level = assignment?.tipoAbilitazione ?? 10;
530
- return `<tr class="${menu.enabled === false ? "is-disabled" : ""}"><td><label class="table-toggle"><input type="checkbox" name="menu" value="${escapeHtml(menu.codiceMenu)}" ${assignment ? "checked" : ""} aria-label="Includi ${escapeHtml(menu.descrizioneMenu)}"><span>${assignment ? "Incluso" : "Escluso"}</span></label></td><td><strong>${escapeHtml(menu.descrizioneMenu)}</strong></td><td><code>${escapeHtml(menu.codiceMenu)}</code></td><td>${escapeHtml(menu.ordineMenu)}</td><td>${escapeHtml(menu.tipo || "Non indicato")}</td><td>${menu.pagina ? `<code>${escapeHtml(menu.pagina)}</code>` : '<span class="muted">Non indicata</span>'}</td><td>${menu.icona ? `<code>${escapeHtml(menu.icona)}</code>` : '<span class="muted">Non indicata</span>'}</td><td><span class="status-label ${menu.enabled === false ? "status-disabled" : "status-enabled"}">${menu.enabled === false ? "Disabilitato" : "Abilitato"}</span></td><td><label class="table-select"><span class="sr-only">Abilitazione per ${escapeHtml(menu.descrizioneMenu)}</span><select name="menu-level:${escapeHtml(menu.codiceMenu)}" ${assignment ? "" : "disabled"}>${permissionSelectOptions(level)}</select></label></td><td>${escapeHtml(menu.note || "Nessuna nota")}</td></tr>`;
531
- }).join("") || '<tr><td colspan="10" class="muted">Nessun menu configurato in questo gruppo.</td></tr>'}</tbody></table></div></article>`;
1938
+ return `<li class="role-menu-option ${menu.enabled === false ? "is-disabled" : ""}"><label class="check"><input type="checkbox" name="menu" value="${escapeHtml(menu.codiceMenu)}" ${assignment ? "checked" : ""}><span>${escapeHtml(menu.descrizioneMenu)}</span></label><label class="role-menu-level"><span class="sr-only">Livello per ${escapeHtml(menu.descrizioneMenu)}</span><select name="menu-level:${escapeHtml(menu.codiceMenu)}" ${assignment ? "" : "disabled"}>${permissionSelectOptions(level)}</select></label></li>`;
1939
+ }).join("");
1940
+ return `<section class="role-menu-group ${group.enabled === false ? "is-disabled" : ""}">
1941
+ <div class="role-menu-group-head"><strong>${escapeHtml(group.descrizioneGruppo)}</strong>${group.enabled === false ? '<span class="muted">Gruppo disabilitato</span>' : ""}</div>
1942
+ <ul class="role-menu-options">${options || '<li class="role-menu-empty muted">Nessun menu in questo gruppo.</li>'}</ul>
1943
+ </section>`;
532
1944
  }
533
1945
  function updateLoadingState(active) {
534
1946
  pendingNetworkRequests = Math.max(0, pendingNetworkRequests + (active ? 1 : -1));
1947
+ if (booting) {
1948
+ return;
1949
+ }
535
1950
  const overlay = byId("loading-overlay");
536
1951
  if (pendingNetworkRequests > 0) {
537
1952
  if (loadingHideTimer !== void 0) {
@@ -566,6 +1981,14 @@ function updateLoadingState(active) {
566
1981
  hide();
567
1982
  }
568
1983
  }
1984
+ function finishBoot(view) {
1985
+ booting = false;
1986
+ const overlay = byId("loading-overlay");
1987
+ overlay.hidden = true;
1988
+ byId("app").removeAttribute("aria-busy");
1989
+ byId("login").hidden = view !== "login";
1990
+ byId("console").hidden = view !== "console";
1991
+ }
569
1992
  function errorMessage(error) {
570
1993
  return error instanceof Error ? error.message : "Operazione non riuscita.";
571
1994
  }
@@ -573,6 +1996,7 @@ function handleAction(action) {
573
1996
  void action().catch((error) => showNotice(errorMessage(error), true));
574
1997
  }
575
1998
  function render(markup) {
1999
+ closeHelp();
576
2000
  byId("view").innerHTML = markup;
577
2001
  }
578
2002
  function setActiveNavigation(view) {
@@ -583,19 +2007,24 @@ function setActiveNavigation(view) {
583
2007
  link.removeAttribute("aria-current");
584
2008
  }
585
2009
  });
2010
+ document.querySelectorAll("#nav details[data-nav-group]").forEach((branch) => {
2011
+ if (branch.querySelector(`[data-view="${view}"]`)) branch.open = true;
2012
+ });
586
2013
  }
587
2014
  function routePath(view) {
588
2015
  return `${consoleBasePath}/${routeSegment[view]}`;
589
2016
  }
590
2017
  function viewFromLocation() {
591
2018
  const relativePath = window.location.pathname.slice(consoleBasePath.length).replace(/^\/+|\/+$/g, "");
592
- return routeView[decodeURIComponent(relativePath)] ?? "users";
2019
+ const decoded = decodeURIComponent(relativePath);
2020
+ return routeView[decoded] ?? parentDefaultView[decoded] ?? "users";
593
2021
  }
594
2022
  async function navigate(view, replace = false) {
595
2023
  const destination = routePath(view);
596
2024
  if (window.location.pathname !== destination) {
597
2025
  window.history[replace ? "replaceState" : "pushState"]({ view }, "", destination);
598
2026
  }
2027
+ if (view === "users") usersPage = 0;
599
2028
  await show(view);
600
2029
  }
601
2030
  function requireAdmin() {
@@ -608,12 +2037,12 @@ function eventForm(event) {
608
2037
  return event.currentTarget;
609
2038
  }
610
2039
  function federatedIdentityFields(providers) {
611
- return `<label>Provider<select name="provider" required><option value="">Seleziona un provider</option>${providers.filter((provider) => provider.active).map((provider) => `<option value="${escapeHtml(provider.provider)}">${escapeHtml(provider.description)} (${escapeHtml(provider.provider)})</option>`).join("")}</select><span class="form-help">Catalogo Accessi dei sistemi SSO disponibili. La chiave selezionata deve corrispondere alla configurazione del backend; per Azure deve distinguere tenant e ambiente.</span></label>
612
- <label>ID utente del provider (subject)<input name="subject" required><span class="form-help">Identifica in modo stabile la singola persona nel provider. Il backend lo estrae dopo la verifica: per esempio <code>oid</code> Azure, <code>sub</code> OpenID Connect/Google/Apple o <code>NameID</code> SAML persistente. \xC8 necessario perch\xE9 il provider da solo non identifica l'utente. Non usare l'email: pu\xF2 cambiare.</span></label>
613
- <label>Nota<input name="note"><span class="form-help">Annotazione interna per gli amministratori. Non viene inviata al provider e non assegna autorizzazioni.</span></label>`;
2040
+ return `<label>Provider ${helpDot("sso-provider")}<select name="provider" required><option value="">Seleziona un provider</option>${providers.filter((provider) => provider.active).map((provider) => `<option value="${escapeHtml(provider.provider)}">${escapeHtml(provider.description)} (${escapeHtml(provider.provider)})</option>`).join("")}</select><span class="form-help">Deve corrispondere alla configurazione del backend.</span></label>
2041
+ <label>ID utente del provider (subject) ${helpDot("sso-subject")}<input name="subject" required><span class="form-help">Identificatore stabile (es. <code>oid</code> Azure, <code>sub</code> Google/OIDC, <code>NameID</code> SAML). Mai l'email: apri la guida.</span></label>
2042
+ <label>Nota ${helpDot("sso-note")}<input name="note"><span class="form-help">Nota interna, non inviata al provider.</span></label>`;
614
2043
  }
615
2044
  function federatedIdentitySection(identities, providers) {
616
- return `<section class="user-editor-section sso-identity-section" data-user-panel="sso" role="tabpanel" aria-labelledby="user-tab-sso"><div class="section-heading"><div><p class="eyebrow">Autenticazione esterna</p><h3>Identit\xE0 SSO</h3></div><span class="muted">Collegamenti verificati dal backend</span></div><p class="form-help">Ogni collegamento riconosce l'utente presso un provider. Disabilita conserva lo storico e blocca il login; elimina rimuove definitivamente il collegamento, senza eliminare l'utente Accessi.</p><div class="sso-identity-list">${identities.map((identity) => `<article class="sso-identity-item ${identity.active ? "" : "is-disabled"}"><div class="sso-identity-data"><strong>${escapeHtml(identity.provider)}</strong><dl class="entity-metadata"><div><dt>Subject</dt><dd>${escapeHtml(identity.subject)}</dd></div><div><dt>Stato</dt><dd>${identity.active ? "Abilitata" : "Disabilitata"}</dd></div></dl>${identity.note ? `<p class="entity-note"><strong>Nota</strong>${escapeHtml(identity.note)}</p>` : ""}</div><div class="sso-identity-actions"><button type="button" class="secondary" data-identity-toggle="${escapeHtml(identity.identityKey)}" data-active="${String(!identity.active)}">${identity.active ? "Disabilita" : "Abilita"}</button><button type="button" class="icon-button danger-button" data-identity-delete="${escapeHtml(identity.identityKey)}" aria-label="Elimina definitivamente il collegamento SSO ${escapeHtml(identity.provider)}" title="Elimina definitivamente il collegamento SSO"><span aria-hidden="true">&#128465;</span></button></div></article>`).join("") || '<p class="muted">Nessun collegamento SSO.</p>'}</div>${providers.some((provider) => provider.active) ? `<form id="link-sso" class="sso-link-form">${federatedIdentityFields(providers)}<button>Collega SSO verificato</button></form>` : '<p class="form-help">Non ci sono provider SSO attivi. Censiscine uno nella sezione SSO prima di creare collegamenti.</p>'}</section>`;
2045
+ return `<section class="user-editor-section sso-identity-section" data-user-panel="sso" role="tabpanel" aria-labelledby="user-tab-sso"><div class="section-heading"><div><p class="eyebrow">Autenticazione esterna</p><h3>Identit\xE0 SSO</h3></div><span class="muted">Collegamenti verificati dal backend</span></div><p class="form-help">Disabilita conserva lo storico; elimina rimuove il collegamento (l'utente resta).</p><div class="sso-identity-list">${identities.map((identity) => `<article class="sso-identity-item ${identity.active ? "" : "is-disabled"}"><div class="sso-identity-data"><strong>${escapeHtml(identity.provider)}</strong><dl class="entity-metadata"><div><dt>Subject</dt><dd>${escapeHtml(identity.subject)}</dd></div><div><dt>Stato</dt><dd>${identity.active ? "Abilitata" : "Disabilitata"}</dd></div></dl>${identity.note ? `<p class="entity-note"><strong>Nota</strong>${escapeHtml(identity.note)}</p>` : ""}</div><div class="sso-identity-actions"><button type="button" class="secondary" data-identity-toggle="${escapeHtml(identity.identityKey)}" data-active="${String(!identity.active)}">${identity.active ? "Disabilita" : "Abilita"}</button><button type="button" class="icon-button danger-button" data-identity-delete="${escapeHtml(identity.identityKey)}" aria-label="Elimina definitivamente il collegamento SSO ${escapeHtml(identity.provider)}" title="Elimina definitivamente il collegamento SSO"><span aria-hidden="true">&#128465;</span></button></div></article>`).join("") || '<p class="muted">Nessun collegamento SSO.</p>'}</div>${providers.some((provider) => provider.active) ? `<form id="link-sso" class="sso-link-form">${federatedIdentityFields(providers)}<button>Collega SSO verificato</button></form>` : '<p class="form-help">Non ci sono provider SSO attivi. Censiscine uno nella sezione SSO prima di creare collegamenti.</p>'}</section>`;
617
2046
  }
618
2047
  function userDetailTabs(hasSso) {
619
2048
  const tabs = [
@@ -643,6 +2072,79 @@ function selectedNumbers(form, name) {
643
2072
  async function loadUsers() {
644
2073
  return result(await getUsers({ includeGrants: true }));
645
2074
  }
2075
+ async function loadUsersPage() {
2076
+ const response = await getUsers({
2077
+ includeGrants: true,
2078
+ limit: USERS_PAGE_SIZE,
2079
+ offset: usersPage * USERS_PAGE_SIZE
2080
+ });
2081
+ const users = result(response);
2082
+ const headers = response.headers;
2083
+ const totalHeader = headers?.get?.("X-Total-Count") ?? null;
2084
+ const parsed = totalHeader !== null && totalHeader !== "" ? Number(totalHeader) : Number.NaN;
2085
+ const total = Number.isFinite(parsed) ? parsed : usersPage * USERS_PAGE_SIZE + users.length;
2086
+ return { users, total };
2087
+ }
2088
+ function pageNumbers(current, total) {
2089
+ if (total <= 7) {
2090
+ return Array.from({ length: total }, (_, index) => index + 1);
2091
+ }
2092
+ const wanted = /* @__PURE__ */ new Set([1, total, current, current - 1, current + 1]);
2093
+ const sorted = Array.from(wanted).filter((page) => page >= 1 && page <= total).sort((a, b) => a - b);
2094
+ const result2 = [];
2095
+ let previous = 0;
2096
+ for (const page of sorted) {
2097
+ if (previous !== 0 && page - previous > 1) {
2098
+ result2.push("gap");
2099
+ }
2100
+ result2.push(page);
2101
+ previous = page;
2102
+ }
2103
+ return result2;
2104
+ }
2105
+ function pagerMarkup(totalPages) {
2106
+ const context = `<span class="pager-context">${usersTotal} utent${usersTotal === 1 ? "e" : "i"}${totalPages > 1 ? ` \xB7 pagina ${usersPage + 1} di ${totalPages}` : ""}</span>`;
2107
+ if (totalPages <= 1) {
2108
+ return `<nav class="pager" aria-label="Paginazione utenti">${context}</nav>`;
2109
+ }
2110
+ const numbers = pageNumbers(usersPage + 1, totalPages).map((page) => page === "gap" ? '<span class="pager-gap" aria-hidden="true">&hellip;</span>' : `<button type="button" class="pager-item" data-goto="${page}"${page === usersPage + 1 ? ' aria-current="page"' : ""}>${page}</button>`).join("");
2111
+ const controls = `<span class="pager-controls"><button type="button" class="pager-item pager-nav" data-goto="prev"${usersPage === 0 ? " disabled" : ""} aria-label="Pagina precedente">&lsaquo;</button>` + numbers + `<button type="button" class="pager-item pager-nav" data-goto="next"${usersPage >= totalPages - 1 ? " disabled" : ""} aria-label="Pagina successiva">&rsaquo;</button></span>`;
2112
+ return `<nav class="pager" aria-label="Paginazione utenti">${controls}${context}</nav>`;
2113
+ }
2114
+ function rolesCell(codiceUtente, userGrants) {
2115
+ const labels = (userGrants?.ruoli ?? []).map((role) => role.descrizioneRuolo?.trim() || (role.codiceRuolo ? `#${role.codiceRuolo}` : "")).filter((label) => label.length > 0);
2116
+ if (labels.length === 0) {
2117
+ return '<span class="muted">&mdash;</span>';
2118
+ }
2119
+ const limit = 2;
2120
+ if (labels.length <= limit) {
2121
+ return `<span class="roles-list">${labels.map(escapeHtml).join(", ")}</span>`;
2122
+ }
2123
+ const hiddenCount = labels.length - limit;
2124
+ return `<span class="roles-cell" data-roles-cell="${codiceUtente}"><span class="roles-list" data-roles-collapsed>${labels.slice(0, limit).map(escapeHtml).join(", ")}</span><span class="roles-list" data-roles-full hidden>${labels.map(escapeHtml).join(", ")}</span><button type="button" class="link-button" data-roles-toggle="${codiceUtente}" data-roles-more="${hiddenCount}" aria-expanded="false">altre ${hiddenCount}</button></span>`;
2125
+ }
2126
+ function setRolesExpanded(cell, expanded) {
2127
+ cell.querySelector("[data-roles-collapsed]")?.toggleAttribute("hidden", expanded);
2128
+ cell.querySelector("[data-roles-full]")?.toggleAttribute("hidden", !expanded);
2129
+ const button = cell.querySelector("[data-roles-toggle]");
2130
+ if (button) {
2131
+ button.setAttribute("aria-expanded", String(expanded));
2132
+ button.textContent = expanded ? "mostra meno" : `altre ${button.dataset.rolesMore ?? ""}`;
2133
+ }
2134
+ }
2135
+ function bindRolesToggles() {
2136
+ document.querySelectorAll("[data-roles-toggle]").forEach((button) => {
2137
+ button.onclick = () => {
2138
+ const cell = button.closest("[data-roles-cell]");
2139
+ if (!cell) return;
2140
+ const willExpand = button.getAttribute("aria-expanded") !== "true";
2141
+ document.querySelectorAll("[data-roles-cell]").forEach((other) => {
2142
+ if (other !== cell) setRolesExpanded(other, false);
2143
+ });
2144
+ setRolesExpanded(cell, willExpand);
2145
+ };
2146
+ });
2147
+ }
646
2148
  async function detectFederatedAuthentication() {
647
2149
  if (!currentUser) return;
648
2150
  try {
@@ -657,7 +2159,7 @@ async function loginView(event) {
657
2159
  try {
658
2160
  const values = formValues(event.currentTarget);
659
2161
  byId("login-error").textContent = "";
660
- const response = result(await login({ email: values.email, password: values.password || void 0 }));
2162
+ const response = result(await login({ email: values.email ?? "", password: values.password || void 0 }));
661
2163
  await completeLoginStep(response);
662
2164
  } catch (error) {
663
2165
  byId("login-error").textContent = error instanceof Error ? error.message : "Login non riuscito.";
@@ -732,10 +2234,9 @@ async function bootstrap() {
732
2234
  currentUser = payload.userData;
733
2235
  requireAdmin();
734
2236
  await detectFederatedAuthentication();
735
- byId("login").hidden = true;
736
- byId("console").hidden = false;
737
2237
  byId("who").textContent = `${currentUser.email} - superutente`;
738
2238
  byId("sso-navigation").hidden = !federatedAuthenticationAvailable;
2239
+ finishBoot("console");
739
2240
  await navigate(viewFromLocation(), window.location.pathname === consoleBasePath);
740
2241
  } catch (error) {
741
2242
  token = null;
@@ -743,35 +2244,48 @@ async function bootstrap() {
743
2244
  setAccessiConsoleToken(null);
744
2245
  federatedAuthenticationAvailable = false;
745
2246
  sessionStorage.removeItem("accessi-console-token");
746
- byId("login").hidden = false;
747
- byId("console").hidden = true;
2247
+ finishBoot("login");
748
2248
  byId("login-error").textContent = error instanceof Error ? error.message : "Sessione non valida.";
749
2249
  }
750
2250
  }
751
2251
  async function showUsers() {
752
- const users = await loadUsers();
2252
+ const { users, total } = await loadUsersPage();
2253
+ usersTotal = total;
2254
+ const totalPages = Math.max(1, Math.ceil(total / USERS_PAGE_SIZE));
2255
+ if (usersPage >= totalPages) usersPage = totalPages - 1;
753
2256
  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>
754
- <h2>Utenti</h2><table><thead><tr><th>Utente</th><th>Stato</th><th>Accesso</th><th>2FA</th><th>Ruoli</th><th></th></tr></thead><tbody>
755
- ${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>${(userGrants?.ruoli ?? []).map((role) => role.codiceRuolo).join(", ")}</td><td><button data-user="${utente.codiceUtente}">Gestisci</button></td></tr>`).join("")}
756
- </tbody></table>`);
2257
+ <div class="pager-row pager-row-top">${pagerMarkup(totalPages)}</div>
2258
+ <table><thead><tr><th>Utente</th><th>Stato</th><th>Accesso</th><th>2FA</th><th>Ruoli</th><th></th></tr></thead><tbody>
2259
+ ${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>'}
2260
+ </tbody></table><div class="pager-row pager-row-bottom">${pagerMarkup(totalPages)}</div>`);
757
2261
  byId("new-local").onclick = () => showLocalUserForm();
758
2262
  if (federatedAuthenticationAvailable) byId("new-sso").onclick = () => handleAction(showSsoUserForm);
759
2263
  byId("reload-users").onclick = () => handleAction(() => show("users"));
2264
+ document.querySelectorAll("[data-goto]").forEach((button) => {
2265
+ button.onclick = () => handleAction(async () => {
2266
+ const target = button.dataset.goto;
2267
+ const nextPage = target === "prev" ? usersPage - 1 : target === "next" ? usersPage + 1 : Number(target) - 1;
2268
+ if (!Number.isInteger(nextPage) || nextPage < 0 || nextPage >= totalPages || nextPage === usersPage) return;
2269
+ usersPage = nextPage;
2270
+ await show("users");
2271
+ });
2272
+ });
760
2273
  document.querySelectorAll("[data-user]").forEach((button) => {
761
2274
  button.onclick = () => handleAction(() => showUser(button.dataset.user ?? ""));
762
2275
  });
2276
+ bindRolesToggles();
763
2277
  }
764
2278
  function authenticationPolicyFields(user) {
765
2279
  return `<fieldset><legend>Verifica dell'accesso (opzionale)</legend>
766
- <label class="check"><input name="flagDueFattori" type="checkbox" ${user.flagDueFattori ? "checked" : ""}> Richiedi un codice via email (2FA)</label>
767
- <p class="form-help">Disattiva per impostazione predefinita. Se attiva, il codice viene richiesto dopo la password o dopo l'accesso SSO.</p>
768
- <label class="check"><input name="passwordlessLoginEnabled" type="checkbox" ${user.passwordlessLoginEnabled ? "checked" : ""} ${user.flagDueFattori ? "" : "disabled"}> Consenti accesso con il solo codice email</label>
769
- <p class="form-help">Permette di accedere senza password. Richiede il codice email attivo; i collegamenti SSO restano disponibili. Questo accesso usa il solo possesso della casella email.</p>
2280
+ <label class="check"><input name="flagDueFattori" type="checkbox" ${user.flagDueFattori ? "checked" : ""}> Richiedi un codice via email (2FA) ${helpDot("user-2fa")}</label>
2281
+ <p class="form-help">Se attiva, il codice \xE8 richiesto dopo password o SSO.</p>
2282
+ <label class="check"><input name="passwordlessLoginEnabled" type="checkbox" ${user.passwordlessLoginEnabled ? "checked" : ""} ${user.flagDueFattori ? "" : "disabled"}> Consenti accesso con il solo codice email ${helpDot("user-passwordless")}</label>
2283
+ <p class="form-help">Accesso senza password (richiede il codice email).</p>
770
2284
  </fieldset>`;
771
2285
  }
772
2286
  function showLocalUserForm() {
773
2287
  render(`<button id="back">Indietro</button><h2>Nuovo utente locale</h2><p class="muted">Viene inviata l'e-mail per impostare la password.</p>
774
- <form id="local-user"><label>Email<input name="email" type="email" required><span class="form-help">Sar\xE0 l'identificativo di accesso dell'utente e ricever\xE0 il messaggio per impostare la password.</span></label><label>Nome<input name="nome"></label><label>Cognome<input name="cognome"></label><button>Crea utente</button></form>`);
2288
+ <form id="local-user"><label>Email ${helpDot("user-email")}<input name="email" type="email" required><span class="form-help">Identificativo di accesso; ricever\xE0 l'email per la password.</span></label><label>Nome ${helpDot("user-name")}<input name="nome"></label><label>Cognome ${helpDot("user-name")}<input name="cognome"></label><button>Crea utente</button></form>`);
775
2289
  byId("back").onclick = () => show("users");
776
2290
  const localUserForm = document.getElementById("local-user");
777
2291
  if (localUserForm) localUserForm.onsubmit = (event) => {
@@ -788,9 +2302,9 @@ async function showSsoUserForm() {
788
2302
  if (!providers.some((provider) => provider.active)) {
789
2303
  throw new Error("Prima di creare un utente SSO, censisci e abilita almeno un provider SSO.");
790
2304
  }
791
- render(`<button id="back">Indietro</button><h2>Nuovo utente SSO</h2><p class="muted">La console salva solo un collegamento gi\xE0 verificato dal backend; non accetta e non verifica token esterni.</p>
792
- <form id="sso-user"><label>Email<input name="email" type="email" required><span class="form-help">Email di contatto e identificativo Accessi dell'utente.</span></label><label>Nome<input name="nome"></label><label>Cognome<input name="cognome"></label>${federatedIdentityFields(providers)}<label class="check"><input name="passwordLoginEnabled" type="checkbox"> Abilita anche la password locale</label><span class="form-help">Se non selezionato, l'utente acceder\xE0 esclusivamente con una delle identit\xE0 SSO attive.</span><button>Crea utente SSO</button></form>`);
793
- byId("back").onclick = () => show("users");
2305
+ render(`<button id="back">Indietro</button><h2>Nuovo utente SSO</h2><p class="muted">Salva solo collegamenti gi\xE0 verificati dal backend.</p>
2306
+ <form id="sso-user"><label>Email ${helpDot("user-email")}<input name="email" type="email" required><span class="form-help">Email di contatto e identificativo Accessi dell'utente.</span></label><label>Nome ${helpDot("user-name")}<input name="nome"></label><label>Cognome ${helpDot("user-name")}<input name="cognome"></label>${federatedIdentityFields(providers)}<label class="check"><input name="passwordLoginEnabled" type="checkbox"> Abilita anche la password locale ${helpDot("user-password-login")}</label><span class="form-help">Se disattivato, l'utente accede solo via SSO.</span><button>Crea utente SSO</button></form>`);
2307
+ byId("back").onclick = () => show("ssoUsers");
794
2308
  const ssoUserForm = document.getElementById("sso-user");
795
2309
  if (ssoUserForm) ssoUserForm.onsubmit = (event) => {
796
2310
  event.preventDefault();
@@ -798,36 +2312,55 @@ async function showSsoUserForm() {
798
2312
  const form = eventForm(event);
799
2313
  const values = formValues(form);
800
2314
  const request = {
801
- provider: values.provider,
802
- subject: values.subject,
2315
+ provider: values.provider ?? "",
2316
+ subject: values.subject ?? "",
803
2317
  note: values.note || void 0,
804
2318
  passwordLoginEnabled: new FormData(form).has("passwordLoginEnabled"),
805
- user: { email: values.email, nome: values.nome || void 0, cognome: values.cognome || void 0 }
2319
+ user: { email: values.email ?? "", nome: values.nome || void 0, cognome: values.cognome || void 0 }
806
2320
  };
807
2321
  await createFederatedUser(request);
808
2322
  showNotice("Utente SSO creato e collegamento registrato.");
809
- await show("users");
2323
+ await show("ssoUsers");
810
2324
  });
811
2325
  };
812
2326
  }
813
- async function showSso() {
2327
+ async function showSsoProviders() {
814
2328
  if (!federatedAuthenticationAvailable) throw new Error("SSO non \xE8 abilitato in questa istanza Accessi.");
815
- const [users, providers] = await Promise.all([loadUsers(), getFederatedProviders()]);
816
- const providerList = result(providers);
817
- render(`<div class="toolbar"><button id="new-sso">Nuovo utente SSO</button><button id="new-provider" class="secondary">Nuovo provider</button></div><h2>Configurazione SSO</h2><p class="muted">Il catalogo definisce le chiavi provider ammesse da Accessi. La validazione dei token, issuer, audience e segreti restano nel backend.</p><h3>Provider censiti</h3><table><thead><tr><th>Provider</th><th>Descrizione</th><th>Stato</th><th></th></tr></thead><tbody>${providerList.map((provider) => `<tr><td><code>${escapeHtml(provider.provider)}</code></td><td>${escapeHtml(provider.description)}</td><td>${provider.active ? "Attivo" : "Disabilitato"}</td><td><button data-provider="${escapeHtml(provider.provider)}">Gestisci</button></td></tr>`).join("") || '<tr><td colspan="4">Nessun provider censito.</td></tr>'}</tbody></table><h3>Utenti e identit\xE0</h3><table><thead><tr><th>Utente</th><th>Identit\xE0 collegate</th><th></th></tr></thead><tbody>${users.map(({ utente }) => `<tr><td>${escapeHtml(utente.email)}</td><td>${utente.passwordLoginEnabled === false ? "solo SSO" : "password e SSO"}</td><td><button data-sso-user="${utente.codiceUtente}">Gestisci identit\xE0</button></td></tr>`).join("")}</tbody></table>`);
818
- byId("new-sso").onclick = () => handleAction(showSsoUserForm);
2329
+ const providerList = result(await getFederatedProviders());
2330
+ render(`<div class="toolbar"><button id="new-provider">Nuovo provider</button><button id="reload-providers" class="secondary">Aggiorna</button></div>
2331
+ <h2>Provider SSO</h2><p class="muted">La validazione di token, issuer e segreti resta nel backend. Un provider si elimina solo se nessuna identit\xE0 \xE8 collegata.</p>
2332
+ <div class="menu-table-wrap"><table><thead><tr><th>Provider</th><th>Descrizione</th><th>Stato</th><th></th></tr></thead><tbody>${providerList.map((provider) => `<tr class="${provider.active ? "" : "is-disabled"}"><td><code>${escapeHtml(provider.provider)}</code></td><td>${escapeHtml(provider.description)}</td><td>${provider.active ? "Attivo" : "Disabilitato"}</td><td class="row-actions"><button data-provider="${escapeHtml(provider.provider)}">Gestisci</button></td></tr>`).join("") || '<tr><td colspan="4" class="muted">Nessun provider censito.</td></tr>'}</tbody></table></div>`);
819
2333
  byId("new-provider").onclick = () => showProviderForm();
2334
+ byId("reload-providers").onclick = () => handleAction(showSsoProviders);
820
2335
  document.querySelectorAll("[data-provider]").forEach((button) => {
821
2336
  button.onclick = () => showProviderForm(providerList.find((provider) => provider.provider === button.dataset.provider));
822
2337
  });
2338
+ }
2339
+ async function showSsoUsers() {
2340
+ if (!federatedAuthenticationAvailable) throw new Error("SSO non \xE8 abilitato in questa istanza Accessi.");
2341
+ const users = await loadUsers();
2342
+ render(`<div class="toolbar"><button id="new-sso">Nuovo utente SSO</button><button id="reload-sso-users" class="secondary">Aggiorna</button></div>
2343
+ <h2>Utenti e identit\xE0 SSO</h2><p class="muted">Gestisci i collegamenti provider/subject gi\xE0 verificati dal backend.</p>
2344
+ <div class="menu-table-wrap"><table><thead><tr><th>Utente</th><th>Accesso</th><th></th></tr></thead><tbody>${users.map(({ utente }) => `<tr><td>${escapeHtml(utente.email)}</td><td>${utente.passwordLoginEnabled === false ? "solo SSO" : "password e SSO"}</td><td class="row-actions"><button data-sso-user="${utente.codiceUtente}">Gestisci identit\xE0</button></td></tr>`).join("") || '<tr><td colspan="3" class="muted">Nessun utente.</td></tr>'}</tbody></table></div>`);
2345
+ byId("new-sso").onclick = () => handleAction(showSsoUserForm);
2346
+ byId("reload-sso-users").onclick = () => handleAction(showSsoUsers);
823
2347
  document.querySelectorAll("[data-sso-user]").forEach((button) => {
824
2348
  button.onclick = () => handleAction(() => showUser(button.dataset.ssoUser ?? ""));
825
2349
  });
826
2350
  }
827
2351
  function showProviderForm(provider) {
828
2352
  const isExisting = provider !== void 0;
829
- render(`<button id="back" type="button">Indietro</button><div class="page-header"><div><p class="eyebrow">Catalogo SSO</p><h2>${isExisting ? "Gestisci provider" : "Nuovo provider"}</h2></div><span class="muted">Le configurazioni tecniche restano nel backend</span></div><form id="provider-form"><label>Chiave provider<input name="provider" value="${escapeHtml(provider?.provider)}" ${isExisting ? "readonly" : ""} required><span class="form-help">Chiave stabile usata dal backend nel payload SSO. Per Azure includi tenant e ambiente, ad esempio <code>azure-ad-acme-produzione</code>. Non \xE8 modificabile dopo il censimento.</span></label><label>Descrizione<input name="description" value="${escapeHtml(provider?.description)}" required><span class="form-help">Nome leggibile per gli amministratori della console.</span></label><label>Nota<input name="note" value="${escapeHtml(provider?.note)}"><span class="form-help">Annotazione interna. Non inserire segreti, issuer, token o dati personali non necessari.</span></label>${isExisting ? `<label class="check"><input name="active" type="checkbox" ${provider.active ? "checked" : ""}> Provider attivo</label><span class="form-help">Disabilitare il provider blocca nuovi login SSO e nuovi collegamenti, ma conserva lo storico delle identit\xE0.</span>` : ""}<button>${isExisting ? "Salva provider" : "Censisci provider"}</button></form>`);
830
- byId("back").onclick = () => show("sso");
2353
+ render(`<button id="back" type="button">Indietro</button><div class="page-header"><div><p class="eyebrow">Catalogo SSO</p><h2>${isExisting ? "Gestisci provider" : "Nuovo provider"}</h2></div><span class="muted">Le configurazioni tecniche restano nel backend</span></div><form id="provider-form"><label>Chiave provider ${helpDot("provider-key")}<input name="provider" value="${escapeHtml(provider?.provider)}" ${isExisting ? "readonly" : ""} required><span class="form-help">Chiave usata dal backend nel payload SSO (es. <code>azure-ad-acme-prod</code>). Non modificabile dopo il censimento.</span></label><label>Descrizione ${helpDot("provider-description")}<input name="description" value="${escapeHtml(provider?.description)}" required><span class="form-help">Nome leggibile per gli amministratori.</span></label><label>Nota ${helpDot("provider-note")}<input name="note" value="${escapeHtml(provider?.note)}"><span class="form-help">Non inserire segreti o dati personali.</span></label>${isExisting ? `<label class="check"><input name="active" type="checkbox" ${provider.active ? "checked" : ""}> Provider attivo ${helpDot("provider-active")}</label><span class="form-help">Blocca nuovi login e collegamenti; lo storico resta.</span>` : ""}<button>${isExisting ? "Salva provider" : "Censisci provider"}</button>${isExisting ? '<button type="button" id="delete-provider" class="danger-button">Elimina provider</button>' : ""}</form>`);
2354
+ byId("back").onclick = () => show("ssoProviders");
2355
+ const deleteProviderButton = document.getElementById("delete-provider");
2356
+ if (deleteProviderButton && provider) {
2357
+ deleteProviderButton.onclick = () => handleAction(async () => {
2358
+ if (!window.confirm(`Eliminare il provider ${provider.provider}? Operazione possibile solo se nessuna identita e collegata.`)) return;
2359
+ await deleteFederatedProvider(provider.provider);
2360
+ showNotice("Provider SSO eliminato.");
2361
+ await show("ssoProviders");
2362
+ });
2363
+ }
831
2364
  const providerForm = document.getElementById("provider-form");
832
2365
  if (providerForm) providerForm.onsubmit = (event) => {
833
2366
  event.preventDefault();
@@ -843,17 +2376,29 @@ function showProviderForm(provider) {
843
2376
  await updateFederatedProvider(provider.provider, request);
844
2377
  } else {
845
2378
  const request = {
846
- provider: values.provider,
847
- description: values.description,
2379
+ provider: values.provider ?? "",
2380
+ description: values.description ?? "",
848
2381
  note: values.note || void 0
849
2382
  };
850
2383
  await createFederatedProvider(request);
851
2384
  }
852
2385
  showNotice(isExisting ? "Provider SSO aggiornato." : "Provider SSO censito.");
853
- await show("sso");
2386
+ await show("ssoProviders");
854
2387
  });
855
2388
  };
856
2389
  }
2390
+ function registrationStateOptions(current) {
2391
+ const states = [
2392
+ [0, "Non definito"],
2393
+ [5, "Inserito"],
2394
+ [10, "Invitato"],
2395
+ [20, "Confermato"],
2396
+ [50, "Eliminato"],
2397
+ [99, "Bloccato"]
2398
+ ];
2399
+ const currentValue = Number(current);
2400
+ return states.map(([value, label]) => `<option value="${value}" ${currentValue === value ? "selected" : ""}>${label} (${value})</option>`).join("");
2401
+ }
857
2402
  async function showUser(rawCode) {
858
2403
  const codiceUtente = Number(rawCode);
859
2404
  const [users, roles, grants, groups, providers] = await Promise.all([
@@ -863,7 +2408,9 @@ async function showUser(rawCode) {
863
2408
  getGroupsWithMenus({ includeDisabled: true }),
864
2409
  federatedAuthenticationAvailable ? getFederatedProviders() : Promise.resolve(null)
865
2410
  ]);
866
- const user = result(users)[0].utente;
2411
+ const userRow = result(users)[0];
2412
+ if (!userRow?.utente) throw new Error("Utente non trovato.");
2413
+ const user = userRow.utente;
867
2414
  const allRoles = result(roles);
868
2415
  const userGrants = result(grants);
869
2416
  const menuGroups = result(groups);
@@ -871,9 +2418,9 @@ async function showUser(rawCode) {
871
2418
  const linked = federatedAuthenticationAvailable ? result(await getFederatedIdentities(codiceUtente)) : [];
872
2419
  const identitySection = federatedAuthenticationAvailable ? federatedIdentitySection(linked, providerList) : "";
873
2420
  const defaultUserTab = federatedAuthenticationAvailable ? "sso" : "profile";
874
- render(`<div class="user-action-bar"><button id="back" type="button" class="secondary">Indietro</button><button id="disable-user" type="button" class="danger-button">Imposta stato eliminato</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>${userDetailTabs(federatedAuthenticationAvailable)}<div class="user-editor">
2421
+ render(`<div class="user-action-bar"><button id="back" type="button" class="secondary">Indietro</button><label class="inline-field">Stato registrazione ${helpDot("user-state")}<select id="user-state">${registrationStateOptions(user.statoRegistrazione)}</select></label><button id="save-state" type="button" class="secondary">Aggiorna stato</button><button id="disable-user" type="button" class="danger-button">Imposta stato eliminato</button>${helpDot("user-delete")}</div><div class="page-header"><div><p class="eyebrow">Utente ${codiceUtente}</p><h2>${escapeHtml(user.email)}</h2></div><span class="muted">Gestione profilo, ruoli e autorizzazioni</span></div>${userDetailTabs(federatedAuthenticationAvailable)}<div class="user-editor">
875
2422
  ${identitySection}
876
- <form id="profile" data-user-panel="profile" role="tabpanel" aria-labelledby="user-tab-profile"><h3>Profilo e accesso</h3><label>Nome<input name="nome" value="${escapeHtml(user.nome)}"></label><label>Cognome<input name="cognome" value="${escapeHtml(user.cognome)}"></label><label>Email<input name="email" type="email" value="${escapeHtml(user.email)}" required><span class="form-help">Identificativo di accesso locale e recapito per le comunicazioni Accessi.</span></label>${federatedAuthenticationAvailable ? `<label class="check"><input name="passwordLoginEnabled" type="checkbox" ${user.passwordLoginEnabled !== false ? "checked" : ""}> Login con password</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)}<button>Salva</button></form>
2423
+ <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>${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)}<button>Salva</button></form>
877
2424
  ${roleAssignmentEditor(allRoles, userGrants.ruoli, menuGroups)}
878
2425
  ${directGrantEditor(menuGroups, userGrants.abilitazioni)}
879
2426
  </div>`);
@@ -885,8 +2432,10 @@ async function showUser(rawCode) {
885
2432
  const targetIndex = event.key === "ArrowRight" ? (index + 1) % userTabButtons.length : event.key === "ArrowLeft" ? (index - 1 + userTabButtons.length) % userTabButtons.length : event.key === "Home" ? 0 : event.key === "End" ? userTabButtons.length - 1 : -1;
886
2433
  if (targetIndex < 0) return;
887
2434
  event.preventDefault();
888
- userTabButtons[targetIndex].focus();
889
- activateUserTab(userTabButtons[targetIndex].dataset.userTab);
2435
+ const targetTab = userTabButtons[targetIndex];
2436
+ if (!targetTab) return;
2437
+ targetTab.focus();
2438
+ activateUserTab(targetTab.dataset.userTab);
890
2439
  };
891
2440
  });
892
2441
  activateUserTab(defaultUserTab);
@@ -905,7 +2454,7 @@ async function showUser(rawCode) {
905
2454
  const fields = new FormData(eventForm(event));
906
2455
  await updateUtente(codiceUtente, {
907
2456
  codiceUtente,
908
- email: values.email,
2457
+ email: values.email ?? "",
909
2458
  nome: values.nome || void 0,
910
2459
  cognome: values.cognome || void 0,
911
2460
  flagDueFattori: fields.has("flagDueFattori"),
@@ -953,7 +2502,7 @@ async function showUser(rawCode) {
953
2502
  event.preventDefault();
954
2503
  handleAction(async () => {
955
2504
  const values = formValues(eventForm(event));
956
- await linkFederatedIdentity(codiceUtente, { provider: values.provider, subject: values.subject, note: values.note || void 0 });
2505
+ await linkFederatedIdentity(codiceUtente, { provider: values.provider ?? "", subject: values.subject ?? "", note: values.note || void 0 });
957
2506
  await showUser(String(codiceUtente));
958
2507
  });
959
2508
  };
@@ -972,6 +2521,13 @@ async function showUser(rawCode) {
972
2521
  });
973
2522
  });
974
2523
  }
2524
+ byId("save-state").onclick = () => handleAction(async () => {
2525
+ const stateSelect = document.getElementById("user-state");
2526
+ const statoRegistrazione = Number(stateSelect?.value ?? 0);
2527
+ await setStatoRegistrazione({ codiceUtente, statoRegistrazione });
2528
+ showNotice("Stato registrazione aggiornato.");
2529
+ await showUser(String(codiceUtente));
2530
+ });
975
2531
  byId("disable-user").onclick = () => handleAction(async () => {
976
2532
  await deleteUser(codiceUtente);
977
2533
  showNotice("Utente impostato come eliminato.");
@@ -982,16 +2538,14 @@ async function showRoles() {
982
2538
  const [rolesResponse, groupsResponse] = await Promise.all([getRoles(), getGroupsWithMenus({ includeDisabled: true })]);
983
2539
  const roles = result(rolesResponse);
984
2540
  const groups = result(groupsResponse);
985
- render(`<div class="toolbar"><button id="new-role">Nuovo ruolo</button></div><h2>Ruoli e grant</h2><p class="form-help">Panoramica completa dei menu inclusi in ogni ruolo. Il livello indica l'abilitazione assegnata dal ruolo per quello specifico menu.</p><div class="role-overview-list">${roles.map((role) => roleOverview(role, groups)).join("") || '<p class="muted">Nessun ruolo configurato.</p>'}</div>`);
2541
+ render(`<div class="toolbar"><button id="new-role">Nuovo ruolo</button></div><h2>Ruoli</h2><p class="form-help">Definisci i ruoli e i menu che concedono. L'assegnazione agli utenti si gestisce dalla scheda utente.</p><div class="role-overview-list">${roles.map((role) => roleOverview(role, groups)).join("") || '<p class="muted">Nessun ruolo configurato.</p>'}</div>`);
986
2542
  const roleForm = (role) => {
987
- render(`<button id="back" type="button">Indietro</button><h2>${role ? "Modifica" : "Nuovo"} ruolo</h2><form id="role-form"><label>Descrizione<input name="descrizione" value="${escapeHtml(role?.descrizioneRuolo)}" required><span class="form-help">Nome mostrato agli amministratori per identificare questo insieme di autorizzazioni.</span></label><p class="form-help">Ogni gruppo presenta una riga per menu. Seleziona i menu da includere e imposta il livello di abilitazione. Il salvataggio sostituisce integralmente la configurazione corrente del ruolo.</p><div class="role-menu-groups">${groups.map((group) => roleMenuEditorTable(group, role)).join("")}</div><button>Salva ruolo</button></form>`);
2543
+ render(`<button id="back" type="button">Indietro</button><h2>${role ? "Modifica" : "Nuovo"} ruolo</h2><form id="role-form"><label>Descrizione ${helpDot("role-description")}<input name="descrizione" value="${escapeHtml(role?.descrizioneRuolo)}" required><span class="form-help">Nome del ruolo.</span></label><p class="form-help">Seleziona i menu e il livello ${helpDot("role-level")}; il salvataggio sostituisce la configurazione del ruolo.</p><div class="role-menu-groups">${groups.map((group) => roleMenuEditorTable(group, role)).join("")}</div><button>Salva ruolo</button></form>`);
988
2544
  byId("back").onclick = () => show("roles");
989
2545
  document.querySelectorAll('#role-form input[name="menu"]').forEach((checkbox) => {
990
2546
  checkbox.onchange = () => {
991
- const row = checkbox.closest("tr");
992
- const status = checkbox.parentElement?.querySelector("span");
993
- const level = row?.querySelector('select[name^="menu-level:"]');
994
- if (status) status.textContent = checkbox.checked ? "Incluso" : "Escluso";
2547
+ const option = checkbox.closest("li");
2548
+ const level = option?.querySelector('select[name^="menu-level:"]');
995
2549
  if (level) level.disabled = !checkbox.checked;
996
2550
  };
997
2551
  });
@@ -1000,7 +2554,7 @@ async function showRoles() {
1000
2554
  event.preventDefault();
1001
2555
  const form = eventForm(event);
1002
2556
  const data = new FormData(form);
1003
- const request = { descrizioneRuolo: formValues(form).descrizione, menu: Array.from(data.getAll("menu"), (codiceMenu) => ({ codiceMenu: String(codiceMenu), tipoAbilitazione: Number(data.get(`menu-level:${codiceMenu}`) ?? 10) })) };
2557
+ const request = { descrizioneRuolo: formValues(form).descrizione ?? "", menu: Array.from(data.getAll("menu"), (codiceMenu) => ({ codiceMenu: String(codiceMenu), tipoAbilitazione: Number(data.get(`menu-level:${codiceMenu}`) ?? 10) })) };
1004
2558
  if (role?.codiceRuolo) await updateRole(role.codiceRuolo, request);
1005
2559
  else await createRole(request);
1006
2560
  await show("roles");
@@ -1008,44 +2562,462 @@ async function showRoles() {
1008
2562
  };
1009
2563
  byId("new-role").onclick = () => roleForm();
1010
2564
  document.querySelectorAll("[data-role]").forEach((button) => button.onclick = () => roleForm(roles.find((role) => role.codiceRuolo === Number(button.dataset.role))));
2565
+ document.querySelectorAll("[data-role-delete]").forEach((button) => button.onclick = () => handleAction(async () => {
2566
+ if (!window.confirm("Eliminare definitivamente questo ruolo? Le assegnazioni agli utenti verranno rimosse.")) return;
2567
+ await deleteRole(Number(button.dataset.roleDelete));
2568
+ showNotice("Ruolo eliminato.");
2569
+ await show("roles");
2570
+ }));
1011
2571
  }
1012
- async function showMenus() {
2572
+ function menuGroupsTable(groups) {
2573
+ return `<div class="menu-table-wrap"><table><thead><tr><th>Codice</th><th>Descrizione</th><th>Ordine</th><th>Menu</th><th>Stato</th><th></th></tr></thead><tbody>${groups.map((group) => `<tr class="${group.enabled === false ? "is-disabled" : ""}"><td><code>${escapeHtml(group.codiceGruppo)}</code></td><td><strong>${escapeHtml(group.descrizioneGruppo)}</strong></td><td>${escapeHtml(group.ordineGruppo)}</td><td>${group.menus.length}</td><td><label class="table-toggle"><input type="checkbox" data-group="${escapeHtml(group.codiceGruppo)}" ${group.enabled !== false ? "checked" : ""}><span>${group.enabled === false ? "Disabilitato" : "Abilitato"}</span></label></td><td class="row-actions"><button type="button" data-group-edit="${escapeHtml(group.codiceGruppo)}">Modifica</button><button type="button" class="danger-button" data-group-delete="${escapeHtml(group.codiceGruppo)}">Elimina</button></td></tr>`).join("") || '<tr><td colspan="6" class="muted">Nessun gruppo configurato.</td></tr>'}</tbody></table></div>`;
2574
+ }
2575
+ function menusTable(entries, menuTypes) {
2576
+ const typeLabel = (codice) => {
2577
+ if (!codice) return "Non indicato";
2578
+ return menuTypes.find((type) => type.codiceTipo === codice)?.descrizioneTipo || codice;
2579
+ };
2580
+ return `<div class="menu-table-wrap"><table><thead><tr><th>Stato</th><th>Menu</th><th>Codice</th><th>Gruppo</th><th>Ordine</th><th>Tipo</th><th>Pagina</th><th></th></tr></thead><tbody>${entries.map(({ menu, group }) => `<tr class="${menu.enabled === false ? "is-disabled" : ""}" data-menu-group="${escapeHtml(group.codiceGruppo)}"><td><label class="table-toggle"><input type="checkbox" data-menu="${escapeHtml(menu.codiceMenu)}" ${menu.enabled !== false ? "checked" : ""}><span>${menu.enabled === false ? "Disabilitato" : "Abilitato"}</span></label></td><td><strong>${escapeHtml(menu.descrizioneMenu)}</strong>${menu.note ? `<br><span class="muted">${escapeHtml(menu.note)}</span>` : ""}</td><td><code>${escapeHtml(menu.codiceMenu)}</code></td><td>${escapeHtml(group.descrizioneGruppo)}</td><td>${escapeHtml(menu.ordineMenu)}</td><td>${escapeHtml(typeLabel(menu.tipo))}</td><td>${menu.pagina ? `<code>${escapeHtml(menu.pagina)}</code>` : '<span class="muted">Non indicata</span>'}</td><td class="row-actions"><button type="button" data-menu-edit="${escapeHtml(menu.codiceMenu)}">Modifica</button><button type="button" class="danger-button" data-menu-delete="${escapeHtml(menu.codiceMenu)}">Elimina</button></td></tr>`).join("") || '<tr><td colspan="8" class="muted">Nessun menu configurato.</td></tr>'}</tbody></table></div>`;
2581
+ }
2582
+ function menuTypeTable(types) {
2583
+ return `<div class="menu-table-wrap"><table><thead><tr><th>Codice</th><th>Descrizione</th><th></th></tr></thead><tbody>${types.map((type) => `<tr><td><code>${escapeHtml(type.codiceTipo)}</code></td><td>${escapeHtml(type.descrizioneTipo || "\u2014")}</td><td class="row-actions"><button type="button" data-type-edit="${escapeHtml(type.codiceTipo)}">Modifica</button><button type="button" class="danger-button" data-type-delete="${escapeHtml(type.codiceTipo)}">Elimina</button></td></tr>`).join("") || '<tr><td colspan="3" class="muted">Nessun tipo menu.</td></tr>'}</tbody></table></div>`;
2584
+ }
2585
+ async function showMenuGroups() {
1013
2586
  const groups = result(await getGroupsWithMenus({ includeDisabled: true }));
1014
- render(`<h2>Menu e gruppi</h2><p class="form-help">Attiva o disattiva gruppi e menu. Ogni riga riporta i dati necessari per individuare l'uso tecnico del menu.</p><div class="menu-group-list">${groups.map(menuGroupTable).join("")}</div>`);
1015
- document.querySelectorAll("[data-group]").forEach((input) => input.onchange = async () => {
1016
- await setGroupEnabled(input.dataset.group ?? "", { enabled: input.checked });
1017
- showNotice("Gruppo aggiornato.");
2587
+ render(`<div class="toolbar"><button id="new-group">Nuovo gruppo</button><button id="reload-groups" class="secondary">Aggiorna</button></div>
2588
+ <h2>Gruppi di menu</h2><p class="form-help">Raggruppano i menu nella navigazione e ne definiscono l'ordine. Un gruppo si elimina solo se non contiene menu.</p>
2589
+ ${menuGroupsTable(groups)}`);
2590
+ byId("new-group").onclick = () => showMenuGroupForm();
2591
+ byId("reload-groups").onclick = () => handleAction(showMenuGroups);
2592
+ document.querySelectorAll("[data-group]").forEach((input) => {
2593
+ input.onchange = () => handleAction(async () => {
2594
+ await setGroupEnabled(input.dataset.group ?? "", { enabled: input.checked });
2595
+ showNotice("Gruppo aggiornato.");
2596
+ });
1018
2597
  });
1019
- document.querySelectorAll("[data-menu]").forEach((input) => input.onchange = async () => {
1020
- await setMenuEnabled(input.dataset.menu ?? "", { enabled: input.checked });
1021
- showNotice("Menu aggiornato.");
2598
+ document.querySelectorAll("[data-group-edit]").forEach((button) => {
2599
+ button.onclick = () => showMenuGroupForm(groups.find((group) => group.codiceGruppo === button.dataset.groupEdit));
2600
+ });
2601
+ document.querySelectorAll("[data-group-delete]").forEach((button) => {
2602
+ button.onclick = () => handleAction(async () => {
2603
+ if (!window.confirm(`Eliminare il gruppo ${button.dataset.groupDelete}? Operazione possibile solo se non contiene menu.`)) return;
2604
+ await deleteMenuGroup(button.dataset.groupDelete ?? "");
2605
+ showNotice("Gruppo eliminato.");
2606
+ await showMenuGroups();
2607
+ });
1022
2608
  });
1023
2609
  }
2610
+ async function showMenuItems() {
2611
+ const [groupsResponse, typesResponse] = await Promise.all([
2612
+ getGroupsWithMenus({ includeDisabled: true }),
2613
+ getMenuTypes()
2614
+ ]);
2615
+ const groups = result(groupsResponse);
2616
+ const menuTypes = result(typesResponse);
2617
+ const entries = groups.flatMap((group) => group.menus.map((menu) => ({ menu, group })));
2618
+ render(`<div class="toolbar"><button id="new-menu">Nuovo menu</button><button id="reload-menus" class="secondary">Aggiorna</button><label class="inline-field">Filtra per gruppo<select id="menu-group-filter"><option value="">Tutti i gruppi</option>${groups.map((group) => `<option value="${escapeHtml(group.codiceGruppo)}">${escapeHtml(group.descrizioneGruppo)}</option>`).join("")}</select></label></div>
2619
+ <h2>Menu</h2><p class="form-help">Voci di navigazione. L'eliminazione rimuove anche i grant utente e le associazioni ai ruoli.</p>
2620
+ ${menusTable(entries, menuTypes)}`);
2621
+ byId("new-menu").onclick = () => showMenuForm(void 0, groups, menuTypes);
2622
+ byId("reload-menus").onclick = () => handleAction(showMenuItems);
2623
+ const groupFilter = document.getElementById("menu-group-filter");
2624
+ if (groupFilter) {
2625
+ groupFilter.onchange = () => {
2626
+ const selected = groupFilter.value;
2627
+ document.querySelectorAll("[data-menu-group]").forEach((row) => {
2628
+ row.hidden = selected !== "" && row.dataset.menuGroup !== selected;
2629
+ });
2630
+ };
2631
+ }
2632
+ document.querySelectorAll("[data-menu]").forEach((input) => {
2633
+ input.onchange = () => handleAction(async () => {
2634
+ await setMenuEnabled(input.dataset.menu ?? "", { enabled: input.checked });
2635
+ showNotice("Menu aggiornato.");
2636
+ });
2637
+ });
2638
+ document.querySelectorAll("[data-menu-edit]").forEach((button) => {
2639
+ button.onclick = () => showMenuForm(entries.find((entry) => entry.menu.codiceMenu === button.dataset.menuEdit)?.menu, groups, menuTypes);
2640
+ });
2641
+ document.querySelectorAll("[data-menu-delete]").forEach((button) => {
2642
+ button.onclick = () => handleAction(async () => {
2643
+ if (!window.confirm(`Eliminare il menu ${button.dataset.menuDelete}? Verranno rimossi anche i grant utente e le associazioni ai ruoli.`)) return;
2644
+ await deleteMenu(button.dataset.menuDelete ?? "");
2645
+ showNotice("Menu eliminato.");
2646
+ await showMenuItems();
2647
+ });
2648
+ });
2649
+ }
2650
+ async function showMenuTypes() {
2651
+ const menuTypes = result(await getMenuTypes());
2652
+ render(`<div class="toolbar"><button id="new-menu-type">Nuovo tipo menu</button><button id="reload-menu-types" class="secondary">Aggiorna</button></div>
2653
+ <h2>Tipi menu</h2><p class="form-help">Classificano i menu (es. amministrazione, operativit\xE0). Un tipo si elimina solo se non \xE8 usato da alcun menu.</p>
2654
+ ${menuTypeTable(menuTypes)}`);
2655
+ byId("new-menu-type").onclick = () => showMenuTypeForm();
2656
+ byId("reload-menu-types").onclick = () => handleAction(showMenuTypes);
2657
+ document.querySelectorAll("[data-type-edit]").forEach((button) => {
2658
+ button.onclick = () => showMenuTypeForm(menuTypes.find((type) => type.codiceTipo === button.dataset.typeEdit));
2659
+ });
2660
+ document.querySelectorAll("[data-type-delete]").forEach((button) => {
2661
+ button.onclick = () => handleAction(async () => {
2662
+ if (!window.confirm(`Eliminare il tipo menu ${button.dataset.typeDelete}? Operazione possibile solo se nessun menu lo utilizza.`)) return;
2663
+ await deleteMenuType(button.dataset.typeDelete ?? "");
2664
+ showNotice("Tipo menu eliminato.");
2665
+ await showMenuTypes();
2666
+ });
2667
+ });
2668
+ }
2669
+ function showMenuGroupForm(group) {
2670
+ const isExisting = group !== void 0;
2671
+ render(`<button id="back" type="button">Indietro</button><h2>${isExisting ? "Modifica" : "Nuovo"} gruppo menu</h2>
2672
+ <form id="group-form">
2673
+ <label>Codice gruppo ${helpDot("menu-group-code")}<input name="codiceGruppo" maxlength="1" value="${escapeHtml(group?.codiceGruppo)}" ${isExisting ? "readonly" : ""} required><span class="form-help">Un solo carattere, es. <code>A</code>. Immutabile dopo la creazione.</span></label>
2674
+ <label>Descrizione<input name="descrizioneGruppo" maxlength="100" value="${escapeHtml(group?.descrizioneGruppo)}" required></label>
2675
+ <label>Ordine ${helpDot("menu-group-order")}<input name="ordineGruppo" type="number" value="${group?.ordineGruppo ?? ""}"><span class="form-help">Usato per ordinare i gruppi nella navigazione.</span></label>
2676
+ <label class="check"><input name="enabled" type="checkbox" ${group?.enabled !== false ? "checked" : ""}> Gruppo abilitato ${helpDot("menu-group-enabled")}</label>
2677
+ <button>${isExisting ? "Salva gruppo" : "Crea gruppo"}</button>
2678
+ </form>`);
2679
+ byId("back").onclick = () => handleAction(showMenuGroups);
2680
+ const form = document.getElementById("group-form");
2681
+ if (form) {
2682
+ form.onsubmit = (event) => {
2683
+ event.preventDefault();
2684
+ handleAction(async () => {
2685
+ const values = formValues(eventForm(event));
2686
+ const payload = {
2687
+ descrizioneGruppo: values.descrizioneGruppo || void 0,
2688
+ ordineGruppo: values.ordineGruppo ? Number(values.ordineGruppo) : void 0,
2689
+ enabled: new FormData(eventForm(event)).has("enabled")
2690
+ };
2691
+ if (isExisting) await updateMenuGroup(group.codiceGruppo, payload);
2692
+ else await createMenuGroup({ codiceGruppo: values.codiceGruppo ?? "", ...payload });
2693
+ showNotice(isExisting ? "Gruppo aggiornato." : "Gruppo creato.");
2694
+ await showMenuGroups();
2695
+ });
2696
+ };
2697
+ }
2698
+ }
2699
+ function showMenuForm(menu, groups, menuTypes) {
2700
+ const isExisting = menu !== void 0;
2701
+ const groupOptions = groups.map((group) => `<option value="${escapeHtml(group.codiceGruppo)}" ${menu?.codiceGruppo === group.codiceGruppo ? "selected" : ""}>${escapeHtml(group.codiceGruppo)} \u2013 ${escapeHtml(group.descrizioneGruppo)}</option>`).join("");
2702
+ const typeOptions = `<option value="">Nessun tipo</option>${menuTypes.map((type) => `<option value="${escapeHtml(type.codiceTipo)}" ${menu?.tipo === type.codiceTipo ? "selected" : ""}>${escapeHtml(type.codiceTipo)} \u2013 ${escapeHtml(type.descrizioneTipo || "")}</option>`).join("")}`;
2703
+ render(`<button id="back" type="button">Indietro</button><h2>${isExisting ? "Modifica" : "Nuovo"} menu</h2>
2704
+ <form id="menu-form">
2705
+ <label>Codice menu ${helpDot("menu-code")}<input name="codiceMenu" maxlength="20" value="${escapeHtml(menu?.codiceMenu)}" ${isExisting ? "readonly" : ""} required><span class="form-help">Chiave tecnica, es. <code>MNU001</code>. Immutabile dopo la creazione.</span></label>
2706
+ <label>Descrizione<input name="descrizioneMenu" maxlength="100" value="${escapeHtml(menu?.descrizioneMenu)}" required></label>
2707
+ <label>Gruppo ${helpDot("menu-group-select")}<select name="codiceGruppo" required>${groupOptions}</select><span class="form-help">Crea prima un gruppo se non ne esistono.</span></label>
2708
+ <label>Tipo menu ${helpDot("menu-type-select")}<select name="tipo">${typeOptions}</select></label>
2709
+ <label>Icona ${helpDot("menu-icon")}<input name="icona" maxlength="50" value="${escapeHtml(menu?.icona)}"><span class="form-help">Identificatore icona usato dalla UI host.</span></label>
2710
+ <label>Pagina ${helpDot("menu-page")}<input name="pagina" maxlength="50" value="${escapeHtml(menu?.pagina)}"><span class="form-help">Percorso o pagina associata al menu.</span></label>
2711
+ <label>Ordine ${helpDot("menu-order")}<input name="ordineMenu" type="number" value="${menu?.ordineMenu ?? ""}"></label>
2712
+ <label>Menu di riferimento ${helpDot("menu-rifmenu")}<input name="rifMenu" maxlength="20" value="${escapeHtml(menu?.rifMenu)}"><span class="form-help">Codice di un menu padre, opzionale.</span></label>
2713
+ <label>Nota ${helpDot("menu-note")}<textarea name="note" rows="3" maxlength="1000">${escapeHtml(menu?.note)}</textarea></label>
2714
+ <label class="check"><input name="enabled" type="checkbox" ${menu?.enabled !== false ? "checked" : ""}> Menu abilitato ${helpDot("menu-enabled")}</label>
2715
+ <button>${isExisting ? "Salva menu" : "Crea menu"}</button>
2716
+ </form>`);
2717
+ byId("back").onclick = () => handleAction(showMenuItems);
2718
+ const form = document.getElementById("menu-form");
2719
+ if (form) {
2720
+ form.onsubmit = (event) => {
2721
+ event.preventDefault();
2722
+ handleAction(async () => {
2723
+ const values = formValues(eventForm(event));
2724
+ const payload = {
2725
+ descrizioneMenu: values.descrizioneMenu || void 0,
2726
+ codiceGruppo: values.codiceGruppo ?? "",
2727
+ tipo: values.tipo || void 0,
2728
+ icona: values.icona || void 0,
2729
+ pagina: values.pagina || void 0,
2730
+ ordineMenu: values.ordineMenu ? Number(values.ordineMenu) : void 0,
2731
+ rifMenu: values.rifMenu || void 0,
2732
+ note: values.note || void 0,
2733
+ enabled: new FormData(eventForm(event)).has("enabled")
2734
+ };
2735
+ if (isExisting) await updateMenu(menu.codiceMenu, payload);
2736
+ else await createMenu({ codiceMenu: values.codiceMenu ?? "", ...payload });
2737
+ showNotice(isExisting ? "Menu aggiornato." : "Menu creato.");
2738
+ await showMenuItems();
2739
+ });
2740
+ };
2741
+ }
2742
+ }
2743
+ function showMenuTypeForm(type) {
2744
+ const isExisting = type !== void 0;
2745
+ render(`<button id="back" type="button">Indietro</button><h2>${isExisting ? "Modifica" : "Nuovo"} tipo menu</h2>
2746
+ <form id="menu-type-form">
2747
+ <label>Codice tipo ${helpDot("menu-type-code")}<input name="codiceTipo" maxlength="1" value="${escapeHtml(type?.codiceTipo)}" ${isExisting ? "readonly" : ""} required><span class="form-help">Un solo carattere, es. <code>A</code>. Immutabile dopo la creazione.</span></label>
2748
+ <label>Descrizione<input name="descrizioneTipo" maxlength="20" value="${escapeHtml(type?.descrizioneTipo)}"></label>
2749
+ <button>${isExisting ? "Salva tipo" : "Crea tipo"}</button>
2750
+ </form>`);
2751
+ byId("back").onclick = () => handleAction(showMenuTypes);
2752
+ const form = document.getElementById("menu-type-form");
2753
+ if (form) {
2754
+ form.onsubmit = (event) => {
2755
+ event.preventDefault();
2756
+ handleAction(async () => {
2757
+ const values = formValues(eventForm(event));
2758
+ if (isExisting) await updateMenuType(type.codiceTipo, { descrizioneTipo: values.descrizioneTipo || void 0 });
2759
+ else await createMenuType({ codiceTipo: values.codiceTipo ?? "", descrizioneTipo: values.descrizioneTipo || void 0 });
2760
+ showNotice(isExisting ? "Tipo menu aggiornato." : "Tipo menu creato.");
2761
+ await showMenuTypes();
2762
+ });
2763
+ };
2764
+ }
2765
+ }
2766
+ function filterTypeRows(types) {
2767
+ 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>`;
2768
+ }
1024
2769
  async function showFilters() {
1025
2770
  const users = await loadUsers();
1026
- render(`<h2>Filtri utente</h2><form id="filters"><label>Utente<select name="codUte">${users.map(({ utente }) => `<option value="${utente.codiceUtente}">${escapeHtml(utente.email)}</option>`).join("")}</select><span class="form-help">I filtri vengono letti e salvati esclusivamente per l'utente selezionato.</span></label><label>Filtro JSON<textarea name="json" rows="12">{}</textarea><span class="form-help">Configurazione tecnica del filtro. Usa Carica per partire dalla struttura esistente e mantieni il JSON valido prima di salvare.</span></label><button name="action" value="load">Carica</button><button name="action" value="save">Salva</button></form>`);
2771
+ 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>`);
1027
2772
  const filtersForm = document.getElementById("filters");
1028
2773
  if (filtersForm) filtersForm.onsubmit = async (event) => {
1029
2774
  event.preventDefault();
1030
2775
  const form = eventForm(event);
1031
2776
  const action = event.submitter?.value;
1032
- const code = Number(formValues(form).codUte);
2777
+ const code3 = Number(formValues(form).codUte);
1033
2778
  if (action === "load") {
1034
- const filters = result(await getFiltriUtente({ codUte: code }));
1035
- form.elements.namedItem("json").value = JSON.stringify(filters[0] ?? { codUte: code }, null, 2);
2779
+ const filters = result(await getFiltriUtente({ codUte: code3 }));
2780
+ form.elements.namedItem("json").value = JSON.stringify(filters[0] ?? { codUte: code3 }, null, 2);
1036
2781
  } else {
1037
- const parsed = JSON.parse(formValues(form).json);
1038
- await saveFiltriUtente({ ...parsed, codUte: code });
2782
+ const parsed = JSON.parse(formValues(form).json ?? "{}");
2783
+ await saveFiltriUtente({ ...parsed, codUte: code3 });
1039
2784
  showNotice("Filtri salvati.");
1040
2785
  }
1041
2786
  };
1042
2787
  }
2788
+ async function showFilterTypes() {
2789
+ const filterTypes = result(await getTipiFiltro());
2790
+ render(`<div class="toolbar"><button id="new-filter-type">Nuovo tipo filtro</button><button id="reload-filter-types" class="secondary">Aggiorna</button></div>
2791
+ <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>
2792
+ ${filterTypeRows(filterTypes)}`);
2793
+ byId("new-filter-type").onclick = () => showFilterTypeForm();
2794
+ byId("reload-filter-types").onclick = () => handleAction(showFilterTypes);
2795
+ document.querySelectorAll("[data-filter-type-edit]").forEach((button) => button.onclick = () => showFilterTypeForm(filterTypes.find((type) => String(type.tipFil) === button.dataset.filterTypeEdit)));
2796
+ document.querySelectorAll("[data-filter-type-delete]").forEach((button) => button.onclick = () => handleAction(async () => {
2797
+ if (!window.confirm(`Eliminare il tipo filtro ${button.dataset.filterTypeDelete}? Operazione possibile solo se nessun filtro lo utilizza.`)) return;
2798
+ await deleteTipoFiltro(Number(button.dataset.filterTypeDelete));
2799
+ showNotice("Tipo filtro eliminato.");
2800
+ await showFilterTypes();
2801
+ }));
2802
+ }
2803
+ function showFilterTypeForm(type) {
2804
+ const isExisting = type !== void 0;
2805
+ render(`<button id="back" type="button">Indietro</button><h2>${isExisting ? "Modifica" : "Nuovo"} tipo filtro</h2>
2806
+ <form id="filter-type-form">
2807
+ <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>
2808
+ <label>Descrizione<input name="desFil" maxlength="20" value="${escapeHtml(type?.desFil)}"></label>
2809
+ <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>
2810
+ <label class="check"><input name="flgEnabled" type="checkbox" ${type?.flgEnabled !== 0 ? "checked" : ""}> Abilitato ${helpDot("filter-type-enabled")}</label>
2811
+ <button>${isExisting ? "Salva tipo" : "Crea tipo"}</button>
2812
+ </form>`);
2813
+ byId("back").onclick = () => handleAction(showFilterTypes);
2814
+ const form = document.getElementById("filter-type-form");
2815
+ if (form) {
2816
+ form.onsubmit = (event) => {
2817
+ event.preventDefault();
2818
+ handleAction(async () => {
2819
+ const values = formValues(eventForm(event));
2820
+ const payload = {
2821
+ desFil: values.desFil || void 0,
2822
+ fldFil: values.fldFil || void 0,
2823
+ flgEnabled: new FormData(eventForm(event)).has("flgEnabled") ? 1 : 0
2824
+ };
2825
+ if (isExisting) await updateTipoFiltro(type.tipFil, payload);
2826
+ else await createTipoFiltro({ tipFil: Number(values.tipFil), ...payload });
2827
+ showNotice(isExisting ? "Tipo filtro aggiornato." : "Tipo filtro creato.");
2828
+ await showFilterTypes();
2829
+ });
2830
+ };
2831
+ }
2832
+ }
2833
+ function scopeList(scopes) {
2834
+ return (scopes ?? []).join(", ") || "nessuno";
2835
+ }
2836
+ function serviceTokenExpiry(token2) {
2837
+ if (!token2.expiresAt) return "nessuna";
2838
+ const date = new Date(token2.expiresAt);
2839
+ const days = Math.ceil((date.getTime() - Date.now()) / 864e5);
2840
+ const label = escapeHtml(date.toLocaleString());
2841
+ if (days < 0) return `${label}<br><span class="error">scaduto</span>`;
2842
+ if (days <= 7) return `${label}<br><span class="error">scade tra ${days} g</span>`;
2843
+ return label;
2844
+ }
2845
+ function serviceTokenRows(tokens, includeRevoked) {
2846
+ const lastUsed = (token2) => token2.lastUsedAt ? `<span title="${escapeHtml(token2.lastUsedIp ?? "")}">${escapeHtml(new Date(token2.lastUsedAt).toLocaleString())}</span>${token2.lastUsedIp ? `<br><span class="muted">${escapeHtml(token2.lastUsedIp)}</span>` : ""}` : "mai";
2847
+ const status = (token2) => token2.revoked ? `Revocato${token2.revokedBy ? ` da #${escapeHtml(token2.revokedBy)}` : ""}${token2.revokedAt ? `<br><span class="muted">${escapeHtml(new Date(token2.revokedAt).toLocaleString())}</span>` : ""}` : "Attivo";
2848
+ return tokens.map((token2) => `<tr class="${token2.revoked ? "is-disabled" : ""}"><td><strong>${escapeHtml(token2.label)}</strong><br><code>${escapeHtml(token2.tokenId)}</code></td><td>${escapeHtml(scopeList(token2.scopes))}</td><td>${escapeHtml(token2.createdAt ? new Date(token2.createdAt).toLocaleString() : "")}</td><td>${serviceTokenExpiry(token2)}</td><td>${lastUsed(token2)}</td><td>${status(token2)}</td><td>${token2.revoked ? "" : `<button type="button" data-rotate="${escapeHtml(token2.tokenId)}">Ruota</button> <button type="button" class="danger-button" data-revoke="${escapeHtml(token2.tokenId)}">Revoca</button>`}</td></tr>`).join("") || `<tr><td colspan="7">Nessun token ${includeRevoked ? "" : "attivo"}.</td></tr>`;
2849
+ }
2850
+ async function showServiceTokens(includeRevoked = false) {
2851
+ const tokens = result(await getServiceTokens({ includeRevoked }));
2852
+ render(`<div class="toolbar"><button id="new-token">Nuovo token</button><button id="toggle-revoked" class="secondary">${includeRevoked ? "Nascondi revocati" : "Mostra revocati"}</button></div><h2>Token di servizio</h2><p class="form-help">Credenziali macchina-a-macchina per chiamate tecniche, non legate a un utente. Il segreto e mostrato <strong>una sola volta</strong> alla creazione: conservalo come variabile d'ambiente/secret. Autenticazione: <code>Authorization: Bearer &lt;token&gt;</code>.</p><table><thead><tr><th>Token</th><th>Scope</th><th>Creato</th><th>Scade</th><th>Ultimo uso</th><th>Stato</th><th></th></tr></thead><tbody>${serviceTokenRows(tokens, includeRevoked)}</tbody></table>`);
2853
+ byId("new-token").onclick = () => showServiceTokenForm();
2854
+ byId("toggle-revoked").onclick = () => handleAction(() => showServiceTokens(!includeRevoked));
2855
+ document.querySelectorAll("[data-revoke]").forEach((button) => {
2856
+ button.onclick = () => handleAction(async () => {
2857
+ if (!window.confirm("Revocare definitivamente questo token? Le integrazioni che lo usano smetteranno di funzionare.")) return;
2858
+ await revokeServiceToken(button.dataset.revoke ?? "");
2859
+ showNotice("Token di servizio revocato.");
2860
+ await showServiceTokens(includeRevoked);
2861
+ });
2862
+ });
2863
+ document.querySelectorAll("[data-rotate]").forEach((button) => {
2864
+ button.onclick = () => handleAction(async () => {
2865
+ const issued = result(await rotateServiceToken(button.dataset.rotate ?? ""));
2866
+ showIssuedServiceToken(issued);
2867
+ });
2868
+ });
2869
+ }
2870
+ function showServiceTokenForm() {
2871
+ render(`<button id="back" type="button" class="secondary">Indietro</button><h2>Nuovo token di servizio</h2><form id="token-form"><label>Descrizione ${helpDot("token-label")}<input name="label" required maxlength="100"><span class="form-help">Identifica l'integrazione, ad esempio "IA - produzione".</span></label><label>Scope ${helpDot("token-scopes")}<input name="scopes" placeholder="ia, chat"><span class="form-help">Separati da virgola (lettere, numeri, ':', '_', '-'). Definiscono cosa pu\xF2 fare il token.</span></label><label>Durata in giorni ${helpDot("token-ttl")}<input name="ttlDays" type="number" min="1" step="1"><span class="form-help">Opzionale. Lascia vuoto per un token senza scadenza.</span></label><button>Crea token</button></form>`);
2872
+ byId("back").onclick = () => void show("tokens");
2873
+ const form = document.getElementById("token-form");
2874
+ if (form) form.onsubmit = (event) => {
2875
+ event.preventDefault();
2876
+ handleAction(async () => {
2877
+ const values = formValues(eventForm(event));
2878
+ const scopes = (values.scopes ?? "").split(",").map((scope) => scope.trim()).filter(Boolean);
2879
+ const ttlDays = values.ttlDays ? Number(values.ttlDays) : void 0;
2880
+ const issued = result(await createServiceToken({
2881
+ label: values.label ?? "",
2882
+ scopes: scopes.length ? scopes : void 0,
2883
+ ttlDays: Number.isFinite(ttlDays) ? ttlDays : void 0
2884
+ }));
2885
+ showIssuedServiceToken(issued);
2886
+ });
2887
+ };
2888
+ }
2889
+ function showIssuedServiceToken(issued) {
2890
+ render(`<h2>Token creato</h2><p class="form-help">Copia ora il token: <strong>non sara piu mostrato</strong>. Salvalo come variabile d'ambiente/secret del servizio chiamante e non inserirlo nel body delle richieste.</p><div class="token-reveal"><code id="issued-token">${escapeHtml(issued.token)}</code><button id="copy-token" type="button">Copia</button></div><dl class="entity-metadata"><div><dt>Descrizione</dt><dd>${escapeHtml(issued.label)}</dd></div><div><dt>Scope</dt><dd>${escapeHtml(scopeList(issued.scopes))}</dd></div><div><dt>Scade</dt><dd>${issued.expiresAt ? escapeHtml(new Date(issued.expiresAt).toLocaleString()) : "nessuna"}</dd></div></dl><button id="done-token">Ho copiato, chiudi</button>`);
2891
+ byId("copy-token").onclick = () => handleAction(async () => {
2892
+ await navigator.clipboard.writeText(issued.token);
2893
+ showNotice("Token copiato negli appunti.");
2894
+ });
2895
+ byId("done-token").onclick = () => void show("tokens");
2896
+ }
2897
+ function renderWikiBlock(block) {
2898
+ switch (block.kind) {
2899
+ case "p":
2900
+ return `<p>${escapeHtml(block.text)}</p>`;
2901
+ case "h":
2902
+ return `<h3>${escapeHtml(block.text)}</h3>`;
2903
+ case "list": {
2904
+ const tag = block.ordered ? "ol" : "ul";
2905
+ return `<${tag}>${block.items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</${tag}>`;
2906
+ }
2907
+ case "code": {
2908
+ const variant = block.variant ? ` is-${block.variant}` : "";
2909
+ const title = block.title ? `<span class="wiki-code-title">${escapeHtml(block.title)}</span>` : "";
2910
+ return `<figure class="wiki-code${variant}"><figcaption class="wiki-code-head">${title}<span class="wiki-code-lang">${escapeHtml(block.language)}</span><button type="button" class="wiki-copy" data-wiki-copy aria-label="Copia il codice">Copia</button></figcaption><pre><code>${escapeHtml(block.code)}</code></pre></figure>`;
2911
+ }
2912
+ case "callout":
2913
+ return `<div class="wiki-callout is-${block.tone}">${block.title ? `<strong>${escapeHtml(block.title)}</strong>` : ""}<p>${escapeHtml(block.text)}</p></div>`;
2914
+ case "table":
2915
+ return `<div class="wiki-table-wrap"><table><thead><tr>${block.head.map((cell) => `<th>${escapeHtml(cell)}</th>`).join("")}</tr></thead><tbody>${block.rows.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`;
2916
+ default:
2917
+ return "";
2918
+ }
2919
+ }
2920
+ function bindCopyButtons(root) {
2921
+ root.querySelectorAll("[data-wiki-copy]").forEach((button) => {
2922
+ button.onclick = () => handleAction(async () => {
2923
+ const codeEl = button.closest(".wiki-code")?.querySelector("code");
2924
+ await navigator.clipboard.writeText(codeEl?.textContent ?? "");
2925
+ showNotice("Codice copiato negli appunti.");
2926
+ });
2927
+ });
2928
+ }
2929
+ function helpDot(key) {
2930
+ return `<button type="button" class="help-dot" data-help="${escapeHtml(key)}" aria-label="Aiuto: ${escapeHtml(HELP[key]?.title ?? key)}">i</button>`;
2931
+ }
2932
+ var helpPopover = null;
2933
+ var helpAnchor = null;
2934
+ function ensureHelpPopover() {
2935
+ if (!helpPopover) {
2936
+ helpPopover = document.createElement("div");
2937
+ helpPopover.className = "help-popover";
2938
+ helpPopover.hidden = true;
2939
+ helpPopover.setAttribute("role", "dialog");
2940
+ document.body.appendChild(helpPopover);
2941
+ }
2942
+ return helpPopover;
2943
+ }
2944
+ function closeHelp() {
2945
+ if (helpPopover) helpPopover.hidden = true;
2946
+ helpAnchor = null;
2947
+ }
2948
+ function positionHelp() {
2949
+ if (!helpPopover || !helpAnchor || helpPopover.hidden) return;
2950
+ const rect = helpAnchor.getBoundingClientRect();
2951
+ const width = helpPopover.offsetWidth;
2952
+ const height = helpPopover.offsetHeight;
2953
+ const left = Math.max(8, Math.min(rect.right - width, window.innerWidth - width - 8));
2954
+ let top = rect.bottom + 6;
2955
+ if (top + height > window.innerHeight - 8) top = Math.max(8, rect.top - height - 6);
2956
+ helpPopover.style.left = `${left}px`;
2957
+ helpPopover.style.top = `${top}px`;
2958
+ }
2959
+ function openHelp(anchor) {
2960
+ const entry = HELP[anchor.dataset.help ?? ""];
2961
+ if (!entry) return;
2962
+ const popover = ensureHelpPopover();
2963
+ popover.innerHTML = `<div class="help-popover-head"><h4>${escapeHtml(entry.title)}</h4><button type="button" class="help-close" data-help-close aria-label="Chiudi">&times;</button></div><div class="help-popover-body">${entry.blocks.map(renderWikiBlock).join("")}</div>`;
2964
+ popover.hidden = false;
2965
+ helpAnchor = anchor;
2966
+ bindCopyButtons(popover);
2967
+ positionHelp();
2968
+ }
2969
+ function wikiIndexMarkup(selectedId) {
2970
+ return wikiGroups().map(({ group, sections }) => `<div class="wiki-index-group"><span class="wiki-index-group-title">${escapeHtml(group)}</span><ul>${sections.map((section) => `<li><a href="#wiki-${escapeHtml(section.id)}" data-wiki="${escapeHtml(section.id)}"${section.id === selectedId ? ' aria-current="page"' : ""}>${escapeHtml(section.title)}</a></li>`).join("")}</ul></div>`).join("");
2971
+ }
2972
+ async function showWiki(sectionId) {
2973
+ const hashId = /^#wiki-([a-z0-9-]+)$/.exec(window.location.hash)?.[1];
2974
+ const requested = sectionId ?? hashId;
2975
+ const selected = WIKI_SECTIONS.find((section) => section.id === requested) ?? WIKI_SECTIONS[0];
2976
+ if (!selected) return;
2977
+ render(`<div class="wiki">
2978
+ <aside class="wiki-index">
2979
+ <div class="wiki-index-head"><strong>Indice</strong><button type="button" id="wiki-copy-ai">Copia per AI</button></div>
2980
+ ${wikiIndexMarkup(selected.id)}
2981
+ </aside>
2982
+ <article class="wiki-content">
2983
+ <header class="wiki-section-head"><h2>${escapeHtml(selected.title)}</h2><p>${escapeHtml(selected.summary)}</p></header>
2984
+ ${selected.blocks.map(renderWikiBlock).join("")}
2985
+ </article>
2986
+ </div>`);
2987
+ document.querySelectorAll("[data-wiki]").forEach((link) => {
2988
+ link.onclick = (event) => {
2989
+ event.preventDefault();
2990
+ const id = link.dataset.wiki ?? "";
2991
+ window.history.replaceState(null, "", `${routePath("wiki")}#wiki-${id}`);
2992
+ showWiki(id);
2993
+ };
2994
+ });
2995
+ bindCopyButtons(document);
2996
+ byId("wiki-copy-ai").onclick = () => handleAction(async () => {
2997
+ await navigator.clipboard.writeText(buildAiDigest());
2998
+ showNotice("Documentazione per AI copiata negli appunti.");
2999
+ });
3000
+ }
1043
3001
  async function show(view) {
1044
3002
  try {
1045
3003
  showNotice("");
1046
- const views = { users: showUsers, roles: showRoles, menus: showMenus, filters: showFilters, sso: showSso };
3004
+ const views = {
3005
+ users: showUsers,
3006
+ roles: showRoles,
3007
+ menuGroups: showMenuGroups,
3008
+ menuItems: showMenuItems,
3009
+ menuTypes: showMenuTypes,
3010
+ filterUser: showFilters,
3011
+ filterTypes: showFilterTypes,
3012
+ ssoProviders: showSsoProviders,
3013
+ ssoUsers: showSsoUsers,
3014
+ tokens: showServiceTokens,
3015
+ wiki: showWiki
3016
+ };
1047
3017
  setActiveNavigation(view);
1048
- await views[view]();
3018
+ const renderView = views[view];
3019
+ if (!renderView) throw new Error("Sezione console non valida.");
3020
+ await renderView();
1049
3021
  } catch (error) {
1050
3022
  render("");
1051
3023
  showNotice(error instanceof Error ? error.message : "Operazione non riuscita.", true);
@@ -1060,7 +3032,7 @@ byId("two-factor-form").onsubmit = async (event) => {
1060
3032
  try {
1061
3033
  byId("login-error").textContent = "";
1062
3034
  const values = formValues(event.currentTarget);
1063
- await completeLoginStep(result(await verifyTwoFactor({ challengeId: pendingChallenge, code: values.code })));
3035
+ await completeLoginStep(result(await verifyTwoFactor({ challengeId: pendingChallenge, code: values.code ?? "" })));
1064
3036
  } catch (error) {
1065
3037
  byId("login-error").textContent = error instanceof Error ? error.message : "Verifica non riuscita.";
1066
3038
  }
@@ -1095,11 +3067,35 @@ window.addEventListener("popstate", () => {
1095
3067
  window.addEventListener("accessi-console-network", (event) => {
1096
3068
  updateLoadingState(event.detail.active);
1097
3069
  });
3070
+ document.addEventListener("click", (event) => {
3071
+ const target = event.target;
3072
+ const dot = target.closest("[data-help]");
3073
+ if (dot) {
3074
+ event.preventDefault();
3075
+ openHelp(dot);
3076
+ return;
3077
+ }
3078
+ if (target.closest("[data-help-close]")) {
3079
+ closeHelp();
3080
+ return;
3081
+ }
3082
+ if (helpPopover && !helpPopover.hidden && !target.closest(".help-popover")) closeHelp();
3083
+ });
3084
+ window.addEventListener("keydown", (event) => {
3085
+ if (event.key === "Escape") closeHelp();
3086
+ });
3087
+ window.addEventListener("resize", positionHelp);
3088
+ window.addEventListener("scroll", positionHelp, true);
1098
3089
  var ssoChallenge = /^#two-factor=([a-f0-9]{64})$/.exec(window.location.hash)?.[1];
1099
3090
  if (ssoChallenge) {
1100
3091
  token = null;
1101
3092
  setAccessiConsoleToken(null);
1102
3093
  sessionStorage.removeItem("accessi-console-token");
1103
3094
  window.history.replaceState(null, "", window.location.pathname);
3095
+ finishBoot("login");
1104
3096
  showTwoFactor({ challengeId: ssoChallenge, resendAfterSeconds: 60 });
1105
- } else if (token) void bootstrap();
3097
+ } else if (token) {
3098
+ void bootstrap();
3099
+ } else {
3100
+ finishBoot("login");
3101
+ }