emilsoftware-utilities 2.0.0-dev.4 → 2.0.0-dev.40
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 +199 -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 +21 -0
- package/dist/DailyFileTransport.js +74 -0
- package/dist/DailyFileTransport.js.map +1 -0
- package/dist/DatabaseUpdater.d.ts +1 -1
- package/dist/DatabaseUpdater.js +32 -29
- 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 +5 -4
- package/dist/ExecutionTimeLogger.js.map +1 -1
- package/dist/Logger.d.ts +22 -11
- package/dist/Logger.js +132 -119
- package/dist/Logger.js.map +1 -1
- package/dist/Orm.d.ts +28 -12
- package/dist/Orm.js +194 -153
- package/dist/Orm.js.map +1 -1
- package/dist/RoutesLoggerMiddleware.js.map +1 -1
- package/dist/Utilities.d.ts +18 -8
- package/dist/Utilities.js +117 -38
- package/dist/Utilities.js.map +1 -1
- package/dist/accessi-module/AccessiModule.d.ts +116 -2
- package/dist/accessi-module/AccessiModule.js +36 -5
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Console/accessiFetch.d.ts +7 -0
- package/dist/accessi-module/Console/accessiFetch.js +59 -0
- package/dist/accessi-module/Console/accessiFetch.js.map +1 -0
- package/dist/accessi-module/Console/console.css +676 -0
- package/dist/accessi-module/Console/console.d.ts +1 -0
- package/dist/accessi-module/Console/console.js +3351 -0
- package/dist/accessi-module/Console/console.js.map +1 -0
- package/dist/accessi-module/Console/generated/accessiApi.d.ts +582 -0
- package/dist/accessi-module/Console/generated/accessiApi.js +549 -0
- package/dist/accessi-module/Console/generated/accessiApi.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenu.d.ts +39 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenu.js +3 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenu.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuTipoAbilitazione.d.ts +16 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuTipoAbilitazione.js +17 -0
- package/dist/accessi-module/Console/generated/model/abilitazioneMenuTipoAbilitazione.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/actionResponse.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/actionResponse.js +9 -0
- package/dist/accessi-module/Console/generated/model/actionResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/assignPermissionsToUserRequest.d.ts +11 -0
- package/dist/accessi-module/Console/generated/model/assignPermissionsToUserRequest.js +3 -0
- package/dist/accessi-module/Console/generated/model/assignPermissionsToUserRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/assignRolesToUserRequest.d.ts +10 -0
- package/dist/accessi-module/Console/generated/model/assignRolesToUserRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/assignRolesToUserRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/authenticatedTokenPayloadDto.d.ts +10 -0
- package/dist/accessi-module/Console/generated/model/authenticatedTokenPayloadDto.js +3 -0
- package/dist/accessi-module/Console/generated/model/authenticatedTokenPayloadDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/authenticatedUserPayloadDto.d.ts +19 -0
- package/dist/accessi-module/Console/generated/model/authenticatedUserPayloadDto.js +9 -0
- package/dist/accessi-module/Console/generated/model/authenticatedUserPayloadDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/bootstrapAdminRequest.d.ts +17 -0
- package/dist/accessi-module/Console/generated/model/bootstrapAdminRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/bootstrapAdminRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/bootstrapAdminResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/bootstrapAdminResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/bootstrapAdminResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/bootstrapAdminResultDto.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/bootstrapAdminResultDto.js +9 -0
- package/dist/accessi-module/Console/generated/model/bootstrapAdminResultDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/confirmResetPasswordRequest.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/confirmResetPasswordRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/confirmResetPasswordRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/createFederatedIdentityRequest.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/createFederatedIdentityRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/createFederatedIdentityRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/createFederatedProviderRequest.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/createFederatedProviderRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/createFederatedProviderRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/createFederatedUserRequest.d.ts +18 -0
- package/dist/accessi-module/Console/generated/model/createFederatedUserRequest.js +3 -0
- package/dist/accessi-module/Console/generated/model/createFederatedUserRequest.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 +22 -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/errorResponse.d.ts +18 -0
- package/dist/accessi-module/Console/generated/model/errorResponse.js +9 -0
- package/dist/accessi-module/Console/generated/model/errorResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/federatedIdentityListResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/federatedIdentityListResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/federatedIdentityListResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/federatedIdentityResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/federatedIdentityResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/federatedIdentityResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/federatedIdentityResponseDto.d.ts +23 -0
- package/dist/accessi-module/Console/generated/model/federatedIdentityResponseDto.js +9 -0
- package/dist/accessi-module/Console/generated/model/federatedIdentityResponseDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/federatedProviderListResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/federatedProviderListResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/federatedProviderListResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/federatedProviderResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/federatedProviderResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/federatedProviderResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/federatedProviderResponseDto.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/federatedProviderResponseDto.js +9 -0
- package/dist/accessi-module/Console/generated/model/federatedProviderResponseDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/filtriUtente.d.ts +32 -0
- package/dist/accessi-module/Console/generated/model/filtriUtente.js +9 -0
- package/dist/accessi-module/Console/generated/model/filtriUtente.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getFiltriResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/getFiltriResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/getFiltriResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getFiltriUtenteParams.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/getFiltriUtenteParams.js +9 -0
- package/dist/accessi-module/Console/generated/model/getFiltriUtenteParams.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getFiltriUtenteResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/getFiltriUtenteResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/getFiltriUtenteResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getGroupsWithMenusParams.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/getGroupsWithMenusParams.js +9 -0
- package/dist/accessi-module/Console/generated/model/getGroupsWithMenusParams.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getGroupsWithMenusResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/getGroupsWithMenusResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/getGroupsWithMenusResponse.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/getMenusResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/getMenusResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/getMenusResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getRolesResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/getRolesResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/getRolesResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getSchemaResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/getSchemaResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/getSchemaResponse.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/getUserByTokenRequest.d.ts +10 -0
- package/dist/accessi-module/Console/generated/model/getUserByTokenRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/getUserByTokenRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getUserByTokenResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/getUserByTokenResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/getUserByTokenResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getUsersParams.d.ts +32 -0
- package/dist/accessi-module/Console/generated/model/getUsersParams.js +9 -0
- package/dist/accessi-module/Console/generated/model/getUsersParams.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getUsersResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/getUsersResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/getUsersResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getUsersResult.d.ts +17 -0
- package/dist/accessi-module/Console/generated/model/getUsersResult.js +3 -0
- package/dist/accessi-module/Console/generated/model/getUsersResult.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/getUsersResultExtensionFields.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/getUsersResultExtensionFields.js +9 -0
- package/dist/accessi-module/Console/generated/model/getUsersResultExtensionFields.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/groupWithMenusEntity.d.ts +19 -0
- package/dist/accessi-module/Console/generated/model/groupWithMenusEntity.js +3 -0
- package/dist/accessi-module/Console/generated/model/groupWithMenusEntity.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/index.d.ts +99 -0
- package/dist/accessi-module/Console/generated/model/index.js +116 -0
- package/dist/accessi-module/Console/generated/model/index.js.map +1 -0
- 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/loginRequest.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/loginRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/loginRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/loginResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/loginResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/loginResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/loginResult.d.ts +32 -0
- package/dist/accessi-module/Console/generated/model/loginResult.js +3 -0
- package/dist/accessi-module/Console/generated/model/loginResult.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/loginResultExtensionFields.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/loginResultExtensionFields.js +9 -0
- package/dist/accessi-module/Console/generated/model/loginResultExtensionFields.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/loginResultPasswordRequired.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/loginResultPasswordRequired.js +14 -0
- package/dist/accessi-module/Console/generated/model/loginResultPasswordRequired.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/menuEntity.d.ts +32 -0
- package/dist/accessi-module/Console/generated/model/menuEntity.js +9 -0
- package/dist/accessi-module/Console/generated/model/menuEntity.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/passwordExpiredResponse.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/passwordExpiredResponse.js +9 -0
- package/dist/accessi-module/Console/generated/model/passwordExpiredResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/permission.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/permission.js +9 -0
- package/dist/accessi-module/Console/generated/model/permission.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/publicRegisterRequest.d.ts +51 -0
- package/dist/accessi-module/Console/generated/model/publicRegisterRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/publicRegisterRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/registerRequest.d.ts +62 -0
- package/dist/accessi-module/Console/generated/model/registerRequest.js +3 -0
- package/dist/accessi-module/Console/generated/model/registerRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/registerResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/registerResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/registerResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/resendTwoFactorRequest.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/resendTwoFactorRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/resendTwoFactorRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/role.d.ts +15 -0
- package/dist/accessi-module/Console/generated/model/role.js +3 -0
- package/dist/accessi-module/Console/generated/model/role.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/ruoliMenu.d.ts +15 -0
- package/dist/accessi-module/Console/generated/model/ruoliMenu.js +3 -0
- package/dist/accessi-module/Console/generated/model/ruoliMenu.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/ruoliMenuTipoAbilitazione.d.ts +16 -0
- package/dist/accessi-module/Console/generated/model/ruoliMenuTipoAbilitazione.js +17 -0
- package/dist/accessi-module/Console/generated/model/ruoliMenuTipoAbilitazione.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/schemaDdlResult.d.ts +20 -0
- package/dist/accessi-module/Console/generated/model/schemaDdlResult.js +3 -0
- package/dist/accessi-module/Console/generated/model/schemaDdlResult.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/schemaDdlSection.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/schemaDdlSection.js +9 -0
- package/dist/accessi-module/Console/generated/model/schemaDdlSection.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/sendResetPasswordEmailRequest.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/sendResetPasswordEmailRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/sendResetPasswordEmailRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/serveResetPasswordPageParams.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/serveResetPasswordPageParams.js +9 -0
- package/dist/accessi-module/Console/generated/model/serveResetPasswordPageParams.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/setPasswordLoginPolicyRequest.d.ts +10 -0
- package/dist/accessi-module/Console/generated/model/setPasswordLoginPolicyRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/setPasswordLoginPolicyRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/setStatoRegistrazioneDto.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/setStatoRegistrazioneDto.js +3 -0
- package/dist/accessi-module/Console/generated/model/setStatoRegistrazioneDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/setStatoRegistrazioneDtoStatoRegistrazione.d.ts +18 -0
- package/dist/accessi-module/Console/generated/model/setStatoRegistrazioneDtoStatoRegistrazione.js +19 -0
- package/dist/accessi-module/Console/generated/model/setStatoRegistrazioneDtoStatoRegistrazione.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/status.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/status.js +9 -0
- package/dist/accessi-module/Console/generated/model/status.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/tipoFiltro.d.ts +23 -0
- package/dist/accessi-module/Console/generated/model/tipoFiltro.js +3 -0
- package/dist/accessi-module/Console/generated/model/tipoFiltro.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/tipoFiltroFlgEnabled.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/tipoFiltroFlgEnabled.js +15 -0
- package/dist/accessi-module/Console/generated/model/tipoFiltroFlgEnabled.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/tokenResult.d.ts +15 -0
- package/dist/accessi-module/Console/generated/model/tokenResult.js +3 -0
- package/dist/accessi-module/Console/generated/model/tokenResult.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/tokenResultType.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/tokenResultType.js +15 -0
- package/dist/accessi-module/Console/generated/model/tokenResultType.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/twoFactorChallengeDto.d.ts +18 -0
- package/dist/accessi-module/Console/generated/model/twoFactorChallengeDto.js +3 -0
- package/dist/accessi-module/Console/generated/model/twoFactorChallengeDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/twoFactorChallengeDtoMethod.d.ts +10 -0
- package/dist/accessi-module/Console/generated/model/twoFactorChallengeDtoMethod.js +14 -0
- package/dist/accessi-module/Console/generated/model/twoFactorChallengeDtoMethod.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/twoFactorChallengeDtoTwoFactorRequired.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/twoFactorChallengeDtoTwoFactorRequired.js +14 -0
- package/dist/accessi-module/Console/generated/model/twoFactorChallengeDtoTwoFactorRequired.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/updateEnabledStatusRequest.d.ts +10 -0
- package/dist/accessi-module/Console/generated/model/updateEnabledStatusRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/updateEnabledStatusRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/updateFederatedIdentityRequest.d.ts +11 -0
- package/dist/accessi-module/Console/generated/model/updateFederatedIdentityRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/updateFederatedIdentityRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/updateFederatedProviderRequest.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/updateFederatedProviderRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/updateFederatedProviderRequest.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 +17 -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/userDto.d.ts +88 -0
- package/dist/accessi-module/Console/generated/model/userDto.js +3 -0
- package/dist/accessi-module/Console/generated/model/userDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/userDtoStatoRegistrazione.d.ts +18 -0
- package/dist/accessi-module/Console/generated/model/userDtoStatoRegistrazione.js +19 -0
- package/dist/accessi-module/Console/generated/model/userDtoStatoRegistrazione.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/userGrantsDto.d.ts +16 -0
- package/dist/accessi-module/Console/generated/model/userGrantsDto.js +3 -0
- package/dist/accessi-module/Console/generated/model/userGrantsDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/userGrantsResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/userGrantsResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/userGrantsResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/verifyTwoFactorRequest.d.ts +18 -0
- package/dist/accessi-module/Console/generated/model/verifyTwoFactorRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/verifyTwoFactorRequest.js.map +1 -0
- package/dist/accessi-module/Console/help.d.ts +13 -0
- package/dist/accessi-module/Console/help.js +355 -0
- package/dist/accessi-module/Console/help.js.map +1 -0
- package/dist/accessi-module/Console/index.html +153 -0
- package/dist/accessi-module/Console/wiki.d.ts +52 -0
- package/dist/accessi-module/Console/wiki.js +730 -0
- package/dist/accessi-module/Console/wiki.js.map +1 -0
- package/dist/accessi-module/Controllers/AccessiConsoleController.d.ts +18 -0
- package/dist/accessi-module/Controllers/AccessiConsoleController.js +111 -0
- package/dist/accessi-module/Controllers/AccessiConsoleController.js.map +1 -0
- package/dist/accessi-module/Controllers/AuthController.d.ts +12 -6
- package/dist/accessi-module/Controllers/AuthController.js +167 -82
- package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
- package/dist/accessi-module/Controllers/ConfiguratorController.d.ts +1 -5
- package/dist/accessi-module/Controllers/ConfiguratorController.js +26 -44
- package/dist/accessi-module/Controllers/ConfiguratorController.js.map +1 -1
- package/dist/accessi-module/Controllers/EmailController.d.ts +4 -6
- package/dist/accessi-module/Controllers/EmailController.js +12 -14
- 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 +125 -6
- package/dist/accessi-module/Controllers/FiltriController.js.map +1 -1
- package/dist/accessi-module/Controllers/PermissionController.d.ts +12 -1
- package/dist/accessi-module/Controllers/PermissionController.js +322 -51
- package/dist/accessi-module/Controllers/PermissionController.js.map +1 -1
- package/dist/accessi-module/Controllers/SchemaController.d.ts +9 -0
- package/dist/accessi-module/Controllers/SchemaController.js +79 -0
- package/dist/accessi-module/Controllers/SchemaController.js.map +1 -0
- 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 +8 -4
- package/dist/accessi-module/Controllers/UserController.js +161 -28
- 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/AdminBootstrapDtos.d.ts +19 -0
- package/dist/accessi-module/Dtos/AdminBootstrapDtos.js +83 -0
- package/dist/accessi-module/Dtos/AdminBootstrapDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/AssignPermissionsToUserRequest.js +0 -1
- package/dist/accessi-module/Dtos/AssignPermissionsToUserRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/AssignRolesToUserRequest.js +1 -2
- package/dist/accessi-module/Dtos/AssignRolesToUserRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/AuthenticatedUserPayloadDto.d.ts +13 -0
- package/dist/accessi-module/Dtos/AuthenticatedUserPayloadDto.js +70 -0
- package/dist/accessi-module/Dtos/AuthenticatedUserPayloadDto.js.map +1 -0
- package/dist/accessi-module/Dtos/BaseResponse.d.ts +22 -0
- package/dist/accessi-module/Dtos/BaseResponse.js +106 -1
- package/dist/accessi-module/Dtos/BaseResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/CatalogDtos.d.ts +64 -0
- package/dist/accessi-module/Dtos/CatalogDtos.js +322 -0
- package/dist/accessi-module/Dtos/CatalogDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/ConfirmResetPasswordRequest.d.ts +3 -0
- package/dist/accessi-module/Dtos/ConfirmResetPasswordRequest.js +31 -0
- package/dist/accessi-module/Dtos/ConfirmResetPasswordRequest.js.map +1 -0
- package/dist/accessi-module/Dtos/FederatedIdentityDtos.d.ts +69 -0
- package/dist/accessi-module/Dtos/FederatedIdentityDtos.js +236 -0
- package/dist/accessi-module/Dtos/FederatedIdentityDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/FiltriUtente.js +5 -1
- package/dist/accessi-module/Dtos/FiltriUtente.js.map +1 -1
- package/dist/accessi-module/Dtos/GetGroupsWithMenusResponse.js +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.d.ts +3 -0
- package/dist/accessi-module/Dtos/GetUserByTokenRequest.js +26 -0
- package/dist/accessi-module/Dtos/GetUserByTokenRequest.js.map +1 -0
- package/dist/accessi-module/Dtos/GetUserByTokenResponse.d.ts +5 -0
- package/dist/accessi-module/Dtos/GetUserByTokenResponse.js +27 -0
- package/dist/accessi-module/Dtos/GetUserByTokenResponse.js.map +1 -0
- 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.d.ts +1 -1
- package/dist/accessi-module/Dtos/LoginRequest.js +3 -2
- package/dist/accessi-module/Dtos/LoginRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/LoginResponse.d.ts +6 -2
- package/dist/accessi-module/Dtos/LoginResponse.js +20 -7
- 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.d.ts +1 -0
- package/dist/accessi-module/Dtos/Permission.js +1 -0
- package/dist/accessi-module/Dtos/Permission.js.map +1 -1
- package/dist/accessi-module/Dtos/PublicRegisterRequest.d.ts +5 -0
- package/dist/accessi-module/Dtos/PublicRegisterRequest.js +15 -0
- package/dist/accessi-module/Dtos/PublicRegisterRequest.js.map +1 -0
- package/dist/accessi-module/Dtos/RegisterRequest.d.ts +2 -4
- package/dist/accessi-module/Dtos/RegisterRequest.js +18 -28
- package/dist/accessi-module/Dtos/RegisterRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/RegisterResponse.d.ts +1 -1
- package/dist/accessi-module/Dtos/RegisterResponse.js +3 -3
- package/dist/accessi-module/Dtos/RegisterResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/Role.d.ts +4 -0
- package/dist/accessi-module/Dtos/Role.js +4 -0
- 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/SchemaDtos.d.ts +19 -0
- package/dist/accessi-module/Dtos/SchemaDtos.js +84 -0
- package/dist/accessi-module/Dtos/SchemaDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/SendResetPasswordEmailRequest.d.ts +4 -0
- package/dist/accessi-module/Dtos/SendResetPasswordEmailRequest.js +36 -0
- package/dist/accessi-module/Dtos/SendResetPasswordEmailRequest.js.map +1 -0
- 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.d.ts +4 -0
- package/dist/accessi-module/Dtos/TipoAbilitazione.js +4 -0
- 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.d.ts +13 -0
- package/dist/accessi-module/Dtos/TwoFactorDtos.js +60 -0
- package/dist/accessi-module/Dtos/TwoFactorDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/UpdateEnabledStatusRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/UserDto.d.ts +9 -4
- package/dist/accessi-module/Dtos/UserDto.js +39 -24
- 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.d.ts +5 -0
- package/dist/accessi-module/Dtos/UserGrantsResponse.js +27 -0
- package/dist/accessi-module/Dtos/UserGrantsResponse.js.map +1 -0
- package/dist/accessi-module/Dtos/index.d.ts +13 -0
- package/dist/accessi-module/Dtos/index.js +13 -0
- package/dist/accessi-module/Dtos/index.js.map +1 -1
- package/dist/accessi-module/Services/AdminBootstrapService/AdminBootstrapService.d.ts +33 -0
- package/dist/accessi-module/Services/AdminBootstrapService/AdminBootstrapService.js +106 -0
- package/dist/accessi-module/Services/AdminBootstrapService/AdminBootstrapService.js.map +1 -0
- package/dist/accessi-module/Services/AuthService/AuthService.d.ts +40 -5
- package/dist/accessi-module/Services/AuthService/AuthService.js +192 -42
- package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
- package/dist/accessi-module/Services/ConfiguratorService/ConfiguratorService.d.ts +3 -1
- package/dist/accessi-module/Services/ConfiguratorService/ConfiguratorService.js +5 -1
- package/dist/accessi-module/Services/ConfiguratorService/ConfiguratorService.js.map +1 -1
- package/dist/accessi-module/Services/EmailService/EmailService.d.ts +9 -2
- package/dist/accessi-module/Services/EmailService/EmailService.js +47 -22
- package/dist/accessi-module/Services/EmailService/EmailService.js.map +1 -1
- package/dist/accessi-module/Services/FiltriService/FiltriService.d.ts +18 -3
- package/dist/accessi-module/Services/FiltriService/FiltriService.js +92 -16
- package/dist/accessi-module/Services/FiltriService/FiltriService.js.map +1 -1
- package/dist/accessi-module/Services/PermissionService/PermissionService.d.ts +77 -2
- package/dist/accessi-module/Services/PermissionService/PermissionService.js +603 -206
- package/dist/accessi-module/Services/PermissionService/PermissionService.js.map +1 -1
- package/dist/accessi-module/Services/SchemaExportService/SchemaExportService.d.ts +36 -0
- package/dist/accessi-module/Services/SchemaExportService/SchemaExportService.js +125 -0
- package/dist/accessi-module/Services/SchemaExportService/SchemaExportService.js.map +1 -0
- 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.d.ts +27 -0
- package/dist/accessi-module/Services/TwoFactorService/TwoFactorService.js +175 -0
- package/dist/accessi-module/Services/TwoFactorService/TwoFactorService.js.map +1 -0
- package/dist/accessi-module/Services/UserService/UserService.d.ts +51 -5
- package/dist/accessi-module/Services/UserService/UserService.js +396 -294
- 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/accessiRuntime.d.ts +45 -0
- package/dist/accessi-module/accessiRuntime.js +39 -0
- package/dist/accessi-module/accessiRuntime.js.map +1 -0
- package/dist/accessi-module/database-updates/AccessiCatalogMigrator.d.ts +83 -0
- package/dist/accessi-module/database-updates/AccessiCatalogMigrator.js +360 -0
- package/dist/accessi-module/database-updates/AccessiCatalogMigrator.js.map +1 -0
- package/dist/accessi-module/database-updates/AccessiDatabaseUpdater.d.ts +57 -10
- package/dist/accessi-module/database-updates/AccessiDatabaseUpdater.js +475 -179
- package/dist/accessi-module/database-updates/AccessiDatabaseUpdater.js.map +1 -1
- package/dist/accessi-module/database-updates/accessiOptionsFromEnv.d.ts +5 -0
- package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js +79 -0
- package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js.map +1 -0
- package/dist/accessi-module/database-updates/accessiSchema.d.ts +67 -0
- package/dist/accessi-module/database-updates/accessiSchema.js +124 -0
- package/dist/accessi-module/database-updates/accessiSchema.js.map +1 -0
- package/dist/accessi-module/database-updates/optionalColumns.d.ts +7 -0
- package/dist/accessi-module/database-updates/optionalColumns.js +58 -0
- package/dist/accessi-module/database-updates/optionalColumns.js.map +1 -0
- package/dist/accessi-module/database-updates/runAccessiCatalogMigrations.d.ts +1 -0
- package/dist/accessi-module/database-updates/runAccessiCatalogMigrations.js +69 -0
- package/dist/accessi-module/database-updates/runAccessiCatalogMigrations.js.map +1 -0
- package/dist/accessi-module/database-updates/runAccessiDbUpdate.js +15 -53
- package/dist/accessi-module/database-updates/runAccessiDbUpdate.js.map +1 -1
- package/dist/accessi-module/federated-auth/FederatedAuthController.d.ts +24 -0
- package/dist/accessi-module/federated-auth/FederatedAuthController.js +333 -0
- package/dist/accessi-module/federated-auth/FederatedAuthController.js.map +1 -0
- package/dist/accessi-module/federated-auth/FederatedAuthService.d.ts +100 -0
- package/dist/accessi-module/federated-auth/FederatedAuthService.js +451 -0
- package/dist/accessi-module/federated-auth/FederatedAuthService.js.map +1 -0
- package/dist/accessi-module/federated-auth/FederatedAuthTypes.d.ts +53 -0
- package/dist/accessi-module/federated-auth/FederatedAuthTypes.js +3 -0
- package/dist/accessi-module/federated-auth/FederatedAuthTypes.js.map +1 -0
- package/dist/accessi-module/index.d.ts +29 -0
- package/dist/accessi-module/index.js +89 -19
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/accessi-module/jwt/jwt.strategy.d.ts +1 -1
- package/dist/accessi-module/jwt/jwt.strategy.js +16 -7
- package/dist/accessi-module/jwt/jwt.strategy.js.map +1 -1
- package/dist/accessi-module/middleware/accessiRequirements.d.ts +23 -1
- package/dist/accessi-module/middleware/accessiRequirements.js +7 -0
- package/dist/accessi-module/middleware/accessiRequirements.js.map +1 -1
- package/dist/accessi-module/middleware/authenticateGen.d.ts +16 -1
- package/dist/accessi-module/middleware/authenticateGen.js +63 -25
- package/dist/accessi-module/middleware/authenticateGen.js.map +1 -1
- package/dist/accessi-module/security/AccessiHttpExceptionFilter.d.ts +12 -0
- package/dist/accessi-module/security/AccessiHttpExceptionFilter.js +74 -0
- package/dist/accessi-module/security/AccessiHttpExceptionFilter.js.map +1 -0
- package/dist/accessi-module/security/accessControl.d.ts +22 -2
- package/dist/accessi-module/security/accessControl.js +47 -8
- package/dist/accessi-module/security/accessControl.js.map +1 -1
- package/dist/accessi-module/security/accessiValidation.d.ts +9 -0
- package/dist/accessi-module/security/accessiValidation.js +41 -0
- package/dist/accessi-module/security/accessiValidation.js.map +1 -0
- package/dist/accessi-module/security/adminBootstrap.d.ts +8 -0
- package/dist/accessi-module/security/adminBootstrap.js +41 -0
- package/dist/accessi-module/security/adminBootstrap.js.map +1 -0
- package/dist/accessi-module/security/authenticatedToken.d.ts +16 -2
- package/dist/accessi-module/security/authenticatedToken.js +31 -4
- package/dist/accessi-module/security/authenticatedToken.js.map +1 -1
- package/dist/accessi-module/security/emailConfiguration.d.ts +20 -0
- package/dist/accessi-module/security/emailConfiguration.js +41 -0
- package/dist/accessi-module/security/emailConfiguration.js.map +1 -0
- package/dist/accessi-module/security/passwordResetToken.d.ts +13 -3
- package/dist/accessi-module/security/passwordResetToken.js +13 -5
- package/dist/accessi-module/security/passwordResetToken.js.map +1 -1
- package/dist/accessi-module/security/publicAuthRateLimit.d.ts +3 -3
- package/dist/accessi-module/security/publicAuthRateLimit.js +31 -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/accessi-module/startupLog.d.ts +6 -0
- package/dist/accessi-module/startupLog.js +25 -0
- package/dist/accessi-module/startupLog.js.map +1 -0
- package/dist/allegati-module/AllegatiModule.d.ts +12 -1
- 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/emilsoftware-utilities-2.0.0-dev.40.tgz +0 -0
- package/dist/firebird-compat.d.ts +27 -6
- package/dist/firebird-compat.js +150 -135
- package/dist/firebird-compat.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/initEmilsoftwareModule.js +13 -3
- package/dist/initEmilsoftwareModule.js.map +1 -1
- package/dist/scripts/generateAccessiOpenApi.d.ts +1 -0
- package/dist/scripts/generateAccessiOpenApi.js +57 -0
- package/dist/scripts/generateAccessiOpenApi.js.map +1 -0
- 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.d.ts +7 -1
- package/dist/swagger/SwaggerConfig.js +22 -8
- package/dist/swagger/SwaggerConfig.js.map +1 -1
- package/package.json +105 -83
|
@@ -0,0 +1,3351 @@
|
|
|
1
|
+
// src/accessi-module/Console/accessiFetch.ts
|
|
2
|
+
var accessToken = null;
|
|
3
|
+
var loadingEventName = "accessi-console-network";
|
|
4
|
+
function notifyNetworkActivity(active) {
|
|
5
|
+
if (typeof window !== "undefined") {
|
|
6
|
+
window.dispatchEvent(new CustomEvent(loadingEventName, { detail: { active } }));
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function setAccessiConsoleToken(token2) {
|
|
10
|
+
accessToken = token2;
|
|
11
|
+
}
|
|
12
|
+
async function accessiFetch(url, options) {
|
|
13
|
+
const headers = new Headers(options.headers);
|
|
14
|
+
headers.set("Accept", "application/json");
|
|
15
|
+
if (options.body !== void 0 && !headers.has("Content-Type")) {
|
|
16
|
+
headers.set("Content-Type", "application/json");
|
|
17
|
+
}
|
|
18
|
+
if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
|
|
19
|
+
notifyNetworkActivity(true);
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(url, { ...options, headers });
|
|
22
|
+
const payload = await response.json().catch(() => void 0);
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const message = payload?.message ?? payload?.error ?? `Errore HTTP ${response.status}`;
|
|
25
|
+
throw new Error(message);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
data: payload,
|
|
29
|
+
status: response.status,
|
|
30
|
+
headers: response.headers
|
|
31
|
+
};
|
|
32
|
+
} finally {
|
|
33
|
+
notifyNetworkActivity(false);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/accessi-module/Console/generated/accessiApi.ts
|
|
38
|
+
var getGetUserByTokenUrl = () => {
|
|
39
|
+
return `/api/accessi/auth/get-user-by-token`;
|
|
40
|
+
};
|
|
41
|
+
var getUserByToken = async (getUserByTokenRequest, options) => {
|
|
42
|
+
return accessiFetch(
|
|
43
|
+
getGetUserByTokenUrl(),
|
|
44
|
+
{
|
|
45
|
+
...options,
|
|
46
|
+
method: "POST",
|
|
47
|
+
body: JSON.stringify(
|
|
48
|
+
getUserByTokenRequest
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
var getLoginUrl = () => {
|
|
54
|
+
return `/api/accessi/auth/login`;
|
|
55
|
+
};
|
|
56
|
+
var login = async (loginRequest, options) => {
|
|
57
|
+
return accessiFetch(
|
|
58
|
+
getLoginUrl(),
|
|
59
|
+
{
|
|
60
|
+
...options,
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: JSON.stringify(
|
|
63
|
+
loginRequest
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
var getVerifyTwoFactorUrl = () => {
|
|
69
|
+
return `/api/accessi/auth/two-factor/verify`;
|
|
70
|
+
};
|
|
71
|
+
var verifyTwoFactor = async (verifyTwoFactorRequest, options) => {
|
|
72
|
+
return accessiFetch(
|
|
73
|
+
getVerifyTwoFactorUrl(),
|
|
74
|
+
{
|
|
75
|
+
...options,
|
|
76
|
+
method: "POST",
|
|
77
|
+
body: JSON.stringify(
|
|
78
|
+
verifyTwoFactorRequest
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
var getResendTwoFactorUrl = () => {
|
|
84
|
+
return `/api/accessi/auth/two-factor/resend`;
|
|
85
|
+
};
|
|
86
|
+
var resendTwoFactor = async (resendTwoFactorRequest, options) => {
|
|
87
|
+
return accessiFetch(
|
|
88
|
+
getResendTwoFactorUrl(),
|
|
89
|
+
{
|
|
90
|
+
...options,
|
|
91
|
+
method: "POST",
|
|
92
|
+
body: JSON.stringify(
|
|
93
|
+
resendTwoFactorRequest
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
var getBootstrapAdminUrl = () => {
|
|
99
|
+
return `/api/accessi/auth/bootstrap-admin`;
|
|
100
|
+
};
|
|
101
|
+
var bootstrapAdmin = async (bootstrapAdminRequest, options) => {
|
|
102
|
+
return accessiFetch(
|
|
103
|
+
getBootstrapAdminUrl(),
|
|
104
|
+
{
|
|
105
|
+
...options,
|
|
106
|
+
method: "POST",
|
|
107
|
+
body: JSON.stringify(
|
|
108
|
+
bootstrapAdminRequest
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
var getGetRolesUrl = () => {
|
|
114
|
+
return `/api/accessi/permission/roles`;
|
|
115
|
+
};
|
|
116
|
+
var getRoles = async (options) => {
|
|
117
|
+
return accessiFetch(
|
|
118
|
+
getGetRolesUrl(),
|
|
119
|
+
{
|
|
120
|
+
...options,
|
|
121
|
+
method: "GET"
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
var getUpdateRoleUrl = (codiceRuolo) => {
|
|
126
|
+
return `/api/accessi/permission/update-role/${codiceRuolo}`;
|
|
127
|
+
};
|
|
128
|
+
var updateRole = async (codiceRuolo, role, options) => {
|
|
129
|
+
return accessiFetch(
|
|
130
|
+
getUpdateRoleUrl(codiceRuolo),
|
|
131
|
+
{
|
|
132
|
+
...options,
|
|
133
|
+
method: "PUT",
|
|
134
|
+
body: JSON.stringify(
|
|
135
|
+
role
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
var getCreateRoleUrl = () => {
|
|
141
|
+
return `/api/accessi/permission/create-role`;
|
|
142
|
+
};
|
|
143
|
+
var createRole = async (role, options) => {
|
|
144
|
+
return accessiFetch(
|
|
145
|
+
getCreateRoleUrl(),
|
|
146
|
+
{
|
|
147
|
+
...options,
|
|
148
|
+
method: "POST",
|
|
149
|
+
body: JSON.stringify(
|
|
150
|
+
role
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
var getAssignRolesToUserUrl = (codiceUtente) => {
|
|
156
|
+
return `/api/accessi/permission/assign-roles/${codiceUtente}`;
|
|
157
|
+
};
|
|
158
|
+
var assignRolesToUser = async (codiceUtente, assignRolesToUserRequest, options) => {
|
|
159
|
+
return accessiFetch(
|
|
160
|
+
getAssignRolesToUserUrl(codiceUtente),
|
|
161
|
+
{
|
|
162
|
+
...options,
|
|
163
|
+
method: "POST",
|
|
164
|
+
body: JSON.stringify(
|
|
165
|
+
assignRolesToUserRequest
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
var getAssignPermissionsToUserUrl = (codiceUtente) => {
|
|
171
|
+
return `/api/accessi/permission/assign-permissions/${codiceUtente}`;
|
|
172
|
+
};
|
|
173
|
+
var assignPermissionsToUser = async (codiceUtente, assignPermissionsToUserRequest, options) => {
|
|
174
|
+
return accessiFetch(
|
|
175
|
+
getAssignPermissionsToUserUrl(codiceUtente),
|
|
176
|
+
{
|
|
177
|
+
...options,
|
|
178
|
+
method: "POST",
|
|
179
|
+
body: JSON.stringify(
|
|
180
|
+
assignPermissionsToUserRequest
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
var getDeleteRoleUrl = (codiceRuolo) => {
|
|
186
|
+
return `/api/accessi/permission/delete-role/${codiceRuolo}`;
|
|
187
|
+
};
|
|
188
|
+
var deleteRole = async (codiceRuolo, options) => {
|
|
189
|
+
return accessiFetch(
|
|
190
|
+
getDeleteRoleUrl(codiceRuolo),
|
|
191
|
+
{
|
|
192
|
+
...options,
|
|
193
|
+
method: "DELETE"
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
var getCreateMenuUrl = () => {
|
|
198
|
+
return `/api/accessi/permission/menus`;
|
|
199
|
+
};
|
|
200
|
+
var createMenu = async (createMenuRequest, options) => {
|
|
201
|
+
return accessiFetch(
|
|
202
|
+
getCreateMenuUrl(),
|
|
203
|
+
{
|
|
204
|
+
...options,
|
|
205
|
+
method: "POST",
|
|
206
|
+
body: JSON.stringify(
|
|
207
|
+
createMenuRequest
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
var getGetGroupsWithMenusUrl = (params) => {
|
|
213
|
+
const normalizedParams = new URLSearchParams();
|
|
214
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
215
|
+
if (value === null) {
|
|
216
|
+
normalizedParams.append(key, "null");
|
|
217
|
+
} else if (value !== void 0) {
|
|
218
|
+
normalizedParams.append(key, value.toString());
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
return `/api/accessi/permission/groups-with-menus?${normalizedParams.toString()}`;
|
|
222
|
+
};
|
|
223
|
+
var getGroupsWithMenus = async (params, options) => {
|
|
224
|
+
return accessiFetch(
|
|
225
|
+
getGetGroupsWithMenusUrl(params),
|
|
226
|
+
{
|
|
227
|
+
...options,
|
|
228
|
+
method: "GET"
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
var getGetMenuTypesUrl = () => {
|
|
233
|
+
return `/api/accessi/permission/menu-types`;
|
|
234
|
+
};
|
|
235
|
+
var getMenuTypes = async (options) => {
|
|
236
|
+
return accessiFetch(
|
|
237
|
+
getGetMenuTypesUrl(),
|
|
238
|
+
{
|
|
239
|
+
...options,
|
|
240
|
+
method: "GET"
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
var getCreateMenuTypeUrl = () => {
|
|
245
|
+
return `/api/accessi/permission/menu-types`;
|
|
246
|
+
};
|
|
247
|
+
var createMenuType = async (createMenuTypeRequest, options) => {
|
|
248
|
+
return accessiFetch(
|
|
249
|
+
getCreateMenuTypeUrl(),
|
|
250
|
+
{
|
|
251
|
+
...options,
|
|
252
|
+
method: "POST",
|
|
253
|
+
body: JSON.stringify(
|
|
254
|
+
createMenuTypeRequest
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
var getUpdateMenuUrl = (codiceMenu) => {
|
|
260
|
+
return `/api/accessi/permission/menus/${codiceMenu}`;
|
|
261
|
+
};
|
|
262
|
+
var updateMenu = async (codiceMenu, updateMenuRequest, options) => {
|
|
263
|
+
return accessiFetch(
|
|
264
|
+
getUpdateMenuUrl(codiceMenu),
|
|
265
|
+
{
|
|
266
|
+
...options,
|
|
267
|
+
method: "PUT",
|
|
268
|
+
body: JSON.stringify(
|
|
269
|
+
updateMenuRequest
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
var getDeleteMenuUrl = (codiceMenu) => {
|
|
275
|
+
return `/api/accessi/permission/menus/${codiceMenu}`;
|
|
276
|
+
};
|
|
277
|
+
var deleteMenu = async (codiceMenu, options) => {
|
|
278
|
+
return accessiFetch(
|
|
279
|
+
getDeleteMenuUrl(codiceMenu),
|
|
280
|
+
{
|
|
281
|
+
...options,
|
|
282
|
+
method: "DELETE"
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
var getCreateMenuGroupUrl = () => {
|
|
287
|
+
return `/api/accessi/permission/menu-groups`;
|
|
288
|
+
};
|
|
289
|
+
var createMenuGroup = async (createMenuGroupRequest, options) => {
|
|
290
|
+
return accessiFetch(
|
|
291
|
+
getCreateMenuGroupUrl(),
|
|
292
|
+
{
|
|
293
|
+
...options,
|
|
294
|
+
method: "POST",
|
|
295
|
+
body: JSON.stringify(
|
|
296
|
+
createMenuGroupRequest
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
var getUpdateMenuGroupUrl = (codiceGruppo) => {
|
|
302
|
+
return `/api/accessi/permission/menu-groups/${codiceGruppo}`;
|
|
303
|
+
};
|
|
304
|
+
var updateMenuGroup = async (codiceGruppo, updateMenuGroupRequest, options) => {
|
|
305
|
+
return accessiFetch(
|
|
306
|
+
getUpdateMenuGroupUrl(codiceGruppo),
|
|
307
|
+
{
|
|
308
|
+
...options,
|
|
309
|
+
method: "PUT",
|
|
310
|
+
body: JSON.stringify(
|
|
311
|
+
updateMenuGroupRequest
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
var getDeleteMenuGroupUrl = (codiceGruppo) => {
|
|
317
|
+
return `/api/accessi/permission/menu-groups/${codiceGruppo}`;
|
|
318
|
+
};
|
|
319
|
+
var deleteMenuGroup = async (codiceGruppo, options) => {
|
|
320
|
+
return accessiFetch(
|
|
321
|
+
getDeleteMenuGroupUrl(codiceGruppo),
|
|
322
|
+
{
|
|
323
|
+
...options,
|
|
324
|
+
method: "DELETE"
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
};
|
|
328
|
+
var getUpdateMenuTypeUrl = (codiceTipo) => {
|
|
329
|
+
return `/api/accessi/permission/menu-types/${codiceTipo}`;
|
|
330
|
+
};
|
|
331
|
+
var updateMenuType = async (codiceTipo, updateMenuTypeRequest, options) => {
|
|
332
|
+
return accessiFetch(
|
|
333
|
+
getUpdateMenuTypeUrl(codiceTipo),
|
|
334
|
+
{
|
|
335
|
+
...options,
|
|
336
|
+
method: "PUT",
|
|
337
|
+
body: JSON.stringify(
|
|
338
|
+
updateMenuTypeRequest
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
var getDeleteMenuTypeUrl = (codiceTipo) => {
|
|
344
|
+
return `/api/accessi/permission/menu-types/${codiceTipo}`;
|
|
345
|
+
};
|
|
346
|
+
var deleteMenuType = async (codiceTipo, options) => {
|
|
347
|
+
return accessiFetch(
|
|
348
|
+
getDeleteMenuTypeUrl(codiceTipo),
|
|
349
|
+
{
|
|
350
|
+
...options,
|
|
351
|
+
method: "DELETE"
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
};
|
|
355
|
+
var getGetUserRolesAndGrantsUrl = (codiceUtente) => {
|
|
356
|
+
return `/api/accessi/permission/grants/${codiceUtente}`;
|
|
357
|
+
};
|
|
358
|
+
var getUserRolesAndGrants = async (codiceUtente, options) => {
|
|
359
|
+
return accessiFetch(
|
|
360
|
+
getGetUserRolesAndGrantsUrl(codiceUtente),
|
|
361
|
+
{
|
|
362
|
+
...options,
|
|
363
|
+
method: "GET"
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
};
|
|
367
|
+
var getGetUsersUrl = (params) => {
|
|
368
|
+
const normalizedParams = new URLSearchParams();
|
|
369
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
370
|
+
if (value === null) {
|
|
371
|
+
normalizedParams.append(key, "null");
|
|
372
|
+
} else if (value !== void 0) {
|
|
373
|
+
normalizedParams.append(key, value.toString());
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
return `/api/accessi/user/get-users?${normalizedParams.toString()}`;
|
|
377
|
+
};
|
|
378
|
+
var getUsers = async (params, options) => {
|
|
379
|
+
return accessiFetch(
|
|
380
|
+
getGetUsersUrl(params),
|
|
381
|
+
{
|
|
382
|
+
...options,
|
|
383
|
+
method: "GET"
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
};
|
|
387
|
+
var getDeleteUserUrl = (codiceUtente) => {
|
|
388
|
+
return `/api/accessi/user/delete-user/${codiceUtente}`;
|
|
389
|
+
};
|
|
390
|
+
var deleteUser = async (codiceUtente, options) => {
|
|
391
|
+
return accessiFetch(
|
|
392
|
+
getDeleteUserUrl(codiceUtente),
|
|
393
|
+
{
|
|
394
|
+
...options,
|
|
395
|
+
method: "DELETE"
|
|
396
|
+
}
|
|
397
|
+
);
|
|
398
|
+
};
|
|
399
|
+
var getSetStatoRegistrazioneUrl = () => {
|
|
400
|
+
return `/api/accessi/user/set-stato`;
|
|
401
|
+
};
|
|
402
|
+
var setStatoRegistrazione = async (setStatoRegistrazioneDto, options) => {
|
|
403
|
+
return accessiFetch(
|
|
404
|
+
getSetStatoRegistrazioneUrl(),
|
|
405
|
+
{
|
|
406
|
+
...options,
|
|
407
|
+
method: "POST",
|
|
408
|
+
body: JSON.stringify(
|
|
409
|
+
setStatoRegistrazioneDto
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
};
|
|
414
|
+
var getCreateManagedUserUrl = () => {
|
|
415
|
+
return `/api/accessi/user/create-managed-user`;
|
|
416
|
+
};
|
|
417
|
+
var createManagedUser = async (registerRequest, options) => {
|
|
418
|
+
return accessiFetch(
|
|
419
|
+
getCreateManagedUserUrl(),
|
|
420
|
+
{
|
|
421
|
+
...options,
|
|
422
|
+
method: "POST",
|
|
423
|
+
body: JSON.stringify(
|
|
424
|
+
registerRequest
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
};
|
|
429
|
+
var getUpdateUtenteUrl = (codiceUtente) => {
|
|
430
|
+
return `/api/accessi/user/update-user/${codiceUtente}`;
|
|
431
|
+
};
|
|
432
|
+
var updateUtente = async (codiceUtente, userDto, options) => {
|
|
433
|
+
return accessiFetch(
|
|
434
|
+
getUpdateUtenteUrl(codiceUtente),
|
|
435
|
+
{
|
|
436
|
+
...options,
|
|
437
|
+
method: "PUT",
|
|
438
|
+
body: JSON.stringify(
|
|
439
|
+
userDto
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
};
|
|
444
|
+
var getGetTipiFiltroUrl = () => {
|
|
445
|
+
return `/api/accessi/filtri/tipi`;
|
|
446
|
+
};
|
|
447
|
+
var getTipiFiltro = async (options) => {
|
|
448
|
+
return accessiFetch(
|
|
449
|
+
getGetTipiFiltroUrl(),
|
|
450
|
+
{
|
|
451
|
+
...options,
|
|
452
|
+
method: "GET"
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
};
|
|
456
|
+
var getCreateTipoFiltroUrl = () => {
|
|
457
|
+
return `/api/accessi/filtri/tipi`;
|
|
458
|
+
};
|
|
459
|
+
var createTipoFiltro = async (createFilterTypeRequest, options) => {
|
|
460
|
+
return accessiFetch(
|
|
461
|
+
getCreateTipoFiltroUrl(),
|
|
462
|
+
{
|
|
463
|
+
...options,
|
|
464
|
+
method: "POST",
|
|
465
|
+
body: JSON.stringify(
|
|
466
|
+
createFilterTypeRequest
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
};
|
|
471
|
+
var getUpdateTipoFiltroUrl = (tipFil) => {
|
|
472
|
+
return `/api/accessi/filtri/tipi/${tipFil}`;
|
|
473
|
+
};
|
|
474
|
+
var updateTipoFiltro = async (tipFil, updateFilterTypeRequest, options) => {
|
|
475
|
+
return accessiFetch(
|
|
476
|
+
getUpdateTipoFiltroUrl(tipFil),
|
|
477
|
+
{
|
|
478
|
+
...options,
|
|
479
|
+
method: "PUT",
|
|
480
|
+
body: JSON.stringify(
|
|
481
|
+
updateFilterTypeRequest
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
};
|
|
486
|
+
var getDeleteTipoFiltroUrl = (tipFil) => {
|
|
487
|
+
return `/api/accessi/filtri/tipi/${tipFil}`;
|
|
488
|
+
};
|
|
489
|
+
var deleteTipoFiltro = async (tipFil, options) => {
|
|
490
|
+
return accessiFetch(
|
|
491
|
+
getDeleteTipoFiltroUrl(tipFil),
|
|
492
|
+
{
|
|
493
|
+
...options,
|
|
494
|
+
method: "DELETE"
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
};
|
|
498
|
+
var getGetFiltriUtenteUrl = (params) => {
|
|
499
|
+
const normalizedParams = new URLSearchParams();
|
|
500
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
501
|
+
if (value === null) {
|
|
502
|
+
normalizedParams.append(key, "null");
|
|
503
|
+
} else if (value !== void 0) {
|
|
504
|
+
normalizedParams.append(key, value.toString());
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
return `/api/accessi/filtri/utente?${normalizedParams.toString()}`;
|
|
508
|
+
};
|
|
509
|
+
var getFiltriUtente = async (params, options) => {
|
|
510
|
+
return accessiFetch(
|
|
511
|
+
getGetFiltriUtenteUrl(params),
|
|
512
|
+
{
|
|
513
|
+
...options,
|
|
514
|
+
method: "GET"
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
var getSaveFiltriUtenteUrl = () => {
|
|
519
|
+
return `/api/accessi/filtri/utente`;
|
|
520
|
+
};
|
|
521
|
+
var saveFiltriUtente = async (filtriUtente, options) => {
|
|
522
|
+
return accessiFetch(
|
|
523
|
+
getSaveFiltriUtenteUrl(),
|
|
524
|
+
{
|
|
525
|
+
...options,
|
|
526
|
+
method: "POST",
|
|
527
|
+
body: JSON.stringify(
|
|
528
|
+
filtriUtente
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
};
|
|
533
|
+
var getSetMenuEnabledUrl = (codiceMenu) => {
|
|
534
|
+
return `/api/accessi/configurator/menus/${codiceMenu}/enabled`;
|
|
535
|
+
};
|
|
536
|
+
var setMenuEnabled = async (codiceMenu, updateEnabledStatusRequest, options) => {
|
|
537
|
+
return accessiFetch(
|
|
538
|
+
getSetMenuEnabledUrl(codiceMenu),
|
|
539
|
+
{
|
|
540
|
+
...options,
|
|
541
|
+
method: "PATCH",
|
|
542
|
+
body: JSON.stringify(
|
|
543
|
+
updateEnabledStatusRequest
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
};
|
|
548
|
+
var getSetGroupEnabledUrl = (codiceGruppo) => {
|
|
549
|
+
return `/api/accessi/configurator/groups/${codiceGruppo}/enabled`;
|
|
550
|
+
};
|
|
551
|
+
var setGroupEnabled = async (codiceGruppo, updateEnabledStatusRequest, options) => {
|
|
552
|
+
return accessiFetch(
|
|
553
|
+
getSetGroupEnabledUrl(codiceGruppo),
|
|
554
|
+
{
|
|
555
|
+
...options,
|
|
556
|
+
method: "PATCH",
|
|
557
|
+
body: JSON.stringify(
|
|
558
|
+
updateEnabledStatusRequest
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
);
|
|
562
|
+
};
|
|
563
|
+
var getGetFederatedProvidersUrl = () => {
|
|
564
|
+
return `/api/accessi/federated-auth/providers`;
|
|
565
|
+
};
|
|
566
|
+
var getFederatedProviders = async (options) => {
|
|
567
|
+
return accessiFetch(
|
|
568
|
+
getGetFederatedProvidersUrl(),
|
|
569
|
+
{
|
|
570
|
+
...options,
|
|
571
|
+
method: "GET"
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
var getCreateFederatedProviderUrl = () => {
|
|
576
|
+
return `/api/accessi/federated-auth/providers`;
|
|
577
|
+
};
|
|
578
|
+
var createFederatedProvider = async (createFederatedProviderRequest, options) => {
|
|
579
|
+
return accessiFetch(
|
|
580
|
+
getCreateFederatedProviderUrl(),
|
|
581
|
+
{
|
|
582
|
+
...options,
|
|
583
|
+
method: "POST",
|
|
584
|
+
body: JSON.stringify(
|
|
585
|
+
createFederatedProviderRequest
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
};
|
|
590
|
+
var getUpdateFederatedProviderUrl = (provider) => {
|
|
591
|
+
return `/api/accessi/federated-auth/providers/${provider}`;
|
|
592
|
+
};
|
|
593
|
+
var updateFederatedProvider = async (provider, updateFederatedProviderRequest, options) => {
|
|
594
|
+
return accessiFetch(
|
|
595
|
+
getUpdateFederatedProviderUrl(provider),
|
|
596
|
+
{
|
|
597
|
+
...options,
|
|
598
|
+
method: "PATCH",
|
|
599
|
+
body: JSON.stringify(
|
|
600
|
+
updateFederatedProviderRequest
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
};
|
|
605
|
+
var getDeleteFederatedProviderUrl = (provider) => {
|
|
606
|
+
return `/api/accessi/federated-auth/providers/${provider}`;
|
|
607
|
+
};
|
|
608
|
+
var deleteFederatedProvider = async (provider, options) => {
|
|
609
|
+
return accessiFetch(
|
|
610
|
+
getDeleteFederatedProviderUrl(provider),
|
|
611
|
+
{
|
|
612
|
+
...options,
|
|
613
|
+
method: "DELETE"
|
|
614
|
+
}
|
|
615
|
+
);
|
|
616
|
+
};
|
|
617
|
+
var getGetFederatedIdentitiesUrl = (codiceUtente) => {
|
|
618
|
+
return `/api/accessi/federated-auth/users/${codiceUtente}/identities`;
|
|
619
|
+
};
|
|
620
|
+
var getFederatedIdentities = async (codiceUtente, options) => {
|
|
621
|
+
return accessiFetch(
|
|
622
|
+
getGetFederatedIdentitiesUrl(codiceUtente),
|
|
623
|
+
{
|
|
624
|
+
...options,
|
|
625
|
+
method: "GET"
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
};
|
|
629
|
+
var getLinkFederatedIdentityUrl = (codiceUtente) => {
|
|
630
|
+
return `/api/accessi/federated-auth/users/${codiceUtente}/identities`;
|
|
631
|
+
};
|
|
632
|
+
var linkFederatedIdentity = async (codiceUtente, createFederatedIdentityRequest, options) => {
|
|
633
|
+
return accessiFetch(
|
|
634
|
+
getLinkFederatedIdentityUrl(codiceUtente),
|
|
635
|
+
{
|
|
636
|
+
...options,
|
|
637
|
+
method: "POST",
|
|
638
|
+
body: JSON.stringify(
|
|
639
|
+
createFederatedIdentityRequest
|
|
640
|
+
)
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
};
|
|
644
|
+
var getCreateFederatedUserUrl = () => {
|
|
645
|
+
return `/api/accessi/federated-auth/users`;
|
|
646
|
+
};
|
|
647
|
+
var createFederatedUser = async (createFederatedUserRequest, options) => {
|
|
648
|
+
return accessiFetch(
|
|
649
|
+
getCreateFederatedUserUrl(),
|
|
650
|
+
{
|
|
651
|
+
...options,
|
|
652
|
+
method: "POST",
|
|
653
|
+
body: JSON.stringify(
|
|
654
|
+
createFederatedUserRequest
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
);
|
|
658
|
+
};
|
|
659
|
+
var getUpdateFederatedIdentityUrl = (identityKey) => {
|
|
660
|
+
return `/api/accessi/federated-auth/identities/${identityKey}`;
|
|
661
|
+
};
|
|
662
|
+
var updateFederatedIdentity = async (identityKey, updateFederatedIdentityRequest, options) => {
|
|
663
|
+
return accessiFetch(
|
|
664
|
+
getUpdateFederatedIdentityUrl(identityKey),
|
|
665
|
+
{
|
|
666
|
+
...options,
|
|
667
|
+
method: "PATCH",
|
|
668
|
+
body: JSON.stringify(
|
|
669
|
+
updateFederatedIdentityRequest
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
);
|
|
673
|
+
};
|
|
674
|
+
var getDeleteFederatedIdentityPermanentlyUrl = (codiceUtente, identityKey) => {
|
|
675
|
+
return `/api/accessi/federated-auth/users/${codiceUtente}/identities/${identityKey}`;
|
|
676
|
+
};
|
|
677
|
+
var deleteFederatedIdentityPermanently = async (codiceUtente, identityKey, options) => {
|
|
678
|
+
return accessiFetch(
|
|
679
|
+
getDeleteFederatedIdentityPermanentlyUrl(codiceUtente, identityKey),
|
|
680
|
+
{
|
|
681
|
+
...options,
|
|
682
|
+
method: "DELETE"
|
|
683
|
+
}
|
|
684
|
+
);
|
|
685
|
+
};
|
|
686
|
+
var getCreateServiceTokenUrl = () => {
|
|
687
|
+
return `/api/accessi/service-token`;
|
|
688
|
+
};
|
|
689
|
+
var createServiceToken = async (createServiceTokenRequest, options) => {
|
|
690
|
+
return accessiFetch(
|
|
691
|
+
getCreateServiceTokenUrl(),
|
|
692
|
+
{
|
|
693
|
+
...options,
|
|
694
|
+
method: "POST",
|
|
695
|
+
body: JSON.stringify(
|
|
696
|
+
createServiceTokenRequest
|
|
697
|
+
)
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
};
|
|
701
|
+
var getGetServiceTokensUrl = (params) => {
|
|
702
|
+
const normalizedParams = new URLSearchParams();
|
|
703
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
704
|
+
if (value === null) {
|
|
705
|
+
normalizedParams.append(key, "null");
|
|
706
|
+
} else if (value !== void 0) {
|
|
707
|
+
normalizedParams.append(key, value.toString());
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
return `/api/accessi/service-token?${normalizedParams.toString()}`;
|
|
711
|
+
};
|
|
712
|
+
var getServiceTokens = async (params, options) => {
|
|
713
|
+
return accessiFetch(
|
|
714
|
+
getGetServiceTokensUrl(params),
|
|
715
|
+
{
|
|
716
|
+
...options,
|
|
717
|
+
method: "GET"
|
|
718
|
+
}
|
|
719
|
+
);
|
|
720
|
+
};
|
|
721
|
+
var getRevokeServiceTokenUrl = (tokenId) => {
|
|
722
|
+
return `/api/accessi/service-token/${tokenId}`;
|
|
723
|
+
};
|
|
724
|
+
var revokeServiceToken = async (tokenId, options) => {
|
|
725
|
+
return accessiFetch(
|
|
726
|
+
getRevokeServiceTokenUrl(tokenId),
|
|
727
|
+
{
|
|
728
|
+
...options,
|
|
729
|
+
method: "DELETE"
|
|
730
|
+
}
|
|
731
|
+
);
|
|
732
|
+
};
|
|
733
|
+
var getRotateServiceTokenUrl = (tokenId) => {
|
|
734
|
+
return `/api/accessi/service-token/${tokenId}/rotate`;
|
|
735
|
+
};
|
|
736
|
+
var rotateServiceToken = async (tokenId, options) => {
|
|
737
|
+
return accessiFetch(
|
|
738
|
+
getRotateServiceTokenUrl(tokenId),
|
|
739
|
+
{
|
|
740
|
+
...options,
|
|
741
|
+
method: "POST"
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
};
|
|
745
|
+
var getGetSchemaUrl = () => {
|
|
746
|
+
return `/api/accessi/schema`;
|
|
747
|
+
};
|
|
748
|
+
var getSchema = async (options) => {
|
|
749
|
+
return accessiFetch(
|
|
750
|
+
getGetSchemaUrl(),
|
|
751
|
+
{
|
|
752
|
+
...options,
|
|
753
|
+
method: "GET"
|
|
754
|
+
}
|
|
755
|
+
);
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
// src/accessi-module/Console/wiki.ts
|
|
759
|
+
var p = (text) => ({ kind: "p", text });
|
|
760
|
+
var h = (text) => ({ kind: "h", text });
|
|
761
|
+
var list = (items, ordered = false) => ({ kind: "list", items, ordered });
|
|
762
|
+
var code = (language, codeText, title, variant) => ({ kind: "code", language, title, code: codeText, variant });
|
|
763
|
+
var good = (language, codeText, title) => code(language, codeText, title, "good");
|
|
764
|
+
var bad = (language, codeText, title) => code(language, codeText, title, "bad");
|
|
765
|
+
var note = (tone, title, text) => ({ kind: "callout", tone, title, text });
|
|
766
|
+
var table = (head, rows) => ({ kind: "table", head, rows });
|
|
767
|
+
var WIKI_SECTIONS = [
|
|
768
|
+
// -------------------------------------------------------------------------
|
|
769
|
+
{
|
|
770
|
+
id: "panoramica",
|
|
771
|
+
group: "Per iniziare",
|
|
772
|
+
title: "Panoramica e architettura",
|
|
773
|
+
summary: "Che cosa fa Accessi, come si incastra nel backend e come scorre una richiesta.",
|
|
774
|
+
blocks: [
|
|
775
|
+
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.`),
|
|
776
|
+
h(`Che cosa possiede il modulo`),
|
|
777
|
+
list([
|
|
778
|
+
`Le rotte sotto /api/accessi/* (login, utenti, ruoli, menu, filtri, SSO, token, console).`,
|
|
779
|
+
`Lo schema database del dominio Accessi, riconciliato automaticamente al bootstrap.`,
|
|
780
|
+
`Il middleware di autorizzazione authorizeAccessi per le rotte della tua applicazione.`,
|
|
781
|
+
`La console web su /api/accessi/console.`
|
|
782
|
+
]),
|
|
783
|
+
h(`Che cosa resta alla tua applicazione`),
|
|
784
|
+
list([
|
|
785
|
+
`Il proprio frontend e le proprie rotte di business.`,
|
|
786
|
+
`La validazione dei token dei provider SSO esterni (Accessi non li vede).`,
|
|
787
|
+
`I segreti: jwtOptions.secret, SMTP, database ed encryptionKey non raggiungono mai il browser.`
|
|
788
|
+
]),
|
|
789
|
+
h(`Flusso di una richiesta protetta`),
|
|
790
|
+
list([
|
|
791
|
+
`Il client chiama una rotta della tua app con header Authorization: Bearer <jwt>.`,
|
|
792
|
+
`La tua rotta esegue authorizeAccessi, che verifica firma HS256, scadenza e stato utente corrente.`,
|
|
793
|
+
`Se passi un requisito, il modulo carica ruoli e grant dell'utente e li valuta.`,
|
|
794
|
+
`Su successo req.user, req.data e (se richiesto) req.userGrants sono popolati; altrimenti risponde con il contratto errore Accessi.`
|
|
795
|
+
], true),
|
|
796
|
+
note("info", `Due modalit\xE0 di bootstrap`, `Express esistente: initializeAccessiModule(app, options). NestJS esistente: AccessiModule.forRoot(options) nei tuoi imports. Non usarle entrambe.`)
|
|
797
|
+
]
|
|
798
|
+
},
|
|
799
|
+
// -------------------------------------------------------------------------
|
|
800
|
+
{
|
|
801
|
+
id: "installazione",
|
|
802
|
+
group: "Per iniziare",
|
|
803
|
+
title: "Installazione",
|
|
804
|
+
summary: "Dipendenze, peer e build.",
|
|
805
|
+
blocks: [
|
|
806
|
+
code("bash", `npm install emilsoftware-utilities`, "Installazione"),
|
|
807
|
+
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.`),
|
|
808
|
+
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.`)
|
|
809
|
+
]
|
|
810
|
+
},
|
|
811
|
+
// -------------------------------------------------------------------------
|
|
812
|
+
{
|
|
813
|
+
id: "bootstrap-express",
|
|
814
|
+
group: "Per iniziare",
|
|
815
|
+
title: "Bootstrap su Express esistente",
|
|
816
|
+
summary: "initializeAccessiModule: ordine corretto e attesa prima di listen.",
|
|
817
|
+
blocks: [
|
|
818
|
+
p(`Chiama initializeAccessiModule prima di aprire la porta HTTP. Durante il bootstrap, authorizeAccessi risponde 503 ACCESSI_AUTH_INITIALIZING invece di autorizzare richieste parziali.`),
|
|
819
|
+
good("ts", [
|
|
820
|
+
"import express from 'express';",
|
|
821
|
+
"import { initializeAccessiModule, authorizeAccessi } from 'emilsoftware-utilities';",
|
|
822
|
+
"",
|
|
823
|
+
"const app = express();",
|
|
824
|
+
"app.use(express.json());",
|
|
825
|
+
"",
|
|
826
|
+
"await initializeAccessiModule(app, options); // prima di app.listen",
|
|
827
|
+
"",
|
|
828
|
+
"app.get('/api/ordini', authorizeAccessi, (req, res) => {",
|
|
829
|
+
" res.json({ utente: req.user });",
|
|
830
|
+
"});",
|
|
831
|
+
"",
|
|
832
|
+
"app.get('/api/ordini/scrivi', (req, res, next) =>",
|
|
833
|
+
" authorizeAccessi(req, res, next, {",
|
|
834
|
+
" requirements: [{ menuCode: 'ORDINI', minPermissionLevel: 20 }],",
|
|
835
|
+
" }),",
|
|
836
|
+
"(req, res) => res.json({ ok: true }));",
|
|
837
|
+
"",
|
|
838
|
+
"app.listen(3000);"
|
|
839
|
+
].join("\n"), "Express + Accessi"),
|
|
840
|
+
bad("ts", [
|
|
841
|
+
"const app = express();",
|
|
842
|
+
"app.listen(3000); // porta aperta prima del bootstrap",
|
|
843
|
+
"await initializeAccessiModule(app, options); // le richieste nel frattempo ricevono 503"
|
|
844
|
+
].join("\n"), "Non aprire la porta prima del bootstrap"),
|
|
845
|
+
h(`Rotte montate`),
|
|
846
|
+
p(`Accessi intercetta solo /api/accessi/*, /accessi/swagger* e /accessi/swagger.json. Tutte le altre rotte passano intatte al tuo stack Express.`),
|
|
847
|
+
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.`)
|
|
848
|
+
]
|
|
849
|
+
},
|
|
850
|
+
// -------------------------------------------------------------------------
|
|
851
|
+
{
|
|
852
|
+
id: "bootstrap-nest",
|
|
853
|
+
group: "Per iniziare",
|
|
854
|
+
title: "Bootstrap su NestJS esistente",
|
|
855
|
+
summary: "AccessiModule.forRoot e uso dei provider esportati.",
|
|
856
|
+
blocks: [
|
|
857
|
+
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).`),
|
|
858
|
+
good("ts", [
|
|
859
|
+
"import { Module } from '@nestjs/common';",
|
|
860
|
+
"import { AccessiModule } from 'emilsoftware-utilities';",
|
|
861
|
+
"",
|
|
862
|
+
"@Module({",
|
|
863
|
+
" imports: [AccessiModule.forRoot(options)],",
|
|
864
|
+
"})",
|
|
865
|
+
"export class AppModule {}"
|
|
866
|
+
].join("\n"), "app.module.ts"),
|
|
867
|
+
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.`)
|
|
868
|
+
]
|
|
869
|
+
},
|
|
870
|
+
// -------------------------------------------------------------------------
|
|
871
|
+
{
|
|
872
|
+
id: "opzioni",
|
|
873
|
+
group: "Configurazione",
|
|
874
|
+
title: "AccessiOptions (riferimento completo)",
|
|
875
|
+
summary: "Ogni campo di configurazione con tipo, obbligo e note.",
|
|
876
|
+
blocks: [
|
|
877
|
+
table(
|
|
878
|
+
["Campo", "Tipo", "Obbligo", "Note"],
|
|
879
|
+
[
|
|
880
|
+
["databaseOptions", "Options (node-firebird)", "s\xEC", "Connessione Firebird al database Accessi."],
|
|
881
|
+
["confirmationEmailUrl", "string", "s\xEC", "Base URL usata per il reset password."],
|
|
882
|
+
["confirmationEmailReturnUrl", "string", "s\xEC", "URL di ritorno dopo il reset."],
|
|
883
|
+
["confirmationEmailPrefix", "string", "no", "Prefisso passato alla pagina di reset."],
|
|
884
|
+
["customResetPage", "string", "no", "Sostituisce la pagina HTML di reset inclusa."],
|
|
885
|
+
["encryptionKey", "string", "s\xEC", "Compatibilit\xE0 e migrazione delle password legacy."],
|
|
886
|
+
["mockDemoUser", "boolean", "s\xEC", "Utenti fittizi admin e demo solo fuori produzione."],
|
|
887
|
+
["passwordExpiration", "boolean", "no", "Attiva il controllo della scadenza password al login."],
|
|
888
|
+
["passwordExpirationDays", "number", "no", "Valore predefinito: 90 giorni."],
|
|
889
|
+
["legacyPasswordMigrationOnStartup", "boolean", "no", "Predefinito true: converte le password legacy."],
|
|
890
|
+
["autoUpdateDatabase", "boolean", "no", "Predefinito true: riconcilia lo schema al boot."],
|
|
891
|
+
["jwtOptions", "{ secret, expiresIn }", "s\xEC", "Firma HS256 e durata dei JWT Accessi."],
|
|
892
|
+
["emailOptions", "EmailOptions", "s\xEC", "SMTP per reset e codici email."],
|
|
893
|
+
["publicAuthRateLimit", "PublicAuthRateLimitOptions", "no", "Limiti in-memory sugli endpoint pubblici."],
|
|
894
|
+
["publicRegistration", "{ enabled }", "no", "Registrazione pubblica: disattivata per impostazione predefinita."],
|
|
895
|
+
["federatedAuthentication", "FederatedAuthenticationOptions", "no", `Abilita l'SSO generico.`],
|
|
896
|
+
["extensionFieldsOptions", "ExtensionFieldsOptions[]", "no", "Tabelle esterne allegate al profilo."],
|
|
897
|
+
["serviceTokens", "{ enabled, defaultTtlDays }", "no", "Token macchina-a-macchina, abilitati per impostazione predefinita."],
|
|
898
|
+
["catalogScripts", "{ enabled, folder, ... }", "no", "Catalog migrations SQL eseguite dopo la riconciliazione dello schema."],
|
|
899
|
+
["adminBootstrap", "{ enabled, superUser }", "no", "Token casuale a ogni avvio per creare il primo admin dalla console."],
|
|
900
|
+
["publicBaseUrl", "string", "no", "Base URL usata solo per loggare console/swagger/API."]
|
|
901
|
+
]
|
|
902
|
+
),
|
|
903
|
+
good("ts", [
|
|
904
|
+
"import type { AccessiOptions } from 'emilsoftware-utilities';",
|
|
905
|
+
"",
|
|
906
|
+
"export const options: AccessiOptions = {",
|
|
907
|
+
" databaseOptions: {",
|
|
908
|
+
" host: process.env.ACCESSI_DB_HOST!,",
|
|
909
|
+
" port: Number(process.env.ACCESSI_DB_PORT ?? 3050),",
|
|
910
|
+
" database: process.env.ACCESSI_DB_NAME!,",
|
|
911
|
+
" user: process.env.ACCESSI_DB_USER!,",
|
|
912
|
+
" password: process.env.ACCESSI_DB_PASSWORD!,",
|
|
913
|
+
" },",
|
|
914
|
+
" confirmationEmailUrl: 'https://app.example.com',",
|
|
915
|
+
" confirmationEmailReturnUrl: 'https://app.example.com/login',",
|
|
916
|
+
" encryptionKey: process.env.ACCESSI_ENCRYPTION_KEY!,",
|
|
917
|
+
" mockDemoUser: false,",
|
|
918
|
+
" passwordExpiration: true,",
|
|
919
|
+
" passwordExpirationDays: 90,",
|
|
920
|
+
" jwtOptions: {",
|
|
921
|
+
" secret: process.env.ACCESSI_JWT_SECRET!, // almeno 32 caratteri casuali, unico per ambiente",
|
|
922
|
+
" expiresIn: '24h',",
|
|
923
|
+
" },",
|
|
924
|
+
" emailOptions: {",
|
|
925
|
+
" host: process.env.SMTP_HOST!,",
|
|
926
|
+
" port: Number(process.env.SMTP_PORT ?? 587),",
|
|
927
|
+
" secure: false,",
|
|
928
|
+
" requireTLS: true,",
|
|
929
|
+
" tls: { rejectUnauthorized: true },",
|
|
930
|
+
" from: 'no-reply@example.com',",
|
|
931
|
+
" auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! },",
|
|
932
|
+
" },",
|
|
933
|
+
" publicRegistration: { enabled: false },",
|
|
934
|
+
" serviceTokens: { enabled: true, defaultTtlDays: 365 },",
|
|
935
|
+
"};"
|
|
936
|
+
].join("\n"), "Configurazione tipica"),
|
|
937
|
+
bad("ts", [
|
|
938
|
+
"jwtOptions: { secret: 'accessi', expiresIn: '24h' }, // segreto debole e condiviso",
|
|
939
|
+
"mockDemoUser: true, // MAI in produzione"
|
|
940
|
+
].join("\n"), "Errori comuni"),
|
|
941
|
+
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.`)
|
|
942
|
+
]
|
|
943
|
+
},
|
|
944
|
+
// -------------------------------------------------------------------------
|
|
945
|
+
{
|
|
946
|
+
id: "database",
|
|
947
|
+
group: "Configurazione",
|
|
948
|
+
title: "Schema e aggiornamento del database",
|
|
949
|
+
summary: "Tabelle, riconciliazione automatica e CLI.",
|
|
950
|
+
blocks: [
|
|
951
|
+
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.`),
|
|
952
|
+
h(`Tabelle principali`),
|
|
953
|
+
list([
|
|
954
|
+
`PARAMETRI, UTENTI, UTENTI_CONFIG, UTENTI_PWD, UTENTI_OLDPWD, UTENTI_GDPR`,
|
|
955
|
+
`RUOLI, MENU_GRP, MENU_TIPI, MENU, ABILITAZIONI, RUOLI_MNU, UTENTI_RUOLI`,
|
|
956
|
+
`FILTRI, FILTRI_TIPO`,
|
|
957
|
+
`SSO_PROVIDER, UTENTI_IDENTITA_EXT`,
|
|
958
|
+
`ACCESSI_2FA, ACCESSI_SERVICE_TOKEN, ACCESSI_CATALOG_SCRIPT`
|
|
959
|
+
]),
|
|
960
|
+
code("bash", `npm run db:update:accessi # riconcilia lo schema
|
|
961
|
+
npm run db:check:accessi # verifica senza modificare`, "CLI schema"),
|
|
962
|
+
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.`)
|
|
963
|
+
]
|
|
964
|
+
},
|
|
965
|
+
// -------------------------------------------------------------------------
|
|
966
|
+
{
|
|
967
|
+
id: "catalog-migrations",
|
|
968
|
+
group: "Configurazione",
|
|
969
|
+
title: "Catalog migrations SQL",
|
|
970
|
+
summary: "Allinea menu, ruoli e cataloghi su piu database in modo idempotente.",
|
|
971
|
+
blocks: [
|
|
972
|
+
p(`Gli script SQL applicativi (menu, ruoli, tipi, cataloghi) restano nel repository del backend ospitante e Accessi li esegue dopo la riconciliazione dello schema. Ogni script viene tracciato nella tabella ACCESSI_CATALOG_SCRIPT (nome + checksum), quindi viene applicato una sola volta per database.`),
|
|
973
|
+
h(`Configurazione`),
|
|
974
|
+
good("ts", [
|
|
975
|
+
"catalogScripts: {",
|
|
976
|
+
" enabled: true,",
|
|
977
|
+
" folder: './db/accessi-catalog', // versionata nel repo del backend",
|
|
978
|
+
" onChecksumMismatch: 'error', // 'error' (default) | 'warn' | 'reapply'",
|
|
979
|
+
" stopOnError: true,",
|
|
980
|
+
"}"
|
|
981
|
+
].join("\n"), "AccessiOptions"),
|
|
982
|
+
list([
|
|
983
|
+
"Gli script sono ordinati per percorso (usa prefissi numerici: 0001_, 0002_, ...).",
|
|
984
|
+
"Devono essere idempotenti e ripetibili: UPDATE OR INSERT ... MATCHING, MERGE, DELETE+INSERT mirati.",
|
|
985
|
+
"Non includere utenti: le catalog migrations allineano solo catalogo e configurazione.",
|
|
986
|
+
"Supportati stringhe, commenti e statement multipli; SET TERM non serve perch\xE9 gli script sono solo DML."
|
|
987
|
+
]),
|
|
988
|
+
note("danger", `Solo righe (DML)`, `Vincolo tassativo: uno script puo eseguire solo INSERT, UPDATE, DELETE, MERGE, SELECT, EXECUTE. Qualunque DDL (CREATE/ALTER/DROP, trigger, procedure, GRANT, transazioni) viene rifiutato. Lo schema si aggiorna solo con db:update:accessi.`),
|
|
989
|
+
note("info", `File non conformi`, `Un file con statement non consentiti non viene applicato ne registrato nel ledger; il run prosegue con gli altri e il report riporta una voce per ogni file con i relativi messaggi di errore (visibili anche con db:catalog:accessi:check).`),
|
|
990
|
+
note("info", `allowDdl`, `Disattivato di default. Impostarlo a true disabilita la validazione e permette DDL: usarlo solo consapevolmente e mai come sostituto dell'updater dello schema.`),
|
|
991
|
+
code("bash", `# Applica (schema + catalog migrations) su un database
|
|
992
|
+
ACCESSI_CATALOG_FOLDER=./db/accessi-catalog npm run db:catalog:accessi
|
|
993
|
+
|
|
994
|
+
# Elenca solo quelle da applicare / modificate
|
|
995
|
+
ACCESSI_CATALOG_FOLDER=./db/accessi-catalog npm run db:catalog:accessi:check`, "CLI catalog migrations"),
|
|
996
|
+
note("info", `Idempotenza`, `Uno script gia applicato con lo stesso checksum viene saltato. Se il file cambia dopo l'applicazione, con policy error il run si interrompe: non modificare uno script applicato, aggiungine uno nuovo.`)
|
|
997
|
+
]
|
|
998
|
+
},
|
|
999
|
+
// -------------------------------------------------------------------------
|
|
1000
|
+
{
|
|
1001
|
+
id: "login",
|
|
1002
|
+
group: "Autenticazione",
|
|
1003
|
+
title: "Login locale e JWT",
|
|
1004
|
+
summary: "Endpoint di login, risposta e gestione della password scaduta.",
|
|
1005
|
+
blocks: [
|
|
1006
|
+
code("http", `POST /api/accessi/auth/login
|
|
1007
|
+
Content-Type: application/json
|
|
1008
|
+
|
|
1009
|
+
{ "email": "utente@example.com", "password": "Segreta123!" }`, "Login con password"),
|
|
1010
|
+
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).`),
|
|
1011
|
+
good("ts", [
|
|
1012
|
+
"const res = await fetch('/api/accessi/auth/login', {",
|
|
1013
|
+
" method: 'POST',",
|
|
1014
|
+
" headers: { 'Content-Type': 'application/json' },",
|
|
1015
|
+
" body: JSON.stringify({ email, password }),",
|
|
1016
|
+
"});",
|
|
1017
|
+
"const body = await res.json();",
|
|
1018
|
+
"if (body.Result?.challenge) { /* richiedi il codice */ }",
|
|
1019
|
+
"const token = body.Result?.token;"
|
|
1020
|
+
].join("\n"), "Chiamata dal frontend"),
|
|
1021
|
+
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.`),
|
|
1022
|
+
h(`Verifica del token`),
|
|
1023
|
+
code("http", `POST /api/accessi/auth/get-user-by-token
|
|
1024
|
+
Content-Type: application/json
|
|
1025
|
+
|
|
1026
|
+
{ "token": "<jwt>" }`, "get-user-by-token"),
|
|
1027
|
+
p(`Usalo per verificare un JWT e ottenere il profilo corrente (utile al bootstrap di una SPA). Restituisce Result.userData.`)
|
|
1028
|
+
]
|
|
1029
|
+
},
|
|
1030
|
+
// -------------------------------------------------------------------------
|
|
1031
|
+
{
|
|
1032
|
+
id: "passwordless-2fa",
|
|
1033
|
+
group: "Autenticazione",
|
|
1034
|
+
title: "Codici email, 2FA e passwordless",
|
|
1035
|
+
summary: "Verifica in due passi, challenge e policy per utente.",
|
|
1036
|
+
blocks: [
|
|
1037
|
+
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.`),
|
|
1038
|
+
table(
|
|
1039
|
+
["Configurazione", "Accesso locale", "Accesso SSO"],
|
|
1040
|
+
[
|
|
1041
|
+
["2FA disattivata", "Password", "Provider"],
|
|
1042
|
+
["2FA attiva", "Password + codice email", "Provider + codice email"],
|
|
1043
|
+
["2FA + passwordless", "Solo codice email", "Provider + codice email"]
|
|
1044
|
+
]
|
|
1045
|
+
),
|
|
1046
|
+
code("json", `{
|
|
1047
|
+
"Result": {
|
|
1048
|
+
"challenge": {
|
|
1049
|
+
"twoFactorRequired": true,
|
|
1050
|
+
"challengeId": "64-caratteri-esadecimali",
|
|
1051
|
+
"method": "email",
|
|
1052
|
+
"expiresAt": "2026-09-11T14:00:00.000Z",
|
|
1053
|
+
"resendAfterSeconds": 60
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}`, "Risposta con challenge"),
|
|
1057
|
+
code("http", `POST /api/accessi/auth/two-factor/verify
|
|
1058
|
+
{ "challengeId": "...", "code": "012345" }
|
|
1059
|
+
|
|
1060
|
+
POST /api/accessi/auth/two-factor/resend
|
|
1061
|
+
{ "challengeId": "..." }`, "Verifica e reinvio"),
|
|
1062
|
+
list([
|
|
1063
|
+
`Il codice ha 6 cifre, dura 10 minuti, consente 5 tentativi totali e al massimo 3 invii per challenge.`,
|
|
1064
|
+
`Fino alla verifica non sono presenti token, profilo o grant.`,
|
|
1065
|
+
`Il challenge non \xE8 un JWT e non va usato nell'header Authorization.`
|
|
1066
|
+
]),
|
|
1067
|
+
bad("ts", [
|
|
1068
|
+
"// Sbagliato: usare il challenge come token",
|
|
1069
|
+
"fetch('/api/ordini', { headers: { Authorization: 'Bearer ' + challengeId } }); // 401"
|
|
1070
|
+
].join("\n"), "Il challenge non \xE8 un token")
|
|
1071
|
+
]
|
|
1072
|
+
},
|
|
1073
|
+
// -------------------------------------------------------------------------
|
|
1074
|
+
{
|
|
1075
|
+
id: "reset-password",
|
|
1076
|
+
group: "Autenticazione",
|
|
1077
|
+
title: "Reset password",
|
|
1078
|
+
summary: "Invio email e conferma con token monouso.",
|
|
1079
|
+
blocks: [
|
|
1080
|
+
code("http", `POST /api/accessi/email/send-reset-password-email
|
|
1081
|
+
{ "email": "utente@example.com" }
|
|
1082
|
+
|
|
1083
|
+
POST /api/accessi/auth/confirm-reset-password/{token}
|
|
1084
|
+
{ "newPassword": "NuovaSegreta123!" }`, "Reset in due passi"),
|
|
1085
|
+
p(`L'invio non rivela se l'email esiste (anti-enumerazione). Il token di reset \xE8 monouso ed \xE8 firmato con una chiave dedicata.`),
|
|
1086
|
+
note("info", `Pagina personalizzata`, `Imposta customResetPage per usare la tua pagina di reset invece di quella inclusa. confirmationEmailReturnUrl e confirmationEmailPrefix controllano il ritorno.`)
|
|
1087
|
+
]
|
|
1088
|
+
},
|
|
1089
|
+
// -------------------------------------------------------------------------
|
|
1090
|
+
{
|
|
1091
|
+
id: "middleware",
|
|
1092
|
+
group: "Autorizzazione",
|
|
1093
|
+
title: "authorizeAccessi (middleware)",
|
|
1094
|
+
summary: "Proteggere le rotte della tua app con il JWT Accessi.",
|
|
1095
|
+
blocks: [
|
|
1096
|
+
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)).`),
|
|
1097
|
+
good("ts", [
|
|
1098
|
+
"app.get('/api/report',",
|
|
1099
|
+
" (req, res, next) => authorizeAccessi(req, res, next, {",
|
|
1100
|
+
" requirements: [{ menuCode: 'REPORT', minPermissionLevel: 10 }],",
|
|
1101
|
+
" }),",
|
|
1102
|
+
" (req, res) => res.json({ user: req.user, grants: req.userGrants }),",
|
|
1103
|
+
");"
|
|
1104
|
+
].join("\n"), "Rotta protetta"),
|
|
1105
|
+
h(`Che cosa trovi nella request`),
|
|
1106
|
+
list([
|
|
1107
|
+
`req.user e req.data: payload utente corrente (alias storici).`,
|
|
1108
|
+
`req.userGrants: grant effettivi, presente solo quando hai richiesto dei requisiti.`
|
|
1109
|
+
]),
|
|
1110
|
+
bad("ts", [
|
|
1111
|
+
"app.get('/api/report', authorizeAccessi, (req, res) => {",
|
|
1112
|
+
" // Sbagliato: assumere che req.userGrants esista senza requisiti",
|
|
1113
|
+
" const grants = req.userGrants.grants; // possibile TypeError",
|
|
1114
|
+
"});"
|
|
1115
|
+
].join("\n"), "Non dare per scontato req.userGrants"),
|
|
1116
|
+
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.`)
|
|
1117
|
+
]
|
|
1118
|
+
},
|
|
1119
|
+
// -------------------------------------------------------------------------
|
|
1120
|
+
{
|
|
1121
|
+
id: "requisiti",
|
|
1122
|
+
group: "Autorizzazione",
|
|
1123
|
+
title: "Requisiti e livelli di abilitazione",
|
|
1124
|
+
summary: "DSL accessiRequirement: permission, and, or, not, custom.",
|
|
1125
|
+
blocks: [
|
|
1126
|
+
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.`),
|
|
1127
|
+
good("ts", [
|
|
1128
|
+
"import { authorizeAccessi, accessiRequirement as r } from 'emilsoftware-utilities';",
|
|
1129
|
+
"",
|
|
1130
|
+
"// AND (entrambi i menu)",
|
|
1131
|
+
'const both = r.and(r.permission("ORDINI", 20), r.permission("CLIENTI", 10));',
|
|
1132
|
+
"",
|
|
1133
|
+
"// OR",
|
|
1134
|
+
'const either = r.or(r.permission("REPORT", 10), r.permission("EXPORT", 20));',
|
|
1135
|
+
"",
|
|
1136
|
+
"// NOT + custom",
|
|
1137
|
+
"const policy = r.and(",
|
|
1138
|
+
' r.permission("AREA_RISERVATA", 10),',
|
|
1139
|
+
' r.not(r.permission("BLOCCO_TOTALE", 0)),',
|
|
1140
|
+
");",
|
|
1141
|
+
"",
|
|
1142
|
+
'app.get("/api/riservato", (req, res, next) => authorizeAccessi(req, res, next, {',
|
|
1143
|
+
" requirementTree: policy,",
|
|
1144
|
+
" customRequirementHandlers: {",
|
|
1145
|
+
" stessoReparto: async (ctx) => ctx.userCode === Number(ctx.req.query.reparto),",
|
|
1146
|
+
" },",
|
|
1147
|
+
"}), handler);"
|
|
1148
|
+
].join("\n"), "Policy composte"),
|
|
1149
|
+
bad("ts", [
|
|
1150
|
+
"// Sbagliato: AND/OR senza figli -> errore di configurazione (500 AUTH_REQUIREMENTS_MISCONFIGURED)",
|
|
1151
|
+
"const empty = r.and();"
|
|
1152
|
+
].join("\n"), "Configurazione non valida"),
|
|
1153
|
+
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.`)
|
|
1154
|
+
]
|
|
1155
|
+
},
|
|
1156
|
+
// -------------------------------------------------------------------------
|
|
1157
|
+
{
|
|
1158
|
+
id: "utenti",
|
|
1159
|
+
group: "Dominio",
|
|
1160
|
+
title: "Utenti",
|
|
1161
|
+
summary: "Creazione, aggiornamento, stati, GDPR e filtri.",
|
|
1162
|
+
blocks: [
|
|
1163
|
+
h(`Stati di registrazione (STAREG)`),
|
|
1164
|
+
table(
|
|
1165
|
+
["Valore", "Nome", "Effetto sul login"],
|
|
1166
|
+
[
|
|
1167
|
+
["0", "NULL", "Non confermato"],
|
|
1168
|
+
["5", "INSERT", "Non confermato"],
|
|
1169
|
+
["10", "INVIO", "Richiede il rinnovo della password"],
|
|
1170
|
+
["20", "CONF", "Valido"],
|
|
1171
|
+
["50", "DELETE", "Bloccato (soft delete)"],
|
|
1172
|
+
["99", "BLOCC", "Bloccato"]
|
|
1173
|
+
]
|
|
1174
|
+
),
|
|
1175
|
+
h(`Endpoint principali (superutente salvo diversamente indicato)`),
|
|
1176
|
+
table(
|
|
1177
|
+
["Metodo", "Percorso", "Descrizione"],
|
|
1178
|
+
[
|
|
1179
|
+
["GET", "/api/accessi/user/get-users", "Elenco con paginazione (limit e offset, header X-Total-Count)."],
|
|
1180
|
+
["POST", "/api/accessi/user/create-managed-user", "Crea un utente locale e invia il reset password."],
|
|
1181
|
+
["PUT", "/api/accessi/user/update-user/:codiceUtente", "Aggiorna profilo e policy (campi privilegiati solo superutente)."],
|
|
1182
|
+
["DELETE", "/api/accessi/user/delete-user/:codiceUtente", "Soft delete: STAREG=DELETE."],
|
|
1183
|
+
["POST", "/api/accessi/user/set-stato", "Imposta lo stato di registrazione."],
|
|
1184
|
+
["PATCH", "/api/accessi/user/set-gdpr/:codiceUtente", "Registra il consenso GDPR."],
|
|
1185
|
+
["POST", "/api/accessi/user/register", "Registrazione pubblica, solo se publicRegistration.enabled."]
|
|
1186
|
+
]
|
|
1187
|
+
),
|
|
1188
|
+
good("ts", [
|
|
1189
|
+
"await fetch('/api/accessi/user/create-managed-user', {",
|
|
1190
|
+
" method: 'POST',",
|
|
1191
|
+
" headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + adminToken },",
|
|
1192
|
+
" body: JSON.stringify({",
|
|
1193
|
+
" email: 'nuovo@example.com',",
|
|
1194
|
+
" nome: 'Mario',",
|
|
1195
|
+
" cognome: 'Rossi',",
|
|
1196
|
+
" roles: [1, 2],",
|
|
1197
|
+
" permissions: [{ codiceMenu: 'REPORT', tipoAbilitazione: 10 }],",
|
|
1198
|
+
" }),",
|
|
1199
|
+
"});"
|
|
1200
|
+
].join("\n"), "Creazione di un utente gestito"),
|
|
1201
|
+
bad("ts", [
|
|
1202
|
+
"// Sbagliato: inviare flag privilegiati dal frontend pubblico",
|
|
1203
|
+
'fetch("/api/accessi/user/register", {',
|
|
1204
|
+
' method: "POST",',
|
|
1205
|
+
" body: JSON.stringify({ email, flagSuper: true }), // ignorato o negato",
|
|
1206
|
+
"});"
|
|
1207
|
+
].join("\n"), "Privilegi dal client"),
|
|
1208
|
+
note("info", `Campi di dominio`, `UTENTI_CONFIG contiene i campi di dominio Accessi: FLGSUPER, FLGADMIN, FLG2FATT (2FA), FLGPWDLESS (passwordless), FLGPASSWORD (login locale), CODLINGUA, CELLULARE, PAGDEF (pagina di default) e JSON_METADATA. I flag/colonne applicativi (FLGMOP, FLGPIANA, FLGADDETTI, FLGOSPITI, FLGPIANARFID, FLGCONTA, FLGCUBI, FLGCICLPASS, FLGDIPENDENTI, FLGINVENTARI, CAUMOV, NUMMAC, RAGSOCCLI, ENABLEIA, CELLUTE e NOMECAMPO) sono stati rimossi: i backend devono gestirli in tabelle proprie. Il telefono canonico e UTENTI_CONFIG.CELLULARE.`),
|
|
1209
|
+
note("info", `Autorizzazioni`, `I gruppi/menu si abilitano tramite ABILITAZIONI, RUOLI_MNU e UTENTI_RUOLI. Il vecchio meccanismo NOMECAMPO (flag applicativo su UTENTI_CONFIG) e stato rimosso.`),
|
|
1210
|
+
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.`)
|
|
1211
|
+
]
|
|
1212
|
+
},
|
|
1213
|
+
// -------------------------------------------------------------------------
|
|
1214
|
+
{
|
|
1215
|
+
id: "ruoli",
|
|
1216
|
+
group: "Dominio",
|
|
1217
|
+
title: "Ruoli e permessi",
|
|
1218
|
+
summary: "Assegnazioni sostitutive e composizione dei grant.",
|
|
1219
|
+
blocks: [
|
|
1220
|
+
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.`),
|
|
1221
|
+
table(
|
|
1222
|
+
["Metodo", "Percorso", "Descrizione"],
|
|
1223
|
+
[
|
|
1224
|
+
["GET", "/api/accessi/permission/roles", "Ruoli con i menu associati."],
|
|
1225
|
+
["POST", "/api/accessi/permission/create-role", "Crea un ruolo (i menu sono obbligatori)."],
|
|
1226
|
+
["PUT", "/api/accessi/permission/update-role/:codiceRuolo", "Aggiorna un ruolo (menu sostitutivi)."],
|
|
1227
|
+
["DELETE", "/api/accessi/permission/delete-role/:codiceRuolo", "Elimina ruolo, mapping e assegnazioni."],
|
|
1228
|
+
["POST", "/api/accessi/permission/assign-roles/:codiceUtente", `Sostituisce i ruoli dell'utente.`],
|
|
1229
|
+
["POST", "/api/accessi/permission/assign-permissions/:codiceUtente", "Sostituisce i permessi diretti."],
|
|
1230
|
+
["GET", "/api/accessi/permission/grants/:codiceUtente", "Grant effettivi, permessi diretti e ruoli."]
|
|
1231
|
+
]
|
|
1232
|
+
),
|
|
1233
|
+
good("ts", [
|
|
1234
|
+
"const role = {",
|
|
1235
|
+
" descrizioneRuolo: 'Gestore ordini',",
|
|
1236
|
+
" menu: [",
|
|
1237
|
+
" { codiceMenu: 'ORDINI', tipoAbilitazione: 20 },",
|
|
1238
|
+
" { codiceMenu: 'CLIENTI', tipoAbilitazione: 10 },",
|
|
1239
|
+
" ],",
|
|
1240
|
+
"};",
|
|
1241
|
+
"await createRole(role); // POST /api/accessi/permission/create-role",
|
|
1242
|
+
"",
|
|
1243
|
+
"// Sostitutivo: questa lista diventa l'intero set di ruoli dell'utente",
|
|
1244
|
+
"await assignRolesToUser(codiceUtente, { roles: [3, 7] });"
|
|
1245
|
+
].join("\n"), "Creazione di un ruolo e assegnazione"),
|
|
1246
|
+
bad("ts", [
|
|
1247
|
+
"// Sbagliato: pensare che l'assegnazione sia incrementale",
|
|
1248
|
+
"await assignRolesToUser(codiceUtente, { roles: [3] });",
|
|
1249
|
+
"// ora l'utente ha SOLO il ruolo 3, non 3 pi\xF9 quelli di prima"
|
|
1250
|
+
].join("\n"), "Assegnazione non additiva"),
|
|
1251
|
+
note("warn", `Menu obbligatori`, `create-role e update-role rifiutano un ruolo senza menu. Se ti serve un ruolo senza accessi, non crearlo.`)
|
|
1252
|
+
]
|
|
1253
|
+
},
|
|
1254
|
+
// -------------------------------------------------------------------------
|
|
1255
|
+
{
|
|
1256
|
+
id: "menu",
|
|
1257
|
+
group: "Dominio",
|
|
1258
|
+
title: "Menu, gruppi e tipi menu",
|
|
1259
|
+
summary: "Catalogo di navigazione e CRUD completo (superutente).",
|
|
1260
|
+
blocks: [
|
|
1261
|
+
table(
|
|
1262
|
+
["Metodo", "Percorso", "Descrizione"],
|
|
1263
|
+
[
|
|
1264
|
+
["GET", "/api/accessi/permission/menus", "Menu attivi."],
|
|
1265
|
+
["GET", "/api/accessi/permission/menu-types", "Catalogo dei tipi menu."],
|
|
1266
|
+
["GET", "/api/accessi/permission/groups-with-menus?includeDisabled=true", "Gruppi con menu (anche disabilitati)."],
|
|
1267
|
+
["POST", "/api/accessi/permission/menus", "Crea un menu."],
|
|
1268
|
+
["PUT", "/api/accessi/permission/menus/:codiceMenu", "Aggiorna un menu."],
|
|
1269
|
+
["DELETE", "/api/accessi/permission/menus/:codiceMenu", "Elimina un menu (con grant e associazioni ai ruoli)."],
|
|
1270
|
+
["POST", "/api/accessi/permission/menu-groups", "Crea un gruppo."],
|
|
1271
|
+
["PUT", "/api/accessi/permission/menu-groups/:codiceGruppo", "Aggiorna un gruppo."],
|
|
1272
|
+
["DELETE", "/api/accessi/permission/menu-groups/:codiceGruppo", "Elimina un gruppo (solo se vuoto)."],
|
|
1273
|
+
["POST/PUT/DELETE", "/api/accessi/permission/menu-types[/:codiceTipo]", "CRUD dei tipi menu."]
|
|
1274
|
+
]
|
|
1275
|
+
),
|
|
1276
|
+
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.`),
|
|
1277
|
+
good("ts", [
|
|
1278
|
+
"await createMenuGroup({ codiceGruppo: 'A', descrizioneGruppo: 'Amministrazione', ordineGruppo: 1, enabled: true });",
|
|
1279
|
+
"await createMenuType({ codiceTipo: 'X', descrizioneTipo: 'Operativo' });",
|
|
1280
|
+
"await createMenu({ codiceMenu: 'ORDINI', descrizioneMenu: 'Ordini', codiceGruppo: 'A', tipo: 'X', ordineMenu: 10, enabled: true });"
|
|
1281
|
+
].join("\n"), "Catalogo in ordine"),
|
|
1282
|
+
bad("ts", [
|
|
1283
|
+
"// Sbagliato: eliminare un gruppo che contiene menu -> 400 con spiegazione",
|
|
1284
|
+
"await deleteMenuGroup('A'); // contiene N menu"
|
|
1285
|
+
].join("\n"), "Vincoli di integrit\xE0")
|
|
1286
|
+
]
|
|
1287
|
+
},
|
|
1288
|
+
// -------------------------------------------------------------------------
|
|
1289
|
+
{
|
|
1290
|
+
id: "filtri",
|
|
1291
|
+
group: "Dominio",
|
|
1292
|
+
title: "Filtri applicativi",
|
|
1293
|
+
summary: "Filtri per utente e catalogo dei tipi filtro.",
|
|
1294
|
+
blocks: [
|
|
1295
|
+
table(
|
|
1296
|
+
["Metodo", "Percorso", "Descrizione"],
|
|
1297
|
+
[
|
|
1298
|
+
["GET", "/api/accessi/filtri/tipi", "Catalogo dei tipi filtro."],
|
|
1299
|
+
["POST", "/api/accessi/filtri/tipi", "Crea un tipo filtro (superutente)."],
|
|
1300
|
+
["PUT", "/api/accessi/filtri/tipi/:tipFil", "Aggiorna un tipo filtro."],
|
|
1301
|
+
["DELETE", "/api/accessi/filtri/tipi/:tipFil", "Elimina un tipo filtro (se non \xE8 usato)."],
|
|
1302
|
+
["GET", "/api/accessi/filtri/utente?codUte=123", "Filtri di un utente (self o superutente)."],
|
|
1303
|
+
["POST", "/api/accessi/filtri/utente", "Upsert dei filtri (self o superutente)."]
|
|
1304
|
+
]
|
|
1305
|
+
),
|
|
1306
|
+
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.`)
|
|
1307
|
+
]
|
|
1308
|
+
},
|
|
1309
|
+
// -------------------------------------------------------------------------
|
|
1310
|
+
{
|
|
1311
|
+
id: "sso",
|
|
1312
|
+
group: "SSO",
|
|
1313
|
+
title: "SSO generico (federated authentication)",
|
|
1314
|
+
summary: "Il backend valida il provider, Accessi gestisce identit\xE0 e sessioni.",
|
|
1315
|
+
blocks: [
|
|
1316
|
+
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.`),
|
|
1317
|
+
good("ts", [
|
|
1318
|
+
"import { FederatedAuthService } from 'emilsoftware-utilities';",
|
|
1319
|
+
"",
|
|
1320
|
+
"// Nel tuo callback SSO, dopo aver validato il token del provider:",
|
|
1321
|
+
'const verified = { provider: "azure-ad-acme", subject: claims.oid };',
|
|
1322
|
+
"const result = await federatedAuthService.authenticate(verified);",
|
|
1323
|
+
"",
|
|
1324
|
+
"if (result.login.challenge) {",
|
|
1325
|
+
" // 2FA attiva: restituisci il challenge al frontend",
|
|
1326
|
+
" return res.json({ challenge: result.login.challenge });",
|
|
1327
|
+
"}",
|
|
1328
|
+
"return res.json({ login: result.login, token: result.token });"
|
|
1329
|
+
].join("\n"), "Login SSO"),
|
|
1330
|
+
h(`API amministrative (superutente)`),
|
|
1331
|
+
list([
|
|
1332
|
+
`GET/POST /api/accessi/federated-auth/providers, PATCH/DELETE providers/:provider`,
|
|
1333
|
+
`GET/POST /api/accessi/federated-auth/users/:codiceUtente/identities`,
|
|
1334
|
+
`POST /api/accessi/federated-auth/users (crea un utente SSO-only)`,
|
|
1335
|
+
`PATCH /api/accessi/federated-auth/users/:codiceUtente/password-login`
|
|
1336
|
+
]),
|
|
1337
|
+
bad("ts", [
|
|
1338
|
+
"// Sbagliato: far validare il token del provider ad Accessi o fidarsi del frontend",
|
|
1339
|
+
"await federatedAuthService.authenticate({ provider, subject: req.body.email }); // subject non verificato"
|
|
1340
|
+
].join("\n"), `Non fidarti dell'input`),
|
|
1341
|
+
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.`)
|
|
1342
|
+
]
|
|
1343
|
+
},
|
|
1344
|
+
// -------------------------------------------------------------------------
|
|
1345
|
+
{
|
|
1346
|
+
id: "service-token",
|
|
1347
|
+
group: "Token di servizio",
|
|
1348
|
+
title: "Token macchina-a-macchina",
|
|
1349
|
+
summary: "Autenticazione tecnica con scope: non \xE8 un superutente.",
|
|
1350
|
+
blocks: [
|
|
1351
|
+
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.`),
|
|
1352
|
+
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.`),
|
|
1353
|
+
good("ts", [
|
|
1354
|
+
"import { ServiceTokenGuard, RequireServiceTokenScopes } from 'emilsoftware-utilities';",
|
|
1355
|
+
"",
|
|
1356
|
+
"@UseGuards(ServiceTokenGuard)",
|
|
1357
|
+
"@RequireServiceTokenScopes('ia')",
|
|
1358
|
+
'@Post("internal/ask")',
|
|
1359
|
+
"ask() { /* req.accessiServiceToken = { tokenId, label, scopes } */ }"
|
|
1360
|
+
].join("\n"), "Rotta tecnica protetta"),
|
|
1361
|
+
h(`Gestione (superutente)`),
|
|
1362
|
+
list([
|
|
1363
|
+
`POST /api/accessi/service-token (label, scopes, expiresAt o ttlDays) -> segreto mostrato una volta`,
|
|
1364
|
+
`GET /api/accessi/service-token?includeRevoked=true`,
|
|
1365
|
+
`DELETE /api/accessi/service-token/:tokenId (revoca immediata)`,
|
|
1366
|
+
`POST /api/accessi/service-token/:tokenId/rotate`
|
|
1367
|
+
]),
|
|
1368
|
+
bad("ts", [
|
|
1369
|
+
"// Sbagliato: usare un service token per autenticare un utente umano",
|
|
1370
|
+
"fetch('/api/profilo', { headers: { Authorization: 'Bearer ' + serviceToken } }); // req.user assente"
|
|
1371
|
+
].join("\n"), "Non \xE8 un utente"),
|
|
1372
|
+
note("warn", `Scadenza`, `Senza expiresAt n\xE9 defaultTtlDays il token non scade: imposta serviceTokens.defaultTtlDays o una scadenza esplicita.`)
|
|
1373
|
+
]
|
|
1374
|
+
},
|
|
1375
|
+
// -------------------------------------------------------------------------
|
|
1376
|
+
{
|
|
1377
|
+
id: "console",
|
|
1378
|
+
group: "Console",
|
|
1379
|
+
title: "Console amministrativa",
|
|
1380
|
+
summary: "Accesso, sezioni e che cosa si pu\xF2 fare.",
|
|
1381
|
+
blocks: [
|
|
1382
|
+
p(`La console \xE8 servita dal modulo su /api/accessi/console. Non ha stato proprio: ogni azione chiama le API protette con il JWT dell'utente connesso.`),
|
|
1383
|
+
list([
|
|
1384
|
+
`Utenti: elenco, creazione locale o SSO, profilo, ruoli, grant diretti, stato, 2FA.`,
|
|
1385
|
+
`Ruoli e grant: CRUD dei ruoli con l'albero delle abilitazioni.`,
|
|
1386
|
+
`Menu e gruppi: CRUD di gruppi, menu e tipi menu.`,
|
|
1387
|
+
`Filtri: filtri utente e tipi filtro.`,
|
|
1388
|
+
`Token di servizio: creazione, rotazione, revoca.`,
|
|
1389
|
+
`SSO: provider e identit\xE0.`,
|
|
1390
|
+
`Wiki e Changelog: documentazione e novit\xE0 della libreria.`,
|
|
1391
|
+
`SQL: esporta il DDL delle entit\xE0 Accessi (tabelle, generatori, FK, indici, check, trigger) per replicare il database.`
|
|
1392
|
+
]),
|
|
1393
|
+
note("info", `Superutente (FLGSUPER)`, `Riceve TUTTE le abilitazioni al livello massimo (30) dopo il login. In console vede solo la sezione Utenti e gestisce utenti, ruoli e grant.`),
|
|
1394
|
+
note("info", `Admin (FLGADMIN)`, `Accede a tutta la console (catalogo, token, SSO, utenti) ma NON conferisce abilitazioni: i menu restano quelli assegnati via ABILITAZIONI/RUOLI_MNU.`),
|
|
1395
|
+
note("info", `Accesso`, `Apri /api/accessi/console e accedi con un utente con FLGSUPER o FLGADMIN. Le sezioni sono raggiungibili anche tramite deep link (per esempio /api/accessi/console/menu-e-gruppi/menu).`),
|
|
1396
|
+
note("info", `Bootstrap admin`, `Con adminBootstrap.enabled il backend stampa a ogni avvio un token casuale (monouso, valido fino al riavvio). Nella schermata di login della console trovi "Crea utente admin": incolla il token e crea il primo admin senza email di conferma. La password \xE8 opzionale: se assente viene generata e mostrata una sola volta.`)
|
|
1397
|
+
]
|
|
1398
|
+
},
|
|
1399
|
+
// -------------------------------------------------------------------------
|
|
1400
|
+
{
|
|
1401
|
+
id: "errori",
|
|
1402
|
+
group: "Riferimenti",
|
|
1403
|
+
title: "Contratto errori e codici",
|
|
1404
|
+
summary: "Formato della risposta e codici stabili ACCESSI_* e AUTH_*.",
|
|
1405
|
+
blocks: [
|
|
1406
|
+
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.`),
|
|
1407
|
+
code("json", `{
|
|
1408
|
+
"severity": "error",
|
|
1409
|
+
"status": 400,
|
|
1410
|
+
"statusCode": 2,
|
|
1411
|
+
"code": "ACCESSI_VALIDATION_ERROR",
|
|
1412
|
+
"error": "ACCESSI_VALIDATION_ERROR",
|
|
1413
|
+
"message": "La richiesta contiene dati non validi.",
|
|
1414
|
+
"details": ["email: deve essere una stringa."]
|
|
1415
|
+
}`, "Errore di validazione"),
|
|
1416
|
+
table(
|
|
1417
|
+
["Codice", "Quando"],
|
|
1418
|
+
[
|
|
1419
|
+
["ACCESSI_VALIDATION_ERROR", "Body o parametri non validi (400)."],
|
|
1420
|
+
["ACCESSI_AUTH_INITIALIZING", "Bootstrap in corso (503)."],
|
|
1421
|
+
["ACCESSI_AUTH_NOT_INITIALIZED", "Modulo non inizializzato (500)."],
|
|
1422
|
+
["AUTH_HEADER_MISSING / AUTH_TOKEN_MISSING", "Header Authorization assente (401)."],
|
|
1423
|
+
["AUTH_TOKEN_INVALID", "JWT non valido o scaduto (401)."],
|
|
1424
|
+
["AUTH_USER_DISABLED", "Utente non pi\xF9 autorizzato (401)."],
|
|
1425
|
+
["AUTH_INSUFFICIENT_PERMISSIONS", "Requisiti non soddisfatti (403)."],
|
|
1426
|
+
["AUTH_REQUIREMENTS_MISCONFIGURED", "Policy malformata o handler custom mancante (500)."],
|
|
1427
|
+
["ACCESSI_DATABASE_SCHEMA_OUTDATED", "Schema del database incompatibile."],
|
|
1428
|
+
["ACCESSI_SERVICE_TOKEN_RATE_LIMITED", "Troppi tentativi (429)."]
|
|
1429
|
+
]
|
|
1430
|
+
)
|
|
1431
|
+
]
|
|
1432
|
+
},
|
|
1433
|
+
// -------------------------------------------------------------------------
|
|
1434
|
+
{
|
|
1435
|
+
id: "endpoint",
|
|
1436
|
+
group: "Riferimenti",
|
|
1437
|
+
title: "Indice degli endpoint",
|
|
1438
|
+
summary: "Tutte le rotte /api/accessi/* in una tabella.",
|
|
1439
|
+
blocks: [
|
|
1440
|
+
table(
|
|
1441
|
+
["Area", "Endpoint principali"],
|
|
1442
|
+
[
|
|
1443
|
+
["Auth", "POST auth/login, auth/get-user-by-token, auth/confirm-reset-password/:token, auth/two-factor/verify, auth/two-factor/resend"],
|
|
1444
|
+
["Email", "POST email/send-reset-password-email, GET email/reset-password-page/:token"],
|
|
1445
|
+
["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"],
|
|
1446
|
+
["Permessi", "GET permission/roles, POST permission/create-role, PUT permission/update-role/:codiceRuolo, DELETE permission/delete-role/:codiceRuolo, POST permission/assign-roles/:codiceUtente, POST permission/assign-permissions/:codiceUtente, GET permission/grants/:codiceUtente, GET permission/menus, GET permission/menu-types, GET permission/groups-with-menus"],
|
|
1447
|
+
["Catalogo", "POST/PUT/DELETE permission/menus[/:codiceMenu], permission/menu-groups[/:codiceGruppo], permission/menu-types[/:codiceTipo]"],
|
|
1448
|
+
["Configurator", "PATCH configurator/menus/:codiceMenu/enabled, PATCH configurator/groups/:codiceGruppo/enabled"],
|
|
1449
|
+
["Filtri", "GET filtri/tipi, POST/PUT/DELETE filtri/tipi[/:tipFil], GET/POST filtri/utente"],
|
|
1450
|
+
["SSO", "GET/POST federated-auth/providers, PATCH/DELETE providers/:provider, GET/POST users/:codiceUtente/identities, POST users, PATCH users/:codiceUtente/password-login"],
|
|
1451
|
+
["Token", "POST/GET service-token, DELETE service-token/:tokenId, POST service-token/:tokenId/rotate"],
|
|
1452
|
+
["Schema", "GET schema (DDL completo + entit\xE0 presenti, solo admin)"],
|
|
1453
|
+
["Console", "GET console, console/:view, console/:view/:sub, console/assets/:file"]
|
|
1454
|
+
]
|
|
1455
|
+
)
|
|
1456
|
+
]
|
|
1457
|
+
},
|
|
1458
|
+
// -------------------------------------------------------------------------
|
|
1459
|
+
{
|
|
1460
|
+
id: "antipattern",
|
|
1461
|
+
group: "Riferimenti",
|
|
1462
|
+
title: "Antipattern e checklist",
|
|
1463
|
+
summary: "Che cosa non fare, in breve.",
|
|
1464
|
+
blocks: [
|
|
1465
|
+
list([
|
|
1466
|
+
`Non aprire la porta HTTP prima di initializeAccessiModule.`,
|
|
1467
|
+
`Non usare authorizeAccessi come unico strato: valida sempre anche l'input applicativo.`,
|
|
1468
|
+
`Non dare per scontato che req.userGrants esista se non hai richiesto dei requisiti.`,
|
|
1469
|
+
`Non considerare incrementali assignRolesToUser e assignPermissionsToUser: sono sostitutivi.`,
|
|
1470
|
+
`Non far validare i token SSO ad Accessi: passagli solo { provider, subject } verificati.`,
|
|
1471
|
+
`Non usare un service token come identit\xE0 utente.`,
|
|
1472
|
+
`Non versionare segreti; jwtOptions.secret deve essere unico per ambiente.`,
|
|
1473
|
+
`Non eliminare gruppi o tipi in uso: gestisci l'errore 400 e disabilita invece.`,
|
|
1474
|
+
`Non usare il challenge 2FA come JWT.`
|
|
1475
|
+
]),
|
|
1476
|
+
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.`)
|
|
1477
|
+
]
|
|
1478
|
+
}
|
|
1479
|
+
];
|
|
1480
|
+
function wikiGroups() {
|
|
1481
|
+
const groups = [];
|
|
1482
|
+
for (const section of WIKI_SECTIONS) {
|
|
1483
|
+
let bucket = groups.find((item) => item.group === section.group);
|
|
1484
|
+
if (!bucket) {
|
|
1485
|
+
bucket = { group: section.group, sections: [] };
|
|
1486
|
+
groups.push(bucket);
|
|
1487
|
+
}
|
|
1488
|
+
bucket.sections.push(section);
|
|
1489
|
+
}
|
|
1490
|
+
return groups;
|
|
1491
|
+
}
|
|
1492
|
+
function buildAiDigest() {
|
|
1493
|
+
const lines = [
|
|
1494
|
+
`# Accessi (emilsoftware-utilities) - guida compatta per l'integrazione backend`,
|
|
1495
|
+
"",
|
|
1496
|
+
`Modulo NestJS/Express + Firebird per autenticazione JWT, autorizzazione per menu, utenti, 2FA e passwordless, SSO generico, service token e console.`,
|
|
1497
|
+
""
|
|
1498
|
+
];
|
|
1499
|
+
for (const { group, sections } of wikiGroups()) {
|
|
1500
|
+
lines.push(`## ${group}`);
|
|
1501
|
+
for (const section of sections) {
|
|
1502
|
+
lines.push(`### ${section.title}`);
|
|
1503
|
+
lines.push(section.summary);
|
|
1504
|
+
for (const block of section.blocks) {
|
|
1505
|
+
switch (block.kind) {
|
|
1506
|
+
case "p":
|
|
1507
|
+
lines.push(block.text);
|
|
1508
|
+
break;
|
|
1509
|
+
case "h":
|
|
1510
|
+
lines.push(`**${block.text}**`);
|
|
1511
|
+
break;
|
|
1512
|
+
case "list":
|
|
1513
|
+
block.items.forEach((item, index) => lines.push(`${block.ordered ? `${index + 1}.` : "-"} ${item}`));
|
|
1514
|
+
break;
|
|
1515
|
+
case "code":
|
|
1516
|
+
lines.push("```" + block.language);
|
|
1517
|
+
lines.push(block.code);
|
|
1518
|
+
lines.push("```");
|
|
1519
|
+
break;
|
|
1520
|
+
case "callout":
|
|
1521
|
+
lines.push(`[${block.tone.toUpperCase()}] ${block.title ? block.title + ": " : ""}${block.text}`);
|
|
1522
|
+
break;
|
|
1523
|
+
case "table":
|
|
1524
|
+
lines.push("| " + block.head.join(" | ") + " |");
|
|
1525
|
+
lines.push("| " + block.head.map(() => "---").join(" | ") + " |");
|
|
1526
|
+
block.rows.forEach((row) => lines.push("| " + row.join(" | ") + " |"));
|
|
1527
|
+
break;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
lines.push("");
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// src/accessi-module/Console/changelog.md
|
|
1537
|
+
var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\n\n## 2.0.0-dev.33\n\n### Cambiamenti\n- Rimosse le logiche applicative residue dallo schema Accessi:\n - `MENU_GRP.NOMECAMPO`\n - `UTENTI.ENABLEIA`\n - `UTENTI_CONFIG.PAGDEF`\n - `UTENTI.CELLUTE`\n- Telefono canonico: `UTENTI_CONFIG.CELLULARE` (allargato a `VARCHAR(30)`).\n- Migrazione automatica: copia `UTENTI.CELLUTE` \u2192 `UTENTI_CONFIG.CELLULARE`, poi `DROP` delle colonne obsolete.\n- Ruoli chiariti:\n - `FLGSUPER`: superutente, riceve **tutte** le abilitazioni al livello massimo (30) dopo il login; in console vede **solo** la sezione **Utenti**.\n - `FLGADMIN`: accede a **tutta** la console (catalogo, token, SSO, utenti) ma **non** influisce sulle abilitazioni.\n- Aggiunta la sezione **Changelog** in console (sotto la Wiki).\n\n### Note\n- `FILTRI` e `FILTRI_TIPO` restano invariati (piano di migrazione futuro).\n- I backend che usavano i campi rimossi devono gestirli in tabelle proprie.\n\n## 2.0.0-dev.32\n\n### Cambiamenti\n- Rimosso il supporto ai flag applicativi in `UTENTI_CONFIG`: `FLGMOP`, `FLGPIANA`, `FLGADDETTI`, `FLGOSPITI`, `FLGPIANARFID`, `FLGCONTA`, `FLGCUBI`, `FLGCICLPASS`, `FLGDIPENDENTI`, `FLGINVENTARI`, `CAUMOV`, `NUMMAC`, `RAGSOCCLI`.\n- Rinominata la colonna `FLGADMINCONFIG` in `FLGADMIN` (con copia dei valori e `DROP`).\n\n## 2.0.0-dev.31\n\n### Aggiunte\n- Esposizione dei servizi Accessi all'host Express (`getAccessiModuleServices`) per delegare login, SSO e token senza reimplementarli.\n\n### Correzioni\n- `UserService.getUsers`: filtri estesi (`codiceUtente[]`, `numRep[]`, `codDipendente`, `cellulare`, `tipFil`, `flagSuper`).\n\n## 2.0.0-dev.24\n\n### Cambiamenti (breaking)\n- Chiavi JWT derivate per scopo (`access`, `reset`, `2fa`) e claim `typ` obbligatorio: gli access token precedenti non sono pi\xF9 validi.\n- Endpoint Allegati protetti di default.\n- Pool di connessioni attivo di default (`poolSize` 5).\n\n### Aggiunte\n- Service token macchina-a-macchina (gestione, guard, console).\n- CRUD completo dei cataloghi dalla console Accessi.\n- Supporto Firebird 2.5 nel migrator e nei test di integrazione.\n";
|
|
1538
|
+
|
|
1539
|
+
// src/accessi-module/Console/help.ts
|
|
1540
|
+
var p2 = (text) => ({ kind: "p", text });
|
|
1541
|
+
var list2 = (items) => ({ kind: "list", items });
|
|
1542
|
+
var code2 = (language, codeText, title) => ({ kind: "code", language, title, code: codeText });
|
|
1543
|
+
var note2 = (tone, title, text) => ({ kind: "callout", tone, title, text });
|
|
1544
|
+
var table2 = (head, rows) => ({ kind: "table", head, rows });
|
|
1545
|
+
var HELP = {
|
|
1546
|
+
// ------------------------------------------------------------------ SSO
|
|
1547
|
+
"sso-provider": {
|
|
1548
|
+
title: "Provider SSO",
|
|
1549
|
+
blocks: [
|
|
1550
|
+
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 }).`),
|
|
1551
|
+
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.`),
|
|
1552
|
+
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.`)
|
|
1553
|
+
]
|
|
1554
|
+
},
|
|
1555
|
+
"sso-subject": {
|
|
1556
|
+
title: "ID utente del provider (subject)",
|
|
1557
|
+
blocks: [
|
|
1558
|
+
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.`),
|
|
1559
|
+
p2(`Ecco il claim corretto per i provider pi\xF9 comuni:`),
|
|
1560
|
+
table2(
|
|
1561
|
+
["Provider", "Claim da usare", "Note"],
|
|
1562
|
+
[
|
|
1563
|
+
["Azure AD / Entra ID", "oid", "Object ID. Non usare preferred_username n\xE9 email."],
|
|
1564
|
+
["Google", "sub", "ID numerico di 21 cifre. Non l indirizzo email."],
|
|
1565
|
+
["Apple", "sub", "Identificatore stabile per la tua app; disponibile solo al primo login, va salvato subito."],
|
|
1566
|
+
["Okta", "sub", "Claim standard OIDC."],
|
|
1567
|
+
["Auth0", "sub", "Formato auth0|... oppure google-oauth2|... ."],
|
|
1568
|
+
["Keycloak", "sub", "Claim standard OIDC."],
|
|
1569
|
+
["Microsoft personale (MSA)", "sub", "Account outlook/live."],
|
|
1570
|
+
["SAML 2.0", "NameID", "Identificatore persistente dell utente."],
|
|
1571
|
+
["OIDC generico", "sub", "Claim standard OIDC."]
|
|
1572
|
+
]
|
|
1573
|
+
),
|
|
1574
|
+
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.`),
|
|
1575
|
+
code2("ts", [
|
|
1576
|
+
"// Nel backend, dopo aver validato il token del provider:",
|
|
1577
|
+
"// Azure AD / Entra ID",
|
|
1578
|
+
"const subject = claims.oid;",
|
|
1579
|
+
"// Google / Okta / Keycloak / OIDC generico",
|
|
1580
|
+
"const subject = claims.sub;",
|
|
1581
|
+
"// Apple (disponibile al primo login, salvalo subito)",
|
|
1582
|
+
"const subject = claims.sub;",
|
|
1583
|
+
"// SAML",
|
|
1584
|
+
"const subject = assertion.nameId;"
|
|
1585
|
+
].join("\n"), "Come ricavare il subject")
|
|
1586
|
+
]
|
|
1587
|
+
},
|
|
1588
|
+
"sso-note": {
|
|
1589
|
+
title: "Nota",
|
|
1590
|
+
blocks: [
|
|
1591
|
+
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.`)
|
|
1592
|
+
]
|
|
1593
|
+
},
|
|
1594
|
+
"provider-key": {
|
|
1595
|
+
title: "Chiave provider",
|
|
1596
|
+
blocks: [
|
|
1597
|
+
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.`),
|
|
1598
|
+
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.`)
|
|
1599
|
+
]
|
|
1600
|
+
},
|
|
1601
|
+
"provider-description": {
|
|
1602
|
+
title: "Descrizione",
|
|
1603
|
+
blocks: [
|
|
1604
|
+
p2(`Nome leggibile mostrato agli amministratori (per esempio "Azure AD \u2013 Acme produzione"). Non \xE8 la chiave tecnica: pu\xF2 contenere spazi.`)
|
|
1605
|
+
]
|
|
1606
|
+
},
|
|
1607
|
+
"provider-note": {
|
|
1608
|
+
title: "Nota",
|
|
1609
|
+
blocks: [
|
|
1610
|
+
p2(`Nota interna del catalogo. Non inserire segreti, issuer, client id o dati personali: quei valori restano nel backend.`)
|
|
1611
|
+
]
|
|
1612
|
+
},
|
|
1613
|
+
"provider-active": {
|
|
1614
|
+
title: "Provider attivo",
|
|
1615
|
+
blocks: [
|
|
1616
|
+
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.`)
|
|
1617
|
+
]
|
|
1618
|
+
},
|
|
1619
|
+
// --------------------------------------------------------- service token
|
|
1620
|
+
"token-label": {
|
|
1621
|
+
title: "Descrizione del token",
|
|
1622
|
+
blocks: [
|
|
1623
|
+
p2(`Nome del token, mostrato agli amministratori (per esempio "Integrazione IA \u2013 produzione"). Non \xE8 un segreto.`)
|
|
1624
|
+
]
|
|
1625
|
+
},
|
|
1626
|
+
"token-scopes": {
|
|
1627
|
+
title: "Scope",
|
|
1628
|
+
blocks: [
|
|
1629
|
+
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.`),
|
|
1630
|
+
list2([
|
|
1631
|
+
`Formato: lettere, numeri, ':', '_' o '-', massimo 40 caratteri.`,
|
|
1632
|
+
`Esempi: ia, chat, tools, report:read.`,
|
|
1633
|
+
`Separa gli scope con una virgola.`
|
|
1634
|
+
]),
|
|
1635
|
+
code2("ts", [
|
|
1636
|
+
"@UseGuards(ServiceTokenGuard)",
|
|
1637
|
+
"@RequireServiceTokenScopes('ia')",
|
|
1638
|
+
'@Post("internal/ask")',
|
|
1639
|
+
"ask() { /* il token deve avere lo scope ia */ }"
|
|
1640
|
+
].join("\n"), "Come li usa il backend"),
|
|
1641
|
+
note2("warn", "Scope vuoto", `Senza scope il token non soddisfa nessun @RequireServiceTokenScopes, ma passa comunque le rotte protette senza scope richiesto.`)
|
|
1642
|
+
]
|
|
1643
|
+
},
|
|
1644
|
+
"token-ttl": {
|
|
1645
|
+
title: "Durata (giorni)",
|
|
1646
|
+
blocks: [
|
|
1647
|
+
p2(`Giorni di validit\xE0 del token. Alla scadenza il token smette di funzionare.`),
|
|
1648
|
+
note2("danger", "Nessuna scadenza", `Se lasci vuoto questo campo e non imposti una data, il token non scade mai: sconsigliato in produzione.`)
|
|
1649
|
+
]
|
|
1650
|
+
},
|
|
1651
|
+
// ----------------------------------------------------------------- utenti
|
|
1652
|
+
"user-email": {
|
|
1653
|
+
title: "Email",
|
|
1654
|
+
blocks: [
|
|
1655
|
+
p2(`Identificativo di accesso dell'utente e recapito per le email di reset o codici. Deve essere univoca.`)
|
|
1656
|
+
]
|
|
1657
|
+
},
|
|
1658
|
+
"user-name": {
|
|
1659
|
+
title: "Nome e cognome",
|
|
1660
|
+
blocks: [
|
|
1661
|
+
p2(`Dati anagrafici mostrati in console e restituiti nel profilo. Non incidono sull'autenticazione.`)
|
|
1662
|
+
]
|
|
1663
|
+
},
|
|
1664
|
+
"user-password-login": {
|
|
1665
|
+
title: "Login con password",
|
|
1666
|
+
blocks: [
|
|
1667
|
+
p2(`Se disattivato, il login con email e password viene rifiutato (PASSWORD_LOGIN_DISABLED). Restano validi i login con identit\xE0 SSO attive.`),
|
|
1668
|
+
note2("info", "Utente solo SSO", `\xC8 l'opzione tipica per gli utenti federati: nessuna password locale, accesso solo dal provider.`)
|
|
1669
|
+
]
|
|
1670
|
+
},
|
|
1671
|
+
"user-2fa": {
|
|
1672
|
+
title: "Codice via email (2FA)",
|
|
1673
|
+
blocks: [
|
|
1674
|
+
p2(`Richiede un codice monouso inviato per email dopo la password o dopo il provider SSO. \xC8 per-utente e disattivata per impostazione predefinita.`),
|
|
1675
|
+
list2([
|
|
1676
|
+
`Codice a 6 cifre, valido 10 minuti, 5 tentativi, massimo 3 invii.`,
|
|
1677
|
+
`Non sono token: fino alla verifica non esiste alcun JWT.`
|
|
1678
|
+
])
|
|
1679
|
+
]
|
|
1680
|
+
},
|
|
1681
|
+
"user-passwordless": {
|
|
1682
|
+
title: "Accesso con il solo codice email",
|
|
1683
|
+
blocks: [
|
|
1684
|
+
p2(`Abilita il login senza password: si inserisce solo l'email e si usa il codice ricevuto. Richiede il codice email (2FA) attivo.`),
|
|
1685
|
+
note2("warn", "Non sono due fattori", `L'accesso con il solo codice \xE8 autenticazione tramite possesso della casella email: un solo fattore.`)
|
|
1686
|
+
]
|
|
1687
|
+
},
|
|
1688
|
+
"user-default-page": {
|
|
1689
|
+
title: "Pagina di default",
|
|
1690
|
+
blocks: [
|
|
1691
|
+
p2(`Percorso o codice della pagina verso cui indirizzare l'utente dopo il login. Esempio: /dashboard oppure la chiave di una pagina gestita dal frontend.`),
|
|
1692
|
+
p2(`E una stringa libera (massimo 50 caratteri): la libreria la conserva e la restituisce nel profilo; l'instradamento effettivo lo decide il frontend.`)
|
|
1693
|
+
]
|
|
1694
|
+
},
|
|
1695
|
+
"user-state": {
|
|
1696
|
+
title: "Stato di registrazione",
|
|
1697
|
+
blocks: [
|
|
1698
|
+
table2(
|
|
1699
|
+
["Stato", "Significato"],
|
|
1700
|
+
[
|
|
1701
|
+
["Non definito / Inserito", "Utente non confermato."],
|
|
1702
|
+
["Invitato (10)", "Richiede il rinnovo della password."],
|
|
1703
|
+
["Confermato (20)", "Utente valido, pu\xF2 accedere."],
|
|
1704
|
+
["Eliminato (50)", "Soft delete: login bloccato."],
|
|
1705
|
+
["Bloccato (99)", "Login bloccato."]
|
|
1706
|
+
]
|
|
1707
|
+
)
|
|
1708
|
+
]
|
|
1709
|
+
},
|
|
1710
|
+
// ------------------------------------------------------------------ ruoli
|
|
1711
|
+
"role-description": {
|
|
1712
|
+
title: "Descrizione del ruolo",
|
|
1713
|
+
blocks: [
|
|
1714
|
+
p2(`Nome del ruolo (per esempio "Gestore ordini"). Deve avere almeno un menu selezionato: il salvataggio sostituisce integralmente i menu del ruolo.`)
|
|
1715
|
+
]
|
|
1716
|
+
},
|
|
1717
|
+
"role-level": {
|
|
1718
|
+
title: "Livello di abilitazione",
|
|
1719
|
+
blocks: [
|
|
1720
|
+
table2(
|
|
1721
|
+
["Livello", "Valore", "Uso tipico"],
|
|
1722
|
+
[
|
|
1723
|
+
["Lettura", "10", "Sola consultazione."],
|
|
1724
|
+
["Scrittura", "20", "Modifica dei dati."],
|
|
1725
|
+
["Legacy (30)", "30", "Valore storico, mantenuto senza perdita."]
|
|
1726
|
+
]
|
|
1727
|
+
),
|
|
1728
|
+
p2(`Il controllo confronta tipoAbilitazione >= soglia: un livello 20 soddisfa anche una richiesta di livello 10.`)
|
|
1729
|
+
]
|
|
1730
|
+
},
|
|
1731
|
+
// --------------------------------------------------------- catalogo menu
|
|
1732
|
+
"menu-code": {
|
|
1733
|
+
title: "Codice menu",
|
|
1734
|
+
blocks: [
|
|
1735
|
+
p2(`Chiave tecnica del menu, stabile e senza spazi (per esempio ORDINI o MNU001). \xC8 la chiave primaria: non si modifica dopo la creazione.`),
|
|
1736
|
+
note2("warn", "Immutabile", `Il codice \xE8 referenziato da ruoli e grant: per cambiarlo, elimina e ricrea il menu.`)
|
|
1737
|
+
]
|
|
1738
|
+
},
|
|
1739
|
+
"menu-group-code": {
|
|
1740
|
+
title: "Codice gruppo",
|
|
1741
|
+
blocks: [
|
|
1742
|
+
p2(`Un solo carattere (per esempio A). Raggruppa i menu nella navigazione. Immutabile dopo la creazione.`)
|
|
1743
|
+
]
|
|
1744
|
+
},
|
|
1745
|
+
"menu-type-code": {
|
|
1746
|
+
title: "Codice tipo menu",
|
|
1747
|
+
blocks: [
|
|
1748
|
+
p2(`Un solo carattere (per esempio X). Classifica i menu (amministrazione, operativo, ...). Immutabile dopo la creazione.`)
|
|
1749
|
+
]
|
|
1750
|
+
},
|
|
1751
|
+
"menu-group-order": {
|
|
1752
|
+
title: "Ordine del gruppo",
|
|
1753
|
+
blocks: [
|
|
1754
|
+
p2(`Numero che determina l'ordine dei gruppi nella navigazione: valori pi\xF9 bassi compaiono prima.`)
|
|
1755
|
+
]
|
|
1756
|
+
},
|
|
1757
|
+
"menu-order": {
|
|
1758
|
+
title: "Ordine del menu",
|
|
1759
|
+
blocks: [
|
|
1760
|
+
p2(`Numero che determina l'ordine dei menu all'interno del gruppo: valori pi\xF9 bassi compaiono prima.`)
|
|
1761
|
+
]
|
|
1762
|
+
},
|
|
1763
|
+
"menu-page": {
|
|
1764
|
+
title: "Pagina",
|
|
1765
|
+
blocks: [
|
|
1766
|
+
p2(`Percorso o nome della pagina del frontend associata al menu (per esempio /ordini). \xC8 informativo: lo usano la UI host e i client.`)
|
|
1767
|
+
]
|
|
1768
|
+
},
|
|
1769
|
+
"menu-icon": {
|
|
1770
|
+
title: "Icona",
|
|
1771
|
+
blocks: [
|
|
1772
|
+
p2(`Identificatore dell'icona usato dalla UI host (per esempio fa-box o un nome concordato). Accessi non impone un set di icone.`)
|
|
1773
|
+
]
|
|
1774
|
+
},
|
|
1775
|
+
"menu-rifmenu": {
|
|
1776
|
+
title: "Menu di riferimento",
|
|
1777
|
+
blocks: [
|
|
1778
|
+
p2(`Codice di un altro menu che fa da padre, per costruire gerarchie di menu. Lascia vuoto per un menu di primo livello.`)
|
|
1779
|
+
]
|
|
1780
|
+
},
|
|
1781
|
+
"menu-note": {
|
|
1782
|
+
title: "Nota",
|
|
1783
|
+
blocks: [
|
|
1784
|
+
p2(`Nota descrittiva mostrata agli amministratori. Non incide sui permessi.`)
|
|
1785
|
+
]
|
|
1786
|
+
},
|
|
1787
|
+
"menu-enabled": {
|
|
1788
|
+
title: "Menu abilitato",
|
|
1789
|
+
blocks: [
|
|
1790
|
+
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.`)
|
|
1791
|
+
]
|
|
1792
|
+
},
|
|
1793
|
+
"menu-group-enabled": {
|
|
1794
|
+
title: "Gruppo abilitato",
|
|
1795
|
+
blocks: [
|
|
1796
|
+
p2(`Disattiva l'intero gruppo: i menu al suo interno non sono considerati attivi, ma restano configurabili per una riattivazione futura.`)
|
|
1797
|
+
]
|
|
1798
|
+
},
|
|
1799
|
+
// ----------------------------------------------------------------- filtri
|
|
1800
|
+
"filter-type-code": {
|
|
1801
|
+
title: "Codice tipo filtro",
|
|
1802
|
+
blocks: [
|
|
1803
|
+
p2(`Numero identificativo del tipo filtro. \xC8 la chiave primaria: non si modifica dopo la creazione.`)
|
|
1804
|
+
]
|
|
1805
|
+
},
|
|
1806
|
+
"filter-type-field": {
|
|
1807
|
+
title: "Campo",
|
|
1808
|
+
blocks: [
|
|
1809
|
+
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.`)
|
|
1810
|
+
]
|
|
1811
|
+
},
|
|
1812
|
+
"filter-type-enabled": {
|
|
1813
|
+
title: "Tipo filtro abilitato",
|
|
1814
|
+
blocks: [
|
|
1815
|
+
p2(`Se disattivato, il tipo non \xE8 proposto ai client, ma i filtri gi\xE0 salvati restano nel database.`)
|
|
1816
|
+
]
|
|
1817
|
+
},
|
|
1818
|
+
"filters-json": {
|
|
1819
|
+
title: "Filtro JSON",
|
|
1820
|
+
blocks: [
|
|
1821
|
+
p2(`Rappresentazione JSON dei filtri applicativi dell'utente. Usa "Carica" per partire dalla struttura esistente, modifica i valori e poi "Salva".`),
|
|
1822
|
+
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.`)
|
|
1823
|
+
]
|
|
1824
|
+
},
|
|
1825
|
+
"filters-user": {
|
|
1826
|
+
title: "Utente",
|
|
1827
|
+
blocks: [
|
|
1828
|
+
p2(`Utente a cui appartengono i filtri. Selezionalo prima di caricare o salvare: i filtri sono sempre per singolo utente.`)
|
|
1829
|
+
]
|
|
1830
|
+
},
|
|
1831
|
+
// ------------------------------------------------------------ assegnazioni
|
|
1832
|
+
"role-assignment": {
|
|
1833
|
+
title: "Assegnazione dei ruoli",
|
|
1834
|
+
blocks: [
|
|
1835
|
+
p2(`Il salvataggio SOSTITUISCE integralmente i ruoli dell'utente: la lista selezionata diventa l'unico set di ruoli.`),
|
|
1836
|
+
list2([
|
|
1837
|
+
`Per aggiungere un ruolo, selezionalo mantenendo quelli gi\xE0 presenti.`,
|
|
1838
|
+
`Per rimuovere un ruolo, deselezionalo.`,
|
|
1839
|
+
`Non modificare i grant diretti: quelli si gestiscono nella scheda "Grant diretti".`
|
|
1840
|
+
]),
|
|
1841
|
+
note2("warn", "Non \xE8 incrementale", `Inviare solo il nuovo ruolo rimuove gli altri. Invia sempre la lista completa desiderata.`)
|
|
1842
|
+
]
|
|
1843
|
+
},
|
|
1844
|
+
"grant-direct": {
|
|
1845
|
+
title: "Grant diretti",
|
|
1846
|
+
blocks: [
|
|
1847
|
+
p2(`Permessi assegnati direttamente all'utente, indipendenti dai ruoli. Hanno precedenza sui permessi ereditati dai ruoli per lo stesso menu.`),
|
|
1848
|
+
list2([
|
|
1849
|
+
`Imposta il livello per concedere l'accesso.`,
|
|
1850
|
+
`Scegli "nessuno" per rimuovere il grant diretto.`,
|
|
1851
|
+
`Il salvataggio sostituisce l'intero set di grant diretti.`
|
|
1852
|
+
])
|
|
1853
|
+
]
|
|
1854
|
+
},
|
|
1855
|
+
"grant-level": {
|
|
1856
|
+
title: "Livello di grant",
|
|
1857
|
+
blocks: [
|
|
1858
|
+
table2(
|
|
1859
|
+
["Voce", "Valore", "Significato"],
|
|
1860
|
+
[
|
|
1861
|
+
["nessuno", "\u2014", "Nessun grant diretto per quel menu (il permesso pu\xF2 arrivare dai ruoli)."],
|
|
1862
|
+
["Lettura", "10", "Sola consultazione."],
|
|
1863
|
+
["Scrittura", "20", "Modifica dei dati."]
|
|
1864
|
+
]
|
|
1865
|
+
),
|
|
1866
|
+
p2(`Il confronto \xE8 tipoAbilitazione >= soglia richiesta dalla rotta.`)
|
|
1867
|
+
]
|
|
1868
|
+
},
|
|
1869
|
+
"user-delete": {
|
|
1870
|
+
title: "Imposta stato eliminato",
|
|
1871
|
+
blocks: [
|
|
1872
|
+
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.`),
|
|
1873
|
+
note2("info", "Reversibile", `Non \xE8 una cancellazione fisica: puoi ripristinare l'utente cambiando lo stato di registrazione.`)
|
|
1874
|
+
]
|
|
1875
|
+
},
|
|
1876
|
+
// ----------------------------------------------------- selezioni di menu
|
|
1877
|
+
"menu-group-select": {
|
|
1878
|
+
title: "Gruppo",
|
|
1879
|
+
blocks: [
|
|
1880
|
+
p2(`Gruppo di appartenenza del menu. Se l'elenco \xE8 vuoto, crea prima un gruppo in "Menu e gruppi \u2192 Gruppi".`)
|
|
1881
|
+
]
|
|
1882
|
+
},
|
|
1883
|
+
"menu-type-select": {
|
|
1884
|
+
title: "Tipo menu",
|
|
1885
|
+
blocks: [
|
|
1886
|
+
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".`)
|
|
1887
|
+
]
|
|
1888
|
+
},
|
|
1889
|
+
// ------------------------------------------------------------------ login
|
|
1890
|
+
"login-email": {
|
|
1891
|
+
title: "Email o utente",
|
|
1892
|
+
blocks: [
|
|
1893
|
+
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.`)
|
|
1894
|
+
]
|
|
1895
|
+
},
|
|
1896
|
+
"login-password": {
|
|
1897
|
+
title: "Password",
|
|
1898
|
+
blocks: [
|
|
1899
|
+
p2(`Password locale dell'utente. Non serve per gli account passwordless, che accedono con il solo codice email.`)
|
|
1900
|
+
]
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
|
|
1904
|
+
// src/accessi-module/Console/console.ts
|
|
1905
|
+
var routeSegment = {
|
|
1906
|
+
users: "utenti",
|
|
1907
|
+
roles: "ruoli-e-grant",
|
|
1908
|
+
menuGroups: "menu-e-gruppi/gruppi",
|
|
1909
|
+
menuItems: "menu-e-gruppi/menu",
|
|
1910
|
+
menuTypes: "menu-e-gruppi/tipi-menu",
|
|
1911
|
+
filterUser: "filtri/utente",
|
|
1912
|
+
filterTypes: "filtri/tipi",
|
|
1913
|
+
ssoProviders: "sso/provider",
|
|
1914
|
+
ssoUsers: "sso/utenti",
|
|
1915
|
+
tokens: "token-di-servizio",
|
|
1916
|
+
wiki: "wiki",
|
|
1917
|
+
changelog: "changelog",
|
|
1918
|
+
sql: "sql"
|
|
1919
|
+
};
|
|
1920
|
+
var routeView = Object.fromEntries(Object.entries(routeSegment).map(([view, segment]) => [segment, view]));
|
|
1921
|
+
var parentDefaultView = {
|
|
1922
|
+
"menu-e-gruppi": "menuGroups",
|
|
1923
|
+
filtri: "filterUser",
|
|
1924
|
+
sso: "ssoProviders"
|
|
1925
|
+
};
|
|
1926
|
+
var consoleMarker = "/accessi/console";
|
|
1927
|
+
var consoleMarkerIndex = window.location.pathname.indexOf(consoleMarker);
|
|
1928
|
+
var consoleBasePath = consoleMarkerIndex >= 0 ? window.location.pathname.slice(0, consoleMarkerIndex + consoleMarker.length) : `${window.location.pathname.replace(/\/$/, "")}`;
|
|
1929
|
+
var USERS_PAGE_SIZE = 25;
|
|
1930
|
+
var token = sessionStorage.getItem("accessi-console-token");
|
|
1931
|
+
var currentUser = null;
|
|
1932
|
+
var federatedAuthenticationAvailable = false;
|
|
1933
|
+
var usersPage = 0;
|
|
1934
|
+
var usersTotal = 0;
|
|
1935
|
+
var booting = true;
|
|
1936
|
+
var pendingNetworkRequests = 0;
|
|
1937
|
+
var loadingRevealTimer;
|
|
1938
|
+
var loadingHideTimer;
|
|
1939
|
+
var loaderVisibleSince = 0;
|
|
1940
|
+
var byId = (id) => document.getElementById(id);
|
|
1941
|
+
var escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
|
1942
|
+
"&": "&",
|
|
1943
|
+
"<": "<",
|
|
1944
|
+
">": ">",
|
|
1945
|
+
'"': """,
|
|
1946
|
+
"'": "'"
|
|
1947
|
+
})[char]);
|
|
1948
|
+
function result(response) {
|
|
1949
|
+
const body = response.data;
|
|
1950
|
+
if (body.Result === void 0) throw new Error(body.message ?? "Risposta Accessi priva di dati.");
|
|
1951
|
+
return body.Result;
|
|
1952
|
+
}
|
|
1953
|
+
function showNotice(message, isError = false) {
|
|
1954
|
+
byId("notice").innerHTML = message ? `<span class="${isError ? "error" : ""}">${escapeHtml(message)}</span>` : "";
|
|
1955
|
+
}
|
|
1956
|
+
function menuMetadata(menu) {
|
|
1957
|
+
const details = [
|
|
1958
|
+
["Codice", menu.codiceMenu],
|
|
1959
|
+
["Ordine", String(menu.ordineMenu)],
|
|
1960
|
+
["Tipo", menu.tipo],
|
|
1961
|
+
["Pagina", menu.pagina],
|
|
1962
|
+
["Icona", menu.icona],
|
|
1963
|
+
["Stato", menu.enabled === false ? "Disabilitato" : "Abilitato"]
|
|
1964
|
+
].filter(([, value]) => value);
|
|
1965
|
+
return `<dl class="entity-metadata">${details.map(([label, value]) => `<div><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`).join("")}</dl>${menu.note ? `<p class="entity-note"><strong>Nota</strong>${escapeHtml(menu.note)}</p>` : ""}`;
|
|
1966
|
+
}
|
|
1967
|
+
function permissionSelectOptions(currentLevel) {
|
|
1968
|
+
return `<option value="10" ${currentLevel === 10 ? "selected" : ""}>Lettura</option><option value="20" ${currentLevel === 20 ? "selected" : ""}>Scrittura</option>${currentLevel === 30 ? '<option value="30" selected>Valore legacy (30)</option>' : ""}`;
|
|
1969
|
+
}
|
|
1970
|
+
function groupMetadata(group) {
|
|
1971
|
+
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>`;
|
|
1972
|
+
}
|
|
1973
|
+
function directGrantEditor(groups, grants) {
|
|
1974
|
+
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) => {
|
|
1975
|
+
const grant = (grants ?? []).find((item) => item.codiceMenu === menu.codiceMenu);
|
|
1976
|
+
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>`;
|
|
1977
|
+
}).join("") || '<p class="form-help">Nessun menu configurato in questo gruppo.</p>'}</div></fieldset>`).join("")}</div><button>Salva grant</button></form>`;
|
|
1978
|
+
}
|
|
1979
|
+
function permissionLevelLabel(value) {
|
|
1980
|
+
return value === 10 ? "Lettura" : value === 20 ? "Scrittura" : value === 30 ? "Valore legacy (30)" : `Livello ${value ?? "non definito"}`;
|
|
1981
|
+
}
|
|
1982
|
+
function rolePermissionLevel(value) {
|
|
1983
|
+
return value === void 0 || value === null || value === "" ? permissionLevelLabel(void 0) : permissionLevelLabel(Number(value));
|
|
1984
|
+
}
|
|
1985
|
+
function roleAssignmentEditor(roles, assignedRoles, groups) {
|
|
1986
|
+
const catalogMenus = new Map(groups.flatMap((group) => group.menus.map((menu) => [menu.codiceMenu, menu])));
|
|
1987
|
+
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) => {
|
|
1988
|
+
const roleMenus = role.menu ?? [];
|
|
1989
|
+
const assigned = (assignedRoles ?? []).some((current) => current.codiceRuolo === role.codiceRuolo);
|
|
1990
|
+
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);
|
|
1991
|
+
const missingMenus = roleMenus.filter((item) => !catalogMenus.has(item.codiceMenu));
|
|
1992
|
+
return `<article class="role-assignment-card"><div class="role-assignment-heading"><label class="check"><input type="checkbox" name="role" value="${escapeHtml(role.codiceRuolo)}" ${assigned ? "checked" : ""}><span><strong>${escapeHtml(role.descrizioneRuolo)}</strong><small>Codice ruolo: ${escapeHtml(role.codiceRuolo)}</small></span></label><span class="role-menu-count">${roleMenus.length} menu</span></div><div class="role-permission-groups">${groupedMenus.map(({ group, entries }) => `<section class="role-permission-group"><div class="role-permission-group-heading"><strong>${escapeHtml(group.descrizioneGruppo)}</strong>${groupMetadata(group)}</div>${entries.map(({ menu, assignment }) => `<div class="role-permission-entry ${menu.enabled === false ? "is-disabled" : ""}"><div><strong>${escapeHtml(menu.descrizioneMenu)}</strong>${menuMetadata(menu)}</div><span class="permission-level">${escapeHtml(rolePermissionLevel(assignment?.tipoAbilitazione))}</span></div>`).join("")}</section>`).join("") || '<p class="form-help">Questo ruolo non contiene menu attivi nel catalogo.</p>'}${missingMenus.length ? `<section class="role-permission-group missing-role-menus"><strong>Menu non pi\xC3\xB9 presenti nel catalogo</strong>${missingMenus.map((menu) => `<div class="role-permission-entry"><div><strong>${escapeHtml(menu.codiceMenu)}</strong><p class="entity-note">Il menu \xC3\xA8 ancora associato al ruolo ma non \xC3\xA8 disponibile nel catalogo corrente.</p></div><span class="permission-level">${escapeHtml(rolePermissionLevel(menu.tipoAbilitazione))}</span></div>`).join("")}</section>` : ""}</div></article>`;
|
|
1993
|
+
}).join("")}</div><button>Salva ruoli</button></form>`;
|
|
1994
|
+
}
|
|
1995
|
+
function roleOverview(role, groups) {
|
|
1996
|
+
const roleMenus = role.menu ?? [];
|
|
1997
|
+
const assignments = new Map(roleMenus.map((item) => [item.codiceMenu, item]));
|
|
1998
|
+
const knownMenus = new Set(groups.flatMap((group) => group.menus.map((menu) => menu.codiceMenu)));
|
|
1999
|
+
const tree = groups.map((group) => ({ group, entries: group.menus.filter((menu) => assignments.has(menu.codiceMenu)) })).filter((node) => node.entries.length > 0);
|
|
2000
|
+
const orphans = roleMenus.filter((item) => !knownMenus.has(item.codiceMenu));
|
|
2001
|
+
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("");
|
|
2002
|
+
const orphanNode = orphans.length ? `<li class="role-tree-group is-orphan"><div class="role-tree-group-head"><strong>Menu non pi\xC3\xB9 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>` : "";
|
|
2003
|
+
return `<article class="role-card">
|
|
2004
|
+
<div class="role-card-head">
|
|
2005
|
+
<div class="role-card-title">
|
|
2006
|
+
<strong>${escapeHtml(role.descrizioneRuolo)}</strong>
|
|
2007
|
+
<span class="role-card-meta">Codice ${escapeHtml(role.codiceRuolo)} · ${roleMenus.length} menu</span>
|
|
2008
|
+
</div>
|
|
2009
|
+
<div class="role-card-actions">
|
|
2010
|
+
<button type="button" class="secondary" data-role="${escapeHtml(role.codiceRuolo)}">Modifica</button>
|
|
2011
|
+
<button type="button" class="danger-button" data-role-delete="${escapeHtml(role.codiceRuolo)}">Elimina</button>
|
|
2012
|
+
</div>
|
|
2013
|
+
</div>
|
|
2014
|
+
${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>'}
|
|
2015
|
+
</article>`;
|
|
2016
|
+
}
|
|
2017
|
+
function roleMenuEditorTable(group, role) {
|
|
2018
|
+
const roleMenus = role?.menu ?? [];
|
|
2019
|
+
const options = group.menus.map((menu) => {
|
|
2020
|
+
const assignment = roleMenus.find((item) => item.codiceMenu === menu.codiceMenu);
|
|
2021
|
+
const level = assignment?.tipoAbilitazione ?? 10;
|
|
2022
|
+
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>`;
|
|
2023
|
+
}).join("");
|
|
2024
|
+
return `<section class="role-menu-group ${group.enabled === false ? "is-disabled" : ""}">
|
|
2025
|
+
<div class="role-menu-group-head"><strong>${escapeHtml(group.descrizioneGruppo)}</strong>${group.enabled === false ? '<span class="muted">Gruppo disabilitato</span>' : ""}</div>
|
|
2026
|
+
<ul class="role-menu-options">${options || '<li class="role-menu-empty muted">Nessun menu in questo gruppo.</li>'}</ul>
|
|
2027
|
+
</section>`;
|
|
2028
|
+
}
|
|
2029
|
+
function updateLoadingState(active) {
|
|
2030
|
+
pendingNetworkRequests = Math.max(0, pendingNetworkRequests + (active ? 1 : -1));
|
|
2031
|
+
if (booting) {
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
const overlay = byId("loading-overlay");
|
|
2035
|
+
if (pendingNetworkRequests > 0) {
|
|
2036
|
+
if (loadingHideTimer !== void 0) {
|
|
2037
|
+
window.clearTimeout(loadingHideTimer);
|
|
2038
|
+
loadingHideTimer = void 0;
|
|
2039
|
+
}
|
|
2040
|
+
if (loadingRevealTimer === void 0 && overlay.hidden) {
|
|
2041
|
+
loadingRevealTimer = window.setTimeout(() => {
|
|
2042
|
+
loadingRevealTimer = void 0;
|
|
2043
|
+
if (pendingNetworkRequests > 0) {
|
|
2044
|
+
overlay.hidden = false;
|
|
2045
|
+
loaderVisibleSince = Date.now();
|
|
2046
|
+
byId("app").setAttribute("aria-busy", "true");
|
|
2047
|
+
}
|
|
2048
|
+
}, 140);
|
|
2049
|
+
}
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
if (loadingRevealTimer !== void 0) {
|
|
2053
|
+
window.clearTimeout(loadingRevealTimer);
|
|
2054
|
+
loadingRevealTimer = void 0;
|
|
2055
|
+
}
|
|
2056
|
+
const hide = () => {
|
|
2057
|
+
loadingHideTimer = void 0;
|
|
2058
|
+
overlay.hidden = true;
|
|
2059
|
+
byId("app").removeAttribute("aria-busy");
|
|
2060
|
+
};
|
|
2061
|
+
const remainingVisibleMs = 280 - (Date.now() - loaderVisibleSince);
|
|
2062
|
+
if (!overlay.hidden && remainingVisibleMs > 0) {
|
|
2063
|
+
loadingHideTimer = window.setTimeout(hide, remainingVisibleMs);
|
|
2064
|
+
} else {
|
|
2065
|
+
hide();
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
function finishBoot(view) {
|
|
2069
|
+
booting = false;
|
|
2070
|
+
const overlay = byId("loading-overlay");
|
|
2071
|
+
overlay.hidden = true;
|
|
2072
|
+
byId("app").removeAttribute("aria-busy");
|
|
2073
|
+
byId("login").hidden = view !== "login";
|
|
2074
|
+
byId("console").hidden = view !== "console";
|
|
2075
|
+
}
|
|
2076
|
+
function errorMessage(error) {
|
|
2077
|
+
return error instanceof Error ? error.message : "Operazione non riuscita.";
|
|
2078
|
+
}
|
|
2079
|
+
function handleAction(action) {
|
|
2080
|
+
void action().catch((error) => showNotice(errorMessage(error), true));
|
|
2081
|
+
}
|
|
2082
|
+
function render(markup) {
|
|
2083
|
+
closeHelp(false);
|
|
2084
|
+
byId("view").innerHTML = markup;
|
|
2085
|
+
}
|
|
2086
|
+
function setActiveNavigation(view) {
|
|
2087
|
+
document.querySelectorAll("#nav [data-view]").forEach((link) => {
|
|
2088
|
+
if (link.dataset.view === view) {
|
|
2089
|
+
link.setAttribute("aria-current", "page");
|
|
2090
|
+
} else {
|
|
2091
|
+
link.removeAttribute("aria-current");
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
document.querySelectorAll("#nav details[data-nav-group]").forEach((branch) => {
|
|
2095
|
+
if (branch.querySelector(`[data-view="${view}"]`)) branch.open = true;
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
function routePath(view) {
|
|
2099
|
+
return `${consoleBasePath}/${routeSegment[view]}`;
|
|
2100
|
+
}
|
|
2101
|
+
function viewFromLocation() {
|
|
2102
|
+
const relativePath = window.location.pathname.slice(consoleBasePath.length).replace(/^\/+|\/+$/g, "");
|
|
2103
|
+
const decoded = decodeURIComponent(relativePath);
|
|
2104
|
+
return routeView[decoded] ?? parentDefaultView[decoded] ?? "users";
|
|
2105
|
+
}
|
|
2106
|
+
async function navigate(view, replace = false) {
|
|
2107
|
+
if (!isViewAllowed(view)) {
|
|
2108
|
+
view = "users";
|
|
2109
|
+
replace = true;
|
|
2110
|
+
}
|
|
2111
|
+
const destination = routePath(view);
|
|
2112
|
+
if (window.location.pathname !== destination) {
|
|
2113
|
+
window.history[replace ? "replaceState" : "pushState"]({ view }, "", destination);
|
|
2114
|
+
}
|
|
2115
|
+
if (view === "users") usersPage = 0;
|
|
2116
|
+
await show(view);
|
|
2117
|
+
}
|
|
2118
|
+
function requireConsoleAccess() {
|
|
2119
|
+
if (!currentUser?.flagSuper && !currentUser?.flagAdmin) {
|
|
2120
|
+
throw new Error("La console richiede un superutente o un admin Accessi.");
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
function isViewAllowed(view) {
|
|
2124
|
+
if (!currentUser) return false;
|
|
2125
|
+
if (currentUser.flagAdmin) return true;
|
|
2126
|
+
return view === "users";
|
|
2127
|
+
}
|
|
2128
|
+
function applyRoleNavigation() {
|
|
2129
|
+
document.querySelectorAll("#nav [data-view]").forEach((link) => {
|
|
2130
|
+
const view = link.dataset.view;
|
|
2131
|
+
const item = link.closest("li") ?? link;
|
|
2132
|
+
item.hidden = !isViewAllowed(view);
|
|
2133
|
+
});
|
|
2134
|
+
document.querySelectorAll("#nav details[data-nav-group]").forEach((branch) => {
|
|
2135
|
+
const hasVisible = Array.from(branch.querySelectorAll("[data-view]")).some((link) => !link.closest("li").hidden);
|
|
2136
|
+
branch.hidden = !hasVisible;
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
function formValues(form) {
|
|
2140
|
+
return Object.fromEntries(new FormData(form).entries());
|
|
2141
|
+
}
|
|
2142
|
+
function eventForm(event) {
|
|
2143
|
+
return event.currentTarget;
|
|
2144
|
+
}
|
|
2145
|
+
function federatedIdentityFields(providers) {
|
|
2146
|
+
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>
|
|
2147
|
+
<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>
|
|
2148
|
+
<label>Nota ${helpDot("sso-note")}<input name="note"><span class="form-help">Nota interna, non inviata al provider.</span></label>`;
|
|
2149
|
+
}
|
|
2150
|
+
function federatedIdentitySection(identities, providers) {
|
|
2151
|
+
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\xC3\xA0 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>`;
|
|
2152
|
+
}
|
|
2153
|
+
function userDetailTabs(hasSso) {
|
|
2154
|
+
const tabs = [
|
|
2155
|
+
...hasSso ? [{ id: "sso", label: "Identit\xC3\xA0 SSO" }] : [],
|
|
2156
|
+
{ id: "profile", label: "Profilo e accesso" },
|
|
2157
|
+
{ id: "roles", label: "Ruoli" },
|
|
2158
|
+
{ id: "grants", label: "Grant diretti" }
|
|
2159
|
+
];
|
|
2160
|
+
return `<div class="user-tabs" role="tablist" aria-label="Sezioni utente">${tabs.map((tab) => `<button id="user-tab-${tab.id}" type="button" role="tab" aria-controls="user-panel-${tab.id}" data-user-tab="${tab.id}">${tab.label}</button>`).join("")}</div>`;
|
|
2161
|
+
}
|
|
2162
|
+
function activateUserTab(tab) {
|
|
2163
|
+
document.querySelectorAll("[data-user-tab]").forEach((button) => {
|
|
2164
|
+
const selected = button.dataset.userTab === tab;
|
|
2165
|
+
button.setAttribute("aria-selected", String(selected));
|
|
2166
|
+
button.tabIndex = selected ? 0 : -1;
|
|
2167
|
+
});
|
|
2168
|
+
document.querySelectorAll("[data-user-panel]").forEach((panel) => {
|
|
2169
|
+
const selected = panel.dataset.userPanel === tab;
|
|
2170
|
+
panel.hidden = !selected;
|
|
2171
|
+
if (!panel.id) panel.id = `user-panel-${panel.dataset.userPanel}`;
|
|
2172
|
+
document.querySelector(`[data-user-tab="${panel.dataset.userPanel}"]`)?.setAttribute("aria-controls", panel.id);
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
function selectedNumbers(form, name) {
|
|
2176
|
+
return Array.from(new FormData(form).getAll(name), (value) => Number(value));
|
|
2177
|
+
}
|
|
2178
|
+
async function loadUsers() {
|
|
2179
|
+
return result(await getUsers({ includeGrants: true }));
|
|
2180
|
+
}
|
|
2181
|
+
async function loadUsersPage() {
|
|
2182
|
+
const response = await getUsers({
|
|
2183
|
+
includeGrants: true,
|
|
2184
|
+
limit: USERS_PAGE_SIZE,
|
|
2185
|
+
offset: usersPage * USERS_PAGE_SIZE
|
|
2186
|
+
});
|
|
2187
|
+
const users = result(response);
|
|
2188
|
+
const headers = response.headers;
|
|
2189
|
+
const totalHeader = headers?.get?.("X-Total-Count") ?? null;
|
|
2190
|
+
const parsed = totalHeader !== null && totalHeader !== "" ? Number(totalHeader) : Number.NaN;
|
|
2191
|
+
const total = Number.isFinite(parsed) ? parsed : usersPage * USERS_PAGE_SIZE + users.length;
|
|
2192
|
+
return { users, total };
|
|
2193
|
+
}
|
|
2194
|
+
function pageNumbers(current, total) {
|
|
2195
|
+
if (total <= 7) {
|
|
2196
|
+
return Array.from({ length: total }, (_, index) => index + 1);
|
|
2197
|
+
}
|
|
2198
|
+
const wanted = /* @__PURE__ */ new Set([1, total, current, current - 1, current + 1]);
|
|
2199
|
+
const sorted = Array.from(wanted).filter((page) => page >= 1 && page <= total).sort((a, b) => a - b);
|
|
2200
|
+
const result2 = [];
|
|
2201
|
+
let previous = 0;
|
|
2202
|
+
for (const page of sorted) {
|
|
2203
|
+
if (previous !== 0 && page - previous > 1) {
|
|
2204
|
+
result2.push("gap");
|
|
2205
|
+
}
|
|
2206
|
+
result2.push(page);
|
|
2207
|
+
previous = page;
|
|
2208
|
+
}
|
|
2209
|
+
return result2;
|
|
2210
|
+
}
|
|
2211
|
+
function pagerMarkup(totalPages) {
|
|
2212
|
+
const context = `<span class="pager-context">${usersTotal} utent${usersTotal === 1 ? "e" : "i"}${totalPages > 1 ? ` \xC2\xB7 pagina ${usersPage + 1} di ${totalPages}` : ""}</span>`;
|
|
2213
|
+
if (totalPages <= 1) {
|
|
2214
|
+
return `<nav class="pager" aria-label="Paginazione utenti">${context}</nav>`;
|
|
2215
|
+
}
|
|
2216
|
+
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("");
|
|
2217
|
+
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>`;
|
|
2218
|
+
return `<nav class="pager" aria-label="Paginazione utenti">${controls}${context}</nav>`;
|
|
2219
|
+
}
|
|
2220
|
+
function rolesCell(codiceUtente, userGrants) {
|
|
2221
|
+
const labels = (userGrants?.ruoli ?? []).map((role) => role.descrizioneRuolo?.trim() || (role.codiceRuolo ? `#${role.codiceRuolo}` : "")).filter((label) => label.length > 0);
|
|
2222
|
+
if (labels.length === 0) {
|
|
2223
|
+
return '<span class="muted">—</span>';
|
|
2224
|
+
}
|
|
2225
|
+
const limit = 2;
|
|
2226
|
+
if (labels.length <= limit) {
|
|
2227
|
+
return `<span class="roles-list">${labels.map(escapeHtml).join(", ")}</span>`;
|
|
2228
|
+
}
|
|
2229
|
+
const hiddenCount = labels.length - limit;
|
|
2230
|
+
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>`;
|
|
2231
|
+
}
|
|
2232
|
+
function setRolesExpanded(cell, expanded) {
|
|
2233
|
+
cell.querySelector("[data-roles-collapsed]")?.toggleAttribute("hidden", expanded);
|
|
2234
|
+
cell.querySelector("[data-roles-full]")?.toggleAttribute("hidden", !expanded);
|
|
2235
|
+
const button = cell.querySelector("[data-roles-toggle]");
|
|
2236
|
+
if (button) {
|
|
2237
|
+
button.setAttribute("aria-expanded", String(expanded));
|
|
2238
|
+
button.textContent = expanded ? "mostra meno" : `altre ${button.dataset.rolesMore ?? ""}`;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
function bindRolesToggles() {
|
|
2242
|
+
document.querySelectorAll("[data-roles-toggle]").forEach((button) => {
|
|
2243
|
+
button.onclick = () => {
|
|
2244
|
+
const cell = button.closest("[data-roles-cell]");
|
|
2245
|
+
if (!cell) return;
|
|
2246
|
+
const willExpand = button.getAttribute("aria-expanded") !== "true";
|
|
2247
|
+
document.querySelectorAll("[data-roles-cell]").forEach((other) => {
|
|
2248
|
+
if (other !== cell) setRolesExpanded(other, false);
|
|
2249
|
+
});
|
|
2250
|
+
setRolesExpanded(cell, willExpand);
|
|
2251
|
+
};
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
async function detectFederatedAuthentication() {
|
|
2255
|
+
if (!currentUser) return;
|
|
2256
|
+
try {
|
|
2257
|
+
await getFederatedIdentities(currentUser.codiceUtente);
|
|
2258
|
+
federatedAuthenticationAvailable = true;
|
|
2259
|
+
} catch {
|
|
2260
|
+
federatedAuthenticationAvailable = false;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
async function loginView(event) {
|
|
2264
|
+
event.preventDefault();
|
|
2265
|
+
try {
|
|
2266
|
+
const values = formValues(event.currentTarget);
|
|
2267
|
+
byId("login-error").textContent = "";
|
|
2268
|
+
const response = result(await login({ email: values.email ?? "", password: values.password || void 0 }));
|
|
2269
|
+
await completeLoginStep(response);
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
byId("login-error").textContent = error instanceof Error ? error.message : "Login non riuscito.";
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
var pendingChallenge = null;
|
|
2275
|
+
var resendTimer;
|
|
2276
|
+
function resetLogin() {
|
|
2277
|
+
pendingChallenge = null;
|
|
2278
|
+
if (resendTimer) clearInterval(resendTimer);
|
|
2279
|
+
byId("login-form").reset();
|
|
2280
|
+
byId("two-factor-form").reset();
|
|
2281
|
+
byId("login-form").hidden = false;
|
|
2282
|
+
byId("two-factor-form").hidden = true;
|
|
2283
|
+
byId("password-field").hidden = true;
|
|
2284
|
+
byId("password-field").querySelector("input").disabled = true;
|
|
2285
|
+
byId("restart-login").hidden = true;
|
|
2286
|
+
byId("login-submit").textContent = "Continua";
|
|
2287
|
+
byId("login-error").textContent = "";
|
|
2288
|
+
}
|
|
2289
|
+
function showTwoFactor(challenge) {
|
|
2290
|
+
pendingChallenge = challenge.challengeId;
|
|
2291
|
+
byId("login-form").hidden = true;
|
|
2292
|
+
const password = byId("password-field").querySelector("input");
|
|
2293
|
+
password.value = "";
|
|
2294
|
+
password.disabled = true;
|
|
2295
|
+
byId("two-factor-form").hidden = false;
|
|
2296
|
+
byId("restart-login").hidden = false;
|
|
2297
|
+
byId("two-factor-form").reset();
|
|
2298
|
+
byId("two-factor-form").querySelector("input").focus();
|
|
2299
|
+
const expires = challenge.expiresAt ? new Date(challenge.expiresAt).toLocaleTimeString() : "";
|
|
2300
|
+
byId("two-factor-status").textContent = expires ? `Codice inviato. Scade alle ${expires}.` : "Codice inviato. Controlla la tua email.";
|
|
2301
|
+
if (resendTimer) clearInterval(resendTimer);
|
|
2302
|
+
const availableAt = Date.now() + challenge.resendAfterSeconds * 1e3;
|
|
2303
|
+
const update = () => {
|
|
2304
|
+
const seconds = Math.max(0, Math.ceil((availableAt - Date.now()) / 1e3));
|
|
2305
|
+
const button = byId("resend-code");
|
|
2306
|
+
button.disabled = seconds > 0;
|
|
2307
|
+
button.textContent = seconds ? `Reinvia tra ${seconds}s` : "Invia un nuovo codice";
|
|
2308
|
+
if (!seconds && resendTimer) clearInterval(resendTimer);
|
|
2309
|
+
};
|
|
2310
|
+
update();
|
|
2311
|
+
resendTimer = setInterval(update, 1e3);
|
|
2312
|
+
}
|
|
2313
|
+
async function completeLoginStep(response) {
|
|
2314
|
+
if (response.challenge) {
|
|
2315
|
+
showTwoFactor(response.challenge);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
if (response.passwordRequired) {
|
|
2319
|
+
byId("password-field").hidden = false;
|
|
2320
|
+
const input = byId("password-field").querySelector("input");
|
|
2321
|
+
input.disabled = false;
|
|
2322
|
+
input.required = true;
|
|
2323
|
+
input.focus();
|
|
2324
|
+
byId("restart-login").hidden = false;
|
|
2325
|
+
byId("login-submit").textContent = "Accedi";
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
if (!response.token?.value) throw new Error("Risposta di autenticazione incompleta.");
|
|
2329
|
+
token = response.token.value;
|
|
2330
|
+
sessionStorage.setItem("accessi-console-token", token);
|
|
2331
|
+
setAccessiConsoleToken(token);
|
|
2332
|
+
resetLogin();
|
|
2333
|
+
await bootstrap();
|
|
2334
|
+
}
|
|
2335
|
+
async function bootstrap() {
|
|
2336
|
+
try {
|
|
2337
|
+
setAccessiConsoleToken(token);
|
|
2338
|
+
if (!token) throw new Error("Inserisci le credenziali amministrative.");
|
|
2339
|
+
const payload = result(await getUserByToken({ token }));
|
|
2340
|
+
currentUser = payload.userData;
|
|
2341
|
+
requireConsoleAccess();
|
|
2342
|
+
applyRoleNavigation();
|
|
2343
|
+
await detectFederatedAuthentication();
|
|
2344
|
+
const roleLabel = currentUser.flagSuper && currentUser.flagAdmin ? "super + admin" : currentUser.flagAdmin ? "admin" : "superutente";
|
|
2345
|
+
byId("who").textContent = `${currentUser.email} - ${roleLabel}`;
|
|
2346
|
+
byId("sso-navigation").hidden = !federatedAuthenticationAvailable;
|
|
2347
|
+
finishBoot("console");
|
|
2348
|
+
await navigate(viewFromLocation(), window.location.pathname === consoleBasePath);
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
token = null;
|
|
2351
|
+
currentUser = null;
|
|
2352
|
+
setAccessiConsoleToken(null);
|
|
2353
|
+
federatedAuthenticationAvailable = false;
|
|
2354
|
+
sessionStorage.removeItem("accessi-console-token");
|
|
2355
|
+
finishBoot("login");
|
|
2356
|
+
byId("login-error").textContent = error instanceof Error ? error.message : "Sessione non valida.";
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
async function showUsers() {
|
|
2360
|
+
const { users, total } = await loadUsersPage();
|
|
2361
|
+
usersTotal = total;
|
|
2362
|
+
const totalPages = Math.max(1, Math.ceil(total / USERS_PAGE_SIZE));
|
|
2363
|
+
if (usersPage >= totalPages) usersPage = totalPages - 1;
|
|
2364
|
+
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>
|
|
2365
|
+
<div class="pager-row pager-row-top">${pagerMarkup(totalPages)}</div>
|
|
2366
|
+
<table><thead><tr><th>Utente</th><th>Stato</th><th>Accesso</th><th>2FA</th><th>Ruoli</th><th></th></tr></thead><tbody>
|
|
2367
|
+
${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>'}
|
|
2368
|
+
</tbody></table><div class="pager-row pager-row-bottom">${pagerMarkup(totalPages)}</div>`);
|
|
2369
|
+
byId("new-local").onclick = () => showLocalUserForm();
|
|
2370
|
+
if (federatedAuthenticationAvailable) byId("new-sso").onclick = () => handleAction(showSsoUserForm);
|
|
2371
|
+
byId("reload-users").onclick = () => handleAction(() => show("users"));
|
|
2372
|
+
document.querySelectorAll("[data-goto]").forEach((button) => {
|
|
2373
|
+
button.onclick = () => handleAction(async () => {
|
|
2374
|
+
const target = button.dataset.goto;
|
|
2375
|
+
const nextPage = target === "prev" ? usersPage - 1 : target === "next" ? usersPage + 1 : Number(target) - 1;
|
|
2376
|
+
if (!Number.isInteger(nextPage) || nextPage < 0 || nextPage >= totalPages || nextPage === usersPage) return;
|
|
2377
|
+
usersPage = nextPage;
|
|
2378
|
+
await show("users");
|
|
2379
|
+
});
|
|
2380
|
+
});
|
|
2381
|
+
document.querySelectorAll("[data-user]").forEach((button) => {
|
|
2382
|
+
button.onclick = () => handleAction(() => showUser(button.dataset.user ?? ""));
|
|
2383
|
+
});
|
|
2384
|
+
bindRolesToggles();
|
|
2385
|
+
}
|
|
2386
|
+
function authenticationPolicyFields(user) {
|
|
2387
|
+
return `<fieldset><legend>Verifica dell'accesso (opzionale)</legend>
|
|
2388
|
+
<label class="check"><input name="flagDueFattori" type="checkbox" ${user.flagDueFattori ? "checked" : ""}> Richiedi un codice via email (2FA) ${helpDot("user-2fa")}</label>
|
|
2389
|
+
<p class="form-help">Se attiva, il codice \xC3\xA8 richiesto dopo password o SSO.</p>
|
|
2390
|
+
<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>
|
|
2391
|
+
<p class="form-help">Accesso senza password (richiede il codice email).</p>
|
|
2392
|
+
</fieldset>`;
|
|
2393
|
+
}
|
|
2394
|
+
function showLocalUserForm() {
|
|
2395
|
+
render(`<button id="back">Indietro</button><h2>Nuovo utente locale</h2><p class="muted">Viene inviata l'e-mail per impostare la password.</p>
|
|
2396
|
+
<form id="local-user"><label>Email ${helpDot("user-email")}<input name="email" type="email" required><span class="form-help">Identificativo di accesso; ricever\xC3\xA0 l'email per la password.</span></label><label>Nome ${helpDot("user-name")}<input name="nome"></label><label>Cognome ${helpDot("user-name")}<input name="cognome"></label><button>Crea utente</button></form>`);
|
|
2397
|
+
byId("back").onclick = () => show("users");
|
|
2398
|
+
const localUserForm = document.getElementById("local-user");
|
|
2399
|
+
if (localUserForm) localUserForm.onsubmit = (event) => {
|
|
2400
|
+
event.preventDefault();
|
|
2401
|
+
handleAction(async () => {
|
|
2402
|
+
await createManagedUser(formValues(eventForm(event)));
|
|
2403
|
+
showNotice("Utente creato; inviata la procedura di impostazione password.");
|
|
2404
|
+
await show("users");
|
|
2405
|
+
});
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
async function showSsoUserForm() {
|
|
2409
|
+
const providers = result(await getFederatedProviders());
|
|
2410
|
+
if (!providers.some((provider) => provider.active)) {
|
|
2411
|
+
throw new Error("Prima di creare un utente SSO, censisci e abilita almeno un provider SSO.");
|
|
2412
|
+
}
|
|
2413
|
+
render(`<button id="back">Indietro</button><h2>Nuovo utente SSO</h2><p class="muted">Salva solo collegamenti gi\xC3\xA0 verificati dal backend.</p>
|
|
2414
|
+
<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>`);
|
|
2415
|
+
byId("back").onclick = () => show("ssoUsers");
|
|
2416
|
+
const ssoUserForm = document.getElementById("sso-user");
|
|
2417
|
+
if (ssoUserForm) ssoUserForm.onsubmit = (event) => {
|
|
2418
|
+
event.preventDefault();
|
|
2419
|
+
handleAction(async () => {
|
|
2420
|
+
const form = eventForm(event);
|
|
2421
|
+
const values = formValues(form);
|
|
2422
|
+
const request = {
|
|
2423
|
+
provider: values.provider ?? "",
|
|
2424
|
+
subject: values.subject ?? "",
|
|
2425
|
+
note: values.note || void 0,
|
|
2426
|
+
passwordLoginEnabled: new FormData(form).has("passwordLoginEnabled"),
|
|
2427
|
+
user: { email: values.email ?? "", nome: values.nome || void 0, cognome: values.cognome || void 0 }
|
|
2428
|
+
};
|
|
2429
|
+
await createFederatedUser(request);
|
|
2430
|
+
showNotice("Utente SSO creato e collegamento registrato.");
|
|
2431
|
+
await show("ssoUsers");
|
|
2432
|
+
});
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
async function showSsoProviders() {
|
|
2436
|
+
if (!federatedAuthenticationAvailable) throw new Error("SSO non \xC3\xA8 abilitato in questa istanza Accessi.");
|
|
2437
|
+
const providerList = result(await getFederatedProviders());
|
|
2438
|
+
render(`<div class="toolbar"><button id="new-provider">Nuovo provider</button><button id="reload-providers" class="secondary">Aggiorna</button></div>
|
|
2439
|
+
<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\xC3\xA0 \xC3\xA8 collegata.</p>
|
|
2440
|
+
<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>`);
|
|
2441
|
+
byId("new-provider").onclick = () => showProviderForm();
|
|
2442
|
+
byId("reload-providers").onclick = () => handleAction(showSsoProviders);
|
|
2443
|
+
document.querySelectorAll("[data-provider]").forEach((button) => {
|
|
2444
|
+
button.onclick = () => showProviderForm(providerList.find((provider) => provider.provider === button.dataset.provider));
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
async function showSsoUsers() {
|
|
2448
|
+
if (!federatedAuthenticationAvailable) throw new Error("SSO non \xC3\xA8 abilitato in questa istanza Accessi.");
|
|
2449
|
+
const users = await loadUsers();
|
|
2450
|
+
render(`<div class="toolbar"><button id="new-sso">Nuovo utente SSO</button><button id="reload-sso-users" class="secondary">Aggiorna</button></div>
|
|
2451
|
+
<h2>Utenti e identit\xC3\xA0 SSO</h2><p class="muted">Gestisci i collegamenti provider/subject gi\xC3\xA0 verificati dal backend.</p>
|
|
2452
|
+
<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\xC3\xA0</button></td></tr>`).join("") || '<tr><td colspan="3" class="muted">Nessun utente.</td></tr>'}</tbody></table></div>`);
|
|
2453
|
+
byId("new-sso").onclick = () => handleAction(showSsoUserForm);
|
|
2454
|
+
byId("reload-sso-users").onclick = () => handleAction(showSsoUsers);
|
|
2455
|
+
document.querySelectorAll("[data-sso-user]").forEach((button) => {
|
|
2456
|
+
button.onclick = () => handleAction(() => showUser(button.dataset.ssoUser ?? ""));
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
function showProviderForm(provider) {
|
|
2460
|
+
const isExisting = provider !== void 0;
|
|
2461
|
+
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>`);
|
|
2462
|
+
byId("back").onclick = () => show("ssoProviders");
|
|
2463
|
+
const deleteProviderButton = document.getElementById("delete-provider");
|
|
2464
|
+
if (deleteProviderButton && provider) {
|
|
2465
|
+
deleteProviderButton.onclick = () => handleAction(async () => {
|
|
2466
|
+
if (!window.confirm(`Eliminare il provider ${provider.provider}? Operazione possibile solo se nessuna identita e collegata.`)) return;
|
|
2467
|
+
await deleteFederatedProvider(provider.provider);
|
|
2468
|
+
showNotice("Provider SSO eliminato.");
|
|
2469
|
+
await show("ssoProviders");
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
const providerForm = document.getElementById("provider-form");
|
|
2473
|
+
if (providerForm) providerForm.onsubmit = (event) => {
|
|
2474
|
+
event.preventDefault();
|
|
2475
|
+
handleAction(async () => {
|
|
2476
|
+
const form = eventForm(event);
|
|
2477
|
+
const values = formValues(form);
|
|
2478
|
+
if (provider) {
|
|
2479
|
+
const request = {
|
|
2480
|
+
description: values.description,
|
|
2481
|
+
note: values.note || void 0,
|
|
2482
|
+
active: new FormData(form).has("active")
|
|
2483
|
+
};
|
|
2484
|
+
await updateFederatedProvider(provider.provider, request);
|
|
2485
|
+
} else {
|
|
2486
|
+
const request = {
|
|
2487
|
+
provider: values.provider ?? "",
|
|
2488
|
+
description: values.description ?? "",
|
|
2489
|
+
note: values.note || void 0
|
|
2490
|
+
};
|
|
2491
|
+
await createFederatedProvider(request);
|
|
2492
|
+
}
|
|
2493
|
+
showNotice(isExisting ? "Provider SSO aggiornato." : "Provider SSO censito.");
|
|
2494
|
+
await show("ssoProviders");
|
|
2495
|
+
});
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
function registrationStateOptions(current) {
|
|
2499
|
+
const states = [
|
|
2500
|
+
[0, "Non definito"],
|
|
2501
|
+
[5, "Inserito"],
|
|
2502
|
+
[10, "Invitato"],
|
|
2503
|
+
[20, "Confermato"],
|
|
2504
|
+
[50, "Eliminato"],
|
|
2505
|
+
[99, "Bloccato"]
|
|
2506
|
+
];
|
|
2507
|
+
const currentValue = Number(current);
|
|
2508
|
+
return states.map(([value, label]) => `<option value="${value}" ${currentValue === value ? "selected" : ""}>${label} (${value})</option>`).join("");
|
|
2509
|
+
}
|
|
2510
|
+
async function showUser(rawCode) {
|
|
2511
|
+
const codiceUtente = Number(rawCode);
|
|
2512
|
+
const [users, roles, grants, groups, providers] = await Promise.all([
|
|
2513
|
+
getUsers({ codiceUtente, includeGrants: true }),
|
|
2514
|
+
getRoles(),
|
|
2515
|
+
getUserRolesAndGrants(codiceUtente),
|
|
2516
|
+
getGroupsWithMenus({ includeDisabled: true }),
|
|
2517
|
+
federatedAuthenticationAvailable ? getFederatedProviders() : Promise.resolve(null)
|
|
2518
|
+
]);
|
|
2519
|
+
const userRow = result(users)[0];
|
|
2520
|
+
if (!userRow?.utente) throw new Error("Utente non trovato.");
|
|
2521
|
+
const user = userRow.utente;
|
|
2522
|
+
const allRoles = result(roles);
|
|
2523
|
+
const userGrants = result(grants);
|
|
2524
|
+
const menuGroups = result(groups);
|
|
2525
|
+
const providerList = providers ? result(providers) : [];
|
|
2526
|
+
const linked = federatedAuthenticationAvailable ? result(await getFederatedIdentities(codiceUtente)) : [];
|
|
2527
|
+
const identitySection = federatedAuthenticationAvailable ? federatedIdentitySection(linked, providerList) : "";
|
|
2528
|
+
const defaultUserTab = federatedAuthenticationAvailable ? "sso" : "profile";
|
|
2529
|
+
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">
|
|
2530
|
+
${identitySection}
|
|
2531
|
+
<form id="profile" data-user-panel="profile" role="tabpanel" aria-labelledby="user-tab-profile"><h3>Profilo e accesso</h3><label>Nome ${helpDot("user-name")}<input name="nome" value="${escapeHtml(user.nome)}"></label><label>Cognome ${helpDot("user-name")}<input name="cognome" value="${escapeHtml(user.cognome)}"></label><label>Email ${helpDot("user-email")}<input name="email" type="email" value="${escapeHtml(user.email)}" required><span class="form-help">Identificativo di accesso e recapito.</span></label><label>Pagina di default ${helpDot("user-default-page")}<input name="paginaDefault" maxlength="50" value="${escapeHtml(user.paginaDefault)}"><span class="form-help">Pagina mostrata dopo il login (percorso o codice pagina).</span></label>${federatedAuthenticationAvailable ? `<label class="check"><input name="passwordLoginEnabled" type="checkbox" ${user.passwordLoginEnabled !== false ? "checked" : ""}> Login con password ${helpDot("user-password-login")}</label><span class="form-help">Se disabilitato, il login con email e password restituisce un errore esplicito; restano valide le identit\xC3\xA0 SSO attive.</span>` : ""}${authenticationPolicyFields(user)}<button>Salva</button></form>
|
|
2532
|
+
${roleAssignmentEditor(allRoles, userGrants.ruoli, menuGroups)}
|
|
2533
|
+
${directGrantEditor(menuGroups, userGrants.abilitazioni)}
|
|
2534
|
+
</div>`);
|
|
2535
|
+
byId("back").onclick = () => show("users");
|
|
2536
|
+
const userTabButtons = Array.from(document.querySelectorAll("[data-user-tab]"));
|
|
2537
|
+
userTabButtons.forEach((button, index) => {
|
|
2538
|
+
button.onclick = () => activateUserTab(button.dataset.userTab);
|
|
2539
|
+
button.onkeydown = (event) => {
|
|
2540
|
+
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;
|
|
2541
|
+
if (targetIndex < 0) return;
|
|
2542
|
+
event.preventDefault();
|
|
2543
|
+
const targetTab = userTabButtons[targetIndex];
|
|
2544
|
+
if (!targetTab) return;
|
|
2545
|
+
targetTab.focus();
|
|
2546
|
+
activateUserTab(targetTab.dataset.userTab);
|
|
2547
|
+
};
|
|
2548
|
+
});
|
|
2549
|
+
activateUserTab(defaultUserTab);
|
|
2550
|
+
const profileForm = document.getElementById("profile");
|
|
2551
|
+
if (profileForm) {
|
|
2552
|
+
const twoFactor = profileForm.elements.namedItem("flagDueFattori");
|
|
2553
|
+
const passwordless = profileForm.elements.namedItem("passwordlessLoginEnabled");
|
|
2554
|
+
twoFactor.onchange = () => {
|
|
2555
|
+
passwordless.disabled = !twoFactor.checked;
|
|
2556
|
+
if (!twoFactor.checked) passwordless.checked = false;
|
|
2557
|
+
};
|
|
2558
|
+
profileForm.onsubmit = (event) => {
|
|
2559
|
+
event.preventDefault();
|
|
2560
|
+
handleAction(async () => {
|
|
2561
|
+
const values = formValues(eventForm(event));
|
|
2562
|
+
const fields = new FormData(eventForm(event));
|
|
2563
|
+
await updateUtente(codiceUtente, {
|
|
2564
|
+
codiceUtente,
|
|
2565
|
+
email: values.email ?? "",
|
|
2566
|
+
nome: values.nome || void 0,
|
|
2567
|
+
cognome: values.cognome || void 0,
|
|
2568
|
+
paginaDefault: values.paginaDefault || void 0,
|
|
2569
|
+
flagDueFattori: fields.has("flagDueFattori"),
|
|
2570
|
+
passwordlessLoginEnabled: fields.has("passwordlessLoginEnabled"),
|
|
2571
|
+
...federatedAuthenticationAvailable ? { passwordLoginEnabled: fields.has("passwordLoginEnabled") } : {}
|
|
2572
|
+
});
|
|
2573
|
+
if (codiceUtente === currentUser?.codiceUtente && (fields.has("flagDueFattori") !== Boolean(user.flagDueFattori) || fields.has("passwordlessLoginEnabled") !== Boolean(user.passwordlessLoginEnabled))) {
|
|
2574
|
+
token = null;
|
|
2575
|
+
sessionStorage.removeItem("accessi-console-token");
|
|
2576
|
+
setAccessiConsoleToken(null);
|
|
2577
|
+
resetLogin();
|
|
2578
|
+
await bootstrap();
|
|
2579
|
+
byId("login-error").textContent = "Policy aggiornata. Accedi di nuovo con la nuova configurazione.";
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
await showUser(String(codiceUtente));
|
|
2583
|
+
activateUserTab("profile");
|
|
2584
|
+
showNotice("Profilo e policy aggiornati.");
|
|
2585
|
+
});
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
const rolesForm = document.getElementById("roles");
|
|
2589
|
+
if (rolesForm) rolesForm.onsubmit = (event) => {
|
|
2590
|
+
event.preventDefault();
|
|
2591
|
+
handleAction(async () => {
|
|
2592
|
+
await assignRolesToUser(codiceUtente, { roles: selectedNumbers(eventForm(event), "role") });
|
|
2593
|
+
showNotice("Ruoli aggiornati.");
|
|
2594
|
+
});
|
|
2595
|
+
};
|
|
2596
|
+
const grantsForm = document.getElementById("grants");
|
|
2597
|
+
if (grantsForm) grantsForm.onsubmit = (event) => {
|
|
2598
|
+
event.preventDefault();
|
|
2599
|
+
handleAction(async () => {
|
|
2600
|
+
const permissions = [];
|
|
2601
|
+
new FormData(eventForm(event)).forEach((value, key) => {
|
|
2602
|
+
if (key.startsWith("grant:") && value) permissions.push({ codiceMenu: key.slice(6), tipoAbilitazione: Number(value) });
|
|
2603
|
+
});
|
|
2604
|
+
await assignPermissionsToUser(codiceUtente, { permissions });
|
|
2605
|
+
showNotice("Grant aggiornati.");
|
|
2606
|
+
});
|
|
2607
|
+
};
|
|
2608
|
+
if (federatedAuthenticationAvailable) {
|
|
2609
|
+
const linkForm = document.getElementById("link-sso");
|
|
2610
|
+
if (linkForm) linkForm.onsubmit = (event) => {
|
|
2611
|
+
event.preventDefault();
|
|
2612
|
+
handleAction(async () => {
|
|
2613
|
+
const values = formValues(eventForm(event));
|
|
2614
|
+
await linkFederatedIdentity(codiceUtente, { provider: values.provider ?? "", subject: values.subject ?? "", note: values.note || void 0 });
|
|
2615
|
+
await showUser(String(codiceUtente));
|
|
2616
|
+
});
|
|
2617
|
+
};
|
|
2618
|
+
document.querySelectorAll("[data-identity-toggle]").forEach((button) => {
|
|
2619
|
+
button.onclick = () => handleAction(async () => {
|
|
2620
|
+
await updateFederatedIdentity(button.dataset.identityToggle ?? "", { active: button.dataset.active === "true" });
|
|
2621
|
+
await showUser(String(codiceUtente));
|
|
2622
|
+
});
|
|
2623
|
+
});
|
|
2624
|
+
document.querySelectorAll("[data-identity-delete]").forEach((button) => {
|
|
2625
|
+
button.onclick = () => handleAction(async () => {
|
|
2626
|
+
if (!window.confirm("Eliminare definitivamente questo collegamento SSO? L\xE2\u20AC\u2122utente Accessi non verr\xC3\xA0 eliminato.")) return;
|
|
2627
|
+
await deleteFederatedIdentityPermanently(codiceUtente, button.dataset.identityDelete ?? "");
|
|
2628
|
+
showNotice("Collegamento SSO eliminato definitivamente.");
|
|
2629
|
+
await showUser(String(codiceUtente));
|
|
2630
|
+
});
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
byId("save-state").onclick = () => handleAction(async () => {
|
|
2634
|
+
const stateSelect = document.getElementById("user-state");
|
|
2635
|
+
const statoRegistrazione = Number(stateSelect?.value ?? 0);
|
|
2636
|
+
await setStatoRegistrazione({ codiceUtente, statoRegistrazione });
|
|
2637
|
+
showNotice("Stato registrazione aggiornato.");
|
|
2638
|
+
await showUser(String(codiceUtente));
|
|
2639
|
+
});
|
|
2640
|
+
byId("disable-user").onclick = () => handleAction(async () => {
|
|
2641
|
+
await deleteUser(codiceUtente);
|
|
2642
|
+
showNotice("Utente impostato come eliminato.");
|
|
2643
|
+
await show("users");
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
async function showRoles() {
|
|
2647
|
+
const [rolesResponse, groupsResponse] = await Promise.all([getRoles(), getGroupsWithMenus({ includeDisabled: true })]);
|
|
2648
|
+
const roles = result(rolesResponse);
|
|
2649
|
+
const groups = result(groupsResponse);
|
|
2650
|
+
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>`);
|
|
2651
|
+
const roleForm = (role) => {
|
|
2652
|
+
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>`);
|
|
2653
|
+
byId("back").onclick = () => show("roles");
|
|
2654
|
+
document.querySelectorAll('#role-form input[name="menu"]').forEach((checkbox) => {
|
|
2655
|
+
checkbox.onchange = () => {
|
|
2656
|
+
const option = checkbox.closest("li");
|
|
2657
|
+
const level = option?.querySelector('select[name^="menu-level:"]');
|
|
2658
|
+
if (level) level.disabled = !checkbox.checked;
|
|
2659
|
+
};
|
|
2660
|
+
});
|
|
2661
|
+
const roleFormElement = document.getElementById("role-form");
|
|
2662
|
+
if (roleFormElement) roleFormElement.onsubmit = async (event) => {
|
|
2663
|
+
event.preventDefault();
|
|
2664
|
+
const form = eventForm(event);
|
|
2665
|
+
const data = new FormData(form);
|
|
2666
|
+
const request = { descrizioneRuolo: formValues(form).descrizione ?? "", menu: Array.from(data.getAll("menu"), (codiceMenu) => ({ codiceMenu: String(codiceMenu), tipoAbilitazione: Number(data.get(`menu-level:${codiceMenu}`) ?? 10) })) };
|
|
2667
|
+
if (role?.codiceRuolo) await updateRole(role.codiceRuolo, request);
|
|
2668
|
+
else await createRole(request);
|
|
2669
|
+
await show("roles");
|
|
2670
|
+
};
|
|
2671
|
+
};
|
|
2672
|
+
byId("new-role").onclick = () => roleForm();
|
|
2673
|
+
document.querySelectorAll("[data-role]").forEach((button) => button.onclick = () => roleForm(roles.find((role) => role.codiceRuolo === Number(button.dataset.role))));
|
|
2674
|
+
document.querySelectorAll("[data-role-delete]").forEach((button) => button.onclick = () => handleAction(async () => {
|
|
2675
|
+
if (!window.confirm("Eliminare definitivamente questo ruolo? Le assegnazioni agli utenti verranno rimosse.")) return;
|
|
2676
|
+
await deleteRole(Number(button.dataset.roleDelete));
|
|
2677
|
+
showNotice("Ruolo eliminato.");
|
|
2678
|
+
await show("roles");
|
|
2679
|
+
}));
|
|
2680
|
+
}
|
|
2681
|
+
function menuGroupsTable(groups) {
|
|
2682
|
+
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>`;
|
|
2683
|
+
}
|
|
2684
|
+
function menusTable(entries, menuTypes) {
|
|
2685
|
+
const typeLabel = (codice) => {
|
|
2686
|
+
if (!codice) return "Non indicato";
|
|
2687
|
+
return menuTypes.find((type) => type.codiceTipo === codice)?.descrizioneTipo || codice;
|
|
2688
|
+
};
|
|
2689
|
+
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>`;
|
|
2690
|
+
}
|
|
2691
|
+
function menuTypeTable(types) {
|
|
2692
|
+
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>`;
|
|
2693
|
+
}
|
|
2694
|
+
async function showMenuGroups() {
|
|
2695
|
+
const groups = result(await getGroupsWithMenus({ includeDisabled: true }));
|
|
2696
|
+
render(`<div class="toolbar"><button id="new-group">Nuovo gruppo</button><button id="reload-groups" class="secondary">Aggiorna</button></div>
|
|
2697
|
+
<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>
|
|
2698
|
+
${menuGroupsTable(groups)}`);
|
|
2699
|
+
byId("new-group").onclick = () => showMenuGroupForm();
|
|
2700
|
+
byId("reload-groups").onclick = () => handleAction(showMenuGroups);
|
|
2701
|
+
document.querySelectorAll("[data-group]").forEach((input) => {
|
|
2702
|
+
input.onchange = () => handleAction(async () => {
|
|
2703
|
+
await setGroupEnabled(input.dataset.group ?? "", { enabled: input.checked });
|
|
2704
|
+
showNotice("Gruppo aggiornato.");
|
|
2705
|
+
});
|
|
2706
|
+
});
|
|
2707
|
+
document.querySelectorAll("[data-group-edit]").forEach((button) => {
|
|
2708
|
+
button.onclick = () => showMenuGroupForm(groups.find((group) => group.codiceGruppo === button.dataset.groupEdit));
|
|
2709
|
+
});
|
|
2710
|
+
document.querySelectorAll("[data-group-delete]").forEach((button) => {
|
|
2711
|
+
button.onclick = () => handleAction(async () => {
|
|
2712
|
+
if (!window.confirm(`Eliminare il gruppo ${button.dataset.groupDelete}? Operazione possibile solo se non contiene menu.`)) return;
|
|
2713
|
+
await deleteMenuGroup(button.dataset.groupDelete ?? "");
|
|
2714
|
+
showNotice("Gruppo eliminato.");
|
|
2715
|
+
await showMenuGroups();
|
|
2716
|
+
});
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
async function showMenuItems() {
|
|
2720
|
+
const [groupsResponse, typesResponse] = await Promise.all([
|
|
2721
|
+
getGroupsWithMenus({ includeDisabled: true }),
|
|
2722
|
+
getMenuTypes()
|
|
2723
|
+
]);
|
|
2724
|
+
const groups = result(groupsResponse);
|
|
2725
|
+
const menuTypes = result(typesResponse);
|
|
2726
|
+
const entries = groups.flatMap((group) => group.menus.map((menu) => ({ menu, group })));
|
|
2727
|
+
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>
|
|
2728
|
+
<h2>Menu</h2><p class="form-help">Voci di navigazione. L'eliminazione rimuove anche i grant utente e le associazioni ai ruoli.</p>
|
|
2729
|
+
${menusTable(entries, menuTypes)}`);
|
|
2730
|
+
byId("new-menu").onclick = () => showMenuForm(void 0, groups, menuTypes);
|
|
2731
|
+
byId("reload-menus").onclick = () => handleAction(showMenuItems);
|
|
2732
|
+
const groupFilter = document.getElementById("menu-group-filter");
|
|
2733
|
+
if (groupFilter) {
|
|
2734
|
+
groupFilter.onchange = () => {
|
|
2735
|
+
const selected = groupFilter.value;
|
|
2736
|
+
document.querySelectorAll("[data-menu-group]").forEach((row) => {
|
|
2737
|
+
row.hidden = selected !== "" && row.dataset.menuGroup !== selected;
|
|
2738
|
+
});
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
document.querySelectorAll("[data-menu]").forEach((input) => {
|
|
2742
|
+
input.onchange = () => handleAction(async () => {
|
|
2743
|
+
await setMenuEnabled(input.dataset.menu ?? "", { enabled: input.checked });
|
|
2744
|
+
showNotice("Menu aggiornato.");
|
|
2745
|
+
});
|
|
2746
|
+
});
|
|
2747
|
+
document.querySelectorAll("[data-menu-edit]").forEach((button) => {
|
|
2748
|
+
button.onclick = () => showMenuForm(entries.find((entry) => entry.menu.codiceMenu === button.dataset.menuEdit)?.menu, groups, menuTypes);
|
|
2749
|
+
});
|
|
2750
|
+
document.querySelectorAll("[data-menu-delete]").forEach((button) => {
|
|
2751
|
+
button.onclick = () => handleAction(async () => {
|
|
2752
|
+
if (!window.confirm(`Eliminare il menu ${button.dataset.menuDelete}? Verranno rimossi anche i grant utente e le associazioni ai ruoli.`)) return;
|
|
2753
|
+
await deleteMenu(button.dataset.menuDelete ?? "");
|
|
2754
|
+
showNotice("Menu eliminato.");
|
|
2755
|
+
await showMenuItems();
|
|
2756
|
+
});
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
async function showMenuTypes() {
|
|
2760
|
+
const menuTypes = result(await getMenuTypes());
|
|
2761
|
+
render(`<div class="toolbar"><button id="new-menu-type">Nuovo tipo menu</button><button id="reload-menu-types" class="secondary">Aggiorna</button></div>
|
|
2762
|
+
<h2>Tipi menu</h2><p class="form-help">Classificano i menu (es. amministrazione, operativit\xC3\xA0). Un tipo si elimina solo se non \xC3\xA8 usato da alcun menu.</p>
|
|
2763
|
+
${menuTypeTable(menuTypes)}`);
|
|
2764
|
+
byId("new-menu-type").onclick = () => showMenuTypeForm();
|
|
2765
|
+
byId("reload-menu-types").onclick = () => handleAction(showMenuTypes);
|
|
2766
|
+
document.querySelectorAll("[data-type-edit]").forEach((button) => {
|
|
2767
|
+
button.onclick = () => showMenuTypeForm(menuTypes.find((type) => type.codiceTipo === button.dataset.typeEdit));
|
|
2768
|
+
});
|
|
2769
|
+
document.querySelectorAll("[data-type-delete]").forEach((button) => {
|
|
2770
|
+
button.onclick = () => handleAction(async () => {
|
|
2771
|
+
if (!window.confirm(`Eliminare il tipo menu ${button.dataset.typeDelete}? Operazione possibile solo se nessun menu lo utilizza.`)) return;
|
|
2772
|
+
await deleteMenuType(button.dataset.typeDelete ?? "");
|
|
2773
|
+
showNotice("Tipo menu eliminato.");
|
|
2774
|
+
await showMenuTypes();
|
|
2775
|
+
});
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
function showMenuGroupForm(group) {
|
|
2779
|
+
const isExisting = group !== void 0;
|
|
2780
|
+
render(`<button id="back" type="button">Indietro</button><h2>${isExisting ? "Modifica" : "Nuovo"} gruppo menu</h2>
|
|
2781
|
+
<form id="group-form">
|
|
2782
|
+
<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>
|
|
2783
|
+
<label>Descrizione<input name="descrizioneGruppo" maxlength="100" value="${escapeHtml(group?.descrizioneGruppo)}" required></label>
|
|
2784
|
+
<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>
|
|
2785
|
+
<label class="check"><input name="enabled" type="checkbox" ${group?.enabled !== false ? "checked" : ""}> Gruppo abilitato ${helpDot("menu-group-enabled")}</label>
|
|
2786
|
+
<button>${isExisting ? "Salva gruppo" : "Crea gruppo"}</button>
|
|
2787
|
+
</form>`);
|
|
2788
|
+
byId("back").onclick = () => handleAction(showMenuGroups);
|
|
2789
|
+
const form = document.getElementById("group-form");
|
|
2790
|
+
if (form) {
|
|
2791
|
+
form.onsubmit = (event) => {
|
|
2792
|
+
event.preventDefault();
|
|
2793
|
+
handleAction(async () => {
|
|
2794
|
+
const values = formValues(eventForm(event));
|
|
2795
|
+
const payload = {
|
|
2796
|
+
descrizioneGruppo: values.descrizioneGruppo || void 0,
|
|
2797
|
+
ordineGruppo: values.ordineGruppo ? Number(values.ordineGruppo) : void 0,
|
|
2798
|
+
enabled: new FormData(eventForm(event)).has("enabled")
|
|
2799
|
+
};
|
|
2800
|
+
if (isExisting) await updateMenuGroup(group.codiceGruppo, payload);
|
|
2801
|
+
else await createMenuGroup({ codiceGruppo: values.codiceGruppo ?? "", ...payload });
|
|
2802
|
+
showNotice(isExisting ? "Gruppo aggiornato." : "Gruppo creato.");
|
|
2803
|
+
await showMenuGroups();
|
|
2804
|
+
});
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
function showMenuForm(menu, groups, menuTypes) {
|
|
2809
|
+
const isExisting = menu !== void 0;
|
|
2810
|
+
const groupOptions = groups.map((group) => `<option value="${escapeHtml(group.codiceGruppo)}" ${menu?.codiceGruppo === group.codiceGruppo ? "selected" : ""}>${escapeHtml(group.codiceGruppo)} \u2013 ${escapeHtml(group.descrizioneGruppo)}</option>`).join("");
|
|
2811
|
+
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("")}`;
|
|
2812
|
+
render(`<button id="back" type="button">Indietro</button><h2>${isExisting ? "Modifica" : "Nuovo"} menu</h2>
|
|
2813
|
+
<form id="menu-form">
|
|
2814
|
+
<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>
|
|
2815
|
+
<label>Descrizione<input name="descrizioneMenu" maxlength="100" value="${escapeHtml(menu?.descrizioneMenu)}" required></label>
|
|
2816
|
+
<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>
|
|
2817
|
+
<label>Tipo menu ${helpDot("menu-type-select")}<select name="tipo">${typeOptions}</select></label>
|
|
2818
|
+
<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>
|
|
2819
|
+
<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>
|
|
2820
|
+
<label>Ordine ${helpDot("menu-order")}<input name="ordineMenu" type="number" value="${menu?.ordineMenu ?? ""}"></label>
|
|
2821
|
+
<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>
|
|
2822
|
+
<label>Nota ${helpDot("menu-note")}<textarea name="note" rows="3" maxlength="1000">${escapeHtml(menu?.note)}</textarea></label>
|
|
2823
|
+
<label class="check"><input name="enabled" type="checkbox" ${menu?.enabled !== false ? "checked" : ""}> Menu abilitato ${helpDot("menu-enabled")}</label>
|
|
2824
|
+
<button>${isExisting ? "Salva menu" : "Crea menu"}</button>
|
|
2825
|
+
</form>`);
|
|
2826
|
+
byId("back").onclick = () => handleAction(showMenuItems);
|
|
2827
|
+
const form = document.getElementById("menu-form");
|
|
2828
|
+
if (form) {
|
|
2829
|
+
form.onsubmit = (event) => {
|
|
2830
|
+
event.preventDefault();
|
|
2831
|
+
handleAction(async () => {
|
|
2832
|
+
const values = formValues(eventForm(event));
|
|
2833
|
+
const payload = {
|
|
2834
|
+
descrizioneMenu: values.descrizioneMenu || void 0,
|
|
2835
|
+
codiceGruppo: values.codiceGruppo ?? "",
|
|
2836
|
+
tipo: values.tipo || void 0,
|
|
2837
|
+
icona: values.icona || void 0,
|
|
2838
|
+
pagina: values.pagina || void 0,
|
|
2839
|
+
ordineMenu: values.ordineMenu ? Number(values.ordineMenu) : void 0,
|
|
2840
|
+
rifMenu: values.rifMenu || void 0,
|
|
2841
|
+
note: values.note || void 0,
|
|
2842
|
+
enabled: new FormData(eventForm(event)).has("enabled")
|
|
2843
|
+
};
|
|
2844
|
+
if (isExisting) await updateMenu(menu.codiceMenu, payload);
|
|
2845
|
+
else await createMenu({ codiceMenu: values.codiceMenu ?? "", ...payload });
|
|
2846
|
+
showNotice(isExisting ? "Menu aggiornato." : "Menu creato.");
|
|
2847
|
+
await showMenuItems();
|
|
2848
|
+
});
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
function showMenuTypeForm(type) {
|
|
2853
|
+
const isExisting = type !== void 0;
|
|
2854
|
+
render(`<button id="back" type="button">Indietro</button><h2>${isExisting ? "Modifica" : "Nuovo"} tipo menu</h2>
|
|
2855
|
+
<form id="menu-type-form">
|
|
2856
|
+
<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>
|
|
2857
|
+
<label>Descrizione<input name="descrizioneTipo" maxlength="20" value="${escapeHtml(type?.descrizioneTipo)}"></label>
|
|
2858
|
+
<button>${isExisting ? "Salva tipo" : "Crea tipo"}</button>
|
|
2859
|
+
</form>`);
|
|
2860
|
+
byId("back").onclick = () => handleAction(showMenuTypes);
|
|
2861
|
+
const form = document.getElementById("menu-type-form");
|
|
2862
|
+
if (form) {
|
|
2863
|
+
form.onsubmit = (event) => {
|
|
2864
|
+
event.preventDefault();
|
|
2865
|
+
handleAction(async () => {
|
|
2866
|
+
const values = formValues(eventForm(event));
|
|
2867
|
+
if (isExisting) await updateMenuType(type.codiceTipo, { descrizioneTipo: values.descrizioneTipo || void 0 });
|
|
2868
|
+
else await createMenuType({ codiceTipo: values.codiceTipo ?? "", descrizioneTipo: values.descrizioneTipo || void 0 });
|
|
2869
|
+
showNotice(isExisting ? "Tipo menu aggiornato." : "Tipo menu creato.");
|
|
2870
|
+
await showMenuTypes();
|
|
2871
|
+
});
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
function filterTypeRows(types) {
|
|
2876
|
+
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>`;
|
|
2877
|
+
}
|
|
2878
|
+
async function showFilters() {
|
|
2879
|
+
const users = await loadUsers();
|
|
2880
|
+
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>`);
|
|
2881
|
+
const filtersForm = document.getElementById("filters");
|
|
2882
|
+
if (filtersForm) filtersForm.onsubmit = async (event) => {
|
|
2883
|
+
event.preventDefault();
|
|
2884
|
+
const form = eventForm(event);
|
|
2885
|
+
const action = event.submitter?.value;
|
|
2886
|
+
const code3 = Number(formValues(form).codUte);
|
|
2887
|
+
if (action === "load") {
|
|
2888
|
+
const filters = result(await getFiltriUtente({ codUte: code3 }));
|
|
2889
|
+
form.elements.namedItem("json").value = JSON.stringify(filters[0] ?? { codUte: code3 }, null, 2);
|
|
2890
|
+
} else {
|
|
2891
|
+
const parsed = JSON.parse(formValues(form).json ?? "{}");
|
|
2892
|
+
await saveFiltriUtente({ ...parsed, codUte: code3 });
|
|
2893
|
+
showNotice("Filtri salvati.");
|
|
2894
|
+
}
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
async function showFilterTypes() {
|
|
2898
|
+
const filterTypes = result(await getTipiFiltro());
|
|
2899
|
+
render(`<div class="toolbar"><button id="new-filter-type">Nuovo tipo filtro</button><button id="reload-filter-types" class="secondary">Aggiorna</button></div>
|
|
2900
|
+
<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>
|
|
2901
|
+
${filterTypeRows(filterTypes)}`);
|
|
2902
|
+
byId("new-filter-type").onclick = () => showFilterTypeForm();
|
|
2903
|
+
byId("reload-filter-types").onclick = () => handleAction(showFilterTypes);
|
|
2904
|
+
document.querySelectorAll("[data-filter-type-edit]").forEach((button) => button.onclick = () => showFilterTypeForm(filterTypes.find((type) => String(type.tipFil) === button.dataset.filterTypeEdit)));
|
|
2905
|
+
document.querySelectorAll("[data-filter-type-delete]").forEach((button) => button.onclick = () => handleAction(async () => {
|
|
2906
|
+
if (!window.confirm(`Eliminare il tipo filtro ${button.dataset.filterTypeDelete}? Operazione possibile solo se nessun filtro lo utilizza.`)) return;
|
|
2907
|
+
await deleteTipoFiltro(Number(button.dataset.filterTypeDelete));
|
|
2908
|
+
showNotice("Tipo filtro eliminato.");
|
|
2909
|
+
await showFilterTypes();
|
|
2910
|
+
}));
|
|
2911
|
+
}
|
|
2912
|
+
function showFilterTypeForm(type) {
|
|
2913
|
+
const isExisting = type !== void 0;
|
|
2914
|
+
render(`<button id="back" type="button">Indietro</button><h2>${isExisting ? "Modifica" : "Nuovo"} tipo filtro</h2>
|
|
2915
|
+
<form id="filter-type-form">
|
|
2916
|
+
<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>
|
|
2917
|
+
<label>Descrizione<input name="desFil" maxlength="20" value="${escapeHtml(type?.desFil)}"></label>
|
|
2918
|
+
<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>
|
|
2919
|
+
<label class="check"><input name="flgEnabled" type="checkbox" ${type?.flgEnabled !== 0 ? "checked" : ""}> Abilitato ${helpDot("filter-type-enabled")}</label>
|
|
2920
|
+
<button>${isExisting ? "Salva tipo" : "Crea tipo"}</button>
|
|
2921
|
+
</form>`);
|
|
2922
|
+
byId("back").onclick = () => handleAction(showFilterTypes);
|
|
2923
|
+
const form = document.getElementById("filter-type-form");
|
|
2924
|
+
if (form) {
|
|
2925
|
+
form.onsubmit = (event) => {
|
|
2926
|
+
event.preventDefault();
|
|
2927
|
+
handleAction(async () => {
|
|
2928
|
+
const values = formValues(eventForm(event));
|
|
2929
|
+
const payload = {
|
|
2930
|
+
desFil: values.desFil || void 0,
|
|
2931
|
+
fldFil: values.fldFil || void 0,
|
|
2932
|
+
flgEnabled: new FormData(eventForm(event)).has("flgEnabled") ? 1 : 0
|
|
2933
|
+
};
|
|
2934
|
+
if (isExisting) await updateTipoFiltro(type.tipFil, payload);
|
|
2935
|
+
else await createTipoFiltro({ tipFil: Number(values.tipFil), ...payload });
|
|
2936
|
+
showNotice(isExisting ? "Tipo filtro aggiornato." : "Tipo filtro creato.");
|
|
2937
|
+
await showFilterTypes();
|
|
2938
|
+
});
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
function scopeList(scopes) {
|
|
2943
|
+
return (scopes ?? []).join(", ") || "nessuno";
|
|
2944
|
+
}
|
|
2945
|
+
function serviceTokenExpiry(token2) {
|
|
2946
|
+
if (!token2.expiresAt) return "nessuna";
|
|
2947
|
+
const date = new Date(token2.expiresAt);
|
|
2948
|
+
const days = Math.ceil((date.getTime() - Date.now()) / 864e5);
|
|
2949
|
+
const label = escapeHtml(date.toLocaleString());
|
|
2950
|
+
if (days < 0) return `${label}<br><span class="error">scaduto</span>`;
|
|
2951
|
+
if (days <= 7) return `${label}<br><span class="error">scade tra ${days} g</span>`;
|
|
2952
|
+
return label;
|
|
2953
|
+
}
|
|
2954
|
+
function serviceTokenRows(tokens, includeRevoked) {
|
|
2955
|
+
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";
|
|
2956
|
+
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";
|
|
2957
|
+
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>`;
|
|
2958
|
+
}
|
|
2959
|
+
async function showServiceTokens(includeRevoked = false) {
|
|
2960
|
+
const tokens = result(await getServiceTokens({ includeRevoked }));
|
|
2961
|
+
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>`);
|
|
2962
|
+
byId("new-token").onclick = () => showServiceTokenForm();
|
|
2963
|
+
byId("toggle-revoked").onclick = () => handleAction(() => showServiceTokens(!includeRevoked));
|
|
2964
|
+
document.querySelectorAll("[data-revoke]").forEach((button) => {
|
|
2965
|
+
button.onclick = () => handleAction(async () => {
|
|
2966
|
+
if (!window.confirm("Revocare definitivamente questo token? Le integrazioni che lo usano smetteranno di funzionare.")) return;
|
|
2967
|
+
await revokeServiceToken(button.dataset.revoke ?? "");
|
|
2968
|
+
showNotice("Token di servizio revocato.");
|
|
2969
|
+
await showServiceTokens(includeRevoked);
|
|
2970
|
+
});
|
|
2971
|
+
});
|
|
2972
|
+
document.querySelectorAll("[data-rotate]").forEach((button) => {
|
|
2973
|
+
button.onclick = () => handleAction(async () => {
|
|
2974
|
+
const issued = result(await rotateServiceToken(button.dataset.rotate ?? ""));
|
|
2975
|
+
showIssuedServiceToken(issued);
|
|
2976
|
+
});
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
function showServiceTokenForm() {
|
|
2980
|
+
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\xC3\xB2 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>`);
|
|
2981
|
+
byId("back").onclick = () => void show("tokens");
|
|
2982
|
+
const form = document.getElementById("token-form");
|
|
2983
|
+
if (form) form.onsubmit = (event) => {
|
|
2984
|
+
event.preventDefault();
|
|
2985
|
+
handleAction(async () => {
|
|
2986
|
+
const values = formValues(eventForm(event));
|
|
2987
|
+
const scopes = (values.scopes ?? "").split(",").map((scope) => scope.trim()).filter(Boolean);
|
|
2988
|
+
const ttlDays = values.ttlDays ? Number(values.ttlDays) : void 0;
|
|
2989
|
+
const issued = result(await createServiceToken({
|
|
2990
|
+
label: values.label ?? "",
|
|
2991
|
+
scopes: scopes.length ? scopes : void 0,
|
|
2992
|
+
ttlDays: Number.isFinite(ttlDays) ? ttlDays : void 0
|
|
2993
|
+
}));
|
|
2994
|
+
showIssuedServiceToken(issued);
|
|
2995
|
+
});
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
function showIssuedServiceToken(issued) {
|
|
2999
|
+
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>`);
|
|
3000
|
+
byId("copy-token").onclick = () => handleAction(async () => {
|
|
3001
|
+
await navigator.clipboard.writeText(issued.token);
|
|
3002
|
+
showNotice("Token copiato negli appunti.");
|
|
3003
|
+
});
|
|
3004
|
+
byId("done-token").onclick = () => void show("tokens");
|
|
3005
|
+
}
|
|
3006
|
+
function renderWikiBlock(block) {
|
|
3007
|
+
switch (block.kind) {
|
|
3008
|
+
case "p":
|
|
3009
|
+
return `<p>${escapeHtml(block.text)}</p>`;
|
|
3010
|
+
case "h":
|
|
3011
|
+
return `<h3>${escapeHtml(block.text)}</h3>`;
|
|
3012
|
+
case "list": {
|
|
3013
|
+
const tag = block.ordered ? "ol" : "ul";
|
|
3014
|
+
return `<${tag}>${block.items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</${tag}>`;
|
|
3015
|
+
}
|
|
3016
|
+
case "code": {
|
|
3017
|
+
const variant = block.variant ? ` is-${block.variant}` : "";
|
|
3018
|
+
const title = block.title ? `<span class="wiki-code-title">${escapeHtml(block.title)}</span>` : "";
|
|
3019
|
+
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>`;
|
|
3020
|
+
}
|
|
3021
|
+
case "callout":
|
|
3022
|
+
return `<div class="wiki-callout is-${block.tone}">${block.title ? `<strong>${escapeHtml(block.title)}</strong>` : ""}<p>${escapeHtml(block.text)}</p></div>`;
|
|
3023
|
+
case "table":
|
|
3024
|
+
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>`;
|
|
3025
|
+
default:
|
|
3026
|
+
return "";
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
function bindCopyButtons(root) {
|
|
3030
|
+
root.querySelectorAll("[data-wiki-copy]").forEach((button) => {
|
|
3031
|
+
button.onclick = () => handleAction(async () => {
|
|
3032
|
+
const codeEl = button.closest(".wiki-code")?.querySelector("code");
|
|
3033
|
+
await navigator.clipboard.writeText(codeEl?.textContent ?? "");
|
|
3034
|
+
showNotice("Codice copiato negli appunti.");
|
|
3035
|
+
});
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
function helpDot(key) {
|
|
3039
|
+
const title = HELP[key]?.title ?? key;
|
|
3040
|
+
return `<button type="button" class="help-dot" data-help="${escapeHtml(key)}" aria-haspopup="dialog" aria-expanded="false" aria-controls="accessi-help-popover" aria-label="Informazioni: ${escapeHtml(title)}" title="Informazioni">i</button>`;
|
|
3041
|
+
}
|
|
3042
|
+
var helpPopover = null;
|
|
3043
|
+
var helpAnchor = null;
|
|
3044
|
+
function ensureHelpPopover() {
|
|
3045
|
+
if (!helpPopover) {
|
|
3046
|
+
helpPopover = document.createElement("div");
|
|
3047
|
+
helpPopover.className = "help-popover";
|
|
3048
|
+
helpPopover.id = "accessi-help-popover";
|
|
3049
|
+
helpPopover.hidden = true;
|
|
3050
|
+
helpPopover.setAttribute("role", "dialog");
|
|
3051
|
+
helpPopover.setAttribute("aria-modal", "false");
|
|
3052
|
+
helpPopover.tabIndex = -1;
|
|
3053
|
+
document.body.appendChild(helpPopover);
|
|
3054
|
+
}
|
|
3055
|
+
return helpPopover;
|
|
3056
|
+
}
|
|
3057
|
+
function closeHelp(restoreFocus = true) {
|
|
3058
|
+
if (!helpPopover) return;
|
|
3059
|
+
const wasOpen = !helpPopover.hidden;
|
|
3060
|
+
helpPopover.hidden = true;
|
|
3061
|
+
const anchor = helpAnchor;
|
|
3062
|
+
if (anchor) anchor.setAttribute("aria-expanded", "false");
|
|
3063
|
+
helpAnchor = null;
|
|
3064
|
+
if (restoreFocus && wasOpen && anchor && document.contains(anchor)) anchor.focus();
|
|
3065
|
+
}
|
|
3066
|
+
function positionHelp() {
|
|
3067
|
+
if (!helpPopover || !helpAnchor || helpPopover.hidden) return;
|
|
3068
|
+
const rect = helpAnchor.getBoundingClientRect();
|
|
3069
|
+
const width = helpPopover.offsetWidth;
|
|
3070
|
+
const height = helpPopover.offsetHeight;
|
|
3071
|
+
const margin = 8;
|
|
3072
|
+
const left = Math.max(margin, Math.min(rect.right - width, window.innerWidth - width - margin));
|
|
3073
|
+
let top = rect.bottom + 6;
|
|
3074
|
+
if (top + height > window.innerHeight - margin) {
|
|
3075
|
+
const above = rect.top - height - 6;
|
|
3076
|
+
top = above >= margin ? above : Math.max(margin, window.innerHeight - height - margin);
|
|
3077
|
+
}
|
|
3078
|
+
helpPopover.style.left = `${left}px`;
|
|
3079
|
+
helpPopover.style.top = `${top}px`;
|
|
3080
|
+
}
|
|
3081
|
+
function openHelp(anchor) {
|
|
3082
|
+
const entry = HELP[anchor.dataset.help ?? ""];
|
|
3083
|
+
if (!entry) return;
|
|
3084
|
+
const popover = ensureHelpPopover();
|
|
3085
|
+
if (helpAnchor && helpAnchor !== anchor) helpAnchor.setAttribute("aria-expanded", "false");
|
|
3086
|
+
popover.innerHTML = `<div class="help-popover-head"><h4 id="accessi-help-title">${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>`;
|
|
3087
|
+
popover.setAttribute("aria-labelledby", "accessi-help-title");
|
|
3088
|
+
popover.hidden = false;
|
|
3089
|
+
helpAnchor = anchor;
|
|
3090
|
+
anchor.setAttribute("aria-expanded", "true");
|
|
3091
|
+
bindCopyButtons(popover);
|
|
3092
|
+
positionHelp();
|
|
3093
|
+
(popover.querySelector(".help-close") ?? popover).focus();
|
|
3094
|
+
}
|
|
3095
|
+
function wikiIndexMarkup(selectedId) {
|
|
3096
|
+
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("");
|
|
3097
|
+
}
|
|
3098
|
+
async function showWiki(sectionId) {
|
|
3099
|
+
const hashId = /^#wiki-([a-z0-9-]+)$/.exec(window.location.hash)?.[1];
|
|
3100
|
+
const requested = sectionId ?? hashId;
|
|
3101
|
+
const selected = WIKI_SECTIONS.find((section) => section.id === requested) ?? WIKI_SECTIONS[0];
|
|
3102
|
+
if (!selected) return;
|
|
3103
|
+
render(`<div class="wiki">
|
|
3104
|
+
<aside class="wiki-index">
|
|
3105
|
+
<div class="wiki-index-head"><strong>Indice</strong><button type="button" id="wiki-copy-ai">Copia per AI</button></div>
|
|
3106
|
+
${wikiIndexMarkup(selected.id)}
|
|
3107
|
+
</aside>
|
|
3108
|
+
<article class="wiki-content">
|
|
3109
|
+
<header class="wiki-section-head"><h2>${escapeHtml(selected.title)}</h2><p>${escapeHtml(selected.summary)}</p></header>
|
|
3110
|
+
${selected.blocks.map(renderWikiBlock).join("")}
|
|
3111
|
+
</article>
|
|
3112
|
+
</div>`);
|
|
3113
|
+
document.querySelectorAll("[data-wiki]").forEach((link) => {
|
|
3114
|
+
link.onclick = (event) => {
|
|
3115
|
+
event.preventDefault();
|
|
3116
|
+
const id = link.dataset.wiki ?? "";
|
|
3117
|
+
window.history.replaceState(null, "", `${routePath("wiki")}#wiki-${id}`);
|
|
3118
|
+
showWiki(id);
|
|
3119
|
+
};
|
|
3120
|
+
});
|
|
3121
|
+
bindCopyButtons(document);
|
|
3122
|
+
byId("wiki-copy-ai").onclick = () => handleAction(async () => {
|
|
3123
|
+
await navigator.clipboard.writeText(buildAiDigest());
|
|
3124
|
+
showNotice("Documentazione per AI copiata negli appunti.");
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
function inlineMarkdown(text) {
|
|
3128
|
+
return escapeHtml(text).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
3129
|
+
}
|
|
3130
|
+
function renderMarkdown(markdown) {
|
|
3131
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
3132
|
+
const html = [];
|
|
3133
|
+
let inList = false;
|
|
3134
|
+
let inCode = false;
|
|
3135
|
+
const closeList = () => {
|
|
3136
|
+
if (inList) {
|
|
3137
|
+
html.push("</ul>");
|
|
3138
|
+
inList = false;
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
for (const line of lines) {
|
|
3142
|
+
if (line.startsWith("```")) {
|
|
3143
|
+
closeList();
|
|
3144
|
+
html.push(inCode ? "</code></pre>" : '<pre class="markdown-code"><code>');
|
|
3145
|
+
inCode = !inCode;
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
if (inCode) {
|
|
3149
|
+
html.push(escapeHtml(line));
|
|
3150
|
+
continue;
|
|
3151
|
+
}
|
|
3152
|
+
const heading = /^(#{1,4})\s+(.*)$/.exec(line);
|
|
3153
|
+
if (heading) {
|
|
3154
|
+
closeList();
|
|
3155
|
+
const level = (heading[1] ?? "").length;
|
|
3156
|
+
html.push(`<h${level}>${inlineMarkdown(heading[2] ?? "")}</h${level}>`);
|
|
3157
|
+
continue;
|
|
3158
|
+
}
|
|
3159
|
+
const bullet = /^\s*[-*]\s+(.*)$/.exec(line);
|
|
3160
|
+
if (bullet) {
|
|
3161
|
+
if (!inList) {
|
|
3162
|
+
html.push("<ul>");
|
|
3163
|
+
inList = true;
|
|
3164
|
+
}
|
|
3165
|
+
html.push(`<li>${inlineMarkdown(bullet[1] ?? "")}</li>`);
|
|
3166
|
+
continue;
|
|
3167
|
+
}
|
|
3168
|
+
if (line.trim() === "") {
|
|
3169
|
+
closeList();
|
|
3170
|
+
continue;
|
|
3171
|
+
}
|
|
3172
|
+
closeList();
|
|
3173
|
+
html.push(`<p>${inlineMarkdown(line)}</p>`);
|
|
3174
|
+
}
|
|
3175
|
+
if (inCode) html.push("</code></pre>");
|
|
3176
|
+
closeList();
|
|
3177
|
+
return html.join("");
|
|
3178
|
+
}
|
|
3179
|
+
async function showChangelog() {
|
|
3180
|
+
render(`<div class="wiki"><article class="wiki-content markdown-body">
|
|
3181
|
+
<header class="wiki-section-head"><h2>Changelog</h2><p>Novita e modifiche rilevanti del modulo Accessi.</p></header>
|
|
3182
|
+
${renderMarkdown(changelog_default)}
|
|
3183
|
+
</article></div>`);
|
|
3184
|
+
}
|
|
3185
|
+
function downloadTextFile(fileName, content) {
|
|
3186
|
+
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
|
3187
|
+
const url = URL.createObjectURL(blob);
|
|
3188
|
+
const anchor = document.createElement("a");
|
|
3189
|
+
anchor.href = url;
|
|
3190
|
+
anchor.download = fileName;
|
|
3191
|
+
document.body.appendChild(anchor);
|
|
3192
|
+
anchor.click();
|
|
3193
|
+
anchor.remove();
|
|
3194
|
+
URL.revokeObjectURL(url);
|
|
3195
|
+
}
|
|
3196
|
+
async function showSql() {
|
|
3197
|
+
const schema = result(await getSchema());
|
|
3198
|
+
const copy = (text, notice) => () => handleAction(async () => {
|
|
3199
|
+
await navigator.clipboard.writeText(text);
|
|
3200
|
+
showNotice(notice);
|
|
3201
|
+
});
|
|
3202
|
+
render(`<div class="toolbar"><button id="sql-copy-all">Copia tutto</button><button id="sql-download" class="secondary">Scarica .sql</button><button id="sql-reload" class="secondary">Aggiorna</button></div>
|
|
3203
|
+
<h2>SQL dello schema</h2>
|
|
3204
|
+
<p class="form-help">DDL generato dallo schema Accessi <strong>${escapeHtml(schema.version)}</strong>. Usalo per creare le entita su una nuova installazione (database vuoto).</p>
|
|
3205
|
+
<div class="entity-summary">
|
|
3206
|
+
<span><strong>${schema.presentTables.length}</strong> tabelle presenti</span>
|
|
3207
|
+
<span><strong>${schema.presentGenerators.length}</strong> generatori</span>
|
|
3208
|
+
<span><strong>${schema.extraTables.length}</strong> tabelle extra fuori schema Accessi</span>
|
|
3209
|
+
</div>
|
|
3210
|
+
${schema.extraTables.length ? `<p class="form-help">Tabelle presenti non gestite da Accessi: <code>${schema.extraTables.map((name) => escapeHtml(name)).join(", ")}</code></p>` : ""}
|
|
3211
|
+
${schema.sections.map((section) => `<section class="sql-section"><div class="sql-section-head"><h3>${escapeHtml(section.title)}</h3><button type="button" class="secondary" data-sql-copy="${escapeHtml(section.id)}">Copia sezione</button></div><pre class="markdown-code"><code>${escapeHtml(section.sql)}</code></pre></section>`).join("")}`);
|
|
3212
|
+
byId("sql-copy-all").onclick = copy(schema.script, "Script SQL completo copiato.");
|
|
3213
|
+
byId("sql-download").onclick = () => downloadTextFile(`accessi-schema-${schema.version}.sql`, schema.script);
|
|
3214
|
+
byId("sql-reload").onclick = () => handleAction(showSql);
|
|
3215
|
+
document.querySelectorAll("[data-sql-copy]").forEach((button) => {
|
|
3216
|
+
const section = schema.sections.find((entry) => entry.id === button.dataset.sqlCopy);
|
|
3217
|
+
if (section) button.onclick = copy(section.sql, `Sezione "${section.title}" copiata.`);
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
async function show(view) {
|
|
3221
|
+
try {
|
|
3222
|
+
showNotice("");
|
|
3223
|
+
const views = {
|
|
3224
|
+
users: showUsers,
|
|
3225
|
+
roles: showRoles,
|
|
3226
|
+
menuGroups: showMenuGroups,
|
|
3227
|
+
menuItems: showMenuItems,
|
|
3228
|
+
menuTypes: showMenuTypes,
|
|
3229
|
+
filterUser: showFilters,
|
|
3230
|
+
filterTypes: showFilterTypes,
|
|
3231
|
+
ssoProviders: showSsoProviders,
|
|
3232
|
+
ssoUsers: showSsoUsers,
|
|
3233
|
+
tokens: showServiceTokens,
|
|
3234
|
+
wiki: showWiki,
|
|
3235
|
+
changelog: showChangelog,
|
|
3236
|
+
sql: showSql
|
|
3237
|
+
};
|
|
3238
|
+
setActiveNavigation(view);
|
|
3239
|
+
const renderView = views[view];
|
|
3240
|
+
if (!renderView) throw new Error("Sezione console non valida.");
|
|
3241
|
+
await renderView();
|
|
3242
|
+
} catch (error) {
|
|
3243
|
+
render("");
|
|
3244
|
+
showNotice(error instanceof Error ? error.message : "Operazione non riuscita.", true);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
var loginForm = document.getElementById("login-form");
|
|
3248
|
+
if (loginForm) loginForm.onsubmit = loginView;
|
|
3249
|
+
byId("restart-login").onclick = resetLogin;
|
|
3250
|
+
var bootstrapAdminForm = document.getElementById("bootstrap-admin-form");
|
|
3251
|
+
if (bootstrapAdminForm) {
|
|
3252
|
+
bootstrapAdminForm.onsubmit = async (event) => {
|
|
3253
|
+
event.preventDefault();
|
|
3254
|
+
const status = document.getElementById("bootstrap-admin-status");
|
|
3255
|
+
try {
|
|
3256
|
+
if (status) status.textContent = "";
|
|
3257
|
+
const values = formValues(bootstrapAdminForm);
|
|
3258
|
+
const fields = new FormData(bootstrapAdminForm);
|
|
3259
|
+
const created = result(await bootstrapAdmin({
|
|
3260
|
+
token: values.token ?? "",
|
|
3261
|
+
email: values.email ?? "",
|
|
3262
|
+
nome: values.nome || void 0,
|
|
3263
|
+
cognome: values.cognome || void 0,
|
|
3264
|
+
password: values.password || void 0,
|
|
3265
|
+
superUser: fields.has("superUser")
|
|
3266
|
+
}));
|
|
3267
|
+
const generated = created.password ? ` Password generata (copiala ora): ${created.password}` : "";
|
|
3268
|
+
if (status) status.textContent = `Utente admin creato: ${created.email}.${generated} Ora accedi dal form principale.`;
|
|
3269
|
+
bootstrapAdminForm.reset();
|
|
3270
|
+
} catch (error) {
|
|
3271
|
+
if (status) status.textContent = error instanceof Error ? error.message : "Creazione non riuscita.";
|
|
3272
|
+
}
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
byId("two-factor-form").onsubmit = async (event) => {
|
|
3276
|
+
event.preventDefault();
|
|
3277
|
+
if (!pendingChallenge) return;
|
|
3278
|
+
try {
|
|
3279
|
+
byId("login-error").textContent = "";
|
|
3280
|
+
const values = formValues(event.currentTarget);
|
|
3281
|
+
await completeLoginStep(result(await verifyTwoFactor({ challengeId: pendingChallenge, code: values.code ?? "" })));
|
|
3282
|
+
} catch (error) {
|
|
3283
|
+
byId("login-error").textContent = error instanceof Error ? error.message : "Verifica non riuscita.";
|
|
3284
|
+
}
|
|
3285
|
+
};
|
|
3286
|
+
byId("resend-code").onclick = async () => {
|
|
3287
|
+
if (!pendingChallenge) return;
|
|
3288
|
+
try {
|
|
3289
|
+
byId("login-error").textContent = "";
|
|
3290
|
+
await completeLoginStep(result(await resendTwoFactor({ challengeId: pendingChallenge })));
|
|
3291
|
+
} catch (error) {
|
|
3292
|
+
byId("login-error").textContent = error instanceof Error ? error.message : "Reinvio non riuscito.";
|
|
3293
|
+
}
|
|
3294
|
+
};
|
|
3295
|
+
byId("logout").onclick = () => {
|
|
3296
|
+
resetLogin();
|
|
3297
|
+
token = null;
|
|
3298
|
+
currentUser = null;
|
|
3299
|
+
setAccessiConsoleToken(null);
|
|
3300
|
+
sessionStorage.removeItem("accessi-console-token");
|
|
3301
|
+
window.history.replaceState(null, "", consoleBasePath);
|
|
3302
|
+
void bootstrap();
|
|
3303
|
+
};
|
|
3304
|
+
byId("nav").onclick = (event) => {
|
|
3305
|
+
const link = event.target.closest("a[data-view]");
|
|
3306
|
+
if (!link) return;
|
|
3307
|
+
event.preventDefault();
|
|
3308
|
+
handleAction(() => navigate(link.dataset.view));
|
|
3309
|
+
};
|
|
3310
|
+
window.addEventListener("popstate", () => {
|
|
3311
|
+
if (token && currentUser) handleAction(() => show(viewFromLocation()));
|
|
3312
|
+
});
|
|
3313
|
+
window.addEventListener("accessi-console-network", (event) => {
|
|
3314
|
+
updateLoadingState(event.detail.active);
|
|
3315
|
+
});
|
|
3316
|
+
document.addEventListener("click", (event) => {
|
|
3317
|
+
const target = event.target;
|
|
3318
|
+
const dot = target.closest("[data-help]");
|
|
3319
|
+
if (dot) {
|
|
3320
|
+
event.preventDefault();
|
|
3321
|
+
if (helpAnchor === dot && helpPopover && !helpPopover.hidden) {
|
|
3322
|
+
closeHelp();
|
|
3323
|
+
} else {
|
|
3324
|
+
openHelp(dot);
|
|
3325
|
+
}
|
|
3326
|
+
return;
|
|
3327
|
+
}
|
|
3328
|
+
if (target.closest("[data-help-close]")) {
|
|
3329
|
+
closeHelp();
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
if (helpPopover && !helpPopover.hidden && !target.closest(".help-popover")) closeHelp();
|
|
3333
|
+
});
|
|
3334
|
+
window.addEventListener("keydown", (event) => {
|
|
3335
|
+
if (event.key === "Escape") closeHelp();
|
|
3336
|
+
});
|
|
3337
|
+
window.addEventListener("resize", positionHelp);
|
|
3338
|
+
window.addEventListener("scroll", positionHelp, true);
|
|
3339
|
+
var ssoChallenge = /^#two-factor=([a-f0-9]{64})$/.exec(window.location.hash)?.[1];
|
|
3340
|
+
if (ssoChallenge) {
|
|
3341
|
+
token = null;
|
|
3342
|
+
setAccessiConsoleToken(null);
|
|
3343
|
+
sessionStorage.removeItem("accessi-console-token");
|
|
3344
|
+
window.history.replaceState(null, "", window.location.pathname);
|
|
3345
|
+
finishBoot("login");
|
|
3346
|
+
showTwoFactor({ challengeId: ssoChallenge, resendAfterSeconds: 60 });
|
|
3347
|
+
} else if (token) {
|
|
3348
|
+
void bootstrap();
|
|
3349
|
+
} else {
|
|
3350
|
+
finishBoot("login");
|
|
3351
|
+
}
|