emilsoftware-utilities 2.0.0-dev.23 → 2.0.0-dev.24
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.
- package/README.MD +182 -38
- package/dist/ContractGenerator.d.ts +4 -3
- package/dist/ContractGenerator.js +15 -5
- package/dist/ContractGenerator.js.map +1 -1
- package/dist/DailyFileTransport.d.ts +8 -2
- package/dist/DailyFileTransport.js +9 -2
- package/dist/DailyFileTransport.js.map +1 -1
- package/dist/DatabaseUpdater.js +32 -27
- package/dist/DatabaseUpdater.js.map +1 -1
- package/dist/EmilsoftwareModule.js.map +1 -1
- package/dist/ExecutionTimeLogger.d.ts +2 -2
- package/dist/ExecutionTimeLogger.js +3 -2
- package/dist/ExecutionTimeLogger.js.map +1 -1
- package/dist/Logger.d.ts +22 -9
- package/dist/Logger.js +131 -54
- package/dist/Logger.js.map +1 -1
- package/dist/Orm.d.ts +26 -6
- package/dist/Orm.js +144 -40
- package/dist/Orm.js.map +1 -1
- package/dist/RoutesLoggerMiddleware.js.map +1 -1
- package/dist/Utilities.d.ts +12 -7
- package/dist/Utilities.js +57 -22
- package/dist/Utilities.js.map +1 -1
- package/dist/accessi-module/AccessiModule.d.ts +9 -0
- package/dist/accessi-module/AccessiModule.js +12 -4
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Console/console.css +529 -281
- package/dist/accessi-module/Console/console.js +2081 -85
- package/dist/accessi-module/Console/console.js.map +1 -1
- package/dist/accessi-module/Console/generated/accessiApi.d.ts +167 -1
- package/dist/accessi-module/Console/generated/accessiApi.js +156 -2
- package/dist/accessi-module/Console/generated/accessiApi.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/abilitazioneMenu.d.ts +20 -8
- package/dist/accessi-module/Console/generated/model/abilitazioneMenu.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuIcona.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuIcona.js +9 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuIcona.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuNote.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuNote.js +9 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuNote.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuPagina.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuPagina.js +9 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuPagina.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuTipo.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuTipo.js +9 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuTipo.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/createFilterTypeRequest.d.ts +23 -0
- package/dist/accessi-module/Console/generated/model/createFilterTypeRequest.js +3 -0
- package/dist/accessi-module/Console/generated/model/createFilterTypeRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/createFilterTypeRequestFlgEnabled.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/createFilterTypeRequestFlgEnabled.js +15 -0
- package/dist/accessi-module/Console/generated/model/createFilterTypeRequestFlgEnabled.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/createMenuGroupRequest.d.ts +27 -0
- package/dist/accessi-module/Console/generated/model/createMenuGroupRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/createMenuGroupRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/createMenuRequest.d.ts +52 -0
- package/dist/accessi-module/Console/generated/model/createMenuRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/createMenuRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/createMenuTypeRequest.d.ts +18 -0
- package/dist/accessi-module/Console/generated/model/createMenuTypeRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/createMenuTypeRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/createServiceTokenRequest.d.ts +16 -0
- package/dist/accessi-module/Console/generated/model/createServiceTokenRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/createServiceTokenRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/createServiceTokenResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/createServiceTokenResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/createServiceTokenResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getMenuTypesResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/getMenuTypesResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/getMenuTypesResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getServiceTokensParams.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/getServiceTokensParams.js +9 -0
- package/dist/accessi-module/Console/generated/model/getServiceTokensParams.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getServiceTokensResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/getServiceTokensResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/getServiceTokensResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getUsersParams.d.ts +8 -0
- package/dist/accessi-module/Console/generated/model/getUsersParams.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/index.d.ts +18 -0
- package/dist/accessi-module/Console/generated/model/index.js +18 -0
- package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/issuedServiceTokenDto.d.ts +30 -0
- package/dist/accessi-module/Console/generated/model/issuedServiceTokenDto.js +9 -0
- package/dist/accessi-module/Console/generated/model/issuedServiceTokenDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/menuTypeEntity.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/menuTypeEntity.js +9 -0
- package/dist/accessi-module/Console/generated/model/menuTypeEntity.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/publicRegisterRequestCellulare.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/publicRegisterRequestCellulare.js +9 -0
- package/dist/accessi-module/Console/generated/model/publicRegisterRequestCellulare.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/registerRequestCellulare.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/registerRequestCellulare.js +9 -0
- package/dist/accessi-module/Console/generated/model/registerRequestCellulare.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/serviceTokenDto.d.ts +28 -0
- package/dist/accessi-module/Console/generated/model/serviceTokenDto.js +9 -0
- package/dist/accessi-module/Console/generated/model/serviceTokenDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/updateFilterTypeRequest.d.ts +21 -0
- package/dist/accessi-module/Console/generated/model/updateFilterTypeRequest.js +3 -0
- package/dist/accessi-module/Console/generated/model/updateFilterTypeRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/updateFilterTypeRequestFlgEnabled.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/updateFilterTypeRequestFlgEnabled.js +15 -0
- package/dist/accessi-module/Console/generated/model/updateFilterTypeRequestFlgEnabled.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/updateMenuGroupRequest.d.ts +22 -0
- package/dist/accessi-module/Console/generated/model/updateMenuGroupRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/updateMenuGroupRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/updateMenuRequest.d.ts +47 -0
- package/dist/accessi-module/Console/generated/model/updateMenuRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/updateMenuRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/updateMenuTypeRequest.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/updateMenuTypeRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/updateMenuTypeRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/userDtoAvatar.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/userDtoAvatar.js +9 -0
- package/dist/accessi-module/Console/generated/model/userDtoAvatar.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/userDtoCellulare.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/userDtoCellulare.js +9 -0
- package/dist/accessi-module/Console/generated/model/userDtoCellulare.js.map +1 -0
- package/dist/accessi-module/Console/help.d.ts +13 -0
- package/dist/accessi-module/Console/help.js +348 -0
- package/dist/accessi-module/Console/help.js.map +1 -0
- package/dist/accessi-module/Console/index.html +89 -21
- package/dist/accessi-module/Console/openApiClient.d.ts +34 -0
- package/dist/accessi-module/Console/openApiClient.js +111 -0
- package/dist/accessi-module/Console/openApiClient.js.map +1 -0
- package/dist/accessi-module/Console/wiki.d.ts +52 -0
- package/dist/accessi-module/Console/wiki.js +689 -0
- package/dist/accessi-module/Console/wiki.js.map +1 -0
- package/dist/accessi-module/Controllers/AccessiConsoleController.d.ts +8 -1
- package/dist/accessi-module/Controllers/AccessiConsoleController.js +52 -15
- package/dist/accessi-module/Controllers/AccessiConsoleController.js.map +1 -1
- package/dist/accessi-module/Controllers/ConfiguratorController.d.ts +1 -3
- package/dist/accessi-module/Controllers/ConfiguratorController.js +6 -8
- package/dist/accessi-module/Controllers/ConfiguratorController.js.map +1 -1
- package/dist/accessi-module/Controllers/EmailController.d.ts +1 -1
- package/dist/accessi-module/Controllers/EmailController.js +1 -1
- package/dist/accessi-module/Controllers/EmailController.js.map +1 -1
- package/dist/accessi-module/Controllers/FiltriController.d.ts +5 -4
- package/dist/accessi-module/Controllers/FiltriController.js +92 -4
- package/dist/accessi-module/Controllers/FiltriController.js.map +1 -1
- package/dist/accessi-module/Controllers/PermissionController.d.ts +11 -0
- package/dist/accessi-module/Controllers/PermissionController.js +273 -8
- package/dist/accessi-module/Controllers/PermissionController.js.map +1 -1
- package/dist/accessi-module/Controllers/ServiceTokenController.d.ts +23 -0
- package/dist/accessi-module/Controllers/ServiceTokenController.js +176 -0
- package/dist/accessi-module/Controllers/ServiceTokenController.js.map +1 -0
- package/dist/accessi-module/Controllers/UserController.d.ts +2 -2
- package/dist/accessi-module/Controllers/UserController.js +25 -9
- package/dist/accessi-module/Controllers/UserController.js.map +1 -1
- package/dist/accessi-module/Dtos/Abilitazione.js.map +1 -1
- package/dist/accessi-module/Dtos/AbilitazioneMenu.js +16 -8
- package/dist/accessi-module/Dtos/AbilitazioneMenu.js.map +1 -1
- package/dist/accessi-module/Dtos/AssignPermissionsToUserRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/AssignRolesToUserRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/AuthenticatedUserPayloadDto.js.map +1 -1
- package/dist/accessi-module/Dtos/BaseResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/CatalogDtos.d.ts +66 -0
- package/dist/accessi-module/Dtos/CatalogDtos.js +336 -0
- package/dist/accessi-module/Dtos/CatalogDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/ConfirmResetPasswordRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/FiltriUtente.js.map +1 -1
- package/dist/accessi-module/Dtos/GetGroupsWithMenusResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/GetMenusResponse.js +1 -1
- package/dist/accessi-module/Dtos/GetMenusResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/GetRolesResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/GetUserByTokenRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/GetUserByTokenResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/GetUsersResponse.d.ts +1 -1
- package/dist/accessi-module/Dtos/GetUsersResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/LoginRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/LoginResponse.d.ts +1 -1
- package/dist/accessi-module/Dtos/LoginResponse.js +1 -1
- package/dist/accessi-module/Dtos/LoginResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/MenuItem.js +1 -0
- package/dist/accessi-module/Dtos/Permission.js.map +1 -1
- package/dist/accessi-module/Dtos/RegisterRequest.js +2 -1
- package/dist/accessi-module/Dtos/RegisterRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/RegisterResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/Role.js.map +1 -1
- package/dist/accessi-module/Dtos/RuoliMenu.js.map +1 -1
- package/dist/accessi-module/Dtos/SendResetPasswordEmailRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/ServiceToken.d.ts +34 -0
- package/dist/accessi-module/Dtos/ServiceToken.js +128 -0
- package/dist/accessi-module/Dtos/ServiceToken.js.map +1 -0
- package/dist/accessi-module/Dtos/SetStatoRegistrazione.js.map +1 -1
- package/dist/accessi-module/Dtos/StatoRegistrazione.js.map +1 -1
- package/dist/accessi-module/Dtos/TipoAbilitazione.js.map +1 -1
- package/dist/accessi-module/Dtos/TipoFiltro.js.map +1 -1
- package/dist/accessi-module/Dtos/TokenResult.js.map +1 -1
- package/dist/accessi-module/Dtos/TwoFactorDtos.js.map +1 -1
- package/dist/accessi-module/Dtos/UpdateEnabledStatusRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/UserDto.js +4 -2
- package/dist/accessi-module/Dtos/UserDto.js.map +1 -1
- package/dist/accessi-module/Dtos/UserGrantsDto.js.map +1 -1
- package/dist/accessi-module/Dtos/UserGrantsResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/index.d.ts +2 -0
- package/dist/accessi-module/Dtos/index.js +2 -0
- package/dist/accessi-module/Dtos/index.js.map +1 -1
- package/dist/accessi-module/Services/AuthService/AuthService.d.ts +2 -2
- package/dist/accessi-module/Services/AuthService/AuthService.js +36 -36
- package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
- package/dist/accessi-module/Services/ConfiguratorService/ConfiguratorService.js.map +1 -1
- package/dist/accessi-module/Services/EmailService/EmailService.d.ts +2 -1
- package/dist/accessi-module/Services/EmailService/EmailService.js +20 -17
- package/dist/accessi-module/Services/EmailService/EmailService.js.map +1 -1
- package/dist/accessi-module/Services/FiltriService/FiltriService.d.ts +10 -3
- package/dist/accessi-module/Services/FiltriService/FiltriService.js +64 -6
- package/dist/accessi-module/Services/FiltriService/FiltriService.js.map +1 -1
- package/dist/accessi-module/Services/PermissionService/PermissionService.d.ts +44 -1
- package/dist/accessi-module/Services/PermissionService/PermissionService.js +508 -200
- package/dist/accessi-module/Services/PermissionService/PermissionService.js.map +1 -1
- package/dist/accessi-module/Services/ServiceTokenService/ServiceTokenService.d.ts +75 -0
- package/dist/accessi-module/Services/ServiceTokenService/ServiceTokenService.js +265 -0
- package/dist/accessi-module/Services/ServiceTokenService/ServiceTokenService.js.map +1 -0
- package/dist/accessi-module/Services/TwoFactorService/TwoFactorService.js +23 -27
- package/dist/accessi-module/Services/TwoFactorService/TwoFactorService.js.map +1 -1
- package/dist/accessi-module/Services/UserService/UserService.d.ts +10 -3
- package/dist/accessi-module/Services/UserService/UserService.js +241 -155
- package/dist/accessi-module/Services/UserService/UserService.js.map +1 -1
- package/dist/accessi-module/Views/reset-password.html +213 -213
- package/dist/accessi-module/database-updates/AccessiDatabaseUpdater.d.ts +8 -0
- package/dist/accessi-module/database-updates/AccessiDatabaseUpdater.js +89 -55
- package/dist/accessi-module/database-updates/AccessiDatabaseUpdater.js.map +1 -1
- package/dist/accessi-module/database-updates/accessiSchema.d.ts +1 -1
- package/dist/accessi-module/database-updates/accessiSchema.js +9 -1
- package/dist/accessi-module/database-updates/accessiSchema.js.map +1 -1
- package/dist/accessi-module/database-updates/optionalColumns.d.ts +2 -0
- package/dist/accessi-module/database-updates/optionalColumns.js +33 -1
- package/dist/accessi-module/database-updates/optionalColumns.js.map +1 -1
- package/dist/accessi-module/database-updates/runAccessiDbUpdate.js +7 -2
- package/dist/accessi-module/database-updates/runAccessiDbUpdate.js.map +1 -1
- package/dist/accessi-module/federated-auth/FederatedAuthController.d.ts +1 -0
- package/dist/accessi-module/federated-auth/FederatedAuthController.js +26 -0
- package/dist/accessi-module/federated-auth/FederatedAuthController.js.map +1 -1
- package/dist/accessi-module/federated-auth/FederatedAuthService.d.ts +6 -0
- package/dist/accessi-module/federated-auth/FederatedAuthService.js +28 -1
- package/dist/accessi-module/federated-auth/FederatedAuthService.js.map +1 -1
- package/dist/accessi-module/index.d.ts +4 -1
- package/dist/accessi-module/index.js +8 -2
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/accessi-module/jwt/jwt.strategy.js +8 -5
- package/dist/accessi-module/jwt/jwt.strategy.js.map +1 -1
- package/dist/accessi-module/middleware/accessiRequirements.d.ts +1 -1
- package/dist/accessi-module/middleware/accessiRequirements.js.map +1 -1
- package/dist/accessi-module/middleware/authenticateGen.js +11 -7
- package/dist/accessi-module/middleware/authenticateGen.js.map +1 -1
- package/dist/accessi-module/security/accessControl.js +8 -3
- package/dist/accessi-module/security/accessControl.js.map +1 -1
- package/dist/accessi-module/security/authenticatedToken.d.ts +1 -1
- package/dist/accessi-module/security/authenticatedToken.js +2 -1
- package/dist/accessi-module/security/authenticatedToken.js.map +1 -1
- package/dist/accessi-module/security/passwordResetToken.d.ts +9 -3
- package/dist/accessi-module/security/passwordResetToken.js +9 -5
- package/dist/accessi-module/security/passwordResetToken.js.map +1 -1
- package/dist/accessi-module/security/publicAuthRateLimit.d.ts +1 -1
- package/dist/accessi-module/security/publicAuthRateLimit.js +28 -8
- package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
- package/dist/accessi-module/security/serviceTokenGuard.d.ts +25 -0
- package/dist/accessi-module/security/serviceTokenGuard.js +85 -0
- package/dist/accessi-module/security/serviceTokenGuard.js.map +1 -0
- package/dist/accessi-module/security/serviceTokenRateLimit.d.ts +8 -0
- package/dist/accessi-module/security/serviceTokenRateLimit.js +49 -0
- package/dist/accessi-module/security/serviceTokenRateLimit.js.map +1 -0
- package/dist/allegati-module/AllegatiModule.d.ts +11 -0
- package/dist/allegati-module/AllegatiModule.js +11 -1
- package/dist/allegati-module/AllegatiModule.js.map +1 -1
- package/dist/allegati-module/Controllers/AllegatiController.d.ts +2 -1
- package/dist/allegati-module/Controllers/AllegatiController.js +23 -15
- package/dist/allegati-module/Controllers/AllegatiController.js.map +1 -1
- package/dist/allegati-module/Dtos/AllegatoDto.js.map +1 -1
- package/dist/allegati-module/Dtos/BaseResponse.js.map +1 -1
- package/dist/allegati-module/Dtos/UploadSingleFileRequest.js.map +1 -1
- package/dist/allegati-module/Dtos/index.js.map +1 -1
- package/dist/allegati-module/Dtos/responses/DownloadAllegatoResponseDto.js.map +1 -1
- package/dist/allegati-module/Dtos/responses/GetListResponse.js.map +1 -1
- package/dist/allegati-module/Dtos/responses/UploadAllegatoResponseDto.js.map +1 -1
- package/dist/allegati-module/Services/AllegatiService/AllegatiService.d.ts +10 -3
- package/dist/allegati-module/Services/AllegatiService/AllegatiService.js +150 -101
- package/dist/allegati-module/Services/AllegatiService/AllegatiService.js.map +1 -1
- package/dist/allegati-module/index.js.map +1 -1
- package/dist/allegati-module/security/allegatiAuthorizationGuard.d.ts +14 -0
- package/dist/allegati-module/security/allegatiAuthorizationGuard.js +70 -0
- package/dist/allegati-module/security/allegatiAuthorizationGuard.js.map +1 -0
- package/dist/autobind.d.ts +10 -3
- package/dist/autobind.js +24 -9
- package/dist/autobind.js.map +1 -1
- package/dist/firebird-compat.d.ts +13 -0
- package/dist/firebird-compat.js +3 -2
- package/dist/firebird-compat.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/initEmilsoftwareModule.js.map +1 -1
- package/dist/scripts/generateAccessiWiki.d.ts +1 -0
- package/dist/scripts/generateAccessiWiki.js +131 -0
- package/dist/scripts/generateAccessiWiki.js.map +1 -0
- package/dist/swagger/SwaggerConfig.js +6 -3
- package/dist/swagger/SwaggerConfig.js.map +1 -1
- package/package.json +103 -92
- 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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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">
|
|
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">
|
|
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
|
|
518
|
-
const
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
return `<article class="
|
|
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)} · ${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
|
-
|
|
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 `<
|
|
531
|
-
}).join("")
|
|
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
|
-
|
|
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">
|
|
612
|
-
<label>ID utente del provider (subject)<input name="subject" required><span class="form-help">
|
|
613
|
-
<label>Nota<input name="note"><span class="form-help">
|
|
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">
|
|
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">🗑</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">…</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">‹</button>` + numbers + `<button type="button" class="pager-item pager-nav" data-goto="next"${usersPage >= totalPages - 1 ? " disabled" : ""} aria-label="Pagina successiva">›</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">—</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
|
-
|
|
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
|
|
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
|
-
<
|
|
755
|
-
|
|
756
|
-
</
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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
|
|
793
|
-
byId("back").onclick = () => show("
|
|
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("
|
|
2323
|
+
await show("ssoUsers");
|
|
810
2324
|
});
|
|
811
2325
|
};
|
|
812
2326
|
}
|
|
813
|
-
async function
|
|
2327
|
+
async function showSsoProviders() {
|
|
814
2328
|
if (!federatedAuthenticationAvailable) throw new Error("SSO non \xE8 abilitato in questa istanza Accessi.");
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
|
830
|
-
byId("back").onclick = () => show("
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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]
|
|
889
|
-
|
|
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
|
|
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
|
|
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
|
|
992
|
-
const
|
|
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
|
-
|
|
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(`<
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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-
|
|
1020
|
-
|
|
1021
|
-
|
|
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">
|
|
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
|
|
2777
|
+
const code3 = Number(formValues(form).codUte);
|
|
1033
2778
|
if (action === "load") {
|
|
1034
|
-
const filters = result(await getFiltriUtente({ codUte:
|
|
1035
|
-
form.elements.namedItem("json").value = JSON.stringify(filters[0] ?? { codUte:
|
|
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:
|
|
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 <token></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">×</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 = {
|
|
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
|
-
|
|
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)
|
|
3097
|
+
} else if (token) {
|
|
3098
|
+
void bootstrap();
|
|
3099
|
+
} else {
|
|
3100
|
+
finishBoot("login");
|
|
3101
|
+
}
|