From 85efdb771409fbcafcc0664bf50320e5d8a7f3c3 Mon Sep 17 00:00:00 2001 From: Jaka Ramdani Date: Tue, 21 Apr 2026 06:25:33 +0700 Subject: [PATCH] initial commit --- .vscode/settings.json | 3 + README.md | 445 ++++++++++++++++++ docker-compose.yml | 56 +++ docs/frontend-api-surface.md | 335 +++++++++++++ docs/frontend-initial-prompt.md | 299 ++++++++++++ docs/sequence-diagrams.md | 417 ++++++++++++++++ pom.xml | 116 +++++ .../id/iptek/utms/UtmsNgBeApplication.java | 21 + .../java/id/iptek/utms/api/ApiResponse.java | 19 + .../id/iptek/utms/api/AuditController.java | 62 +++ .../id/iptek/utms/api/RoleController.java | 50 ++ .../id/iptek/utms/api/TenantController.java | 31 ++ .../id/iptek/utms/api/UserController.java | 136 ++++++ .../iptek/utms/auth/config/JwtProperties.java | 12 + .../utms/auth/config/LdapAuthConfig.java | 45 ++ .../utms/auth/config/LdapProperties.java | 17 + .../utms/auth/config/SecurityConfig.java | 86 ++++ .../utms/auth/controller/AuthController.java | 52 ++ .../auth/domain/AuthenticationSource.java | 6 + .../id/iptek/utms/auth/domain/Permission.java | 37 ++ .../iptek/utms/auth/domain/RefreshToken.java | 40 ++ .../java/id/iptek/utms/auth/domain/Role.java | 45 ++ .../java/id/iptek/utms/auth/domain/User.java | 55 +++ .../utms/auth/dto/AuthTokenResponse.java | 10 + .../auth/dto/CreateRoleManagementRequest.java | 13 + .../auth/dto/CreateUserManagementRequest.java | 26 + .../utms/auth/dto/CurrentUserResponse.java | 12 + .../id/iptek/utms/auth/dto/LoginRequest.java | 10 + .../iptek/utms/auth/dto/RefreshRequest.java | 9 + .../dto/UpdateRolePermissionsRequest.java | 12 + .../utms/auth/dto/UpdateUserRolesRequest.java | 12 + .../auth/repository/PermissionRepository.java | 16 + .../repository/RefreshTokenRepository.java | 13 + .../utms/auth/repository/RoleRepository.java | 16 + .../utms/auth/repository/UserRepository.java | 14 + .../security/JwtAuthenticationFilter.java | 87 ++++ .../iptek/utms/auth/security/JwtService.java | 91 ++++ .../TenantAwareUserDetailsService.java | 27 ++ .../utms/auth/security/UserPrincipal.java | 87 ++++ .../iptek/utms/auth/service/AuthService.java | 153 ++++++ .../auth/service/LoginThrottleService.java | 84 ++++ .../service/SingleLoginSessionService.java | 80 ++++ .../auth/service/TokenBlacklistService.java | 27 ++ .../service/UserRoleManagementService.java | 413 ++++++++++++++++ .../iptek/utms/auth/service/UserService.java | 37 ++ .../utms/core/audit/domain/AuditTrail.java | 69 +++ .../core/audit/dto/AuditTrailResponse.java | 23 + .../repository/AuditTrailRepository.java | 12 + .../core/audit/service/AuditTrailService.java | 146 ++++++ .../utms/core/config/ActiveMqConfig.java | 10 + .../utms/core/config/AuditLoggingAspect.java | 72 +++ .../id/iptek/utms/core/config/DataSeeder.java | 183 +++++++ .../id/iptek/utms/core/config/I18nConfig.java | 20 + .../utms/core/config/JpaAuditConfig.java | 20 + .../iptek/utms/core/config/LocaleConfig.java | 20 + .../iptek/utms/core/config/OpenApiConfig.java | 36 ++ .../iptek/utms/core/config/RedisConfig.java | 31 ++ .../id/iptek/utms/core/domain/BaseEntity.java | 48 ++ .../core/domain/TenantEntityListener.java | 14 + .../utms/core/exception/AppException.java | 9 + .../exception/GlobalExceptionHandler.java | 53 +++ .../iptek/utms/core/i18n/MessageResolver.java | 20 + .../utms/core/security/SecurityUtils.java | 19 + .../messaging/ApprovalCompletedEvent.java | 14 + .../utms/messaging/ApprovalEventConsumer.java | 27 ++ .../utms/messaging/ApprovalEventProducer.java | 19 + .../module/controller/ModuleController.java | 43 ++ .../utms/module/domain/SystemModule.java | 35 ++ .../iptek/utms/module/dto/ModuleResponse.java | 9 + .../utms/module/dto/ModuleToggleRequest.java | 5 + .../repository/SystemModuleRepository.java | 14 + .../id/iptek/utms/module/service/Module.java | 8 + .../module/service/ModuleRegistryService.java | 87 ++++ .../module/service/NotificationModule.java | 27 ++ .../preference/domain/UserUiPreference.java | 41 ++ .../dto/TablePreferenceProfile.java | 10 + .../dto/TablePreferenceRequest.java | 13 + .../dto/TablePreferenceSavedProfile.java | 12 + .../dto/UserUiPreferencesResponse.java | 11 + .../UserUiPreferenceRepository.java | 19 + .../service/UserPreferenceService.java | 187 ++++++++ .../java/id/iptek/utms/tenant/Tenant.java | 33 ++ .../id/iptek/utms/tenant/TenantContext.java | 30 ++ .../id/iptek/utms/tenant/TenantFilter.java | 35 ++ .../utms/tenant/TenantHibernateFilter.java | 44 ++ .../iptek/utms/tenant/TenantRepository.java | 11 + .../id/iptek/utms/tenant/TenantService.java | 22 + .../ApprovalWorkflowController.java | 71 +++ .../utms/workflow/domain/ApprovalAction.java | 9 + .../utms/workflow/domain/ApprovalHistory.java | 38 ++ .../utms/workflow/domain/ApprovalRequest.java | 46 ++ .../utms/workflow/domain/ApprovalStatus.java | 9 + .../utms/workflow/domain/ApprovalStep.java | 38 ++ .../workflow/dto/ApprovalActionRequest.java | 8 + .../workflow/dto/ApprovalRequestSummary.java | 21 + .../utms/workflow/dto/ApprovalResponse.java | 16 + .../workflow/dto/CreateApprovalRequest.java | 13 + .../repository/ApprovalHistoryRepository.java | 10 + .../repository/ApprovalRequestRepository.java | 34 ++ .../repository/ApprovalStepRepository.java | 12 + .../service/ApprovalWorkflowService.java | 327 +++++++++++++ src/main/resources/application-dev.yml | 48 ++ src/main/resources/application-local.yml | 48 ++ src/main/resources/application-prd.yml | 51 ++ src/main/resources/application.yml | 55 +++ src/main/resources/db/schema.sql | 172 +++++++ src/main/resources/i18n/messages.properties | 33 ++ .../resources/i18n/messages_id.properties | 33 ++ target/classes/application-dev.yml | 48 ++ target/classes/application-local.yml | 48 ++ target/classes/application-prd.yml | 51 ++ target/classes/application.yml | 55 +++ target/classes/db/schema.sql | 172 +++++++ target/classes/i18n/messages.properties | 33 ++ target/classes/i18n/messages_id.properties | 33 ++ .../id/iptek/utms/UtmsNgBeApplication.class | Bin 0 -> 1196 bytes .../id/iptek/utms/api/ApiResponse.class | Bin 0 -> 2835 bytes .../id/iptek/utms/api/AuditController.class | Bin 0 -> 4238 bytes .../id/iptek/utms/api/RoleController.class | Bin 0 -> 3292 bytes .../id/iptek/utms/api/TenantController.class | Bin 0 -> 1540 bytes .../id/iptek/utms/api/UserController.class | Bin 0 -> 8285 bytes .../utms/auth/config/JwtProperties.class | Bin 0 -> 1903 bytes .../utms/auth/config/LdapAuthConfig.class | Bin 0 -> 3098 bytes .../utms/auth/config/LdapProperties.class | Bin 0 -> 2844 bytes .../utms/auth/config/SecurityConfig.class | Bin 0 -> 7653 bytes .../utms/auth/controller/AuthController.class | Bin 0 -> 3639 bytes .../auth/domain/AuthenticationSource.class | Bin 0 -> 1236 bytes .../iptek/utms/auth/domain/Permission.class | Bin 0 -> 2445 bytes .../iptek/utms/auth/domain/RefreshToken.class | Bin 0 -> 2682 bytes .../id/iptek/utms/auth/domain/Role.class | Bin 0 -> 3140 bytes .../id/iptek/utms/auth/domain/User.class | Bin 0 -> 3895 bytes .../utms/auth/dto/AuthTokenResponse.class | Bin 0 -> 1941 bytes .../dto/CreateRoleManagementRequest.class | Bin 0 -> 2572 bytes .../dto/CreateUserManagementRequest.class | Bin 0 -> 3322 bytes .../utms/auth/dto/CurrentUserResponse.class | Bin 0 -> 2271 bytes .../id/iptek/utms/auth/dto/LoginRequest.class | Bin 0 -> 1903 bytes .../iptek/utms/auth/dto/RefreshRequest.class | Bin 0 -> 1681 bytes .../dto/UpdateRolePermissionsRequest.class | Bin 0 -> 2341 bytes .../auth/dto/UpdateUserRolesRequest.class | Bin 0 -> 2301 bytes .../repository/PermissionRepository.class | Bin 0 -> 865 bytes .../repository/RefreshTokenRepository.class | Bin 0 -> 729 bytes .../utms/auth/repository/RoleRepository.class | Bin 0 -> 835 bytes .../utms/auth/repository/UserRepository.class | Bin 0 -> 697 bytes .../security/JwtAuthenticationFilter.class | Bin 0 -> 4898 bytes .../iptek/utms/auth/security/JwtService.class | Bin 0 -> 4830 bytes .../TenantAwareUserDetailsService.class | Bin 0 -> 2595 bytes .../utms/auth/security/UserPrincipal.class | Bin 0 -> 3510 bytes .../iptek/utms/auth/service/AuthService.class | Bin 0 -> 9678 bytes .../auth/service/LoginThrottleService.class | Bin 0 -> 4691 bytes .../service/SingleLoginSessionService.class | Bin 0 -> 3923 bytes .../auth/service/TokenBlacklistService.class | Bin 0 -> 1982 bytes .../service/UserRoleManagementService$1.class | Bin 0 -> 919 bytes .../service/UserRoleManagementService.class | Bin 0 -> 21258 bytes .../iptek/utms/auth/service/UserService.class | Bin 0 -> 3536 bytes .../utms/core/audit/domain/AuditTrail.class | Bin 0 -> 4975 bytes .../core/audit/dto/AuditTrailResponse.class | Bin 0 -> 3847 bytes .../repository/AuditTrailRepository.class | Bin 0 -> 737 bytes .../audit/service/AuditTrailService.class | Bin 0 -> 7340 bytes .../utms/core/config/ActiveMqConfig.class | Bin 0 -> 469 bytes .../utms/core/config/AuditLoggingAspect.class | Bin 0 -> 3349 bytes .../iptek/utms/core/config/DataSeeder.class | Bin 0 -> 11742 bytes .../iptek/utms/core/config/I18nConfig.class | Bin 0 -> 972 bytes .../utms/core/config/JpaAuditConfig.class | Bin 0 -> 1800 bytes .../iptek/utms/core/config/LocaleConfig.class | Bin 0 -> 900 bytes .../utms/core/config/OpenApiConfig.class | Bin 0 -> 2617 bytes .../iptek/utms/core/config/RedisConfig.class | Bin 0 -> 2636 bytes .../iptek/utms/core/domain/BaseEntity.class | Bin 0 -> 2536 bytes .../core/domain/TenantEntityListener.class | Bin 0 -> 766 bytes .../utms/core/exception/AppException.class | Bin 0 -> 423 bytes .../exception/GlobalExceptionHandler.class | Bin 0 -> 5563 bytes .../utms/core/i18n/MessageResolver.class | Bin 0 -> 1064 bytes .../utms/core/security/SecurityUtils.class | Bin 0 -> 977 bytes .../messaging/ApprovalCompletedEvent.class | Bin 0 -> 2184 bytes .../messaging/ApprovalEventConsumer.class | Bin 0 -> 1627 bytes .../messaging/ApprovalEventProducer.class | Bin 0 -> 968 bytes .../module/controller/ModuleController.class | Bin 0 -> 2877 bytes .../utms/module/domain/SystemModule.class | Bin 0 -> 2081 bytes .../utms/module/dto/ModuleResponse.class | Bin 0 -> 1734 bytes .../utms/module/dto/ModuleToggleRequest.class | Bin 0 -> 1453 bytes .../repository/SystemModuleRepository.class | Bin 0 -> 798 bytes .../id/iptek/utms/module/service/Module.class | Bin 0 -> 277 bytes .../service/ModuleRegistryService.class | Bin 0 -> 6631 bytes .../module/service/NotificationModule.class | Bin 0 -> 1228 bytes .../preference/domain/UserUiPreference.class | Bin 0 -> 2254 bytes .../dto/TablePreferenceProfile.class | Bin 0 -> 1966 bytes .../dto/TablePreferenceRequest.class | Bin 0 -> 2357 bytes .../dto/TablePreferenceSavedProfile.class | Bin 0 -> 2212 bytes .../dto/UserUiPreferencesResponse.class | Bin 0 -> 2094 bytes .../UserUiPreferenceRepository.class | Bin 0 -> 992 bytes .../service/UserPreferenceService.class | Bin 0 -> 12315 bytes .../classes/id/iptek/utms/tenant/Tenant.class | Bin 0 -> 2123 bytes .../id/iptek/utms/tenant/TenantContext.class | Bin 0 -> 1209 bytes .../id/iptek/utms/tenant/TenantFilter.class | Bin 0 -> 1713 bytes .../utms/tenant/TenantHibernateFilter.class | Bin 0 -> 2166 bytes .../iptek/utms/tenant/TenantRepository.class | Bin 0 -> 542 bytes .../id/iptek/utms/tenant/TenantService.class | Bin 0 -> 1920 bytes .../ApprovalWorkflowController.class | Bin 0 -> 4929 bytes .../utms/workflow/domain/ApprovalAction.class | Bin 0 -> 1328 bytes .../workflow/domain/ApprovalHistory.class | Bin 0 -> 2810 bytes .../workflow/domain/ApprovalRequest.class | Bin 0 -> 3611 bytes .../utms/workflow/domain/ApprovalStatus.class | Bin 0 -> 1331 bytes .../utms/workflow/domain/ApprovalStep.class | Bin 0 -> 2947 bytes .../workflow/dto/ApprovalActionRequest.class | Bin 0 -> 1663 bytes .../workflow/dto/ApprovalRequestSummary.class | Bin 0 -> 3217 bytes .../utms/workflow/dto/ApprovalResponse.class | Bin 0 -> 2376 bytes .../workflow/dto/CreateApprovalRequest.class | Bin 0 -> 2453 bytes .../ApprovalHistoryRepository.class | Bin 0 -> 387 bytes .../ApprovalRequestRepository.class | Bin 0 -> 2329 bytes .../repository/ApprovalStepRepository.class | Bin 0 -> 710 bytes .../service/ApprovalWorkflowService.class | Bin 0 -> 16752 bytes .../compile/default-compile/createdFiles.lst | 0 .../compile/default-compile/inputFiles.lst | 94 ++++ .../default-testCompile/createdFiles.lst | 0 .../default-testCompile/inputFiles.lst | 0 214 files changed, 6821 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 docs/frontend-api-surface.md create mode 100644 docs/frontend-initial-prompt.md create mode 100644 docs/sequence-diagrams.md create mode 100644 pom.xml create mode 100644 src/main/java/id/iptek/utms/UtmsNgBeApplication.java create mode 100644 src/main/java/id/iptek/utms/api/ApiResponse.java create mode 100644 src/main/java/id/iptek/utms/api/AuditController.java create mode 100644 src/main/java/id/iptek/utms/api/RoleController.java create mode 100644 src/main/java/id/iptek/utms/api/TenantController.java create mode 100644 src/main/java/id/iptek/utms/api/UserController.java create mode 100644 src/main/java/id/iptek/utms/auth/config/JwtProperties.java create mode 100644 src/main/java/id/iptek/utms/auth/config/LdapAuthConfig.java create mode 100644 src/main/java/id/iptek/utms/auth/config/LdapProperties.java create mode 100644 src/main/java/id/iptek/utms/auth/config/SecurityConfig.java create mode 100644 src/main/java/id/iptek/utms/auth/controller/AuthController.java create mode 100644 src/main/java/id/iptek/utms/auth/domain/AuthenticationSource.java create mode 100644 src/main/java/id/iptek/utms/auth/domain/Permission.java create mode 100644 src/main/java/id/iptek/utms/auth/domain/RefreshToken.java create mode 100644 src/main/java/id/iptek/utms/auth/domain/Role.java create mode 100644 src/main/java/id/iptek/utms/auth/domain/User.java create mode 100644 src/main/java/id/iptek/utms/auth/dto/AuthTokenResponse.java create mode 100644 src/main/java/id/iptek/utms/auth/dto/CreateRoleManagementRequest.java create mode 100644 src/main/java/id/iptek/utms/auth/dto/CreateUserManagementRequest.java create mode 100644 src/main/java/id/iptek/utms/auth/dto/CurrentUserResponse.java create mode 100644 src/main/java/id/iptek/utms/auth/dto/LoginRequest.java create mode 100644 src/main/java/id/iptek/utms/auth/dto/RefreshRequest.java create mode 100644 src/main/java/id/iptek/utms/auth/dto/UpdateRolePermissionsRequest.java create mode 100644 src/main/java/id/iptek/utms/auth/dto/UpdateUserRolesRequest.java create mode 100644 src/main/java/id/iptek/utms/auth/repository/PermissionRepository.java create mode 100644 src/main/java/id/iptek/utms/auth/repository/RefreshTokenRepository.java create mode 100644 src/main/java/id/iptek/utms/auth/repository/RoleRepository.java create mode 100644 src/main/java/id/iptek/utms/auth/repository/UserRepository.java create mode 100644 src/main/java/id/iptek/utms/auth/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/id/iptek/utms/auth/security/JwtService.java create mode 100644 src/main/java/id/iptek/utms/auth/security/TenantAwareUserDetailsService.java create mode 100644 src/main/java/id/iptek/utms/auth/security/UserPrincipal.java create mode 100644 src/main/java/id/iptek/utms/auth/service/AuthService.java create mode 100644 src/main/java/id/iptek/utms/auth/service/LoginThrottleService.java create mode 100644 src/main/java/id/iptek/utms/auth/service/SingleLoginSessionService.java create mode 100644 src/main/java/id/iptek/utms/auth/service/TokenBlacklistService.java create mode 100644 src/main/java/id/iptek/utms/auth/service/UserRoleManagementService.java create mode 100644 src/main/java/id/iptek/utms/auth/service/UserService.java create mode 100644 src/main/java/id/iptek/utms/core/audit/domain/AuditTrail.java create mode 100644 src/main/java/id/iptek/utms/core/audit/dto/AuditTrailResponse.java create mode 100644 src/main/java/id/iptek/utms/core/audit/repository/AuditTrailRepository.java create mode 100644 src/main/java/id/iptek/utms/core/audit/service/AuditTrailService.java create mode 100644 src/main/java/id/iptek/utms/core/config/ActiveMqConfig.java create mode 100644 src/main/java/id/iptek/utms/core/config/AuditLoggingAspect.java create mode 100644 src/main/java/id/iptek/utms/core/config/DataSeeder.java create mode 100644 src/main/java/id/iptek/utms/core/config/I18nConfig.java create mode 100644 src/main/java/id/iptek/utms/core/config/JpaAuditConfig.java create mode 100644 src/main/java/id/iptek/utms/core/config/LocaleConfig.java create mode 100644 src/main/java/id/iptek/utms/core/config/OpenApiConfig.java create mode 100644 src/main/java/id/iptek/utms/core/config/RedisConfig.java create mode 100644 src/main/java/id/iptek/utms/core/domain/BaseEntity.java create mode 100644 src/main/java/id/iptek/utms/core/domain/TenantEntityListener.java create mode 100644 src/main/java/id/iptek/utms/core/exception/AppException.java create mode 100644 src/main/java/id/iptek/utms/core/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/id/iptek/utms/core/i18n/MessageResolver.java create mode 100644 src/main/java/id/iptek/utms/core/security/SecurityUtils.java create mode 100644 src/main/java/id/iptek/utms/messaging/ApprovalCompletedEvent.java create mode 100644 src/main/java/id/iptek/utms/messaging/ApprovalEventConsumer.java create mode 100644 src/main/java/id/iptek/utms/messaging/ApprovalEventProducer.java create mode 100644 src/main/java/id/iptek/utms/module/controller/ModuleController.java create mode 100644 src/main/java/id/iptek/utms/module/domain/SystemModule.java create mode 100644 src/main/java/id/iptek/utms/module/dto/ModuleResponse.java create mode 100644 src/main/java/id/iptek/utms/module/dto/ModuleToggleRequest.java create mode 100644 src/main/java/id/iptek/utms/module/repository/SystemModuleRepository.java create mode 100644 src/main/java/id/iptek/utms/module/service/Module.java create mode 100644 src/main/java/id/iptek/utms/module/service/ModuleRegistryService.java create mode 100644 src/main/java/id/iptek/utms/module/service/NotificationModule.java create mode 100644 src/main/java/id/iptek/utms/preference/domain/UserUiPreference.java create mode 100644 src/main/java/id/iptek/utms/preference/dto/TablePreferenceProfile.java create mode 100644 src/main/java/id/iptek/utms/preference/dto/TablePreferenceRequest.java create mode 100644 src/main/java/id/iptek/utms/preference/dto/TablePreferenceSavedProfile.java create mode 100644 src/main/java/id/iptek/utms/preference/dto/UserUiPreferencesResponse.java create mode 100644 src/main/java/id/iptek/utms/preference/repository/UserUiPreferenceRepository.java create mode 100644 src/main/java/id/iptek/utms/preference/service/UserPreferenceService.java create mode 100644 src/main/java/id/iptek/utms/tenant/Tenant.java create mode 100644 src/main/java/id/iptek/utms/tenant/TenantContext.java create mode 100644 src/main/java/id/iptek/utms/tenant/TenantFilter.java create mode 100644 src/main/java/id/iptek/utms/tenant/TenantHibernateFilter.java create mode 100644 src/main/java/id/iptek/utms/tenant/TenantRepository.java create mode 100644 src/main/java/id/iptek/utms/tenant/TenantService.java create mode 100644 src/main/java/id/iptek/utms/workflow/controller/ApprovalWorkflowController.java create mode 100644 src/main/java/id/iptek/utms/workflow/domain/ApprovalAction.java create mode 100644 src/main/java/id/iptek/utms/workflow/domain/ApprovalHistory.java create mode 100644 src/main/java/id/iptek/utms/workflow/domain/ApprovalRequest.java create mode 100644 src/main/java/id/iptek/utms/workflow/domain/ApprovalStatus.java create mode 100644 src/main/java/id/iptek/utms/workflow/domain/ApprovalStep.java create mode 100644 src/main/java/id/iptek/utms/workflow/dto/ApprovalActionRequest.java create mode 100644 src/main/java/id/iptek/utms/workflow/dto/ApprovalRequestSummary.java create mode 100644 src/main/java/id/iptek/utms/workflow/dto/ApprovalResponse.java create mode 100644 src/main/java/id/iptek/utms/workflow/dto/CreateApprovalRequest.java create mode 100644 src/main/java/id/iptek/utms/workflow/repository/ApprovalHistoryRepository.java create mode 100644 src/main/java/id/iptek/utms/workflow/repository/ApprovalRequestRepository.java create mode 100644 src/main/java/id/iptek/utms/workflow/repository/ApprovalStepRepository.java create mode 100644 src/main/java/id/iptek/utms/workflow/service/ApprovalWorkflowService.java create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application-local.yml create mode 100644 src/main/resources/application-prd.yml create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/db/schema.sql create mode 100644 src/main/resources/i18n/messages.properties create mode 100644 src/main/resources/i18n/messages_id.properties create mode 100644 target/classes/application-dev.yml create mode 100644 target/classes/application-local.yml create mode 100644 target/classes/application-prd.yml create mode 100644 target/classes/application.yml create mode 100644 target/classes/db/schema.sql create mode 100644 target/classes/i18n/messages.properties create mode 100644 target/classes/i18n/messages_id.properties create mode 100644 target/classes/id/iptek/utms/UtmsNgBeApplication.class create mode 100644 target/classes/id/iptek/utms/api/ApiResponse.class create mode 100644 target/classes/id/iptek/utms/api/AuditController.class create mode 100644 target/classes/id/iptek/utms/api/RoleController.class create mode 100644 target/classes/id/iptek/utms/api/TenantController.class create mode 100644 target/classes/id/iptek/utms/api/UserController.class create mode 100644 target/classes/id/iptek/utms/auth/config/JwtProperties.class create mode 100644 target/classes/id/iptek/utms/auth/config/LdapAuthConfig.class create mode 100644 target/classes/id/iptek/utms/auth/config/LdapProperties.class create mode 100644 target/classes/id/iptek/utms/auth/config/SecurityConfig.class create mode 100644 target/classes/id/iptek/utms/auth/controller/AuthController.class create mode 100644 target/classes/id/iptek/utms/auth/domain/AuthenticationSource.class create mode 100644 target/classes/id/iptek/utms/auth/domain/Permission.class create mode 100644 target/classes/id/iptek/utms/auth/domain/RefreshToken.class create mode 100644 target/classes/id/iptek/utms/auth/domain/Role.class create mode 100644 target/classes/id/iptek/utms/auth/domain/User.class create mode 100644 target/classes/id/iptek/utms/auth/dto/AuthTokenResponse.class create mode 100644 target/classes/id/iptek/utms/auth/dto/CreateRoleManagementRequest.class create mode 100644 target/classes/id/iptek/utms/auth/dto/CreateUserManagementRequest.class create mode 100644 target/classes/id/iptek/utms/auth/dto/CurrentUserResponse.class create mode 100644 target/classes/id/iptek/utms/auth/dto/LoginRequest.class create mode 100644 target/classes/id/iptek/utms/auth/dto/RefreshRequest.class create mode 100644 target/classes/id/iptek/utms/auth/dto/UpdateRolePermissionsRequest.class create mode 100644 target/classes/id/iptek/utms/auth/dto/UpdateUserRolesRequest.class create mode 100644 target/classes/id/iptek/utms/auth/repository/PermissionRepository.class create mode 100644 target/classes/id/iptek/utms/auth/repository/RefreshTokenRepository.class create mode 100644 target/classes/id/iptek/utms/auth/repository/RoleRepository.class create mode 100644 target/classes/id/iptek/utms/auth/repository/UserRepository.class create mode 100644 target/classes/id/iptek/utms/auth/security/JwtAuthenticationFilter.class create mode 100644 target/classes/id/iptek/utms/auth/security/JwtService.class create mode 100644 target/classes/id/iptek/utms/auth/security/TenantAwareUserDetailsService.class create mode 100644 target/classes/id/iptek/utms/auth/security/UserPrincipal.class create mode 100644 target/classes/id/iptek/utms/auth/service/AuthService.class create mode 100644 target/classes/id/iptek/utms/auth/service/LoginThrottleService.class create mode 100644 target/classes/id/iptek/utms/auth/service/SingleLoginSessionService.class create mode 100644 target/classes/id/iptek/utms/auth/service/TokenBlacklistService.class create mode 100644 target/classes/id/iptek/utms/auth/service/UserRoleManagementService$1.class create mode 100644 target/classes/id/iptek/utms/auth/service/UserRoleManagementService.class create mode 100644 target/classes/id/iptek/utms/auth/service/UserService.class create mode 100644 target/classes/id/iptek/utms/core/audit/domain/AuditTrail.class create mode 100644 target/classes/id/iptek/utms/core/audit/dto/AuditTrailResponse.class create mode 100644 target/classes/id/iptek/utms/core/audit/repository/AuditTrailRepository.class create mode 100644 target/classes/id/iptek/utms/core/audit/service/AuditTrailService.class create mode 100644 target/classes/id/iptek/utms/core/config/ActiveMqConfig.class create mode 100644 target/classes/id/iptek/utms/core/config/AuditLoggingAspect.class create mode 100644 target/classes/id/iptek/utms/core/config/DataSeeder.class create mode 100644 target/classes/id/iptek/utms/core/config/I18nConfig.class create mode 100644 target/classes/id/iptek/utms/core/config/JpaAuditConfig.class create mode 100644 target/classes/id/iptek/utms/core/config/LocaleConfig.class create mode 100644 target/classes/id/iptek/utms/core/config/OpenApiConfig.class create mode 100644 target/classes/id/iptek/utms/core/config/RedisConfig.class create mode 100644 target/classes/id/iptek/utms/core/domain/BaseEntity.class create mode 100644 target/classes/id/iptek/utms/core/domain/TenantEntityListener.class create mode 100644 target/classes/id/iptek/utms/core/exception/AppException.class create mode 100644 target/classes/id/iptek/utms/core/exception/GlobalExceptionHandler.class create mode 100644 target/classes/id/iptek/utms/core/i18n/MessageResolver.class create mode 100644 target/classes/id/iptek/utms/core/security/SecurityUtils.class create mode 100644 target/classes/id/iptek/utms/messaging/ApprovalCompletedEvent.class create mode 100644 target/classes/id/iptek/utms/messaging/ApprovalEventConsumer.class create mode 100644 target/classes/id/iptek/utms/messaging/ApprovalEventProducer.class create mode 100644 target/classes/id/iptek/utms/module/controller/ModuleController.class create mode 100644 target/classes/id/iptek/utms/module/domain/SystemModule.class create mode 100644 target/classes/id/iptek/utms/module/dto/ModuleResponse.class create mode 100644 target/classes/id/iptek/utms/module/dto/ModuleToggleRequest.class create mode 100644 target/classes/id/iptek/utms/module/repository/SystemModuleRepository.class create mode 100644 target/classes/id/iptek/utms/module/service/Module.class create mode 100644 target/classes/id/iptek/utms/module/service/ModuleRegistryService.class create mode 100644 target/classes/id/iptek/utms/module/service/NotificationModule.class create mode 100644 target/classes/id/iptek/utms/preference/domain/UserUiPreference.class create mode 100644 target/classes/id/iptek/utms/preference/dto/TablePreferenceProfile.class create mode 100644 target/classes/id/iptek/utms/preference/dto/TablePreferenceRequest.class create mode 100644 target/classes/id/iptek/utms/preference/dto/TablePreferenceSavedProfile.class create mode 100644 target/classes/id/iptek/utms/preference/dto/UserUiPreferencesResponse.class create mode 100644 target/classes/id/iptek/utms/preference/repository/UserUiPreferenceRepository.class create mode 100644 target/classes/id/iptek/utms/preference/service/UserPreferenceService.class create mode 100644 target/classes/id/iptek/utms/tenant/Tenant.class create mode 100644 target/classes/id/iptek/utms/tenant/TenantContext.class create mode 100644 target/classes/id/iptek/utms/tenant/TenantFilter.class create mode 100644 target/classes/id/iptek/utms/tenant/TenantHibernateFilter.class create mode 100644 target/classes/id/iptek/utms/tenant/TenantRepository.class create mode 100644 target/classes/id/iptek/utms/tenant/TenantService.class create mode 100644 target/classes/id/iptek/utms/workflow/controller/ApprovalWorkflowController.class create mode 100644 target/classes/id/iptek/utms/workflow/domain/ApprovalAction.class create mode 100644 target/classes/id/iptek/utms/workflow/domain/ApprovalHistory.class create mode 100644 target/classes/id/iptek/utms/workflow/domain/ApprovalRequest.class create mode 100644 target/classes/id/iptek/utms/workflow/domain/ApprovalStatus.class create mode 100644 target/classes/id/iptek/utms/workflow/domain/ApprovalStep.class create mode 100644 target/classes/id/iptek/utms/workflow/dto/ApprovalActionRequest.class create mode 100644 target/classes/id/iptek/utms/workflow/dto/ApprovalRequestSummary.class create mode 100644 target/classes/id/iptek/utms/workflow/dto/ApprovalResponse.class create mode 100644 target/classes/id/iptek/utms/workflow/dto/CreateApprovalRequest.class create mode 100644 target/classes/id/iptek/utms/workflow/repository/ApprovalHistoryRepository.class create mode 100644 target/classes/id/iptek/utms/workflow/repository/ApprovalRequestRepository.class create mode 100644 target/classes/id/iptek/utms/workflow/repository/ApprovalStepRepository.class create mode 100644 target/classes/id/iptek/utms/workflow/service/ApprovalWorkflowService.class create mode 100644 target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst create mode 100644 target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst create mode 100644 target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst create mode 100644 target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c857a26 --- /dev/null +++ b/README.md @@ -0,0 +1,445 @@ +# UTMS NG Backend (Spring Boot) + +Production-ready Spring Boot 3.x backend for user tenancy, RBAC, maker-checker workflow, module management, Redis caching/session support, ActiveMQ eventing, and i18n. + +## Table of Contents + +- [Project Overview](#project-overview) +- [Stack and Runtime Versions](#stack-and-runtime-versions) +- [High-Level Architecture](#high-level-architecture) +- [Repository and Package Layout](#repository-and-package-layout) +- [Getting Started](#getting-started) +- [Configuration](#configuration) +- [Security & AuthN/AuthZ](#security--authnauthz) +- [Multi-Tenancy](#multi-tenancy) +- [Workflow Engine](#workflow-engine) +- [Module System](#module-system) +- [API Documentation](#api-documentation) +- [Eventing and Messaging](#eventing-and-messaging) +- [Persistence Model](#persistence-model) +- [i18n and Error Handling](#i18n-and-error-handling) +- [Observability](#observability) +- [Sequence Diagrams](#sequence-diagrams) +- [Useful Commands](#useful-commands) +- [Contributing Notes](#contributing-notes) + +## Project Overview + +The system is organized into modular packages and service layers: + +- `api`: REST-facing controllers. +- `auth`: authentication, authorization, JWT, LDAP integration, token handling, and user-role primitives. +- `core`: cross-cutting concerns (errors, base entities, audit, caching, i18n, DB config). +- `tenant`: tenant context and tenant isolation filters. +- `workflow`: maker-checker workflow engine. +- `module`: pluggable feature module registry. +- `messaging`: ActiveMQ producer/consumer for async post-approval actions. + +## Stack and Runtime Versions + +The project runs on Java 17 and uses Spring Boot starter dependencies. + +- Java 17+ +- Spring Boot 3.3.5 +- Spring Security +- Spring Data JPA + Hibernate +- PostgreSQL +- Redis +- ActiveMQ +- Maven +- SpringDoc OpenAPI + +Main build and dependency file: +- [pom.xml](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/pom.xml) + +## High-Level Architecture + +The application follows request/response flow with cross-cutting servlet filters: + +1) `TenantFilter` resolves tenant ID from `X-Tenant-Id`. +2) `JwtAuthenticationFilter` authenticates bearer tokens when present. +3) Method-level security checks RBAC/permission requirements. +4) Business services apply transaction boundaries and audit/logging. +5) Workflow events are published to ActiveMQ on approval completion. + +Core architectural file references: + +- [application entrypoint](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/UtmsNgBeApplication.java) +- [security config](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/config/SecurityConfig.java) +- [openapi config](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/config/OpenApiConfig.java) +- [tenant context filter](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/tenant/TenantFilter.java) +- [tenant hibernate filter](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/tenant/TenantHibernateFilter.java) +- [tenant entity listener](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/domain/TenantEntityListener.java) +- [base entity and auditing](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/domain/BaseEntity.java) +- [audit trail](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/audit/domain/AuditTrail.java) + +## Repository and Package Layout + +Top-level structure: + +- `src/main/java/id/iptek/utms` +- `src/main/resources` +- `docs` (documentation) +- `docker-compose.yml` +- `pom.xml` + +Important package-level references: + +- [api controllers](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/api) +- [auth module](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth) +- [core module](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core) +- [tenant module](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/tenant) +- [workflow module](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow) +- [module system](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/module) +- [messaging](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/messaging) + +## Getting Started + +### Prerequisites + +- JDK 17 +- PostgreSQL 16 +- Redis 7+ +- ActiveMQ 5.18+ +- Maven +- PowerShell (for local commands in this environment) + +### Local run with Docker + +1) Start infrastructure: + +```shell +docker compose up -d +``` + +2) Build the application image and run as in compose: + +```shell +docker compose up --build -d +``` + +3) Access services: + +- Backend: `http://localhost:9191` +- Swagger UI: `http://localhost:9191/swagger-ui.html` +- API docs: `http://localhost:9191/v3/api-docs` +- Postgres: `localhost:5432` +- Redis: `localhost:6379` +- ActiveMQ admin: `http://localhost:8161` + +## Configuration + +Profiles are defined in: + +- [application.yml](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/resources/application.yml) +- [application-dev.yml](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/resources/application-dev.yml) +- [application-prd.yml](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/resources/application-prd.yml) +- [application-local.yml](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/resources/application-local.yml) +- [Docker compose](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/docker-compose.yml) + +Common config sections: + +- datasource: PostgreSQL connection +- data.redis: Redis host/port and cache config +- activemq: broker and credentials +- app.ldap: optional LDAP integration (disabled by default) +- app.security.login.*: login brute-force thresholds +- app.seed.enabled: bootstrap sample data flag + +Brute-force defaults for login: + +- max failed attempts: `app.security.login.max-failed-attempts` +- attempt window (seconds): `app.security.login.failed-attempt-window-seconds` +- lockout window (seconds): `app.security.login.lockout-duration-seconds` + +Single-session login option: +- `app.security.single-login.enabled` (default `false`) + - `true` = user can only have one active session at a time; new login invalidates previous access/refresh session. + +## Security & AuthN/AuthZ + +### Login and JWT + +- login endpoint: `POST /api/auth/login` +- refresh endpoint: `POST /api/auth/refresh` +- logout endpoint: `POST /api/auth/logout` +- JWT utility: [JwtService](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/security/JwtService.java) +- JWT principal adapter: [UserPrincipal](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/security/UserPrincipal.java) +- token filter: [JwtAuthenticationFilter](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/security/JwtAuthenticationFilter.java) +- auth service: [AuthService](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/service/AuthService.java) +- rate limit lockout service: [LoginThrottleService](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/service/LoginThrottleService.java) +- token blacklist: [TokenBlacklistService](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/service/TokenBlacklistService.java) +- refresh token entity: [RefreshToken](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/domain/RefreshToken.java) + +### RBAC model + +- Roles are prefixed with `ROLE_` in authorities via `UserPrincipal`. +- Permissions are loaded from `Role -> Permission` and exposed as authorities directly. +- Default protected role/permission checks use `@PreAuthorize`. +- Common checks: +- `hasRole('ADMIN')` +- `hasAuthority('WORKFLOW_APPROVE')` +- `hasAuthority('USER_MANAGE')` +- `hasAuthority('ROLE_MANAGE')` + +### Optional LDAP + +LDAP can be enabled without code changes using profile configuration. + +- `app.ldap.enabled=true` to switch it on. +- LDAP-specific properties are in [application.yml](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/resources/application.yml). +- Provider wiring is in [LdapAuthConfig](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/config/LdapAuthConfig.java). + +## Multi-Tenancy + +Tenant is always provided by: + +- `X-Tenant-Id` header via [TenantFilter](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/tenant/TenantFilter.java) +- JWT claim `tenant` via [JwtService](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/security/JwtService.java) + +Tenant context is thread-bound: +- set by `TenantContext` and used by services and entities. + +Tenant isolation strategy: +- Hibernate `tenantFilter` is defined on tenant-scoped entities. +- [TenantHibernateFilter](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/tenant/TenantHibernateFilter.java) enables filter per request. +- `BaseEntity` holds `tenant_id` for all shared tables. +- `TenantService` validates active tenant with cache. + +Tenant validation for active tenants: +- [TenantService](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/tenant/TenantService.java) +- [TenantRepository](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/tenant/TenantRepository.java) + +## Workflow Engine + +Workflow is required for user/role management operations and available as a first-class service. + +- workflow service: [ApprovalWorkflowService](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/service/ApprovalWorkflowService.java) +- approval controller: [ApprovalWorkflowController](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/controller/ApprovalWorkflowController.java) +- workflow entities: +- [ApprovalRequest](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/domain/ApprovalRequest.java) +- [ApprovalStep](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/domain/ApprovalStep.java) +- [ApprovalHistory](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/domain/ApprovalHistory.java) +- approval DTOs: [CreateApprovalRequest](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/dto/CreateApprovalRequest.java), [ApprovalActionRequest](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/dto/ApprovalActionRequest.java), [ApprovalResponse](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/dto/ApprovalResponse.java) + +Workflow state progression: + +- Maker creates request with required steps. +- Checker role per step enforces which role can approve. +- Each step is persisted as `PENDING`. +- Approve action updates step and request progress. +- Reject action sets request to `REJECTED`. +- Final approval publishes `ApprovalCompletedEvent` to ActiveMQ and user-role changes are applied by consumer. + +## Module System + +- module domain: [SystemModule](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/module/domain/SystemModule.java) +- module registry service: [ModuleRegistryService](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/module/service/ModuleRegistryService.java) +- module contract: [Module](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/module/service/Module.java) +- sample module: [NotificationModule](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/module/service/NotificationModule.java) + +Module operations: +- list: `GET /api/modules` +- toggle by code: `POST /api/modules/{code}/toggle` + +## API Documentation + +Swagger/OpenAPI: +- UI: `http://localhost:9191/swagger-ui.html` +- JSON spec: `http://localhost:9191/v3/api-docs` +- openapi settings: [OpenApiConfig](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/config/OpenApiConfig.java) + +### Endpoints + +Auth endpoints: +- POST `/api/auth/login` +- POST `/api/auth/refresh` +- POST `/api/auth/logout` + +Tenant endpoint: +- GET `/api/tenant/context` + +User endpoint: +- GET `/api/users/me` +- POST `/api/users/management/requests/create` +- POST `/api/users/management/requests/update-roles` + +Role endpoints: +- POST `/api/roles/management/requests/create` +- POST `/api/roles/management/requests/update-permissions` + +Workflow endpoints: +- POST `/api/workflow/request` +- POST `/api/workflow/{id}/approve` +- POST `/api/workflow/{id}/reject` + +Module endpoints: +- GET `/api/modules` +- POST `/api/modules/{code}/toggle` + +Audit endpoints: +- GET `/api/audit?limit=50` + +Health endpoint: +- GET `/actuator/health` + +Swagger-safe quick sample JSON: + +```json +{ + "username": "maker", + "password": "Passw0rd!" +} +``` + +```json +{ + "resourceType": "USER_MANAGEMENT", + "resourceId": "sample-user", + "payload": "{\"operation\":\"CREATE_USER\",\"username\":\"alice\"}", + "requiredSteps": 1 +} +``` + +```json +{ + "username": "admin", + "roleCodes": ["ADMIN"] +} +``` + +### Mandatory headers + +- Tenant: +- `X-Tenant-Id: acme` +- Locale: +- `Accept-Language: en-US` or `id-ID` +- Authorization: +- `Authorization: Bearer ` + +## Eventing and Messaging + +ActiveMQ integration: +- producer: [ApprovalEventProducer](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/messaging/ApprovalEventProducer.java) +- consumer: [ApprovalEventConsumer](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/messaging/ApprovalEventConsumer.java) +- event payload: [ApprovalCompletedEvent](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/messaging/ApprovalCompletedEvent.java) +- queue name: `approval.completed.queue` + +Post-approval process: +- event published when an approval request reaches `APPROVED`. +- consumer invokes [UserRoleManagementService.applyApprovedRequest](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/service/UserRoleManagementService.java). + +## Persistence Model + +### Naming and schema strategy + +- security tables use `sec_` prefix. +- workflow/system/audit tables use `sys_` prefix. +- This is maintained in all entities and confirmed in the schema file. + +Database schema reference: +- [schema.sql](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/resources/db/schema.sql) + +Core entities: + +- Tenant: [Tenant](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/tenant/Tenant.java) +- RBAC: +- User: [User](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/domain/User.java) +- Role: [Role](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/domain/Role.java) +- Permission: [Permission](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/domain/Permission.java) +- Refresh token: [RefreshToken](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/domain/RefreshToken.java) +- Workflow: +- ApprovalRequest: [ApprovalRequest](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/domain/ApprovalRequest.java) +- ApprovalStep: [ApprovalStep](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/domain/ApprovalStep.java) +- ApprovalHistory: [ApprovalHistory](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/domain/ApprovalHistory.java) +- Module: [SystemModule](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/module/domain/SystemModule.java) +- Audit: [AuditTrail](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/audit/domain/AuditTrail.java) + +### Repositories + +- [PermissionRepository](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/repository/PermissionRepository.java) +- [RoleRepository](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/repository/RoleRepository.java) +- [UserRepository](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/repository/UserRepository.java) +- [RefreshTokenRepository](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/auth/repository/RefreshTokenRepository.java) +- [AuditTrailRepository](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/audit/repository/AuditTrailRepository.java) +- [Workflow repositories](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/workflow/repository) + +## i18n and Error Handling + +Message bundles: +- default (en_US): [messages.properties](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/resources/i18n/messages.properties) +- indonesia locale (id_ID): [messages_id.properties](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/resources/i18n/messages_id.properties) + +Message resolution helper: +- [MessageResolver](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/i18n/MessageResolver.java) + +Global errors: +- [GlobalExceptionHandler](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/exception/GlobalExceptionHandler.java) +- app exception model: +- [AppException](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/exception/AppException.java) + +## Observability + +- Actuator: +- health/info endpoints via config in [application.yml](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/resources/application.yml) +- Audit logger: +- [AuditLoggingAspect](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/config/AuditLoggingAspect.java) +- persistent audit trail records: +- [AuditTrailService](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/audit/service/AuditTrailService.java) +- [Audit API](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/api/AuditController.java) + +## Sequence Diagrams + +Full controller interaction diagrams are available in: + +- [docs/sequence-diagrams.md](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/docs/sequence-diagrams.md) + +## Useful Commands + +Build only: + +```shell +mvn -q -DskipTests compile +``` + +Run tests: + +```shell +mvn test +``` + +Run locally (default profile): + +```shell +$env:SPRING_PROFILES_ACTIVE="local"; mvn spring-boot:run +``` + +Run dev profile: + +```shell +$env:SPRING_PROFILES_ACTIVE="dev"; mvn spring-boot:run +``` + +Run prd profile: + +```shell +$env:SPRING_PROFILES_ACTIVE="prd"; mvn spring-boot:run +``` + +## Contributing Notes + +Data seeding: +- enabled in `dev` and `local` via profile/setting. +- seed source: [DataSeeder](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/core/config/DataSeeder.java). +- to add bootstrap users/roles/permissions, modify this component intentionally. + +Extending modules: +- implement [Module](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/src/main/java/id/iptek/utms/module/service/Module.java) +- register as Spring component +- expose behavior in toggle handlers. + +Extending API: +- add DTOs under `auth|workflow|module|api` +- add service under domain package +- create controller endpoint and secure with `@PreAuthorize` +- update docs in this file and [docs/sequence-diagrams.md](/D:/Projects/Personal/IPTEK/utms-ng/utms-ng-be/docs/sequence-diagrams.md). diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bd0b97c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,56 @@ +services: + utms-ng-be: + build: + context: . + image: utms-ng-be:local + container_name: utms-ng-be + depends_on: + - postgres + - redis + - activemq + profiles: + - local + environment: + SPRING_PROFILES_ACTIVE: local + DB_URL: jdbc:postgresql://postgres:5432/utmsng + DB_USERNAME: utms + DB_PASSWORD: utms + REDIS_HOST: redis + REDIS_PORT: 6379 + ACTIVEMQ_BROKER_URL: tcp://activemq:61616 + ACTIVEMQ_USER: admin + ACTIVEMQ_PASSWORD: admin + JWT_SECRET: local-dev-fallback-jwt-secret-key-for-local-dev-environment-256-bits-min + ports: + - "9191:9191" + + postgres: + image: postgres:16 + container_name: utms-postgres + environment: + POSTGRES_DB: utmsng + POSTGRES_USER: utms + POSTGRES_PASSWORD: utms + ports: + - "5432:5432" + volumes: + - pg_data:/var/lib/postgresql/data + + redis: + image: redis:7 + container_name: utms-redis + ports: + - "6379:6379" + + activemq: + image: symptoma/activemq:5.18.3 + container_name: utms-activemq + environment: + ACTIVEMQ_ADMIN_LOGIN: admin + ACTIVEMQ_ADMIN_PASSWORD: admin + ports: + - "61616:61616" + - "8161:8161" + +volumes: + pg_data: diff --git a/docs/frontend-api-surface.md b/docs/frontend-api-surface.md new file mode 100644 index 0000000..47072cc --- /dev/null +++ b/docs/frontend-api-surface.md @@ -0,0 +1,335 @@ +# Frontend API Surface (Backend: Current Spring Boot) + +Use this as the exact frontend integration reference for the existing backend. + +## 1) API Envelope + +Most responses use: + +```ts +type ApiResponse = { + success: boolean + message: string + data: T + timestamp: string +} +``` + +### Error policy (actual backend behavior) +- Business errors and validation in request payloads return HTTP `400` with: + - `{ success: false, message: "...", data: null, timestamp: "..." }` +- Authorization failures return HTTP `403`: + - `{ success: false, message: "Access denied", data: null, timestamp: "..." }` +- Unhandled internal exceptions return HTTP `500` with: + - `{ success: false, message: "Internal server error", data: null, timestamp: "..." }` +- Authentication failures from Spring Security typically return `401` and are handled by frontend interceptor. +- JWT/session validation/blacklist failures can return `401` or be handled by security filters before controller. + +## 2) Global request headers + +For **every protected request** after login: +- `Authorization: Bearer ` +- `X-Tenant-Id: ` +- Optional: `Accept-Language: en-US` or `id-ID` + +`POST /api/auth/login` also requires: +- `X-Tenant-Id` + +## 3) Auth APIs + +### POST `/api/auth/login` + +Request: + +```ts +{ username: string; password: string } +``` + +Success (`200`): + +```ts +{ + "success": true, + "message": "Login successful", + "data": { + "tokenType": "Bearer", + "accessToken": "eyJhbGciOiJIUzI1NiJ9...", + "refreshToken": "...", + "expiresInSeconds": 900 + } +} +``` + +Common login failures: +- `401`: invalid credentials from security layer +- `400`: `{ message: "Invalid username or password" }` +- `400`: `{ message: "Account locked. Please try again in {0} seconds" }` +- LDAP mode + not provisioned local tenant user: `{ message: "LDAP user authenticated but not provisioned in this tenant" }` + +### POST `/api/auth/refresh` + +Request: + +```ts +{ refreshToken: string } +``` + +Success: + +```ts +{ + "success": true, + "message": "Token refreshed successfully", + "data": { + "tokenType": "Bearer", + "accessToken": "...", + "refreshToken": "...", + "expiresInSeconds": 900 + } +} +``` + +Failure: +- `400` with message `Refresh token not found` / `Token expired or revoked` + +### POST `/api/auth/logout` + +Request headers: +- `Authorization` and `X-Tenant-Id` +- optional body + +Success: + +```ts +{ "success": true, "message": "Logout successful", "data": null } +``` + +## 4) Profile API + +### GET `/api/users/me` + +Success: + +```ts +{ + "success": true, + "message": "Current user fetched successfully", + "data": { + "tenantId": "acme", + "username": "alice", + "roles": ["ADMIN", "USER_ROLE_ADMIN"], + "permissions": ["USER_MANAGE", "WORKFLOW_APPROVE", "ROLE_MANAGE"] + } +} +``` + +Use `roles` and `permissions` for: +- menu visibility +- action visibility +- route guards + +## 5) Tenant APIs + +### GET `/api/tenant/context` + +```ts +{ tenantId: "acme" } +``` + +## 6) User Management APIs (Workflow-first) + +### POST `/api/users/management/requests/create` + +#### Local mode (default) +```ts +{ + username: string, + password: string, // required local mode + enabled?: boolean, + roleCodes: string[] +} +``` + +#### LDAP mode +```ts +{ + username: string, + ldapDn?: string, // optional metadata + enabled?: boolean, + roleCodes: string[] +} +``` + +Success always returns workflow request: + +```ts +{ + "success": true, + "message": "User management request created", + "data": { + "id": "uuid", + "resourceType": "USER_MANAGEMENT", + "resourceId": "jane", + "status": "PENDING", + "requiredSteps": 1, + "currentStep": 0 + } +} +``` + +### POST `/api/users/management/requests/update-roles` + +```ts +{ username: string; roleCodes: string[] } +``` + +Returns same response shape as approval response. + +## 7) Role Management APIs (Workflow-first) + +### POST `/api/roles/management/requests/create` + +```ts +{ code: string; name: string; permissionCodes: string[] } +``` + +### POST `/api/roles/management/requests/update-permissions` + +```ts +{ code: string; permissionCodes: string[] } +``` + +Both return `ApprovalResponse` with request status PENDING. + +## 8) Workflow APIs + +### POST `/api/workflow/request` + +Generic endpoint for custom workflow request: + +```ts +{ + resourceType: string, + resourceId: string, + payload: string, + requiredSteps: number +} +``` + +### GET `/api/workflow/requests` + +Query params: +- `status` = `DRAFT|PENDING|APPROVED|REJECTED` (optional) +- `resourceType` (optional) +- `makerUsername` (optional) +- `limit` default `50`, max internally clamped to `200` + +Response list item: + +```ts +{ + id: "uuid", + tenantId: "acme", + resourceType: "USER_MANAGEMENT", + resourceId: "jane", + makerUsername: "alice", + payload: "{\"operation\":\"CREATE_USER\",...}", + status: "PENDING", + requiredSteps: 1, + currentStep: 0, + createdAt: "2026-04-20T08:00:00Z", + updatedAt: "2026-04-20T08:00:00Z" +} +``` + +### POST `/api/workflow/{id}/approve` + +```ts +{ notes?: string; checkerRole?: string } +``` + +- If `checkerRole` omitted, backend uses step role default (`CHECKER` unless overridden by system default). +- Maker cannot approve own request. +- Success returns updated `ApprovalResponse`. + +### POST `/api/workflow/{id}/reject` + +```ts +{ notes?: string; checkerRole?: string } +``` + +Same behavior and guards as approve. + +## 9) Module APIs (Admin only) + +### GET `/api/modules` + +Returns: + +```ts +{ code: string; name: string; enabled: boolean }[] +``` + +### POST `/api/modules/{code}/toggle` + +```ts +{ enabled: boolean } +``` + +Requires admin role. + +## 10) Audit APIs (Admin only) + +### GET `/api/audit?limit=50` + +Response items include at least: +- `id`, `tenantId`, `actor`, `correlationId`, `action`, `domain`, `resourceType`, `resourceId`, `outcome`, `httpMethod`, `requestPath`, `beforeState`, `afterState`, `details`, `createdAt`. + +Used for security/auditor trail and troubleshooting. + +## 11) Statuses and RBAC to gate UI + +- Approval status from backend: `DRAFT`, `PENDING`, `APPROVED`, `REJECTED`. +- User create / update role actions: `USER_MANAGE` OR `USER_ROLE_ADMIN` +- Role create / update permissions: `ROLE_MANAGE` OR `USER_ROLE_ADMIN` +- Workflow approve/reject: `WORKFLOW_APPROVE` OR `CHECKER` +- Workflow list: `WORKFLOW_APPROVE` OR `CHECKER` OR `ADMIN` +- Audit & modules listing/toggle: `ADMIN` +- Profile (`/api/users/me`): `USER_READ` OR `ADMIN` + +## 12) Frontend negative-path checklist (QA-ready) + +1. Login without tenant header should fail on protected flows. +2. Login with valid credentials but wrong tenant should fail on tenant-dependent services. +3. Repeated wrong password: + - eventually returns lockout message after configured threshold. +4. Create user (local mode) without password -> shows localized required validation error. +5. Create user (LDAP mode) with password payload should still be accepted by UI only if intentionally sent; backend ignores and should not rely on it. +6. Create user request duplicate username returns `400 User already exists`. +7. Workflow approve/reject where maker == checker returns error message `Maker cannot approve own request`. +8. Approving/rejecting without proper role returns `403`. +9. Audit API called by non-admin should return `403`. +10. Refresh with invalid token returns `400` and clear token state. + +## 13) Suggested QA smoke script + +- Validate auth: + - login, refresh, me, logout +- Validate tenant: + - switch tenant header and ensure data partitions by tenant +- Validate management flow: + - create user (local/LDAP variant) -> should appear in workflow as `PENDING` + - role create -> approval created + - approve/reject -> state transition to `APPROVED/REJECTED` +- Validate guard: + - hide actions by permissions and re-check with token from restricted user + +## 14) Setup checklist + +- Create `.env.local`: + - `NEXT_PUBLIC_API_BASE_URL=http://localhost:9191` + - `NEXT_PUBLIC_DEFAULT_TENANT=acme` + - `NEXT_PUBLIC_LOCALE=en` +- npm install / pnpm install +- Add Axios interceptor for auth and tenant headers +- Add 401/403 interceptor handling for logout and route redirect diff --git a/docs/frontend-initial-prompt.md b/docs/frontend-initial-prompt.md new file mode 100644 index 0000000..08f1880 --- /dev/null +++ b/docs/frontend-initial-prompt.md @@ -0,0 +1,299 @@ +You are a senior frontend engineer. Generate a **production-ready Next.js (App Router) admin dashboard** using **Tabler UI** as the design system. + +Use this prompt as the current source of truth for backend behavior. + +## Backend Summary (Current) + +- Base URL: `http://` (Swagger: `/swagger-ui.html`, OpenAPI: `/v3/api-docs`) +- Security: JWT (Bearer token) +- Multi-tenancy: required via header `X-Tenant-Id` +- Optional LDAP mode: `app.ldap.enabled` (backend switch) +- API pattern: most state-changing endpoints are workflow-driven approvals +- Default responses use: + +```ts +type ApiResponse = { + success: boolean + message: string + data: T + timestamp: string +} +``` + +## Tech Stack + +- Next.js (App Router) +- React 18+ +- TypeScript +- Tabler UI (CSS/React components) +- Axios +- Zustand (recommended) +- Tailwind (optional only for utility overrides) +- react-intl or next-intl for i18n + +## Recommended Project Structure + +- `app/` + - `(auth)/login/page.tsx` + - `(dashboard)/layout.tsx` + - `(dashboard)/page.tsx` + - `api-proxy/` or service barrel exports +- `components/` + - `layout/` (DashboardShell, Sidebar, Header) + - `ui/` (Table, Form, Modal, Alert, Badge, Drawer) + - `workflow/` (ApprovalTable, StatusBadge, ApprovalActionModal) + - `user/` (UserForm, UpdateRolesForm) + - `role/` (RoleForm, RolePermissionForm) +- `services/` + - `api.ts` + - `auth.ts` + - `users.ts` + - `workflow.ts` + - `tenant.ts` + - `audit.ts` +- `store/` + - `authStore.ts` + - `uiStore.ts` + - `tenantStore.ts` + - `permissionStore.ts` +- `hooks/` + - `useAuth.ts`, `useTenantHeader.ts`, `useApi.ts`, `usePermissions.ts` +- `types/` + - API contracts and DTO types + +## API Endpoints to Use (exact) + +### Auth +- `POST /api/auth/login` +- `POST /api/auth/refresh` +- `POST /api/auth/logout` + +### Authenticated profile +- `GET /api/users/me` + +### User management (workflow requests) +- `POST /api/users/management/requests/create` +- `POST /api/users/management/requests/update-roles` + +### Role management (workflow requests) +- `POST /api/roles/management/requests/create` +- `POST /api/roles/management/requests/update-permissions` + +### Workflow +- `POST /api/workflow/request` +- `POST /api/workflow/{id}/approve` +- `POST /api/workflow/{id}/reject` +- `GET /api/workflow/requests?status=PENDING&resourceType=...&makerUsername=...&limit=50` + +### Modules +- `GET /api/modules` +- `POST /api/modules/{code}/toggle` + +### Tenant & audit +- `GET /api/tenant/context` +- `GET /api/audit?limit=50` + +## Authentication and request headers + +For **every request** after login: +- `Authorization: Bearer ` +- `X-Tenant-Id: ` + +Login request also requires tenant context because backend resolves tenant at auth time: +- `POST /api/auth/login` with header `X-Tenant-Id` + +Logout request behavior: +- `POST /api/auth/logout` requires a valid Bearer token because backend invalidates/revokes refresh/session context. + +Optional: +- `Accept-Language: en-US` or `id-ID` + +## JWT/session behavior + +- Access token in response includes `tokenType: "Bearer"`, `accessToken`, `refreshToken`, `expiresIn` +- Store tokens in secure storage strategy (HTTP-only cookies preferred if possible; otherwise memory + storage hardening) +- Add request interceptor to attach token and `X-Tenant-Id` +- Add response interceptor for 401: + - clear auth state + - redirect to login + - keep tenant and locale selections persisted + +## Important authorization model + +Backend sends authorities as roles/permissions: +- Roles come as `ROLE_` (from DB role code) +- Permissions come as plain `...` codes +- Controllers currently check: + - User create/update-roles: `hasAuthority('USER_MANAGE') or hasRole('USER_ROLE_ADMIN')` + - Role create/update-permissions: `hasAuthority('ROLE_MANAGE') or hasRole('USER_ROLE_ADMIN')` + - Create workflow request: `hasAuthority('WORKFLOW_CREATE') or hasRole('MAKER')` + - Approve/reject: `hasAuthority('WORKFLOW_APPROVE') or hasRole('CHECKER')` + - Workflow list: `hasAuthority('WORKFLOW_APPROVE') or hasRole('CHECKER') or hasRole('ADMIN')` + - `/api/audit`: `hasRole('ADMIN')` + - `/api/users/me`: `hasAuthority('USER_READ') or hasRole('ADMIN')` + +So frontend should render actions conditionally using permissions derived from `/api/users/me`. + +## LDAP mode alignment + +Backend has optional LDAP mode (`app.ldap.enabled`). + +- **Local mode** + - `/api/users/management/requests/create` requires `password` +- **LDAP mode** + - Password is managed in directory (backend does not require password for user provisioning) + - `password` should not be sent for user creation + - optional `ldapDn` may be included +- Common for both modes + - user update roles still workflow-driven + - role create/update-permissions still workflow-driven + - no direct mutation endpoints for user/role entities + +## Required front-end behavior by page + +### 1) Login page +- Input: username, password, tenant selector +- Submit `POST /api/auth/login` +- Pass `X-Tenant-Id` header +- Handle error responses from backend localization keys and lockout messages + +### 2) Dashboard shell +- Sidebar: Dashboard, Users, Roles, Workflow, Audit, Modules, Settings +- Top bar: tenant selector, locale switch, user menu/logout +- Display auth mode indicator (Local / LDAP) when available + +### 3) Dashboard home +- Show summary cards: + - pending workflow count + - pending checker workload (from `/api/workflow/requests?status=PENDING`) + - audit/approval health snapshots (from `/api/audit?limit=50`) + - recent audits (from /api/audit) + +### 4) Users page +- There is no direct `/api/users` list endpoint in current backend, so derive list/context from workflow/request history and `/api/users/me` context. +- Actions: + - create user request (workflow) + - update user roles request (workflow) +- In LDAP mode hide password input on create form +- In local mode enforce password validation before submit + +### 5) Roles page +- No direct role list endpoint exists in current backend; show role/permission operations using current user context and workflow history as available. +- Implement create role request + permission update request flows. +- Permission selector from current in-app permission catalog (from `/api/users/me`, seeded defaults, and known workflow operations). + +### 6) Workflow page +- Show `/api/workflow/requests` with filters + - `status` (`DRAFT`, `PENDING`, `APPROVED`, `REJECTED`) + - `resourceType` + - `makerUsername` + - `limit` +- Actions: + - Approve modal + - Reject modal + - show notes and optional checkerRole (if omitted, backend uses step role default `CHECKER`) + +### 7) Audit page +- Admin-only +- `GET /api/audit?limit=50` +- render `action`, `resourceType`, `resourceId`, before/after snapshots, outcome, correlation id +- Keep pagination/infinite-load support for audit + workflow lists. + +## DTO references for implementation + +### Login +```ts +{ username: string; password: string } +``` + +### Create user management request +- Local mode: +```ts +{ + username: string + password: string + enabled?: boolean + roleCodes: string[] +} +``` +- LDAP mode: +```ts +{ + username: string + ldapDn?: string + enabled?: boolean + roleCodes: string[] +} +``` + +### Update user roles +```ts +{ username: string; roleCodes: string[] } +``` + +### Create role request +```ts +{ code: string; name: string; permissionCodes: string[] } +``` + +### Update role permissions +```ts +{ code: string; permissionCodes: string[] } +``` + +### Workflow action +```ts +{ notes?: string; checkerRole?: string } +``` + +### Create approval request (generic) +```ts +{ resourceType: string; resourceId: string; payload?: string; requiredSteps: number } +``` + +### Response from `/api/users/me` +```ts +{ tenantId: string; username: string; roles: string[]; permissions: string[] } +``` + +## UI requirements + +- Use Tabler-inspired components for + - tables + - forms + - modals + - badges + - alerts +- Keep navigation corporate and simple +- Add loading states, inline error states, and toast notifications +- Keep table columns configurable (search, sort, pagination) + +## Error handling + +Backend may return these patterns: +- Login failures with localized message +- Lockout message key in i18n when brute force threshold exceeded +- Standard `ApiResponse` with `success` false + +Frontend should: +- show notification from `message` +- maintain tenant context in state across page refresh/login switch +- keep unauthorized navigation blocked by RBAC-derived route guards + +## Delivery expectations + +Please generate runnable code for: +- `app/` shell and route layout +- Axios client with interceptors +- Login/auth flow +- Tenant-aware request wrapper +- Users module screens + workflow request forms +- Roles module screens + workflow request forms +- Workflow list/detail with approve/reject action +- Audit list +- Reusable table/form/modal components + +Please include a short setup checklist: +- env vars (`NEXT_PUBLIC_API_BASE_URL` etc) +- install commands +- run instructions diff --git a/docs/sequence-diagrams.md b/docs/sequence-diagrams.md new file mode 100644 index 0000000..6165a88 --- /dev/null +++ b/docs/sequence-diagrams.md @@ -0,0 +1,417 @@ +# Controller Sequence Diagrams + +All diagrams are in Mermaid syntax (` ```mermaid `) and can be rendered by GitHub, IntelliJ, VS Code Mermaid extensions, and most markdown tools. + +## 1) AuthController (`/api/auth`) + +### 1.1 POST `/api/auth/login` +```mermaid +sequenceDiagram + autonumber + actor Client + participant AC as AuthController + participant AF as AuthService + participant TF as TenantFilter + participant TS as TenantService + participant LT as LoginThrottleService + participant AM as AuthenticationManager + participant JR as JwtService + participant UR as UserRepository + participant RTR as RefreshTokenRepository + + Client->>AC: POST /api/auth/login {tenant, username, password} + AC->>AC: MessageResolver message key + AC-->>TF: tenant header (X-Tenant-Id) + TF->>TF: TenantContext.setTenantId(tenant) + + AC->>AF: login(request) + AF->>TS: getActiveTenant(tenantId) + TS-->>AF: tenant entity + AF->>LT: ensureAllowed(tenantId, username) + alt account locked + LT-->>AF: AppException(auth.login.locked) + else allowed + AF->>AM: authenticate(UsernamePasswordAuthenticationToken) + alt invalid credential + AM-->>AF: AuthenticationException + AF->>LT: recordFailure(tenantId, username) + AF-->>AC: throw AppException(auth.invalid.credentials) + AC-->>Client: 400 Error via GlobalExceptionHandler + else success + AF->>LT: recordSuccess(tenantId, username) + AF->>UR: findByTenantIdAndUsername + UR-->>AF: User + AF->>AF: build UserPrincipal + AF->>JR: generateAccessToken(principal) + AF->>JR: generateRefreshToken(principal) + AF->>RTR: delete old refresh tokens + AF->>RTR: save RefreshToken + AF-->>AC: AuthTokenResponse(Bearer, access, refresh) + AC-->>Client: 200 {success:true, message, token} + end + end +``` + +### 1.2 POST `/api/auth/refresh` +```mermaid +sequenceDiagram + autonumber + actor Client + participant AF as AuthService + participant AC as AuthController + participant TF as TenantFilter + participant RTR as RefreshTokenRepository + participant JR as JwtService + participant UR as UserRepository + + Client->>AC: POST /api/auth/refresh {tenant, refreshToken} + AC-->>TF: resolve tenant header + AC->>AF: refresh(request) + AF->>AF: TenantContext.getRequiredTenantId + AF->>RTR: findByTokenAndTenantId(refreshToken, tenantId) + alt token not found + RTR-->>AF: empty + AF-->>AC: AppException("Refresh token not found") + AC-->>Client: 400 via GlobalExceptionHandler + else token found + AF->>AF: validate revoked/expired + AF->>JR: parseClaims(refreshToken) + AF->>UR: findByTenantIdAndUsername(claims.sub) + AF->>JR: generateAccessToken(principal) + AF-->>AC: AuthTokenResponse(new access token) + AC-->>Client: 200 {success:true, message, token} + end +``` + +### 1.3 POST `/api/auth/logout` +```mermaid +sequenceDiagram + autonumber + actor Client + participant AC as AuthController + participant SE as Spring Security + participant AF as AuthService + participant TBS as TokenBlacklistService + participant JR as JwtService + + Client->>AC: POST /api/auth/logout Authorization: Bearer + AC->>SE: isAuthenticated() method security + alt unauthenticated + SE-->>Client: 401/403 + else authenticated + AC->>AF: logout(bearer token) + alt token missing/blank + AF->>AF: return + else token present + AF->>JR: parseClaims(access token) + AF->>AF: compute ttl from exp + AF->>TBS: blacklist(token, ttl) + end + AF-->>AC: void + AC-->>Client: 200 {success:true, logout message} + end +``` + +## 2) WorkflowController (`/api/workflow`) + +### 2.1 POST `/api/workflow/request` +```mermaid +sequenceDiagram + autonumber + actor Client + participant SC as Spring Security + participant WC as ApprovalWorkflowController + participant AFS as ApprovalWorkflowService + participant TS as TenantService + participant AR as ApprovalRequestRepository + participant ASR as ApprovalStepRepository + participant AH as ApprovalHistoryRepository + participant AT as AuditTrailService + + Client->>WC: POST /api/workflow/request (workflow payload) + WC->>SC: hasAuthority('WORKFLOW_CREATE') OR hasRole('MAKER') + alt auth failed + SC-->>Client: 403 + else authorized + WC->>AFS: createRequest(dto, servletRequest) + AFS->>TS: getActiveTenant(tenantId) + AFS->>AFS: resolve maker + checkerRole default + AFS->>AR: save ApprovalRequest(PENDING, status=0) + loop for each requiredSteps + AFS->>ASR: save ApprovalStep(stepOrder, CHECKER role) + end + AFS->>AH: addHistory(action=CREATE) + AFS->>AT: record(ACTION_CREATE, before=null, after=snapshot) + AFS-->>WC: ApprovalResponse + WC-->>Client: 200 workflow.request.created + end +``` + +### 2.2 POST `/api/workflow/{id}/approve` +```mermaid +sequenceDiagram + autonumber + actor Client + participant SC as Spring Security + participant WC as ApprovalWorkflowController + participant AFS as ApprovalWorkflowService + participant AR as ApprovalRequestRepository + participant ASR as ApprovalStepRepository + participant AH as ApprovalHistoryRepository + participant EVT as ApprovalEventProducer + participant AT as AuditTrailService + + Client->>WC: POST /api/workflow/{id}/approve (action notes) + WC->>SC: hasAuthority('WORKFLOW_APPROVE') OR hasRole('CHECKER') + alt auth failed + SC-->>Client: 403 + else authorized + WC->>AFS: approve(id, dto, auth, servletRequest) + AFS->>AR: findByIdAndTenantId(id, tenantId) + alt request not found or not pending + AFS-->>WC: AppException + WC-->>Client: 400 via handler + else valid + AFS->>ASR: find current step + AFS->>SC: validateCheckerRole(auth, expectedRole) + AFS->>ASR: save step status=APPROVED + AFS->>AR: update currentStep + alt all steps completed + AFS->>AR: set request status=APPROVED + AFS->>EVT: publishCompleted(ApprovalCompletedEvent) + end + AFS->>AH: addHistory(action=APPROVE) + AFS->>AT: record(ACTION_APPROVE, before/after states) + AFS-->>WC: ApprovalResponse + WC-->>Client: 200 workflow.request.approved + end + end +``` + +### 2.3 POST `/api/workflow/{id}/reject` +```mermaid +sequenceDiagram + autonumber + actor Client + participant SC as Spring Security + participant WC as ApprovalWorkflowController + participant AFS as ApprovalWorkflowService + participant AR as ApprovalRequestRepository + participant ASR as ApprovalStepRepository + participant AH as ApprovalHistoryRepository + participant AT as AuditTrailService + + Client->>WC: POST /api/workflow/{id}/reject (action notes) + WC->>SC: hasAuthority('WORKFLOW_APPROVE') OR hasRole('CHECKER') + alt auth failed + SC-->>Client: 403 + else authorized + WC->>AFS: reject(id, dto, auth, servletRequest) + AFS->>AR: findByIdAndTenantId(id, tenantId) + alt request not found or not pending + AFS-->>WC: AppException + WC-->>Client: 400 via handler + else valid + AFS->>ASR: find current step + AFS->>SC: validateCheckerRole(auth, expectedRole) + AFS->>ASR: save step status=REJECTED + AFS->>AR: set request status=REJECTED + AFS->>AH: addHistory(action=REJECT) + AFS->>AT: record(ACTION_REJECT, before/after states) + AFS-->>WC: ApprovalResponse + WC-->>Client: 200 workflow.request.rejected + end + end +``` + +## 3) UserController (`/api/users`) + +### 3.1 GET `/api/users/me` +```mermaid +sequenceDiagram + autonumber + actor Client + participant SC as Security Filter + Method Security + participant UC as UserController + participant US as UserService + participant UR as UserRepository + + Client->>UC: GET /api/users/me + UC->>SC: hasAuthority('USER_READ') OR hasRole('ADMIN') + alt unauthorized + SC-->>Client: 403 + else authorized + UC->>US: me(authentication.username) + US->>UR: findByTenantIdAndUsername + UR-->>US: User + US-->>UC: CurrentUserResponse(roles, permissions) + UC-->>Client: 200 {tenantId, user details} + end +``` + +### 3.2 POST `/api/users/management/requests/create` +```mermaid +sequenceDiagram + autonumber + actor Client + participant SC as Spring Security + participant UC as UserController + participant URS as UserRoleManagementService + participant AS as ApprovalWorkflowService + participant AR as ApprovalRequestRepository + + Client->>UC: POST /api/users/management/requests/create + UC->>SC: hasAuthority('USER_MANAGE') OR hasRole('USER_ROLE_ADMIN') + alt unauthorized + SC-->>Client: 403 + else authorized + UC->>URS: submitCreateUserRequest(request, servletRequest) + URS->>AS: createRequest(resource=USER_MANAGEMENT, requiredSteps=1, checkerRole=USER_ROLE_ADMIN) + AS->>AR: persist pending approval request + steps + AS-->>URS: ApprovalResponse + URS-->>UC: ApprovalResponse + UC-->>Client: 200 user.management.request.created + end +``` + +### 3.3 POST `/api/users/management/requests/update-roles` +```mermaid +sequenceDiagram + autonumber + actor Client + participant SC as Spring Security + participant UC as UserController + participant URS as UserRoleManagementService + participant AR as ApprovalRequestRepository + + Client->>UC: POST /api/users/management/requests/update-roles + UC->>SC: hasAuthority('USER_MANAGE') OR hasRole('USER_ROLE_ADMIN') + alt unauthorized + SC-->>Client: 403 + else authorized + UC->>URS: submitUpdateUserRolesRequest(request) + URS->>AR: validate user + roles, build payload + URS->>AR: create approval request with step checker role + URS-->>UC: ApprovalResponse + UC-->>Client: 200 user.management.request.created + end +``` + +## 4) RoleController (`/api/roles`) + +### 4.1 POST `/api/roles/management/requests/create` +```mermaid +sequenceDiagram + autonumber + actor Client + participant SC as Spring Security + participant RC as RoleController + participant URS as UserRoleManagementService + participant AS as ApprovalWorkflowService + + Client->>RC: POST /api/roles/management/requests/create + RC->>SC: hasAuthority('ROLE_MANAGE') OR hasRole('USER_ROLE_ADMIN') + alt unauthorized + SC-->>Client: 403 + else authorized + RC->>URS: submitCreateRoleRequest(request, servletRequest) + URS->>AS: createRequest(resource=ROLE_MANAGEMENT, requiredSteps=1) + AS-->>URS: ApprovalResponse + URS-->>RC: ApprovalResponse + RC-->>Client: 200 role.management.request.created + end +``` + +### 4.2 POST `/api/roles/management/requests/update-permissions` +```mermaid +sequenceDiagram + autonumber + actor Client + participant SC as Spring Security + participant RC as RoleController + participant URS as UserRoleManagementService + participant AS as ApprovalWorkflowService + + Client->>RC: POST /api/roles/management/requests/update-permissions + RC->>SC: hasAuthority('ROLE_MANAGE') OR hasRole('USER_ROLE_ADMIN') + alt unauthorized + SC-->>Client: 403 + else authorized + RC->>URS: submitUpdateRolePermissionsRequest(request, servletRequest) + URS->>AS: createRequest(resource=ROLE_MANAGEMENT, requiredSteps=1) + AS-->>URS: ApprovalResponse + URS-->>RC: ApprovalResponse + RC-->>Client: 200 role.management.request.created + end +``` + +## 5) AuditController (`/api/audit`) + +### 5.1 GET `/api/audit?limit={n}` +```mermaid +sequenceDiagram + autonumber + actor Client + participant SC as Spring Security + participant AC as AuditController + participant TS as TenantContext + participant ATS as AuditTrailService + participant ARepo as AuditTrailRepository + + Client->>AC: GET /api/audit?limit=50 + AC->>SC: hasRole('ADMIN') + alt unauthorized + SC-->>Client: 403 + else authorized + AC->>TS: getRequiredTenantId() + AC->>ATS: listRecent(tenantId, limit) + ATS->>ARepo: find top by tenant/order by createdAt desc + ARepo-->>ATS: list audit entities + ATS-->>AC: mapped list entities + AC-->>AC: map to AuditTrailResponse DTOs + AC-->>Client: 200 audit.list.success + end +``` + +## 6) TenantController (`/api/tenant`) + +### 6.1 GET `/api/tenant/context` +```mermaid +sequenceDiagram + autonumber + actor Client + participant SC as Spring Security + participant TC as TenantController + participant TCX as TenantContext + + Client->>TC: GET /api/tenant/context + TC->>SC: isAuthenticated() + alt unauthenticated + SC-->>Client: 401/403 + else authenticated + TC->>TCX: getRequiredTenantId() + TC-->>Client: 200 {tenantId} + end +``` + +## Common Cross-Cutting Exception Flow + +### Validation and Error handling for all controllers +```mermaid +sequenceDiagram + autonumber + actor Client + participant Controller + participant Handler as GlobalExceptionHandler + + Client->>Controller: Invalid body / business rule violation + Controller-->>Handler: throws MethodArgumentNotValidException or AppException + alt AppException + Handler-->>Client: 400 ApiResponse.fail(message) + else AccessDenied + Handler-->>Client: 403 ApiResponse.fail("error.forbidden") + else General exception + Handler-->>Client: 500 ApiResponse.fail("error.internal") + end +``` + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..796ef05 --- /dev/null +++ b/pom.xml @@ -0,0 +1,116 @@ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.5 + + + + id.iptek + utms-ng-be + 1.0.0 + utms-ng-be + Multi-tenant Spring Boot backend with workflow and modular system + + + 17 + 0.12.6 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-activemq + + + org.springframework.boot + spring-boot-starter-data-ldap + + + org.springframework.security + spring-security-ldap + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + + + + org.postgresql + postgresql + runtime + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/main/java/id/iptek/utms/UtmsNgBeApplication.java b/src/main/java/id/iptek/utms/UtmsNgBeApplication.java new file mode 100644 index 0000000..f0b2edd --- /dev/null +++ b/src/main/java/id/iptek/utms/UtmsNgBeApplication.java @@ -0,0 +1,21 @@ +package id.iptek.utms; + +import java.time.ZoneId; +import java.util.Locale; +import java.util.TimeZone; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; + +@EnableCaching +@SpringBootApplication +public class UtmsNgBeApplication { + + public static void main(String[] args) { + TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Asia/Jakarta"))); + Locale.setDefault(Locale.forLanguageTag("en-US")); + SpringApplication.run(UtmsNgBeApplication.class, args); + } +} + diff --git a/src/main/java/id/iptek/utms/api/ApiResponse.java b/src/main/java/id/iptek/utms/api/ApiResponse.java new file mode 100644 index 0000000..286d34a --- /dev/null +++ b/src/main/java/id/iptek/utms/api/ApiResponse.java @@ -0,0 +1,19 @@ +package id.iptek.utms.api; + +import java.time.Instant; + +public record ApiResponse( + boolean success, + String message, + T data, + Instant timestamp +) { + public static ApiResponse ok(String message, T data) { + return new ApiResponse<>(true, message, data, Instant.now()); + } + + public static ApiResponse fail(String message) { + return new ApiResponse<>(false, message, null, Instant.now()); + } +} + diff --git a/src/main/java/id/iptek/utms/api/AuditController.java b/src/main/java/id/iptek/utms/api/AuditController.java new file mode 100644 index 0000000..f625ce2 --- /dev/null +++ b/src/main/java/id/iptek/utms/api/AuditController.java @@ -0,0 +1,62 @@ +package id.iptek.utms.api; + +import id.iptek.utms.core.audit.dto.AuditTrailResponse; +import id.iptek.utms.core.audit.domain.AuditTrail; +import id.iptek.utms.core.audit.service.AuditTrailService; +import id.iptek.utms.core.i18n.MessageResolver; +import id.iptek.utms.tenant.TenantContext; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/audit") +@SecurityRequirement(name = "bearerAuth") +public class AuditController { + + private final AuditTrailService auditTrailService; + private final MessageResolver messageResolver; + + public AuditController(AuditTrailService auditTrailService, MessageResolver messageResolver) { + this.auditTrailService = auditTrailService; + this.messageResolver = messageResolver; + } + + @GetMapping + @PreAuthorize("hasRole('ADMIN')") + public ApiResponse> listRecent(@RequestParam(defaultValue = "50") int limit) { + String tenantId = TenantContext.getRequiredTenantId(); + List trails = auditTrailService.listRecent(tenantId, limit); + + return ApiResponse.ok( + messageResolver.get("audit.list.success"), + trails.stream().map(this::toResponse).toList() + ); + } + + private AuditTrailResponse toResponse(AuditTrail trail) { + return new AuditTrailResponse( + trail.getId(), + trail.getTenantId(), + trail.getActor(), + trail.getCorrelationId(), + trail.getAction(), + trail.getDomain(), + trail.getResourceType(), + trail.getResourceId(), + trail.getOutcome(), + trail.getHttpMethod(), + trail.getRequestPath(), + trail.getErrorMessage(), + trail.getBeforeState(), + trail.getAfterState(), + trail.getDetails(), + trail.getCreatedAt() + ); + } +} diff --git a/src/main/java/id/iptek/utms/api/RoleController.java b/src/main/java/id/iptek/utms/api/RoleController.java new file mode 100644 index 0000000..a1bcc52 --- /dev/null +++ b/src/main/java/id/iptek/utms/api/RoleController.java @@ -0,0 +1,50 @@ +package id.iptek.utms.api; + +import id.iptek.utms.auth.dto.CreateRoleManagementRequest; +import id.iptek.utms.auth.dto.UpdateRolePermissionsRequest; +import id.iptek.utms.auth.service.UserRoleManagementService; +import id.iptek.utms.core.i18n.MessageResolver; +import id.iptek.utms.workflow.dto.ApprovalResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/roles") +@SecurityRequirement(name = "bearerAuth") +public class RoleController { + + private final UserRoleManagementService userRoleManagementService; + private final MessageResolver messageResolver; + + public RoleController(UserRoleManagementService userRoleManagementService, + MessageResolver messageResolver) { + this.userRoleManagementService = userRoleManagementService; + this.messageResolver = messageResolver; + } + + @PostMapping("/management/requests/create") + @PreAuthorize("hasAuthority('ROLE_MANAGE') or hasRole('USER_ROLE_ADMIN')") + public ApiResponse create(@Valid @RequestBody CreateRoleManagementRequest request, + HttpServletRequest servletRequest) { + return ApiResponse.ok( + messageResolver.get("role.management.request.created"), + userRoleManagementService.submitCreateRoleRequest(request, servletRequest) + ); + } + + @PostMapping("/management/requests/update-permissions") + @PreAuthorize("hasAuthority('ROLE_MANAGE') or hasRole('USER_ROLE_ADMIN')") + public ApiResponse updatePermissions(@Valid @RequestBody UpdateRolePermissionsRequest request, + HttpServletRequest servletRequest) { + return ApiResponse.ok( + messageResolver.get("role.management.request.created"), + userRoleManagementService.submitUpdateRolePermissionsRequest(request, servletRequest) + ); + } +} diff --git a/src/main/java/id/iptek/utms/api/TenantController.java b/src/main/java/id/iptek/utms/api/TenantController.java new file mode 100644 index 0000000..aae3af8 --- /dev/null +++ b/src/main/java/id/iptek/utms/api/TenantController.java @@ -0,0 +1,31 @@ +package id.iptek.utms.api; + +import id.iptek.utms.core.i18n.MessageResolver; +import id.iptek.utms.tenant.TenantContext; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/api/tenant") +@SecurityRequirement(name = "bearerAuth") +public class TenantController { + + private final MessageResolver messageResolver; + + public TenantController(MessageResolver messageResolver) { + this.messageResolver = messageResolver; + } + + @GetMapping("/context") + @PreAuthorize("isAuthenticated()") + public ApiResponse> tenantContext() { + return ApiResponse.ok("Tenant context resolved", + Map.of("tenantId", TenantContext.getRequiredTenantId())); + } +} + diff --git a/src/main/java/id/iptek/utms/api/UserController.java b/src/main/java/id/iptek/utms/api/UserController.java new file mode 100644 index 0000000..80c37a5 --- /dev/null +++ b/src/main/java/id/iptek/utms/api/UserController.java @@ -0,0 +1,136 @@ +package id.iptek.utms.api; + +import id.iptek.utms.auth.dto.CreateUserManagementRequest; +import id.iptek.utms.auth.dto.CurrentUserResponse; +import id.iptek.utms.auth.dto.UpdateUserRolesRequest; +import id.iptek.utms.auth.service.UserRoleManagementService; +import id.iptek.utms.auth.service.UserService; +import id.iptek.utms.core.exception.AppException; +import id.iptek.utms.core.i18n.MessageResolver; +import id.iptek.utms.preference.dto.TablePreferenceProfile; +import id.iptek.utms.preference.dto.TablePreferenceRequest; +import id.iptek.utms.preference.dto.TablePreferenceSavedProfile; +import id.iptek.utms.preference.dto.UserUiPreferencesResponse; +import id.iptek.utms.preference.service.UserPreferenceService; +import id.iptek.utms.tenant.TenantContext; +import id.iptek.utms.tenant.TenantFilter; +import id.iptek.utms.workflow.dto.ApprovalResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Objects; + +@RestController +@RequestMapping("/api/users") +@SecurityRequirement(name = "bearerAuth") +public class UserController { + + private final UserService userService; + private final UserRoleManagementService userRoleManagementService; + private final UserPreferenceService userPreferenceService; + private final MessageResolver messageResolver; + + public UserController(UserService userService, + UserRoleManagementService userRoleManagementService, + UserPreferenceService userPreferenceService, + MessageResolver messageResolver) { + this.userService = userService; + this.userRoleManagementService = userRoleManagementService; + this.userPreferenceService = userPreferenceService; + this.messageResolver = messageResolver; + } + + @GetMapping("/me") + @PreAuthorize("hasAuthority('USER_READ') or hasRole('ADMIN')") + public ApiResponse me(Authentication authentication) { + CurrentUserResponse response = userService.me(authentication.getName()); + return ApiResponse.ok(messageResolver.get("user.me.success"), response); + } + + @GetMapping("/preferences") + @PreAuthorize("hasAuthority('USER_READ') or hasRole('USER') or isAuthenticated()") + public ApiResponse getPreferences( + @RequestHeader(value = TenantFilter.TENANT_HEADER, required = false) String tenantId, + Authentication authentication) { + requireTenantHeader(tenantId); + return ApiResponse.ok(messageResolver.get("user.preferences.get.success"), + userPreferenceService.getAll(authentication)); + } + + @PutMapping("/preferences/table") + @PreAuthorize("hasAuthority('USER_READ') or hasRole('USER') or isAuthenticated()") + public ApiResponse upsertTablePreference( + @RequestHeader(value = TenantFilter.TENANT_HEADER, required = false) String tenantId, + Authentication authentication, + @Valid @RequestBody TablePreferenceRequest request) { + requireTenantHeader(tenantId); + return ApiResponse.ok(messageResolver.get("user.preferences.upsert.success"), + userPreferenceService.upsert(authentication, request)); + } + + @DeleteMapping("/preferences/table/{preferenceKey}") + @PreAuthorize("hasAuthority('USER_READ') or hasRole('USER') or isAuthenticated()") + public ApiResponse resetTablePreference( + @RequestHeader(value = TenantFilter.TENANT_HEADER, required = false) String tenantId, + Authentication authentication, + @PathVariable String preferenceKey) { + requireTenantHeader(tenantId); + return ApiResponse.ok(messageResolver.get("user.preferences.reset.table.success"), + userPreferenceService.resetTablePreference(authentication, preferenceKey)); + } + + @DeleteMapping("/preferences") + @PreAuthorize("hasAuthority('USER_READ') or hasRole('USER') or isAuthenticated()") + public ApiResponse resetAllPreferences( + @RequestHeader(value = TenantFilter.TENANT_HEADER, required = false) String tenantId, + Authentication authentication) { + requireTenantHeader(tenantId); + userPreferenceService.resetAll(authentication); + return ApiResponse.ok(messageResolver.get("user.preferences.reset.all.success"), null); + } + + @PostMapping("/management/requests/create") + @PreAuthorize("hasAuthority('USER_MANAGE') or hasRole('USER_ROLE_ADMIN')") + public ApiResponse create(@Valid @RequestBody CreateUserManagementRequest request, + HttpServletRequest servletRequest) { + return ApiResponse.ok( + messageResolver.get("user.management.request.created"), + userRoleManagementService.submitCreateUserRequest(request, servletRequest) + ); + } + + @PostMapping("/management/requests/update-roles") + @PreAuthorize("hasAuthority('USER_MANAGE') or hasRole('USER_ROLE_ADMIN')") + public ApiResponse updateRoles(@Valid @RequestBody UpdateUserRolesRequest request, + HttpServletRequest servletRequest) { + return ApiResponse.ok( + messageResolver.get("user.management.request.created"), + userRoleManagementService.submitUpdateUserRolesRequest(request, servletRequest) + ); + } + + private void requireTenantHeader(String tenantId) { + if (tenantId == null || tenantId.isBlank()) { + throw new AppException(messageResolver.get("tenant.header.required")); + } + String contextTenant = TenantContext.getTenantId(); + if (contextTenant == null || contextTenant.isBlank()) { + throw new AppException(messageResolver.get("tenant.header.required")); + } + if (!Objects.equals(tenantId, contextTenant)) { + throw new AppException(messageResolver.get("tenant.header.mismatch")); + } + } +} diff --git a/src/main/java/id/iptek/utms/auth/config/JwtProperties.java b/src/main/java/id/iptek/utms/auth/config/JwtProperties.java new file mode 100644 index 0000000..01161e3 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/config/JwtProperties.java @@ -0,0 +1,12 @@ +package id.iptek.utms.auth.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "app.jwt") +public record JwtProperties( + String secret, + long accessTokenMinutes, + long refreshTokenDays +) { +} + diff --git a/src/main/java/id/iptek/utms/auth/config/LdapAuthConfig.java b/src/main/java/id/iptek/utms/auth/config/LdapAuthConfig.java new file mode 100644 index 0000000..1e54f89 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/config/LdapAuthConfig.java @@ -0,0 +1,45 @@ +package id.iptek.utms.auth.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.authentication.BindAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; +import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; + +@Configuration +@EnableConfigurationProperties(LdapProperties.class) +public class LdapAuthConfig { + + @Bean + @ConditionalOnProperty(name = "app.ldap.enabled", havingValue = "true") + public DefaultSpringSecurityContextSource ldapContextSource(LdapProperties ldapProperties) { + DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(ldapProperties.url()); + contextSource.setBase(ldapProperties.base()); + contextSource.setUserDn(ldapProperties.managerDn()); + contextSource.setPassword(ldapProperties.managerPassword()); + contextSource.afterPropertiesSet(); + return contextSource; + } + + @Bean(name = "ldapAuthenticationProvider") + @ConditionalOnProperty(name = "app.ldap.enabled", havingValue = "true") + public AuthenticationProvider ldapAuthenticationProvider(DefaultSpringSecurityContextSource ldapContextSource, + LdapProperties ldapProperties) { + BindAuthenticator bindAuthenticator = new BindAuthenticator(ldapContextSource); + FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch( + ldapProperties.userSearchBase(), + ldapProperties.userSearchFilter(), + ldapContextSource + ); + bindAuthenticator.setUserSearch(userSearch); + + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(bindAuthenticator); + provider.setUserDetailsContextMapper(new LdapUserDetailsMapper()); + return provider; + } +} diff --git a/src/main/java/id/iptek/utms/auth/config/LdapProperties.java b/src/main/java/id/iptek/utms/auth/config/LdapProperties.java new file mode 100644 index 0000000..ca06fb4 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/config/LdapProperties.java @@ -0,0 +1,17 @@ +package id.iptek.utms.auth.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "app.ldap") +public record LdapProperties( + boolean enabled, + String url, + String base, + String managerDn, + String managerPassword, + String userSearchBase, + String userSearchFilter, + String groupSearchBase, + String groupSearchFilter +) { +} diff --git a/src/main/java/id/iptek/utms/auth/config/SecurityConfig.java b/src/main/java/id/iptek/utms/auth/config/SecurityConfig.java new file mode 100644 index 0000000..261ef5f --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/config/SecurityConfig.java @@ -0,0 +1,86 @@ +package id.iptek.utms.auth.config; + +import id.iptek.utms.auth.config.LdapProperties; +import id.iptek.utms.auth.security.JwtAuthenticationFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableMethodSecurity +@EnableConfigurationProperties(JwtProperties.class) +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final UserDetailsService userDetailsService; + private final AuthenticationProvider ldapAuthenticationProvider; + private final LdapProperties ldapProperties; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, + UserDetailsService userDetailsService, + @Autowired(required = false) @Qualifier("ldapAuthenticationProvider") AuthenticationProvider ldapAuthenticationProvider, + LdapProperties ldapProperties) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + this.userDetailsService = userDetailsService; + this.ldapAuthenticationProvider = ldapAuthenticationProvider; + this.ldapProperties = ldapProperties; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.authenticationProvider(authenticationProvider()); + + if (ldapProperties.enabled() && ldapAuthenticationProvider != null) { + http.authenticationProvider(ldapAuthenticationProvider); + } + + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/actuator/health").permitAll() + .requestMatchers( + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html" + ).permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} + diff --git a/src/main/java/id/iptek/utms/auth/controller/AuthController.java b/src/main/java/id/iptek/utms/auth/controller/AuthController.java new file mode 100644 index 0000000..0d5de7f --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/controller/AuthController.java @@ -0,0 +1,52 @@ +package id.iptek.utms.auth.controller; + +import id.iptek.utms.api.ApiResponse; +import id.iptek.utms.auth.dto.AuthTokenResponse; +import id.iptek.utms.auth.dto.LoginRequest; +import id.iptek.utms.auth.dto.RefreshRequest; +import id.iptek.utms.auth.service.AuthService; +import id.iptek.utms.core.i18n.MessageResolver; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.springframework.http.HttpHeaders; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/auth") +@Tag(name = "Authentication") +public class AuthController { + + private final AuthService authService; + private final MessageResolver messageResolver; + + public AuthController(AuthService authService, MessageResolver messageResolver) { + this.authService = authService; + this.messageResolver = messageResolver; + } + + @PostMapping("/login") + @Operation(summary = "Login", description = "Returns access and refresh token") + public ApiResponse login(@Valid @RequestBody LoginRequest request) { + return ApiResponse.ok(messageResolver.get("auth.login.success"), authService.login(request)); + } + + @PostMapping("/refresh") + @Operation(summary = "Refresh token") + public ApiResponse refresh(@Valid @RequestBody RefreshRequest request) { + return ApiResponse.ok(messageResolver.get("auth.refresh.success"), authService.refresh(request)); + } + + @PostMapping("/logout") + @PreAuthorize("isAuthenticated()") + @Operation(summary = "Logout and blacklist token") + public ApiResponse logout(HttpServletRequest request) { + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + String token = authHeader != null && authHeader.startsWith("Bearer ") ? authHeader.substring(7) : null; + authService.logout(token); + return ApiResponse.ok(messageResolver.get("auth.logout.success"), null); + } +} + diff --git a/src/main/java/id/iptek/utms/auth/domain/AuthenticationSource.java b/src/main/java/id/iptek/utms/auth/domain/AuthenticationSource.java new file mode 100644 index 0000000..b026844 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/domain/AuthenticationSource.java @@ -0,0 +1,6 @@ +package id.iptek.utms.auth.domain; + +public enum AuthenticationSource { + LOCAL, + LDAP +} diff --git a/src/main/java/id/iptek/utms/auth/domain/Permission.java b/src/main/java/id/iptek/utms/auth/domain/Permission.java new file mode 100644 index 0000000..b068178 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/domain/Permission.java @@ -0,0 +1,37 @@ +package id.iptek.utms.auth.domain; + +import id.iptek.utms.core.domain.BaseEntity; +import id.iptek.utms.core.domain.TenantEntityListener; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.Filter; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +@Getter +@Setter +@Entity +@EntityListeners(TenantEntityListener.class) +@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId") +@Table(name = "sec_permissions", uniqueConstraints = { + @UniqueConstraint(name = "sec_uk_permissions_tenant_code", columnNames = {"tenant_id", "code"}) +}) +public class Permission extends BaseEntity { + + @Id + @GeneratedValue + private UUID id; + + @Column(nullable = false) + private String code; + + @Column(nullable = false) + private String name; + + @ManyToMany(mappedBy = "permissions") + private Set roles = new HashSet<>(); +} + diff --git a/src/main/java/id/iptek/utms/auth/domain/RefreshToken.java b/src/main/java/id/iptek/utms/auth/domain/RefreshToken.java new file mode 100644 index 0000000..7647960 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/domain/RefreshToken.java @@ -0,0 +1,40 @@ +package id.iptek.utms.auth.domain; + +import id.iptek.utms.core.domain.BaseEntity; +import id.iptek.utms.core.domain.TenantEntityListener; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.Filter; + +import java.time.Instant; +import java.util.UUID; + +@Getter +@Setter +@Entity +@EntityListeners(TenantEntityListener.class) +@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId") +@Table(name = "sec_refresh_tokens", indexes = { + @Index(name = "sec_idx_refresh_token", columnList = "token", unique = true) +}) +public class RefreshToken extends BaseEntity { + + @Id + @GeneratedValue + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false, unique = true, length = 512) + private String token; + + @Column(name = "expires_at", nullable = false) + private Instant expiresAt; + + @Column(nullable = false) + private boolean revoked; +} + diff --git a/src/main/java/id/iptek/utms/auth/domain/Role.java b/src/main/java/id/iptek/utms/auth/domain/Role.java new file mode 100644 index 0000000..29f6c36 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/domain/Role.java @@ -0,0 +1,45 @@ +package id.iptek.utms.auth.domain; + +import id.iptek.utms.core.domain.BaseEntity; +import id.iptek.utms.core.domain.TenantEntityListener; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.Filter; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +@Getter +@Setter +@Entity +@EntityListeners(TenantEntityListener.class) +@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId") +@Table(name = "sec_roles", uniqueConstraints = { + @UniqueConstraint(name = "sec_uk_roles_tenant_code", columnNames = {"tenant_id", "code"}) +}) +public class Role extends BaseEntity { + + @Id + @GeneratedValue + private UUID id; + + @Column(nullable = false) + private String code; + + @Column(nullable = false) + private String name; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "sec_role_permissions", + joinColumns = @JoinColumn(name = "role_id"), + inverseJoinColumns = @JoinColumn(name = "permission_id") + ) + private Set permissions = new HashSet<>(); + + @ManyToMany(mappedBy = "roles") + private Set users = new HashSet<>(); +} + diff --git a/src/main/java/id/iptek/utms/auth/domain/User.java b/src/main/java/id/iptek/utms/auth/domain/User.java new file mode 100644 index 0000000..0e5fdba --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/domain/User.java @@ -0,0 +1,55 @@ +package id.iptek.utms.auth.domain; + +import id.iptek.utms.core.domain.BaseEntity; +import id.iptek.utms.core.domain.TenantEntityListener; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.Filter; +import org.hibernate.annotations.FilterDef; +import org.hibernate.annotations.ParamDef; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +@Getter +@Setter +@Entity +@EntityListeners(TenantEntityListener.class) +@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = String.class)) +@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId") +@Table(name = "sec_users", uniqueConstraints = { + @UniqueConstraint(name = "sec_uk_users_tenant_username", columnNames = {"tenant_id", "username"}) +}) +public class User extends BaseEntity { + + @Id + @GeneratedValue + private UUID id; + + @Column(nullable = false) + private String username; + + @Column + private String password; + + @Enumerated(EnumType.STRING) + @Column(name = "auth_source", nullable = false) + private AuthenticationSource authSource = AuthenticationSource.LOCAL; + + @Column(name = "ldap_dn") + private String ldapDn; + + @Column(nullable = false) + private boolean enabled = true; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "sec_user_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + private Set roles = new HashSet<>(); +} + diff --git a/src/main/java/id/iptek/utms/auth/dto/AuthTokenResponse.java b/src/main/java/id/iptek/utms/auth/dto/AuthTokenResponse.java new file mode 100644 index 0000000..d708fb5 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/dto/AuthTokenResponse.java @@ -0,0 +1,10 @@ +package id.iptek.utms.auth.dto; + +public record AuthTokenResponse( + String tokenType, + String accessToken, + String refreshToken, + long expiresInSeconds +) { +} + diff --git a/src/main/java/id/iptek/utms/auth/dto/CreateRoleManagementRequest.java b/src/main/java/id/iptek/utms/auth/dto/CreateRoleManagementRequest.java new file mode 100644 index 0000000..dff9f9f --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/dto/CreateRoleManagementRequest.java @@ -0,0 +1,13 @@ +package id.iptek.utms.auth.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; + +import java.util.Set; + +public record CreateRoleManagementRequest( + @NotBlank String code, + @NotBlank String name, + @NotEmpty Set<@NotBlank String> permissionCodes +) { +} diff --git a/src/main/java/id/iptek/utms/auth/dto/CreateUserManagementRequest.java b/src/main/java/id/iptek/utms/auth/dto/CreateUserManagementRequest.java new file mode 100644 index 0000000..98f1d17 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/dto/CreateUserManagementRequest.java @@ -0,0 +1,26 @@ +package id.iptek.utms.auth.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; + +import java.util.Set; + +public record CreateUserManagementRequest( + @NotBlank String username, + String password, + String ldapDn, + Boolean enabled, + @NotEmpty Set<@NotBlank String> roleCodes +) { + public boolean isEnabled() { + return enabled == null || enabled; + } + + public String normalizedLdapDn() { + return ldapDn == null ? null : ldapDn.trim(); + } + + public boolean hasPassword() { + return password != null && !password.isBlank(); + } +} diff --git a/src/main/java/id/iptek/utms/auth/dto/CurrentUserResponse.java b/src/main/java/id/iptek/utms/auth/dto/CurrentUserResponse.java new file mode 100644 index 0000000..d547230 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/dto/CurrentUserResponse.java @@ -0,0 +1,12 @@ +package id.iptek.utms.auth.dto; + +import java.util.Set; + +public record CurrentUserResponse( + String tenantId, + String username, + Set roles, + Set permissions +) { +} + diff --git a/src/main/java/id/iptek/utms/auth/dto/LoginRequest.java b/src/main/java/id/iptek/utms/auth/dto/LoginRequest.java new file mode 100644 index 0000000..4f32732 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/dto/LoginRequest.java @@ -0,0 +1,10 @@ +package id.iptek.utms.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record LoginRequest( + @NotBlank String username, + @NotBlank String password +) { +} + diff --git a/src/main/java/id/iptek/utms/auth/dto/RefreshRequest.java b/src/main/java/id/iptek/utms/auth/dto/RefreshRequest.java new file mode 100644 index 0000000..e8b0844 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/dto/RefreshRequest.java @@ -0,0 +1,9 @@ +package id.iptek.utms.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record RefreshRequest( + @NotBlank String refreshToken +) { +} + diff --git a/src/main/java/id/iptek/utms/auth/dto/UpdateRolePermissionsRequest.java b/src/main/java/id/iptek/utms/auth/dto/UpdateRolePermissionsRequest.java new file mode 100644 index 0000000..efa6248 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/dto/UpdateRolePermissionsRequest.java @@ -0,0 +1,12 @@ +package id.iptek.utms.auth.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; + +import java.util.Set; + +public record UpdateRolePermissionsRequest( + @NotBlank String code, + @NotEmpty Set<@NotBlank String> permissionCodes +) { +} diff --git a/src/main/java/id/iptek/utms/auth/dto/UpdateUserRolesRequest.java b/src/main/java/id/iptek/utms/auth/dto/UpdateUserRolesRequest.java new file mode 100644 index 0000000..d0deead --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/dto/UpdateUserRolesRequest.java @@ -0,0 +1,12 @@ +package id.iptek.utms.auth.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; + +import java.util.Set; + +public record UpdateUserRolesRequest( + @NotBlank String username, + @NotEmpty Set<@NotBlank String> roleCodes +) { +} diff --git a/src/main/java/id/iptek/utms/auth/repository/PermissionRepository.java b/src/main/java/id/iptek/utms/auth/repository/PermissionRepository.java new file mode 100644 index 0000000..f2f8815 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/repository/PermissionRepository.java @@ -0,0 +1,16 @@ +package id.iptek.utms.auth.repository; + +import id.iptek.utms.auth.domain.Permission; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface PermissionRepository extends JpaRepository { + Optional findByTenantIdAndCode(String tenantId, String code); + + List findByTenantIdAndCodeIn(String tenantId, Collection codes); +} + diff --git a/src/main/java/id/iptek/utms/auth/repository/RefreshTokenRepository.java b/src/main/java/id/iptek/utms/auth/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..cb14802 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/repository/RefreshTokenRepository.java @@ -0,0 +1,13 @@ +package id.iptek.utms.auth.repository; + +import id.iptek.utms.auth.domain.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface RefreshTokenRepository extends JpaRepository { + Optional findByTokenAndTenantId(String token, String tenantId); + void deleteByUser_IdAndTenantId(UUID userId, String tenantId); +} + diff --git a/src/main/java/id/iptek/utms/auth/repository/RoleRepository.java b/src/main/java/id/iptek/utms/auth/repository/RoleRepository.java new file mode 100644 index 0000000..b826087 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/repository/RoleRepository.java @@ -0,0 +1,16 @@ +package id.iptek.utms.auth.repository; + +import id.iptek.utms.auth.domain.Role; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface RoleRepository extends JpaRepository { + Optional findByTenantIdAndCode(String tenantId, String code); + + List findByTenantIdAndCodeIn(String tenantId, Collection codes); +} + diff --git a/src/main/java/id/iptek/utms/auth/repository/UserRepository.java b/src/main/java/id/iptek/utms/auth/repository/UserRepository.java new file mode 100644 index 0000000..02aa59a --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/repository/UserRepository.java @@ -0,0 +1,14 @@ +package id.iptek.utms.auth.repository; + +import id.iptek.utms.auth.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface UserRepository extends JpaRepository { + Optional findByTenantIdAndUsername(String tenantId, String username); + + boolean existsByTenantIdAndUsername(String tenantId, String username); +} + diff --git a/src/main/java/id/iptek/utms/auth/security/JwtAuthenticationFilter.java b/src/main/java/id/iptek/utms/auth/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..1fd1a17 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/security/JwtAuthenticationFilter.java @@ -0,0 +1,87 @@ +package id.iptek.utms.auth.security; + +import id.iptek.utms.auth.service.TokenBlacklistService; +import id.iptek.utms.auth.service.SingleLoginSessionService; +import id.iptek.utms.tenant.TenantContext; +import io.jsonwebtoken.Claims; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + private final TokenBlacklistService tokenBlacklistService; + private final SingleLoginSessionService singleLoginSessionService; + + public JwtAuthenticationFilter(JwtService jwtService, + UserDetailsService userDetailsService, + TokenBlacklistService tokenBlacklistService, + SingleLoginSessionService singleLoginSessionService) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + this.tokenBlacklistService = tokenBlacklistService; + this.singleLoginSessionService = singleLoginSessionService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + String token = authHeader.substring(7); + if (tokenBlacklistService.isBlacklisted(token)) { + filterChain.doFilter(request, response); + return; + } + + try { + Claims claims = jwtService.parseClaims(token); + String username = claims.getSubject(); + String tenant = claims.get("tenant", String.class); + String sessionId = claims.get("sid", String.class); + + if (TenantContext.getTenantId() == null && tenant != null) { + TenantContext.setTenantId(tenant); + } + + if (singleLoginSessionService.isEnabled() && !singleLoginSessionService.isSessionActive(tenant, username, sessionId)) { + filterChain.doFilter(request, response); + return; + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + if (jwtService.isTokenValid(token)) { + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + } catch (Exception ignored) { + SecurityContextHolder.clearContext(); + } + + filterChain.doFilter(request, response); + } +} + diff --git a/src/main/java/id/iptek/utms/auth/security/JwtService.java b/src/main/java/id/iptek/utms/auth/security/JwtService.java new file mode 100644 index 0000000..19f3514 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/security/JwtService.java @@ -0,0 +1,91 @@ +package id.iptek.utms.auth.security; + +import id.iptek.utms.auth.config.JwtProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.UUID; + +@Component +public class JwtService { + + private final JwtProperties jwtProperties; + + public JwtService(JwtProperties jwtProperties) { + this.jwtProperties = jwtProperties; + } + + public String generateAccessToken(UserPrincipal principal) { + return generateAccessToken(principal, null); + } + + public String generateAccessToken(UserPrincipal principal, String sessionId) { + Instant now = Instant.now(); + return buildToken(principal, now.plus(jwtProperties.accessTokenMinutes(), ChronoUnit.MINUTES), sessionId); + } + + public String generateRefreshToken(UserPrincipal principal) { + return generateRefreshToken(principal, null); + } + + public String generateRefreshToken(UserPrincipal principal, String sessionId) { + Instant now = Instant.now(); + return buildToken(principal, now.plus(jwtProperties.refreshTokenDays(), ChronoUnit.DAYS), sessionId); + } + + private String buildToken(UserPrincipal principal, Instant expiresAt, String sessionId) { + return Jwts.builder() + .subject(principal.getUsername()) + .claim("uid", principal.getId().toString()) + .claim("tenant", principal.getTenantId()) + .claim("sid", sessionId) + .issuedAt(Date.from(Instant.now())) + .expiration(Date.from(expiresAt)) + .signWith(secretKey()) + .compact(); + } + + public Claims parseClaims(String token) { + return Jwts.parser() + .verifyWith(secretKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public String extractUsername(String token) { + return parseClaims(token).getSubject(); + } + + public String extractTenant(String token) { + return parseClaims(token).get("tenant", String.class); + } + + public UUID extractUserId(String token) { + return UUID.fromString(parseClaims(token).get("uid", String.class)); + } + + public String extractSessionId(String token) { + return parseClaims(token).get("sid", String.class); + } + + public boolean isTokenValid(String token) { + return parseClaims(token).getExpiration().after(new Date()); + } + + public long getAccessExpiresInSeconds() { + return jwtProperties.accessTokenMinutes() * 60; + } + + private SecretKey secretKey() { + return Keys.hmacShaKeyFor(jwtProperties.secret().getBytes(StandardCharsets.UTF_8)); + } +} + diff --git a/src/main/java/id/iptek/utms/auth/security/TenantAwareUserDetailsService.java b/src/main/java/id/iptek/utms/auth/security/TenantAwareUserDetailsService.java new file mode 100644 index 0000000..2ff83f0 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/security/TenantAwareUserDetailsService.java @@ -0,0 +1,27 @@ +package id.iptek.utms.auth.security; + +import id.iptek.utms.auth.repository.UserRepository; +import id.iptek.utms.tenant.TenantContext; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class TenantAwareUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + public TenantAwareUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + String tenantId = TenantContext.getRequiredTenantId(); + return userRepository.findByTenantIdAndUsername(tenantId, username) + .map(UserPrincipal::new) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + } +} + diff --git a/src/main/java/id/iptek/utms/auth/security/UserPrincipal.java b/src/main/java/id/iptek/utms/auth/security/UserPrincipal.java new file mode 100644 index 0000000..011602e --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/security/UserPrincipal.java @@ -0,0 +1,87 @@ +package id.iptek.utms.auth.security; + +import id.iptek.utms.auth.domain.Permission; +import id.iptek.utms.auth.domain.Role; +import id.iptek.utms.auth.domain.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class UserPrincipal implements UserDetails { + + private final UUID id; + private final String tenantId; + private final String username; + private final String password; + private final boolean enabled; + private final Set authorities; + + public UserPrincipal(User user) { + this.id = user.getId(); + this.tenantId = user.getTenantId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = user.isEnabled(); + this.authorities = mapAuthorities(user.getRoles()); + } + + private Set mapAuthorities(Set roles) { + Set mapped = new HashSet<>(); + for (Role role : roles) { + mapped.add(new SimpleGrantedAuthority("ROLE_" + role.getCode())); + for (Permission permission : role.getPermissions()) { + mapped.add(new SimpleGrantedAuthority(permission.getCode())); + } + } + return mapped; + } + + public UUID getId() { + return id; + } + + public String getTenantId() { + return tenantId; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return enabled; + } +} + diff --git a/src/main/java/id/iptek/utms/auth/service/AuthService.java b/src/main/java/id/iptek/utms/auth/service/AuthService.java new file mode 100644 index 0000000..6376f6a --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/service/AuthService.java @@ -0,0 +1,153 @@ +package id.iptek.utms.auth.service; + +import id.iptek.utms.auth.domain.RefreshToken; +import id.iptek.utms.auth.domain.User; +import id.iptek.utms.auth.dto.AuthTokenResponse; +import id.iptek.utms.auth.dto.LoginRequest; +import id.iptek.utms.auth.dto.RefreshRequest; +import id.iptek.utms.auth.repository.RefreshTokenRepository; +import id.iptek.utms.auth.repository.UserRepository; +import id.iptek.utms.auth.config.LdapProperties; +import id.iptek.utms.auth.security.JwtService; +import id.iptek.utms.auth.security.UserPrincipal; +import id.iptek.utms.core.i18n.MessageResolver; +import id.iptek.utms.core.exception.AppException; +import id.iptek.utms.tenant.TenantContext; +import id.iptek.utms.tenant.TenantService; +import io.jsonwebtoken.Claims; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; + +@Service +public class AuthService { + + private final AuthenticationManager authenticationManager; + private final JwtService jwtService; + private final RefreshTokenRepository refreshTokenRepository; + private final UserRepository userRepository; + private final TokenBlacklistService tokenBlacklistService; + private final TenantService tenantService; + private final LoginThrottleService loginThrottleService; + private final SingleLoginSessionService singleLoginSessionService; + private final MessageResolver messageResolver; + private final LdapProperties ldapProperties; + + public AuthService(AuthenticationManager authenticationManager, + JwtService jwtService, + RefreshTokenRepository refreshTokenRepository, + UserRepository userRepository, + TokenBlacklistService tokenBlacklistService, + TenantService tenantService, + LoginThrottleService loginThrottleService, + SingleLoginSessionService singleLoginSessionService, + MessageResolver messageResolver, + LdapProperties ldapProperties) { + this.authenticationManager = authenticationManager; + this.jwtService = jwtService; + this.refreshTokenRepository = refreshTokenRepository; + this.userRepository = userRepository; + this.tokenBlacklistService = tokenBlacklistService; + this.tenantService = tenantService; + this.loginThrottleService = loginThrottleService; + this.singleLoginSessionService = singleLoginSessionService; + this.messageResolver = messageResolver; + this.ldapProperties = ldapProperties; + } + + @Transactional + public AuthTokenResponse login(LoginRequest request) { + String tenantId = TenantContext.getRequiredTenantId(); + tenantService.getActiveTenant(tenantId); + loginThrottleService.ensureAllowed(tenantId, request.username()); + + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.username(), request.password()) + ); + loginThrottleService.recordSuccess(tenantId, request.username()); + User user = resolveAuthenticatedUser(tenantId, authentication.getName()); + UserPrincipal principal = new UserPrincipal(user); + String sessionId = UUID.randomUUID().toString(); + String accessToken = jwtService.generateAccessToken(principal, sessionId); + String refreshToken = jwtService.generateRefreshToken(principal, sessionId); + + refreshTokenRepository.deleteByUser_IdAndTenantId(user.getId(), tenantId); + + RefreshToken entity = new RefreshToken(); + entity.setUser(user); + entity.setToken(refreshToken); + Instant refreshExpiresAt = jwtService.parseClaims(refreshToken).getExpiration().toInstant(); + entity.setExpiresAt(refreshExpiresAt); + entity.setRevoked(false); + entity.setTenantId(tenantId); + refreshTokenRepository.save(entity); + singleLoginSessionService.registerSession(tenantId, user.getUsername(), sessionId, refreshExpiresAt); + + return new AuthTokenResponse("Bearer", accessToken, refreshToken, jwtService.getAccessExpiresInSeconds()); + } catch (AppException ex) { + throw ex; + } catch (AuthenticationException ex) { + loginThrottleService.recordFailure(tenantId, request.username()); + throw new AppException(messageResolver.get("auth.invalid.credentials")); + } + } + + private User resolveAuthenticatedUser(String tenantId, String username) { + return userRepository.findByTenantIdAndUsername(tenantId, username) + .orElseThrow(() -> new AppException(ldapProperties.enabled() + ? messageResolver.get("auth.user.notfound.for.ldap") + : messageResolver.get("auth.user.notfound"))); + } + + @Transactional + public AuthTokenResponse refresh(RefreshRequest request) { + String tenantId = TenantContext.getRequiredTenantId(); + RefreshToken refreshToken = refreshTokenRepository.findByTokenAndTenantId(request.refreshToken(), tenantId) + .orElseThrow(() -> new AppException(messageResolver.get("auth.refresh.notfound"))); + + if (refreshToken.isRevoked() || refreshToken.getExpiresAt().isBefore(Instant.now())) { + throw new AppException(messageResolver.get("auth.refresh.invalid")); + } + + Claims claims = jwtService.parseClaims(request.refreshToken()); + String username = claims.getSubject(); + User user = userRepository.findByTenantIdAndUsername(tenantId, username) + .orElseThrow(() -> new AppException(messageResolver.get("auth.user.notfound"))); + + String tokenSessionId = jwtService.extractSessionId(request.refreshToken()); + if (singleLoginSessionService.isEnabled() && !singleLoginSessionService.isSessionActive(tenantId, username, tokenSessionId)) { + throw new AppException(messageResolver.get("auth.single.login.invalid_session")); + } + + UserPrincipal principal = new UserPrincipal(user); + String accessToken = jwtService.generateAccessToken(principal, tokenSessionId); + + return new AuthTokenResponse("Bearer", accessToken, request.refreshToken(), jwtService.getAccessExpiresInSeconds()); + } + + @Transactional + public void logout(String accessToken) { + if (accessToken == null || accessToken.isBlank()) { + return; + } + Claims claims = jwtService.parseClaims(accessToken); + Instant expiry = claims.getExpiration().toInstant(); + String tenantId = claims.get("tenant", String.class); + String username = claims.getSubject(); + String sessionId = jwtService.extractSessionId(accessToken); + singleLoginSessionService.clearSession(tenantId, username, sessionId); + Duration ttl = Duration.between(Instant.now(), expiry); + if (!ttl.isNegative() && !ttl.isZero()) { + tokenBlacklistService.blacklist(accessToken, ttl); + } + } +} + diff --git a/src/main/java/id/iptek/utms/auth/service/LoginThrottleService.java b/src/main/java/id/iptek/utms/auth/service/LoginThrottleService.java new file mode 100644 index 0000000..e7c5d50 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/service/LoginThrottleService.java @@ -0,0 +1,84 @@ +package id.iptek.utms.auth.service; + +import id.iptek.utms.core.exception.AppException; +import id.iptek.utms.core.i18n.MessageResolver; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Locale; +import java.util.Optional; + +@Service +public class LoginThrottleService { + + private final StringRedisTemplate redisTemplate; + private final MessageResolver messageResolver; + private final int maxFailedAttempts; + private final int failedAttemptWindowSeconds; + private final int lockoutDurationSeconds; + + private static final String FAIL_PREFIX = "utms:auth:login:fail:"; + private static final String LOCK_PREFIX = "utms:auth:login:lock:"; + + public LoginThrottleService(StringRedisTemplate redisTemplate, + MessageResolver messageResolver, + @Value("${app.security.login.max-failed-attempts:5}") int maxFailedAttempts, + @Value("${app.security.login.failed-attempt-window-seconds:900}") int failedAttemptWindowSeconds, + @Value("${app.security.login.lockout-duration-seconds:300}") int lockoutDurationSeconds) { + this.redisTemplate = redisTemplate; + this.messageResolver = messageResolver; + this.maxFailedAttempts = maxFailedAttempts; + this.failedAttemptWindowSeconds = failedAttemptWindowSeconds; + this.lockoutDurationSeconds = lockoutDurationSeconds; + } + + public void ensureAllowed(String tenantId, String username) { + String lockKey = lockKey(tenantId, username); + Long ttl = redisTemplate.getExpire(lockKey); + if (ttl == null || ttl < 1) { + redisTemplate.delete(lockKey); + return; + } + + throw new AppException(messageResolver.get("auth.login.locked", Math.max(ttl, 0L))); + } + + public void recordFailure(String tenantId, String username) { + String failKey = failKey(tenantId, username); + String lockKey = lockKey(tenantId, username); + + Long attempts = Optional.ofNullable(redisTemplate.opsForValue().increment(failKey)).orElse(1L); + if (attempts == 1) { + redisTemplate.expire(failKey, Duration.ofSeconds(failedAttemptWindowSeconds)); + } + + if (attempts >= maxFailedAttempts) { + redisTemplate.opsForValue().set(lockKey, "locked"); + redisTemplate.expire(lockKey, Duration.ofSeconds(lockoutDurationSeconds)); + redisTemplate.delete(failKey); + long ttl = redisTemplate.getExpire(lockKey); + throw new AppException(messageResolver.get("auth.login.locked", Math.max(ttl, (long) lockoutDurationSeconds))); + } + } + + public void recordSuccess(String tenantId, String username) { + redisTemplate.delete(failKey(tenantId, username)); + redisTemplate.delete(lockKey(tenantId, username)); + } + + private String failKey(String tenantId, String username) { + return FAIL_PREFIX + normalize(tenantId) + ":" + normalize(username); + } + + private String lockKey(String tenantId, String username) { + return LOCK_PREFIX + normalize(tenantId) + ":" + normalize(username); + } + + private String normalize(String value) { + return URLEncoder.encode(value.toLowerCase(Locale.ROOT), StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/id/iptek/utms/auth/service/SingleLoginSessionService.java b/src/main/java/id/iptek/utms/auth/service/SingleLoginSessionService.java new file mode 100644 index 0000000..7652536 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/service/SingleLoginSessionService.java @@ -0,0 +1,80 @@ +package id.iptek.utms.auth.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.Instant; + +@Service +public class SingleLoginSessionService { + + private static final String SESSION_KEY_PREFIX = "utms:auth:single-login:"; + + private final StringRedisTemplate redisTemplate; + private final boolean enabled; + + public SingleLoginSessionService(StringRedisTemplate redisTemplate, + @Value("${app.security.single-login.enabled:false}") boolean enabled) { + this.redisTemplate = redisTemplate; + this.enabled = enabled; + } + + public void registerSession(String tenantId, String username, String sessionId, Instant sessionExpiresAt) { + if (!enabled) { + return; + } + String key = sessionKey(tenantId, username); + Duration ttl = ttlUntil(sessionExpiresAt); + redisTemplate.opsForValue().set(key, sessionId, ttl); + } + + public boolean isSessionActive(String tenantId, String username, String sessionId) { + if (!enabled) { + return true; + } + + if (tenantId == null || username == null || sessionId == null || tenantId.isBlank() || username.isBlank()) { + return false; + } + + String activeSession = redisTemplate.opsForValue().get(sessionKey(tenantId, username)); + return sessionId.equals(activeSession); + } + + public void clearSession(String tenantId, String username, String sessionId) { + if (!enabled || tenantId == null || username == null || tenantId.isBlank() || username.isBlank()) { + return; + } + + String key = sessionKey(tenantId, username); + String activeSession = redisTemplate.opsForValue().get(key); + if (sessionId == null || sessionId.equals(activeSession)) { + redisTemplate.delete(key); + } + } + + public void clearAllSessions(String tenantId, String username) { + if (!enabled || tenantId == null || username == null || tenantId.isBlank() || username.isBlank()) { + return; + } + redisTemplate.delete(sessionKey(tenantId, username)); + } + + public boolean isEnabled() { + return enabled; + } + + private Duration ttlUntil(Instant expiresAt) { + long seconds = Duration.between(Instant.now(), expiresAt).getSeconds(); + if (seconds <= 0) { + return Duration.ofSeconds(1); + } + return Duration.ofSeconds(seconds); + } + + private String sessionKey(String tenantId, String username) { + return SESSION_KEY_PREFIX + tenantId + ":" + username; + } +} diff --git a/src/main/java/id/iptek/utms/auth/service/TokenBlacklistService.java b/src/main/java/id/iptek/utms/auth/service/TokenBlacklistService.java new file mode 100644 index 0000000..7863134 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/service/TokenBlacklistService.java @@ -0,0 +1,27 @@ +package id.iptek.utms.auth.service; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; + +@Service +public class TokenBlacklistService { + + private static final String KEY_PREFIX = "auth:blacklist:"; + + private final StringRedisTemplate redisTemplate; + + public TokenBlacklistService(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public void blacklist(String token, Duration ttl) { + redisTemplate.opsForValue().set(KEY_PREFIX + token, "1", ttl); + } + + public boolean isBlacklisted(String token) { + return Boolean.TRUE.equals(redisTemplate.hasKey(KEY_PREFIX + token)); + } +} + diff --git a/src/main/java/id/iptek/utms/auth/service/UserRoleManagementService.java b/src/main/java/id/iptek/utms/auth/service/UserRoleManagementService.java new file mode 100644 index 0000000..56fb1a9 --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/service/UserRoleManagementService.java @@ -0,0 +1,413 @@ +package id.iptek.utms.auth.service; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import id.iptek.utms.auth.domain.Permission; +import id.iptek.utms.auth.domain.Role; +import id.iptek.utms.auth.domain.User; +import id.iptek.utms.auth.domain.AuthenticationSource; +import id.iptek.utms.auth.config.LdapProperties; +import id.iptek.utms.auth.dto.CreateRoleManagementRequest; +import id.iptek.utms.auth.dto.CreateUserManagementRequest; +import id.iptek.utms.auth.dto.UpdateRolePermissionsRequest; +import id.iptek.utms.auth.dto.UpdateUserRolesRequest; +import id.iptek.utms.auth.repository.PermissionRepository; +import id.iptek.utms.auth.repository.RoleRepository; +import id.iptek.utms.auth.repository.UserRepository; +import id.iptek.utms.core.audit.service.AuditTrailService; +import id.iptek.utms.core.exception.AppException; +import id.iptek.utms.messaging.ApprovalCompletedEvent; +import id.iptek.utms.tenant.TenantContext; +import id.iptek.utms.workflow.domain.ApprovalRequest; +import id.iptek.utms.workflow.dto.ApprovalResponse; +import id.iptek.utms.workflow.repository.ApprovalRequestRepository; +import id.iptek.utms.workflow.service.ApprovalWorkflowService; +import id.iptek.utms.workflow.domain.ApprovalStatus; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import jakarta.servlet.http.HttpServletRequest; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class UserRoleManagementService { + + public static final String CHECKER_ROLE_MANAGER = "USER_ROLE_ADMIN"; + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PermissionRepository permissionRepository; + private final ApprovalRequestRepository approvalRequestRepository; + private final ApprovalWorkflowService approvalWorkflowService; + private final AuditTrailService auditTrailService; + private final PasswordEncoder passwordEncoder; + private final ObjectMapper objectMapper; + private final LdapProperties ldapProperties; + + public UserRoleManagementService(UserRepository userRepository, + RoleRepository roleRepository, + PermissionRepository permissionRepository, + ApprovalRequestRepository approvalRequestRepository, + ApprovalWorkflowService approvalWorkflowService, + AuditTrailService auditTrailService, + PasswordEncoder passwordEncoder, + ObjectMapper objectMapper, + LdapProperties ldapProperties) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.permissionRepository = permissionRepository; + this.approvalRequestRepository = approvalRequestRepository; + this.approvalWorkflowService = approvalWorkflowService; + this.auditTrailService = auditTrailService; + this.passwordEncoder = passwordEncoder; + this.objectMapper = objectMapper; + this.ldapProperties = ldapProperties; + } + + @Transactional + public ApprovalResponse submitCreateUserRequest(CreateUserManagementRequest request, HttpServletRequest servletRequest) { + String tenantId = TenantContext.getRequiredTenantId(); + assertUserNotExists(tenantId, request.username()); + Set roles = resolveRoles(tenantId, request.roleCodes()); + + if (!ldapProperties.enabled() && !request.hasPassword()) { + throw new AppException("Password is required when LDAP is disabled"); + } + + Map payload = new LinkedHashMap<>(); + payload.put("operation", "CREATE_USER"); + payload.put("username", request.username()); + if (!ldapProperties.enabled()) { + payload.put("passwordHash", passwordEncoder.encode(request.password())); + } + payload.put("authSource", ldapProperties.enabled() ? AuthenticationSource.LDAP.name() : AuthenticationSource.LOCAL.name()); + payload.put("ldapDn", request.normalizedLdapDn()); + payload.put("enabled", request.isEnabled()); + payload.put("roleCodes", request.roleCodes()); + + return approvalWorkflowService.createRequest( + "USER_MANAGEMENT", + request.username(), + toJson(payload), + 1, + CHECKER_ROLE_MANAGER, + servletRequest + ); + } + + @Transactional + public ApprovalResponse submitUpdateUserRolesRequest(UpdateUserRolesRequest request, HttpServletRequest servletRequest) { + String tenantId = TenantContext.getRequiredTenantId(); + User target = userRepository.findByTenantIdAndUsername(tenantId, request.username()) + .orElseThrow(() -> new AppException("User not found")); + + Set roles = resolveRoles(tenantId, request.roleCodes()); + + Map payload = new LinkedHashMap<>(); + payload.put("operation", "UPDATE_USER_ROLES"); + payload.put("userId", target.getId().toString()); + payload.put("username", request.username()); + payload.put("roleCodes", request.roleCodes()); + + return approvalWorkflowService.createRequest( + "USER_MANAGEMENT", + request.username(), + toJson(payload), + 1, + CHECKER_ROLE_MANAGER, + servletRequest + ); + } + + @Transactional + public ApprovalResponse submitCreateRoleRequest(CreateRoleManagementRequest request, HttpServletRequest servletRequest) { + String tenantId = TenantContext.getRequiredTenantId(); + if (roleRepository.findByTenantIdAndCode(tenantId, request.code()).isPresent()) { + throw new AppException("Role already exists"); + } + resolvePermissions(tenantId, request.permissionCodes()); + + Map payload = new LinkedHashMap<>(); + payload.put("operation", "CREATE_ROLE"); + payload.put("code", request.code()); + payload.put("name", request.name()); + payload.put("permissionCodes", request.permissionCodes()); + + return approvalWorkflowService.createRequest( + "ROLE_MANAGEMENT", + request.code(), + toJson(payload), + 1, + CHECKER_ROLE_MANAGER, + servletRequest + ); + } + + @Transactional + public ApprovalResponse submitUpdateRolePermissionsRequest(UpdateRolePermissionsRequest request, HttpServletRequest servletRequest) { + String tenantId = TenantContext.getRequiredTenantId(); + roleRepository.findByTenantIdAndCode(tenantId, request.code()) + .orElseThrow(() -> new AppException("Role not found")); + resolvePermissions(tenantId, request.permissionCodes()); + + Map payload = new LinkedHashMap<>(); + payload.put("operation", "UPDATE_ROLE_PERMISSIONS"); + payload.put("code", request.code()); + payload.put("permissionCodes", request.permissionCodes()); + + return approvalWorkflowService.createRequest( + "ROLE_MANAGEMENT", + request.code(), + toJson(payload), + 1, + CHECKER_ROLE_MANAGER, + servletRequest + ); + } + + @Transactional + public void applyApprovedRequest(ApprovalCompletedEvent event) { + ApprovalRequest approvalRequest = approvalRequestRepository + .findByIdAndTenantId(event.requestId(), event.tenantId()) + .orElse(null); + if (approvalRequest == null) { + return; + } + if (approvalRequest.getStatus() != ApprovalStatus.APPROVED) { + return; + } + + String tenantId = event.tenantId(); + try { + TenantContext.setTenantId(tenantId); + Map payload = objectMapper.readValue(approvalRequest.getPayload(), new TypeReference<>() {}); + String operation = payload.get("operation") != null ? payload.get("operation").toString() : ""; + switch (operation) { + case "CREATE_USER" -> applyCreateUser(approvalRequest, payload, tenantId, event); + case "UPDATE_USER_ROLES" -> applyUpdateUserRoles(approvalRequest, payload, tenantId, event); + case "CREATE_ROLE" -> applyCreateRole(approvalRequest, payload, tenantId, event); + case "UPDATE_ROLE_PERMISSIONS" -> applyUpdateRolePermissions(approvalRequest, payload, tenantId, event); + default -> { + auditTrailService.record( + "USER_ROLE_MANAGEMENT_UNKNOWN", + "WORKFLOW", + approvalRequest.getResourceType(), + approvalRequest.getId().toString(), + AuditTrailService.FAILURE, + "Unknown workflow operation", + null, + null, + null, + null + ); + throw new AppException("Unknown user/role management operation"); + } + } + } catch (Exception ex) { + throw new AppException("Failed to apply approved management request: " + ex.getMessage()); + } finally { + TenantContext.clear(); + } + } + + @Transactional + protected void applyCreateUser(ApprovalRequest approvalRequest, Map payload, String tenantId, ApprovalCompletedEvent event) { + String username = (String) payload.get("username"); + String authSourceName = (String) payload.get("authSource"); + AuthenticationSource authSource = parseAuthSource(authSourceName, "LOCAL"); + boolean enabled = asBoolean(payload.get("enabled"), true); + List roleCodes = castToStringList(payload.get("roleCodes")); + Set roles = resolveRoles(tenantId, roleCodes); + String ldapDn = (String) payload.get("ldapDn"); + + User user = userRepository.findByTenantIdAndUsername(tenantId, username) + .orElseGet(() -> { + User created = new User(); + created.setTenantId(tenantId); + created.setUsername(username); + return created; + }); + + Map before = snapshotUser(user); + if (authSource == AuthenticationSource.LOCAL) { + String passwordHash = (String) payload.get("passwordHash"); + if (passwordHash == null) { + throw new AppException("Local user creation requires password hash"); + } + user.setPassword(passwordHash); + } else { + user.setPassword(null); + user.setLdapDn(ldapDn); + } + user.setAuthSource(authSource); + user.setEnabled(enabled); + user.setRoles(new LinkedHashSet<>(roles)); + User after = userRepository.save(user); + recordManagementTrail("USER_CREATE_APPLY", approvalRequest, event.approvedBy(), before, snapshotUser(after), tenantId); + } + + @Transactional + protected void applyUpdateUserRoles(ApprovalRequest approvalRequest, Map payload, String tenantId, ApprovalCompletedEvent event) { + String username = (String) payload.get("username"); + User user = userRepository.findByTenantIdAndUsername(tenantId, username) + .orElseThrow(() -> new AppException("User not found")); + List roleCodes = castToStringList(payload.get("roleCodes")); + Set roles = resolveRoles(tenantId, roleCodes); + Map before = snapshotUser(user); + user.setRoles(new LinkedHashSet<>(roles)); + User after = userRepository.save(user); + recordManagementTrail("USER_UPDATE_ROLES_APPLY", approvalRequest, event.approvedBy(), before, snapshotUser(after), tenantId); + } + + @Transactional + protected void applyCreateRole(ApprovalRequest approvalRequest, Map payload, String tenantId, ApprovalCompletedEvent event) { + String code = (String) payload.get("code"); + String name = (String) payload.get("name"); + List permissionCodes = castToStringList(payload.get("permissionCodes")); + Set permissions = resolvePermissions(tenantId, permissionCodes); + + Role role = roleRepository.findByTenantIdAndCode(tenantId, code) + .orElseGet(() -> { + Role created = new Role(); + created.setTenantId(tenantId); + created.setCode(code); + created.setName(name); + created.setPermissions(permissions); + return roleRepository.save(created); + }); + + Map before = snapshotRole(role); + role.setName(name); + role.setPermissions(permissions); + Role after = roleRepository.save(role); + recordManagementTrail("ROLE_CREATE_APPLY", approvalRequest, event.approvedBy(), before, snapshotRole(after), tenantId); + } + + @Transactional + protected void applyUpdateRolePermissions(ApprovalRequest approvalRequest, Map payload, String tenantId, ApprovalCompletedEvent event) { + String code = (String) payload.get("code"); + List permissionCodes = castToStringList(payload.get("permissionCodes")); + Role role = roleRepository.findByTenantIdAndCode(tenantId, code) + .orElseThrow(() -> new AppException("Role not found")); + + Set permissions = resolvePermissions(tenantId, permissionCodes); + Map before = snapshotRole(role); + role.setPermissions(permissions); + Role after = roleRepository.save(role); + recordManagementTrail("ROLE_UPDATE_PERMISSIONS_APPLY", approvalRequest, event.approvedBy(), before, snapshotRole(after), tenantId); + } + + private void recordManagementTrail(String action, + ApprovalRequest approvalRequest, + String actor, + Map before, + Map after, + String tenantId) { + TenantContext.setTenantId(tenantId); + auditTrailService.record( + action, + "AUTH", + approvalRequest.getResourceType(), + approvalRequest.getResourceId(), + AuditTrailService.SUCCESS, + "Management request applied", + auditTrailService.toJson(before), + auditTrailService.toJson(after), + null, + null + ); + } + + private void assertUserNotExists(String tenantId, String username) { + if (userRepository.existsByTenantIdAndUsername(tenantId, username)) { + throw new AppException("User already exists"); + } + } + + private AuthenticationSource parseAuthSource(String source, String fallback) { + if (source == null || source.isBlank()) { + return AuthenticationSource.valueOf(fallback); + } + try { + return AuthenticationSource.valueOf(source); + } catch (IllegalArgumentException ex) { + return AuthenticationSource.valueOf(fallback); + } + } + + private Set resolveRoles(String tenantId, Collection roleCodes) { + if (CollectionUtils.isEmpty(roleCodes)) { + return Set.of(); + } + Set roles = roleRepository.findByTenantIdAndCodeIn(tenantId, roleCodes) + .stream().collect(Collectors.toCollection(LinkedHashSet::new)); + if (roles.size() != roleCodes.size()) { + throw new AppException("Some role codes are invalid"); + } + return roles; + } + + private Set resolvePermissions(String tenantId, Collection permissionCodes) { + if (CollectionUtils.isEmpty(permissionCodes)) { + return Set.of(); + } + List permissions = permissionRepository.findByTenantIdAndCodeIn(tenantId, permissionCodes); + if (permissions.size() != permissionCodes.size()) { + throw new AppException("Some permission codes are invalid"); + } + return new LinkedHashSet<>(permissions); + } + + private List castToStringList(Object raw) { + if (!(raw instanceof Collection values) || CollectionUtils.isEmpty(values)) { + return List.of(); + } + return values.stream() + .map(String::valueOf) + .toList(); + } + + private Map snapshotUser(User user) { + Map snapshot = new LinkedHashMap<>(); + snapshot.put("id", user.getId() != null ? user.getId().toString() : null); + snapshot.put("tenantId", user.getTenantId()); + snapshot.put("username", user.getUsername()); + snapshot.put("authSource", user.getAuthSource()); + snapshot.put("ldapDn", user.getLdapDn()); + snapshot.put("enabled", user.isEnabled()); + snapshot.put("roles", user.getRoles().stream() + .map(Role::getCode) + .sorted() + .toList()); + return snapshot; + } + + private Map snapshotRole(Role role) { + Map snapshot = new LinkedHashMap<>(); + snapshot.put("id", role.getId() != null ? role.getId().toString() : null); + snapshot.put("tenantId", role.getTenantId()); + snapshot.put("code", role.getCode()); + snapshot.put("name", role.getName()); + snapshot.put("permissions", role.getPermissions().stream() + .map(Permission::getCode) + .sorted() + .toList()); + return snapshot; + } + + private boolean asBoolean(Object value, boolean defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Boolean bool) { + return bool; + } + return Boolean.parseBoolean(value.toString()); + } + + private String toJson(Map payload) { + return auditTrailService.toJson(payload); + } +} diff --git a/src/main/java/id/iptek/utms/auth/service/UserService.java b/src/main/java/id/iptek/utms/auth/service/UserService.java new file mode 100644 index 0000000..772149a --- /dev/null +++ b/src/main/java/id/iptek/utms/auth/service/UserService.java @@ -0,0 +1,37 @@ +package id.iptek.utms.auth.service; + +import id.iptek.utms.auth.domain.Role; +import id.iptek.utms.auth.domain.User; +import id.iptek.utms.auth.dto.CurrentUserResponse; +import id.iptek.utms.auth.repository.UserRepository; +import id.iptek.utms.core.exception.AppException; +import id.iptek.utms.tenant.TenantContext; +import org.springframework.stereotype.Service; + +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class UserService { + + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public CurrentUserResponse me(String username) { + String tenantId = TenantContext.getRequiredTenantId(); + User user = userRepository.findByTenantIdAndUsername(tenantId, username) + .orElseThrow(() -> new AppException("User not found")); + + Set roleCodes = user.getRoles().stream().map(Role::getCode).collect(Collectors.toSet()); + Set permissions = user.getRoles().stream() + .flatMap(role -> role.getPermissions().stream()) + .map(permission -> permission.getCode()) + .collect(Collectors.toSet()); + + return new CurrentUserResponse(tenantId, user.getUsername(), roleCodes, permissions); + } +} + diff --git a/src/main/java/id/iptek/utms/core/audit/domain/AuditTrail.java b/src/main/java/id/iptek/utms/core/audit/domain/AuditTrail.java new file mode 100644 index 0000000..28b2bef --- /dev/null +++ b/src/main/java/id/iptek/utms/core/audit/domain/AuditTrail.java @@ -0,0 +1,69 @@ +package id.iptek.utms.core.audit.domain; + +import id.iptek.utms.core.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.UUID; + +@Getter +@Setter +@Entity +@Table( + name = "sys_audit_trails", + indexes = { + @Index(name = "sys_idx_audit_tenant_created", columnList = "tenant_id,created_at"), + @Index(name = "sys_idx_audit_correlation", columnList = "correlation_id"), + @Index(name = "sys_idx_audit_actor", columnList = "actor"), + @Index(name = "sys_idx_audit_action", columnList = "action") + } +) +public class AuditTrail extends BaseEntity { + + @Id + @GeneratedValue + private UUID id; + + @Column(name = "actor", nullable = false, length = 255) + private String actor; + + @Column(name = "action", nullable = false, length = 100) + private String action; + + @Column(name = "correlation_id", length = 100) + private String correlationId; + + @Column(name = "domain", length = 100) + private String domain; + + @Column(name = "resource_type", length = 100) + private String resourceType; + + @Column(name = "resource_id", length = 255) + private String resourceId; + + @Column(name = "outcome", nullable = false, length = 20) + private String outcome; + + @Column(name = "http_method", length = 20) + private String httpMethod; + + @Column(name = "request_path", length = 500) + private String requestPath; + + @Column(name = "client_ip", length = 80) + private String clientIp; + + @Column(name = "error_message", length = 1000) + private String errorMessage; + + @Column(name = "details", columnDefinition = "text") + private String details; + + @Column(name = "before_state", columnDefinition = "text") + private String beforeState; + + @Column(name = "after_state", columnDefinition = "text") + private String afterState; +} diff --git a/src/main/java/id/iptek/utms/core/audit/dto/AuditTrailResponse.java b/src/main/java/id/iptek/utms/core/audit/dto/AuditTrailResponse.java new file mode 100644 index 0000000..8e794ae --- /dev/null +++ b/src/main/java/id/iptek/utms/core/audit/dto/AuditTrailResponse.java @@ -0,0 +1,23 @@ +package id.iptek.utms.core.audit.dto; + +import java.time.Instant; +import java.util.UUID; + +public record AuditTrailResponse( + UUID id, + String tenantId, + String actor, + String correlationId, + String action, + String domain, + String resourceType, + String resourceId, + String outcome, + String httpMethod, + String requestPath, + String errorMessage, + String beforeState, + String afterState, + String details, + Instant createdAt +) {} diff --git a/src/main/java/id/iptek/utms/core/audit/repository/AuditTrailRepository.java b/src/main/java/id/iptek/utms/core/audit/repository/AuditTrailRepository.java new file mode 100644 index 0000000..15fe34e --- /dev/null +++ b/src/main/java/id/iptek/utms/core/audit/repository/AuditTrailRepository.java @@ -0,0 +1,12 @@ +package id.iptek.utms.core.audit.repository; + +import id.iptek.utms.core.audit.domain.AuditTrail; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface AuditTrailRepository extends JpaRepository { + Page findByTenantIdOrderByCreatedAtDesc(String tenantId, Pageable pageable); +} diff --git a/src/main/java/id/iptek/utms/core/audit/service/AuditTrailService.java b/src/main/java/id/iptek/utms/core/audit/service/AuditTrailService.java new file mode 100644 index 0000000..24ef69a --- /dev/null +++ b/src/main/java/id/iptek/utms/core/audit/service/AuditTrailService.java @@ -0,0 +1,146 @@ +package id.iptek.utms.core.audit.service; + +import id.iptek.utms.core.audit.domain.AuditTrail; +import id.iptek.utms.core.audit.repository.AuditTrailRepository; +import id.iptek.utms.core.security.SecurityUtils; +import id.iptek.utms.tenant.TenantContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@Service +public class AuditTrailService { + + public static final String SUCCESS = "SUCCESS"; + public static final String FAILURE = "FAILURE"; + public static final String CORRELATION_ID_ATTRIBUTE = "UTMS_AUDIT_CORRELATION_ID"; + + private final AuditTrailRepository auditTrailRepository; + private final ObjectMapper objectMapper; + + public AuditTrailService(AuditTrailRepository auditTrailRepository, ObjectMapper objectMapper) { + this.auditTrailRepository = auditTrailRepository; + this.objectMapper = objectMapper; + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void record(String action, + String domain, + String resourceType, + String resourceId, + String outcome, + String details, + String errorMessage, + HttpServletRequest request) { + record(action, domain, resourceType, resourceId, outcome, details, null, null, null, errorMessage, request); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void record(String action, + String domain, + String resourceType, + String resourceId, + String outcome, + String details, + String beforeState, + String afterState, + String errorMessage, + HttpServletRequest request) { + String correlationId = resolveOrCreateCorrelationId(request); + record(action, domain, resourceType, resourceId, outcome, details, beforeState, afterState, correlationId, errorMessage, request); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void record(String action, + String domain, + String resourceType, + String resourceId, + String outcome, + String details, + String beforeState, + String afterState, + String correlationId, + String errorMessage, + HttpServletRequest request) { + String tenantId = TenantContext.getTenantId() != null ? TenantContext.getTenantId() : "system"; + String actor = SecurityUtils.currentUsername(); + if (actor == null || actor.isBlank()) { + actor = "anonymous"; + } + + AuditTrail trail = new AuditTrail(); + trail.setTenantId(tenantId); + trail.setCorrelationId(correlationId); + trail.setActor(actor); + trail.setAction(action); + trail.setDomain(domain); + trail.setResourceType(resourceType); + trail.setResourceId(resourceId); + trail.setOutcome(outcome != null ? outcome : SUCCESS); + trail.setErrorMessage(errorMessage); + trail.setDetails(details); + trail.setBeforeState(beforeState); + trail.setAfterState(afterState); + + if (request != null) { + trail.setHttpMethod(request.getMethod()); + trail.setRequestPath(request.getRequestURI()); + trail.setClientIp(request.getRemoteAddr()); + } + + auditTrailRepository.save(trail); + } + + private String resolveOrCreateCorrelationId(HttpServletRequest request) { + if (request == null) { + return java.util.UUID.randomUUID().toString(); + } + Object existing = request.getAttribute(CORRELATION_ID_ATTRIBUTE); + if (existing instanceof String existingValue && !existingValue.isBlank()) { + return existingValue; + } + String created = java.util.UUID.randomUUID().toString(); + request.setAttribute(CORRELATION_ID_ATTRIBUTE, created); + return created; + } + + public String toJson(Object value) { + if (value == null) { + return null; + } + try { + if (value instanceof String stringValue) { + return stringValue; + } + return objectMapper.writeValueAsString(value); + } catch (Exception ex) { + return objectMapper.createObjectNode() + .put("type", value.getClass().getName()) + .put("value", String.valueOf(value)) + .toString(); + } + } + + public String toChangeSummary(String event, Map before, Map after) { + return toJson(Map.of( + "event", event, + "before", before, + "after", after + )); + } + + public List listRecent(String tenantId, int limit) { + int normalized = Math.max(1, Math.min(limit, 500)); + return auditTrailRepository.findByTenantIdOrderByCreatedAtDesc( + tenantId, + PageRequest.of(0, normalized, Sort.by(Sort.Direction.DESC, "createdAt")) + ).getContent(); + } +} diff --git a/src/main/java/id/iptek/utms/core/config/ActiveMqConfig.java b/src/main/java/id/iptek/utms/core/config/ActiveMqConfig.java new file mode 100644 index 0000000..99b903b --- /dev/null +++ b/src/main/java/id/iptek/utms/core/config/ActiveMqConfig.java @@ -0,0 +1,10 @@ +package id.iptek.utms.core.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.annotation.EnableJms; + +@EnableJms +@Configuration +public class ActiveMqConfig { +} + diff --git a/src/main/java/id/iptek/utms/core/config/AuditLoggingAspect.java b/src/main/java/id/iptek/utms/core/config/AuditLoggingAspect.java new file mode 100644 index 0000000..72eef67 --- /dev/null +++ b/src/main/java/id/iptek/utms/core/config/AuditLoggingAspect.java @@ -0,0 +1,72 @@ +package id.iptek.utms.core.config; + +import id.iptek.utms.core.security.SecurityUtils; +import id.iptek.utms.core.audit.service.AuditTrailService; +import id.iptek.utms.tenant.TenantContext; +import jakarta.servlet.http.HttpServletRequest; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Aspect +@Component +public class AuditLoggingAspect { + + private static final Logger log = LoggerFactory.getLogger(AuditLoggingAspect.class); + private final AuditTrailService auditTrailService; + + public AuditLoggingAspect(AuditTrailService auditTrailService) { + this.auditTrailService = auditTrailService; + } + + @Around("execution(* id.iptek.utms..controller..*(..))") + public Object auditControllerCalls(ProceedingJoinPoint joinPoint) throws Throwable { + String user = SecurityUtils.currentUsername(); + String tenant = TenantContext.getTenantId() != null ? TenantContext.getTenantId() : "system"; + String method = joinPoint.getSignature().toShortString(); + HttpServletRequest request = currentRequest(); + + log.info("AUDIT START method={} user={} tenant={}", method, user, tenant); + try { + Object result = joinPoint.proceed(); + log.info("AUDIT SUCCESS method={} user={} tenant={}", method, user, tenant); + auditTrailService.record( + method, + "CONTROLLER", + null, + null, + AuditTrailService.SUCCESS, + "Controller invocation completed", + null, + request + ); + return result; + } catch (Throwable t) { + log.warn("AUDIT FAIL method={} user={} tenant={} error={}", method, user, tenant, t.getMessage()); + auditTrailService.record( + method, + "CONTROLLER", + null, + null, + AuditTrailService.FAILURE, + "Controller invocation failed", + t.getMessage(), + request + ); + throw t; + } + } + + private HttpServletRequest currentRequest() { + if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attributes) { + return attributes.getRequest(); + } + return null; + } +} + diff --git a/src/main/java/id/iptek/utms/core/config/DataSeeder.java b/src/main/java/id/iptek/utms/core/config/DataSeeder.java new file mode 100644 index 0000000..1b13f99 --- /dev/null +++ b/src/main/java/id/iptek/utms/core/config/DataSeeder.java @@ -0,0 +1,183 @@ +package id.iptek.utms.core.config; + +import id.iptek.utms.auth.domain.Permission; +import id.iptek.utms.auth.domain.Role; +import id.iptek.utms.auth.domain.User; +import id.iptek.utms.auth.repository.PermissionRepository; +import id.iptek.utms.auth.repository.RoleRepository; +import id.iptek.utms.auth.repository.UserRepository; +import id.iptek.utms.module.domain.SystemModule; +import id.iptek.utms.module.repository.SystemModuleRepository; +import id.iptek.utms.tenant.Tenant; +import id.iptek.utms.tenant.TenantContext; +import id.iptek.utms.tenant.TenantRepository; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +@Profile("dev") +public class DataSeeder implements CommandLineRunner { + + private final TenantRepository tenantRepository; + private final PermissionRepository permissionRepository; + private final RoleRepository roleRepository; + private final UserRepository userRepository; + private final SystemModuleRepository moduleRepository; + private final PasswordEncoder passwordEncoder; + private final boolean seedEnabled; + + public DataSeeder(TenantRepository tenantRepository, + PermissionRepository permissionRepository, + RoleRepository roleRepository, + UserRepository userRepository, + SystemModuleRepository moduleRepository, + PasswordEncoder passwordEncoder, + @org.springframework.beans.factory.annotation.Value("${app.seed.enabled:false}") boolean seedEnabled) { + this.tenantRepository = tenantRepository; + this.permissionRepository = permissionRepository; + this.roleRepository = roleRepository; + this.userRepository = userRepository; + this.moduleRepository = moduleRepository; + this.passwordEncoder = passwordEncoder; + this.seedEnabled = seedEnabled; + } + + @Override + @Transactional + public void run(String... args) { + if (!seedEnabled) { + return; + } + seedTenantData("acme", "Acme Corporation"); + seedTenantData("test", "Test Tenant"); + } + + private void seedTenantData(String tenantId, String tenantName) { + Tenant tenant = tenantRepository.findByTenantIdAndActiveTrue(tenantId).orElseGet(() -> { + Tenant t = new Tenant(); + t.setTenantId(tenantId); + t.setName(tenantName); + t.setActive(true); + return tenantRepository.save(t); + }); + + TenantContext.setTenantId(tenant.getTenantId()); + try { + Permission userRead = getOrCreatePermission("USER_READ", "Read user profile"); + Permission userManage = getOrCreatePermission("USER_MANAGE", "Manage users"); + Permission roleManage = getOrCreatePermission("ROLE_MANAGE", "Manage roles"); + Permission workflowCreate = getOrCreatePermission("WORKFLOW_CREATE", "Create workflow request"); + Permission workflowApprove = getOrCreatePermission("WORKFLOW_APPROVE", "Approve workflow request"); + + // Legacy sample roles + Role maker = getOrCreateRole("MAKER", "Maker", Set.of(workflowCreate)); + Role checker = getOrCreateRole("CHECKER", "Checker", Set.of(workflowApprove)); + Role admin = getOrCreateRole("ADMIN", "Administrator", Set.of(userRead, workflowCreate, workflowApprove)); + + // Bootstrap manager role that can manage both user and role lifecycle via workflow + Role userRoleAdmin = getOrCreateRole( + "USER_ROLE_ADMIN", + "User & Role Administrator", + Set.of(userRead, userManage, roleManage, workflowCreate, workflowApprove) + ); + + getOrCreateUser("maker", "Passw0rd!", Set.of(maker)); + getOrCreateUser("checker", "Passw0rd!", Set.of(checker)); + getOrCreateUser("admin", "Passw0rd!", Set.of(admin)); + getOrCreateUser("system.manager", "Passw0rd!", Set.of(userRoleAdmin)); + getOrCreateUser("system.owner", "Passw0rd!", Set.of(admin, userRoleAdmin)); + + if ("acme".equals(tenantId)) { + getOrCreateUser("acme.owner", "Passw0rd!", Set.of(admin)); + getOrCreateModule("NOTIFICATION", "Notification Module", true); + getOrCreateModule("REPORTING", "Reporting Module", false); + getOrCreateModule("AUDIT", "Audit Trail Module", true); + } else { + getOrCreateModule("NOTIFICATION", "Notification Module", true); + } + } finally { + TenantContext.clear(); + } + } + + private Permission getOrCreatePermission(String code, String name) { + return permissionRepository.findByTenantIdAndCode(TenantContext.getRequiredTenantId(), code) + .orElseGet(() -> { + Permission p = new Permission(); + p.setCode(code); + p.setName(name); + p.setTenantId(TenantContext.getRequiredTenantId()); + return permissionRepository.save(p); + }); + } + + private Role getOrCreateRole(String code, String name, Set permissions) { + String tenantId = TenantContext.getRequiredTenantId(); + Role role = roleRepository.findByTenantIdAndCode(tenantId, code) + .orElseGet(() -> { + Role roleEntity = new Role(); + roleEntity.setCode(code); + roleEntity.setName(name); + roleEntity.setPermissions(new HashSet<>()); + roleEntity.setTenantId(tenantId); + return roleEntity; + }); + role.setName(name); + role.setTenantId(tenantId); + addMissingByCode(role.getPermissions(), permissions, Permission::getCode); + return roleRepository.save(role); + } + + private void getOrCreateUser(String username, String rawPassword, Set roles) { + String tenantId = TenantContext.getRequiredTenantId(); + User user = userRepository.findByTenantIdAndUsername(tenantId, username) + .orElseGet(() -> { + User created = new User(); + created.setUsername(username); + created.setPassword(passwordEncoder.encode(rawPassword)); + created.setEnabled(true); + created.setRoles(new HashSet<>()); + created.setTenantId(tenantId); + return created; + }); + + if (user.getId() == null) { + user.setPassword(passwordEncoder.encode(rawPassword)); + } + user.setTenantId(tenantId); + addMissingByCode(user.getRoles(), roles, Role::getCode); + userRepository.save(user); + } + + private void getOrCreateModule(String code, String name, boolean enabled) { + moduleRepository.findByTenantIdAndCode(TenantContext.getRequiredTenantId(), code) + .orElseGet(() -> { + SystemModule module = new SystemModule(); + module.setCode(code); + module.setName(name); + module.setEnabled(enabled); + module.setTenantId(TenantContext.getRequiredTenantId()); + return moduleRepository.save(module); + }); + } + + private void addMissingByCode(Collection target, Collection requested, java.util.function.Function codeExtractor) { + Set existingCodes = target.stream() + .map(codeExtractor) + .collect(Collectors.toSet()); + List missing = requested.stream() + .filter(item -> item != null && !existingCodes.contains(codeExtractor.apply(item))) + .toList(); + target.addAll(missing); + } +} + diff --git a/src/main/java/id/iptek/utms/core/config/I18nConfig.java b/src/main/java/id/iptek/utms/core/config/I18nConfig.java new file mode 100644 index 0000000..c52b2c2 --- /dev/null +++ b/src/main/java/id/iptek/utms/core/config/I18nConfig.java @@ -0,0 +1,20 @@ +package id.iptek.utms.core.config; + +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; + +@Configuration +public class I18nConfig { + + @Bean + public MessageSource messageSource() { + ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource(); + source.setBasename("classpath:i18n/messages"); + source.setDefaultEncoding("UTF-8"); + source.setFallbackToSystemLocale(false); + return source; + } +} + diff --git a/src/main/java/id/iptek/utms/core/config/JpaAuditConfig.java b/src/main/java/id/iptek/utms/core/config/JpaAuditConfig.java new file mode 100644 index 0000000..355b6c6 --- /dev/null +++ b/src/main/java/id/iptek/utms/core/config/JpaAuditConfig.java @@ -0,0 +1,20 @@ +package id.iptek.utms.core.config; + +import id.iptek.utms.core.security.SecurityUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import java.util.Optional; + +@Configuration +@EnableJpaAuditing +public class JpaAuditConfig { + + @Bean + public AuditorAware auditorAware() { + return () -> Optional.ofNullable(SecurityUtils.currentUsername()).or(() -> Optional.of("system")); + } +} + diff --git a/src/main/java/id/iptek/utms/core/config/LocaleConfig.java b/src/main/java/id/iptek/utms/core/config/LocaleConfig.java new file mode 100644 index 0000000..6213506 --- /dev/null +++ b/src/main/java/id/iptek/utms/core/config/LocaleConfig.java @@ -0,0 +1,20 @@ +package id.iptek.utms.core.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; + +import java.util.Locale; + +@Configuration +public class LocaleConfig { + + @Bean + public LocaleResolver localeResolver() { + AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver(); + resolver.setDefaultLocale(Locale.ENGLISH); + return resolver; + } +} + diff --git a/src/main/java/id/iptek/utms/core/config/OpenApiConfig.java b/src/main/java/id/iptek/utms/core/config/OpenApiConfig.java new file mode 100644 index 0000000..9bc11c6 --- /dev/null +++ b/src/main/java/id/iptek/utms/core/config/OpenApiConfig.java @@ -0,0 +1,36 @@ +package id.iptek.utms.core.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.security.SecurityScheme.In; +import io.swagger.v3.oas.models.security.SecurityScheme.Type; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI() { + final String securitySchemeName = "bearerAuth"; + + return new OpenAPI() + .info(new Info() + .title("UTMS NG BE API") + .version("1.0.0") + .description("Authentication: click Authorize and paste only the raw JWT. UI will send 'Authorization: Bearer '.")) + .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) + .components(new Components().addSecuritySchemes(securitySchemeName, + new SecurityScheme() + .name("Authorization") + .type(Type.HTTP) + .in(In.HEADER) + .scheme("bearer") + .bearerFormat("JWT") + )); + } +} + diff --git a/src/main/java/id/iptek/utms/core/config/RedisConfig.java b/src/main/java/id/iptek/utms/core/config/RedisConfig.java new file mode 100644 index 0000000..3e45d65 --- /dev/null +++ b/src/main/java/id/iptek/utms/core/config/RedisConfig.java @@ -0,0 +1,31 @@ +package id.iptek.utms.core.config; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@Configuration +@EnableCaching +public class RedisConfig { + + @Bean + public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(10)) + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .disableCachingNullValues(); + + return RedisCacheManager.builder(connectionFactory) + .cacheDefaults(config) + .build(); + } +} + diff --git a/src/main/java/id/iptek/utms/core/domain/BaseEntity.java b/src/main/java/id/iptek/utms/core/domain/BaseEntity.java new file mode 100644 index 0000000..6446821 --- /dev/null +++ b/src/main/java/id/iptek/utms/core/domain/BaseEntity.java @@ -0,0 +1,48 @@ +package id.iptek.utms.core.domain; + +import id.iptek.utms.tenant.TenantContext; +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.Instant; + +@Getter +@Setter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @Column(name = "tenant_id", nullable = false, updatable = false) + private String tenantId; + + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; + + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private Instant updatedAt; + + @CreatedBy + @Column(name = "created_by") + private String createdBy; + + @LastModifiedBy + @Column(name = "updated_by") + private String updatedBy; + + protected void applyTenantFromContext() { + if (tenantId == null) { + tenantId = TenantContext.getRequiredTenantId(); + } + } +} + diff --git a/src/main/java/id/iptek/utms/core/domain/TenantEntityListener.java b/src/main/java/id/iptek/utms/core/domain/TenantEntityListener.java new file mode 100644 index 0000000..b1662de --- /dev/null +++ b/src/main/java/id/iptek/utms/core/domain/TenantEntityListener.java @@ -0,0 +1,14 @@ +package id.iptek.utms.core.domain; + +import jakarta.persistence.PrePersist; + +public class TenantEntityListener { + + @PrePersist + public void prePersist(Object entity) { + if (entity instanceof BaseEntity baseEntity) { + baseEntity.applyTenantFromContext(); + } + } +} + diff --git a/src/main/java/id/iptek/utms/core/exception/AppException.java b/src/main/java/id/iptek/utms/core/exception/AppException.java new file mode 100644 index 0000000..bfebee9 --- /dev/null +++ b/src/main/java/id/iptek/utms/core/exception/AppException.java @@ -0,0 +1,9 @@ +package id.iptek.utms.core.exception; + +public class AppException extends RuntimeException { + + public AppException(String message) { + super(message); + } +} + diff --git a/src/main/java/id/iptek/utms/core/exception/GlobalExceptionHandler.java b/src/main/java/id/iptek/utms/core/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..1d7511b --- /dev/null +++ b/src/main/java/id/iptek/utms/core/exception/GlobalExceptionHandler.java @@ -0,0 +1,53 @@ +package id.iptek.utms.core.exception; + +import id.iptek.utms.api.ApiResponse; +import id.iptek.utms.core.i18n.MessageResolver; +import jakarta.validation.ConstraintViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private final MessageResolver messageResolver; + + public GlobalExceptionHandler(MessageResolver messageResolver) { + this.messageResolver = messageResolver; + } + + @ExceptionHandler(AppException.class) + public ResponseEntity> handleAppException(AppException ex) { + return ResponseEntity.badRequest().body(ApiResponse.fail(ex.getMessage())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException ex) { + String errorMessage = ex.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(err -> err.getField() + " " + err.getDefaultMessage()) + .orElse(messageResolver.get("error.validation")); + return ResponseEntity.badRequest().body(ApiResponse.fail(errorMessage)); + } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolation(ConstraintViolationException ex) { + return ResponseEntity.badRequest().body(ApiResponse.fail(ex.getMessage())); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity> handleAccessDenied() { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.fail(messageResolver.get("error.forbidden"))); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGeneralException(Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.fail(messageResolver.get("error.internal"))); + } +} + diff --git a/src/main/java/id/iptek/utms/core/i18n/MessageResolver.java b/src/main/java/id/iptek/utms/core/i18n/MessageResolver.java new file mode 100644 index 0000000..ba2f157 --- /dev/null +++ b/src/main/java/id/iptek/utms/core/i18n/MessageResolver.java @@ -0,0 +1,20 @@ +package id.iptek.utms.core.i18n; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class MessageResolver { + + private final MessageSource messageSource; + + public MessageResolver(MessageSource messageSource) { + this.messageSource = messageSource; + } + + public String get(String key, Object... args) { + return messageSource.getMessage(key, args, LocaleContextHolder.getLocale()); + } +} + diff --git a/src/main/java/id/iptek/utms/core/security/SecurityUtils.java b/src/main/java/id/iptek/utms/core/security/SecurityUtils.java new file mode 100644 index 0000000..30cc995 --- /dev/null +++ b/src/main/java/id/iptek/utms/core/security/SecurityUtils.java @@ -0,0 +1,19 @@ +package id.iptek.utms.core.security; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public final class SecurityUtils { + + private SecurityUtils() { + } + + public static String currentUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return null; + } + return authentication.getName(); + } +} + diff --git a/src/main/java/id/iptek/utms/messaging/ApprovalCompletedEvent.java b/src/main/java/id/iptek/utms/messaging/ApprovalCompletedEvent.java new file mode 100644 index 0000000..45f6120 --- /dev/null +++ b/src/main/java/id/iptek/utms/messaging/ApprovalCompletedEvent.java @@ -0,0 +1,14 @@ +package id.iptek.utms.messaging; + +import java.io.Serializable; +import java.util.UUID; + +public record ApprovalCompletedEvent( + UUID requestId, + String tenantId, + String resourceType, + String resourceId, + String approvedBy +) implements Serializable { +} + diff --git a/src/main/java/id/iptek/utms/messaging/ApprovalEventConsumer.java b/src/main/java/id/iptek/utms/messaging/ApprovalEventConsumer.java new file mode 100644 index 0000000..bce7db7 --- /dev/null +++ b/src/main/java/id/iptek/utms/messaging/ApprovalEventConsumer.java @@ -0,0 +1,27 @@ +package id.iptek.utms.messaging; + +import id.iptek.utms.auth.service.UserRoleManagementService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class ApprovalEventConsumer { + + private static final Logger log = LoggerFactory.getLogger(ApprovalEventConsumer.class); + private final UserRoleManagementService userRoleManagementService; + + public ApprovalEventConsumer(UserRoleManagementService userRoleManagementService) { + this.userRoleManagementService = userRoleManagementService; + } + + @JmsListener(destination = "approval.completed.queue") + public void onApprovalCompleted(ApprovalCompletedEvent event) { + log.info("Received approval.completed event requestId={} tenant={} resourceType={}", + event.requestId(), event.tenantId(), event.resourceType()); + userRoleManagementService.applyApprovedRequest(event); + } + +} + diff --git a/src/main/java/id/iptek/utms/messaging/ApprovalEventProducer.java b/src/main/java/id/iptek/utms/messaging/ApprovalEventProducer.java new file mode 100644 index 0000000..3873654 --- /dev/null +++ b/src/main/java/id/iptek/utms/messaging/ApprovalEventProducer.java @@ -0,0 +1,19 @@ +package id.iptek.utms.messaging; + +import org.springframework.jms.core.JmsTemplate; +import org.springframework.stereotype.Component; + +@Component +public class ApprovalEventProducer { + + private final JmsTemplate jmsTemplate; + + public ApprovalEventProducer(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + public void publishCompleted(ApprovalCompletedEvent event) { + jmsTemplate.convertAndSend("approval.completed.queue", event); + } +} + diff --git a/src/main/java/id/iptek/utms/module/controller/ModuleController.java b/src/main/java/id/iptek/utms/module/controller/ModuleController.java new file mode 100644 index 0000000..d611608 --- /dev/null +++ b/src/main/java/id/iptek/utms/module/controller/ModuleController.java @@ -0,0 +1,43 @@ +package id.iptek.utms.module.controller; + +import id.iptek.utms.api.ApiResponse; +import id.iptek.utms.core.i18n.MessageResolver; +import id.iptek.utms.module.dto.ModuleResponse; +import id.iptek.utms.module.dto.ModuleToggleRequest; +import id.iptek.utms.module.service.ModuleRegistryService; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/modules") +@SecurityRequirement(name = "bearerAuth") +public class ModuleController { + + private final ModuleRegistryService moduleRegistryService; + private final MessageResolver messageResolver; + + public ModuleController(ModuleRegistryService moduleRegistryService, MessageResolver messageResolver) { + this.moduleRegistryService = moduleRegistryService; + this.messageResolver = messageResolver; + } + + @GetMapping + @PreAuthorize("hasRole('ADMIN')") + public ApiResponse> list() { + return ApiResponse.ok(messageResolver.get("module.list.success"), moduleRegistryService.listModules()); + } + + @PostMapping("/{code}/toggle") + @PreAuthorize("hasRole('ADMIN')") + public ApiResponse toggle(@PathVariable String code, + @RequestBody ModuleToggleRequest request, + HttpServletRequest servletRequest) { + return ApiResponse.ok(messageResolver.get("module.toggle.success"), + moduleRegistryService.setEnabled(code, request.enabled(), servletRequest)); + } +} + diff --git a/src/main/java/id/iptek/utms/module/domain/SystemModule.java b/src/main/java/id/iptek/utms/module/domain/SystemModule.java new file mode 100644 index 0000000..beebe3e --- /dev/null +++ b/src/main/java/id/iptek/utms/module/domain/SystemModule.java @@ -0,0 +1,35 @@ +package id.iptek.utms.module.domain; + +import id.iptek.utms.core.domain.BaseEntity; +import id.iptek.utms.core.domain.TenantEntityListener; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.Filter; + +import java.util.UUID; + +@Getter +@Setter +@Entity +@EntityListeners(TenantEntityListener.class) +@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId") +@Table(name = "sys_system_modules", uniqueConstraints = { + @UniqueConstraint(name = "sys_uk_system_modules_tenant_code", columnNames = {"tenant_id", "code"}) +}) +public class SystemModule extends BaseEntity { + + @Id + @GeneratedValue + private UUID id; + + @Column(nullable = false) + private String code; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private boolean enabled; +} + diff --git a/src/main/java/id/iptek/utms/module/dto/ModuleResponse.java b/src/main/java/id/iptek/utms/module/dto/ModuleResponse.java new file mode 100644 index 0000000..f02af6b --- /dev/null +++ b/src/main/java/id/iptek/utms/module/dto/ModuleResponse.java @@ -0,0 +1,9 @@ +package id.iptek.utms.module.dto; + +public record ModuleResponse( + String code, + String name, + boolean enabled +) { +} + diff --git a/src/main/java/id/iptek/utms/module/dto/ModuleToggleRequest.java b/src/main/java/id/iptek/utms/module/dto/ModuleToggleRequest.java new file mode 100644 index 0000000..50520a5 --- /dev/null +++ b/src/main/java/id/iptek/utms/module/dto/ModuleToggleRequest.java @@ -0,0 +1,5 @@ +package id.iptek.utms.module.dto; + +public record ModuleToggleRequest(boolean enabled) { +} + diff --git a/src/main/java/id/iptek/utms/module/repository/SystemModuleRepository.java b/src/main/java/id/iptek/utms/module/repository/SystemModuleRepository.java new file mode 100644 index 0000000..e7e94fc --- /dev/null +++ b/src/main/java/id/iptek/utms/module/repository/SystemModuleRepository.java @@ -0,0 +1,14 @@ +package id.iptek.utms.module.repository; + +import id.iptek.utms.module.domain.SystemModule; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface SystemModuleRepository extends JpaRepository { + Optional findByTenantIdAndCode(String tenantId, String code); + List findByTenantId(String tenantId); +} + diff --git a/src/main/java/id/iptek/utms/module/service/Module.java b/src/main/java/id/iptek/utms/module/service/Module.java new file mode 100644 index 0000000..9577aa6 --- /dev/null +++ b/src/main/java/id/iptek/utms/module/service/Module.java @@ -0,0 +1,8 @@ +package id.iptek.utms.module.service; + +public interface Module { + String code(); + void onEnabled(String tenantId); + void onDisabled(String tenantId); +} + diff --git a/src/main/java/id/iptek/utms/module/service/ModuleRegistryService.java b/src/main/java/id/iptek/utms/module/service/ModuleRegistryService.java new file mode 100644 index 0000000..eafea28 --- /dev/null +++ b/src/main/java/id/iptek/utms/module/service/ModuleRegistryService.java @@ -0,0 +1,87 @@ +package id.iptek.utms.module.service; + +import id.iptek.utms.core.exception.AppException; +import id.iptek.utms.core.audit.service.AuditTrailService; +import id.iptek.utms.module.domain.SystemModule; +import id.iptek.utms.module.dto.ModuleResponse; +import id.iptek.utms.module.repository.SystemModuleRepository; +import id.iptek.utms.tenant.TenantContext; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.LinkedHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class ModuleRegistryService { + + private final SystemModuleRepository systemModuleRepository; + private final Map moduleHandlers; + private final AuditTrailService auditTrailService; + + public ModuleRegistryService(SystemModuleRepository systemModuleRepository, + List moduleHandlers, + AuditTrailService auditTrailService) { + this.systemModuleRepository = systemModuleRepository; + this.moduleHandlers = moduleHandlers.stream().collect(Collectors.toMap(Module::code, Function.identity())); + this.auditTrailService = auditTrailService; + } + + public List listModules() { + String tenantId = TenantContext.getRequiredTenantId(); + return systemModuleRepository.findByTenantId(tenantId).stream() + .map(module -> new ModuleResponse(module.getCode(), module.getName(), module.isEnabled())) + .toList(); + } + + @Transactional + public ModuleResponse setEnabled(String code, boolean enabled, HttpServletRequest request) { + String tenantId = TenantContext.getRequiredTenantId(); + SystemModule module = systemModuleRepository.findByTenantIdAndCode(tenantId, code) + .orElseThrow(() -> new AppException("Module not found: " + code)); + String beforeState = auditTrailService.toJson(moduleSnapshot(module)); + + module.setEnabled(enabled); + systemModuleRepository.save(module); + + String afterState = auditTrailService.toJson(moduleSnapshot(module)); + auditTrailService.record( + "MODULE_TOGGLE", + "MODULE", + "SystemModule", + module.getId() != null ? module.getId().toString() : code, + AuditTrailService.SUCCESS, + "Module toggle changed", + beforeState, + afterState, + null, + request + ); + + Module handler = moduleHandlers.get(code); + if (handler != null) { + if (enabled) { + handler.onEnabled(tenantId); + } else { + handler.onDisabled(tenantId); + } + } + + return new ModuleResponse(module.getCode(), module.getName(), module.isEnabled()); + } + + private Map moduleSnapshot(SystemModule module) { + Map snapshot = new LinkedHashMap<>(); + snapshot.put("id", module.getId() != null ? module.getId().toString() : null); + snapshot.put("tenantId", module.getTenantId()); + snapshot.put("code", module.getCode()); + snapshot.put("name", module.getName()); + snapshot.put("enabled", module.isEnabled()); + return snapshot; + } +} + diff --git a/src/main/java/id/iptek/utms/module/service/NotificationModule.java b/src/main/java/id/iptek/utms/module/service/NotificationModule.java new file mode 100644 index 0000000..bf07ce6 --- /dev/null +++ b/src/main/java/id/iptek/utms/module/service/NotificationModule.java @@ -0,0 +1,27 @@ +package id.iptek.utms.module.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class NotificationModule implements Module { + + private static final Logger log = LoggerFactory.getLogger(NotificationModule.class); + + @Override + public String code() { + return "NOTIFICATION"; + } + + @Override + public void onEnabled(String tenantId) { + log.info("Notification module enabled for tenant={}", tenantId); + } + + @Override + public void onDisabled(String tenantId) { + log.info("Notification module disabled for tenant={}", tenantId); + } +} + diff --git a/src/main/java/id/iptek/utms/preference/domain/UserUiPreference.java b/src/main/java/id/iptek/utms/preference/domain/UserUiPreference.java new file mode 100644 index 0000000..94bf3cb --- /dev/null +++ b/src/main/java/id/iptek/utms/preference/domain/UserUiPreference.java @@ -0,0 +1,41 @@ +package id.iptek.utms.preference.domain; + +import id.iptek.utms.core.domain.BaseEntity; +import id.iptek.utms.core.domain.TenantEntityListener; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.Filter; + +import java.util.UUID; + +@Getter +@Setter +@Entity +@EntityListeners(TenantEntityListener.class) +@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId") +@Table(name = "sec_user_ui_preferences", uniqueConstraints = { + @UniqueConstraint(name = "sec_uk_user_ui_preferences", columnNames = {"tenant_id", "user_id", "preference_key"}) +}) +public class UserUiPreference extends BaseEntity { + + @Id + @GeneratedValue + private UUID id; + + @Column(name = "user_id", nullable = false, updatable = false) + private UUID userId; + + @Column(name = "preference_key", nullable = false, length = 255) + private String preferenceKey; + + @Column(name = "value_json", nullable = false, columnDefinition = "text") + private String valueJson; +} + diff --git a/src/main/java/id/iptek/utms/preference/dto/TablePreferenceProfile.java b/src/main/java/id/iptek/utms/preference/dto/TablePreferenceProfile.java new file mode 100644 index 0000000..1d205b5 --- /dev/null +++ b/src/main/java/id/iptek/utms/preference/dto/TablePreferenceProfile.java @@ -0,0 +1,10 @@ +package id.iptek.utms.preference.dto; + +import java.util.List; + +public record TablePreferenceProfile( + String preferenceKey, + List visibleColumns +) { +} + diff --git a/src/main/java/id/iptek/utms/preference/dto/TablePreferenceRequest.java b/src/main/java/id/iptek/utms/preference/dto/TablePreferenceRequest.java new file mode 100644 index 0000000..0f9b7fe --- /dev/null +++ b/src/main/java/id/iptek/utms/preference/dto/TablePreferenceRequest.java @@ -0,0 +1,13 @@ +package id.iptek.utms.preference.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; + +import java.util.List; + +public record TablePreferenceRequest( + @NotBlank String preferenceKey, + @NotEmpty List<@NotBlank String> visibleColumns +) { +} + diff --git a/src/main/java/id/iptek/utms/preference/dto/TablePreferenceSavedProfile.java b/src/main/java/id/iptek/utms/preference/dto/TablePreferenceSavedProfile.java new file mode 100644 index 0000000..09207b1 --- /dev/null +++ b/src/main/java/id/iptek/utms/preference/dto/TablePreferenceSavedProfile.java @@ -0,0 +1,12 @@ +package id.iptek.utms.preference.dto; + +import java.time.Instant; +import java.util.List; + +public record TablePreferenceSavedProfile( + String preferenceKey, + List visibleColumns, + Instant updatedAt +) { +} + diff --git a/src/main/java/id/iptek/utms/preference/dto/UserUiPreferencesResponse.java b/src/main/java/id/iptek/utms/preference/dto/UserUiPreferencesResponse.java new file mode 100644 index 0000000..022d988 --- /dev/null +++ b/src/main/java/id/iptek/utms/preference/dto/UserUiPreferencesResponse.java @@ -0,0 +1,11 @@ +package id.iptek.utms.preference.dto; + +import java.time.Instant; +import java.util.List; + +public record UserUiPreferencesResponse( + List columns, + Instant updatedAt +) { +} + diff --git a/src/main/java/id/iptek/utms/preference/repository/UserUiPreferenceRepository.java b/src/main/java/id/iptek/utms/preference/repository/UserUiPreferenceRepository.java new file mode 100644 index 0000000..4859f3f --- /dev/null +++ b/src/main/java/id/iptek/utms/preference/repository/UserUiPreferenceRepository.java @@ -0,0 +1,19 @@ +package id.iptek.utms.preference.repository; + +import id.iptek.utms.preference.domain.UserUiPreference; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface UserUiPreferenceRepository extends JpaRepository { + List findByUserId(UUID userId); + + Optional findByUserIdAndPreferenceKey(UUID userId, String preferenceKey); + + void deleteByUserIdAndPreferenceKey(UUID userId, String preferenceKey); + + void deleteByUserId(UUID userId); +} + diff --git a/src/main/java/id/iptek/utms/preference/service/UserPreferenceService.java b/src/main/java/id/iptek/utms/preference/service/UserPreferenceService.java new file mode 100644 index 0000000..5f46705 --- /dev/null +++ b/src/main/java/id/iptek/utms/preference/service/UserPreferenceService.java @@ -0,0 +1,187 @@ +package id.iptek.utms.preference.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import id.iptek.utms.auth.security.UserPrincipal; +import id.iptek.utms.core.exception.AppException; +import id.iptek.utms.core.i18n.MessageResolver; +import id.iptek.utms.preference.domain.UserUiPreference; +import id.iptek.utms.preference.dto.TablePreferenceProfile; +import id.iptek.utms.preference.dto.TablePreferenceRequest; +import id.iptek.utms.preference.dto.TablePreferenceSavedProfile; +import id.iptek.utms.preference.dto.UserUiPreferencesResponse; +import id.iptek.utms.preference.repository.UserUiPreferenceRepository; +import id.iptek.utms.tenant.TenantContext; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.regex.Pattern; + +@Service +public class UserPreferenceService { + + private static final String VALUE_JSON_FIELD = "visibleColumns"; + private static final Pattern PREFERENCE_KEY_PATTERN = + Pattern.compile("^(users|roles|workflow|audit|modules):[A-Za-z0-9_./-]+$"); + private static final Map> DEFAULT_COLUMNS_BY_KEY = Map.of( + "users:workflow-requests", List.of("id", "resourceType", "resourceId", "makerUsername", "status", "requiredSteps", "currentStep", "createdAt", "updatedAt", "actions"), + "workflow:requests", List.of("id", "resourceType", "resourceId", "makerUsername", "status", "updatedAt") + ); + + private final UserUiPreferenceRepository repository; + private final ObjectMapper objectMapper; + private final MessageResolver messageResolver; + + public UserPreferenceService(UserUiPreferenceRepository repository, + ObjectMapper objectMapper, + MessageResolver messageResolver) { + this.repository = repository; + this.objectMapper = objectMapper; + this.messageResolver = messageResolver; + } + + public UserUiPreferencesResponse getAll(Authentication authentication) { + UUID userId = getUserId(authentication); + TenantContext.getRequiredTenantId(); + + List preferences = repository.findByUserId(userId); + preferences.sort(Comparator.comparing(UserUiPreference::getUpdatedAt, Comparator.nullsLast(Comparator.reverseOrder()))); + + List columns = preferences.stream() + .map(this::toProfile) + .toList(); + Instant latestUpdatedAt = preferences.stream() + .map(UserUiPreference::getUpdatedAt) + .filter(Objects::nonNull) + .max(Instant::compareTo) + .orElse(null); + + return new UserUiPreferencesResponse(columns, latestUpdatedAt); + } + + @Transactional + public TablePreferenceSavedProfile upsert(Authentication authentication, TablePreferenceRequest request) { + UUID userId = getUserId(authentication); + String tenantId = TenantContext.getRequiredTenantId(); + String normalizedKey = normalizePreferenceKey(request.preferenceKey()); + List normalizedColumns = normalizeVisibleColumns(request.visibleColumns()); + + UserUiPreference preference = repository.findByUserIdAndPreferenceKey(userId, normalizedKey) + .orElseGet(() -> { + UserUiPreference created = new UserUiPreference(); + created.setUserId(userId); + created.setTenantId(tenantId); + created.setPreferenceKey(normalizedKey); + return created; + }); + preference.setValueJson(serializePreferenceValue(normalizedColumns)); + UserUiPreference saved = repository.save(preference); + return new TablePreferenceSavedProfile( + saved.getPreferenceKey(), + normalizedColumns, + saved.getUpdatedAt() + ); + } + + @Transactional + public TablePreferenceProfile resetTablePreference(Authentication authentication, String preferenceKey) { + UUID userId = getUserId(authentication); + String normalizedKey = normalizePreferenceKey(preferenceKey); + + repository.deleteByUserIdAndPreferenceKey(userId, normalizedKey); + return new TablePreferenceProfile(normalizedKey, getDefaultColumns(normalizedKey)); + } + + @Transactional + public void resetAll(Authentication authentication) { + UUID userId = getUserId(authentication); + repository.deleteByUserId(userId); + } + + private TablePreferenceProfile toProfile(UserUiPreference preference) { + return new TablePreferenceProfile( + preference.getPreferenceKey(), + parseVisibleColumns(preference.getValueJson()) + ); + } + + private UUID getUserId(Authentication authentication) { + Object principal = authentication != null ? authentication.getPrincipal() : null; + if (principal instanceof UserPrincipal userPrincipal) { + return userPrincipal.getId(); + } + throw new AppException(messageResolver.get("auth.invalid.credentials")); + } + + private String normalizePreferenceKey(String preferenceKey) { + if (preferenceKey == null || preferenceKey.isBlank()) { + throw new AppException(messageResolver.get("user.preferences.invalid.key")); + } + String normalized = preferenceKey.trim(); + if (!PREFERENCE_KEY_PATTERN.matcher(normalized).matches()) { + throw new AppException(messageResolver.get("user.preferences.invalid.key")); + } + return normalized; + } + + private List normalizeVisibleColumns(List visibleColumns) { + if (visibleColumns == null || visibleColumns.isEmpty()) { + throw new AppException(messageResolver.get("user.preferences.invalid.columns")); + } + List normalized = visibleColumns.stream() + .map(column -> column == null ? null : column.trim()) + .filter(Objects::nonNull) + .toList(); + if (normalized.isEmpty() || normalized.stream().anyMatch(String::isBlank)) { + throw new AppException(messageResolver.get("user.preferences.invalid.columns")); + } + return List.copyOf(normalized); + } + + private String serializePreferenceValue(List visibleColumns) { + try { + Map> value = new LinkedHashMap<>(); + value.put(VALUE_JSON_FIELD, visibleColumns); + return objectMapper.writeValueAsString(value); + } catch (Exception ex) { + throw new AppException(messageResolver.get("user.preferences.serialize.failed")); + } + } + + private List parseVisibleColumns(String valueJson) { + try { + JsonNode root = objectMapper.readTree(valueJson); + JsonNode columns = root.get(VALUE_JSON_FIELD); + if (columns == null || !columns.isArray()) { + throw new AppException(messageResolver.get("user.preferences.invalid.value")); + } + List visibleColumns = new ArrayList<>(); + for (JsonNode value : columns) { + String column = value != null ? value.asText() : null; + if (column == null || column.isBlank()) { + throw new AppException(messageResolver.get("user.preferences.invalid.value")); + } + visibleColumns.add(column); + } + return visibleColumns; + } catch (AppException ex) { + throw ex; + } catch (Exception ex) { + throw new AppException(messageResolver.get("user.preferences.invalid.value")); + } + } + + private List getDefaultColumns(String preferenceKey) { + return DEFAULT_COLUMNS_BY_KEY.getOrDefault(preferenceKey, List.of("id", "createdAt", "updatedAt", "actions")); + } +} + diff --git a/src/main/java/id/iptek/utms/tenant/Tenant.java b/src/main/java/id/iptek/utms/tenant/Tenant.java new file mode 100644 index 0000000..c55d5fb --- /dev/null +++ b/src/main/java/id/iptek/utms/tenant/Tenant.java @@ -0,0 +1,33 @@ +package id.iptek.utms.tenant; + +import id.iptek.utms.core.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.time.Instant; +import java.util.UUID; + +@Getter +@Setter +@Entity +@Table(name = "sys_tenants") +public class Tenant { + + @Id + @GeneratedValue + private UUID id; + + @Column(name = "tenant_id", nullable = false, unique = true) + private String tenantId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private boolean active = true; + + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt = Instant.now(); +} + diff --git a/src/main/java/id/iptek/utms/tenant/TenantContext.java b/src/main/java/id/iptek/utms/tenant/TenantContext.java new file mode 100644 index 0000000..327bd1f --- /dev/null +++ b/src/main/java/id/iptek/utms/tenant/TenantContext.java @@ -0,0 +1,30 @@ +package id.iptek.utms.tenant; + +public final class TenantContext { + + private static final ThreadLocal TENANT = new ThreadLocal<>(); + + private TenantContext() { + } + + public static void setTenantId(String tenantId) { + TENANT.set(tenantId); + } + + public static String getTenantId() { + return TENANT.get(); + } + + public static String getRequiredTenantId() { + String tenantId = TENANT.get(); + if (tenantId == null || tenantId.isBlank()) { + throw new IllegalStateException("Tenant context is not set"); + } + return tenantId; + } + + public static void clear() { + TENANT.remove(); + } +} + diff --git a/src/main/java/id/iptek/utms/tenant/TenantFilter.java b/src/main/java/id/iptek/utms/tenant/TenantFilter.java new file mode 100644 index 0000000..43062ee --- /dev/null +++ b/src/main/java/id/iptek/utms/tenant/TenantFilter.java @@ -0,0 +1,35 @@ +package id.iptek.utms.tenant; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class TenantFilter extends OncePerRequestFilter { + + public static final String TENANT_HEADER = "X-Tenant-Id"; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + String tenantId = request.getHeader(TENANT_HEADER); + if (tenantId != null && !tenantId.isBlank()) { + TenantContext.setTenantId(tenantId); + } + filterChain.doFilter(request, response); + } finally { + TenantContext.clear(); + } + } +} + diff --git a/src/main/java/id/iptek/utms/tenant/TenantHibernateFilter.java b/src/main/java/id/iptek/utms/tenant/TenantHibernateFilter.java new file mode 100644 index 0000000..0896e42 --- /dev/null +++ b/src/main/java/id/iptek/utms/tenant/TenantHibernateFilter.java @@ -0,0 +1,44 @@ +package id.iptek.utms.tenant; + +import jakarta.persistence.EntityManager; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.hibernate.Session; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE + 1) +public class TenantHibernateFilter extends OncePerRequestFilter { + + private final EntityManager entityManager; + + public TenantHibernateFilter(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String tenantId = TenantContext.getTenantId(); + Session session = entityManager.unwrap(Session.class); + if (tenantId != null && !tenantId.isBlank()) { + session.enableFilter("tenantFilter").setParameter("tenantId", tenantId); + } + try { + filterChain.doFilter(request, response); + } finally { + if (session.getEnabledFilter("tenantFilter") != null) { + session.disableFilter("tenantFilter"); + } + } + } +} + diff --git a/src/main/java/id/iptek/utms/tenant/TenantRepository.java b/src/main/java/id/iptek/utms/tenant/TenantRepository.java new file mode 100644 index 0000000..514ba0d --- /dev/null +++ b/src/main/java/id/iptek/utms/tenant/TenantRepository.java @@ -0,0 +1,11 @@ +package id.iptek.utms.tenant; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface TenantRepository extends JpaRepository { + Optional findByTenantIdAndActiveTrue(String tenantId); +} + diff --git a/src/main/java/id/iptek/utms/tenant/TenantService.java b/src/main/java/id/iptek/utms/tenant/TenantService.java new file mode 100644 index 0000000..3e20ca2 --- /dev/null +++ b/src/main/java/id/iptek/utms/tenant/TenantService.java @@ -0,0 +1,22 @@ +package id.iptek.utms.tenant; + +import id.iptek.utms.core.exception.AppException; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +@Service +public class TenantService { + + private final TenantRepository tenantRepository; + + public TenantService(TenantRepository tenantRepository) { + this.tenantRepository = tenantRepository; + } + + @Cacheable(cacheNames = "tenant:active", key = "#tenantId") + public Tenant getActiveTenant(String tenantId) { + return tenantRepository.findByTenantIdAndActiveTrue(tenantId) + .orElseThrow(() -> new AppException("Tenant is invalid or inactive")); + } +} + diff --git a/src/main/java/id/iptek/utms/workflow/controller/ApprovalWorkflowController.java b/src/main/java/id/iptek/utms/workflow/controller/ApprovalWorkflowController.java new file mode 100644 index 0000000..2f41231 --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/controller/ApprovalWorkflowController.java @@ -0,0 +1,71 @@ +package id.iptek.utms.workflow.controller; + +import id.iptek.utms.api.ApiResponse; +import id.iptek.utms.core.i18n.MessageResolver; +import id.iptek.utms.workflow.dto.ApprovalActionRequest; +import id.iptek.utms.workflow.dto.ApprovalResponse; +import id.iptek.utms.workflow.dto.CreateApprovalRequest; +import id.iptek.utms.workflow.dto.ApprovalRequestSummary; +import id.iptek.utms.workflow.service.ApprovalWorkflowService; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/workflow") +@SecurityRequirement(name = "bearerAuth") +public class ApprovalWorkflowController { + + private final ApprovalWorkflowService workflowService; + private final MessageResolver messageResolver; + + public ApprovalWorkflowController(ApprovalWorkflowService workflowService, MessageResolver messageResolver) { + this.workflowService = workflowService; + this.messageResolver = messageResolver; + } + + @PostMapping("/request") + @PreAuthorize("hasAuthority('WORKFLOW_CREATE') or hasRole('MAKER')") + public ApiResponse create(@Valid @RequestBody CreateApprovalRequest request, + HttpServletRequest servletRequest) { + return ApiResponse.ok(messageResolver.get("workflow.request.created"), + workflowService.createRequest(request, servletRequest)); + } + + @PostMapping("/{id}/approve") + @PreAuthorize("hasAuthority('WORKFLOW_APPROVE') or hasRole('CHECKER')") + public ApiResponse approve(@PathVariable UUID id, + @Valid @RequestBody ApprovalActionRequest request, + Authentication authentication, + HttpServletRequest servletRequest) { + return ApiResponse.ok(messageResolver.get("workflow.request.approved"), + workflowService.approve(id, request, authentication, servletRequest)); + } + + @PostMapping("/{id}/reject") + @PreAuthorize("hasAuthority('WORKFLOW_APPROVE') or hasRole('CHECKER')") + public ApiResponse reject(@PathVariable UUID id, + @Valid @RequestBody ApprovalActionRequest request, + Authentication authentication, + HttpServletRequest servletRequest) { + return ApiResponse.ok(messageResolver.get("workflow.request.rejected"), + workflowService.reject(id, request, authentication, servletRequest)); + } + + @GetMapping("/requests") + @PreAuthorize("hasAuthority('WORKFLOW_APPROVE') or hasRole('CHECKER') or hasRole('ADMIN')") + public ApiResponse> listRequests(@RequestParam(required = false) String status, + @RequestParam(required = false) String resourceType, + @RequestParam(required = false) String makerUsername, + @RequestParam(defaultValue = "50") int limit) { + return ApiResponse.ok(messageResolver.get("workflow.request.listed"), + workflowService.listRequests(status, resourceType, makerUsername, limit)); + } +} + diff --git a/src/main/java/id/iptek/utms/workflow/domain/ApprovalAction.java b/src/main/java/id/iptek/utms/workflow/domain/ApprovalAction.java new file mode 100644 index 0000000..fb1aef9 --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/domain/ApprovalAction.java @@ -0,0 +1,9 @@ +package id.iptek.utms.workflow.domain; + +public enum ApprovalAction { + CREATE, + SUBMIT, + APPROVE, + REJECT +} + diff --git a/src/main/java/id/iptek/utms/workflow/domain/ApprovalHistory.java b/src/main/java/id/iptek/utms/workflow/domain/ApprovalHistory.java new file mode 100644 index 0000000..8287298 --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/domain/ApprovalHistory.java @@ -0,0 +1,38 @@ +package id.iptek.utms.workflow.domain; + +import id.iptek.utms.core.domain.BaseEntity; +import id.iptek.utms.core.domain.TenantEntityListener; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.Filter; + +import java.util.UUID; + +@Getter +@Setter +@Entity +@EntityListeners(TenantEntityListener.class) +@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId") +@Table(name = "sys_approval_history") +public class ApprovalHistory extends BaseEntity { + + @Id + @GeneratedValue + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "request_id", nullable = false) + private ApprovalRequest request; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ApprovalAction action; + + @Column(name = "actor_username", nullable = false) + private String actorUsername; + + @Column(columnDefinition = "text") + private String notes; +} + diff --git a/src/main/java/id/iptek/utms/workflow/domain/ApprovalRequest.java b/src/main/java/id/iptek/utms/workflow/domain/ApprovalRequest.java new file mode 100644 index 0000000..3a8fa2f --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/domain/ApprovalRequest.java @@ -0,0 +1,46 @@ +package id.iptek.utms.workflow.domain; + +import id.iptek.utms.core.domain.BaseEntity; +import id.iptek.utms.core.domain.TenantEntityListener; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.Filter; + +import java.util.UUID; + +@Getter +@Setter +@Entity +@EntityListeners(TenantEntityListener.class) +@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId") +@Table(name = "sys_approval_requests") +public class ApprovalRequest extends BaseEntity { + + @Id + @GeneratedValue + private UUID id; + + @Column(name = "resource_type", nullable = false) + private String resourceType; + + @Column(name = "resource_id", nullable = false) + private String resourceId; + + @Column(name = "payload", columnDefinition = "text") + private String payload; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ApprovalStatus status = ApprovalStatus.DRAFT; + + @Column(name = "required_steps", nullable = false) + private Integer requiredSteps = 1; + + @Column(name = "current_step", nullable = false) + private Integer currentStep = 0; + + @Column(name = "maker_username", nullable = false) + private String makerUsername; +} + diff --git a/src/main/java/id/iptek/utms/workflow/domain/ApprovalStatus.java b/src/main/java/id/iptek/utms/workflow/domain/ApprovalStatus.java new file mode 100644 index 0000000..9fc8f30 --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/domain/ApprovalStatus.java @@ -0,0 +1,9 @@ +package id.iptek.utms.workflow.domain; + +public enum ApprovalStatus { + DRAFT, + PENDING, + APPROVED, + REJECTED +} + diff --git a/src/main/java/id/iptek/utms/workflow/domain/ApprovalStep.java b/src/main/java/id/iptek/utms/workflow/domain/ApprovalStep.java new file mode 100644 index 0000000..23eb7db --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/domain/ApprovalStep.java @@ -0,0 +1,38 @@ +package id.iptek.utms.workflow.domain; + +import id.iptek.utms.core.domain.BaseEntity; +import id.iptek.utms.core.domain.TenantEntityListener; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.Filter; + +import java.util.UUID; + +@Getter +@Setter +@Entity +@EntityListeners(TenantEntityListener.class) +@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId") +@Table(name = "sys_approval_steps") +public class ApprovalStep extends BaseEntity { + + @Id + @GeneratedValue + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "request_id", nullable = false) + private ApprovalRequest request; + + @Column(name = "step_order", nullable = false) + private Integer stepOrder; + + @Column(name = "checker_role", nullable = false) + private String checkerRole; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ApprovalStatus status = ApprovalStatus.PENDING; +} + diff --git a/src/main/java/id/iptek/utms/workflow/dto/ApprovalActionRequest.java b/src/main/java/id/iptek/utms/workflow/dto/ApprovalActionRequest.java new file mode 100644 index 0000000..bdf215a --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/dto/ApprovalActionRequest.java @@ -0,0 +1,8 @@ +package id.iptek.utms.workflow.dto; + +public record ApprovalActionRequest( + String notes, + String checkerRole +) { +} + diff --git a/src/main/java/id/iptek/utms/workflow/dto/ApprovalRequestSummary.java b/src/main/java/id/iptek/utms/workflow/dto/ApprovalRequestSummary.java new file mode 100644 index 0000000..7037b11 --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/dto/ApprovalRequestSummary.java @@ -0,0 +1,21 @@ +package id.iptek.utms.workflow.dto; + +import id.iptek.utms.workflow.domain.ApprovalStatus; + +import java.time.Instant; +import java.util.UUID; + +public record ApprovalRequestSummary( + UUID id, + String tenantId, + String resourceType, + String resourceId, + String makerUsername, + String payload, + ApprovalStatus status, + int requiredSteps, + int currentStep, + Instant createdAt, + Instant updatedAt +) { +} diff --git a/src/main/java/id/iptek/utms/workflow/dto/ApprovalResponse.java b/src/main/java/id/iptek/utms/workflow/dto/ApprovalResponse.java new file mode 100644 index 0000000..98dd7c1 --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/dto/ApprovalResponse.java @@ -0,0 +1,16 @@ +package id.iptek.utms.workflow.dto; + +import id.iptek.utms.workflow.domain.ApprovalStatus; + +import java.util.UUID; + +public record ApprovalResponse( + UUID id, + String resourceType, + String resourceId, + ApprovalStatus status, + int requiredSteps, + int currentStep +) { +} + diff --git a/src/main/java/id/iptek/utms/workflow/dto/CreateApprovalRequest.java b/src/main/java/id/iptek/utms/workflow/dto/CreateApprovalRequest.java new file mode 100644 index 0000000..4cb8823 --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/dto/CreateApprovalRequest.java @@ -0,0 +1,13 @@ +package id.iptek.utms.workflow.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public record CreateApprovalRequest( + @NotBlank String resourceType, + @NotBlank String resourceId, + String payload, + @Min(1) int requiredSteps +) { +} + diff --git a/src/main/java/id/iptek/utms/workflow/repository/ApprovalHistoryRepository.java b/src/main/java/id/iptek/utms/workflow/repository/ApprovalHistoryRepository.java new file mode 100644 index 0000000..3365992 --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/repository/ApprovalHistoryRepository.java @@ -0,0 +1,10 @@ +package id.iptek.utms.workflow.repository; + +import id.iptek.utms.workflow.domain.ApprovalHistory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface ApprovalHistoryRepository extends JpaRepository { +} + diff --git a/src/main/java/id/iptek/utms/workflow/repository/ApprovalRequestRepository.java b/src/main/java/id/iptek/utms/workflow/repository/ApprovalRequestRepository.java new file mode 100644 index 0000000..030fcfd --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/repository/ApprovalRequestRepository.java @@ -0,0 +1,34 @@ +package id.iptek.utms.workflow.repository; + +import id.iptek.utms.workflow.domain.ApprovalRequest; +import id.iptek.utms.workflow.domain.ApprovalStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface ApprovalRequestRepository extends JpaRepository { + Optional findByIdAndTenantId(UUID id, String tenantId); + List findByTenantIdAndStatus(String tenantId, ApprovalStatus status, Pageable pageable); + List findByTenantIdOrderByCreatedAtDesc(String tenantId, Pageable pageable); + + @Query(""" + SELECT r + FROM ApprovalRequest r + WHERE r.tenantId = :tenantId + AND (:status IS NULL OR r.status = :status) + AND (:resourceType IS NULL OR LOWER(r.resourceType) = LOWER(:resourceType)) + AND (:makerUsername IS NULL OR LOWER(r.makerUsername) = LOWER(:makerUsername)) + ORDER BY r.createdAt DESC + """) + List findByTenantIdWithFilters(@Param("tenantId") String tenantId, + @Param("status") ApprovalStatus status, + @Param("resourceType") String resourceType, + @Param("makerUsername") String makerUsername, + Pageable pageable); +} + diff --git a/src/main/java/id/iptek/utms/workflow/repository/ApprovalStepRepository.java b/src/main/java/id/iptek/utms/workflow/repository/ApprovalStepRepository.java new file mode 100644 index 0000000..76ca7ff --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/repository/ApprovalStepRepository.java @@ -0,0 +1,12 @@ +package id.iptek.utms.workflow.repository; + +import id.iptek.utms.workflow.domain.ApprovalStep; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface ApprovalStepRepository extends JpaRepository { + Optional findByRequestIdAndTenantIdAndStepOrder(UUID requestId, String tenantId, Integer stepOrder); +} + diff --git a/src/main/java/id/iptek/utms/workflow/service/ApprovalWorkflowService.java b/src/main/java/id/iptek/utms/workflow/service/ApprovalWorkflowService.java new file mode 100644 index 0000000..9d11258 --- /dev/null +++ b/src/main/java/id/iptek/utms/workflow/service/ApprovalWorkflowService.java @@ -0,0 +1,327 @@ +package id.iptek.utms.workflow.service; + +import id.iptek.utms.core.exception.AppException; +import id.iptek.utms.core.audit.service.AuditTrailService; +import id.iptek.utms.core.security.SecurityUtils; +import id.iptek.utms.messaging.ApprovalCompletedEvent; +import id.iptek.utms.messaging.ApprovalEventProducer; +import id.iptek.utms.tenant.TenantContext; +import id.iptek.utms.workflow.domain.*; +import id.iptek.utms.workflow.dto.ApprovalActionRequest; +import id.iptek.utms.workflow.dto.ApprovalResponse; +import id.iptek.utms.workflow.dto.ApprovalRequestSummary; +import id.iptek.utms.workflow.dto.CreateApprovalRequest; +import id.iptek.utms.workflow.repository.ApprovalHistoryRepository; +import id.iptek.utms.workflow.repository.ApprovalRequestRepository; +import id.iptek.utms.workflow.repository.ApprovalStepRepository; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Locale; +import java.util.UUID; + +@Service +public class ApprovalWorkflowService { + + private final ApprovalRequestRepository requestRepository; + private final ApprovalStepRepository stepRepository; + private final ApprovalHistoryRepository historyRepository; + private final ApprovalEventProducer eventProducer; + private final AuditTrailService auditTrailService; + + public ApprovalWorkflowService(ApprovalRequestRepository requestRepository, + ApprovalStepRepository stepRepository, + ApprovalHistoryRepository historyRepository, + ApprovalEventProducer eventProducer, + AuditTrailService auditTrailService) { + this.requestRepository = requestRepository; + this.stepRepository = stepRepository; + this.historyRepository = historyRepository; + this.eventProducer = eventProducer; + this.auditTrailService = auditTrailService; + } + + @Transactional + public ApprovalResponse createRequest(CreateApprovalRequest dto, HttpServletRequest httpServletRequest) { + return createRequest(dto, null, httpServletRequest); + } + + @Transactional + public ApprovalResponse createRequest(CreateApprovalRequest dto, String checkerRole, HttpServletRequest httpServletRequest) { + return createRequest(dto.resourceType(), dto.resourceId(), dto.payload(), dto.requiredSteps(), checkerRole, httpServletRequest); + } + + @Transactional + public ApprovalResponse createRequest(String resourceType, + String resourceId, + String payload, + int requiredSteps, + String checkerRole, + HttpServletRequest httpServletRequest) { + String tenantId = TenantContext.getRequiredTenantId(); + String maker = SecurityUtils.currentUsername(); + if (maker == null) { + throw new AppException("Authenticated maker is required"); + } + + String resolvedCheckerRole = (checkerRole == null || checkerRole.isBlank()) + ? "CHECKER" + : checkerRole; + + ApprovalRequest request = new ApprovalRequest(); + request.setTenantId(tenantId); + request.setResourceType(resourceType); + request.setResourceId(resourceId); + request.setPayload(payload); + request.setRequiredSteps(requiredSteps); + request.setCurrentStep(0); + request.setStatus(ApprovalStatus.PENDING); + request.setMakerUsername(maker); + ApprovalRequest saved = requestRepository.save(request); + + for (int i = 1; i <= requiredSteps; i++) { + ApprovalStep step = new ApprovalStep(); + step.setTenantId(tenantId); + step.setRequest(saved); + step.setStepOrder(i); + step.setCheckerRole(resolvedCheckerRole); + step.setStatus(ApprovalStatus.PENDING); + stepRepository.save(step); + } + + addHistory(saved, ApprovalAction.CREATE, maker, "Request created and submitted"); + auditTrailService.record( + "APPROVAL_REQUEST_CREATE", + "WORKFLOW", + "ApprovalRequest", + saved.getId().toString(), + AuditTrailService.SUCCESS, + "Approval request created", + null, + auditTrailService.toJson(snapshotApprovalRequest(saved)), + null, + httpServletRequest + ); + return toResponse(saved); + } + + @Transactional + public ApprovalResponse approve(UUID id, ApprovalActionRequest dto, Authentication auth, HttpServletRequest httpServletRequest) { + String tenantId = TenantContext.getRequiredTenantId(); + String checker = auth.getName(); + + ApprovalRequest request = requestRepository.findByIdAndTenantId(id, tenantId) + .orElseThrow(() -> new AppException("Approval request not found")); + String beforeState = auditTrailService.toJson(snapshotApprovalRequest(request)); + + if (request.getStatus() != ApprovalStatus.PENDING) { + throw new AppException("Only pending request can be approved"); + } + if (request.getMakerUsername().equals(checker)) { + throw new AppException("Maker cannot approve own request"); + } + + int nextStep = request.getCurrentStep() + 1; + ApprovalStep step = stepRepository + .findByRequestIdAndTenantIdAndStepOrder(request.getId(), tenantId, nextStep) + .orElseThrow(() -> new AppException("Approval step not found")); + String checkerRole = resolveCheckerRole(step.getCheckerRole(), dto == null ? null : dto.checkerRole()); + validateCheckerRole(auth, checkerRole); + step.setStatus(ApprovalStatus.APPROVED); + + request.setCurrentStep(nextStep); + if (nextStep >= request.getRequiredSteps()) { + request.setStatus(ApprovalStatus.APPROVED); + eventProducer.publishCompleted(new ApprovalCompletedEvent( + request.getId(), + tenantId, + request.getResourceType(), + request.getResourceId(), + checker + )); + } + + stepRepository.save(step); + ApprovalRequest saved = requestRepository.save(request); + auditTrailService.record( + "APPROVAL_REQUEST_APPROVE", + "WORKFLOW", + "ApprovalRequest", + request.getId().toString(), + AuditTrailService.SUCCESS, + "Approval request approved", + beforeState, + auditTrailService.toJson(snapshotApprovalRequest(saved)), + null, + httpServletRequest + ); + addHistory(saved, ApprovalAction.APPROVE, checker, dto.notes()); + return toResponse(saved); + } + + @Transactional + public ApprovalResponse reject(UUID id, ApprovalActionRequest dto, Authentication auth, HttpServletRequest httpServletRequest) { + String tenantId = TenantContext.getRequiredTenantId(); + String checker = auth.getName(); + + ApprovalRequest request = requestRepository.findByIdAndTenantId(id, tenantId) + .orElseThrow(() -> new AppException("Approval request not found")); + String beforeState = auditTrailService.toJson(snapshotApprovalRequest(request)); + + if (request.getStatus() != ApprovalStatus.PENDING) { + throw new AppException("Only pending request can be rejected"); + } + if (request.getMakerUsername().equals(checker)) { + throw new AppException("Maker cannot reject own request"); + } + + int nextStep = request.getCurrentStep() + 1; + ApprovalStep step = stepRepository + .findByRequestIdAndTenantIdAndStepOrder(request.getId(), tenantId, nextStep) + .orElseThrow(() -> new AppException("Approval step not found")); + String checkerRole = resolveCheckerRole(step.getCheckerRole(), dto == null ? null : dto.checkerRole()); + validateCheckerRole(auth, checkerRole); + + step.setStatus(ApprovalStatus.REJECTED); + stepRepository.save(step); + + request.setStatus(ApprovalStatus.REJECTED); + ApprovalRequest saved = requestRepository.save(request); + auditTrailService.record( + "APPROVAL_REQUEST_REJECT", + "WORKFLOW", + "ApprovalRequest", + request.getId().toString(), + AuditTrailService.SUCCESS, + "Approval request rejected", + beforeState, + auditTrailService.toJson(snapshotApprovalRequest(saved)), + null, + httpServletRequest + ); + addHistory(saved, ApprovalAction.REJECT, checker, dto.notes()); + return toResponse(saved); + } + + @Transactional(readOnly = true) + public List listRequests(String statusText, String resourceType, String makerUsername, int limit) { + String tenantId = TenantContext.getRequiredTenantId(); + int maxLimit = Math.max(1, Math.min(limit, 200)); + Pageable pageable = PageRequest.of(0, maxLimit, Sort.by(Sort.Direction.DESC, "createdAt")); + + ApprovalStatus status = parseStatus(statusText); + List requests = requestRepository.findByTenantIdWithFilters( + tenantId, + status, + normalizeQueryParam(resourceType), + normalizeQueryParam(makerUsername), + pageable + ); + + return requests.stream().map(this::toSummary).toList(); + } + + private String normalizeQueryParam(String value) { + if (value == null || value.isBlank()) { + return null; + } + return value.trim(); + } + + private ApprovalStatus parseStatus(String statusText) { + if (statusText == null || statusText.isBlank()) { + return null; + } + try { + return ApprovalStatus.valueOf(statusText.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + throw new AppException("Invalid status value: " + statusText); + } + } + + private void validateCheckerRole(Authentication auth, String expectedRole) { + if (auth.getAuthorities().stream().map(GrantedAuthority::getAuthority).anyMatch("WORKFLOW_APPROVE"::equals)) { + return; + } + + if (expectedRole == null || expectedRole.isBlank()) { + return; + } + + String normalized = "ROLE_" + expectedRole.toUpperCase(Locale.ROOT); + boolean hasRole = auth.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .anyMatch(role -> role.equals(normalized)); + if (!hasRole) { + throw new AppException("Checker does not have required role: " + expectedRole); + } + } + + private String resolveCheckerRole(String stepRole, String overrideRole) { + if (overrideRole != null && !overrideRole.isBlank()) { + return overrideRole; + } + return stepRole; + } + + private void addHistory(ApprovalRequest request, ApprovalAction action, String actor, String notes) { + ApprovalHistory history = new ApprovalHistory(); + history.setTenantId(request.getTenantId()); + history.setRequest(request); + history.setAction(action); + history.setActorUsername(actor); + history.setNotes(notes); + historyRepository.save(history); + } + + private ApprovalResponse toResponse(ApprovalRequest request) { + return new ApprovalResponse( + request.getId(), + request.getResourceType(), + request.getResourceId(), + request.getStatus(), + request.getRequiredSteps(), + request.getCurrentStep() + ); + } + + private ApprovalRequestSummary toSummary(ApprovalRequest request) { + return new ApprovalRequestSummary( + request.getId(), + request.getTenantId(), + request.getResourceType(), + request.getResourceId(), + request.getMakerUsername(), + request.getPayload(), + request.getStatus(), + request.getRequiredSteps(), + request.getCurrentStep(), + request.getCreatedAt(), + request.getUpdatedAt() + ); + } + + private Map snapshotApprovalRequest(ApprovalRequest request) { + Map snapshot = new LinkedHashMap<>(); + snapshot.put("id", request.getId() != null ? request.getId().toString() : null); + snapshot.put("tenantId", request.getTenantId()); + snapshot.put("resourceType", request.getResourceType()); + snapshot.put("resourceId", request.getResourceId()); + snapshot.put("status", request.getStatus() != null ? request.getStatus().name() : null); + snapshot.put("requiredSteps", request.getRequiredSteps()); + snapshot.put("currentStep", request.getCurrentStep()); + snapshot.put("makerUsername", request.getMakerUsername()); + snapshot.put("payload", request.getPayload()); + return snapshot; + } +} + diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..301c30c --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,48 @@ +server: + port: 8080 + +spring: + config: + activate: + on-profile: dev + jackson: + time-zone: Asia/Jakarta + datasource: + url: jdbc:postgresql://localhost:5432/utmsng + username: utms + password: utms1234 + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + open-in-view: true + data: + redis: + host: localhost + port: 6379 + timeout: 2s + cache: + type: redis + activemq: + broker-url: tcp://localhost:61616 + user: admin + password: admin + jms: + listener: + acknowledge-mode: auto +app: + security: + login: + max-failed-attempts: 5 + failed-attempt-window-seconds: 900 + lockout-duration-seconds: 300 + single-login: + enabled: false + jwt: + secret: change-me-this-is-a-very-long-dev-jwt-secret-key-256-bits-min + seed: + enabled: true + + diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..b16c4f6 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,48 @@ +server: + port: 9191 + +spring: + config: + activate: + on-profile: local + jackson: + time-zone: Asia/Jakarta + datasource: + url: jdbc:postgresql://localhost:5432/utmsng + username: utms + password: utms1234 + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + open-in-view: true + data: + redis: + host: localhost + port: 6379 + timeout: 2s + cache: + type: redis + activemq: + broker-url: tcp://localhost:61616 + user: admin + password: admin + jms: + listener: + acknowledge-mode: auto +app: + security: + login: + max-failed-attempts: 5 + failed-attempt-window-seconds: 900 + lockout-duration-seconds: 300 + single-login: + enabled: false + jwt: + secret: local-dev-fallback-jwt-secret-key-for-local-dev-environment-256-bits-min + seed: + enabled: true + + diff --git a/src/main/resources/application-prd.yml b/src/main/resources/application-prd.yml new file mode 100644 index 0000000..4b94885 --- /dev/null +++ b/src/main/resources/application-prd.yml @@ -0,0 +1,51 @@ +server: + port: 8080 + +spring: + config: + activate: + on-profile: prd + jackson: + time-zone: Asia/Jakarta + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + format_sql: false + jdbc: + time_zone: UTC + open-in-view: false + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + timeout: ${REDIS_TIMEOUT:2s} + cache: + type: redis + activemq: + broker-url: ${ACTIVEMQ_BROKER_URL} + user: ${ACTIVEMQ_USER} + password: ${ACTIVEMQ_PASSWORD} + jms: + listener: + acknowledge-mode: auto +app: + security: + login: + max-failed-attempts: ${MAX_LOGIN_ATTEMPTS:5} + failed-attempt-window-seconds: ${LOGIN_FAILED_WINDOW_SECONDS:900} + lockout-duration-seconds: ${LOGIN_LOCKOUT_SECONDS:300} + single-login: + enabled: ${SINGLE_LOGIN_ENABLED:false} + jwt: + secret: ${JWT_SECRET} + seed: + enabled: false + + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..c66d215 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,55 @@ +server: + port: 8080 + +spring: + application: + name: utms-ng-be + profiles: + active: dev + jackson: + time-zone: Asia/Jakarta + messages: + basename: i18n/messages + default-locale: en_US + +app: + security: + single-login: + enabled: false + jwt: + access-token-minutes: 15 + refresh-token-days: 7 + seed: + enabled: false + ldap: + enabled: false + url: ldap://localhost:389 + base: dc=example,dc=org + manager-dn: "" + manager-password: "" + user-search-base: ou=people + user-search-filter: (uid={0}) + group-search-base: ou=groups + group-search-filter: (uniqueMember={0}) + +springdoc: + api-docs: + path: /v3/api-docs + swagger-ui: + path: /swagger-ui.html + persist-authorization: true + +management: + endpoints: + web: + exposure: + include: health,info + +logging: + level: + org.springframework.security: INFO + id.iptek.utms: INFO + +spring.mvc: + locale: en_US + diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql new file mode 100644 index 0000000..fa29dd9 --- /dev/null +++ b/src/main/resources/db/schema.sql @@ -0,0 +1,172 @@ +-- Optional reference schema for PostgreSQL (JPA ddl-auto=update is enabled by default) + +create table if not exists sys_tenants ( + id uuid primary key, + tenant_id varchar(100) not null unique, + name varchar(255) not null, + active boolean not null, + created_at timestamp with time zone not null +); + +create table if not exists sec_permissions ( + id uuid primary key, + tenant_id varchar(100) not null, + code varchar(100) not null, + name varchar(255) not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255), + constraint sec_uk_permissions_tenant_code unique (tenant_id, code) +); + +create table if not exists sec_roles ( + id uuid primary key, + tenant_id varchar(100) not null, + code varchar(100) not null, + name varchar(255) not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255), + constraint sec_uk_roles_tenant_code unique (tenant_id, code) +); + +create table if not exists sec_users ( + id uuid primary key, + tenant_id varchar(100) not null, + username varchar(100) not null, + password varchar(255), + auth_source varchar(20) not null default 'LOCAL', + ldap_dn varchar(512), + enabled boolean not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255), + constraint sec_uk_users_tenant_username unique (tenant_id, username) +); + +create table if not exists sec_user_roles ( + user_id uuid not null references sec_users(id), + role_id uuid not null references sec_roles(id), + primary key (user_id, role_id) +); + +create table if not exists sec_role_permissions ( + role_id uuid not null references sec_roles(id), + permission_id uuid not null references sec_permissions(id), + primary key (role_id, permission_id) +); + +create table if not exists sec_user_ui_preferences ( + id uuid primary key, + tenant_id varchar(100) not null, + user_id uuid not null references sec_users(id), + preference_key varchar(255) not null, + value_json text not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255), + constraint sec_uk_user_ui_preferences unique (tenant_id, user_id, preference_key) +); +create index if not exists sec_idx_user_ui_preferences_tenant_user_updated on sec_user_ui_preferences (tenant_id, user_id, updated_at); +create index if not exists sec_idx_user_ui_preferences_tenant_user on sec_user_ui_preferences (tenant_id, user_id); +create index if not exists sec_idx_user_ui_preferences_user on sec_user_ui_preferences (user_id); + +create table if not exists sec_refresh_tokens ( + id uuid primary key, + tenant_id varchar(100) not null, + user_id uuid not null references sec_users(id), + token varchar(512) not null unique, + expires_at timestamp with time zone not null, + revoked boolean not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255) +); + +create table if not exists sys_system_modules ( + id uuid primary key, + tenant_id varchar(100) not null, + code varchar(100) not null, + name varchar(255) not null, + enabled boolean not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255), + constraint sys_uk_system_modules_tenant_code unique (tenant_id, code) +); + +create table if not exists sys_approval_requests ( + id uuid primary key, + tenant_id varchar(100) not null, + resource_type varchar(255) not null, + resource_id varchar(255) not null, + payload text, + status varchar(50) not null, + required_steps integer not null, + current_step integer not null, + maker_username varchar(255) not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255) +); + +create table if not exists sys_approval_steps ( + id uuid primary key, + tenant_id varchar(100) not null, + request_id uuid not null references sys_approval_requests(id), + step_order integer not null, + checker_role varchar(255) not null, + status varchar(50) not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255) +); + +create table if not exists sys_approval_history ( + id uuid primary key, + tenant_id varchar(100) not null, + request_id uuid not null references sys_approval_requests(id), + action varchar(50) not null, + actor_username varchar(255) not null, + notes text, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255) +); + +create table if not exists sys_audit_trails ( + id uuid primary key, + tenant_id varchar(100) not null, + correlation_id varchar(100), + actor varchar(255) not null, + action varchar(100) not null, + domain varchar(100), + resource_type varchar(100), + resource_id varchar(255), + outcome varchar(20) not null, + http_method varchar(20), + request_path varchar(500), + client_ip varchar(80), + error_message varchar(1000), + details text, + before_state text, + after_state text, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255) +); + +create index if not exists sys_idx_audit_tenant_created_on on sys_audit_trails (tenant_id, created_at); +create index if not exists sys_idx_audit_correlation on sys_audit_trails (correlation_id); +create index if not exists sys_idx_audit_actor on sys_audit_trails (actor); +create index if not exists sys_idx_audit_action on sys_audit_trails (action); diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..b75508a --- /dev/null +++ b/src/main/resources/i18n/messages.properties @@ -0,0 +1,33 @@ +auth.login.success=Login successful +auth.refresh.success=Token refreshed successfully +auth.logout.success=Logout successful +user.me.success=Current user fetched successfully +workflow.request.created=Approval request created +workflow.request.approved=Approval request approved +workflow.request.rejected=Approval request rejected +workflow.request.listed=Workflow requests fetched +module.list.success=Modules fetched +module.toggle.success=Module updated +audit.list.success=Audit trail fetched +error.validation=Validation failed +error.forbidden=Access denied +error.internal=Internal server error +user.management.request.created=User management request created +role.management.request.created=Role management request created +auth.invalid.credentials=Invalid username or password +auth.login.locked=Account locked. Please try again in {0} seconds +auth.user.notfound=User not found +auth.user.notfound.for.ldap=LDAP user authenticated but not provisioned in this tenant +auth.refresh.notfound=Refresh token not found +auth.refresh.invalid=Refresh token expired or revoked +auth.single.login.invalid_session=Session is no longer active. Please log in again. +tenant.header.required=X-Tenant-Id header is required +tenant.header.mismatch=X-Tenant-Id header does not match authenticated tenant context +user.preferences.get.success=Preferences retrieved +user.preferences.upsert.success=Table preference saved +user.preferences.reset.table.success=Table preference reset +user.preferences.reset.all.success=All UI preferences reset +user.preferences.invalid.key=Invalid preference key +user.preferences.invalid.columns=Invalid visible columns +user.preferences.serialize.failed=Unable to save preference +user.preferences.invalid.value=Stored preference value is invalid diff --git a/src/main/resources/i18n/messages_id.properties b/src/main/resources/i18n/messages_id.properties new file mode 100644 index 0000000..90338aa --- /dev/null +++ b/src/main/resources/i18n/messages_id.properties @@ -0,0 +1,33 @@ +auth.login.success=Login berhasil +auth.refresh.success=Token berhasil diperbarui +auth.logout.success=Logout berhasil +user.me.success=Data pengguna berhasil diambil +workflow.request.created=Permintaan persetujuan berhasil dibuat +workflow.request.approved=Permintaan persetujuan disetujui +workflow.request.rejected=Permintaan persetujuan ditolak +workflow.request.listed=Permintaan persetujuan diambil +module.list.success=Daftar modul berhasil diambil +module.toggle.success=Status modul berhasil diperbarui +audit.list.success=Riwayat audit berhasil diambil +error.validation=Validasi gagal +error.forbidden=Akses ditolak +error.internal=Terjadi kesalahan internal +user.management.request.created=Permintaan manajemen pengguna telah dibuat +role.management.request.created=Permintaan manajemen peran telah dibuat +auth.invalid.credentials=Username atau password tidak valid +auth.login.locked=Akun terkunci. Silakan coba lagi dalam {0} detik +auth.user.notfound=Pengguna tidak ditemukan +auth.user.notfound.for.ldap=Pengguna LDAP berhasil diautentikasi tetapi belum diprovisioning di tenant ini +auth.refresh.notfound=Token refresh tidak ditemukan +auth.refresh.invalid=Token refresh kedaluwarsa atau dibatalkan +auth.single.login.invalid_session=Session tidak lagi aktif. Silakan masuk kembali. +tenant.header.required=Header X-Tenant-Id wajib diisi +tenant.header.mismatch=Header X-Tenant-Id tidak sesuai dengan tenant yang terautentikasi +user.preferences.get.success=Preferensi berhasil diambil +user.preferences.upsert.success=Preferensi tabel berhasil disimpan +user.preferences.reset.table.success=Preferensi tabel berhasil diatur ulang +user.preferences.reset.all.success=Semua preferensi UI berhasil dihapus +user.preferences.invalid.key=Kunci preferensi tidak valid +user.preferences.invalid.columns=Kolom yang terlihat tidak valid +user.preferences.serialize.failed=Tidak dapat menyimpan preferensi +user.preferences.invalid.value=Nilai preferensi tersimpan tidak valid diff --git a/target/classes/application-dev.yml b/target/classes/application-dev.yml new file mode 100644 index 0000000..301c30c --- /dev/null +++ b/target/classes/application-dev.yml @@ -0,0 +1,48 @@ +server: + port: 8080 + +spring: + config: + activate: + on-profile: dev + jackson: + time-zone: Asia/Jakarta + datasource: + url: jdbc:postgresql://localhost:5432/utmsng + username: utms + password: utms1234 + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + open-in-view: true + data: + redis: + host: localhost + port: 6379 + timeout: 2s + cache: + type: redis + activemq: + broker-url: tcp://localhost:61616 + user: admin + password: admin + jms: + listener: + acknowledge-mode: auto +app: + security: + login: + max-failed-attempts: 5 + failed-attempt-window-seconds: 900 + lockout-duration-seconds: 300 + single-login: + enabled: false + jwt: + secret: change-me-this-is-a-very-long-dev-jwt-secret-key-256-bits-min + seed: + enabled: true + + diff --git a/target/classes/application-local.yml b/target/classes/application-local.yml new file mode 100644 index 0000000..b16c4f6 --- /dev/null +++ b/target/classes/application-local.yml @@ -0,0 +1,48 @@ +server: + port: 9191 + +spring: + config: + activate: + on-profile: local + jackson: + time-zone: Asia/Jakarta + datasource: + url: jdbc:postgresql://localhost:5432/utmsng + username: utms + password: utms1234 + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + open-in-view: true + data: + redis: + host: localhost + port: 6379 + timeout: 2s + cache: + type: redis + activemq: + broker-url: tcp://localhost:61616 + user: admin + password: admin + jms: + listener: + acknowledge-mode: auto +app: + security: + login: + max-failed-attempts: 5 + failed-attempt-window-seconds: 900 + lockout-duration-seconds: 300 + single-login: + enabled: false + jwt: + secret: local-dev-fallback-jwt-secret-key-for-local-dev-environment-256-bits-min + seed: + enabled: true + + diff --git a/target/classes/application-prd.yml b/target/classes/application-prd.yml new file mode 100644 index 0000000..4b94885 --- /dev/null +++ b/target/classes/application-prd.yml @@ -0,0 +1,51 @@ +server: + port: 8080 + +spring: + config: + activate: + on-profile: prd + jackson: + time-zone: Asia/Jakarta + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + format_sql: false + jdbc: + time_zone: UTC + open-in-view: false + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + timeout: ${REDIS_TIMEOUT:2s} + cache: + type: redis + activemq: + broker-url: ${ACTIVEMQ_BROKER_URL} + user: ${ACTIVEMQ_USER} + password: ${ACTIVEMQ_PASSWORD} + jms: + listener: + acknowledge-mode: auto +app: + security: + login: + max-failed-attempts: ${MAX_LOGIN_ATTEMPTS:5} + failed-attempt-window-seconds: ${LOGIN_FAILED_WINDOW_SECONDS:900} + lockout-duration-seconds: ${LOGIN_LOCKOUT_SECONDS:300} + single-login: + enabled: ${SINGLE_LOGIN_ENABLED:false} + jwt: + secret: ${JWT_SECRET} + seed: + enabled: false + + diff --git a/target/classes/application.yml b/target/classes/application.yml new file mode 100644 index 0000000..c66d215 --- /dev/null +++ b/target/classes/application.yml @@ -0,0 +1,55 @@ +server: + port: 8080 + +spring: + application: + name: utms-ng-be + profiles: + active: dev + jackson: + time-zone: Asia/Jakarta + messages: + basename: i18n/messages + default-locale: en_US + +app: + security: + single-login: + enabled: false + jwt: + access-token-minutes: 15 + refresh-token-days: 7 + seed: + enabled: false + ldap: + enabled: false + url: ldap://localhost:389 + base: dc=example,dc=org + manager-dn: "" + manager-password: "" + user-search-base: ou=people + user-search-filter: (uid={0}) + group-search-base: ou=groups + group-search-filter: (uniqueMember={0}) + +springdoc: + api-docs: + path: /v3/api-docs + swagger-ui: + path: /swagger-ui.html + persist-authorization: true + +management: + endpoints: + web: + exposure: + include: health,info + +logging: + level: + org.springframework.security: INFO + id.iptek.utms: INFO + +spring.mvc: + locale: en_US + diff --git a/target/classes/db/schema.sql b/target/classes/db/schema.sql new file mode 100644 index 0000000..fa29dd9 --- /dev/null +++ b/target/classes/db/schema.sql @@ -0,0 +1,172 @@ +-- Optional reference schema for PostgreSQL (JPA ddl-auto=update is enabled by default) + +create table if not exists sys_tenants ( + id uuid primary key, + tenant_id varchar(100) not null unique, + name varchar(255) not null, + active boolean not null, + created_at timestamp with time zone not null +); + +create table if not exists sec_permissions ( + id uuid primary key, + tenant_id varchar(100) not null, + code varchar(100) not null, + name varchar(255) not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255), + constraint sec_uk_permissions_tenant_code unique (tenant_id, code) +); + +create table if not exists sec_roles ( + id uuid primary key, + tenant_id varchar(100) not null, + code varchar(100) not null, + name varchar(255) not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255), + constraint sec_uk_roles_tenant_code unique (tenant_id, code) +); + +create table if not exists sec_users ( + id uuid primary key, + tenant_id varchar(100) not null, + username varchar(100) not null, + password varchar(255), + auth_source varchar(20) not null default 'LOCAL', + ldap_dn varchar(512), + enabled boolean not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255), + constraint sec_uk_users_tenant_username unique (tenant_id, username) +); + +create table if not exists sec_user_roles ( + user_id uuid not null references sec_users(id), + role_id uuid not null references sec_roles(id), + primary key (user_id, role_id) +); + +create table if not exists sec_role_permissions ( + role_id uuid not null references sec_roles(id), + permission_id uuid not null references sec_permissions(id), + primary key (role_id, permission_id) +); + +create table if not exists sec_user_ui_preferences ( + id uuid primary key, + tenant_id varchar(100) not null, + user_id uuid not null references sec_users(id), + preference_key varchar(255) not null, + value_json text not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255), + constraint sec_uk_user_ui_preferences unique (tenant_id, user_id, preference_key) +); +create index if not exists sec_idx_user_ui_preferences_tenant_user_updated on sec_user_ui_preferences (tenant_id, user_id, updated_at); +create index if not exists sec_idx_user_ui_preferences_tenant_user on sec_user_ui_preferences (tenant_id, user_id); +create index if not exists sec_idx_user_ui_preferences_user on sec_user_ui_preferences (user_id); + +create table if not exists sec_refresh_tokens ( + id uuid primary key, + tenant_id varchar(100) not null, + user_id uuid not null references sec_users(id), + token varchar(512) not null unique, + expires_at timestamp with time zone not null, + revoked boolean not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255) +); + +create table if not exists sys_system_modules ( + id uuid primary key, + tenant_id varchar(100) not null, + code varchar(100) not null, + name varchar(255) not null, + enabled boolean not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255), + constraint sys_uk_system_modules_tenant_code unique (tenant_id, code) +); + +create table if not exists sys_approval_requests ( + id uuid primary key, + tenant_id varchar(100) not null, + resource_type varchar(255) not null, + resource_id varchar(255) not null, + payload text, + status varchar(50) not null, + required_steps integer not null, + current_step integer not null, + maker_username varchar(255) not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255) +); + +create table if not exists sys_approval_steps ( + id uuid primary key, + tenant_id varchar(100) not null, + request_id uuid not null references sys_approval_requests(id), + step_order integer not null, + checker_role varchar(255) not null, + status varchar(50) not null, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255) +); + +create table if not exists sys_approval_history ( + id uuid primary key, + tenant_id varchar(100) not null, + request_id uuid not null references sys_approval_requests(id), + action varchar(50) not null, + actor_username varchar(255) not null, + notes text, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255) +); + +create table if not exists sys_audit_trails ( + id uuid primary key, + tenant_id varchar(100) not null, + correlation_id varchar(100), + actor varchar(255) not null, + action varchar(100) not null, + domain varchar(100), + resource_type varchar(100), + resource_id varchar(255), + outcome varchar(20) not null, + http_method varchar(20), + request_path varchar(500), + client_ip varchar(80), + error_message varchar(1000), + details text, + before_state text, + after_state text, + created_at timestamp with time zone, + updated_at timestamp with time zone, + created_by varchar(255), + updated_by varchar(255) +); + +create index if not exists sys_idx_audit_tenant_created_on on sys_audit_trails (tenant_id, created_at); +create index if not exists sys_idx_audit_correlation on sys_audit_trails (correlation_id); +create index if not exists sys_idx_audit_actor on sys_audit_trails (actor); +create index if not exists sys_idx_audit_action on sys_audit_trails (action); diff --git a/target/classes/i18n/messages.properties b/target/classes/i18n/messages.properties new file mode 100644 index 0000000..b75508a --- /dev/null +++ b/target/classes/i18n/messages.properties @@ -0,0 +1,33 @@ +auth.login.success=Login successful +auth.refresh.success=Token refreshed successfully +auth.logout.success=Logout successful +user.me.success=Current user fetched successfully +workflow.request.created=Approval request created +workflow.request.approved=Approval request approved +workflow.request.rejected=Approval request rejected +workflow.request.listed=Workflow requests fetched +module.list.success=Modules fetched +module.toggle.success=Module updated +audit.list.success=Audit trail fetched +error.validation=Validation failed +error.forbidden=Access denied +error.internal=Internal server error +user.management.request.created=User management request created +role.management.request.created=Role management request created +auth.invalid.credentials=Invalid username or password +auth.login.locked=Account locked. Please try again in {0} seconds +auth.user.notfound=User not found +auth.user.notfound.for.ldap=LDAP user authenticated but not provisioned in this tenant +auth.refresh.notfound=Refresh token not found +auth.refresh.invalid=Refresh token expired or revoked +auth.single.login.invalid_session=Session is no longer active. Please log in again. +tenant.header.required=X-Tenant-Id header is required +tenant.header.mismatch=X-Tenant-Id header does not match authenticated tenant context +user.preferences.get.success=Preferences retrieved +user.preferences.upsert.success=Table preference saved +user.preferences.reset.table.success=Table preference reset +user.preferences.reset.all.success=All UI preferences reset +user.preferences.invalid.key=Invalid preference key +user.preferences.invalid.columns=Invalid visible columns +user.preferences.serialize.failed=Unable to save preference +user.preferences.invalid.value=Stored preference value is invalid diff --git a/target/classes/i18n/messages_id.properties b/target/classes/i18n/messages_id.properties new file mode 100644 index 0000000..90338aa --- /dev/null +++ b/target/classes/i18n/messages_id.properties @@ -0,0 +1,33 @@ +auth.login.success=Login berhasil +auth.refresh.success=Token berhasil diperbarui +auth.logout.success=Logout berhasil +user.me.success=Data pengguna berhasil diambil +workflow.request.created=Permintaan persetujuan berhasil dibuat +workflow.request.approved=Permintaan persetujuan disetujui +workflow.request.rejected=Permintaan persetujuan ditolak +workflow.request.listed=Permintaan persetujuan diambil +module.list.success=Daftar modul berhasil diambil +module.toggle.success=Status modul berhasil diperbarui +audit.list.success=Riwayat audit berhasil diambil +error.validation=Validasi gagal +error.forbidden=Akses ditolak +error.internal=Terjadi kesalahan internal +user.management.request.created=Permintaan manajemen pengguna telah dibuat +role.management.request.created=Permintaan manajemen peran telah dibuat +auth.invalid.credentials=Username atau password tidak valid +auth.login.locked=Akun terkunci. Silakan coba lagi dalam {0} detik +auth.user.notfound=Pengguna tidak ditemukan +auth.user.notfound.for.ldap=Pengguna LDAP berhasil diautentikasi tetapi belum diprovisioning di tenant ini +auth.refresh.notfound=Token refresh tidak ditemukan +auth.refresh.invalid=Token refresh kedaluwarsa atau dibatalkan +auth.single.login.invalid_session=Session tidak lagi aktif. Silakan masuk kembali. +tenant.header.required=Header X-Tenant-Id wajib diisi +tenant.header.mismatch=Header X-Tenant-Id tidak sesuai dengan tenant yang terautentikasi +user.preferences.get.success=Preferensi berhasil diambil +user.preferences.upsert.success=Preferensi tabel berhasil disimpan +user.preferences.reset.table.success=Preferensi tabel berhasil diatur ulang +user.preferences.reset.all.success=Semua preferensi UI berhasil dihapus +user.preferences.invalid.key=Kunci preferensi tidak valid +user.preferences.invalid.columns=Kolom yang terlihat tidak valid +user.preferences.serialize.failed=Tidak dapat menyimpan preferensi +user.preferences.invalid.value=Nilai preferensi tersimpan tidak valid diff --git a/target/classes/id/iptek/utms/UtmsNgBeApplication.class b/target/classes/id/iptek/utms/UtmsNgBeApplication.class new file mode 100644 index 0000000000000000000000000000000000000000..c801d28e08bdbdf620bbf39c3b53d271cd2e2a19 GIT binary patch literal 1196 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 literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/api/RoleController.class b/target/classes/id/iptek/utms/api/RoleController.class new file mode 100644 index 0000000000000000000000000000000000000000..3b55396ad586e4a28524c4c01ee29629b4fdd887 GIT binary patch literal 3292 zcmc&$>rNX-6#m8#99#nlv`|da5R%k3w`mecA*QqrA(z$$gacBQAJN#Jm^irnUU0Dt$9u&{7y# zjNpBI5XN8>L->#(e$?W&!ilbuB=+VLnFggfqm@%iJwLRx&vGhxXsLE8f*3}^xDv%x ze8ezwRIxY}RSZLZHD6I?DF9>Lu)YtE;fF*MQGv*!ae3o#!)LW?DYA20Xd?nQm8IcZ z#(4Y*y{uPh;oxAi8+Kj2{j%)ta;qWjdD`6(W%8QmI=)OrFpBG8j74z+HyN%rfu*=g zd+OjcL&)7`SZ+b>AsXw<9xID9$MA6!aU>YV_Q~4tisX4q)|%86<@<_ktn>C`Ti%Yl zcV5IXjOKy=8wlAX`xl*m#LdN7uobm`tfo+ zIqK?B+O~Be5#PuyufGVk(+{&xSK>)3T1O#2+w+dXj@4aHI24r_VuNqmAt@d^dbyqs z|C6{{wR+$d_el8VYEaP*RWpX~+XmG6l(DyyYqlw<>i?f(4Pf~B-CBP3T!^=f*L|N% zd5vETLnP-`bwNH-wzLk|NNcoG)3%Rbm}?o{t&)d+m!@%@!r&}dgC3}ndXb>F(_|3k zFEQY9e^-=BQuCd=+!fUN)sxnz)}WSax!S-+qtZd|M`m<7l=6f%FNK!cW*e?kM;D@o zG=zTJSkkE9ws-XH0OdWBv@=e#uWh$V-@ftR!NwE))eVT@4(^f`YwQeW8wT8=r5Sqe z1tLGi9DRpqd!7#8e-7p&g#|nIseNCh&2F$k`g~5$7xeUm=&+rn>9IH-VChsiZneNM xML4Dj2PYg;ZE-w28OL%99FNX|BZS9<-Xq@%2@*n-5U*CTKc!=c!y3n0PridQ&G7P;FZNZzuYw#EK zchWHoz01l|W`&`E)t4IADUrVm$7d^gy)U=Yr0K<3^bK0Sx@`IVyNgy|eA%>iZ0iU@A z3}YmV(IQS`>{x1v)uHSt5W`4Anhp6rQd+tR$TOE=v{bGh@UUsL@*0&soWa>F&J{6% zNrwK|ATp}S*G0h4<-cLLTRP;I@K-smNrUhCRth+e3t3DTaS<~NQ%AY6oUR6n$P0Wg zlon~5;mKj7G{a6ve~ndPxL*3_R+MWDna93MDGXMXC!a^Hy3|{u-XzU%)pta*CbY70 zs?3cpffCWj1)>I(#m|~!g1)m@IcGJ4-J2* zVWe9sGZZ%cNIP;(*`}RNM*iTf-n2u_u-Fl`Q~orShn_lN z{*UYXh`bNV?~B(*(s736s`7cbBN`2N9D&Z~!qu z5R(*YB8Dg?9x252*!Bi)(m76Eoc`W=4HjY^x9vT|0;%uBW_M$=2?D)G^8J0&MLeMU HQi#fL)6cJA literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/api/UserController.class b/target/classes/id/iptek/utms/api/UserController.class new file mode 100644 index 0000000000000000000000000000000000000000..503708309a39f13d04cab43d92879ba337bf4c58 GIT binary patch literal 8285 zcmd5>S$iDC6+NZJBTFM0%Nv##$-+UKrS^_(IS#TVYzIkJBul||0%^^ZrM5@YLwAp4 z;Xoi8Ss?oY&O!)TNC*THyZ{06azS{OgghxLJG7GhI(YZ zX@d>aTirF+vR6&i5XTzaBd}QwF>0sekeN284Xf1J`@@{6ErD7T-H+$zA=tXxL07!jC5Us9+j@0npFnr2o0OC9cfs54yBEu z(x$z2Xu_5lw#Kmy+XZ&p&XnP194kF_(J^P_ob6mDl0??Aym>!<38*ZxPN)R5vWijKG zalLWnNa<$mv@3gSa6j#%jNdaOd)#ayLH@O9M@I~uadcsqKwBxK1#LxjlB&Sr2v8S_ zaQdR|jkHA%j2L#uu?Kqvwv|q-l--xH^4Mtt`?A15#B5QMEjkvxP=oz(9Kb<=b&h;I zYdP|)Oq*%%lr)n{G8-c1>6oa&A%U&heg$c|J+wo?m`BhX!_hcbIv4Q4QXGX&i|d&o zfMfQhQUa;=!Ig@Twq9^rr3@vBajUS1+`Nscj|T;s%lKt8>`_ATcpN9tPniBIfmdz= z=He*Nnv*G6069u8N!MG7Td3EVIV+PRj(yQeDfK@ju(OPvBVGE>Q_~fD@l+hA@kuUY z{XR6Vz}4F!Tow+N@Kk8lJds))!C)Li7#66}@U&D8+y*42=@Eg}1*~GGQhA%yU^K6d znII|jsKrCK^duZiuQ ze1Yd8vzZQeLE}Qo%4>!J;Cn6l?|K<&$9 z9DCMG1*=7oJs-yfe1<{6hdiE1(hM3hlP4FJwvNEF3*wIM(fg4>eiVYe4LLV$@E$3MV%4km4wWwjK>(A8OT4>;G)1rPe;<8X&p+nLKt2t zjRdAu&2z98mvA|TR2(xZ$+ngb@Ao$RaIDH2d4*Y~j`7g{=`=^Ok%{5)I2^bFYxSzi zdR8h|K=HR#G`yMOOd~v=8*)rramOEp5iD`kqMY^w8$x28psCq*&ZF!0h z_gZe62=`ksSNC>I2vnW0lPn_E3|eV9oSm7Jjt(yZje~Z=Oih@MrD9<2$0~2yqWxOK zgv#p8Udm`ldee4t#8fqsCmr@1yPa4RF;`Iqz=;}^J&UWYy(>?Se#q4Ikif3+l#5kQ z0;|WYskG^39oq4kJJ0awO3)mmJvNVKS$fXM3Cp$Uy1sPU_H-Re5A2UL&77PxCarYR zFoU+nN$CxlnT#q=1*#e0vZPmOsEYGQZ0Z8ipk6b!>lw_}eKer$SWid})je&x zxo2;_y={DKVDz!kfxiB>juzW#;Y$_%+uQp3hfWW-bM>cZtXzh;&Dk$a1OBudj)??vZXko_8y=vx?D0c(RgvWT*|IE;&Q> zkHED%;)&2QE0|8_`Ou^b)duAOggQeuD=E8`K9?ViMzTf0 zsIgeqhNsdK_lc##JUgE$4SY>O?Zi-Ku^v-Z%7nTlkOM0f>~T9e&wg`v#xqN90LoQW zUy$d+a$ew#yMtt90?6-Fl<$`xBl;z4aUMD4b5U!#`;Dt5=6I4@>}7j&#Pp`~RSt1+ z{9bH zf2)GHu$xlnZp+gr!kI(_4(f?%BvC4A;oJ{%}O%9_kzJ zJ2?<4n*3tw%;3OdLHSgd)tgUMRIvn@U(lX+ddFF=A7df?EHJget9J|S${oKW(t*D1 zyB**7^$CC9>Q^`d^<$oyxJ(h~JSk8+W@nv*e2Aw~j6>?Upm2WQqjH46p=F0Po=13v z?v&@_R$DZEy<*;bXZIaaAgDtFOKm z5Jpv6AA{FUO4E^!+6Z6=&s{56;8mefea+xqqx#;Uulz8euRO!&bv28?YRt0r9G{-% zwVKza&YKXOUDpxo+;tuGo!!^5wsUjUb!>QxZ!2&`wZSU>-_1GpaFo4hz&?I-*pJOT z?w#kG{r*UJ0$2H@+AAbN-zh8#Uz7Sw0ac4Osc#A#>+`%{Tg9dzVDL5gLRG5w;Yq$$ zASc;Db!q8*8?ml;uvy>+nuqwbq48st@1x=d+IHPS_nU0vLsegbz_Yvt^lE4=e*XYk z3!cIkI7b7`^faDfYXzg48G@HD>S4ZwFY~&IufD>DU%dd~;#qu6-MjcYFWwZ^G^_`5u=ucc*9{ylLDxuL;4yDC`Z>V>P0?S3a(jSsJD$VyoC(;j@%kuN zSMX;PA^lba(r@RHeh1%W<8>7r=6mEq?BB-^&f|ytQQ$}TF@35ixP+uYH5Vv+qL@PW z4fI8D7$%1^bmIuN@Tx@6=?@cdXbw0u=QwZ@ynq*JGiCGo%7D!aeDrO9EnxH$qV#S5 z(*mQPu_0x=kv{%8Zxz99l@SCh^y`=STJXgA7ddZ5P#p+SJsCmu9J`+<)eEG0eg#xt zrVGoczD!I$)n5iwUkRxGsz6n7+Z@GB;MbbhZvqv{oC*UMD&ak>)p#byPn7U#U(j2k_qrobboI6W&B#Z)~5}TYUw` zQV&X9N?&i`Ps`$QE`rA_dCZZ=74n!}kjI~w=J9p}kLxSpQK7!vk(mk!|D-UKKPJ2W zj#>t`&%TIz-$UY@c8|=t=O(5;yaj7$7u(QF3%2QEd`@308o(;H|07#>}} iH6P(s?5w~o{r??puPvPXT|U1T`2H_=pZ6cQ`0&5T4Arjy literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/config/JwtProperties.class b/target/classes/id/iptek/utms/auth/config/JwtProperties.class new file mode 100644 index 0000000000000000000000000000000000000000..0d662ec3cd43673e1fef690d82cd71e0160126f5 GIT binary patch literal 1903 zcmb7ETT|Oc6#f=$%ebQ04ko1+lBP*xixidKTLZa3lHzeLlQ2BA(+43hc)@7Z)hZX^PO|Pqi@eY{~rAfU>Of97{j=Qf{h|d4Aoul zh35{uV8?yNeW`j3r6myvy}~fwY;8`Uj0p=B8}GnoxG8$B2sPhxW4#}_Uab4BFN1Bd z<8}`8Qzb*LwBQj%iMX%0W~g_EDV}R3f}OSr`mT*BRO#6BeI7*{a*qd(L=bBOW*y^F zw{gk1R2AP=JnAPtYu>A~5p>^=3Ax6d_K1fOo6)4W71 z>kN&2sW}Qvo)$uOl)yR+mFF^6K7SyR+D|EI!KjB}_E{WgvClU}B#6=7AduSALIzaP z{jO9yZWJ1AZJR<5q}p@0q|`>6n!nU;c%n>qHIZwq67*29ZTe#&2~E6Yu)Hu_*gYWe zmX0q)^&W+bwDQ8VBT*UOFgU5m+h;Z0ep9nk6qcSzdTLW8v@2M`vV|2Jw{eHz%J7n? zl&PEcC`-pMVF)D^FucyUSp+ZW^0=wvk6zFl@aTG1%Dp&jpA}Bdk^whfg?x=izM_jQ zRUWcB@S-Sh$i(;a*&zAsr*7BDpSIV7fU6`Vk1VV*+!%7cfP;nm409K|(vM0BW0B^@ z7~R5g)Xl`AUER!?WTjz3s|7}*?<#qHOkcl{XNlH^^B0&ie+bLDdWcD9rf`VbYdRRi zHF}o_WZs|9+d+Ze4P3_!TFuTRGUtsXl}X<)lZ*U5rTyeMIe?BpMxaJ>>n6FI?2EJ- zpyeE48u){@`FzDBUmDxW^3`ZUHP3;+2mCe?d=9OQ`x33D*s?Q!gb&|_pGkdA=$5Gg z^HhMLR-`LH3nPNZlwA5Ul#0%uur4B0%px-C%q@bLez=tD7(Dqq8Q#{7A^6g{;MyqQ zB0ft}EKpt$1%}V*S&Z-nz9hpmNh6g|Dxg$8U(rdG+piUD H0cQUL+ZL%; literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/config/LdapAuthConfig.class b/target/classes/id/iptek/utms/auth/config/LdapAuthConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..483cdcfafb471c2d5e6c59f9efc1e171a738c2d2 GIT binary patch literal 3098 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 IWrLOf0hj3zasU7T literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/domain/AuthenticationSource.class b/target/classes/id/iptek/utms/auth/domain/AuthenticationSource.class new file mode 100644 index 0000000000000000000000000000000000000000..a35a4ed1addacdc5b437c1cdcd362c89883e076b GIT binary patch literal 1236 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 NS-~5Ro5C!G{{g^BAIks$ literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/domain/Role.class b/target/classes/id/iptek/utms/auth/domain/Role.class new file mode 100644 index 0000000000000000000000000000000000000000..4fecb42d81e7c4bc04751f04c4f1ab43d2a30ef9 GIT binary patch literal 3140 zcmb7``%@cL6vxjdgpiQ(2#-FwdGJny;u``_R1i0Ba=rzuA9Bz0vdLEReNk!4+; zx#Cp!+?u18EL-Sht7b@3Uo#xBVY<@2(5QPwn$mr&QG9l;n4}(!245TJ29#t~e`Yw9 zf^gH6qhV2?U{B@+`3bQ2i?ea1*7`e42Oko?Y zC>M>YCm?nsQf%F-dNmW)CMZp>p5PHL|jj@0`@VMmCop zgE|#ybjL6+4lI5`B=MVYOBIc7MT&0n=E21oN08W9-P+jiB>CKu<^cx)$zey7j%?KT zsI9uauOXu%My;cLV@x9{NuR-0&Pwwfi5Jan5Uw@pM=Z2(`8*w>m;4O%)Qqz;QC>rj zC)gUWUF&|+^yj51cD>rMu>H8WjjWPUEgH7uID?N}ZbkahbS?^aI_K|1xZ51d>|BeZ ze$Q$&5j5Ync^=Vz*?iTi9b2b*qpiS8f~Q;^t`l23TnX&@t=$LFW^2IR!MaAhslfX> z{Ot;j)&m2aQ=lJQ1=FZQ!coZ?nhob>u82dvBixEr-ZN~3RNA8ju$nm#D;Qb_vJd0}b~YzLWHyaFdk3=`4?%d<;jQ}Vn_aWmcH zDcNbHsF96&&P1MQwVn4z7!t{SdCntBqx+$`(s~yh2-7g#c3r4W=Pmn0uSlE`3@f1< z&6%mgUsMW084OD(t2H7hS!P-CT-9hqZQ=AzJ(^ll3qB$fk*?RrOR7FzxRE_m{_cr& zTpzBDTg%0_yOG+5AwhUH#vt$1A63p#;ETF4dQ1Lf=KcXO)$o&U;{u9dL?`gXErd~h z6Dz(|Z{a;scNcjy6}N5QEZ&qWy90ae3$BKMZG+&`3j z@s?6=DBb)y@Tc$77kKtTm=JLnO~XcxzQih{)afC8g%xH9Ex|gYb*vbj$o)k_f8a-q z7{ZnbS!gOJ0_EjkcLi?^|EWBV09_A*CO-)JCK5Ce22Fhs^n_NCfN*H(FlaUcr0Tjx z?IEs50tdpt`HsM+A>amW1|D(|xJkQn^KWRVSwfZlexEq1B!cX7WmG$FK`()$NdqDL zGun>AU%xC~or(ZI{vQ5$G=Ac;cy+b{{N#K1Zz+$?IP{WUsnt*Pvp-ROQOhIx6&ns24vqg0xzeX4 literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/domain/User.class b/target/classes/id/iptek/utms/auth/domain/User.class new file mode 100644 index 0000000000000000000000000000000000000000..bcf9e66741952454ebbe5561d13f4b6c7ce1d366 GIT binary patch literal 3895 zcmb7HZC4XV6n-Wogg_8MrBYB)L_>U8U)w5bML@(xv;=6ywzwoCSxI)|?gmR={!u^J zo^m{=J*PjQKdPtC>}((;i{J-#Gjs2K=I(RvJTv_J-(UX#xQkyq5kp%N@iY=>7dUGb z4Xf(Qb))82Jfq;a(kMC=)3S{RrYEOu-|{yF+V5Dl<=+!%%Zx3h(1BDEooRF-EpT}^ zgjw@Tb%+W6mV_vnzUA0?r{)%iCHHZW<0%|MPZEdI=v51!+&g^HldeFg z%0;iLLXV_z6vqUT(l%GivM3;yllVlS_qDlUk^rl0JTkpfUizKrN1KZIG>t(FF=y8) zOHZIDx8qB{Af=OOoWig`%p&CB28YGP*{Q(eOd2CNE6`D6Uba~w34PkHY}#u^-ghl~ zE$}>-#`(arYI@#V$1SQP#?r{D$@X&5tWMbi1M@YqT9HeZXYorWY}@ezikabY0%DmKj>cCB1i1h%oGATS*1 zK5f@38kQl64F%1L?diI(SyfB7=NIN@=Vk=rnx!sP!4)rToTU2xl_Cj9<*hZ_^lL6b zuM)IgK9=Op2fAvWMI67!b)s{AV%nPvj(Vz`UrE1E5;zr^^Fw6}lTJ)e%uLVk<@wmL z>;**^F%NmNu%dckC2Xgz*R*P8S-R+~6u>t5$sk%aTAyWau)uQ1OoXJuT~X%vq*Ek0 zJvqykbG6Ep7aC0mSYota6Dz64|IBF#4dgf!fQJ5*B)Y<9e?`?YGnPp|~;9SU0U*p&0 zNsqGk?`|?~?y%8rGlUTkt?DnkL#dZ2yYN|=k1vQXv=!QnE;&L?VWJ6D|M=@;WE zljM_UEKi#Y9H~3ZMZPcbFQ!h`aK4N0%ltEzafJpRq@kfbFeqnU9KyLlj;`|eHHquE z!MGT1dibm!%mX)ctfSKUIh}ZgXflM$@t#2Q7eP+9f;?|IHNwA7#voM;G6 zgu%Jc`h;(RIXsO3I^7I3yf4tRNTAVXppkunz9wa@pVr+Bbbeo;d}KzMW}xi8Knqxm z$f)l(64*%grG0^x@D016xoQK=8QqKk(rx)Ic31B?mP5eXOhs+9_Uwf%oY-k7-3Pq^ zV!!qX`#|-G-jXjEm*Dk>-L@P4JA5AnfBK;CdS{35BQ5YhM8l6B6kh8?2!Fl>eg!Y1 zvhO}9yq1~}zPAP5jLtrDP<1|u2Zh%%7s4NHfiFT*WK=8W8apSD zqP7MF*{gGuSPHyiyq2bR{+o4+GvR`RTu`^{+OD38p~R3FtUzC1QYT+?yw1RD6ww08 tsL)DPwc!M+TGhiha0?W4uWs}`)UcsPZ}DT`x~Ydh;pZ2KdHBV{$p6nwKh6LE literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/dto/AuthTokenResponse.class b/target/classes/id/iptek/utms/auth/dto/AuthTokenResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..16405b8eba8702d52946da8b0fb62763a9fc0128 GIT binary patch literal 1941 zcmb7ETT|0O6#f=UQ%aCh1f^c_+6FNq-ix4u7Z|*ZFg)l}XjdC3Nlj8l|C7$>jEplp z_yhb=j^8FJ1gT7YNY3urbNSBMZ_lsaJ3j&3$I~pj(5)e*qX%h$+`6@8nRUx~Z@!il z*Q*Mor)|d$W(2y6rPW?!(5oS@ zs$&=BQtZ3qYqAsW)XLXF>9NQ~DG_5)j&;Cb<1g@8B>#`D*OK$}36%+cnubpTXOREB@ zsE~mr+mSDtn`_crvDWJ0Ww{lrzG`{4+Q-UNP_v2I=)9M_rROrJxz$&eXKl(rdh|b9 zY}4Y9X$u0w$#N293U@z);-k=QQ6RhQHob~`Vk__?2bg$OQIBnJ(L49Zbpt=}tVRUS z&)}xO)Gk}g2{M+G1T6nex&Z-=Uq8PbzoCIN%2`a~zJ?hcvv?qIW{>w?(+TWNX-4Sd z+5`Wog0F7DObCQ7RY4UYv*A8yK9iwedhAAc7gQHq0s zgNUoV8XckJs0bw|17!w+lkGgMFK|~!H`0^^;|GK>z75T|vWJe}ggTh07>Xs+G#B&I@5)g{7c|6z$bYNOn1@f=u zp>&$kJ8*VX)emX{qsy-6>J@=>X>o51S&Ug2w{Z=&z(cp@xNWT(PDi&wM|O1GscGL? z3zXDq$8V}F>B$4tQl8#Xzjjop1u|8?rUVL`CwJ}Y!1WF)2K_x7IRl;XWJ?*tq>Tcm z1afT^wA?UseQ%8bAuTaNN4rgDSLsTDAzimo#QOqS?Z==3>!r>APqVUkmY%_%vGE~3 zB7R+lb;Cp8$<^>T4BQPHv$#o-DUNJ1rj09s^>O`i|Fx>>%Hq!gGdmqmyDhcnhVFh- zJ@q_aOHKWFj|s0K11+6H*>r1>*{SlC4y5bp(AoC&D(j$85x6mk^Xg4|5NmALJ@BON z1Qg@WU{03%m?cSGA>w6jf8J{AH$3~ebQa@0ORp3b_gF!3dUBhtr?xw-eHFZtl)+S< zUzN>08MtPj7&E%=QkO?psTG0ZAS4w^VDno_*Zta0GGIAt74V9eR|sgRjwZ<4gSqM5 zGmz}*(vWJ78hS(EdOw}M>Jk26k0!4KCc9`owFMSV+lCSr7~l0fK~=qQO&;bh7!&u+ zaA03_r%-;?_q8d`b}VR^#WE}0jFO6(6qQrcAtzw*nc$PpjpFGzR|e_dN%UV;0VZR~im|yx}hul6es(M+nH#|=T5u^$&FwqMqF%LoM#$e$Gf!T|XGB0t! zc)$Uj;v7qJM)Mbe-{-g+g}^PYxA|ltrV#B4X0ZRq2~K)q-JxF!^F6GE1S^l!lt@Qt z#57imH;v#EKA&>u(JRAsy8I_ZdFcpN`R)-W%J+_te@lBBC4L%qhSwrL%kg=xOWe^j zio(2~PS~1nF{`_@+L5oZ%oFV%EfZlocPE`=`sTFF)MP3txB2M&+3xt7D0zB*$NtEo!vA|K_!i&OI!^04USbn7{{nTj BFuDK$ literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/dto/CreateUserManagementRequest.class b/target/classes/id/iptek/utms/auth/dto/CreateUserManagementRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..47a890ea8511e41b7ccea26d7e53ebfea2887de8 GIT binary patch literal 3322 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 Bmudh2 literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/dto/RefreshRequest.class b/target/classes/id/iptek/utms/auth/dto/RefreshRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..ed5e8e20a6d1cd624bc079af816a4eb8e36f4491 GIT binary patch literal 1681 zcma)6ZBG+H5PlZg-nE>T*CHz53rJg(6W_I!L@-8@iUvqO=%;PF(xdGy-fcyZYW8x5byP z@D+pQ1&UqS5-!7djVn|J;=V9Z_|Br`8s)TvsZt z9O)9YCeh-a;`SkLJB^;&uG5k7xZ^6{-jZsan$s#V%*JtczaGS~rt*>aVUY_}hD1<> zY{hZKR<~0V-Y%y+bW>!Vx2xQ9v>YlEs_9T-O9OU+2I!qoP1*RsJ>C&Yc;tL5p6TgP zjQV80PX~K5x5*IW5~(GV@mUIw7&1Gu>(#{@M-!WfPveT7J?iJle2=b6sdR-8g4BHz zBUuI`o5KAJR`I~VLko}am|^~GY`vh1b`Uc3WJuRez!;9A9dO)3nteMke9PTNTlfnV zDO=rx()mV3G@+yJ?`z@LJ-Tkvi@I#Ix$j36VY5G-2Z=_H-I&wDS(~maydI?R4LqYO ze#ZF%4hCK@%w23sH4g|=QB8Vw0P3E8M`%AoQi|lOv|gjn1e-ulN|H8->quah^lN0R zJu}5$V8!L1FpfwX!W{jzKgf2T{<**kH*k|AvhT&AS)_fEkr0GQgXL(equ!>^U||Yl z^bjr3UWZ(zRRbHvV_X8h)AoEkD=-P;jnSjC*vEYg_%@LHH11Fk9lcDeCTtd$k1_Q> z|A4M}O4q~^@{~YGcZmQr=>-h;Ky8vVrDX93jEe{*L&QGqnGDL7K!JdUP{cB=xn3Au cjCd38(wWxUlz@qQSiuw0WJvQA&r!n6KVE2rNB{r; literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/dto/UpdateRolePermissionsRequest.class b/target/classes/id/iptek/utms/auth/dto/UpdateRolePermissionsRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..49b633bb2577fe2535f27a14c652027492b06939 GIT binary patch literal 2341 zcmbVNU2hvj6g{)Hv$ngb9mh3ycQB5&aWEP*?2+ja*^cI~0+hqg0R9lNc3 z`{zO1QF6!c$rm!{yJ6`1UbrKFAIeY(q+5Pl3Y4}_ob9T>^$r>u{-T8fiUNhf$aIVP zAtkZFP`N#OSE@#dA(SjEqAZYAevBc|tZtu!R--meM?0uk_y8Z$LB|O@nux%?b2?~h zypJqg#>Y%6v*Gj@)lcU`8)N%?|CMa1M(r1YrJbRt+`im%LwCO?A9$Xx9Hp}_pabkW zfpY9ar{}Uj6x%IcslahP723~ywaLJ6rd7vf z7Ve&_IRq+`m`q55TYM^2$8W!I0;eyP4CsRmGS%9X{AVUf$8>En@gvlUN|viLnl*Z$ zO@T|}&c|9q_>)gAxe{17MjJgWfgA72W)c?2@A|`_B_F#w?JK7}`&&9AZ2RM}Qrz@? zrCV z>lo||?-LVfR2{47c`}F~WoY6NYjYCw43y&-O#CSD$?2*rM;tJ2bMP9RP6kSPvU9z{ z(;NuF^(uc^h($!FlAg~m_c?=#TBIQ~ z&$TFuC8R1mGN=kI$gLt0s<=t{yg?uxF&`rT7x-=YoO{jsHdpP$tRGR1Ke;ug*v34} zgzW;q9JgkeUjqI(llf)XbfUelb2YeT>uX0?dY^qI))^%$i!UPHIw2XsgAVKhLrO)U zb`nyi{uj*C0?H(aGkT&N_XI!A_!8+U*!i=-E|ZsmJF^<8oK%NybV1ViD$>1+uep9l uE8VOd13aBHXxtaek>*NtH{Fuo@b)4t*NK_MJz_PmMOmJ*hj@%9So$BbTN;!A literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/dto/UpdateUserRolesRequest.class b/target/classes/id/iptek/utms/auth/dto/UpdateUserRolesRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..6aebc7cbbd47b9c744bbe792e9a22b324fdb2ff5 GIT binary patch literal 2301 zcmb7FU31e$6g?|Dk!=+0IKge2QUZk5b^sOnMe7hs5-6Do&`BT-V@IkN+D@d}c1!v8 z%YMsI@?|K4J-;i%J^5=SLnV-lDDs@16e#bWy4qKP>m4;T_(cmvlmzkt4Y&N33yk3msI)36x_W zJ6)G$pxAEmN(GMVsnCArt4$WT(-62kj`QMme;g~n?;d%MiUOwK`gjr6N0_DM+aTgu zyg%yo)obdmRwpsWoiu6(0@Q^Wl-7(N;EuqhQRgE~ zA^h=2m0k(V5734WNZ`hQWm5?Y6!!fnXv)W~PWbZlXMI~gWH$L=V3am}U+H%1Cz%U# z_?FZLProsI*+l`9zq#Tp?ia9u2PVF^@DQ5>CVmk3wYMNIC}U+~*A{YLSM}JlEnVmXN9N z$e=2;Ah(K0sp2N(a|VHQ#C(AKpXayZ7WbO<9j@AmSwEp1zjJF$uuXWFDcgB|A8t=E zzXbetCi7+3bfUelb2Yf;>T4%hdY64A(HSNyhtFf)Iw3QH2OZc2hLnjx?KGsB`X4ag z7tl8(okrzKnIh!q;5Cqm^z}jsc!d n8#L*Qm00s?d^g#W-|+S#E!T;e!#!d(utiycvTZ!Z6D<7;5?%?6 literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/repository/PermissionRepository.class b/target/classes/id/iptek/utms/auth/repository/PermissionRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..02b511e200e0174e08270036b16b459384597c04 GIT binary patch literal 865 zcmbVL-A=+V82vWE0si3+F}{J`*u=yeha`$I!OTEJ9-y!?3R}B$9fnI^%L^aChcbQ} z2Bx4CXAi*VDK6msbF|go*{G0cYGLJk)GNVjV<;#(F>$3uVN$R1?x-D&SGX zrFfVXEyx*A9?~%-J{2Bmc89E|Em$|;Tq=)5q2j{pD;lsTsYb-5nv!8iv%Gg9of8^R z>2u*;Pac_|LO0wi;ntj6T@sl0Zb_3SJ8wLZX38Z6A-o#-iDp8aCrvrA}~F;2m9#{ F2OpRy5bpp0 literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/repository/RefreshTokenRepository.class b/target/classes/id/iptek/utms/auth/repository/RefreshTokenRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..00c2307adc3679e929d1be6f67d3f4669af8beb3 GIT binary patch literal 729 zcmbVKOHTqZ5T35W0>1DO4|+Dyi@oTDRTGIZ!C){vyqZeOQdrtecUHsMf91g+;EytP zmkp1|!Ek7r&P?Zf%zXcRegVKW9Jt^RxR9L65XF$jIEpBXu}_T%btF-nF>Q*T5mCRb zhe9={VHeg3ln3mM(SRwRHeLs!i!S5|Tx#RfC^S;}J;O%gU7I203>h7SY`*hD$Yu=* z9QLH*)o}`Rt9V-|rm)5dR8H$NpcW>Ly}yyONhn4c&_;+-D;9W`lmUgm&UD!(t_kRJ1ec}NC literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/repository/RoleRepository.class b/target/classes/id/iptek/utms/auth/repository/RoleRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..c54b24e43b5328f119ce3ea60292dd758823e0d1 GIT binary patch literal 835 zcmbVK-A=+V82vWE0sf&#d<9L6z4FE(iDFDJGZ>INZz?TN812$^8seLI;RE` zEGQ1>6D2;CJ<@m_@Q!g{-GWo4dn63BkiD*^BmS)PkT7Z}83Z)VcoWbWp#|lxknD1N z&n1<>)2FG--kBt<-=&)lx6qZr87pJPR!q=?=dZKE7RPzN05` yZLp9Qtl`RH)om=T139oEkEgs11>81pZN?yMA-oMc^Kfwr?;vW1qY@XreJD7z{?_V!X6;Gc*j{lb#xt_-7vc06)rD z0|ao3hC`*hmb_Q>s@~pTUIE|;NP-6o z+4YpRjYj2cFh!wmQ`c1IIwHkNpD$!TTNs@KgE_JVIy8??#)lkukS8ziL4lqnvgIrZ VE0kV^H7fp+_C^IoSRWK_d;rv@*AV~! literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/security/JwtAuthenticationFilter.class b/target/classes/id/iptek/utms/auth/security/JwtAuthenticationFilter.class new file mode 100644 index 0000000000000000000000000000000000000000..a49cc8b19fa232fd91e5a07666f9ac3dd1507609 GIT binary patch literal 4898 zcmb_f*>@Ay8UI~d*2vPB#R(}xASHx2-e4$!P%H@8hF}MS4RTDO>sY$jW6Lwn%*cjx zNz&4VuIXOVeM!=EPZ}FLv^{;uA?Ng|FMa59{)E0ZO@DW^Sh6MI=JXt$xmS1jzTdap zZ~5DQUjGY#qxf?i4QSL5(b0scz{9RLVfwS4?Mz(utQk4)dei2-95*l9*+6>cu#=V} z(mOAo%t=3ZEL;?b9qtN+MLT2Kvw@s8bHR*nTDf4-^kq8d z*}+xw@%f;a3(^Vfv=!K{Q=}N5nh#Rao3qnWpnd2LGzuq^iuDE^8?i~CHRnt3gbXY@ z>z63?4ONq`1Sjo!(p2{{`FrLQYiNW#T6Nroy9Krc?zD9JvQ~OJYx^sJ_g4oTierve zyAzVVN5^}xRlxA6{;V8wCu}DreV=+MN$^NL(y3bak{Y%N?5-yKWDv}n1N;m~DpJK&QZ_)dU3Men2Sx1v{9e zIjXU^lt4H3XxOX6#6E$0SEJ9nT>qqHI|8wcTXew_idv zj)!r8+VFGZekiztI#@;cI3B?v4e!@+SjoGus@vD`A{x&2OYSH$aU^j>!%-cN;uuX( zmEOuzEIC*W{lxYfkRb5m3b4<__M_NNhhs0%pW7Q%@W!OyTm1L$0vAv+Q0XNI2lDBtBk4I_mcTJHl zHVIq9l#Xd+DdfARB(RwVtxQ0)%(}o;-XbOku7+71PlD1OzcZ%#X@>bL>slF=fBLSf zuft?4FkHo;>rrJ5W=Rzw0v$OOZFJiYQ`4B0wKE##$*ryw%4Js)cbF;_$Pqp~Rt=d- z1%;FoxQeGVd|byT@X2)G<;g%Y+aqGN@bND>1 zFrKrsnZg-?oI+fgglc442D|?kv`}Z)b$k(DQW;hTB{&rswxF(#G4&w7qT{Rh8ebHT zSIR4Ju)RLCujHPJr2Lat(sEXrRK=!m>iCwbSM_w3r9vsN{A1mv;yMu3 zaK1v8Ox7#Js@Hh+Zq$tV)Pz8!-_7v(Zyd56c{VpQF1=Be1Q;cT+_aS)vpie}cs(kfY}1`!WGlPpvO(5Q8W4>KEu-$F+i~sH@9i z`&X!(cdWTuZac;iWshPr&=ja9h~;W3VDEIsnhiZy!ynn~-5GchZM-DAuVGPO=jyBK zsj?p_9n-K>3@NNj2Zu`=gqI5AF){O1ZLiUn?p4u<{n_~(8`c>)X8Sf@U9aP~LD(qq zrF2$z8NM3n23Kb#Q_~Ea13G4s-BgU-HxNOq>XkWbRlRFCvcC*RHiR4-Aeh0v&G9?@ z_YB7-j@vqKLUeX5KI|a%-cKI8(2NJL0lV3|J_v)2 z$@jQ(AWsQ5@I3#i^9qRwX9|nLw@r1sXbfixPXyoRd~<|HLBQZP*tse}et;kHAVD@< zCt->tHgvV#yNn$T=!qDS+h{Q&tq&~X!RMjx>FQoaS0i3ABAc{DysxLJ^EGTXnig^J zM%gYGHMQ8&6E&jCI4;nm8PV3qTHBT}(1^dHttZC+LuLP)mvHt?vWneC%!n-^MH*Wc zap4BGtCSgps&~XD{BwXSx<~Z4qA}86&ZbFE`*N#CoDj_(n-qD)cyBq<){{-rM2W2xX`_ z?BKT>aqOXS_R?e~O|~ChY#tBMOviALHxJ`9dN9heErTNn*w;PHoondBkI;`7*>t^x zQv@}DH!z65vt#-vZ@h&e{0nFBHqMjdh-kn$(S%Xa!uCZu!8Nq-%r?^aF>U$=x%>n_ zC6d#46+gqz=|ZD;8Na{_U`s$(ImKCw+^>`43uq?2M)Lioay7A`=t`Wuj9)2N;_TP> zjdCTI7W&1Pr!cxohr{y literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/security/JwtService.class b/target/classes/id/iptek/utms/auth/security/JwtService.class new file mode 100644 index 0000000000000000000000000000000000000000..e75d6f7a09da6577e35c717c45e4861e4d01132b GIT binary patch literal 4830 zcmbtX`Fj)B6+Po^Z2193U{WZHX%etyj3CWIA`;>SHzo!YWRn0*)3Nky4ahUf%n0Ld z>AsilrF+wT`I0uFiLsM3rEA(W>AwFl{d(TaNE(^3lYaFN)-3Oyd+xpG-uLFk|2*?N zfIIQ;1e(ySqeY+V zcA15=S@z_)bj6!>(?-QBrd^q@IHotBJ~HR!q;uZPOATwz&UsnKE=$KVrK_QDJjOC_ zThr!@va2u6BjCCGKAp+*UHd43BsS>SDA0{f8rJ(Zo;fSiW0va~7PHc9*>f7YdqeB!OHzIfHtX0T z@LFuuu=`bpsv&9A<2q_u6;Gv#X+P3|?bxBCM_?yjr(s*2kSAx$wqumi!$rro>`9hN zgMM`E*yP0FoQ54ia6Y}5rKFm~4FWe}mxh*dsX~9Z^&W{cng|^!57nG4i@F`X0)0qn zNK94CQX#BQ;eW4B1Ms!=i7i<>_6Q6pnQW}eyZYQE?r_Z{!H7BP$B|4>1rb_q2l#Yh&6hh3JA(aN{ zFwGep)N!Z4T{uMb$0#CRuNpdMq^Bfo8MEx8ja9+z%AC06mwQhIW7Y=l7{#W5Yn1+psW zwZ3R;uQjxLb`XH#e?lOK3F75ThB>QYD282xZ!nyw!&Uj75EJ4DCFHEddL9FS5< z`lP_8@M$)MPq|8VPn2wJpNvN#(~J15z~_`G6ro%&VRU@H;XG08^^5qTz?bl4_8k|i z{U*}ap|8jjQgYquh7&oM_~YfP0$;3bSsZm>hGZ1a~>D?p~_5K(5fAH0#E9= zEbwDIrD3N}%VK8pl!Pn2bdKkXg5eaDM0n34w@yxsK6HQ$9)uNL94=xmE@4673KrSm zjAm%wKRRp=$;FxUof%5t89b}wIf0+xdCGrzeWP=MJk}IvjeM?Xa7v8Y41H_wnW13x z#k8OXYA9x#i#_A@aZtAm+XW7~b>pTbk5y);q%&bmmG~q%Zs(2CNy9PKefY4&E1IMf z>!L=xegMr-iASYZv>l z=e2sJu!4sT8x76;eL%WuJ8AWgB21aQ;5i(`^;5#Gl{+AMZeU46(_lNS-zzA^tSfw< z<7qo2E6m{BebOkIT+top>NLkART!Mo{mzo^n3a<(et~&U4}P^!aixbCM!N@S+?izd^Ams+s;hdRcW7@R+XHyopWiI^^~?ZUzX`%Zc4T#EiZ$e z9B)l*LrO?}t?+6suUqj`KE2AjQU0p;iPRNnss5*-Kgqi${EYwCqM84@(Sl9rz;*aJ z?=}Ui@eBNtceL|SHT;U#t(>6zVL13TmtEy`h7bx~PxWgHSn~ug1MD^~1GYpk^%|y* z7w{Xt4>4chLWS8CVoJ1;60ms@>yM@S2U1T#U%+*jv26j@?^(d^x)=3f2Oeu%u*3J1 z3JhzWHiw=FhTq~vu5Mwx-S{18EAJgmTmZgO3EJH}@~#jwRgPgybkM?^(e367Fk8a*}uV2bZS@{9hJf z5pP_=R1^027t>8hEx=sDY!jC7u*O+mEqaP_0rt?#0PDFI+mL1m`*^n>x8o)Z@#EtN zZpLx0QDO_qm93R4TPs%ebIS%eqRyP)X3 zgClDR?^c%|)Tr2tb+TJ1Gw{o&h13M@VE<&;0|%+31Cjdn)X4SVWh$tq+Lvjp;rIAM zNZ?RNpfkl3eqsrqi6$n{&D{~)&KhoK9PS@$xT9f*C4JoFWb)DyzR=82F4aQ`;720x z$r^kz4*pMgG^9Ni!gmMoqjmU2d}XH9U(4PhG*c7x10OSfuDY%0@iALI8imR@V0_0dA#4)G!m_Alir@n2#?5p74Q1 yurAC@llpuKF*2l~FxCeck6p%(7Vz{UmX_z>fe6t08qnW;cmKdY`KyZkFW|qCE8@-o literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/security/TenantAwareUserDetailsService.class b/target/classes/id/iptek/utms/auth/security/TenantAwareUserDetailsService.class new file mode 100644 index 0000000000000000000000000000000000000000..2f56bbcbcb6d0f710091d8422d95ec9ec93536b9 GIT binary patch literal 2595 zcmbVO`&S!96#fPR3(Hz!T4=GgAka#9t+l?|)V4fZYe&3y$zy3M+1Hh-)GSP;111Sp~=wwJg<-6Q= zxwmDnZax(y#n5?2dQ#nGXdlZKyU~Si1Ez%@SPZwMW6P=%J9eZhq0J+;ZHJ;11yb$X z8^YtBn%U!lcpQpgUMMczupok6SrQEABAQ$mRX>!<5B3?R^DVpuM+UZbQn$~UIEP*X zX$x;6!!Ue&v5JG&FwFU$63=Nb=eLAf7tbOY2q%HJ^h>QdSU57Eb_<;W{(V|%gIp*&s1pn)L^?_iiA6YunxTcWnYs|+`5z)o;< z0wRV{3)gU+yij&e#8Zd}t`tFxXUswt;|x81u;7MbV>|Ho7;dzTf3&cXq(q!K1Ct~| zKiIOvs%EVmP=@#XV5hFak{<|LOU+627t5%oMFZ0e56-?^^SI)xML+VKh36#^)6<3b zNYt9(zXY&X;AOwtZ)fYoe40$YI9yd0ZW{ z`}G=GZI!J2|4tJ{?$R1a3NX`LZS*}kG4y$09&t+WF1x(4>G08Jl%vG=BPzbLqiU%i z!e|!!C@6_VsYw`44v!Pg&uLx8bk3|to{|+&l%XW5W;~J}*Y!`x$66z!tP5Z5SA|_j zj$%#AtnaH(1-zQ@6Q=Qq;d&zpwTf#|guPu@HnDrKQ^aGitSyk(YR5dHiP> zlM}z;d^-*>0Q`!#4=|$JcmKr1k7Sm_?I(OdAI0sbSHwkJ!6gh+U&kLx+#SUq!9Cok zkSPpehI(z7C94$8OdVpn2c%7{MVi!@hGQ25{dY_~$=EM&Lx*PZmOH5hg-c<9+BQnh l18Vi`Lu!}krxWlMdH4)@tjE0#6!8R{zL{jV2?u2i{0oPt`>X%} literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/security/UserPrincipal.class b/target/classes/id/iptek/utms/auth/security/UserPrincipal.class new file mode 100644 index 0000000000000000000000000000000000000000..29b331ac986feb5ba2277f5dd1c8644b0441beeb GIT binary patch literal 3510 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 R=~>59+U02V3_GY}?jI9J6@35z literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/service/UserRoleManagementService$1.class b/target/classes/id/iptek/utms/auth/service/UserRoleManagementService$1.class new file mode 100644 index 0000000000000000000000000000000000000000..18a44fece8cd1eff578e235a597d14eef3d17689 GIT binary patch literal 919 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 literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/auth/service/UserService.class b/target/classes/id/iptek/utms/auth/service/UserService.class new file mode 100644 index 0000000000000000000000000000000000000000..df33e9db93c95cba9c3d446d0b35b44aa70fb342 GIT binary patch literal 3536 zcma)8X;TzO7=9X929{CQ^+cl(L<4w?CMGeiF(QFz0%}+!9=V3y21a&g)|pw9-1mLo z_x%HsszgPrQhrY5H>L9Q%)su#NLd`y-S7RpNB2MfUHcor{rD}3I@D`u(2+o+KUYt*Bpu5Otm*Cb5=u%t_rwxC@@hmPCPDUc50tWhe^;^nw( z*>$Dut0EtfUdgdNsWSeEjv>{YEzJ0Q10-=2$JJ~(Auw=Dtq?XxP>-rY7HxaRl6Vv& z8nQY@m9efkAHzyKDcz#wdF&7dN1ytgJpCNQV;WBDIHQVlnL1WA%eh6liXKnHIf0&9 zlR3wghP;%MLEjpErP9FsrU_3_g~Fo4cKnVBr)=kwIE$wgB2NpfjX|VmEa=Kw8_Q3i zjFM?k;R&=3S++c0E{;og)ErkKq=%fGSr{{2OYNh>27k(8$#us~VY<6`fM<9!3`>8? z$)7Y`&NW}Ef~`{n?r>ZTaccANl|$7{<%MyXUOF2|59hm@-VKUXpd;2mWdKoWv&#f3 zOz3FqqAFsImKKVG^mVlm&6eh2+FC1kaH)5PXsV@P7RU2uS5bCx265uYBrlqdm0HSF z_asM|x`qUj6~B19-kPEs2lfhNmUBRr^}Okz$!Z4{5mvY@k8q%)#_Aq{U7T9AaWB&t zD$=Y|c5`yjQr1$cN3vnv?N&pQm+DB__N}5EvpkD++h?va4O&!nS`IP#7he@ce z?})QHb@Rdryr+iCND3ba?7P(z1TU>9`?UX&vj4G;OPEbzmIt4ygU@yND5p^7!BAXmTrH+^gPJ{{gc49 zC78s8r{Nb~0L!!T2@+z#2UQa@^qZLL4FlI*?R?|cgJ7Q{@|_- zIsRYC8x>fM2EHRxUHcraRtM_({g=Jyr;^^*1_HN1+~@m8>U d8}H)%X!RjJiB_ND3;aO-BxQfZ&-fK>{{w_O+x-9l literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/audit/domain/AuditTrail.class b/target/classes/id/iptek/utms/core/audit/domain/AuditTrail.class new file mode 100644 index 0000000000000000000000000000000000000000..a2d5eec75b45b15364b3c86fcfed375da4935963 GIT binary patch literal 4975 zcma);>vr2j5XVQ(#Yw7=G)XV%g3?+r%`cb6Ew@lO5nIvi*jzoqT$8I&Qk|(bgP924~h#jOnkb@U&a%I+Ty=#fi*g7oA;}Yng_Ytb7}&A)oWC% z4&L5{RY#bus_-`CCR_jV1&iRB*ld+}RSY&0Uy_&-l}0^7PQ=OIg=s{CS%N-&&Ve_;WZ)%Pxlbzas;4L)3oZ3(8%a4F;zetr%twL+k!0Q8VR_~~>CNjRKkTM;ivM`M2`S>o<@`9=e z_~(%C$My}<8}smI55T^scZwEox3&nTTm`rG2GR@1e6%Sv{YGpzXxf6_272_|?)umG z+1SGG33cj5C(@alT*aL0jk@FnSfSW0n3_Heh;p+p8WT?qkLuSUIyrbwCkL)pa`2o= zPRyf{1A8|)FcQg8!SUq45larNRC3_iO%9A?a^Q$3XJA=XyPGbr*P-2#e2z=;|7~ks z{;`S<8+W~0=un5lov@@zeo1nfM{4t~R}tF+Pv)UTmNE1Q4|OZs&EdjxJXtCSf@=;I zfZnK!s8kk>p<1!(R;HtXk^}d65)msa7L6vgsI`o>c`?6q0);k*qA`Rk8XKzCC0N`q zU&H+}emU+?k$)fVGt@;_LO8EJq-*qgk**^I-Kjlfv=uUo5Ok|3gzq!(fHKDbR?)HA z)$yi^=GCswH&t}7c6FioX=r4T8vFW_txvmwm!q$>18y>aRkXKu^%kqQp`yvPt3_7d zfQoL{uD;3YB2=`zcJ(~n3F$6WG{1KBUQQ?nI+_MecLX^`(D5{= z&=Is|1f5EQE_4Kyji57W(8Z3RiV-xK23_h15=PK;8Z^@pv~C0y(x7V{K^sQUg*0fk zBWTkIx|jwnbOgEd7|nvq$YoRkZZo}8XMd%9x(Mx2r2RS~DneXEbVJ(0xE>svQ1Ft) zrAjpuelROu_q7PmMiTFt@q4r4bx(}&Y$Wl%89$a4uls3)XCsNPoAC#-;&nrg@N6XU zPt5p3S@F7~M|d`p_`r-mnia2)1QDK%BtA6bk7vc}Gev}FBZ-%0{Hd&XeIAMMY$Wjw zGyY6gygtxGcs7#wr)Kau^>8yBtq>AutB=Os3d?71dpUEOT8%g|* z8Gj)wULWcrJR3>;$7cM+tayDYjPPv4@j3bgr8GyMsx$r)o<4*9Iqo=#e?eb9qVDVT cl{UYoZ$kREWq!xzT+95P&7qe01Dm7&0TEOW7ytkO literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/audit/dto/AuditTrailResponse.class b/target/classes/id/iptek/utms/core/audit/dto/AuditTrailResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..ef92772fffada6ccb995feef18b75cabb0ce4664 GIT binary patch literal 3847 zcmd^BYje{^6g>*@BhH3650Z)kp@C3a2~k5sp@`CkM~d6Isfg_db_6Uj?DWJqK3TD9enzpwm_^#a*oyaBt zx7}{j6LwPs_L>}Rg|63rz@xpukGO#$k5ZO5)HYq=we5!wYd0O4_9(+R$_9#p`>ro) zv~E(bQ^(m9q37?Ym6Hrpa%IkKh#-{MV+=DgGDar|dD|6U;MbaR<{bu8&g5w=1!d@6 zh7&5(3_7l-LZ=w!WQc`43c6u~Z@t*#a_xHz^Kzopo6uR$GAzh&A?S)m&{0d@XQ-&9 zEg|;qaM23X0W5|^86FM!vo4RseOI((=JO28GR}Ay2H_nZMedHu^8v$(0@V2v;&W5D zLd8F1SXKGmCxV9u;g1+D$#9{`1u-4T+#fSsfn#8#5pqH|*Mxy7&86@<+^+eNAc~zy zc?TG-;ZsR_peZ(RtFrNyb#u;ZF;FS03PkBQ))klUgU(xaYeH>Vq+`Io<(^UmNwzrj;`HHfs(!HU$yc3yy`wG5PDYoq*DcG}gFr+gbdwek z74g`>uU^yXwKYd|E$2|Dk}>K|j_ycKui3~hbnsAXI!U*2`W=R554wso*h!p$?&0)1 zhLdy&=b$r;Q|llbBby+bCYvQYPIi*)G}#%lb7WPrB{G}r0@+2f%j0=;O7IaBu*dKW zp@F%+h=g5V5vs}8m8F7?)O#!$csW=<&wn25a$7IF?fT6&kIrrc!ESfYdE+OT03~BL zdDI9=m4a{(a-;1=(O@9?upbYkP1*fWXFIq;tmgYXR4I8>K-a)R|D98^^xmO>pADRP z^HRD@`h`gbiloj!8Iw|OX*4M%Qh5?U9Im7{@KM5aXm}7Gs>Y%#j#pEVC5joMn#2s9GkAv1FNJF>K2`8smaxj>ouYndKOl zU(zl^_=0}(bOQPNCH?-UcgF_q&=Z|Qu_Nc}iXX{YUH*%JBWff$8df98QcjH|PkA+x zOcm5fa#d6#$<~M(Nxn*IBpDl3BgvVpMqLUu#?(mic2tcdbK`0xxhvx?jn_0(hNsA8 z$V{>mWOHQmWD8^!vPH6GvK6vbvP)!F$R+;z;mhKli9%Y zso>MuzzeD1GugnERPecM;KfvMH5+(26}*%UypjsGvw>Gr!56ZDFQtMnW&>Zr!<0|* zau)C~w&@EV!q@nQe4ToqRxPbrTCucVX|>W?rIkwSlvXLNQCgw2K52E*+N709>(aNh uk449AlQI_Z2;bp*{D2?v6I|NAfhIVfU1F*)stSFlZnZy2}n#Z7>K|JG&{{Yuqae_bl-X&Nx z6mUG!v1-iv6l2US)v=0fV|HT+nN*#dlm-ISNxLBJaZcj5R}v~SMIFm7hJ?e2{L^23 z^_{YN;zy&pm}z4t zntiP^=ugZG(u{Q^2ZL5~K1sD_G8@pXj@+(}iZP1}oaNTK1PR!1*m5})E@d8+p#&B8 Ys+6GWjva?xhZ5{LEN=U7Ft60U0D_?MH2?qr literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/audit/service/AuditTrailService.class b/target/classes/id/iptek/utms/core/audit/service/AuditTrailService.class new file mode 100644 index 0000000000000000000000000000000000000000..afa3d765c7c65dea213020832a37d3d01ce9ee82 GIT binary patch literal 7340 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 literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/config/ActiveMqConfig.class b/target/classes/id/iptek/utms/core/config/ActiveMqConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..d1fd8430aece06ad056aaf4be8642a5da03dbf39 GIT binary patch literal 469 zcma)3O-}+b5PjuaL_s)t^oAVh%?nCGh#t(Ui5l)@DeC~+Vz-O_El(yM`~m(bNhrZp+2MR)y{AIepZdZ{&i|R#d<;svDnBM>y;;KSgRjc z7s+Jp$V79eV*Oye9(wAg0z+hf}XJ^)_rd{Y1b literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/config/AuditLoggingAspect.class b/target/classes/id/iptek/utms/core/config/AuditLoggingAspect.class new file mode 100644 index 0000000000000000000000000000000000000000..715f074cb62a4d2abf9c30835056066c695b6c44 GIT binary patch literal 3349 zcmcIn-FF+s75|MTYh`(p7$qS%Kq5k7B-_zKnwB<7QWRkmgPf$6?A8rmYiTU6z1|hO zD?4tQLZKggz4E}}g*P}nr5^+bPQPCI(7%ZT{APC(%dr5Tp2M+sXJ_Wly?1`|yLYbs z_toeB1aJ=TX3&R(hNO;uqy&bR%{9}oO{ZmCURaimKp=J2a;)IIKq6n58$cQZ8ZtTt zp$m*#O~Yyja>eKbtG>~2J;|4|Xtj)Tr)dRMx7D(oR@rZds_rw@VBIqA`_~p}Mv0THw)RS~!4%8t&I|NLe_x-9jK8(+P}v*i>96kk^RRU`qzk&{Q*xVM6C` z_<>v<#DjQ9!^1j`;HbcH*J~MO2+48;^%c);NZF*_XI;y=!iU;)YO7f-#|%0iy*xrM zcPzX+UmC=wZ9JUOMr66b<7x_|W$bne6q_{DvV+`ZuitV-pvZ2Smy?A=rP*jj!DUG7QW98YW zr|P*{y*yLTt;%4@ZJxV+BiG@Ps5U~EUxRoYPiQ!$<3~76a;i;rYZFLX&Y~-j&+k0J zt6f78mrI2?bre6*@g#mKptU2|?`1$NoW>b4EB17@QkkgLzRTA%&I)8ImoL?4E?27) zGa48&r?%53jF0c(b{bF7wOV8_jSB)t6~R2$wxyS|oHe&$2A1pO8t!VF=*wn?as0H7 z2~3i4o}`hcz$ZNr=>jBX`42j>JJuBDG+Y$ezZHS?CC^UY7P zlb+`)_GIu3sye1s6l54e)6)0NmQ+-_tmBH}QF7h%oHS;Lvr;=dGm%DJ;Gy0)FEU#x zkmvAA4ZqSck6#O%4XM~xsbbM%7Fl<_6=Pj4sO+bbf#F3FWW>P{=ZK50-IN{)N5|rs za=?VM&a^ZmG%ODqhs_l1c{(zu3(TOe|<<=xBo3 z=+M2PIB7Nl*V_;n;F>7b1x|F6uwt9OUy71gcMz8Zp3MJ0mq1~TM_F;3bZ1}Ha^$7X z>Vov@iaBBGX_)q$=~=2D4<>^pi!c{^S>wU=zP9M!&~%uiu9)hm1L;x6fiR4>7*jEA zb-SF&?@ZD?vO8he;(MHL!r=8gHnVQF*R0^g!LEtBVL}H7mbdPWk==Pf>{PzpZY)j< zbS28c{D{&sLnUT3b0VGCU2OBJsRd?ZW!h|qC!pcRwh|*Ur{Psz3A@xuQchfJ$hJy> z#Cv$A;{?{KoU?q3=UjFiHwe=s=Z^0L!n`|gls&iOG>KjRnrU~)n3M7vD+Q%DKbo_e z#jvg^s=B6FR8bve;9_w!Un~|1^uz3m+zZi7w~M&_wz#7w>9Y+xuC+2Xx8pVBBvTKA zt-Pa_Dk}PX?-s)vgNjFcddJellWwrlmPUnliR-W`DzS`6vm^wc!|E#MS60<}Y?B~Z zSmaELy-Bu8E;ITW#OTFvA_eauL#tM5}7itx-`Q6Xj ziH*Q294?}I@je%79Q$Gud!{$B|GyaE`y=#cAGkDj8;1pMBgc44j^4tFGyQ)>A$v0Q z5A@9^hLg4VpH z-K%y!HJrMI=cxWt0$po=k*$Rj@%n#pLuLF1*Lfo)LR`*cAGh={w|E5maTGcJjo=ui zj$;ZZm|910jThB*Jb@cHg%4Tx+{77t%65~tyuxvY=QG7Q7j2{{bsn$4;}@a2i|rTi z5zu-PpP_>_BEKIWW1U|KTKs@E);KqU_pm|#`e_fyBk?7MH2k*A=WDu~(va5hAbWTI ze+Qv)FRxKrap9ReXXkU^_k0F9_&T3AeEfknSy8g1xbi&PkP(}hogV)WuD*{wewy3a8ul*NCZ$soC_F21qWHt+#${oo%|#IL^X<55rve~35bKl^)q$c$^`W-33| z*kS|+j7VRwebc#SPg0{2ra&~_7fi(B;YeR^+!!>sM&td#P0?sF*ccrgG$NsvaK!8w zibTvfP$bQW5lMEKv1lTkjK;Sy&2GtMz*-UP%3D~^bac#&4~7$oa5UmXupkeCF_hdK zjAtkWR~LW+wz2W(fa%0D_Xv18a@Grmp@bR#BpAAL)&oQNU^Fz8%f%^qTv#id@^o%X zB+bFrJV;<07c&wGs6W&c>50Nkou~G^)uUGMKc>s|)oYlh zAr6w^L32$w5r#@v8xk-{Gu{x1M3Y7mj-6myP>|3j(}*O3y+)5jP0+{y1lJe?LncgB zyhR2~{uvhg-#P_GE-4i^_o&*s?;+vFgm0Y)0x#N(QPIE&Fq| zoTJ>jg33+CpJGm}i={A_U;dk@#i@EIq`-l2$Pw$7ngY#?mL>%Fv^Iy5>R z?x(yXhclfzsf(#J8gD{GtQ5|W$Fl2&qqB)Qs$RWDaI#4wsMSl|v`(ip2>G%M;-}S# z5?1BJXZx&|&Z3PPZ9wpifV43hNt)*;bvm2QQHj}sc0wV;z?U{b)b7rvjp3z14^qeWqe9;UUC#Sx0+I)7j}IvFs8l+*pG8Hg0Wu z$7w5C+Si`l2noA@2?jmJ;=fWFk=}vm*2=hf-jE8YEr{9-M8oRU9qnrXaz4|s4KZkA zi&+5X0%X?KhSQolJamx^jeav;Mib$h-w1J?7_OE5q8do(nDw#eb51Y*% zNadl=LcoUQt<7y7x&g8^ga%RT6G)DzIU@5-BH+zTE1jI#vN`cD=9@+uHEe91_ z+q7>(3YVH)Ijd4wt{le49m2=wnfm`uKK?#l?}By)#rz(+8*!zYrFrqtRI%c{I^9QK z%(bG3z_ifG~axyS#(|GAgdRn8W5Ha~RR;OoZ7aBoC71|0UjRFL!*L&zWCa+Y>xr!wZ?E#Z* z?On|)nj0Isn%m)I&!d*LMU&y)aF42>l~z+L3f-sEema1B

MwZQ(NHFsZF%G+Z9~ zG7Qzxw7R{ctGR8ZhhBm>(y+&q=oM^g9(qOGzI%CdmxsQJs?jhM3Zv%6jqpH*p|j}g zI=w0#Q*qCLX~3?Yq}re~hosHPR}tD4ul2qS4IXr5Jup?|g{L)dZ*~CXk<2V9dU%86 zjO1)?7D@tRWluava!fyrO{YLBBf>q?6L!8suBIN4H9|C^hlNI=w3;^eFg12fQsDPxn;Kp)&dj{Y;~u zGR-eQ;Rw4woqkTgfa4BpcBDOKs&hK@VMErL%p8pmiX|E~*?-|HqO4!q8SaZ1$)Px= zwvP(8pl!zZgss%f$n z=v3G6F3U@Y=ubNRi?lXc!c0oo$lONx_=`^eM*ohq2S3Qli_9}V-A$!6g~qdpKGNww z>Axglq9eos!<3v=8QLrC&QVq2|Iz7x>95ERMkv%OM+}j^W!o%oNcTR9Mq_kf00$rF za_b#7Dm*)!QuP`!z{fgv(hToN7+c^Gvz#80+bB4jPV_^u%s~cZALpPig&+7N^sz0< z-E?h>r8izD=R%CytC-d~Sv+h&j2^v8EO6H~Wn9W-8helEk5Wss{;0Dq@#`NJzp_kX z&WGcwv2a)($8_O8apz%)IC{6%c>IVdQ26z7IUl9-(L4bz3lCQnCC6pEyPKESm+>*| z*Z5eb#sA2ej?TyNBq`qNhzNRjnznJX5$}{{InraMn^mWU*nLf5_o0SpE;>OlAFr*g8g-g4o(2QJ?L}76C6F#x8mGK;8 zKCVSWmYexTd;H_WS!YmOY97|7I$@_ zA~^JvO;W>N4a-!r!a>Onc`ITU@IsAG!d5oB^~s{v`D8u??v+Sl4>^cka*iWgvq7N4 zOL%l0FV-05JSK}OXNk^BcqvX01`X`~PI1aPjN`PsDlOz7srT}!yiDT;a5IdK&3QDQ z(7BP9!xxiLWK#rfA;vSS74Iu_UdgMZOSg&w(>y1OHWOJG+tALW?jwyqt>eh&wCo-` zQ_CE-8(G}U+tp~0q?I-mf2}&VaXXBGT|UkjnHD+OD*)v5#9S4MsZQ6qgFB^{jkaJ@ zh_*LJMKTAn3%Fb7HM|zIw#Vdny6M@ z0Np{_C~dibAzss9^FLYtFp@aVeTM2Gq&oyhhd8eBcJzZ;8?7=Do1t(oUt}$~ROibW zXEQjM&Fvw5PVWSSIzd*us3siMC(%Eq1zGU_BZw+#Qb8-Ef?lihb?NddZ=_zz{PjA2 zRt)6@|Mq>1Oy8jMjUtu@)2iS~I#S1X2$wYD3z<$ImHVI;6)cYS8J_dvsb1q-u@fKd zaxd=?`F1ecogMHMjINi_Mybnim=dcD<62?#9ApK=xqL&u866=tJT*8XC*5(cFXJ!r z{ThEMr%Vh3#omG$$1D7x&JW2khzED2Nt|#>{}RT%T<&pzjU6=%6&2R&j1I+n%oR9x zMF*d~UY#qBE5a9S?U209XcCRQ+^EK)5i=s~{=9;Vg&Uj_J8v;oTj#t0DGHf#1iB2D zv&ip8%(_`gc=;KAR$~Y=Et{UgkuA}FGuWas5EdHt)sxQ8@osEJaBr2KVtUKzUN)qK zd=)N$(cH~wiAMW}Vg;HChbeLZyCAP~`RHF-Bu9pI)?yoTqcJeh2@BSbE8(Z}PwH(u zmU9Y+Q@l^-5S!yP-fQhHbE6

6Y^W8|o#UqqG@NFUk>CeyBNfTXvIMU(T=C;9uAI zeBK7;z6Lvu2(D6NIlorUEZO=^o!^pU6b*$|vh}!PenTB{o-`E8r0 zcXYmzuYyS5ITBAQ%a!v%#nT~v6HMTAXhD7>?PNlUQIPx(ZSwEwd^6ty^6$z_s5&q~+e0roYV^(yA3t)VLyz6dYf(^MEx(}+^k{xWs9)uw3eUOF@2GkiTMGV9y{U)a|#qvC*X&=-|aNx*lWFCil>;2ZL7 zm60dl+vdB_ukEGnDY~MrG~m0&cb(s3KY9bc>wTZ|yZxF>-6$){0=`>(w_>W;Ut&$) zF4KCzcc$S9=Zo~6kw1IZgM%s;Q z(^u#mdK;_XqaON@Li86haq8F0#ni`Ras4@-!h9U{^Ay}iPNPAdiSH_kC>OX=e2%Nr zE`ZT%^g6hexo=?PB0pNVH|R~cQvm0fZ_zj5*b6}CZH$V+@4NIZj7q@uJM?XgG;sbp zeFq~Cc-jX}-+_O}=rOt@9nAfI zNF)0{(Z@jHcCHq{A6Ri!>Vg~u3B+-61#CGkZ36ESpvS{;r|zX6!FS(-T;jXGw0-yB zGYUo$#8|cVJZLBmw?9wmtI~o`>x+)3_bq*4^nucsgf+;&kFO6DDqMpC=tDLBl?vbQ zgAK_9tzZG6v74s*-C*U{MN~7#@7_ug6RxfuSnou+|_D!K-IUkjUEr({`bF_Dp_H6u%_ zEelv=7Z(9;` z;F-3KH}EWFW5SObX4`sMXX|A;)o~S~#;F%s6@c)PP|Jw_N_FDb2q;TYz)QeLJR9#_ zaxrN8X+L;%V{Q^;o~`_pYm{;-_A|;6bfd(x>X_xG_rpyeKwW%Xi4d^BGa^jNn08Xe zw3GM*)S+QU6V_aEYmhP8eC4zs;aQw^2`qIYQ|&%pT&l466N8NQS#md7njfeGVGhh0?bhRcDXG zC`&RDE#Y;1hH}0o`1#%$U|D|pc_yC)HvKe%H^4k@VDR!rKHE0R0;pA1t>betkmqC| zjSQru2@A3Z5Voz>!A|S(cP3G}OZsG>kXjrD0{m|s*SMRa`XmG(7_5Nt5uh0+Om46f+LcEK^Bd+O}5`AJNp0~ z1lgD(d!CR@*1}^_JOsj1L3oQ0c4Re+Z~}yrAUp)ZTcEJ54#KlDgs1RUg6d_UdO1~~ z7ED8BlLkxjh=-2nE09@4P_*ARy{kY^%(mzNmjn6Lb#5e!6kp?)#OX)~ZYcXRes_ii z^a-^op5k|BnV77at(#DeZbqAR3r(gSG=pwMPj?$Fq}v@TSY)f9l;-hG2x>Q2LQ8J5 zbOCfPEX_Z_JHXOyHcPkr-BAA>es@KRKks)dE8Hm&?04_uyI=<1k>$lw$b;bJA@K4r zdfuIIr$>;O9(C|BKf^~ge}V4?A71I}Z9c?frJNl%=T)>GHHg3aB;+1&A+c%ho#ZZh z0&lG(DD5L5w$jQ56hF*6@k?ou#7g5b^9W>Hj+tU|9eXPF{`@E&T>O~oL2+Tqk0S=2 z05boX#0h?qpTc{z$GnG%_<7#XFR0O%`DOl!8hw>t<=55d8~jcFmKuGVzsuiKqeJ`y g{*fB}n18}QQ=^~r`}_fa2)weB`4#_$e@FiR1AY$BQUCw| literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/config/I18nConfig.class b/target/classes/id/iptek/utms/core/config/I18nConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..e348f63f4ebc39905f3da04ab15ae1003ca0aedb GIT binary patch literal 972 zcmb7CTW=CU6#j;LDHTdB73wKJ-La zXg>^G(GJzf69+DUU3YIV>}5xE|Mf&duQy`DjNuvp7uKh?N2^p~$eTg=upw z6y;=CuXWDE_Ka22stPZqm)6lhK)ea18c6a$h5B-M|vAlP$ynxPhB= zW4J|Ri3t3WPD{G^6WRC2Sn4#&`2`A%a%q8bBe}qO;~TjXu#^?b3alVU`%D{6t63sV ht5J$dOmmVVhc(>6T}rh@2rK1Y#IhaP`=_rp;4ko>{=xtN literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/config/JpaAuditConfig.class b/target/classes/id/iptek/utms/core/config/JpaAuditConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..1c28de8f4d8b5d6245bb087f16319b5ca48df6c4 GIT binary patch literal 1800 zcmb7FTXPdP6#nD{8-3^^PS6g&XNE6XYWq{x3M0egoz3!LmWeyp|&Mn2_6YO z;LkRg<&Hs;&L4tNqYiV}t@efG{@B+_bf#BheZzfYj!d<^8Fj?)#K32_$F4E0PQB@_Hv?3B?WVhBus zJ&hu<8IhGnqhMsjLNm8}0M@L7;foM6I7j3V84Tz51IQHnsqT6YypzUpq@>Ma`6|TM zm}Mw=zHa8eKA=C=?QvY7sw6v!lS7}}r4W~WW+l_F;0nb*hQqGWeKp{ZW0A4n%0!0A zmg!N()wa@dJsoaJ`+USfy=}T8+7MQGIsZ86UMk{!p?y>bO@>g6S|h7A!-XyH;z zUhjTluPhhJlGk>HJk0!-YNv*BLnVr0UDet+;c2H(ua9j;BE!kw9T)$p@--u8mTI{D z|B1?nf%NWpyAzUcbc7)}n0(%;3Q z%R!^a9tz!yWHQ$_W;>0Whnt}A+JuH`{icX4tTQ+xOvc#AK+X;uhqia^v)IcSzv|0~ze@8aSiUCQ_-otE%@=GOm> l{Xp_9dVZv5n&eeV=qLP4kKZk0g?10=EFjB|@B~jW^Dk~{>aPF* literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/config/LocaleConfig.class b/target/classes/id/iptek/utms/core/config/LocaleConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..2eab88a98ffcdcca5ece5a28ec238cf7e48d1cb4 GIT binary patch literal 900 zcma)4+invv5Is)PZW5Nu(zZw~io~O{ae~{8H}ZN@J_~rD z5=eXiAB7n2CKU*zbRW)W#&gb$&-&-DZ$AJ$!L2F^D0(RQSi&+xV_!@KkAxoaxBYz? zT88DvN-O)6q1ayUc_=eHi_M596QlHKXvA0^#^!(@%05q|nMTrbb^np(-7u6ByCuay znjj8EBzI*JM^kC4@KCJa6nqb-ebi8&4^FL$c;3w5z1(>nyxH4gXa(6?f1GxlM$<VeX7 zCmr{tc_;c2Ra)7Uo-oSgvtr49PzggjIGKEhp%(q+3d4i;da%ga{DN~QNu_LNt}=8N zvKRaql3UwNwN+!;Q;DL>b+wMIuqxJcx4ZwDjuk0OlSp;VNaEBBQu;=Lqa`c6^S|9fO|Rc|t#L zz#1-4#&8jrNW_u1DLT>ScX;oQPzg4^p!yY!jZai8zzNk*B&?&124OQ3T+K-{!7?hi Xj4LF3l{($vYZME#vBMk3)tkU?>zeH4 literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/config/OpenApiConfig.class b/target/classes/id/iptek/utms/core/config/OpenApiConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..2d2e3eb37b6ade747cce83431bd249dec45ed6dd GIT binary patch literal 2617 zcmbtWYg5}s6g_JLHjYA4m5`7ojT&fyw6UNxk77c32NMS2J#9u52C|#x8C>zR<;+?(g>H4oMTXl=mkPP*1oa)tdKS~vc|rsR1BZfqM@Tb z4WQXbKw~m+%Un(kr4@yn8a`DNE)=4y6#o((rlAYoS5ICz%8W z8SXWe%I9{?6TyL*Z@jGYS2f{rhXhGs0CCj}X&A-`$z~OcIpI^ZJZaA%txm}(>%Y|S z749-zq7#moWCQLnyl5drOW1R1V=@dh)n?9hs;(p<==!8pWPi;t+I009GI@K4WK3a9 z#rZvkiyuUX8h@bSA-*M_OUt1sU;Lk<)(^~dBZJ8V9%-1ucWwE;4lr0as1lowr9xqi z;X(5_Y+`Y=pS(JT84a_TBd8!kEVtvzQ(RhI^(Guhgas@n@L0nVvJB(xx@|BkDJL#1 z%*-#WlZNe!kd=`HPc-E4lp!VvPy7b~2&Y9PH#0wpRia-{;K~!=%`992F_ zS2-d1O3m5gUcuT@l}gXKwpH1*JfZrH!H!@@kYq&TeE1nk7Iw`KT&F>gVQ}~yM$qv` z0a5qUN|aahvz}t;U$0TgbNHt41u>nG(hZ`rMMUrBT(4yMRaIxVJ;K;`y|Pjw;J*c? zb-dK10w;-VD(}`jn=gt;fe)lgs~};xe_q78z%?(TLeN@PO76{7EZ^tEtfQWrVp&Pm z@>lw(QNI|d#-oecE$W%{Wc2L3^9JnCXxgS$O#?kBQmfS$fMc6_3?=Lkh}!%Q0V&X; zp2!`d`}rGOID|%z9{o~?kMt`cuIkrA+z4^&5Cb&vxt<8|MTk3xNKuCbb5xP+9E9 zJZ@l_M%L)lVFS15RvLJT>$JBwvZt@kxnWNn76f+5t}>x2yDRjJ!=W|?c|7V;ze<0d SMl+NLUQPE?(j0>~-Txln-~Fus literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/config/RedisConfig.class b/target/classes/id/iptek/utms/core/config/RedisConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..ecd49d37f86d91e7b802a9ae235544f1189ac5dd GIT binary patch literal 2636 zcmcIm>vGdZ6#h1jBVt5JQXm&d3p4>cTm+Ot!D%RQNDJ6WnxyIQb7QZa1zBQNtCsX{ zpP(~z`Wo#YlPNPi01wsaS;hdyL1^xMc{ zfMMt<|B*W`_ZrUf=2KDC3452o=}T_ZUWMqRvBCcdRjdoiCK%hix%SH#1)@ zhan7G7_sp&PLQ z>-mE=zo+sRCK&Fe+&>UXa##K=l(VLd@gobbCDa_okvWPWWDEIxDRkWp?uH`BVFs5hT()rq zR~c@lc>3QGE8OD^<2h?Hly1$8eoHk+)%QGt2TJ31dMEQmUe&(ZPIqM&J*M{HqOI9{ ztQHyL8m?QoVdHb$WLSQ;dv@i|a4b@3AqFJ^gkRIm&NySSN672gh4D z`5NO-Ug7i(&a`m8g$tESJ1AUk!D-=(KWK9wjDYj>76DF@pi}g!8Kpu$Msk5hsotr%s$UZA#NNEshI03Z?vTe-JxONl}1lN*>^$bC#~@#j+%% zQy~8p%)r3#z$-KSQ4G7%IhJj8;uoFn?t7ox-P_%N|NHB20C)su8FG*>L7@Ugm@#1S zn4fUwaQBFP(>)fJGGOMRbftQ1K)$hgI0v&ZSAuc{=AmN1hP0XVmFTfZ^+RTPfnc`R z=h9_u9*U=~lIqleStVTVsy*9)nmN4TKn2o0YL#IDswG&gz(uGTaC4NI9Az(KLdSEJ z_!*5_Iuh!I_%V`!u#+uBbu7tf!h(xMnU$8wlRA^v}w3jQe z8fzopPPDXowE}D8@qo}irPX&Ta6Mkt+BQ17`XWNN`{GcBvg?R#*Yy-v(sM%t*6_|A z4-{v<2tpa6bCzHo&x!hO%YcH*`vUJse10X-rCB#}9FCkkz8BzLd=o7gaL@FDBNqA; z#n%BI{mBb@42|ZDpWVtjiOW0a3tmyy5qQNh1P`VHm^@Vbo-JQXVGk(U-nbSn(YQ&|11kH94Kz}_PMXY^O%*xY`m%yVb49uNQIi|c>S)|W6y*u z0!lQzse%9yYlg>SYlgctEgyJMV2NEx30xgEi7jdi^d?yaf)VzGeGp;M zSPlzD7GEhQejn%@>+!(So1lo z!yDBFx>IifWa zS&mwdR#Z`OH&$S$4B_$bZH#4elF-?rfXNm zK^y0S9%X{Aje{EJf*!*Yd>aZj#ftKGrg>MtT{_yen6#28^qWzvlT7~t&lRzqz#@!3 z!8WwB@Jr(v_BMP{B^qC!z<09om!`!h)ur*P6ZoC%;9Qv&pOmJ?uT9{eX5+6-i%*JM fV(z7JT(T-TCZr literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/domain/TenantEntityListener.class b/target/classes/id/iptek/utms/core/domain/TenantEntityListener.class new file mode 100644 index 0000000000000000000000000000000000000000..6507041e6818d673a355adb5589138b0d2db4301 GIT binary patch literal 766 zcmb7?OK%e~5Xb+MO|o>^wn?Fd@M@6|YALF@7m%Q$rwv53${A;4Q8!t8<#iDKD0~bq zAaOu&HD zJv&xL8h_|)abS&CZ+(nq!rtFu^MpAr)0J07IYRTmjul}w(?&g?6(i-2J43!SIv<#D_a8-mE{4%C194LZ`3%i5(B5lZEo!5ux{T#)Lu*b)|XMC&pMWy|#v9Tg;qD=cOp; z-7AwTF*qM4jCN#aE?0*-tgZK_HSdMKa2qK;eoJRVN0+k)~$OVLGSDED$5XN zCps+C#kq?$wg{JS8Bv}5imix!-QBN<-yz-og1CSG1C|0PLIADQ7nT^3Ff6TsTkK2G p#5y+Wp1PQ$8H6ji%Fte&PaRz2LyOo8T<1Tub%QnGCd&qH{Q#3jx{3e* literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/exception/AppException.class b/target/classes/id/iptek/utms/core/exception/AppException.class new file mode 100644 index 0000000000000000000000000000000000000000..dfa433e9a45e7f8c16143c074ae48faa54af3cc3 GIT binary patch literal 423 zcma)&&q~8U5XQfaX``{)KcWY3-mC@p0a^QZiFTD({-Y>YOg0W2&q!3xW1T z7urq*_K&0AzL_<;NXNr@fK7p+PNc3ZJ<7)BwTw$cl70~KysFlC{zaLPKsV9_-8A`v z%)MG<>RP4W%s(Rw^sdP+%j8xWm6IjI{BTw_ zCZ;Rx2I;R4eChx=Lcl9?NATsPvD#ua<=6S`lhG^03uhiej&8y1a%X^T&JI?(gB@0` TvWqU0dYrce3kU9V^f34WY+h!B literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/exception/GlobalExceptionHandler.class b/target/classes/id/iptek/utms/core/exception/GlobalExceptionHandler.class new file mode 100644 index 0000000000000000000000000000000000000000..921f819b5cbbf19e112b546eaea1f7ce3c98fa61 GIT binary patch literal 5563 zcmcgw`F9(|9si7Ddu?TLgdq-TLK46UKGH0PQUWpoA99jbv8kL2+EBts8rvJMcExJt zBt3xMg&y>xOY0vhoV1*xw6bgOr=(rBlq;~SqG~m{sPd}jSS$X>a4vz7 zId_E=bd&3x?V`t%q|RZDuD9 z;T{e5>bMVsWU&n>D8EQx1Rjse4`U{?X>e)t5!|m~NXIZn1jdszxu}Xp(X`8q1#ihI zPq>Tqin6^~$16z7EXOcT6XQv)B$~vjZqd^Z+`!W-KB)}Cw1tA$9L4)J9MfUo0fD1Q z8r_IkppP2Nn93?=UDt7I0{v^Au6w3sk*i4e@Lk!E^u zinU1j7#?eaox+FkVGTJQXE4j$Z%vS(_$1W~DnhRaJQ|Nw!szGRxmfZvf;S;EC-Iby zk04L)IBwReDS;z#b_ul6kcP7@Wi=F>LcuR2PYW0q=rm{;;T0wG*qBNT@o0*x%Ms0nFaIaqRmKliODOsM6dC6`mAfb z<1xn<9J8D`!Jok1e4U$=iYk~jlMb4&ZHIZw9TY)*EHUF6Em1bs8jT7O?X$?ZiveDz zvFXq=g+|X4Fq2cc!FYpSyCqkhxVa4*i4xF*)Nbsa`eQIg`6KYg|I46l`Z#n!LS+|t z)-MrTY64q=e{9R#1zx*_Ds2zIwdztT@kB7KY*Uq)|J*k(0_+ZtU13@MOG&G%2=X`tDfDG&u;={zul^!)dQ2 zE5))LTwj(3x#4~yIgb-tHPfJNug2XTtA*bhk^&={4NqtDPTei38Po5V`;xofF<+Lz z2GuN9k$DF3nDCzKSUmSmlvm7>qMy@~j^p|JyJ}eZYH8$mb>rtLo~?hOm_I{3kG3m0K*Ydr zHhnliRS)7Yzpfa>)<@(}D5s(HIFSOO-|)kJ`r&oTXanJU z3Cg^lPWe@t%Y6`zQxzYCzL8a!{M*;@0@`BsV9eY;`~W{BMJIm5YX{y4WcWV%G2ef} ke?LcmLy&)ppW&DIGiQE#%yw4qS0<=R|OEhHX51|$@zLLvdwqP+0rB#XO^y&Kt^LLd1f zkU(4>_y9f%F>9w#K~$BO-JP>DXXeb#{`mRrJAj91IVixgQFKv4nPF)!*blrYkfHZ{ zu*Zj*q5MEdp&v0=jb^`sQ>fT*Tr9w4*bpO6Of(;RQyr(?P$k?GckW5A$I~WyYMQ##cm@^aK)KpkgnthzXha}3Sn(#-T7s&R;W z1~EKs

  • oceGLHzd8ctjrIR6U{=%7IEKx}e{`zZXDD{mh#Fk$3dx^M;{i`z27`!@ zw-&NRR7XHF^=4XVW~PaEtUa+&`J{xiYOWKQJZ!nZH@* zNKlZ3lw)O14;h^N3ATh$SpRG6HVqrY+V)gx5%a!C1u1_lrP6^GN)l^pZg*3X#g#so zaId4{iIQAuA6IGk3-oQMPBZ>=D$u!vHQLXUwMDzRcUqspTJ2A;KV&`^XkCOwtBWG4 zsE~D$tkv8bmvEUZipy9TuF$!}$N=79ozmv?Jqk7?wbn=2?b}~a{fd<$-V;=S$x)_i qK!3z=ujj76xofzF>)Cw`H*k|8OqNQ4U=Uy^Ze{BR8nhd#Ch!X?J`@H3 literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/core/security/SecurityUtils.class b/target/classes/id/iptek/utms/core/security/SecurityUtils.class new file mode 100644 index 0000000000000000000000000000000000000000..7e70a7c06e510771b202749576224e6ffc95bc90 GIT binary patch literal 977 zcmbVL%Wl&^6g@YO8oN%Dh7bxATHZ;JFs}^?1))j^iL@+2yI?o5hjg06j>cmizrt6P z6_f=_7JL&zTqmY9Y=~;hGjq?(oH@Q{?vGzzzX8}l%Rvr#3k4TNED$RD;;rz8BJ6uF zd;8KigoQ^cROT@u-)QbSI0f5+<6;pm;Ypt^WYG=3fp1E~p4Uz!XeY&4pk6En6Kv4paPii>4b2@g-qL|A53PZP5zL!*3Q zR1^{(%q4Di7i+5FjD@<3vsfYA`M;)F0zz5E#~>L@e0ZJmo_BEps|1V1ZgcVpHJ13- zz^+MsXqkzfE9Tm#$WYm-TmFNu_sdge? z40nZAX`F!tv!{5|tQSIQMXV_B{mqyq4^uF5eno0 literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/messaging/ApprovalCompletedEvent.class b/target/classes/id/iptek/utms/messaging/ApprovalCompletedEvent.class new file mode 100644 index 0000000000000000000000000000000000000000..2ddc9d1cd094dba3b1336ca39e013db59865ba4a GIT binary patch literal 2184 zcmbVOU31$+6g}(Mmg6cUcAW%MXal6hR@$hglrM*r=A+cqZ5h($fj%krPNPPaj3j5K z3_pnnI)jIqJn#efQ4D8SvSX;pI6TPvb?@18?>&3RfBtpy2Y|2esE7<^3}j8@kQZ2Z z=^Q(@=lCz|?`1m(I|BI)*LUMhftgx;e-;JI8Yr5$08?Py?bvQFmPd9!?nZW3Mv?Qv zrSrXBFART*?y>Y^f!R?0*q2eG zRb~z@nRplPsSASyTwtfxI$vCk`kNFL_7xK=c%RsZPIR~(bfm!T^J6y^)Q2Wkah26z z{v0n7*gZGU7{@0EFJ(J!)PFGWkw8V)!42#^8M=<={^T5Zr24$}?*fe>q=oo@JN12m zY?6=pmg~#M{qBJbpC;FqTS43L_8n3ojdU{`AG!?s=6MT4l|7R2VbJ-`37sy57!svY zn@}&U@20?F8nu?&C{F?+5Exftdc@5k+NqGKy*(|-`>smlrOB^hU8S8B9X_;$tssb_ zICOdm<*0x!1@4SqOk?;Y8be_##g7)vPUBSB^8yCHIesO66@JU}dE6^v3)=>EOzh&m zz?IYI9`^m%?Mgd|D`mh3doUy=@OnHou74aHNjouo;P@R+MystLIO_KrXR0D~RNQxD z)DGD{f^ghr+jF95d?VG`^D)>7xBuf2j;doeeP4zeQbqg%;2OJVS+z#K*s^M5dSsi@BfF9w*@igI(msb8{pwu(%rVb##rhq>S{uNy zt`9J0-58)`RkH(BtZHt6<=3>yz~XP7{_1y)znh6KuHyz*MA017`HE(d(KE$kM$Z(J ztez<@IjnQOn4=SD4)#^Bg@XMOD6u`-yh@S0#ZiG8)(PGLe&zH{E@onqaw(C)%?a+8 zfnTO_U*)YyiZ(c^Knm8{2|oBQ|E0ucl!^jA#%&T%A?5@Z;Nk*4Nja&|R7g4NHyCFH zluHo>Gc?S{xbxg}&KD;-m!@-Gn&@1a&bd0#d3hS=EbcHx8GMS*IId7IiNq6uC-P3j kok;t0T2+|VuVL_Ee1Qfw_+MiackvBRiadFMZ_z^aZ+(u?&Hw-a literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/messaging/ApprovalEventConsumer.class b/target/classes/id/iptek/utms/messaging/ApprovalEventConsumer.class new file mode 100644 index 0000000000000000000000000000000000000000..ecb51d25c59efcd8cb7d0607a199f8673dc26240 GIT binary patch literal 1627 zcma)6Yi}Dx6g^`*@gv)p+onl?l2S_jNWF!I0(DXdZX<%+7R61ZsGlbG#F=!xYj$Ui zBE*m4{gB`X@S_lC)`_tbs&u8@yLaY3&OLMQ{Qb`#e*svQsHLcsVCBxUDe}eON9g7Rs#2%lfh(c_FbCWU3cqMeszoJ zyn_q4NQ7R{6(}}?u);yZ1ZtDGPm|C%&XTYpkRPQX z*iyNfa2^}&xm#NskLou3f`d2kra*28VxuE)<=8Sgot6p7b7Fhl!Q0l>8HYK88+a#+ zn-1Q^EuQO1N(3_6-wOmvrD6W@s4vErhHX$UZx?VIr7X%0DyRx9PgebUr^PDBy>~cl z57pTm@-1+$bmH{SVle@92em9*2Y2zlz_pVluFJLw!b9@iRc6=-0=LG?ZFn+@>gCh@ zBJjycK(ALIVUgKJ&|yPgXlh?QjeEN)d@grAuCO!P(%Y7yw))Xx#_a1zz&(?IiCL=i zOrI#TA9TKwq3p4TLk77V_{WnsYC3_bq`3v|o|$6gl;mQL`OVljx~H~vqzQS=_X8u1 z=0MwU3FCd+hWecM`rGqy{M@pb}^?=V5Yapz^6NjZAaISc1`cI^G($!yZxwVs_m~ZV&QM}dq9gAsHenOVod;bBL zB~aihVb9_^y{C7%%wr;!^sDD9Ik e=6=J{k2q)3a_srFCScMO)+kf>Jc(pi8^FIaa<)tW literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/messaging/ApprovalEventProducer.class b/target/classes/id/iptek/utms/messaging/ApprovalEventProducer.class new file mode 100644 index 0000000000000000000000000000000000000000..1ef855a72c70da490ef8961d90b5b068583464e0 GIT binary patch literal 968 zcmb7DO>Yx15Pi;Pvq{r5KxqrK6p%<$kwxMdK#C%IKmjURCqQuac@)`FduEzem(NqMM zq4ZoTXQbR!}L!tD%Y-!xI_$GO^;+&upCfu}D)smQ>tJ5~F84dNmWu9vK~G zfiMi!X`G&jIElCw4DEq7V?Rv{ReUf!7N4{^^$Fq!+6e#k65CxtonezNSUZ6x4q-*u z`Iw1JcxYg~jAjitaFd~RRa%C6pw&zm+f(7OP$9$P)?g{Yv30F=FPxl-(%m1@?5(S} z@9>4b4r%DMfmGsc7LSBE;iHI7ngbp1XvmFp`Qor(Co*O52iH=#OE36F*oh90xD(V0 zLnMtP8$~jm^#7^G@T~P;;&b%;4d){lg?mnhr`IiJ@Q!t60&yta#M_tJqvM2U*m|ES zD`PQ~sU$smN@>fj)QTp#+%YMQDYX5Z2;ViW6*bw#9$7U3JiB~Z$aND z3P)dUe?_^udhi`UW`q8H literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/module/controller/ModuleController.class b/target/classes/id/iptek/utms/module/controller/ModuleController.class new file mode 100644 index 0000000000000000000000000000000000000000..61cf8acec3dd61b00fa28d1dd89bc96b96255e29 GIT binary patch literal 2877 zcmd5;>rxy=6#jZyvSG8ZxQKu;B*e>JAlnJKNU{Vagj|#*xQkdORhF~6?J{9@X6@;L zLdzHMX|zf(WtBdF4`q3JX4qRcT>Ryq?w;;*&UY@~>FK}!`QtAD_wiL65e&o-O<)k` z7)DIJaoprdnbc$Yi&Ma2`W3#1puH1jBW+#?7V@JKR$Zmp7c6 zX9-?)Y$YAb5|S7Fot4%G!+0Q65_Qv6a=$F(u2~ffx7PaFa)a~GP>)tYyEBqN5~B>s zhHzb@E=t06tX&})vOOlNjuhOyv0(FJw`k!!QVe5(XL*Yp%e!8+NXq zFkH})1Ql=@Qkf9OQ>Mk&=%^6K70kqNHG%i>0mG$k5Tj}G<)%sbY&y0}C5kvZ46A)Y z>Ttgm(XqBD;UBfAkXKHq(n@-?SwSX+iP^>?@#Rof=f zVZH|)O-8Pgb$+d4zl3xr+ z)gtR+kNMY#(8~EzVTr(|N}jFEhS)M)lY(EiZATf(Bqr;-#*uaIHg!LKr$gCuOu-D<=x zdfz!cnxVFjt_R}i>D>&g_rR&`lU}^+c(N)UnflTh5B@mqe|}#70vQ%g?G<&hYO9m| zv#9$2fp!KHTr)ekEU5FTYz}w#jC!3OC%ZSfW4Imd?$XU4Hgh>#=s-;=8p2lONYtjw zg5s?RLkg+e8#D9-iO^drf|Py}(JH0iBQy_yx*j*^WS+jh)(n4B z{3~rmaFfOqqBKs>IEf)lVTAr&w`k`|poZJHLtkXThhhFo14ZyLt%oACtq(z{3%E;4 z8ufwi1u&WW4a2Fo2L43k5M%TaJH*s0T>gb*0{C;J2VC|c6aIQKguLg=6tGBhlBDm` zyhIY8xcHQ&Wda%W!EBm!4AVZ?haF(EbB9P@JH)jv$PquXG{%sjSkpdqF0jywb{Z>q z=({tGRXieivX94jLb^Kc;Q?7WKR`cJ)&J})7jHL$hu!g+bIxzhoXh&3e?R;U;33w=P{BwY)h22f6}YZE zN4-ttzLO?F>o9Q>73x@Ef#iKR zN?hlyjAF&;^guf6-YRoZs`^Q48M%q{`mUc!nq4k6>xO76&JJh8SblXYIb)mgS_J4vlTJI}?s zJ}AjXtLKNoPPp$J-;%sKc{b?TBjV{oSc6xrfIvgVYe$)9YcTh<*akDxDaNwgXYF6g zWH0o7b|bncEC6A2u&2%CE~gJOPR7C>H^yJb=T$pfMmx39dd3 z%XT(zd!w|RR^7|`gJGI1ADH?J+$ogi^vJws8)`;qhUUnZuGe~DwCn6C)?Txd9QULl z9mdT((5T5yq_dTo*+Hm1WtvuC&Z@SRxA<`Jz7!*Aw<`a1vfn|NrzD6er9_#D@H?6C7Y)c>Hef)nS366YJVG3c8a=nHg8Kodn!`%KVs zDQLO~x_TyP1-D8fn*F_SwWTvbU*ao_WI4J|psDTX%B}aP7c)GOk>;&Mo?(+B+Xc7j zR}&PbFq}CDcgpY+=f&G*%JD`R#(!OopFS_%wq%Ys!Z7|DeEYxX+wU=ZUc7DW96w*c zSMXg{t?#oL>T;&;QohHg`{51506)T1WG2Qc9>o0De`KY{ctZ6PJhkF8bl;#7<9Uq9 F{{YN8m+$}p literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/module/dto/ModuleResponse.class b/target/classes/id/iptek/utms/module/dto/ModuleResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..528563ec7b6344f8dc8904a2a0347cd9f9ce6fdf GIT binary patch literal 1734 zcmb7FTT|0O6#llil#<#D1w{qCfV2ULc-L}Ka2%N_>Hxz7`jWOgG|*;ik}~6e@wPI{-t9y9fp;`J%qCQ zP(9!^w_Ms|C>BHdpjW+`z0xkV%}+X0F)jYS)2ecNM|MrNThxr^u3?*UA1GsP!=waD z1GkN6vB}+rsDCtUnvctEf)|RHbkifr2E%AL_fSntT%Tj2p*)<_t$F+^NpPQ4!Lx!BBSy{5E955$CB%&uxPd1alvlO1=g@TtV%2;BU zI_IO^v0Ssobw8=VE5lH=8;IdJT$gDbi9@dYhVKlk-sH}7MTo;ryWBgdkR|}`d~NeL z+^N}gf`lE0tTzqE2`hr}f9iD-j(&mPXRTE?EQ{NoOYSIGWtcgyxlauWUNcPgH>G*_ z1!Iv8ZG_H2l+G<(4YbP3Bkf5#$s`ppx?^sU)ot2+CK)4H(0+nxb0<)=`4gnIi4$ax zNgu%-dM5}Yecq+F=G$W$GqfY57e{_Cc#&lEg1j}z{vNHqcen9D0P(1L+s59Mugu&^-zlNcPXm{`1jjyCt2-6SkwIz1=zP$wtEy zJK|F$Ld9SRpVvG=pzL6b>R+``zzBn>5^WLW>nK91}pt_h=#f?R8u-1c)(jiiGVQ1i#;0G8SxgwM6ow% zCN;5czY{&3G}4scl~GU^Z(J>4y2k>n=u~Ru*=CJwNU1^<@ODfvG%;MDEh*&iAdeN; z1|C{i#UqAW-Ks(4E4L-=xO$SwFjPAOV>n3*aQ$O>B&( zlqX5t``i|VNI*c@M2E-xom#QiKMcGAwpAU!uXl zbB4LgL#gMnU}~mG4<4XF>K9C>ES9aY%wWN)Px>kMvRC z%YUU$;Vu=?)z`>sQD%AR6chi-k0qJsl1$8Ffe0j$<_SOxTTD3FWVNJh`5TPO7-bX0 n4DM0*0Pdrxxpctv07~@J(wC{VCdydCWAfz5^90ZE0yBRAPF+s! literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/module/repository/SystemModuleRepository.class b/target/classes/id/iptek/utms/module/repository/SystemModuleRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..8ebb56b8ec5b34bcc9d3d6732ec8acff467adf94 GIT binary patch literal 798 zcmbVK%T59@6um`o0AKi8xpko%yV8ZDCO%?<83J+S11c?JVWypQ?r8V`f69d);71ud zGejV=5Ef0_bJ~0E<9vO8d;-8F>^V>(a3(pGF^VBgF-mBpc^V38M645u+Ki|_N>D_t z9Dn?U9VioM4A?89AyWZuKMzC?9atrBs!c$X*hm%h4U5E^Hbcr8GCGLabmWJaO#%|w z?@PsRMo&U9g-w2~_^sxG!1<9k(eW{h>;6TK3n)by(sqneD;Byo#g@Pq%{wg11cgZm zRB^08pxnz9YQ78MuocAIy3OX|K(O;6m*&<^5YzVS{nFIblmSh1^Oom{d`bf5|) lsM%Ys1a-Tu*|naLuwm&<*c#InlG6p%uwZf8hMoNJ?hj@Z`jh|w literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/module/service/Module.class b/target/classes/id/iptek/utms/module/service/Module.class new file mode 100644 index 0000000000000000000000000000000000000000..2f9641f4552746a847b7aaa876f3c2a12b824338 GIT binary patch literal 277 zcmZXPO=$ zStPVFZ4y4$kmv)^8yJ2A DzfwVb literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/module/service/ModuleRegistryService.class b/target/classes/id/iptek/utms/module/service/ModuleRegistryService.class new file mode 100644 index 0000000000000000000000000000000000000000..91628e81127a83df64d5b95f62748629b1f5752d GIT binary patch literal 6631 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 literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/module/service/NotificationModule.class b/target/classes/id/iptek/utms/module/service/NotificationModule.class new file mode 100644 index 0000000000000000000000000000000000000000..a41098608a43536646ceb75b25be89863a11743b GIT binary patch literal 1228 zcma)5+fExX5Is%;xsU=4cX}a|Qj*ZJrR`gw5(-6%6hc){-j`ixF>LH;*P%jH|EZUU zULN`Z{iv$rYzixgP#<>2_RgF+b3Fe2=jSf~+jyBn0?9N|d5mC`Ve-^Iw=Lh6o^^11 z${o!xx+SF0uNji1aw~%jL%w-%RNt%Dc8=-?%`9>lOCz7h9gH(r!nH(K^E0cbJE7H4 zZqMgd$b)m?aI2}bXbZ>ILdpI9FM{=z$53dfz_UWX{p!?eD9__THG@fpay+FN!49QbM*B!&<3v)#afMg!HPhlc?{i>2;o= zGOWHU!XX55Sj4?F?&t9U4;kjJ6WFsIt%3`NtjBfKBg1N(OwG5$uv)$;MH)qhm0P9# zCugdrT+T4j5Rx~0onsyx8LoYW9oui&fiU~QVM?EfkYTfND|TFis7{sCbozy+OXZk& z139TjCErB1#4r=@aS%~%F=V2j?r_ZmBAQcjcOX9HZ#n*8 zHgkun7dU)Rm`>+z^4&1-3=8jiQi~36iBOOTJ5nm$A4Y<%M6!3yJhV;4_bNEELaNJ^ zzUXqRraE0ExztrG)6gftuteXY8GL$7BebSBM0S~W7I|h)E8oCA(;|UKWX*3NgB7y* zC;*SKN;`%pSR-qapOH6l$wK-Qxp?Y$EH!zVT0)tkOzJL@v?A~?$QCBnf8cHsSLF?B zU%}Kse*MX{gk3cND^|4*bD`q8xB5C;0yl?p25bYV$1!OL9}z>4gl+k9bk3+CJw~XC1239HW^r z`MH2kb*t}l)puX%P^x}HXgIWB#H$C~kHxbnP>e2o7bB3N=*UR!`r@Hdf#zBUDq?gV zm;nzpclSjY$w()zH@yucC->@!P$J}7cpYN^VwZBowgNvMDqK$~J`{{9CZeth{FI7) zpTmzLN<#27-uJjR2aL{)gX|7Wlx1J2zTPvU|1g?Pg6h6YqN?>06WijgDGs86EiHc3 zShfNWu-TSW;zc~%72#`(ueX98_d7h4W}hBT={<==+-d#aLL#_QU+5-cs;{oua*WH# zXfaKDQyt_azTWbK;chT+M+pV53ZCf~!>r-|xNDM{&%ZY_S#?GZB$H~`a+f0O)ZA*; zr?n2S-wM4KcrST~x@goKR-rJmivJm_jg&KK)Q}vAPMDp36~tjr>_}6{hT%?!MwEjD>UlKoV;A^f4KCI$vyn+C&(K=Q}H|ZnTIjsCERer{0fyT<0bCf@Z zj6vT@KyEH*CJS0V7Id3F$?>R`1+5$lx|0i<&w_3o3;L8kL;XyS?!q%u`SSYB->8z! z@LWP#uo7v8O^R%fd=9-5!=^@uGxtS4e&(cj+iEF(_6Yt<`YO-++DY-YO;ddR2>$DQ z{QODrw#8HYLIz(z#!Vw`BonrSiMxmWCYtX48)7~Bh92M+ll71uMYKiRcK4WeaQ%Ru S*zHqlzM*17&oF@zE&U7dKGPup literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/preference/dto/TablePreferenceProfile.class b/target/classes/id/iptek/utms/preference/dto/TablePreferenceProfile.class new file mode 100644 index 0000000000000000000000000000000000000000..007e29e57f9e9df72afba99c8458bd3127f79109 GIT binary patch literal 1966 zcmbVNT~pIg5Iwi0q?ACR0*WA11#Alv5m7LJ3XCIKl@W#qeQ-j1fk2wnd@#d*@_f(#E$?%nL3-Lqf)^Y`R8fCa3i5JOx-i;M(X8M^kg15GhBb4U5a zOO{<`Xr0$h-Cbme=kl9vNTN+bN=7?mhH1U5=v9~RE1p|%l&a0Qxy{WISIVxXY-n2s zUptkp+19phaE6Y$-aCHCkSW%^uDiBw?x?~~r;IeZ7&;GhM<=9Z%kV0uLqP`mo~s*5 zQFmN5LtHX4ZlH%D=~@8>!)mU0jri33`EZ1TJ{dQ0iyZ7~&hD~RCL*)fbg(M$Zp#?J zAmx{T^E88Ky}ce-vtVxt>j7j-1KYe8eQo_oB)$nmaE)W2xph z8CnAJ>7s7(4_;-9+kT-%9j%gPY-+YH3Lgqv++Cf>&0MP#41JB5ht(Qx*L&{nTIDs( z)+*fPHu;$QKY^yy1*x7~ld}6gzsk@X1&$<{S&hvbej;HFt=^*yW9NiTqhUy`Tb^Cw zul->eXg)=gVq~a!YE#;^WLd7`+FCWpi<86?hQWqOQyT{<%^=aQBi)MG6z1?;f+}Mk z3k<{cfwVo-)hk>HxP{=PrkVTobr za#bozKp0cB#$q((`7XrwwC^N_Q98@Mg^&yp_!vprVx%aP5JvF;-LyM% zq?jey6ukwAR5&3ye$X*?&Ub(+g}%FKGv=F^_X6LqWIlibImwdlJiSH6l7;aT^j~D( z7wDW)PhuPs6o8kg`bcvBa{dc8D-~T7*W)?<0{5MX(!PFg?z^F nh*C;_A9M;@ps@iS)3;8F?iuMPF^w5K#Uja4Bzu8ZSVsRp#Y@xF literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/preference/dto/TablePreferenceRequest.class b/target/classes/id/iptek/utms/preference/dto/TablePreferenceRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..571354b74cb2e414721db62879af751ae82692d4 GIT binary patch literal 2357 zcmbVNZF3V<6n^d|X2bFr`WhNH8MPGX0<*oNaP#mn6G!_og%b zPkxXY8E5*zAK;I2eD3ZxFC<{6A2#>ibI*Cs%YDwtKmQ*84d4M9IT%Q!ku;IQoIvrF zeP~-<+ugUeWy|y10(0w*>!=NZM5Vf$MF!b4awg8f6u9HGEvK(!#~P?!VD)|ZtMsMY zl2%)J)=PV@D_;y{+w!-843t2Ar1!l%5-2rCUU!u5xchbOXWm2sMS=N4Cvb?g<#h)= zH(-#F{y;fhtLX%)UScjK6AM@r$S5zu5NK4IXNa#}oeoDkD4X~IAJV~r9UN?VZ7Fc? zj1C$a?;{h-xXAjlEOwV^Jv$rPShPRxy^<|eul^*kv^{W@)04YV?H;eSeR4? z4(o9DO!N>aPhzqpF>dh(sSdpM3){DQQc0ga?*D%RGs~otx;k0=A?(BhELLXlHEh;~ zz=d(|W9$&#WY@)46rP}snoHoulyGLq0=XS;;J4%xr%N_0&u;%)y4T72lkrmA^gN}R z*N=)9WN=^L;>3ukPfmb>K$>5^Foy>@Y~Y(TzBTa>n*x_dlf;i2z>0Ewdr}CLBIYk5{82^Hq@>0NCqvR1JU!xO}4sr5R41rX83sw zc82$fK{R|Jv*EhZ46T^UFXiAUu`ym_`CClp44))VvyN7GW%iM5s#(E!8+FT;sRO(=W8*H43%AAZlxG zkgnZ)gZyiX61X0#Ylk(G=z?G zEzDvGi87B&s!R*URu*xnxJh~5Adt?O&B)*L{EPXNdmZ&{uG&euc1$_`;MSOmHi{!1 zM|+-sAh%|iUjTkTmH9F(I?>+Oxf)zEwY6g`z01BF=?sgN!DnIIx*#dRgHG%mQ%Z!O zR-96*_9xQsODGj1&fvsi#0kMle~#o7Z2mN`D~!v)omoU$j9-Ut^n)bvMW}lhUvmAL uR+_9F13Zlg8u4N!)VvbjO;hqK-pBC}8YUm$)5SUx5m|DjQ)c*2 z{Qxt~Fq04bfc~gX_aw!ZaY!@pL8rUD-F)58#lVaP9WH&T>fp{wd;dO** zOW%COH(asWm#y&~VXp@MTgMd)m4Wha;ypvHJ%C=5f#Ypj3hcO!2~08^-*G~R+^zU- z)b&D&G|`WwWJ{b02N#s%g~cBl-K+y=!hqA zt6n&yk#DH3B_{4qi^+$+FGCsdUQ)-fgnJavP_tXZy=?WHb%H^ouR>pSVhl@VJixCS zmUTSD3d77`LIjZ)N1T))g(B8&?4vV$%vRFzcKmH&CW_CvXS*VtZTtRq)Jr)T&%Bh) z86KyGuZSRzMA!*v+WA4&WX0uSm=&Zx-i$iQq7TiB{$uJb&l5rHQiK|wFkBhrd`J!& zo-v$1T$LUvNf`4qS93H*@~EjXPP>{KvS7GK`y4LOnZ_vXHIfx^nKWnVJ&%enf*#WEtbB`+AJ1qNvAMc{FLS$F}P3 z5@8u6spQE7Bmk))4R8)sn%&n(S%o!Ef8|m$_HY9DK-=6%B#CD&jigFL`T7Cor-8qY zWIlr%1ftLv=&#Z!8FPF1;cNEOiB7)|C78HL0jLyom5Ii<8Nq(B~(m3 zGOEyg8d27`aunl>gp|WALY%@+w5qhSPO3+7Qo!xl%^mzq|8qo!gj&PIN@(>9X(+la Y61hsai~F#sj*m!MCh225MGL3@19pB3g8%>k literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/preference/dto/UserUiPreferencesResponse.class b/target/classes/id/iptek/utms/preference/dto/UserUiPreferencesResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..f0179013b81507266d9be573f7d7bae3641094cd GIT binary patch literal 2094 zcmbVN+iuf95IvhFbsAjK3zT~aP)d>l=6-Xzwp=0wsM^v85JGOUw5Ea@8(}+X)%4>|@tqhhQ|HX{pdo3l37IyT^PC_^ z3z7lxX19|xde=zl{4ks~y5}n@Ze*;>J)t_M1nIAB!=YL@M8;J&RioRN1e^1l56szJ4 zrszaQ354Eh=NX(OsRoVjCTTZK`=JaeY7vrX&S8{Bc$uzCB)d$jHZiK3B*$0UW_Dr6 zUX28Ml%{j#ALa*vFaKqJ3{?Utk?t(5nz4L!W)nyDvY&``wo}hz2InaNonDU70d&}h z3yD*UPCBJr^*fAS3FQ)xag{ETQ4!j>M8;_+!~f$Xi%VpdAwsXvszdH>fw%##(%pW7 YxP>e=%Qf7FMI$W z%6K*=j1Z#PMboU``s+DgzkYpxd;-7~?CYQrs0oV-Pw^oQ)X1lvC;Cxz4RNjBndt{hT}DMkk# zOVaN>HUknU^n_#GjKeeQ7J=i_MuZQPu&HU*Z;fhVp&@)_U?*A llwv8bMbgGpE~C5&TgVA_&SYG!z;?VG1GN~r1G^D=?+0QeE%pEa literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/preference/service/UserPreferenceService.class b/target/classes/id/iptek/utms/preference/service/UserPreferenceService.class new file mode 100644 index 0000000000000000000000000000000000000000..608c122b44d7ee41d54e03daeeed2dee28ae62e0 GIT binary patch literal 12315 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 M`nCL4evg{}28lMWAOHXW literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/tenant/Tenant.class b/target/classes/id/iptek/utms/tenant/Tenant.class new file mode 100644 index 0000000000000000000000000000000000000000..ab3a0848fcbff5a05d7d580d07fe0133383e57d0 GIT binary patch literal 2123 zcma)-TXWk)7>3{VAwCI`*tOf>gb-+{<3h#xlu`)k0aLehXzL5#WE5MIZX-)UT7~I< z;sR#Kz;MA2;72jMt5vXx)g~AATKc~4*>}Ig`tN`K{0qRBxK~9E`4S2iidYa>dgdHC zw&(c!_V;_w+>sJkxFLP1ZVKc(n}Z6pQ-(A14bV zosp79u7DU;@hTQeShBE;y1;U>jgk}B?)i~o5b=KSlR&ex*`G6R>1{7qXrd)h8HKK+ z-0?PJ8ZYp@WMKub3FPFMXEIK)lAitKN$*xdTeYx;*9FQ(bB}&?u(ke63Qb+beODzZJgMF^ z*=&+DV?F;UBWLx#7fkko1N*;RkXIwmbdjcBIHAs#MxaEe2T4uJGTKgCjZHcfuxQ$u zwn;HE@){je;;FjL0sG^=tDXkqhfYYCV&S+y1E%{o)3Dj?bzq5JMnHwXP81Y6Lv6J9 zYDPjUWz6>cmiDVY3gU3&-jTXEHM8k;JpnIjhTCjE^_)p+XZA``IGw`id9-WVC1L}Y zIp^Sv=k#E(Ti)c8p8dDDUgjsK$02>+vH#Zh;HE+rS2biSmJ8)tro#xyodL>3VeVM3DjVlPuf=O{)UUc@J9|BruPLJGr`3)_@mVNF;{_4 z@M#99eHzp{6ZBaoX#F&31WEFJ=*G-pe@uhK#kLi*3Sgp;NM9;Nw!v| zyKSL+?HCuQr8C*K5>m@}q}kS$FnxKGaYf#;tm*$t@D;w!!ne~EJ_>DRIH`tj|-)4KC7jFhE#n literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/tenant/TenantContext.class b/target/classes/id/iptek/utms/tenant/TenantContext.class new file mode 100644 index 0000000000000000000000000000000000000000..d8ee42a50dbc7b840068759e9edc625185a9ad4e GIT binary patch literal 1209 zcmah|TTc@~7(LU{ZZFHlLap4ST-3HGRZ+ZvBuX$L7BsdY!M9;MmVxaqZl{F5;=k~c zKq86y?`Ry3FH}u1JijcaGjT}yyL1oNlsxLQ#!rV=fnq(hvKi9xULHj_e9e@5ZX_aAz76T zhpV(ecwd2-bagR5J5+1#irV4ck&Q>b)z%WRl*Wz7U zdZOL|_fqbyy>-4HDvw#V;2x=1vh1)=Q&qR^S>n0$?FLuFW}MTd#2sql2(>@Mh;CzA z3v`O0h&X*|4k%Kh$ULo#)>!En?4u8vrLP_tfM$khZqiN@5D2$0M{5#gOcO#AEYPa| zOMHS+IzsPxjx@2DT^tGACUL-B`o$O>G~GPz5s9Xo^2ta5(H&v%J8~Z=N}WMptr_Si z{QXn>TSWcn~S8ztZv6 M6(fpA1c~7BA5KB~5C8xG literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/tenant/TenantFilter.class b/target/classes/id/iptek/utms/tenant/TenantFilter.class new file mode 100644 index 0000000000000000000000000000000000000000..a807be5874cbfccae4530cbe230647010bdacdc2 GIT binary patch literal 1713 zcma)6+j1L45ItjEMwVpkD1@LS#!g}!U*asFaI+J@krk{e$4=QQ6~zlRmd4gb(yrJY z+2M`v;0Jil3nVq|MB-PzXG_AY8E3%NF?(} zAuUkygMBCL1+KTh7pSg2^nKRzUO+b`Ai@& zH(!^?2wZoYj@vW3?etAIbd2_tXPjCj;#(PH1#(|4#k!?xGlx9JB;Lql91{Y!o~X78 zjB-L99CWmCTE_I8buK%xVvvKt=)N}VS~YbbFgv&LL>(xnqr81*$5I|9-TAJ9oV!t`|(Z=)!2C6BAPCUE05=*qrl^ixA@p*AtNYE$6Sd5P6? zcn4Dw(|KIS4S}m?F~?MuTgvqWGEM)8@HOl0G(D$TS$Xw&*z>(mugupisF^(8wRKN5 zI$Dvxv07zwb+h)(dS!L3vMo>;=8i_h7#bF|t~z}!kSzO6it!EC)0_S7t`2Hyw?j=~ z!*8fgT?MY)50pvMazlZe8?PZT#<{}E9AU1K)xc97fp8f6D_Ucigki%08$)!RV4hTw z{J4c}DP<6Mzkuzm(^VJ_5Iqzk$pku%C9v>6t_|{H8|_7pY|sOfLsw5G%F^r@)o4Fb zy@+gyPX*@wJI6Sf1k=3u?^CF?0{<{BWVZ6Op?k(<-6iP?o94pFDRIZ9BT(G#d&ceRx*NLeq*c%Jjf&E- zY@Ro>LVDJ|`L3s(vfpLPYtIng;e9453*9oPm>e9uwI2P!!#0Vac4 z+@hT0ZUm$J(-LW}^VB$$f-<%6GsLsVWPz(a1}3n`H6I(`HkP+Gdw0QdkCKGs$n?EAAoT4nIetQ;WHv=1Nfv&Ag zMC&Yb1hdZK3NG6LhDpp|iWsJOKE>eE_<;CqYIljm+P%lqB=@C3lJ@I;T3HY!{_aN6 zO85{TG0Ml(P9gCZu1OSLVhl+MDUp}>L~_CjiDij<+lFw}egBXlwq#nST4o5(q)NT$ zL9d3mjs$duv{^AsM~RxzQ2VZ-gvBjoZ2Dx?+!4~^O0*312}_yk*&4U_o{$Vz^G|q< zOT`UGNY`{pyDW^APN7U3=g_ZVK*w7cWSDx5zpQO3@l-J+_Jj&T$W<85&!ouD0XOR0 z+B1sE%RZywJj1XDN3|8DQ53Fg+ExN17}YSQBZ&(PSKb&SA=I!AC3hIE&vdxT*179u zI-M7Go`|xdoGxKp!-S5@m}D5}j*dYy-MbWS&By#v52hIO0NVh|1cq=;$8}5-gTUUY zw?R&KhS1D5m47wUz(}SCGYma}-CQMsG-fr->9~n`g6%XF^5zP)&b{KHf?-%X#ajmi z&4kbJpTOIAN5i{1Zs9$Ki!G#GArI<889|uYDmN{PvtqaE=$_{^D4dpPF;Hn%Zsu&h z>)4hnGO3aW;rlv1@E{zZf?4rVtOPzh=0`dfkzweom~JP)!>6R2DltqSM|E~NdxDXy zT@eiZdD9XP8~fBGo1T4!!Mt7O^%9q+w{IRsRMm7D=JKzp{H{);pa<53s@j$IO_7Cx z7JxZw6Y9%4gL@ou3&^TcjJ3c8z-Tx)JH85l;p8mT!lyecUwUaqZi$GkLE3rZ%h6J`657zVfsz97Ti1)lg)Z?1rETd|OPzR?C)Y8@WQ8!4NOn4O#YDpQ`6V zF!H);VcrXZA-U0@vAQoxrfX7d-LWiNaX$zT&eNU+n#NMt>X{>qti4ZNL&KZ$oIO>z zY)fJAwo#)XD?*ZV^nlkJBK#ddgOpy55Isu-viEk-Ux@yrSR#6xcK3;T{CN5WSbFv+ zXwQ9_Wm?Z6OzQ|D7)39}@Ug%1rSS>w(2n$cs|0pp9Y4vyaDBxAonr=%7rf-UmBg>9dP;UzWi$2dU{T*fdaF+~t9;2OzJ zBZnI#IgK*WI#RG{ID1JiBj)AMPTIi=0muBa2Uzt1bV&PC$~KA`X7CxIFpS5@V~tK8 zlH?d7|6oDGB@IL0Xej)R5nBHu-Oq^|Bf5c2AI82gnFk)wm!mI)lD}@_3!?qB|B~n< Ka{oAoul@tVvy zRvaqCVYK!hW_D&izurFp;1c>Cv>2Qyfhs3C!;vdLys8@DkHrA?Z;)OMhoaxHZWI3}S1H>qMP65?s%og`DQcUHUO zl>94@fQlj^o)NzZarV-9O(H7~yEAjWM<_wq?=3z0cO2?MI7JYlD2cfNn$31O#HC{+4zpNgxS7;yMTRTVJK?T$R#ZS+9@ovpGAa>6?lLT1 zRK0zMdi9b$t&wuRr5x%+p)EbJGaMXb-?|kFJTchORl|nNLgkVX7H6s1Ey)=GdwKfAql=Vv574M z-&lBxZJIcv!V^(dU)b$9Zvy9sRGoOd%v0k>r+>1HO^uRRo^sD|MObMo)gSte^G(f2 z#tgfZV^;n0mx;^Cw%JFYBDc8fcBGDYVQqV!2wE-=LlGKi(^x!LlZ(J6y+@}lTBCcj{tB$V_A88^>6Am2{#QsBWRu4N=IB(T zQ#sK_9cy$VyBL+>ecDgcP^5*3iu+_5!SAK;<@zr$)_%uA4zF<)_ziFWg_}Q-M%Gmc z1-e|MP)DxHXRgYyu}(*KY)e!7@vgrv}i`mql+kp)i@0b^$#q6 zU%LGYcOt(;e)`YQ8bRdoIjuQ7iXkIugU*lXvuNKUgeUkC8~BzaCP}`-GwfpNKLvmi AWB>pF literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/workflow/controller/ApprovalWorkflowController.class b/target/classes/id/iptek/utms/workflow/controller/ApprovalWorkflowController.class new file mode 100644 index 0000000000000000000000000000000000000000..e0497a5c3e9003d20f53058121779010c62f3b0f GIT binary patch literal 4929 zcmdT{?N$><7=9)O0yKbP{QyPLRw1aYA4tU@HU?BQ2n`AvzNTw=y_nNxmY&OuBhWl%6b)xwV9!8oz7;KKji4=xj}R3YGqRdda^$L3 zb_%w(Zd$9$d2?OMm_^4j^Lc4$iBicj*Yx~?KbNdb33LWO%t>p_$Vh=JsRj~lZ^bro z{-1HBSVt6{*h{emY1?{ErloD>*Q6y7+afk&T2eDETq$bPwM*kI=n^;_=rv@?7iDQX zLm5lzj?A{l z9Esp)6umemFjBj5psw}?mIT^8AZZVl!1n`Nn5l@$I;NI%Q3n?Ee~MGPRo!xQcelKB zv=zrGX}9@M#^6A(a4YLIvNxsLC9`PDcpLg~B7&1qe1cB}dTSx-C4;s%Xi|5zK*(Gb zn5;u+6?qjtca%k&pBCs2w5!-$cRg1by$1tP#4soj@dpL|^0zKKMqYdHU}_@16O#ej zM8+}9Vn9&Jv~rqVQhu;(=>@4&Agh_OWjGrQB06cJ?5xP5V`OyoCYy^_QX;#$|!18u9_#0#iF%%y;)H8~P(teo(k{rzkQm zkZtI^Z-|4frfL|t@z}gTIB8~?nmSWPQQj*TmZWuGU&?c&D`jT%{Jd@%>f0X-J1Yip zjyLj#U8P2X+dD0t6*D`lE5~r8MVZZB@Ctm}5cqqD+AWbs)go4qJ}x#w*;o&O4rQIq zG@-KdZZnEN>zt7*>Q31r|3ChZG>q+pJc#xnTgAu8T6en4I4;O}!#3zHiDJ=o+@Pjq zFW1ck>vBn3GKyJEudLN(P1~7f5>ll@poxXGOeR{iib<~46;(+pdM3k)rAN>mGYij>>B+?X$^KZMY4veQ+RV#={^`Ws$#j2= zsk#=~{f$yBx}J)CsjiWAL2L7>%PmjV{#^0*cjU*3I%upw>8MLBBI5$%gNQ+ASh9$Fmc%uWYl$FbR zIqxvRs?rd;d|se!&b8Z3LmgX=2l-)P+m&ocT~E|4tX^KI=c%l4&gPAd+PuxLu2nXR zSvzCQsN5lWxl9?RX0PkH9IugUmo!tit16+rc~GA7Pt1xK5>eoJnVN(>uWVbAx+N`D z)o_BB#1Kgep-bIg+3Qj_M!uU!ToXn(^BKP$sY@~6orAwZ44!$3$l%$RX#bf*Azb0J z3t>Kw^4W`4976|=W0WI(-fy^yIKL>s-A1@QwNVIT?6-zEtR|JYu3;R_?yg@^oZ7W# z@E5dq?c4JQLa(r&ACWU>U!nUq9RG>4o&p{2%2U{n(>RDzRh#!zHt)f8e9j#x5eeMb z-26I+5^PCV+yo~1KE&ypTzqR0!p3b(slJUnxXZ~_h=Y8k*rwUqLtLyr?y)HhG0gZF z*7ksSOjD?MsI}_(TBrwT)r)nYCJA+dP?HUyF5*kys!!PStooEa6#~6@1{Dq} zT4>cpzMil>WqZcvIj&x@>JkNcj++UfW?fG`r_3nXmLDUxV|T@dK_#A3dI+noJG8sWM0yTm- z_p&NxFw5l-owOKone2dqQ0d>v!IIk`E#ei+G2gYDd#1Nz?Aa7q`$tdN!E|RxR@}N_ zR}IhNq&?=KV>GEi@^7^m5*wy}=r*%cMmwnw~DCR?R_E7a|tx~qMK_%|pFHpNPwitBFMtDA2uuFTN&8#2#VWHm>hCPkMl zv^vHw3qY0@URc_4VQI60M2fF7N7DHmW-0Fpc~fNhPw61>8N*i?`A8mJHF}4|nMC*r zXb1@W0$HA+B-zX*#;(9PK5z~Hlp<;ug&dxd_ne*(ZOjJ_r5%JM$Z}*QI+AB*$1iaI z6aD=klZ*vEg%=tIV9mc3JOOS&f}196yZ#Vg%i6(<{Sw=Ot-)& aNe&{6kxkJM)8NBMG-|i|*T~a(0EJ(g2_g6Z literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/workflow/domain/ApprovalHistory.class b/target/classes/id/iptek/utms/workflow/domain/ApprovalHistory.class new file mode 100644 index 0000000000000000000000000000000000000000..e91c32502c1ba471d2d3591bcc7d0f1ca34208de GIT binary patch literal 2810 zcmb7`*-{%v6oyX=2>}*17K@E>uhgO^}7%MsWCp{h6+R3kMoVbrb6g2P)S zRY|2P7kPj@R4RW@H&_xwAzv_~GkwlCXCMCk@9%$z=qXhPC`CP4O6Mp;y^N;Zy5lxO zaqL84BXDZU7fxL@xGS9(JP>O#bi-3dy-!@}hR+!F6sN2G)JOeU8pzQ#$}yU4N4!)1 z@qwq_one-nO<$dG@1+}r$_Fdu))@_zf959~FLgacK1ahe0-t#;bd_W@*Ck{*){hxuIl4u+84ZG={M|tKk~ajSym8y(^3d4{ zeODgF>UVN9q17`|g(6@yz8xWP4N-Lici$7s63@AI24}Z{IOcxHou=>uBt^)Ya7y(h z6eICyLrCHCP}Hm3iv-l}C8}+5d0J7grPjzCh_H5andZ7ateiHrX1cum_D4p2s;S-M zUJts27!?v<@k+UJMR`#}YD<%m0b8p*UDZArc^*f^ml4)v)G)zlFVPhdR3{T(rd6$z zy*rid($)r}yNR0TE?tkSwHKY2Yp51`yDAQJ`^L?X4#lr%tn`YiIcB|IivZm?d@b>Y>j#ANQ|6?);bhu1!!)WX~+a73B$$C z986b%+!W!Fs=whrCM?8AAW9gVNBOw;fkA3$KL}@GE_4g9QN~Ug`_?!|t@o{AYSlid z+CT?(ROHvhx~m6Zq+{{T>H1&R!e-B+zz-8U+#HwVWq}MqZ@wcbt&6)NB$r{k4NM-C zm4E0Qx#)F?b>i^%TgidTNIjHtSVu56GV)kcvTi45)Er^Z3r`D+M$o-Ik?3+fIRK6X zr@{R6n&4Pq^KNk iifKyICgd#)$IMT}YV?}k;1nC-2igv3hblOMwEI7V*<=L( literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/workflow/domain/ApprovalRequest.class b/target/classes/id/iptek/utms/workflow/domain/ApprovalRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..bc054f6ede88f25607c4dfa7e492380da9d8136d GIT binary patch literal 3611 zcmb7GYg-#d6h0FODX`%lgo1#jv`Gpq^=_e6%FSq?7$9n^jY~3wZIa!%7b^csedMW+ zAN&FSC?DV13%TrK(hp?z%sJQwQf|- zs`_gmX0u*5^b4hWWGR+yGKy~JHg}I1t!CfGZ@r&}XgEeA5{-&OSGse~@tFL8ABQ4x@-#Vl?79+m>3DPfjx1&PjJA zTB3W5B!io}Z4~+OWt}q`YdYx2GE}YXSSKY)3F`shI#UwIJdkKvY{%-#WmQ+4y;X^1 zu@|o?6>glE+|ZO7kf(Dt9=*o%s;Qn<`KG4npib2_TyF}eRuschJ{ZAY_fZd5oi>O422xf@Ld;V|qwI?iPd_81NMZ(hLt#iL8! zqdGG54xt76n$g}Zy#!*1v#Q1q?Aj?ej>TQXBkDz^npX@}$lhkJb*=(b?y@|*3>P;k zb1Q>18cTXv+ejovGv2Faw2P2;9m`hr+NoZVuip!J!@$e6B;ACM>{N`GcF3S6LlM9L zn81+N&OMeai-PNt@fgna4?3Nl!v_#?6Elh`Cy45Hz*LS})xud4b(RdQijnZ#W zl*+?lpIhg8=}<9H2^KOOB*3ASgoNkX%z=nN}~tK2pKN}BMHq1&aR9+E9zRwtNw(mDyXG} z#|w{KK|IM!Xvj|80We`+ngzw1Rs~Vym>eeh^AJWRVdh0duSWM-gCxf2L#*%PFCsd$ zw?BgQ1MGZg(ns|1bNU3|=*U6sr@j`AIjCKx&rEs*Ukp1>I|*qW-y0lb{0R({G5Sf2Y_L#nZii(%2PErF)!p z_BV%(E&R?=48LR43(f|?PaNlrQ<4&3V+QLswBlyKqaE7C%4mN#gT?N?YCeNEXApv88e>5f4Ap+NWBfo3}b9fSg<+kxgg0zIR{ zkgLVpffhRgeM7krps9ACdmVv}LV;%6fl?iTjzc+GX$M;F2y{Yu>T&W&RFJ6MzVw4D znr~#Mn@Dj7Nz|1{EjJ6rms{oEVmHblj8JQKeMjGi!7p|T?=}k$eklO|JRJUhxA1Pa z@!(Sd_!r^u>2BfOcICk@2jG98AH&FrcMI?KI}d&|0RNH-VenJk!n>`~gTEbs|0x{4 zsjvNi=D5AqgP#k)E8*O)bQ`_ftUY+S4L(Ar$k+%Ko%xhP=Pg0zXonjwh!yFK%Ftpi Oos(+PE2=;PsrnzxlEVoA literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/workflow/domain/ApprovalStatus.class b/target/classes/id/iptek/utms/workflow/domain/ApprovalStatus.class new file mode 100644 index 0000000000000000000000000000000000000000..80d5205806b7732f7dfc1dc72614c4be673b158b GIT binary patch literal 1331 zcmb7D?@!ZE6g_X<)~$9j#}8$KB2L)=sZ&sNgD_zj%!GyFlEqJDD^TcK(sl6FKS@U- zM5E!ef0XgQw;4y&4_ngSo%`-R_ukX{e*FCM9l#QnWCR#yt-59%`{t3>@*7R<)b)<` zZ1+^FyA8u~w8HVR=bjk0?i+rq83jX7MF?Sr(AsukeTQML)DJ_BQ$$omIfYyKtp;^L;w^kN8<(k1B;B3GD*KwCpXpsu2;7l z!)A!;*1kg<>k+S%&Qxfx@-@4w?n+M~{tfc|O|hJ#;=0@NYUaAdl^MKxN#^*9tYqoa zBa53c(Z&U?C1XqvyFuS&S=uIjJNcX#qic1LN{Hu&UICq*sG&$a*!@ zCPk~^Hex#O`s$Psyuf0YqSmFzeIAqKU^@{Cqy=vDCQ|K=Ymv0Ks^e!0so|(e*o-_2!&6g4wK%yT>4TI1a7TOVSZ0GE? zWxr`)*6MY~J~ym_XL!EL(7(UAyOG=7W?0O>i{?rK!?@Lt5e=jA*lf4TyiWGJ!RZn) zrr|cm8DeIYVI=>vac+?BrllVq<~D*u-qSFFJ7lTDfAP8NG3Z@ddY}{n%9Ms_++|3( z9@e}R@viCG(aGznYa>^@b+&Qo<&X8)6E_jY9PF5K19Qc%z z8ZViyd2I1DA#8HZv;~EBf)vgS$20UgcU+SW78S1Ns-!j>xwXv&cMOkLONQlh61x{E zwquBkqP-`iMEo`PDyP>;ZpqE!MO{kv=GTs1F$~ysX`W&APyvD=6M4i-+Z5}z<=2GN z6h@8wPloX?%S;Uj-?9wym|;4S(9}-KfR}9*aV?B!*$F}#efXyEYbp-XL8?`sPML{3 zMT&<0^%aT-K^^z5+f`Dyl{W?7^=rr6DJnx#dAnj*CBrf0I{et{otl)Zg)U2~K+Z_3 zPPmt&Fr?<@$*#7NWta+UA}7uxsv?!Q?Aoz?rZ=-qyd?3$YGqg^Xm{PzC~yXi0MfRw zvd0^CwbeGmFo~_V7WM5m!Dd+}0-Tx?2`QZNw!RS1^5wJ3~$)K3;{ z->L8|Q})Q{<=Ex*MOiV|^^F?w6vKzt6ui6v^1(2n$dTMqN3dG&j3O6?@D#G4Ln;sQ zwsWGNn$#MC#*J=VPA#46rIe76M&pW<)tbUlu|?ICqk~~wZP6getY)67h3tl@NRxv` zBVfwCaLYz`{gmZYaB-h*fEdl8IQ`N!K=bGUt>mTnK0RmYFD8e7_&r0bhxGRW$A_51 z{1LKvMC%0?A7N3hVsglb(j)ZL)$USbm&W?l-Q~tEU1Z^In)Dvi`xB0j@d;t*dX)S@ zdZCM&em^DKo{|)!rB?bwQ#{lUT<#?#P8ty))C&QQcDgu=51Wn!uTEmM7&}1j*?v0>zY(#)& zIzgEmL7Ui$01b75W^V*-<7@OZlO_|>mwogWsmpY!Xb%M>nSL36l`8qBiZe%F%u z6uzZZ{y+ArEkpch2R?=a%0vu>U~OjILVS-`E{;GSwi3 zAtgl+&9IU$UzWa9=wwysgF1$AozQEhUt1Pc&M5mtYZW>sc|IPbV%ykD?ml+ z{7~89mMj%MF-+zo{;+GEp28MGJW!D>+b)0KYE-zlX;vK4l`9L&bhb>-R{PKymo=LT zox6NPX=K*8tcmJJ(=!`fa*uEZ@@Mq9JwU4rgHa6;ZA_dVitrOdW<#_*i?7(K{E;)q zYDUcxb^K^%`(6tned(Fapg2E;hYbDg(JMtJE6bqKuP2+tYzA|9tl^1{c|2tpI~h-} zC-%?yJ7)y4-6+a$my>hKUo+=V;JSVi-L?l+Gd$j9I$W zF}f8oy1(>h&?={HIN2oVHNY+Ua+`K*>LDVVE&c*4P8~oiP9LCWpIkAFhT)))F2mGLACxae@gbe-kJdB@_ zJ^*~bQ2Gcah(xh3kX8Li6{ikyK}<+b(RhGKDnQ9fFxr4N-N%v)+9^qi;!kMj z2}*>BjJk5vk7#nwMUvyl6IToc6v_6|Dmd8SSf|=P`fos-%AUpy3_K%WhAMoHm(+ma Ee?+K!egFUf literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/workflow/dto/ApprovalRequestSummary.class b/target/classes/id/iptek/utms/workflow/dto/ApprovalRequestSummary.class new file mode 100644 index 0000000000000000000000000000000000000000..6ed8a42465dfed64409b17d1cdae11bb86250ed1 GIT binary patch literal 3217 zcmb_dTT|Oc6#f>BZER7%+-ed+XhLJeMWJcarlM&HG(oL%DF)gmy$O2*3by1*G8y`t z`q0eO(-|K6(zpJG{+3S9?#jkY1miG$Se@1GIcN9#cK17f|9ku=fKRZWK@WOUNNDIo zl40nDeP|n1+ubuZdD-(T49OM8b;7$0y@k1*ex%W#LPkRt8pEH~G&E9)zVvt!Dd23_T7d9V*I&8lh7)wzsWyQzU*%!ytwj z(jj+kH?+tvr~KrGN}=z#d(tzfVMKUpJ`cQxU*=m!buLnjX&4t1nbw3=5yNk5$cy2D zntj0i?ST8PU6TQqG+Y)lsk(hs^=#>VMZ+|%67nFl!$!ccR6Gl(SF;_r4YefwpiZ6x-g)_H*<}sObuM;oQzpvo~+@|#V zcCf$ZRXD@#3#GS2wvRO2!N){2ab#DCosEm+>e%Vy-50zZnsZMX{wk>PlPlKY7hR?Q zZRCxvhh?dCPn~GY(A*9~LUBA;bX@+ZQQPJImc3gg)^kO#Y*%+|-x2@OU?SXih{anM zIB$BA4|%xnRi4;BJ&ln2lxVaNa}A^rSPWwk!SZc(5YN2R&o-K(!B)1DBVo8ngKxy1 zSUB3%DFW%um!uTjcLYC|;^k*a$PQh&mbeYw^Sm$!eY>vE2WdQJaGi?T(jc>?Btl}^ zKUT#&B_(DgAEuHF^6Pl@X2&Zx+b`Q}zuQ5QA8G^ChN+EGo1ivHZHn3qwOMKfY72u& ze3!xZ_#uTKHT(nuGkv1je#4cdD9$5DBqzHqlnk#rkIiupy#sD2#|_)9RC#c{=y?Z? zx_R#7hyn#C>pUp?R0KS~Gi0r52SMjR^yHp(q>WkqlOG-_45RA&KNvo{T-#=uDLS+UWSehdTnjk>Sn)09huZ#P>whP^Ddnq<}p_||YzMu=)gD+7eE95~5pOQT#dP?$?;3>IN zVyC1|37wKTC2|ib(^W$A3f617lK* Ak^lez literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/workflow/dto/ApprovalResponse.class b/target/classes/id/iptek/utms/workflow/dto/ApprovalResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..81893f136d93ab9bf382a74c595e539186ab8e08 GIT binary patch literal 2376 zcmb7GTT|Oc6#mxuh7kojm_kZPnW>gfy*{Q><^ot|CEMhSv359_1dJ?HFqF5emd``_t50KUd<2^mZjkTsA)o?-Td zbK+PX$3L`od0R-AA;0eVUib~eM7^;;i6SNoC>fZ7!LZ`Fme&jUvDFW|f%Q_z<6k=B zrR9dg+UWJ9IB`0=Jm?8O;0zg$+|9Q16aCQZSkIr^+fC*2Edw){WiTWUL|?Y~v)_7T zP>FOkoV`$b{-M^c7`UdiOG9njRZgk~=J7TG4nimF2MlYii{Oc_o9&$-8=ISYh(M+%zs&V+bJrikR;`Pprujp{-?X3A#JsTI|Ol?!@ z`#sH(2cF76^-Z3wDSlC@hy0!06e0{l>GY!bf+C2_P0wu(>7&Rt+SqP0;t!o9D9}u_ z{0u{Z)-Ma>=a-d_#Qtn+&G&jvhRmpms?RfVk``k2Sa!auaX({ z{1b7^tw?dl@!bv&ZncCs?)RE0U3Kdy(cDX+gq z(oskgS=1gPAJ>6wEsVOq;6%l+DHL0CQ$FH^72f%@45j ziZnA=rQbYx0a>om?@r_a7VgrHd}&1LzNRrLoHaF}F`>T{uB`r2$a4BiVarogz+#@v zL53g`&;$ofV9=Ze$~09!qK6gmdn7dk^AtCLKk4W^G9yMAAuH4D{5Zw@1K^Lb%p3S5 zPTM+36;{z)ImL$;+0REhqr?>PDLx|rD$E?C4QMlk&tps$NflDg{0oK40?Nf68D(jh zkEHSRc*fUKjmzU1&*O^}*mL6bQtI u5pyEmM68K86ES9;bmuA9?}&3n`j2!U-(mxs*upkibf*~g2v6`M7XJq;Q20jx literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/workflow/dto/CreateApprovalRequest.class b/target/classes/id/iptek/utms/workflow/dto/CreateApprovalRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..daadbe0ec1a3f2bf5d513a71b7c725672892afb1 GIT binary patch literal 2453 zcmbtWT~pge6g>+zHjZ43fwZ{!XcE#I6QV$ye#A5>kdinxv;!F)+9zRegJPp~r8SxS zr#>_@Wjez{e?Wg!r)MSE$iXEf@r<;)clVyX=k7f#|MTzh-vGYFRt_T=%^+nVjWL13 zbLY^p+m83l-j)sJHwDI4T+h|_1xCyByW_}WJcFEt3$O(4xJ}#bXxXy6`XI1hD8Kb< zTfMNGTG{KqbhKRUbbNK_w72DNT^VQr%a?)b`VIN?Wk(7W>nA(zXy5goRm~nBSeP&y zv2r_a>}O*CR}tJ9TB62{b<;aIc(*J1&FsSZedT0;wqR6Lr^cg;99ru4o`*2`~S160SR)cdOWljA!FQcIsW*9SAwpBhNZba_bqgo3IG z%p|!d&P=F5b;MKD#7?N64cBli4VBq#BPv!>Ps4>ZrL;NFPQ)O{VntxFCoXf2)!v}i ztj6l1DWRwd%8pN-Pl3$*wCqO(XGc-Ty9$i&^#T!il_c(Z zhpHv*XyIeWYqn)DS68al?NraKgxJwgcqoI0&+AC}NtgAu69h>^TwlK=V2A8JAo+go zt(xaaKZKM)22TWTo^T$5(&LapUEs>uOL<-qVJz{g8=*2%yzO|;vVVi4F$m1Dy~!sF zF@K~xrw zkg41{LcX$ighFK|b%d!`oE^a}zD<Z>!rfQcngh;O7LIZC|NPBinWOHdFN?eQ zk^z_mrTL{{BbcUTX3tQvIWm;el|PX=i%>d798y%%QBetIei&^2ePN6J14u?&I1emu zN~Zrb)v2?RpQJjh=E=8T9w~gq?2X`Se8aZJN;KM|!Z|w8n^D{Awn$&5dZU@w#B`4U dMNX|!7FksJx9dJ0;5$5~Jx6;DKVTEH{{cZf`cwb_ literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/workflow/repository/ApprovalHistoryRepository.class b/target/classes/id/iptek/utms/workflow/repository/ApprovalHistoryRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..a5874375a2fe22f663da72aefadf8cd641b0ef27 GIT binary patch literal 387 zcmbV|O-chn5QSgIpD}Uc2|R+ea@8sbA}RxdAqOZkouMW@(@jlJf>-cJEXz0)mkS@(q%MY5QqUK=j#0yk{hhSK z{`7Onuy1fu@rucFQ_|ye<>o2Fp}~33lG>=!lndqB4O}WXF;$WO!XG040u9DfDH~3m zI)m4I8*TG{VK4F>CW}zBG}{L4_unpwr`hcO#$H*2$ux92cemnuWv4qMIU6MR{tfo} RI_QH2M#wh(2; z!Y|+#@WBt@n_tK{_ouzPN(~kheYkXYXZAP0nc11?w;x}=0>C;vDnL$w61PluzUE!S4mdW1v%#&tMQh-qfW{&A8Ra?|Z=FYsdnO zDe%-0P1SP+x0{E8wpoCyT2xZ?*rjRkS1wHe6_`EbwzbizT4meXV>Y#A)l%TuvL0IT zC2y(qdbLuD_Mz3946Kw^QYwZkxnt8-3F&P!dE{8TG+-f_@D#{#gdCSW^obg8+En_2 zDZq!c^jsehN6TqbZuiHq^a82PN4|F5M2~9{X&}S*F^jZ+7c>w-l=Ok|j|uJ4CZh)} zmWi9rJ&DA|yqL$h8*3@>`5#gmCb&!4;2OeWVxzMu7?sQ_%L?-v3hbQc)MX;~;jaP? zWdeO5`a$7T0xAztd~O$p{XzG$$$j*I0u-KHl@)IBC_ zEMEn_|K}bIl`+$|C2zAg+~Zh~W!rYJ{IGvvBTxqiv#*c7_L=Bl?dGvj`%F6shnG=E0cOt;(J(8TNjBah{YJM9kcw8@ zEGn>+%|UNkWRme@a57TU5@#gN%n$+7@iA2&(Rln#Ywk;FW;Fik%Suhda9{=lr&tS1 z3;O|<(!IV1kxb#$pvZBCD=>mz9{((i;+!wQIOJdgcM~}%;?E?0Q~2dz8pT)OYAl`} z2}Ls~y4F*4GqlYdk71twwO`91-EfOiOlZcch`e^a32rU bJnl$z5(wkvOtM@^mH}ceCjS7I!rjB4d=~Cs literal 0 HcmV?d00001 diff --git a/target/classes/id/iptek/utms/workflow/repository/ApprovalStepRepository.class b/target/classes/id/iptek/utms/workflow/repository/ApprovalStepRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..0ee71e41ea2cd76d1500203d2a0ab8fa96a7f754 GIT binary patch literal 710 zcmb_a%}xR_5T1g%fPd)KyU~k1nV77aV2lZh!LafGl@4oRX=}F&;R$>v4?choWo&m< z)?EZ#WyZrq&8R`GQel;u)0Wmo zO<59I)VcB(Ga-W>*U0l6$$P4+vj1C=RzXuiOZF?v>2la z_e;OJ+6pCE61pL-(cUY5!@O3DCM8fe9~ubM?7Ie6P8S;Lk%%R;nLz^c|I>5a`Qad3 zO_-2hjuDfFGS*2vh%%b4*cDmMF>s4sDXVqcNrw$d^a44cpXy%ozH=2_U%OBlAK=CNtBS34)@n zORcrl*4El;ZL6(IS4AqrR+rXKt*x!CwX3bww$`?G)7CCl<@=xe-kZEhW*~gO`u!j? z@7?v>bN^?#=bri2+Xo&ZqSLj3AZb*hlg}VOl`@UJ$lPW&#LW1XhR)3wS-lyi(#6qu zG;;=1$*h`nWfY(?oq`61$Y5F+jWk4)8Eb39U}hlQaB(8FwJ(;qxFKz&wnclbhUR24 zmDpy+);ks5b~)3yl=ZGbE1l`Gl8JOQlSu7gI=#KfXsKMI+<;xfyEh4CV+^XGu}ovq z87t`>y74H6?#}D0#;r7HJRJk7`=e>0)jRajP`z?^cQyJ1gC^1>rgCeW70;|rB_e~p zR*I=%7|{b(I&E%=VryKswt73Lv5qz9I65AbnuC#OrYB`aV>zDBA2wcZB4stGc0>C$ zyjN2YY@K4zRH_Dny(!DgSPt)*Zksi1#N7TzGKq#3)z~G~>D1Id1hDQ*3X74Ft!64? zDk5T5rlCKRNj9v+hft0Jw`Jj06{VEPNLlH` zV5-;Z*^#uEj+s?6l6aYAmO(W#MbK^E7LnpQgX*PNPntVo2~(BLF{pv&f|DuWH=42{ z0=Bdgtv2EGJmK^yOn1y0In`)N+6p#$XV|-d7V31WL5pOO)AHGyvEpVt)6k>7S`zV$ zwH=yd{1$sNW#M*}aH^4-bXsiC=`v1D{x}MvX{&cI70v8u=yo2~W}-1Ltpdd2zk{0j%x4Rx%S!#6@sg-IoA0GfizC%=BZ7XfJ>rsU9%5TB+)2y4u~JAe})i zq8pbnRTbEjnsq^1PAha;X;2%Tm8U>$d;rJb;KY@9aJJ8YYgfVvO;MqGsMjf>M>}olvkj<%Y$I)v)-U{q^{gk zCwtIA8x1;FYJ#X)?E?0ExFso+Xt=AKBft?+Pvc7w15TeX5|PElo&&vm<}T1V2LcM%p{28!GPPcE(k3yx#2Eo0E2Fk3j%A&_$+ z9HNV9yG}a{x`ZxeYAUk2(HKM~U)tOT2F~>I*f#1--mP+qbh?bGw#bB$;ft}7LE0%e z+(~QX;|hbWq^oTBI6!5ZH(KU6J(`3&JL$a!T_ZbhC_Y=s&Q!!oNyW7WyvW?*AEA$q8d}Z0qFLZ3wRE*M_q2{4 zS$4;!0DX*UvcrLD+rmbw&3L3bJ-B%wnt@sp%kXi7K0&vD!Db}lxDiZOA1#AN33`XL zg2u#k4;|}Wvo1iNWSZE#dUaRly5{yxU9D@@ws!Yyve^-!Pca47cXpkVhe4mE&oTL7A=)BLm9DKFgsN{?yS8l^B=9b}Tc>*r`n(v5F-jZdsxe6> zVUwiO7XiI|5RAsv3DB3JAiCGKw6u122k2g=NiL90Up1n6r9JxC8hE;0!iJLUQ@U2ycA&&SJ<4Td8-w6TtB8KOt% z>pFeIphpFYa}%j84e6vfvwbP}A<`MntmCvO=y|*hr7U#dd=;9CzJJW1$LR@pM}5(F zWa*B!NOL^r9YLEpTb4t50T0@Xn@;8En6aixobR4A=v(w{_#K!IzD}M{^$o_A=4$93 zOeSMdD+RcHhrX-R(*}Kyz7H83k}!8rI2(ynYbZBEqfcHZ}YR0QKTh*pIdy4k#pr0D_ zGuf)Km@?1ciud+&gMLBJLrmb4o8nmJIeMiZ>1FKK<69m*rp(h{iMP#HSK__Irc=K(9dLZDcr#B@ZCL ziWo6vBAL$i?uXKvhm~BHfFk%|dQwd5pAGtp(zvo3uho5(UeoDs2K`+LL9yNQ2#{@7 za{4jU>m4mX|1jvE^e+HU&QY;#mS?gdIZ&aAqN>Zw-kc%~&>IH*hu(yU!Cj3;;OXZL zy<%4T(MoV3Aw+ECB6{1PL*kbPYHgf3nNAt5%SR59Ypq6u7a8CxvEIX75@$~WJdr8v zYIRp|U15&E4dk%F$BGZAJ6(eGDj#p~WMQlyl5eFe5jT4>z=(<*Rtcpn38Gn%08fJg z&Ql(Cl~;FEQVBf?6!8p$XDSqRwVu`5($l&uz_SYZSax-QYYVN%om$pYZ}4ncPpRD_ z$PGMK=aUVd$EUcyl#;9tGt&=yF%SiY{j=KIYTBgj0)rPy!;%4Wdl@g{({ye$xQQ17 zy@f;vB4#!?XNK-XDl>f5*6{P6Vjw&Ku_ilC#Q7*FX8 z;-zUEEciVJU%^+}&Z8U~5(s@pEi7o}XvsjQ1oFLmoI}8B#Clb5Rc-W# z4Ze{-0{6&_?|`Z4g{?Wk>x~rXRM1*)7#Nsw<= zF;l+P;7qGH@5j+`R79!(QTO1$fSKAM&h1wXen9pwi2ch&sXI7p@Lt}B zh#`vYYo|yvK$<&lWugOCLz^vRko^umVDLe=!`h^r2+$$OyZAxj1;PoB{@-{pLM95> zkdbYQv7+F?7v6vxA#Y6uiiFRP|q2ZH*;HDdiP+HD86?>NFo|8uw1sEZSKt|mKwM7CnBS19)ep=Z) zIm?SK{l3A^D9tVxIRR=2FaiW2=YmI0xBOqo2yLOSH?s2%*PhBodtr$&bf8y?zgEJ^b2&(k2D`m&k-^{yz|KOP~M?x zc=OSOLUDwGrf*3^U=S++8*A0zz-BAeBd(3uQ?ak>%v4m~okCxxKMEtYs2ER<2U zH7V@r{7ESoyJ{35AYNU=Yrine4)3)(0Q3ZK_xY+MIhtILL=2dE2S??o& z)m4o73&QT3@H(Y$p~KE$mj>oKAwB5s%~l`sOXN%mRtMrp01zDrrEkY2V5|}7adSHW z<2GkEyioMm64%=pVLjbzXWLd(Vt1}&jaKH<4z1&vk6I_Ak>qR7uIJ&Xfvoq3m@hAn$1=$%`QdxI&OB~iR1#Vvc z-(pp9LOg(XsBpZnCLsoVFe4IsFTo`PLoihu0K^}}P4{eCv9l0{Fs$uK6&aHoTM^eo6PS?po!NLg^4FWH;Eqo+M@>QPU0e9Ro! z95JUCh^bGX4~;x3kk(*>eJKTj3THf9KTJObhrAJ&Ldtn5oX&%#4;x_~q|_PH(fQL) z9$7RO;OkatUI-=uiO0KoaeaQy;vC@;Q{mVlAeyog8HhZRlvLCO*Zl|`77cNIE(AUAB_)%#=V`3gThH&$CS>@v! z)(unA)!E*>Xc;G>({+p5C5F zY#mG%xHR$zbzVowIP^9jeWg9+;rn{yS$JAF!46<8cbW1DufXRK{9j{(v;F6K*jC`!IebZgYFj*x0AI{ZrzMhBmb;G7H8`y^ zMy#*U?&%s5_@*4bDIjTYHJyOe1P#W~PnD7>ji*XUO;+DXC05@^`c>aZ*v0oT1UtsR zAdR2L+fC?Qitp;$y~MS3dq}UX-$UiKv-i-r+OTgARn><5dnkNA+GzYsd`=-9pR>tF zbEphirekO>l9o@VFwIjrr7xiN40}v|k$;6ZjIL;7^(52@#nqDPjEg9F5(<6lNoe%* zukl>vM<>E!2n`8M2pPxl0%$7#hDsFi%h5>&2-MbbeU>Kg!m~|eDcTX8$}ixv+HR90 zCcrQ8Z&iJOU*_MThfG(d@kt|=_8roz@r#2cDqvgLKA*$|ZYg>BC|F;}i<}v($PE1uDx^NK7*boj@`S#IyS=t;9mZ*omaBw$8vy?!eK$g4n*L%xMZl~LvU9JL;#of!S{#`18vjQ(zXg|hUXaCw!XM!FgWWm$S(xLgf*9Y#Xa z{qzBi)?bZC<)H($;m~fnAsnc@DN8q_I$U{ciTbC>qMuVA z{SMdiZ{Rkyg!;G?`I|3*f*Y_&HzU#dkNhVpN48-b|C#?HVv755M>x2gW&j7kE1qg> zMBu3&Prx>w8qfo+DT%XJFaW%$Ic|8kxGk$cyv1OGwd8?;iPG+c^o zcS1TkA@`gxX%WQozxdw{V&4X0%RzjPAl5&7KRt|)LaAV>F6`e=|5HK-=$k}YdP+d0 z+nWp!$tDBbczOn)xyyn0j}*jF_!HUx=WcO|f9V##n5CC|7cMNrFbaydhy7J$z~FxR zoevZJ(V1vxW3V%zL~o~6F3GSI@H z^QAkeKCG+i1g5Dv$R!(uOiU#$)JGs(x|c&)9;4`h%m~u)tcUfS{1|u-kLP0)&ZWP? z`FNfX4oFRH*l>~VQ%E;<^Q3TTeU^_C5s}&vz`qzU*$!@B0&RUMK);g~(B-s*-c4uG z6@d4ZfbV;e*nAB@{yzM>78$AUr@QGo`U-sj;J%(726Uf7>9h1m0~?5CMKefxHI_@IRvVxQE~5 zw-m^Glmhet(47jgZYa24L;gcpHhxS5eBY(ht2#a6!8U-soM^%!YNF6#nm{6Ul4i@uA<%rpw)m}+ zBTsKqw}_M&nQ=aT6blza^CtdA7#odq;&QhmE>nt&3z&aoaTy~PVLVTD1;dpWr~CxB z{O|(9Cmw~s@N@`_7x0i0DHACv0Q{4N1HYy?@au{KzYqGI0AD{~P)@r5{QCj?-2nar z0RA3m_`T3_`>2-=Qi8sQy!ZzJ`G)}Qhmq*}2;E2DK#NE5?=ku|5+$FbC!o>4O>ffE z?58IJA|bj9^4b(L2!=_)|ys=i~{(Ze4H37Y?P&7?W^*Qqi^ox zIs172!qSqe(%pQj??GC=p=3Ot-o1~{+{djj1$9|IyHS*(4%Hf{h3f09osADDQSL6K z#<$u?ym>;-~)hr zc(xaEfImvKs$loo2lytZ^Fmvo{dO8(17o4uqOyzdD$KhWZk!tzkXNUFv##2xQ-RD$>VZ6 zgDyF`Dzr%g85u?k3tfta67g{b(Pf&3^QR{2S(D$EI8*P!TPeyG9psxg?B!21;(P+Q z-je0p)WfI3ehB)f!+uEmXCe2#EZ+qcFb>Wle;!`c(1~;!#EQ5I0yKdtp`<7AB$~ou znvUGcI%u3z`FKUQeJWj)qkEB3y+ZdQ4Oy6=N1l#T{KOMa#*3FCxF${XdItNRXiQPvYi3FGgU3Yme!fMCCs#!;DBF9gQh@i&LQX1kSbYK<3Zl# zl5>lPq@Un<_ej!Ex(9#z_wv_mBvu`5VH`$EjSt z;2_@vr0-`O8rGxkTOjt9+}l%l14f*>@8B(vd;6{|)*j`VEPpR=lpmla6oJ$4S+smH z_x2;a{UrCM*yg+mgM8}DsYIL`kwnx4=q-kLpH55Q6tChlk$A9#wsSLG!%OKVZlTX2 zmsA)dN)GGHG3HJOk^#C+tJY4yX1Z3wE}4dn6#>6c?EYl=K|P#IsK3YHoAP6j_Cu)j zl$O|~I%$V!nYZFd%{_>6?9Yi~gA(?HX%KVM2wVxWO6zKI{Q5GUJb@S1J-c42G!z<8 zsWwye1ja!)*%{|V#Q>s^?WX<|W(mJ)(s;0|%?{VJOYkimrso}_xUZQPT64v^)i?kSp{}<`6x?}(V literal 0 HcmV?d00001 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..0019cbc --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,94 @@ +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\api\ApiResponse.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\api\AuditController.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\api\RoleController.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\api\TenantController.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\api\UserController.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\config\JwtProperties.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\config\LdapAuthConfig.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\config\LdapProperties.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\config\SecurityConfig.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\controller\AuthController.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\domain\AuthenticationSource.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\domain\Permission.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\domain\RefreshToken.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\domain\Role.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\domain\User.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\dto\AuthTokenResponse.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\dto\CreateRoleManagementRequest.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\dto\CreateUserManagementRequest.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\dto\CurrentUserResponse.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\dto\LoginRequest.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\dto\RefreshRequest.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\dto\UpdateRolePermissionsRequest.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\dto\UpdateUserRolesRequest.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\repository\PermissionRepository.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\repository\RefreshTokenRepository.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\repository\RoleRepository.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\repository\UserRepository.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\security\JwtAuthenticationFilter.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\security\JwtService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\security\TenantAwareUserDetailsService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\security\UserPrincipal.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\service\AuthService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\service\LoginThrottleService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\service\SingleLoginSessionService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\service\TokenBlacklistService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\service\UserRoleManagementService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\auth\service\UserService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\audit\domain\AuditTrail.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\audit\dto\AuditTrailResponse.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\audit\repository\AuditTrailRepository.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\audit\service\AuditTrailService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\config\ActiveMqConfig.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\config\AuditLoggingAspect.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\config\DataSeeder.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\config\I18nConfig.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\config\JpaAuditConfig.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\config\LocaleConfig.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\config\OpenApiConfig.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\config\RedisConfig.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\domain\BaseEntity.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\domain\TenantEntityListener.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\exception\AppException.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\exception\GlobalExceptionHandler.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\i18n\MessageResolver.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\core\security\SecurityUtils.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\messaging\ApprovalCompletedEvent.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\messaging\ApprovalEventConsumer.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\messaging\ApprovalEventProducer.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\module\controller\ModuleController.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\module\domain\SystemModule.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\module\dto\ModuleResponse.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\module\dto\ModuleToggleRequest.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\module\repository\SystemModuleRepository.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\module\service\Module.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\module\service\ModuleRegistryService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\module\service\NotificationModule.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\preference\domain\UserUiPreference.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\preference\dto\TablePreferenceProfile.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\preference\dto\TablePreferenceRequest.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\preference\dto\TablePreferenceSavedProfile.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\preference\dto\UserUiPreferencesResponse.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\preference\repository\UserUiPreferenceRepository.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\preference\service\UserPreferenceService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\tenant\Tenant.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\tenant\TenantContext.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\tenant\TenantFilter.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\tenant\TenantHibernateFilter.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\tenant\TenantRepository.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\tenant\TenantService.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\UtmsNgBeApplication.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\controller\ApprovalWorkflowController.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\domain\ApprovalAction.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\domain\ApprovalHistory.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\domain\ApprovalRequest.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\domain\ApprovalStatus.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\domain\ApprovalStep.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\dto\ApprovalActionRequest.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\dto\ApprovalRequestSummary.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\dto\ApprovalResponse.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\dto\CreateApprovalRequest.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\repository\ApprovalHistoryRepository.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\repository\ApprovalRequestRepository.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\repository\ApprovalStepRepository.java +D:\Projects\Personal\IPTEK\utms-ng\utms-ng-be\src\main\java\id\iptek\utms\workflow\service\ApprovalWorkflowService.java diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst new file mode 100644 index 0000000..e69de29