From 8d0b0eb6849b8363c54f497a11d4421a0ce5314d Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 25 Dec 2025 16:08:50 +0800 Subject: [PATCH 01/60] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 149 ++++++ build.gradle.kts | 60 +++ config/application-prod.yml.example | 84 ++++ database/init.d/init-en_GB.sql | 324 +++++++++++++ gradle.properties | 1 + gradle/libs.versions.toml | 55 +++ gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 251 ++++++++++ gradlew.bat | 94 ++++ settings.gradle.kts | 1 + .../com/onixbyte/helix/HelixApplication.java | 31 ++ .../onixbyte/helix/client/RedisClient.java | 127 +++++ .../onixbyte/helix/client/TokenClient.java | 57 +++ .../common/datetime/DateTimeFormatters.java | 45 ++ .../helix/common/jackson/JacksonModules.java | 34 ++ .../onixbyte/helix/common/regex/Patterns.java | 12 + .../onixbyte/helix/config/AssetConfig.java | 67 +++ .../helix/config/AuthenticationConfig.java | 28 ++ .../onixbyte/helix/config/CacheConfig.java | 99 ++++ .../onixbyte/helix/config/CaptchaConfig.java | 24 + .../com/onixbyte/helix/config/GuidConfig.java | 53 ++ .../onixbyte/helix/config/JacksonConfig.java | 20 + .../onixbyte/helix/config/MyBatisConfig.java | 9 + .../onixbyte/helix/config/SecurityConfig.java | 209 ++++++++ .../helix/config/SpringDataConfig.java | 9 + .../onixbyte/helix/config/WebFluxConfig.java | 57 +++ .../onixbyte/helix/constant/AssetPrefix.java | 10 + .../onixbyte/helix/constant/CacheName.java | 18 + .../onixbyte/helix/constant/ExternalHost.java | 26 + .../com/onixbyte/helix/constant/FileType.java | 23 + .../helix/constant/IdentityProvider.java | 40 ++ .../onixbyte/helix/constant/SettingName.java | 8 + .../onixbyte/helix/constant/SettingType.java | 9 + .../com/onixbyte/helix/constant/Status.java | 43 ++ .../onixbyte/helix/constant/UserStatus.java | 60 +++ .../helix/controller/AssetController.java | 81 ++++ .../helix/controller/AuthController.java | 39 ++ .../helix/controller/CaptchaController.java | 29 ++ .../controller/DepartmentController.java | 31 ++ .../helix/controller/ExceptionController.java | 97 ++++ .../helix/controller/MenuController.java | 26 + .../helix/controller/PositionController.java | 31 ++ .../helix/controller/RoleController.java | 31 ++ .../helix/controller/SettingController.java | 14 + .../helix/controller/UserController.java | 84 ++++ .../helix/domain/common/Treeable.java | 34 ++ .../query/wrapper/QueryRoleWrapper.java | 44 ++ .../query/wrapper/QueryUserWrapper.java | 84 ++++ .../onixbyte/helix/domain/entity/Asset.java | 126 +++++ .../helix/domain/entity/Authority.java | 265 ++++++++++ .../helix/domain/entity/Department.java | 290 +++++++++++ .../onixbyte/helix/domain/entity/Menu.java | 227 +++++++++ .../helix/domain/entity/Position.java | 284 +++++++++++ .../onixbyte/helix/domain/entity/Role.java | 308 ++++++++++++ .../helix/domain/entity/RoleAuthority.java | 125 +++++ .../onixbyte/helix/domain/entity/Setting.java | 269 +++++++++++ .../onixbyte/helix/domain/entity/User.java | 453 ++++++++++++++++++ .../helix/domain/entity/UserIdentity.java | 227 +++++++++ .../helix/domain/entity/UserRole.java | 176 +++++++ .../entity/embeddable/RoleAuthorityId.java | 70 +++ .../entity/embeddable/UserIdentityId.java | 97 ++++ .../domain/entity/embeddable/UserRoleId.java | 72 +++ .../onixbyte/helix/domain/model/TreeNode.java | 18 + .../helix/domain/view/AuthorityView.java | 264 ++++++++++ .../helix/domain/view/DepartmentView.java | 270 +++++++++++ .../helix/domain/view/PositionView.java | 271 +++++++++++ .../helix/domain/view/RoleAuthorityView.java | 154 ++++++ .../onixbyte/helix/domain/view/RoleView.java | 294 ++++++++++++ .../helix/domain/view/UserIdentityView.java | 202 ++++++++ .../helix/domain/view/UserRoleView.java | 154 ++++++ .../onixbyte/helix/domain/view/UserView.java | 366 ++++++++++++++ .../domain/web/request/AddUserRequest.java | 24 + .../domain/web/request/QueryRoleRequest.java | 13 + .../domain/web/request/QueryUserRequest.java | 19 + .../web/request/ResetPasswordRequest.java | 10 + .../domain/web/request/UpdateUserRequest.java | 22 + .../request/UsernamePasswordLoginRequest.java | 9 + .../web/response/BizExceptionResponse.java | 38 ++ .../domain/web/response/CaptchaResponse.java | 7 + .../web/response/FileUploadResponse.java | 9 + .../web/response/LoginSuccessResponse.java | 13 + .../web/response/UserDetailResponse.java | 252 ++++++++++ .../helix/exception/BizException.java | 74 +++ .../helix/extension/package-info.java | 43 ++ .../redis/serializer/JacksonSerialiser.java | 21 + .../filter/TokenAuthenticationFilter.java | 81 ++++ .../helix/manager/ApplicationManager.java | 18 + .../onixbyte/helix/manager/AssetManager.java | 40 ++ .../helix/manager/AuthorityManager.java | 24 + .../helix/manager/CaptchaManager.java | 39 ++ .../helix/manager/DepartmentManager.java | 26 + .../onixbyte/helix/manager/MenuManager.java | 23 + .../helix/manager/PositionManager.java | 25 + .../onixbyte/helix/manager/RoleManager.java | 45 ++ .../helix/manager/SettingManager.java | 21 + .../onixbyte/helix/manager/UserManager.java | 123 +++++ .../helix/manager/UserRoleManager.java | 34 ++ .../onixbyte/helix/manager/package-info.java | 60 +++ .../onixbyte/helix/mapper/AssetMapper.java | 12 + .../helix/mapper/AuthorityMapper.java | 19 + .../helix/mapper/DepartmentMapper.java | 7 + .../com/onixbyte/helix/mapper/MenuMapper.java | 12 + .../onixbyte/helix/mapper/PositionMapper.java | 7 + .../com/onixbyte/helix/mapper/RoleMapper.java | 22 + .../onixbyte/helix/mapper/SettingMapper.java | 7 + .../com/onixbyte/helix/mapper/UserMapper.java | 22 + .../onixbyte/helix/mapper/UserRoleMapper.java | 7 + .../helix/processor/package-info.java | 59 +++ .../properties/ApplicationProperties.java | 10 + .../helix/properties/AssetProperties.java | 41 ++ .../helix/properties/CaptchaProperties.java | 11 + .../helix/properties/CorsProperties.java | 82 ++++ .../helix/properties/MsalProperties.java | 49 ++ .../helix/properties/TokenProperties.java | 57 +++ .../helix/repository/AssetRepository.java | 9 + .../helix/repository/AuthorityRepository.java | 9 + .../repository/DepartmentRepository.java | 9 + .../helix/repository/MenuRepository.java | 9 + .../helix/repository/PositionRepository.java | 9 + .../repository/RoleAuthorityRepository.java | 10 + .../helix/repository/RoleRepository.java | 9 + .../helix/repository/SettingRepository.java | 10 + .../repository/UserIdentityRepository.java | 10 + .../helix/repository/UserRepository.java | 10 + .../helix/repository/UserRoleRepository.java | 16 + .../UsernamePasswordAuthentication.java | 80 ++++ .../UnauthorisedAuthenticationEntryPoint.java | 36 ++ ...sernamePasswordAuthenticationProvider.java | 74 +++ .../onixbyte/helix/service/AssetService.java | 118 +++++ .../onixbyte/helix/service/AuthService.java | 94 ++++ .../helix/service/CaptchaService.java | 76 +++ .../helix/service/DepartmentService.java | 30 ++ .../onixbyte/helix/service/MenuService.java | 28 ++ .../helix/service/PositionService.java | 23 + .../onixbyte/helix/service/RoleService.java | 42 ++ .../helix/service/SettingService.java | 7 + .../onixbyte/helix/service/UserService.java | 167 +++++++ .../onixbyte/helix/utils/DateTimeUtil.java | 13 + .../com/onixbyte/helix/utils/EncodeUtil.java | 94 ++++ .../onixbyte/helix/utils/SecurityUtil.java | 26 + .../com/onixbyte/helix/utils/TreeUtil.java | 130 +++++ .../helix/validation/group/OnCreate.java | 64 +++ .../helix/validation/group/OnUpdate.java | 84 ++++ src/main/resources/application.yml | 29 ++ src/main/resources/mapper/AssetMapper.xml | 5 + src/main/resources/mapper/AuthorityMapper.xml | 12 + .../resources/mapper/DepartmentMapper.xml | 5 + src/main/resources/mapper/MenuMapper.xml | 38 ++ src/main/resources/mapper/PositionMapper.xml | 5 + src/main/resources/mapper/RoleMapper.xml | 70 +++ src/main/resources/mapper/SettingMapper.xml | 5 + src/main/resources/mapper/UserMapper.xml | 228 +++++++++ src/main/resources/mapper/UserRoleMapper.xml | 5 + 153 files changed, 10986 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle.kts create mode 100644 config/application-prod.yml.example create mode 100644 database/init.d/init-en_GB.sql create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts create mode 100644 src/main/java/com/onixbyte/helix/HelixApplication.java create mode 100644 src/main/java/com/onixbyte/helix/client/RedisClient.java create mode 100644 src/main/java/com/onixbyte/helix/client/TokenClient.java create mode 100644 src/main/java/com/onixbyte/helix/common/datetime/DateTimeFormatters.java create mode 100644 src/main/java/com/onixbyte/helix/common/jackson/JacksonModules.java create mode 100644 src/main/java/com/onixbyte/helix/common/regex/Patterns.java create mode 100644 src/main/java/com/onixbyte/helix/config/AssetConfig.java create mode 100644 src/main/java/com/onixbyte/helix/config/AuthenticationConfig.java create mode 100644 src/main/java/com/onixbyte/helix/config/CacheConfig.java create mode 100644 src/main/java/com/onixbyte/helix/config/CaptchaConfig.java create mode 100644 src/main/java/com/onixbyte/helix/config/GuidConfig.java create mode 100644 src/main/java/com/onixbyte/helix/config/JacksonConfig.java create mode 100644 src/main/java/com/onixbyte/helix/config/MyBatisConfig.java create mode 100644 src/main/java/com/onixbyte/helix/config/SecurityConfig.java create mode 100644 src/main/java/com/onixbyte/helix/config/SpringDataConfig.java create mode 100644 src/main/java/com/onixbyte/helix/config/WebFluxConfig.java create mode 100644 src/main/java/com/onixbyte/helix/constant/AssetPrefix.java create mode 100644 src/main/java/com/onixbyte/helix/constant/CacheName.java create mode 100644 src/main/java/com/onixbyte/helix/constant/ExternalHost.java create mode 100644 src/main/java/com/onixbyte/helix/constant/FileType.java create mode 100644 src/main/java/com/onixbyte/helix/constant/IdentityProvider.java create mode 100644 src/main/java/com/onixbyte/helix/constant/SettingName.java create mode 100644 src/main/java/com/onixbyte/helix/constant/SettingType.java create mode 100644 src/main/java/com/onixbyte/helix/constant/Status.java create mode 100644 src/main/java/com/onixbyte/helix/constant/UserStatus.java create mode 100644 src/main/java/com/onixbyte/helix/controller/AssetController.java create mode 100644 src/main/java/com/onixbyte/helix/controller/AuthController.java create mode 100644 src/main/java/com/onixbyte/helix/controller/CaptchaController.java create mode 100644 src/main/java/com/onixbyte/helix/controller/DepartmentController.java create mode 100644 src/main/java/com/onixbyte/helix/controller/ExceptionController.java create mode 100644 src/main/java/com/onixbyte/helix/controller/MenuController.java create mode 100644 src/main/java/com/onixbyte/helix/controller/PositionController.java create mode 100644 src/main/java/com/onixbyte/helix/controller/RoleController.java create mode 100644 src/main/java/com/onixbyte/helix/controller/SettingController.java create mode 100644 src/main/java/com/onixbyte/helix/controller/UserController.java create mode 100644 src/main/java/com/onixbyte/helix/domain/common/Treeable.java create mode 100644 src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java create mode 100644 src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/Asset.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/Authority.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/Department.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/Menu.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/Position.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/Role.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/Setting.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/User.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/UserRole.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/embeddable/RoleAuthorityId.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserIdentityId.java create mode 100644 src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserRoleId.java create mode 100644 src/main/java/com/onixbyte/helix/domain/model/TreeNode.java create mode 100644 src/main/java/com/onixbyte/helix/domain/view/AuthorityView.java create mode 100644 src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java create mode 100644 src/main/java/com/onixbyte/helix/domain/view/PositionView.java create mode 100644 src/main/java/com/onixbyte/helix/domain/view/RoleAuthorityView.java create mode 100644 src/main/java/com/onixbyte/helix/domain/view/RoleView.java create mode 100644 src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java create mode 100644 src/main/java/com/onixbyte/helix/domain/view/UserRoleView.java create mode 100644 src/main/java/com/onixbyte/helix/domain/view/UserView.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/QueryRoleRequest.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/QueryUserRequest.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/UpdateUserRequest.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/UsernamePasswordLoginRequest.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/response/BizExceptionResponse.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/response/CaptchaResponse.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/response/FileUploadResponse.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/response/LoginSuccessResponse.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java create mode 100644 src/main/java/com/onixbyte/helix/exception/BizException.java create mode 100644 src/main/java/com/onixbyte/helix/extension/package-info.java create mode 100644 src/main/java/com/onixbyte/helix/extension/redis/serializer/JacksonSerialiser.java create mode 100644 src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java create mode 100644 src/main/java/com/onixbyte/helix/manager/ApplicationManager.java create mode 100644 src/main/java/com/onixbyte/helix/manager/AssetManager.java create mode 100644 src/main/java/com/onixbyte/helix/manager/AuthorityManager.java create mode 100644 src/main/java/com/onixbyte/helix/manager/CaptchaManager.java create mode 100644 src/main/java/com/onixbyte/helix/manager/DepartmentManager.java create mode 100644 src/main/java/com/onixbyte/helix/manager/MenuManager.java create mode 100644 src/main/java/com/onixbyte/helix/manager/PositionManager.java create mode 100644 src/main/java/com/onixbyte/helix/manager/RoleManager.java create mode 100644 src/main/java/com/onixbyte/helix/manager/SettingManager.java create mode 100644 src/main/java/com/onixbyte/helix/manager/UserManager.java create mode 100644 src/main/java/com/onixbyte/helix/manager/UserRoleManager.java create mode 100644 src/main/java/com/onixbyte/helix/manager/package-info.java create mode 100644 src/main/java/com/onixbyte/helix/mapper/AssetMapper.java create mode 100644 src/main/java/com/onixbyte/helix/mapper/AuthorityMapper.java create mode 100644 src/main/java/com/onixbyte/helix/mapper/DepartmentMapper.java create mode 100644 src/main/java/com/onixbyte/helix/mapper/MenuMapper.java create mode 100644 src/main/java/com/onixbyte/helix/mapper/PositionMapper.java create mode 100644 src/main/java/com/onixbyte/helix/mapper/RoleMapper.java create mode 100644 src/main/java/com/onixbyte/helix/mapper/SettingMapper.java create mode 100644 src/main/java/com/onixbyte/helix/mapper/UserMapper.java create mode 100644 src/main/java/com/onixbyte/helix/mapper/UserRoleMapper.java create mode 100644 src/main/java/com/onixbyte/helix/processor/package-info.java create mode 100644 src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java create mode 100644 src/main/java/com/onixbyte/helix/properties/AssetProperties.java create mode 100644 src/main/java/com/onixbyte/helix/properties/CaptchaProperties.java create mode 100644 src/main/java/com/onixbyte/helix/properties/CorsProperties.java create mode 100644 src/main/java/com/onixbyte/helix/properties/MsalProperties.java create mode 100644 src/main/java/com/onixbyte/helix/properties/TokenProperties.java create mode 100644 src/main/java/com/onixbyte/helix/repository/AssetRepository.java create mode 100644 src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java create mode 100644 src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java create mode 100644 src/main/java/com/onixbyte/helix/repository/MenuRepository.java create mode 100644 src/main/java/com/onixbyte/helix/repository/PositionRepository.java create mode 100644 src/main/java/com/onixbyte/helix/repository/RoleAuthorityRepository.java create mode 100644 src/main/java/com/onixbyte/helix/repository/RoleRepository.java create mode 100644 src/main/java/com/onixbyte/helix/repository/SettingRepository.java create mode 100644 src/main/java/com/onixbyte/helix/repository/UserIdentityRepository.java create mode 100644 src/main/java/com/onixbyte/helix/repository/UserRepository.java create mode 100644 src/main/java/com/onixbyte/helix/repository/UserRoleRepository.java create mode 100644 src/main/java/com/onixbyte/helix/security/authentication/UsernamePasswordAuthentication.java create mode 100644 src/main/java/com/onixbyte/helix/security/entrypoint/UnauthorisedAuthenticationEntryPoint.java create mode 100644 src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java create mode 100644 src/main/java/com/onixbyte/helix/service/AssetService.java create mode 100644 src/main/java/com/onixbyte/helix/service/AuthService.java create mode 100644 src/main/java/com/onixbyte/helix/service/CaptchaService.java create mode 100644 src/main/java/com/onixbyte/helix/service/DepartmentService.java create mode 100644 src/main/java/com/onixbyte/helix/service/MenuService.java create mode 100644 src/main/java/com/onixbyte/helix/service/PositionService.java create mode 100644 src/main/java/com/onixbyte/helix/service/RoleService.java create mode 100644 src/main/java/com/onixbyte/helix/service/SettingService.java create mode 100644 src/main/java/com/onixbyte/helix/service/UserService.java create mode 100644 src/main/java/com/onixbyte/helix/utils/DateTimeUtil.java create mode 100644 src/main/java/com/onixbyte/helix/utils/EncodeUtil.java create mode 100644 src/main/java/com/onixbyte/helix/utils/SecurityUtil.java create mode 100644 src/main/java/com/onixbyte/helix/utils/TreeUtil.java create mode 100644 src/main/java/com/onixbyte/helix/validation/group/OnCreate.java create mode 100644 src/main/java/com/onixbyte/helix/validation/group/OnUpdate.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/mapper/AssetMapper.xml create mode 100644 src/main/resources/mapper/AuthorityMapper.xml create mode 100644 src/main/resources/mapper/DepartmentMapper.xml create mode 100644 src/main/resources/mapper/MenuMapper.xml create mode 100644 src/main/resources/mapper/PositionMapper.xml create mode 100644 src/main/resources/mapper/RoleMapper.xml create mode 100644 src/main/resources/mapper/SettingMapper.xml create mode 100644 src/main/resources/mapper/UserMapper.xml create mode 100644 src/main/resources/mapper/UserRoleMapper.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d548fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,149 @@ +### macOS +# General +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# Metadata left by Dolphin file manager, which comes with KDE Plasma +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# Log files created by default by the nohup command +nohup.out + +### Windows + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### JetBrains IDE +# Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea + +# Gradle and Maven with auto-import +*.iml +*.ipr + +# File-based project format +*.iws + +# IntelliJ +out/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based HTTP Client +http-client.private.env.json + +### Gradle +.gradle +**/build/ +!**/src/**/build/ + +!gradle/wrapper/ +!gradle/wrapper/gradle-wrapper.jar +!gradle/wrapper/gradle-wrapper.properties +gradle-app.setting +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Server +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +*.jar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +# Config +config/*.yml +config/*.yaml +config/*.properties + +# Tests +test/ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..6a9e9ae --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + id("java") + id("org.springframework.boot") version "3.5.4" + id("io.spring.dependency-management") version "1.1.7" +} + +val artefactVersion: String by project + +group = "com.onixbyte.helix" +version = artefactVersion + +tasks.withType { + options.encoding = "UTF-8" +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +dependencies { + implementation(libs.jspecify.core) + implementation(platform(libs.aws.sdk.bom)) + implementation(libs.aws.sdk.s3) + implementation(libs.commons.io) + implementation(libs.commons.lang) + implementation(libs.commons.collections) + implementation(libs.hypersistence.core) + implementation(platform(libs.onixbyte.versionCatalogue)) + implementation(libs.onixbyte.tuple) + implementation(libs.onixbyte.commonToolbox) + implementation(libs.onixbyte.identityGenerator) + implementation(libs.onixbyte.captcha) + implementation(libs.onixbyte.regions) + implementation(libs.jwt.core) + implementation(libs.spring.boot.configurationProcessor) + implementation(libs.spring.boot.starter.web) + implementation(libs.spring.boot.starter.webFlux) + implementation(libs.spring.boot.starter.validation) + implementation(libs.spring.boot.starter.redis) + implementation(libs.spring.boot.starter.cache) + implementation(libs.spring.boot.starter.security) + implementation(libs.spring.boot.starter.jpa) + implementation(libs.mybatis.starter.core) + implementation(libs.jackson.jsr310) + testImplementation(libs.spring.boot.starter.test) + testImplementation(libs.reactor.test) + testImplementation(libs.spring.security.test) + testImplementation(libs.mybatis.starter.test) + runtimeOnly(libs.postgres.driver) + testRuntimeOnly(libs.h2.database) + testRuntimeOnly(libs.junit.launcher) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/config/application-prod.yml.example b/config/application-prod.yml.example new file mode 100644 index 0000000..101e908 --- /dev/null +++ b/config/application-prod.yml.example @@ -0,0 +1,84 @@ +spring: + data: + redis: + # Redis 主机 + host: ${REDIS_HOST:-localhost} + # Redis 端口 + port: ${REDIS_PORT:-6379} + # Redis 数据库索引 + database: ${REDIS_DATABASE_INDEX:-0} + # Redis 数据库密码 + password: ${REDIS_PASSWORD:-} + datasource: + # 数据库连接池类型 + type: com.zaxxer.hikari.HikariDataSource + # 数据库驱动类 + driver-class-name: org.postgresql.Driver + # 数据库 URL + url: jdbc:postgresql://${PG_HOST:-localhost}:${PG_PORT:-5432}/${PG_DATABASE:-helix} + # 数据库用户 + username: ${PG_USER} + # 数据库密码 + password: ${PG_PASSWORD} + +app: + asset: + # 是否开启 S3 文件存储服务 + enabled: true + # S3 服务端点(若使用非 AWS 提供的 S3 兼容 API,请添加该配置) +# endpoint: https://endpoint.s3.service + # S3 服务区域(详情请见 S3 服务提供商) +# region: apac + # 公开域名 + public-host: https://s3.my.app + # 是否开启 Path Style + path-style: false + # 存储桶名称 + bucket: dev + # S3 访问密钥 ID + access-key-id: ${S3_ACCESS_KEY_ID} + # S3 访问密钥机密 + secret-access-key: ${S3_SECRET_ACCESS_KEY} + # 鉴权配置 + authentication: + # Microsoft Entra ID 鉴权配置 + msal: + # Microsoft Entra ID 提供的客户端 ID + client-id: ${MSAL_CLIENT_ID} + # Microsoft Entra ID 提供的租户 ID + tenant-id: ${MSAL_TENANT_ID} + # 令牌配置 + jwt: + # 令牌签发人 + issuer: Helix Server + # 令牌机密 + secret: ${TOKEN_SECRET:-1234567890abcdefghijklmnopqrstuv} + # 令牌有效期(Ref java.time.Duration) + valid-time: PT2H + # 跨域配置 + cors: + # 是否允许身份验证 + allow-credentials: true + # 是否允许私有网络 + allow-private-network: true + # 允许的请求头列表 + allowed-headers: Content-Type + # 允许的请求方法列表(Ref org.springframework.http.HttpMethod) + # 2025.11.6注 + # 由于 Spring 解析问题,在此处使用小写的情况下会导致在请求头中存在 Origin 时出现 Invalid CORS Request 的问题,请务必使用大写 + allowed-methods: + - GET + - POST + - PUT + - PATCH + - DELETE + # 允许的来源域名 + allowed-origins: '*' + # 要对外暴露的响应头列表 + exposed-headers: X-Authorisation + # 跨域缓存时长 + max-age: PT2H + # Captcha 配置 + captcha: + # Captcha 长度 + length: 6 diff --git a/database/init.d/init-en_GB.sql b/database/init.d/init-en_GB.sql new file mode 100644 index 0000000..629473c --- /dev/null +++ b/database/init.d/init-en_GB.sql @@ -0,0 +1,324 @@ +/** + * IMPORTANT NOTE ON DATABASE CREATION: + * * If you intend to create the database using a user other than the one specified + * for the application (e.g., 'system_admin' creating a database for 'app_user'), + * please ensure this SQL file is executed by the **corresponding database user**. + * * This is crucial for correctly setting up ownership and default privileges. + * * Example: CREATE DATABASE my_database OWNER app_user; + * * Ensure all necessary user roles and permissions are in place **prior** to + * running this initialisation script. + */ + +--- Type Definitions --- +DROP TYPE IF EXISTS USER_STATUS CASCADE; +DROP TYPE IF EXISTS STATUS CASCADE; +DROP TYPE IF EXISTS IDENTITY_PROVIDER CASCADE; +DROP TYPE IF EXISTS SETTING_TYPE CASCADE; + +CREATE TYPE USER_STATUS AS ENUM ('ACTIVE', 'INACTIVE', 'LOCKED'); +CREATE TYPE STATUS AS ENUM ('ACTIVE', 'INACTIVE'); +CREATE TYPE IDENTITY_PROVIDER AS ENUM ('LOCAL', 'OIDC', 'MICROSOFT_ENTRA_ID', 'GOOGLE_OIDC', 'SAML'); +CREATE TYPE SETTING_TYPE AS ENUM ('STRING', 'BOOLEAN', 'INT'); + +--- Departments Table --- +DROP TABLE IF EXISTS departments CASCADE; +CREATE TABLE departments +( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(128) NOT NULL UNIQUE, + parent_id BIGINT NULL REFERENCES departments (id), + sort INT NOT NULL DEFAULT NULL, + status STATUS NOT NULL DEFAULT 'ACTIVE'::STATUS, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX departments_name_index ON departments (name); +CREATE INDEX departments_parent_id_index ON departments (parent_id); + +--- Departments Data Insertion --- +INSERT INTO departments (id, name, parent_id, sort, status) +VALUES (1, 'Company HQ', NULL, 1, 'ACTIVE'::STATUS), + (2, 'Human Resources', 1, 1, 'ACTIVE'::STATUS), + (3, 'Finance', 1, 2, 'ACTIVE'::STATUS), + (4, 'Technology', 1, 3, 'ACTIVE'::STATUS), + (5, 'IT Support', 4, 1, 'ACTIVE'::STATUS), + (6, 'Software Development', 4, 2, 'ACTIVE'::STATUS), + (7, 'Operations', 1, 4, 'INACTIVE'::STATUS); + +--- Positions Table --- +DROP TABLE IF EXISTS positions CASCADE; +CREATE TABLE positions +( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(128) NOT NULL UNIQUE, + code VARCHAR(64) NULL UNIQUE, + description TEXT, + sort INT NOT NULL DEFAULT 0, + status STATUS NOT NULL DEFAULT 'ACTIVE', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX positions_name_index ON positions (name); +CREATE INDEX positions_code_index ON positions (code); +CREATE INDEX positions_name_code_index ON positions (name, code); + +--- Positions Data Insertion --- +INSERT INTO positions (id, name, code, description, sort, status) +VALUES (1, 'HR Manager', 'HR-MGR', + 'Responsible for overseeing recruitment, employee relations, and staff wellbeing.', 1, + 'ACTIVE'), + (2, 'Finance Officer', 'FIN-OFC', + 'Handles accounts, prepares financial statements, and ensures compliance with regulations.', + 2, 'ACTIVE'), + (3, 'IT Support Specialist', 'IT-SPT', + 'Provides technical assistance, manages helpdesk queries, and maintains computer systems.', + 3, 'ACTIVE'), + (4, 'Software Engineer', 'SWE-ENG', + 'Develops and maintains in-house applications, ensuring code quality and system reliability.', + 4, 'ACTIVE'), + (5, 'Operations Coordinator', 'OPS-CRD', + 'Assists with day-to-day logistics, procurement, and office organisation.', 5, 'INACTIVE'); + +--- Users Table --- +DROP TABLE IF EXISTS users CASCADE; +CREATE TABLE users +( + id BIGINT PRIMARY KEY, + username VARCHAR(64) UNIQUE NOT NULL, + password VARCHAR(255), + full_name VARCHAR(128) NOT NULL, + email VARCHAR(128) UNIQUE, + region_code VARCHAR(10), + phone_number VARCHAR(32), + avatar_url TEXT, + status USER_STATUS NOT NULL DEFAULT 'ACTIVE', + department_id BIGINT REFERENCES departments (id), + position_id BIGINT REFERENCES positions (id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX users_username_index ON users (username); + +--- Users Table Indexes --- +CREATE UNIQUE INDEX uidx_users_region_abbreviation_phone_number + ON users (region_abbreviation, phone_number); + +--- Users Data Insertion --- +-- NOTE: All phone numbers are generated by ChatGPT, they should not be connected any real person. +INSERT INTO users(id, username, password, full_name, email, region_abbreviation, phone_number, avatar_url, + department_id, position_id, created_at, updated_at) +VALUES (1, 'helix', null, 'Helix Admin', 'admin@helix.onixbyte.dev', 'GB', '7000000000', + 'https://gravatar.com/avatar/6ef4c4033f6aa8e43d06bd5e462a6173cc2a960633473721a6f1289cd1b5146f', + 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + (2, 'johndoe', null, 'John Doe', 'johndoe@helix.onixbyte.dev', 'GB', '7000000001', + 'https://gravatar.com/avatar/41bcebddd573747d1bd35ef7fae72ebefd6b47f077d42442a2510d35b0c2db92', + 6, 4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +--- User Identities Table --- +DROP TABLE IF EXISTS user_identities CASCADE; +CREATE TABLE user_identities +( + user_id BIGINT NOT NULL REFERENCES users (id), + provider IDENTITY_PROVIDER NOT NULL, + external_id VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, provider, external_id) +); + +--- Roles Table --- +DROP TABLE IF EXISTS roles CASCADE; +CREATE TABLE roles +( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(128) NOT NULL UNIQUE, + code VARCHAR(64) NOT NULL UNIQUE, + sort INTEGER NOT NULL, + default_value BOOLEAN NOT NULL DEFAULT FALSE, + description TEXT, + status STATUS NOT NULL DEFAULT 'ACTIVE', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX roles_name_index ON roles (name); +CREATE INDEX roles_code_index ON roles (code); +CREATE INDEX roles_name_code_index ON roles (name, code); + +--- Roles Data Insertion --- +INSERT INTO roles (name, code, sort, default_value, description, status, created_at, updated_at) +VALUES ('Admin', 'admin', 1, FALSE, 'Administrator of this system.', 'ACTIVE'::STATUS, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('Normal User', 'user', 2, TRUE, 'Normal user of this system.', 'ACTIVE'::STATUS, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +--- User Roles Table --- +DROP TABLE IF EXISTS user_roles CASCADE; +CREATE TABLE user_roles +( + user_id BIGINT NOT NULL + CONSTRAINT user_roles_users_id_fk REFERENCES users, + role_id BIGINT NOT NULL + CONSTRAINT user_roles_roles_id_fk REFERENCES roles, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT user_roles_pk PRIMARY KEY (user_id, role_id) +); + +--- User Roles Data Insertion --- +INSERT INTO user_roles +VALUES (1, 1), + (2, 2); + +--- Authorities Table --- +DROP TABLE IF EXISTS authorities CASCADE; +CREATE TABLE authorities +( + id BIGSERIAL PRIMARY KEY, + code VARCHAR(128) NOT NULL UNIQUE, + name VARCHAR(128) NOT NULL, + description TEXT, + status STATUS NOT NULL DEFAULT 'ACTIVE'::STATUS, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX authorities_code_index ON authorities (code); + +--- Authorities Data Insertion --- +INSERT INTO authorities(code, name, description, status, created_at, updated_at) +VALUES ('system:dashboard:read', 'Read Dashboard', 'Read dashboard.', 'ACTIVE'::STATUS, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('system:user:read', 'Read User', 'Read user.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP), + ('system:user_detail:read', 'Read User', 'Read user detail.', 'ACTIVE'::STATUS, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('system:user:write', 'Write User', 'Write user, such as add, edit or delete.', + 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('system:department:read', 'Read Department', 'Read departments', 'ACTIVE'::STATUS, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('system:department:write', 'Write Department', 'Write departments.', 'ACTIVE'::STATUS, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('system:role:read', 'Read Roles', 'Read roles.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP), + ('system:role:write', 'Write Roles', 'Write roles.', 'ACTIVE'::STATUS, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('system:authority:read', 'Read Authorities', 'Read authorities.', 'ACTIVE'::STATUS, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('system:authority:write', 'Write Authorities', 'Write authorities.', + 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('system:audit_log:read', 'Read Audit Logs', 'Read audit logs.', 'ACTIVE'::STATUS, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('system:sso:write', 'Manage SSO', + 'Manage SSO configurations (such as Microsoft Entra ID, etc.).', 'ACTIVE'::STATUS, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('system:setting:write', 'Write System Settings', 'Write system settings.', + 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +--- Role Authorities Table --- +DROP TABLE IF EXISTS role_authorities CASCADE; +CREATE TABLE role_authorities +( + role_id BIGINT NOT NULL REFERENCES roles (id), + authority_id BIGINT NOT NULL REFERENCES authorities (id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (role_id, authority_id) +); + +--- Role Authorities Data Insertion --- +INSERT INTO role_authorities +VALUES (1, 1, CURRENT_TIMESTAMP), + (1, 2, CURRENT_TIMESTAMP), + (1, 3, CURRENT_TIMESTAMP), + (1, 4, CURRENT_TIMESTAMP), + (1, 5, CURRENT_TIMESTAMP), + (1, 6, CURRENT_TIMESTAMP), + (1, 7, CURRENT_TIMESTAMP), + (1, 8, CURRENT_TIMESTAMP), + (1, 9, CURRENT_TIMESTAMP), + (1, 10, CURRENT_TIMESTAMP), + (1, 11, CURRENT_TIMESTAMP), + (1, 12, CURRENT_TIMESTAMP), + (1, 13, CURRENT_TIMESTAMP), + (2, 1, CURRENT_TIMESTAMP), + (2, 2, CURRENT_TIMESTAMP), + (2, 3, CURRENT_TIMESTAMP), + (2, 5, CURRENT_TIMESTAMP), + (2, 7, CURRENT_TIMESTAMP), + (2, 9, CURRENT_TIMESTAMP), + (2, 11, CURRENT_TIMESTAMP); + +DROP TABLE IF EXISTS assets; +CREATE TABLE assets +( + id BIGSERIAL NOT NULL PRIMARY KEY, + key VARCHAR(255) NOT NULL UNIQUE, + upload_by BIGINT NOT NULL REFERENCES users (id), + upload_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX assets_key_index ON assets (key); + +COMMENT ON TABLE assets IS 'Stores metadata for files or other digital assets within the system.'; +COMMENT ON COLUMN assets.id IS 'The unique identifier for the asset, automatically generated by the database.'; +COMMENT ON COLUMN assets.key IS 'The unique key or path of the asset within the storage system.'; +COMMENT ON COLUMN assets.upload_by IS 'The unique ID of the user who uploaded this asset, referencing the ID in the users table.'; +COMMENT ON COLUMN assets.upload_time IS 'The timestamp indicating when the asset was uploaded to the system.'; + +DROP TABLE IF EXISTS settings; +CREATE TABLE settings +( + id BIGSERIAL NOT NULL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description VARCHAR(255) NULL, + type SETTING_TYPE NOT NULL, + value VARCHAR(255) NULL, + default_value VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX settings_name_index ON settings (name); + +COMMENT ON TABLE settings IS 'Hot-deployable application settings.'; +COMMENT ON COLUMN settings.id IS 'Setting unique identifier.'; +COMMENT ON COLUMN settings.name IS 'Setting name.'; +COMMENT ON COLUMN settings.description IS 'Setting description.'; +COMMENT ON COLUMN settings.type IS 'The type of the value.'; +COMMENT ON COLUMN settings.value IS 'Setting current value.'; +COMMENT ON COLUMN settings.default_value IS 'Setting default value.'; + +INSERT INTO settings(name, description, type, value, default_value) +VALUES ('captcha-setting::enabled', 'Whether captcha is enabled.', 'BOOLEAN'::SETTING_TYPE, 'true', + 'false'), + ('auth-setting::register-enabled', 'Whether register is enabled', 'BOOLEAN'::SETTING_TYPE, + 'true', 'false'); + +DROP TABLE IF EXISTS menus; +CREATE TABLE menus +( + id BIGSERIAL NOT NULL PRIMARY KEY, + name VARCHAR(50) NOT NULL, + parent_id BIGINT NULL DEFAULT NULL, + code VARCHAR(255) NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + path VARCHAR(255) NULL DEFAULT NULL, + is_external_link BOOLEAN NOT NULL DEFAULT FALSE, + is_visible BOOLEAN NOT NULL DEFAULT TRUE, + status STATUS NOT NULL DEFAULT 'ACTIVE'::STATUS, + authority_code VARCHAR(128) NULL DEFAULT NULL, + icon VARCHAR(128) NULL DEFAULT NULL, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX menus_code_uindex ON menus (code); + +INSERT INTO menus(id, name, parent_id, code, sort, path, is_external_link, is_visible, status, + authority_code, icon, create_time, update_time) +VALUES (1, '系统管理', NULL, 'system-manage', 99, NULL, FALSE, TRUE, 'ACTIVE'::STATUS, NULL, NULL, + NOW(), NOW()), + (2, '用户管理', 1, 'user-manage', 1, '/users', FALSE, TRUE, 'ACTIVE'::STATUS, + 'system:user:write', NULL, NOW(), NOW()); \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7c0ddbc --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +artefactVersion=0.0.1 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..744b7f0 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,55 @@ +[versions] +jspecifyVersion = "1.0.0" +javaJwtVersion = "4.5.0" +postgresDriverVersion = "42.7.7" +h2Version = "2.2.224" +springSecurityVersion = "6.5.2" +springBootVersion = "3.5.4" +reactorVersion = "3.7.8" +junitPlatformVersion = "1.12.2" +onixbyteVersion = "3.2.0" +onixbyteCaptcha = "1.1.0" +onixbyteRegions = "2025.12.0" +awsSdkVersion = "2.25.48" +commonsIoVersion = "2.16.1" +commonsCollections = "4.5.0" +commonsLangVersion = "3.20.0" +mybatisVersion = "3.0.5" +jacksonVersion = "2.19.2" +hypersistenceVersion = "3.14.0" + +[libraries] +jwt-core = { group = "com.auth0", name = "java-jwt", version.ref = "javaJwtVersion" } +postgres-driver = { group = "org.postgresql", name = "postgresql", version.ref = "postgresDriverVersion" } +h2-database = { group = "com.h2database", name = "h2", version.ref = "h2Version" } +spring-boot-configurationProcessor = { group = "org.springframework.boot", name = "spring-boot-configuration-processor", version.ref = "springBootVersion" } +spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web", version.ref = "springBootVersion" } +spring-boot-starter-webFlux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux", version.ref = "springBootVersion" } +spring-boot-starter-validation = { group = "org.springframework.boot", name = "spring-boot-starter-validation", version.ref = "springBootVersion" } +spring-boot-starter-redis = { group = "org.springframework.boot", name = "spring-boot-starter-data-redis", version.ref = "springBootVersion" } +spring-boot-starter-cache = { group = "org.springframework.boot", name = "spring-boot-starter-cache", version.ref = "springBootVersion" } +spring-boot-starter-security = { group = "org.springframework.boot", name = "spring-boot-starter-security", version.ref = "springBootVersion" } +spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test", version.ref = "springBootVersion" } +spring-security-test = { group = "org.springframework.security", name = "spring-security-test", version.ref = "springSecurityVersion" } +reactor-test = { group = "io.projectreactor", name = "reactor-test", version.ref = "reactorVersion" } +junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junitPlatformVersion" } +onixbyte-versionCatalogue = { group = "com.onixbyte", name = "version-catalogue", version.ref = "onixbyteVersion" } +onixbyte-tuple = { group = "com.onixbyte", name = "tuple", version.ref = "onixbyteVersion" } +onixbyte-commonToolbox = { group = "com.onixbyte", name = "common-toolbox", version.ref = "onixbyteVersion" } +onixbyte-identityGenerator = { group = "com.onixbyte", name = "identity-generator", version.ref = "onixbyteVersion" } +onixbyte-captcha = { group = "com.onixbyte", name = "captcha", version.ref = "onixbyteCaptcha" } +onixbyte-regions = { group = "com.onixbyte", name = "regions4j", version.ref = "onixbyteRegions" } +aws-sdk-bom = { group = "software.amazon.awssdk", name = "bom", version.ref = "awsSdkVersion" } +aws-sdk-s3 = { group = "software.amazon.awssdk", name = "s3" } +commons-io = { group = "commons-io", name = "commons-io", version.ref = "commonsIoVersion" } +commons-collections = { group = "org.apache.commons", name = "commons-collections4", version.ref = "commonsCollections" } +mybatis-starter-core = { group = "org.mybatis.spring.boot", name = "mybatis-spring-boot-starter", version.ref = "mybatisVersion" } +mybatis-starter-test = { group = "org.mybatis.spring.boot", name = "mybatis-spring-boot-starter-test", version.ref = "mybatisVersion" } +jackson-jsr310 = { group = "com.fasterxml.jackson.datatype", name = "jackson-datatype-jsr310", version.ref = "jacksonVersion" } +spring-boot-starter-jpa = { group = "org.springframework.boot", name = "spring-boot-starter-data-jpa" } +hypersistence-core = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version.ref = "hypersistenceVersion" } +commons-lang = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commonsLangVersion" } +jspecify-core = { group = "org.jspecify", name = "jspecify", version.ref = "jspecifyVersion" } + +[plugins] + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d4081da --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..faf9300 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..6f13425 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "helix-server" \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/HelixApplication.java b/src/main/java/com/onixbyte/helix/HelixApplication.java new file mode 100644 index 0000000..4bf85ae --- /dev/null +++ b/src/main/java/com/onixbyte/helix/HelixApplication.java @@ -0,0 +1,31 @@ +package com.onixbyte.helix; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; + +/** + * Application entrance. + * + * @author zihluwang + * @see SpringBootApplication + * @see EnableCaching + * @see SpringApplication + * @since 1.0.0 + */ +@EnableCaching +@SpringBootApplication +public class HelixApplication { + + /** + * Main method that serves as the entry point for the Helix application. + * + * @param args command-line arguments passed to the application, which can be used to override + * default configuration properties or specify runtime options + */ + public static void main(String[] args) { + SpringApplication.run(HelixApplication.class, args); + } + +} diff --git a/src/main/java/com/onixbyte/helix/client/RedisClient.java b/src/main/java/com/onixbyte/helix/client/RedisClient.java new file mode 100644 index 0000000..3c57bdf --- /dev/null +++ b/src/main/java/com/onixbyte/helix/client/RedisClient.java @@ -0,0 +1,127 @@ +package com.onixbyte.helix.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.Objects; + +/** + * A client class for interacting with Redis, providing simple and type-safe access to common Redis + * operations such as setting, retrieving, incrementing, decrementing, and deleting keys. + *

+ * This class abstracts the direct usage of {@link RedisTemplate} for value operations. + * + * @author zihluwang + */ +@Component +public class RedisClient { + + private final RedisTemplate redisTemplate; + + /** + * Constructs a new RedisClient with the specified {@link RedisTemplate}. + * + * @param redisTemplate the template used for Redis interaction + */ + @Autowired + public RedisClient(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + /** + * Set a value for a given key in Redis. + * + * @param key the key to set + * @param value the value associated with the key + * @param the type of the value + */ + public void set(String key, T value) { + redisTemplate.opsForValue().set(key, value); + } + + /** + * Set a value for a given key in Redis with a specified expiry timeout. + * + * @param key the key to set + * @param value the value associated with the key + * @param timeout the time after which the key should expire + * @param the type of the value + */ + public void set(String key, T value, Duration timeout) { + redisTemplate.opsForValue().set(key, value, timeout); + } + + /** + * Get the value associated with a given key from Redis. + *

+ * The returned object is of type {@code Object} and may require manual casting. + * + * @param key the key to retrieve + * @return the value associated with the key, or {@code null} if the key does not exist + */ + public Object get(String key) { + return redisTemplate.opsForValue().get(key); + } + + /** + * Get the value associated with a given key from Redis, attempting to cast it to the + * specified type. + * + * @param key the key to retrieve + * @param type the class type to cast the retrieved value to + * @param the type of the expected value + * @return the value associated with the key, cast to type T, or {@code null} if the key does + * not exist + * @throws IllegalStateException if the retrieved value cannot be cast to the specified type + */ + public T get(String key, Class type) { + var value = redisTemplate.opsForValue().get(key); + if (Objects.isNull(value)) { + return null; + } + + if (type.isInstance(value)) { + return type.cast(value); + } + + throw new IllegalStateException("Cannot cast " + value.getClass().getName() + " to " + type.getName()); + } + + /** + * Increment the value of the key by one. + *

+ * If the key does not exist, it is created and set to 0 before the increment operation. If the + * value stored at the key is not an integer, an exception may be thrown by Redis. + * + * @param key the key to increment + * @return the new value of the key after the increment + */ + public Long increment(String key) { + return redisTemplate.opsForValue().increment(key); + } + + /** + * Decrement the value of the key by one. + *

+ * If the key does not exist, it is created and set to 0 before the decrement operation. If the + * value stored at the key is not an integer, an exception may be thrown by Redis. + * + * @param key the key to decrement + * @return the new value of the key after the decrement + */ + public Long decrement(String key) { + return redisTemplate.opsForValue().decrement(key); + } + + /** + * Delete a key from Redis. + * + * @param key the key to delete + * @return {@code true} if the key was deleted, {@code false} if the key did not exist + */ + public boolean delete(String key) { + return redisTemplate.delete(key); + } +} diff --git a/src/main/java/com/onixbyte/helix/client/TokenClient.java b/src/main/java/com/onixbyte/helix/client/TokenClient.java new file mode 100644 index 0000000..b6af92f --- /dev/null +++ b/src/main/java/com/onixbyte/helix/client/TokenClient.java @@ -0,0 +1,57 @@ +package com.onixbyte.helix.client; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.onixbyte.helix.domain.entity.User; +import com.onixbyte.helix.properties.TokenProperties; +import com.onixbyte.helix.utils.DateTimeUtil; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * A client class responsible for generating JSON Web Tokens (JWT) for user authentication and + * authorisation purposes. + *

+ * It uses the {@link com.auth0.jwt.JWT} library to create and sign tokens based on user details and + * configured token properties. + * + * @author zihluwang + */ +@Component +public class TokenClient { + + private final Algorithm algorithm; + private final TokenProperties tokenProperties; + + /** + * Constructs a new TokenClient with the necessary algorithm and token properties. + * + * @param algorithm the signing algorithm used to secure the JWT + * @param tokenProperties the configuration properties for the token, such as issuer and + * validity period + */ + public TokenClient(Algorithm algorithm, TokenProperties tokenProperties) { + this.algorithm = algorithm; + this.tokenProperties = tokenProperties; + } + + /** + * Generate a JSON Web Token to the current user. + * + * @param user the current user for whom the token is being generated + * @return a JWT string + */ + public String generateToken(User user) { + var issuedAt = LocalDateTime.now(); + var expiresAt = issuedAt.plus(tokenProperties.validTime()); + + return JWT.create() + .withSubject(user.getUsername()) + .withAudience("Helix Web") + .withIssuer(tokenProperties.issuer()) + .withIssuedAt(DateTimeUtil.asInstant(issuedAt)) + .withExpiresAt(DateTimeUtil.asInstant(expiresAt)) + .sign(algorithm); + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/common/datetime/DateTimeFormatters.java b/src/main/java/com/onixbyte/helix/common/datetime/DateTimeFormatters.java new file mode 100644 index 0000000..5a51cac --- /dev/null +++ b/src/main/java/com/onixbyte/helix/common/datetime/DateTimeFormatters.java @@ -0,0 +1,45 @@ +package com.onixbyte.helix.common.datetime; + +import java.time.format.DateTimeFormatter; + +/** + * Utility class providing predefined {@link DateTimeFormatter} instances for common date and + * time patterns. These formatters can be used to parse and format {@code java.time} objects + * consistently throughout an application. + * + * @author zihluwang + */ +public class DateTimeFormatters { + + /** + * A {@link DateTimeFormatter} for formatting and parsing full date and time with seconds, using + * the pattern "yyyy-MM-dd HH:mm:ss". + *

+ * Example: "{@code 2023-10-27 15:30:45}" + */ + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * A {@link DateTimeFormatter} for formatting and parsing dates only, using the + * pattern "yyyy-MM-dd". + *

+ * Example: "{@code 2023-10-27}" + */ + public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * A {@link DateTimeFormatter} for formatting and parsing times only, using the + * pattern "HH:mm:ss". + *

+ * Example: "{@code 15:30:45}" + */ + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); + + /** + * A {@link DateTimeFormatter} for formatting and parsing year and month only, using the + * pattern "yyyy-MM". + *

+ * Example: "{@code 2023-10}" + */ + public static final DateTimeFormatter YEAR_MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); +} diff --git a/src/main/java/com/onixbyte/helix/common/jackson/JacksonModules.java b/src/main/java/com/onixbyte/helix/common/jackson/JacksonModules.java new file mode 100644 index 0000000..78430fb --- /dev/null +++ b/src/main/java/com/onixbyte/helix/common/jackson/JacksonModules.java @@ -0,0 +1,34 @@ +package com.onixbyte.helix.common.jackson; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; +import com.onixbyte.helix.common.datetime.DateTimeFormatters; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public class JacksonModules { + + public static final SimpleModule DATE_TIME_MODULE = initialiseDateTimeModule(); + + private static SimpleModule initialiseDateTimeModule() { + var javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatters.DATE_TIME_FORMATTER)); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatters.DATE_TIME_FORMATTER)); + + javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatters.DATE_FORMATTER)); + javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatters.DATE_FORMATTER)); + + javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatters.TIME_FORMATTER)); + javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatters.TIME_FORMATTER)); + + return javaTimeModule; + } +} diff --git a/src/main/java/com/onixbyte/helix/common/regex/Patterns.java b/src/main/java/com/onixbyte/helix/common/regex/Patterns.java new file mode 100644 index 0000000..43d5394 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/common/regex/Patterns.java @@ -0,0 +1,12 @@ +package com.onixbyte.helix.common.regex; + +import java.util.regex.Pattern; + +public class Patterns { + + public static final Pattern EMAIL = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); + + public static final Pattern IMAGE_URL = Pattern.compile("^https?://.*\\.(?:png|jpg|jpeg|gif|webp|svg|avif)(?:\\?.*)?$", Pattern.CASE_INSENSITIVE); + + public static final Pattern GRAVATAR_IMAGE_URL = Pattern.compile("^https?://(?:[a-z0-9-]+\\.)?gravatar\\.com/avatar/([a-f0-9]{32})(?:\\?.*)?$", Pattern.CASE_INSENSITIVE); +} diff --git a/src/main/java/com/onixbyte/helix/config/AssetConfig.java b/src/main/java/com/onixbyte/helix/config/AssetConfig.java new file mode 100644 index 0000000..be792c6 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/AssetConfig.java @@ -0,0 +1,67 @@ +package com.onixbyte.helix.config; + +import com.onixbyte.helix.properties.AssetProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; + +/** + * Configuration class for asset storage services. + *

+ * Enables configuration properties for S3 file storage services. Individual service beans are + * created by their respective service classes to better support conditional configuration. + * + * @author zihluwang + * @since 1.0.0 + */ +@Configuration +@EnableConfigurationProperties({AssetProperties.class}) +public class AssetConfig { + + /** + * S3Client to store assets into S3 service. + * + * @param assetProperties asset properties + * @return an S3 Client reference to custom S3 configuration properties + */ + @Bean + public S3Client s3Client(AssetProperties assetProperties) { + // initialise AWS credentials + var credentials = AwsBasicCredentials.create( + assetProperties.accessKeyId(), + assetProperties.secretAccessKey() + ); + + // prepare s3 client + var s3ClientBuilder = S3Client.builder() + .region(Region.of(assetProperties.region())) + .credentialsProvider(StaticCredentialsProvider.create(credentials)); + + // override endpoint + Optional.ofNullable(assetProperties.endpoint()) + .ifPresent((endpoint) -> { + try { + s3ClientBuilder.endpointOverride(new URI(endpoint)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Endpoint is not valid."); + } + }); + + // set path style + s3ClientBuilder.serviceConfiguration(S3Configuration.builder() + .pathStyleAccessEnabled(assetProperties.pathStyle()) + .build() + ); + + return s3ClientBuilder.build(); + } +} diff --git a/src/main/java/com/onixbyte/helix/config/AuthenticationConfig.java b/src/main/java/com/onixbyte/helix/config/AuthenticationConfig.java new file mode 100644 index 0000000..d5aff8a --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/AuthenticationConfig.java @@ -0,0 +1,28 @@ +package com.onixbyte.helix.config; + +import com.onixbyte.helix.properties.ApplicationProperties; +import com.onixbyte.helix.properties.MsalProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration class for authentication-related components and properties. + *

+ * This configuration class is responsible for enabling and managing custom configuration properties + * related to authentication mechanisms within the Helix application. It specifically enables the + * {@link MsalProperties} configuration properties to support Microsoft Authentication Library + * (MSAL) integration. + *

+ * The class serves as a central point for authentication configuration, ensuring that all + * authentication-related properties are properly loaded and made available to the Spring + * application context. + * + * @author zihluwang + * @since 1.0.0 + * @see MsalProperties + * @see EnableConfigurationProperties + */ +@Configuration +@EnableConfigurationProperties({MsalProperties.class, ApplicationProperties.class}) +public class AuthenticationConfig { +} diff --git a/src/main/java/com/onixbyte/helix/config/CacheConfig.java b/src/main/java/com/onixbyte/helix/config/CacheConfig.java new file mode 100644 index 0000000..97b4aff --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/CacheConfig.java @@ -0,0 +1,99 @@ +package com.onixbyte.helix.config; + +import com.onixbyte.helix.extension.redis.serializer.JacksonSerialiser; +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.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; + +import java.time.Duration; + +/** + * Configuration class for Redis-based caching components. + *

+ * This configuration class provides beans for Redis cache management and template operations + * within the Helix application. It configures custom serialisation strategies using + * {@link GenericJackson2JsonRedisSerializer} for values and string serialisation for keys, + * ensuring optimal performance and compatibility with JSON-based data structures. + *

+ * The configuration includes: + *

+ * + * @author zihluwang + * @see RedisCacheManager + * @see RedisTemplate + * @see GenericJackson2JsonRedisSerializer + * @since 1.0.0 + */ +@Configuration +public class CacheConfig { + + /** + * Creates a custom Redis cache manager with JSON serialisation support. + *

+ * This method configures a {@link RedisCacheManager} that uses string serialisation for cache + * keys and {@link GenericJackson2JsonRedisSerializer} for cache values. This setup ensures that + * complex objects can be stored and retrieved from Redis cache whilst maintaining readability + * and compatibility with JSON-based systems. + * + * @param connectionFactory the Redis connection factory used to establish connections + * @return a configured {@link RedisCacheManager} with custom serialisation settings + * @see RedisCacheManager + * @see GenericJackson2JsonRedisSerializer + * @see RedisSerializationContext + */ + @Bean + public RedisCacheManager cacheManager( + RedisConnectionFactory connectionFactory + ) { + var _keySerializer = RedisSerializer.string(); + + var cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair + .fromSerializer(_keySerializer)) + .serializeValuesWith(RedisSerializationContext.SerializationPair + .fromSerializer(JacksonSerialiser.INSTANCE)) + .entryTtl(Duration.ofMinutes(90L)); + + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(connectionFactory) + .cacheDefaults(cacheConfiguration) + .build(); + } + + /** + * Creates a Redis template for direct Redis operations with custom serialisation. + *

+ * This method configures a {@link RedisTemplate} that uses string serialisation for keys + * and {@link GenericJackson2JsonRedisSerializer} for values. This template provides low-level + * access to Redis operations whilst ensuring consistent serialisation strategies across + * the application. + *

+ * The template is fully configured and ready for use after bean creation. + * + * @param connectionFactory the Redis connection factory used to establish connections + * @return a fully configured {@link RedisTemplate} for Redis operations + * @see RedisTemplate + * @see GenericJackson2JsonRedisSerializer + * @see RedisSerializer + */ + @Bean + public RedisTemplate redisTemplate( + RedisConnectionFactory connectionFactory + ) { + var redisTemplate = new RedisTemplate(); + redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setKeySerializer(RedisSerializer.string()); + redisTemplate.setValueSerializer(JacksonSerialiser.INSTANCE); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} diff --git a/src/main/java/com/onixbyte/helix/config/CaptchaConfig.java b/src/main/java/com/onixbyte/helix/config/CaptchaConfig.java new file mode 100644 index 0000000..14f7b50 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/CaptchaConfig.java @@ -0,0 +1,24 @@ +package com.onixbyte.helix.config; + +import com.onixbyte.captcha.Producer; +import com.onixbyte.captcha.impl.DefaultCaptchaProducer; +import com.onixbyte.captcha.text.impl.DefaultTextProducer; +import com.onixbyte.helix.properties.CaptchaProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties({CaptchaProperties.class}) +public class CaptchaConfig { + + @Bean + public Producer producer(CaptchaProperties captchaProperties) { + var textProducer = DefaultTextProducer.builder() + .length(captchaProperties.length()) + .build(); + return DefaultCaptchaProducer.builder() + .textProducer(textProducer) + .build(); + } +} diff --git a/src/main/java/com/onixbyte/helix/config/GuidConfig.java b/src/main/java/com/onixbyte/helix/config/GuidConfig.java new file mode 100644 index 0000000..690b6ca --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/GuidConfig.java @@ -0,0 +1,53 @@ +package com.onixbyte.helix.config; + +import com.onixbyte.identitygenerator.IdentityGenerator; +import com.onixbyte.identitygenerator.impl.SequentialUuidGenerator; +import com.onixbyte.identitygenerator.impl.SnowflakeIdentityGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration class for GUID (Globally Unique Identifier) generation components. + *

+ * This configuration class provides beans for generating unique identifiers throughout the + * Helix application. It utilises the Snowflake algorithm implementation to ensure globally unique, + * time-ordered identifiers that are suitable for distributed systems. + *

+ * The Snowflake algorithm generates 64-bit identifiers composed of: + *

+ * + * @author zihluwang + * @since 1.0.0 + * @see IdentityGenerator + * @see SnowflakeIdentityGenerator + */ +@Configuration +public class GuidConfig { + + /** + * Creates a Snowflake-based identity generator for user IDs. + *

+ * This method configures a {@link SnowflakeIdentityGenerator} with machine ID and data centre + * ID both set to 0. The generator produces unique 64-bit Long identifiers suitable for user + * entity primary keys in distributed environments. + *

+ * The generated IDs are: + *

+ * + * @return a configured {@link SnowflakeIdentityGenerator} instance for generating user IDs + * @see SnowflakeIdentityGenerator + * @see IdentityGenerator + */ + @Bean + public IdentityGenerator userIdentityGenerator() { + return new SnowflakeIdentityGenerator(0x0, 0x0); + } +} diff --git a/src/main/java/com/onixbyte/helix/config/JacksonConfig.java b/src/main/java/com/onixbyte/helix/config/JacksonConfig.java new file mode 100644 index 0000000..cc686d3 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/JacksonConfig.java @@ -0,0 +1,20 @@ +package com.onixbyte.helix.config; + +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.onixbyte.helix.common.jackson.JacksonModules; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfig { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jacksonCustomiser() { + return (builder) -> { + builder.modules(JacksonModules.DATE_TIME_MODULE); + builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + }; + } +} diff --git a/src/main/java/com/onixbyte/helix/config/MyBatisConfig.java b/src/main/java/com/onixbyte/helix/config/MyBatisConfig.java new file mode 100644 index 0000000..b852fe2 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/MyBatisConfig.java @@ -0,0 +1,9 @@ +package com.onixbyte.helix.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@MapperScan(basePackages = {"com.onixbyte.helix.mapper"}) +public class MyBatisConfig { +} diff --git a/src/main/java/com/onixbyte/helix/config/SecurityConfig.java b/src/main/java/com/onixbyte/helix/config/SecurityConfig.java new file mode 100644 index 0000000..7d80d9d --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/SecurityConfig.java @@ -0,0 +1,209 @@ +package com.onixbyte.helix.config; + +import com.auth0.jwt.algorithms.Algorithm; +import com.onixbyte.helix.filter.TokenAuthenticationFilter; +import com.onixbyte.helix.properties.CorsProperties; +import com.onixbyte.helix.properties.TokenProperties; +import com.onixbyte.helix.security.entrypoint.UnauthorisedAuthenticationEntryPoint; +import com.onixbyte.helix.security.provider.UsernamePasswordAuthenticationProvider; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +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.access.ExceptionTranslationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; +import java.util.stream.Stream; + +/** + * Configuration class for Spring Security components and policies. + *

+ * This configuration class establishes comprehensive security settings for the Helix application, + * including CORS policies, authentication mechanisms, password encoding, and JWT token handling. + * It configures a stateless security architecture suitable for modern web applications and APIs. + *

+ * Key security features configured: + *

    + *
  • CORS (Cross-Origin Resource Sharing) configuration
  • + *
  • Stateless session management
  • + *
  • JWT-based authentication with HMAC256 algorithm
  • + *
  • BCrypt password encoding
  • + *
  • Method-level security annotations
  • + *
  • Custom authentication providers
  • + *
+ * + * @author zihluwang + * @see EnableWebSecurity + * @see EnableMethodSecurity + * @see TokenProperties + * @see CorsProperties + * @since 1.0.0 + */ +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@EnableConfigurationProperties({TokenProperties.class, CorsProperties.class}) +public class SecurityConfig { + + /** + * Creates a CORS configuration source based on application properties. + *

+ * This method configures Cross-Origin Resource Sharing (CORS) policies using the + * {@link CorsProperties} configuration. It sets up allowed origins, headers, methods, + * credentials handling, and other CORS-related settings to enable secure cross-origin requests + * from web browsers. + *

+ * The configuration is applied globally to all endpoints (/**) within the application. + * + * @param properties the CORS configuration properties containing allowed origins, headers, + * methods, etc + * @return a configured {@link CorsConfigurationSource} for handling cross-origin requests + * @see CorsProperties + * @see CorsConfiguration + * @see UrlBasedCorsConfigurationSource + */ + @Bean + public CorsConfigurationSource corsConfigurationSource( + CorsProperties properties + ) { + var corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowCredentials(properties.allowCredentials()); + corsConfiguration.setAllowedOrigins(List.of(properties.allowedOrigins())); + corsConfiguration.setAllowedHeaders(List.of(properties.allowedHeaders())); + corsConfiguration.setAllowedMethods(Stream.of(properties.allowedMethods()) + .map(HttpMethod::name) + .toList()); + corsConfiguration.setMaxAge(properties.maxAge()); + corsConfiguration.setAllowPrivateNetwork(properties.allowPrivateNetwork()); + corsConfiguration.setExposedHeaders(List.of(properties.exposedHeaders())); + + var corsConfigurationSource = new UrlBasedCorsConfigurationSource(); + corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); + return corsConfigurationSource; + } + + /** + * Configures the Spring Security filter chain for HTTP requests. + *

+ * This method establishes the core security policies for the application, including: + *

    + *
  • CORS configuration integration
  • + *
  • CSRF protection disabled (suitable for stateless APIs)
  • + *
  • Stateless session management
  • + *
  • Request authorization rules with public and protected endpoints
  • + *
+ *

+ * The configuration permits access to error pages and authentication endpoints whilst requiring + * authentication for all other requests. Logout endpoints require authentication to prevent + * unauthorised session termination. + * + * @param httpSecurity the HTTP security configuration builder + * @param corsConfigurationSource the CORS configuration source for cross-origin requests + * @return a configured {@link SecurityFilterChain} for processing HTTP requests + * @throws Exception if any exception occurs during security filter chain construction + * @see HttpSecurity + * @see SecurityFilterChain + * @see SessionCreationPolicy#STATELESS + */ + @Bean + public SecurityFilterChain securityFilterChain( + HttpSecurity httpSecurity, + CorsConfigurationSource corsConfigurationSource, + TokenAuthenticationFilter tokenAuthenticationFilter, + UnauthorisedAuthenticationEntryPoint unauthorisedAuthenticationEntryPoint + ) throws Exception { + return httpSecurity + .cors((cors) -> cors + .configurationSource(corsConfigurationSource)) + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement((customiser) -> customiser + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests((customiser) -> customiser + .requestMatchers("/error", "/error/**").permitAll() + .requestMatchers("/captcha", "/captcha/**").permitAll() + .requestMatchers("/auth/**").permitAll() + .requestMatchers("/auth/logout").authenticated() + .anyRequest().authenticated() + ) + .exceptionHandling((exceptionHandling) -> exceptionHandling + .authenticationEntryPoint(unauthorisedAuthenticationEntryPoint)) + .addFilterAfter(tokenAuthenticationFilter, ExceptionTranslationFilter.class) + .build(); + } + + /** + * Creates a password encoder for secure password hashing. + *

+ * This method provides a {@link BCryptPasswordEncoder} instance that uses the BCrypt hashing + * algorithm to securely encode passwords. BCrypt is a adaptive hash function designed for + * password hashing that includes a salt to protect against rainbow table attacks and is + * computationally expensive to resist brute-force attacks. + *

+ * The encoder is used throughout the application for password verification during + * authentication and for encoding new passwords during user registration. + * + * @return a {@link BCryptPasswordEncoder} instance for secure password operations + * @see BCryptPasswordEncoder + * @see PasswordEncoder + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * Creates an authentication manager with custom authentication providers. + *

+ * This method configures a {@link ProviderManager} that coordinates multiple authentication + * providers to handle different authentication mechanisms within the application. The manager + * attempts authentication using each configured provider until one succeeds or all fail. + *

+ * Currently configured for extensibility to support various authentication providers such as + * Microsoft Entra ID, local database authentication, or other identity providers as needed. + * + * @return a {@link ProviderManager} instance configured with authentication providers + * @see ProviderManager + * @see AuthenticationManager + */ + @Bean + public AuthenticationManager authenticationManager( + UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider + ) { + return new ProviderManager( + usernamePasswordAuthenticationProvider + ); + } + + /** + * Creates the JWT signing algorithm using application token properties. + *

+ * This method configures an HMAC256 algorithm instance using the secret key specified in + * the {@link TokenProperties}. The algorithm is used for signing and verifying JWT tokens + * throughout the application, ensuring token integrity and authenticity. + *

+ * HMAC256 provides a good balance of security and performance for JWT token signing in + * most applications. + * + * @param properties the token configuration properties containing the signing secret + * @return a configured {@link Algorithm} instance for JWT token operations + * @see Algorithm + * @see TokenProperties + */ + @Bean + public Algorithm algorithm(TokenProperties properties) { + return Algorithm.HMAC256(properties.secret()); + } +} diff --git a/src/main/java/com/onixbyte/helix/config/SpringDataConfig.java b/src/main/java/com/onixbyte/helix/config/SpringDataConfig.java new file mode 100644 index 0000000..476eda4 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/SpringDataConfig.java @@ -0,0 +1,9 @@ +package com.onixbyte.helix.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@Configuration +@EnableJpaRepositories(basePackages = {"com.onixbyte.helix.repository"}) +public class SpringDataConfig { +} diff --git a/src/main/java/com/onixbyte/helix/config/WebFluxConfig.java b/src/main/java/com/onixbyte/helix/config/WebFluxConfig.java new file mode 100644 index 0000000..6dae4eb --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/WebFluxConfig.java @@ -0,0 +1,57 @@ +package com.onixbyte.helix.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Configuration class for Spring WebFlux reactive web components. + *

+ * This configuration class provides beans for reactive web programming components within the Helix + * application. It configures WebClient instances and other reactive web-related components that + * enable non-blocking, asynchronous HTTP communication with external services. + *

+ * The configuration supports: + *

    + *
  • Reactive HTTP client configuration
  • + *
  • Non-blocking I/O operations
  • + *
  • Asynchronous request/response handling
  • + *
+ * + * @author zihluwang + * @since 1.0.0 + * @see WebClient + * @see Configuration + */ +@Configuration +public class WebFluxConfig { + + /** + * Creates a reactive WebClient for HTTP communication with external services. + *

+ * This method configures a {@link WebClient} instance that provides a modern, reactive approach + * to HTTP client operations. The WebClient supports non-blocking I/O and is built on top of + * Reactor Netty, making it suitable for high-performance, scalable applications. + *

+ * The client is configured with default settings and can be used throughout the application for + * making HTTP requests to external APIs, microservices, or other web resources in a reactive, + * non-blocking manner. + *

+ * Key features: + *

    + *
  • Non-blocking I/O operations
  • + *
  • Reactive streams support
  • + *
  • Built-in support for JSON serialisation/deserialisation
  • + *
  • Configurable timeouts and retry mechanisms
  • + *
+ * + * @return a configured {@link WebClient} instance for reactive HTTP operations + * @see WebClient + * @see WebClient.Builder + */ + @Bean + public WebClient webClient() { + return WebClient.builder() + .build(); + } +} diff --git a/src/main/java/com/onixbyte/helix/constant/AssetPrefix.java b/src/main/java/com/onixbyte/helix/constant/AssetPrefix.java new file mode 100644 index 0000000..09c9612 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/constant/AssetPrefix.java @@ -0,0 +1,10 @@ +package com.onixbyte.helix.constant; + +public class AssetPrefix { + + public static final String UPLOADS = "uploads"; + + public static final String AVATARS = "avatars"; + + public static final String PROFILES = "profiles"; +} diff --git a/src/main/java/com/onixbyte/helix/constant/CacheName.java b/src/main/java/com/onixbyte/helix/constant/CacheName.java new file mode 100644 index 0000000..8c60a6e --- /dev/null +++ b/src/main/java/com/onixbyte/helix/constant/CacheName.java @@ -0,0 +1,18 @@ +package com.onixbyte.helix.constant; + +public class CacheName { + + public static final String USER = "user"; + + public static final String AUTHORITIES_OF_USER = "user-authorities"; + + public static final String ASSET = "asset"; + + public static final String SETTING = "setting"; + + public static final String CAPTCHA = "captcha"; + + public static final String CAPTCHA_SETTING = "captcha-setting"; + + public static final String AUTH_SETTING = "auth-setting"; +} diff --git a/src/main/java/com/onixbyte/helix/constant/ExternalHost.java b/src/main/java/com/onixbyte/helix/constant/ExternalHost.java new file mode 100644 index 0000000..5512434 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/constant/ExternalHost.java @@ -0,0 +1,26 @@ +package com.onixbyte.helix.constant; + +/** + * Constants for external host configurations and endpoints. + *

+ * This utility class provides centralised definitions for external service hosts, API endpoints, + * and third-party integration points used throughout the Helix application. It serves as a single + * source of truth for external service configurations, promoting maintainability and consistency. + *

+ * The class is designed to hold static final constants representing: + *

    + *
  • External API base URLs
  • + *
  • Third-party service endpoints
  • + *
  • Integration service hosts
  • + *
  • External resource locations
  • + *
+ *

+ * This class cannot be instantiated as it serves purely as a constant container. + * + * @author zihluwang + * @since 1.0.0 + */ +public final class ExternalHost { + + +} diff --git a/src/main/java/com/onixbyte/helix/constant/FileType.java b/src/main/java/com/onixbyte/helix/constant/FileType.java new file mode 100644 index 0000000..45f55a4 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/constant/FileType.java @@ -0,0 +1,23 @@ +package com.onixbyte.helix.constant; + +public interface FileType { + + String getExtension(); + + enum Image implements FileType { + JPEG("jpeg"), + PNG("png") + ; + + private final String extension; + + Image(String extension) { + this.extension = extension; + } + + @Override + public String getExtension() { + return extension; + } + } +} diff --git a/src/main/java/com/onixbyte/helix/constant/IdentityProvider.java b/src/main/java/com/onixbyte/helix/constant/IdentityProvider.java new file mode 100644 index 0000000..9e06844 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/constant/IdentityProvider.java @@ -0,0 +1,40 @@ +package com.onixbyte.helix.constant; + +import com.onixbyte.helix.config.AuthenticationConfig; + +/** + * Enumeration of supported identity providers for user authentication. + *

+ * This enumeration defines the various identity providers that the Helix application supports for + * user authentication and authorisation. Each provider represents a different authentication + * mechanism or external identity service that can be used to verify user credentials and establish + * user sessions. + *

+ * The application supports both local authentication (using internal user database) and external + * identity providers (such as Microsoft Entra ID) to provide flexible authentication options for + * different deployment scenarios and organisational requirements. + * + * @author zihluwang + * @since 1.0.0 + * @see AuthenticationConfig + */ +public enum IdentityProvider { + + /** + * Local identity provider using the application's internal user database. + *

+ * This provider authenticates users against locally stored credentials, typically using + * username/email and password combinations. User accounts are managed entirely within the Helix + * application's database. + */ + LOCAL, + + /** + * Microsoft Entra ID (formerly Azure Active Directory) identity provider. + *

+ * This provider enables authentication through Microsoft's cloud-based identity and access + * management service. Users authenticate using their organisational Microsoft accounts, + * supporting features such as single sign-on (SSO) and multi-factor authentication (MFA). + */ + MICROSOFT_ENTRA_ID +} diff --git a/src/main/java/com/onixbyte/helix/constant/SettingName.java b/src/main/java/com/onixbyte/helix/constant/SettingName.java new file mode 100644 index 0000000..b539d96 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/constant/SettingName.java @@ -0,0 +1,8 @@ +package com.onixbyte.helix.constant; + +public class SettingName { + + public static final String CAPTCHA_ENABLED = "captcha-enabled"; + + public static final String REGISTER_ENABLED = "register-enabled"; +} diff --git a/src/main/java/com/onixbyte/helix/constant/SettingType.java b/src/main/java/com/onixbyte/helix/constant/SettingType.java new file mode 100644 index 0000000..a4dfdd3 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/constant/SettingType.java @@ -0,0 +1,9 @@ +package com.onixbyte.helix.constant; + +public enum SettingType { + + BOOLEAN, + STRING, + INT, + ; +} diff --git a/src/main/java/com/onixbyte/helix/constant/Status.java b/src/main/java/com/onixbyte/helix/constant/Status.java new file mode 100644 index 0000000..5b2a80d --- /dev/null +++ b/src/main/java/com/onixbyte/helix/constant/Status.java @@ -0,0 +1,43 @@ +package com.onixbyte.helix.constant; + +/** + * Enumeration representing general status states for system entities. + *

+ * This enumeration provides a standardised way to represent the operational status of various + * system entities, resources, or components within the Helix application. It offers a binary state + * model that can be applied across different domain objects to indicate their current + * operational condition. + *

+ * The status values are designed to be generic and reusable across multiple contexts, such as + * system configurations, feature toggles, service states, or any other binary operational + * indicators within the application. + * + * @author zihluwang + * @since 1.0.0 + */ +public enum Status { + + /** + * Indicates that the entity is currently active and operational. + *

+ * When an entity has an ACTIVE status, it means the entity is: + *

    + *
  • Currently enabled and functioning
  • + *
  • Available for use by the system or users
  • + *
  • Participating in normal application operations
  • + *
+ */ + ACTIVE, + + /** + * Indicates that the entity is currently inactive or disabled. + *

+ * When an entity has an INACTIVE status, it means the entity is: + *

    + *
  • Currently disabled or not functioning
  • + *
  • Temporarily or permanently unavailable
  • + *
  • Excluded from normal application operations
  • + *
+ */ + INACTIVE +} diff --git a/src/main/java/com/onixbyte/helix/constant/UserStatus.java b/src/main/java/com/onixbyte/helix/constant/UserStatus.java new file mode 100644 index 0000000..bbc05a3 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/constant/UserStatus.java @@ -0,0 +1,60 @@ +package com.onixbyte.helix.constant; + +/** + * Enumeration representing the various states of user accounts within the system. + *

+ * This enumeration defines the possible status values for user accounts in the Helix application, + * providing a standardised way to manage user account lifecycle and access control. Each status + * represents a different level of account accessibility and operational capability within + * the system. + *

+ * User status directly affects authentication, authorisation, and system access permissions. + * The status is typically managed through administrative functions and security policies to ensure + * proper access control and account management. + * + * @author zihluwang + * @since 1.0.0 + * @see com.onixbyte.helix.config.SecurityConfiguration + */ +public enum UserStatus { + + /** + * Indicates that the user account is active and fully operational. + *

+ * An ACTIVE user account has: + *

    + *
  • Full access to system features and resources
  • + *
  • Ability to authenticate and establish sessions
  • + *
  • Normal operational privileges as per assigned roles
  • + *
  • No restrictions on account usage
  • + *
+ */ + ACTIVE, + + /** + * Indicates that the user account is inactive or disabled. + *

+ * An INACTIVE user account: + *

    + *
  • Cannot authenticate or access the system
  • + *
  • Is temporarily or permanently disabled
  • + *
  • Retains user data but blocks all system access
  • + *
  • May be reactivated by administrators if appropriate
  • + *
+ */ + INACTIVE, + + /** + * Indicates that the user account is locked due to security concerns. + *

+ * A LOCKED user account: + *

    + *
  • Is temporarily blocked from system access
  • + *
  • May result from failed authentication attempts
  • + *
  • Could be locked due to security policy violations
  • + *
  • Requires administrative intervention to unlock
  • + *
  • Maintains user data whilst preventing access
  • + *
+ */ + LOCKED +} diff --git a/src/main/java/com/onixbyte/helix/controller/AssetController.java b/src/main/java/com/onixbyte/helix/controller/AssetController.java new file mode 100644 index 0000000..be1f185 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/controller/AssetController.java @@ -0,0 +1,81 @@ +package com.onixbyte.helix.controller; + +import com.onixbyte.helix.constant.AssetPrefix; +import com.onixbyte.helix.domain.web.response.FileUploadResponse; +import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.service.AssetService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +/** + * REST controller for file storage operations. Provides endpoints for uploading, downloading, and + * deleting assets using the configured storage service. + * + * @author zihluwang + * @since 1.0.0 + */ +@RestController +@RequestMapping("/assets") +public class AssetController { + + private static final Logger log = LoggerFactory.getLogger(AssetController.class); + + private final AssetService assetService; + + /** + * Constructs a new FileController with the specified file service. + * + * @param assetService the file service to use for file operations + */ + public AssetController(AssetService assetService) { + this.assetService = assetService; + } + + /** + * Uploads a file to the configured storage service. + * + * @param file the multipart file to upload + * @return ResponseEntity containing the file URL and metadata, or error message + */ + @PostMapping + public ResponseEntity uploadFile( + @RequestParam MultipartFile file + ) { + try { + if (file.isEmpty()) { + throw new BizException(HttpStatus.BAD_REQUEST, "File cannot be empty."); + } + + var fileUrl = assetService.uploadFile(AssetPrefix.UPLOADS, file); + + return ResponseEntity.ok() + .header("Location", fileUrl) + .body(new FileUploadResponse( + file.getOriginalFilename(), + file.getContentType(), + file.getSize(), + fileUrl + )); + } catch (Exception e) { + log.error("File upload failed: {}", e.getMessage(), e); + throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, + "Failed upload file: " + e.getMessage()); + } + } + + /** + * Delete an asset by asset ID. + * + * @param assetId asset ID + */ + @DeleteMapping("/{id:\\d+}") + public void deleteFile( + @PathVariable("id") Long assetId + ) { + assetService.deleteAsset(assetId); + } +} diff --git a/src/main/java/com/onixbyte/helix/controller/AuthController.java b/src/main/java/com/onixbyte/helix/controller/AuthController.java new file mode 100644 index 0000000..dbb49b6 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/controller/AuthController.java @@ -0,0 +1,39 @@ +package com.onixbyte.helix.controller; + +import com.onixbyte.helix.domain.web.request.UsernamePasswordLoginRequest; +import com.onixbyte.helix.domain.web.response.LoginSuccessResponse; +import com.onixbyte.helix.service.AuthService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + private static final Logger log = LoggerFactory.getLogger(AuthController.class); + private final AuthService authService; + + public AuthController(AuthService authService) { + this.authService = authService; + } + + /** + * Perform login with username and password. + * + * @param request login request + * @return detailed user info and authentication token + */ + @PostMapping("/login") + public LoginSuccessResponse loginWithUsernameAndPassword( + @Validated @RequestBody UsernamePasswordLoginRequest request + ) { + return authService.login(request); + } + + @GetMapping("/register-enabled") + public boolean getRegisterEnabled() { + return authService.getRegisterEnabled(); + } +} diff --git a/src/main/java/com/onixbyte/helix/controller/CaptchaController.java b/src/main/java/com/onixbyte/helix/controller/CaptchaController.java new file mode 100644 index 0000000..8afd721 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/controller/CaptchaController.java @@ -0,0 +1,29 @@ +package com.onixbyte.helix.controller; + +import com.onixbyte.helix.domain.web.response.CaptchaResponse; +import com.onixbyte.helix.service.CaptchaService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Optional; + +@RestController +@RequestMapping("/captcha") +public class CaptchaController { + + private final CaptchaService captchaService; + + public CaptchaController(CaptchaService captchaService) { + this.captchaService = captchaService; + } + + @GetMapping + public ResponseEntity getCaptcha() { + var captchaTuple = captchaService.buildCaptcha(); + return Optional.ofNullable(captchaTuple) + .map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.noContent().build()); + } +} diff --git a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java new file mode 100644 index 0000000..09565cd --- /dev/null +++ b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java @@ -0,0 +1,31 @@ +package com.onixbyte.helix.controller; + +import com.onixbyte.helix.domain.entity.Department; +import com.onixbyte.helix.domain.model.TreeNode; +import com.onixbyte.helix.service.DepartmentService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/departments") +public class DepartmentController { + + private final DepartmentService departmentService; + + public DepartmentController(DepartmentService departmentService) { + this.departmentService = departmentService; + } + + @GetMapping("/tree") + public TreeNode getDepartmentTree() { + return departmentService.getDepartmentTree(); + } + + @GetMapping + public List getDepartments() { + return departmentService.getDepartments(); + } +} diff --git a/src/main/java/com/onixbyte/helix/controller/ExceptionController.java b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java new file mode 100644 index 0000000..e47df94 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java @@ -0,0 +1,97 @@ +package com.onixbyte.helix.controller; + +import com.onixbyte.helix.domain.web.response.BizExceptionResponse; +import com.onixbyte.helix.exception.BizException; +import jakarta.validation.ConstraintViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.LocalDateTime; +import java.util.stream.Collectors; + +/** + * Global exception handler for the Helix application. + *

+ * This controller advice provides centralised exception handling across all controllers in + * the application. It intercepts exceptions thrown during request processing and converts them into + * appropriate HTTP responses with standardised error formats. + *

+ * The controller handles various types of exceptions including: + *

    + *
  • Business logic exceptions ({@link BizException})
  • + *
  • Bean validation constraint violations ({@link ConstraintViolationException})
  • + *
+ *

+ * All error responses are formatted consistently using {@link BizExceptionResponse} to provide a + * uniform API error structure for client applications. + * + * @author zihluwang + * @see BizException + * @see BizExceptionResponse + * @see RestControllerAdvice + * @since 1.0.0 + */ +@RestControllerAdvice +public class ExceptionController { + + + /** + * Handles business logic exceptions thrown throughout the application. + *

+ * This method intercepts {@link BizException} instances and converts them into appropriate HTTP + * responses. The HTTP status code is determined by the exception's status property, whilst the + * error message is extracted from the exception and included in the response body. + *

+ * The response includes a timestamp indicating when the error occurred and the specific error + * message describing the business logic violation. + * + * @param ex the business exception that was thrown + * @return a {@link ResponseEntity} containing the error response with appropriate HTTP status + * and {@link BizExceptionResponse} body + * @see BizException + * @see BizExceptionResponse + */ + @ExceptionHandler(BizException.class) + public ResponseEntity handleBizException(BizException ex) { + return ResponseEntity.status(ex.getStatus()) + .body(new BizExceptionResponse( + LocalDateTime.now(), + ex.getMessage()) + ); + } + + /** + * Handles bean validation constraint violation exceptions. + *

+ * This method processes {@link ConstraintViolationException} instances that occur when bean + * validation constraints are violated during request processing. It extracts all constraint + * violations, formats them into a readable error message, and returns a standardised + * error response. + *

+ * The error message includes the property path and violation message for each constraint that + * was violated, separated by commas for multiple violations. The response is automatically + * assigned a {@code 400 Bad Request} status. + * + * @param ex the constraint violation exception containing validation errors + * @return a {@link BizExceptionResponse} containing the formatted validation + * error messages and timestamp + * @see ConstraintViolationException + * @see BizExceptionResponse + * @see jakarta.validation.constraints + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public BizExceptionResponse handleConstraintViolation(ConstraintViolationException ex) { + var errorMessage = ex.getConstraintViolations().stream() + .map((violation) -> violation.getPropertyPath() + ": " + violation.getMessage()) + .collect(Collectors.joining(", ")); + + return new BizExceptionResponse( + LocalDateTime.now(), + errorMessage + ); + } +} diff --git a/src/main/java/com/onixbyte/helix/controller/MenuController.java b/src/main/java/com/onixbyte/helix/controller/MenuController.java new file mode 100644 index 0000000..e7b218f --- /dev/null +++ b/src/main/java/com/onixbyte/helix/controller/MenuController.java @@ -0,0 +1,26 @@ +package com.onixbyte.helix.controller; + +import com.onixbyte.helix.domain.entity.Menu; +import com.onixbyte.helix.domain.model.TreeNode; +import com.onixbyte.helix.service.MenuService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/menus") +public class MenuController { + + private final MenuService menuService; + + public MenuController(MenuService menuService) { + this.menuService = menuService; + } + + @GetMapping + public List> getMenuTree() { + return menuService.getMenuTree(); + } +} diff --git a/src/main/java/com/onixbyte/helix/controller/PositionController.java b/src/main/java/com/onixbyte/helix/controller/PositionController.java new file mode 100644 index 0000000..25d8681 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/controller/PositionController.java @@ -0,0 +1,31 @@ +package com.onixbyte.helix.controller; + +import com.onixbyte.helix.domain.entity.Position; +import com.onixbyte.helix.service.PositionService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +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; + +@RestController +@RequestMapping("/positions") +public class PositionController { + + private final PositionService positionService; + + public PositionController(PositionService positionService) { + this.positionService = positionService; + } + + @GetMapping + public Page getPositions( + @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @RequestParam(required = false, defaultValue = "10") Integer pageSize + ) { + var pageRequest = PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Order.asc("id"))); + return positionService.getPositions(pageRequest); + } +} diff --git a/src/main/java/com/onixbyte/helix/controller/RoleController.java b/src/main/java/com/onixbyte/helix/controller/RoleController.java new file mode 100644 index 0000000..8f25535 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/controller/RoleController.java @@ -0,0 +1,31 @@ +package com.onixbyte.helix.controller; + +import com.onixbyte.helix.domain.entity.Role; +import com.onixbyte.helix.domain.web.request.QueryRoleRequest; +import com.onixbyte.helix.service.RoleService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/roles") +public class RoleController { + + private final RoleService roleService; + + public RoleController(RoleService roleService) { + this.roleService = roleService; + } + + @GetMapping + public Page getRoles( + @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @RequestParam(required = false, defaultValue = "10") Integer pageSize, + @Validated @ModelAttribute QueryRoleRequest request + ) { + var pageRequest = PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Order.asc("id"))); + return roleService.getRoles(pageRequest, request); + } +} diff --git a/src/main/java/com/onixbyte/helix/controller/SettingController.java b/src/main/java/com/onixbyte/helix/controller/SettingController.java new file mode 100644 index 0000000..8adb7d3 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/controller/SettingController.java @@ -0,0 +1,14 @@ +package com.onixbyte.helix.controller; + +import com.onixbyte.helix.domain.entity.Setting; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping +public class SettingController { + + +} diff --git a/src/main/java/com/onixbyte/helix/controller/UserController.java b/src/main/java/com/onixbyte/helix/controller/UserController.java new file mode 100644 index 0000000..30d9f8e --- /dev/null +++ b/src/main/java/com/onixbyte/helix/controller/UserController.java @@ -0,0 +1,84 @@ +package com.onixbyte.helix.controller; + +import com.onixbyte.helix.domain.entity.User; +import com.onixbyte.helix.domain.web.request.AddUserRequest; +import com.onixbyte.helix.domain.web.request.QueryUserRequest; +import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; +import com.onixbyte.helix.domain.web.request.UpdateUserRequest; +import com.onixbyte.helix.domain.web.response.UserDetailResponse; +import com.onixbyte.helix.service.UserService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/users") +public class UserController { + + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + /** + * Get user list. + * + * @param pageNum page number + * @param pageSize page size + * @return paginated user list + */ + @PreAuthorize("hasAnyAuthority('system:user:read')") + @GetMapping + public Page queryUsers( + @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @RequestParam(required = false, defaultValue = "10") Integer pageSize, + @Validated @ModelAttribute QueryUserRequest request + ) { + var pageRequest = PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Order.asc("id"))); + return userService.queryUserDetailsPage(pageRequest, request); + } + + /** + * Get user by user ID. + * + * @param userId user ID + * @return paginated user list + */ + @PreAuthorize("hasAnyAuthority('system:user:read')") + @GetMapping("/{userId:\\d+}") + public UserDetailResponse getUserDetailByUserId(@PathVariable Long userId) { + return userService.getUserDetailByUserId(userId); + } + + @PostMapping + @PreAuthorize("hasAnyAuthority('system:user:write')") + public ResponseEntity addUser(@Validated @RequestBody AddUserRequest request) { + userService.addUser(request); + return ResponseEntity.ok(null); + } + + @PutMapping + public ResponseEntity editUser(@Validated @RequestBody UpdateUserRequest request) { + userService.updateUser(request); + return ResponseEntity.ok(null); + } + + @PreAuthorize("hasAnyAuthority('system:user:reset-password')") + @PatchMapping("/reset-password") + public ResponseEntity resetPassword(@Validated @RequestBody ResetPasswordRequest request) { + userService.resetPassword(request); + return ResponseEntity.ok(null); + } + + @PreAuthorize("hasAnyAuthority('system:user:write')") + @DeleteMapping("/{userId:\\d+}") + public ResponseEntity deleteUser(@PathVariable Long userId) { + userService.deleteUser(userId); + return ResponseEntity.ok(null); + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/common/Treeable.java b/src/main/java/com/onixbyte/helix/domain/common/Treeable.java new file mode 100644 index 0000000..f15a9d9 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/common/Treeable.java @@ -0,0 +1,34 @@ +package com.onixbyte.helix.domain.common; + +/** + * Represents an element that can be part of a tree structure. + *

+ * This interface provides the basic methods necessary for an object to be identified, + * linked to a parent, and sorted within its siblings in a tree-like hierarchy. + * + * @param the type of the key used for identification + */ +public interface Treeable { + /** + * Retrieves the sort order value for this element amongst its siblings. + * + * @return the integer sort value + */ + Integer getSort(); + + /** + * Retrieves the unique identifier for this element. + * + * @return the element's unique key + */ + K getId(); + + /** + * Retrieves the identifier of this element's parent. + *

+ * If the element is a root element, this method should return {@code null}. + * + * @return the key of the parent element, or {@code null} if it is a root + */ + K getParentId(); +} diff --git a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java new file mode 100644 index 0000000..29a2a8d --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java @@ -0,0 +1,44 @@ +package com.onixbyte.helix.domain.database.query.wrapper; + +import com.onixbyte.helix.constant.Status; + +public class QueryRoleWrapper { + private String name; + private String code; + private Status status; + + public QueryRoleWrapper() { + } + + public QueryRoleWrapper(String name, String code, Status status) { + this.name = name; + this.code = code; + this.status = status; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + +} diff --git a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java new file mode 100644 index 0000000..1ee80ae --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java @@ -0,0 +1,84 @@ +package com.onixbyte.helix.domain.database.query.wrapper; + +import com.onixbyte.helix.constant.UserStatus; + +import java.time.LocalDateTime; + +public class QueryUserWrapper { + private Long departmentId; + private String username; + private String regionAbbreviation; + private String phoneNumber; + private UserStatus status; + private LocalDateTime createdAtStart; + private LocalDateTime createdAtEnd; + + public QueryUserWrapper() { + } + + public QueryUserWrapper(Long departmentId, String username, String regionAbbreviation, String phoneNumber, UserStatus status, LocalDateTime createdAtStart, LocalDateTime createdAtEnd) { + this.departmentId = departmentId; + this.username = username; + this.regionAbbreviation = regionAbbreviation; + this.phoneNumber = phoneNumber; + this.status = status; + this.createdAtStart = createdAtStart; + this.createdAtEnd = createdAtEnd; + } + + public Long getDepartmentId() { + return departmentId; + } + + public void setDepartmentId(Long departmentId) { + this.departmentId = departmentId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getRegionAbbreviation() { + return regionAbbreviation; + } + + public void setRegionAbbreviation(String regionAbbreviation) { + this.regionAbbreviation = regionAbbreviation; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public UserStatus getStatus() { + return status; + } + + public void setStatus(UserStatus status) { + this.status = status; + } + + public LocalDateTime getCreatedAtStart() { + return createdAtStart; + } + + public void setCreatedAtStart(LocalDateTime createdAtStart) { + this.createdAtStart = createdAtStart; + } + + public LocalDateTime getCreatedAtEnd() { + return createdAtEnd; + } + + public void setCreatedAtEnd(LocalDateTime createdAtEnd) { + this.createdAtEnd = createdAtEnd; + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Asset.java b/src/main/java/com/onixbyte/helix/domain/entity/Asset.java new file mode 100644 index 0000000..c133dc5 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/Asset.java @@ -0,0 +1,126 @@ +package com.onixbyte.helix.domain.entity; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "assets") +public class Asset { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String key; + + @Column(nullable = false) + private Long uploadBy; + + @Column(nullable = false) + private LocalDateTime uploadTime; + + public Asset() { + } + + public Asset(Long id, String key, Long uploadBy, LocalDateTime uploadTime) { + this.id = id; + this.key = key; + this.uploadBy = uploadBy; + this.uploadTime = uploadTime; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Long getUploadBy() { + return uploadBy; + } + + public void setUploadBy(Long uploadBy) { + this.uploadBy = uploadBy; + } + + public LocalDateTime getUploadTime() { + return uploadTime; + } + + public void setUploadTime(LocalDateTime uploadTime) { + this.uploadTime = uploadTime; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Asset that = (Asset) o; + return Objects.equals(id, that.id) && Objects.equals(key, that.key) && Objects.equals(uploadBy, that.uploadBy) && Objects.equals(uploadTime, that.uploadTime); + } + + @Override + public int hashCode() { + return Objects.hash(id, key, uploadBy, uploadTime); + } + + @Override + public String toString() { + return "Attachment{" + + "id=" + id + + ", key='" + key + '\'' + + ", uploadBy=" + uploadBy + + ", uploadTime=" + uploadTime + + '}'; + } + + public static AssetBuilder builder() { + return new AssetBuilder(); + } + + public static class AssetBuilder { + private Long id; + private String key; + private Long uploadBy; + private LocalDateTime uploadTime; + + private AssetBuilder() { + } + + public AssetBuilder id(Long id) { + this.id = id; + return this; + } + + public AssetBuilder key(String key) { + this.key = key; + return this; + } + + public AssetBuilder uploadBy(Long uploadBy) { + this.uploadBy = uploadBy; + return this; + } + + public AssetBuilder uploadTime(LocalDateTime uploadTime) { + this.uploadTime = uploadTime; + return this; + } + + public Asset build() { + return new Asset(id, key, uploadBy, uploadTime); + } + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Authority.java b/src/main/java/com/onixbyte/helix/domain/entity/Authority.java new file mode 100644 index 0000000..1851591 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/Authority.java @@ -0,0 +1,265 @@ +package com.onixbyte.helix.domain.entity; + +import com.onixbyte.helix.constant.Status; +import jakarta.persistence.*; +import org.hibernate.annotations.JdbcType; +import org.hibernate.annotations.Type; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; +import org.springframework.security.core.GrantedAuthority; +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * Represents an authority (permission) entity within the access control system. + *

+ * Authorities define specific permissions or capabilities that can be granted to roles. They + * represent the finest level of access control granularity, allowing for precise + * permission management. Authorities are typically associated with specific actions or resources + * within the application. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +@Entity +@Table(name = "authorities") +public class Authority { + + /** + * The unique identifier for the authority. + *

+ * This serves as the primary key in the database and is used for all internal references to the + * authority entity. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The unique code identifier for the authority. + *

+ * This field contains a system-level identifier that uniquely identifies the permission. It is + * typically used in code for permission checks and should follow a consistent naming convention + * (e.g., "USER_READ", "ADMIN_WRITE"). + */ + @Column(unique = true, nullable = false, length = 128) + private String code; + + /** + * The human-readable name of the authority. + *

+ * This field contains the display name of the authority as it should appear in user interfaces + * and administrative panels for permission management. + */ + @Column(nullable = false, length = 128) + private String name; + + /** + * A detailed description of what this authority grants. + *

+ * This field provides additional context about what specific permissions or capabilities this + * authority represents, helping administrators understand the implications of granting + * this authority. + */ + @Column + private String description; + + /** + * The current status of the authority. + *

+ * This field determines whether the authority is active, inactive, or in any other state as + * defined by the {@link Status} enumeration. + */ + @Column(nullable = false) + @Enumerated + @JdbcType(PostgreSQLEnumJdbcType.class) + private Status status; + + /** + * The timestamp when this authority record was created. + *

+ * This field is automatically set when the authority entity is first persisted and provides + * audit information about when the authority was established. + */ + @Column + private LocalDateTime createdAt; + + /** + * The timestamp when this authority record was last updated. + *

+ * This field is automatically updated whenever any changes are made to the authority entity and + * provides audit information about the most recent modification. + */ + @Column + private LocalDateTime updatedAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public Authority() { + } + + public Authority(Long id, String code, String name, String description, Status status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.code = code; + this.name = name; + this.description = description; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Authority authority = (Authority) o; + return Objects.equals(id, authority.id) && Objects.equals(code, authority.code) && Objects.equals(name, authority.name) && Objects.equals(description, authority.description) && status == authority.status && Objects.equals(createdAt, authority.createdAt) && Objects.equals(updatedAt, authority.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, code, name, description, status, createdAt, updatedAt); + } + + @Override + public String toString() { + return "Authority{" + + "id=" + id + + ", code='" + code + '\'' + + ", name='" + name + '\'' + + ", description='" + description + '\'' + + ", status=" + status + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + /** + * Creates a new Builder instance for constructing Authority objects. + * + * @return a new AuthorityBuilder instance + */ + public static AuthorityBuilder builder() { + return new AuthorityBuilder(); + } + + /** + * Builder class for constructing Authority instances with a fluent API. + *

+ * This builder provides a convenient way to construct Authority objects with optional parameters, + * following the Builder pattern for improved readability and maintainability. + */ + public static class AuthorityBuilder { + private Long id; + private String code; + private String name; + private String description; + private Status status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private AuthorityBuilder() { + } + + public AuthorityBuilder id(Long id) { + this.id = id; + return this; + } + + public AuthorityBuilder code(String code) { + this.code = code; + return this; + } + + public AuthorityBuilder name(String name) { + this.name = name; + return this; + } + + public AuthorityBuilder description(String description) { + this.description = description; + return this; + } + + public AuthorityBuilder status(Status status) { + this.status = status; + return this; + } + + public AuthorityBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public AuthorityBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + /** + * Builds and returns a new Authority instance with the configured properties. + * + * @return a new Authority instance + */ + public Authority build() { + return new Authority(id, code, name, description, status, createdAt, updatedAt); + } + } + + public GrantedAuthority asGrantedAuthority() { + return this::getCode; + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Department.java b/src/main/java/com/onixbyte/helix/domain/entity/Department.java new file mode 100644 index 0000000..d4f0d40 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/Department.java @@ -0,0 +1,290 @@ +package com.onixbyte.helix.domain.entity; + +import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.domain.common.Treeable; +import jakarta.persistence.*; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; + +import java.time.LocalDateTime; +import java.util.Objects; + + +/** + * Represents a department entity within the organisational hierarchy. + *

+ * This entity models departments as hierarchical structures where each department can have a + * parent department, creating a tree-like organisational structure. Departments are used to group + * users and define organisational boundaries within the Helix system. + *

+ * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +@Entity +@Table(name = "departments") +public class Department implements Treeable { + + /** + * The unique identifier for the department. + *

+ * This serves as the primary key in the database and is used for all + * internal references to the department entity. + *

+ */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The name of the department. + *

+ * This field contains the human-readable name of the department as it + * should appear in the organisational chart and user interfaces. + *

+ */ + @Column(length = 128, nullable = false) + private String name; + + /** + * The identifier of the parent department. + *

+ * This field establishes the hierarchical relationship between departments. + * A null value indicates that this is a root-level department with no parent. + *

+ */ + @Column(nullable = false) + private Long parentId; + + /** + * The sort order for displaying departments. + *

+ * This field determines the order in which departments should be displayed + * when listed alongside their siblings in the hierarchy. Lower values + * indicate higher priority in sorting. + *

+ */ + @Column(nullable = false) + private Integer sort; + + /** + * The current status of the department. + *

+ * This field determines whether the department is active, inactive, or in any + * other state as defined by the {@link Status} enumeration. + *

+ */ + @Column(nullable = false) + @Enumerated + @JdbcType(PostgreSQLEnumJdbcType.class) + private Status status; + + /** + * The timestamp when this department record was created. + *

+ * This field is automatically set when the department entity is first persisted + * and provides audit information about when the department was established. + *

+ */ + @Column + private LocalDateTime createdAt; + + /** + * The timestamp when this department record was last updated. + *

+ * This field is automatically updated whenever any changes are made to the + * department entity and provides audit information about the most recent modification. + *

+ */ + @Column + private LocalDateTime updatedAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public Integer getSort() { + return sort; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + /** + * Default constructor for Department. + *

+ * Creates a new Department instance with all fields initialised to their default values. + * This constructor is typically used by JPA and other frameworks for entity instantiation. + *

+ */ + public Department() { + } + + /** + * Constructs a new Department with all specified parameters. + *

+ * This constructor allows for the creation of a fully initialised Department entity + * with all field values provided at instantiation time. + *

+ * + * @param id the unique identifier for the department + * @param name the name of the department + * @param parentId the identifier of the parent department (null for root departments) + * @param sort the sort order for display purposes + * @param status the current status of the department + * @param createdAt the timestamp when the department was created + * @param updatedAt the timestamp when the department was last updated + */ + public Department(Long id, String name, Long parentId, Integer sort, Status status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.name = name; + this.parentId = parentId; + this.sort = sort; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Department that = (Department) o; + return Objects.equals(id, that.id) && Objects.equals(name, that.name) && Objects.equals(parentId, that.parentId) && Objects.equals(sort, that.sort) && status == that.status && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, parentId, sort, status, createdAt, updatedAt); + } + + @Override + public String toString() { + return "Department{" + + "id=" + id + + ", name='" + name + '\'' + + ", parentId=" + parentId + + ", sort=" + sort + + ", status=" + status + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + /** + * Creates a new Builder instance for constructing Department objects. + * + * @return a new DepartmentBuilder instance + */ + public static DepartmentBuilder builder() { + return new DepartmentBuilder(); + } + + /** + * Builder class for constructing Department instances with a fluent API. + *

+ * This builder provides a convenient way to construct Department objects with optional parameters, + * following the Builder pattern for improved readability and maintainability. + */ + public static class DepartmentBuilder { + private Long id; + private String name; + private Long parentId; + private Integer sort; + private Status status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private DepartmentBuilder() { + } + + public DepartmentBuilder id(Long id) { + this.id = id; + return this; + } + + public DepartmentBuilder name(String name) { + this.name = name; + return this; + } + + public DepartmentBuilder parentId(Long parentId) { + this.parentId = parentId; + return this; + } + + public DepartmentBuilder sort(Integer sort) { + this.sort = sort; + return this; + } + + public DepartmentBuilder status(Status status) { + this.status = status; + return this; + } + + public DepartmentBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public DepartmentBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + /** + * Builds and returns a new Department instance with the configured properties. + * + * @return a new Department instance + */ + public Department build() { + return new Department(id, name, parentId, sort, status, createdAt, updatedAt); + } + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Menu.java b/src/main/java/com/onixbyte/helix/domain/entity/Menu.java new file mode 100644 index 0000000..c45dd3b --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/Menu.java @@ -0,0 +1,227 @@ +package com.onixbyte.helix.domain.entity; + +import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.domain.common.Treeable; +import jakarta.persistence.*; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "menus") +public class Menu implements Treeable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 50) + private String name; + + @Column + private Long parentId; + + @Column(nullable = false) + private String code; + + @Column(nullable = false) + private Integer sort; + + @Column + private String path; + + @Column + private Boolean isExternalLink; + + @Column + private Boolean isVisible; + + @Column(nullable = false) + @Enumerated + @JdbcType(PostgreSQLEnumJdbcType.class) + private Status status; + + @Column(length = 128) + private String authorityCode; + + @Column(length = 128) + private String icon; + + @Column + private LocalDateTime createdAt; + + @Column + private LocalDateTime updatedAt; + + public Menu() { + } + + public Menu(Long id, String name, Long parentId, String code, Integer sort, String path, Boolean isExternalLink, Boolean isVisible, Status status, String authorityCode, String icon, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.name = name; + this.parentId = parentId; + this.code = code; + this.sort = sort; + this.path = path; + this.isExternalLink = isExternalLink; + this.isVisible = isVisible; + this.status = status; + this.authorityCode = authorityCode; + this.icon = icon; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Boolean getExternalLink() { + return isExternalLink; + } + + public void setExternalLink(Boolean externalLink) { + isExternalLink = externalLink; + } + + public Boolean getVisible() { + return isVisible; + } + + public void setVisible(Boolean visible) { + isVisible = visible; + } + + public Integer getSort() { + return sort; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Boolean getIsExternalLink() { + return isExternalLink; + } + + public void setIsExternalLink(Boolean isExternalLink) { + this.isExternalLink = isExternalLink; + } + + public Boolean getIsVisible() { + return isVisible; + } + + public void setIsVisible(Boolean isVisible) { + this.isVisible = isVisible; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getAuthorityCode() { + return authorityCode; + } + + public void setAuthorityCode(String authorityCode) { + this.authorityCode = authorityCode; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Menu menu = (Menu) o; + return Objects.equals(id, menu.id) && Objects.equals(name, menu.name) && Objects.equals(parentId, menu.parentId) && Objects.equals(code, menu.code) && Objects.equals(sort, menu.sort) && Objects.equals(path, menu.path) && Objects.equals(isExternalLink, menu.isExternalLink) && Objects.equals(isVisible, menu.isVisible) && status == menu.status && Objects.equals(authorityCode, menu.authorityCode) && Objects.equals(icon, menu.icon) && Objects.equals(createdAt, menu.createdAt) && Objects.equals(updatedAt, menu.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, parentId, code, sort, path, isExternalLink, isVisible, status, authorityCode, icon, createdAt, updatedAt); + } + + @Override + public String toString() { + return "Menu{" + + "id=" + id + + ", name='" + name + '\'' + + ", parentId=" + parentId + + ", code=" + code + + ", sort=" + sort + + ", path=" + path + + ", isExternalLink=" + isExternalLink + + ", isVisible=" + isVisible + + ", status=" + status + + ", authorityCode='" + authorityCode + '\'' + + ", icon='" + icon + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Position.java b/src/main/java/com/onixbyte/helix/domain/entity/Position.java new file mode 100644 index 0000000..fc0733a --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/Position.java @@ -0,0 +1,284 @@ +package com.onixbyte.helix.domain.entity; + +import com.onixbyte.helix.constant.Status; +import jakarta.persistence.*; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * Represents a position entity within the organisational structure. + *

+ * Positions define job roles or titles that can be assigned to users within the organisation. They + * provide a way to categorise users based on their responsibilities and functions, complementing + * the department-based organisational hierarchy. Positions can be used for reporting, + * access control, and organisational management purposes. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +@Entity +@Table(name = "positions") +public class Position { + + /** + * The unique identifier for the position. + *

+ * This serves as the primary key in the database and is used for all internal references to the + * position entity. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The human-readable name of the position. + *

+ * This field contains the job title or position name as it should appear in + * organisational charts, user profiles, and administrative interfaces. + */ + @Column(nullable = false, length = 128, unique = true) + private String name; + + /** + * The unique code identifier for the position. + *

+ * This field contains a system-level identifier that uniquely identifies the position. It is + * typically used for integration purposes and should follow a consistent naming convention. + */ + @Column(nullable = false, length = 64, unique = true) + private String code; + + /** + * A detailed description of the position's responsibilities and requirements. + *

+ * This field provides additional context about the role, including key responsibilities, + * required skills, or other relevant information that helps define what this position entails. + */ + @Column + private String description; + + /** + * The sort order for displaying positions. + *

+ * This field determines the order in which positions should be displayed in lists and + * selection interfaces. Lower values indicate higher priority in sorting, which can reflect + * organisational hierarchy or importance. + */ + @Column(nullable = false) + private Integer sort; + + /** + * The current status of the position. + *

+ * This field determines whether the position is active, inactive, or in any other state as + * defined by the {@link Status} enumeration. + */ + @Column(nullable = false) + @Enumerated + @JdbcType(PostgreSQLEnumJdbcType.class) + private Status status; + + /** + * The timestamp when this position record was created. + *

+ * This field is automatically set when the position entity is first persisted and provides + * audit information about when the position was established. + */ + @Column + private LocalDateTime createdAt; + + /** + * The timestamp when this position record was last updated. + *

+ * This field is automatically updated whenever any changes are made to the position entity and + * provides audit information about the most recent modification. + */ + @Column + private LocalDateTime updatedAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getSort() { + return sort; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public Position() { + } + + public Position(Long id, String name, String code, String description, Integer sort, Status status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.name = name; + this.code = code; + this.description = description; + this.sort = sort; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Position position = (Position) o; + return Objects.equals(id, position.id) && Objects.equals(name, position.name) && Objects.equals(code, position.code) && Objects.equals(description, position.description) && Objects.equals(sort, position.sort) && status == position.status && Objects.equals(createdAt, position.createdAt) && Objects.equals(updatedAt, position.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, code, description, sort, status, createdAt, updatedAt); + } + + @Override + public String toString() { + return "Position{" + + "id=" + id + + ", name='" + name + '\'' + + ", code='" + code + '\'' + + ", description='" + description + '\'' + + ", sort=" + sort + + ", status=" + status + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + /** + * Creates a new Builder instance for constructing Position objects. + * + * @return a new PositionBuilder instance + */ + public static PositionBuilder builder() { + return new PositionBuilder(); + } + + /** + * Builder class for constructing Position instances with a fluent API. + *

+ * This builder provides a convenient way to construct Position objects with optional parameters, + * following the Builder pattern for improved readability and maintainability. + */ + public static class PositionBuilder { + private Long id; + private String name; + private String code; + private String description; + private Integer sort; + private Status status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private PositionBuilder() { + } + + public PositionBuilder id(Long id) { + this.id = id; + return this; + } + + public PositionBuilder name(String name) { + this.name = name; + return this; + } + + public PositionBuilder code(String code) { + this.code = code; + return this; + } + + public PositionBuilder description(String description) { + this.description = description; + return this; + } + + public PositionBuilder sort(Integer sort) { + this.sort = sort; + return this; + } + + public PositionBuilder status(Status status) { + this.status = status; + return this; + } + + public PositionBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public PositionBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + /** + * Builds and returns a new Position instance with the configured properties. + * + * @return a new Position instance + */ + public Position build() { + return new Position(id, name, code, description, sort, status, createdAt, updatedAt); + } + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Role.java b/src/main/java/com/onixbyte/helix/domain/entity/Role.java new file mode 100644 index 0000000..d000fba --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/Role.java @@ -0,0 +1,308 @@ +package com.onixbyte.helix.domain.entity; + +import com.onixbyte.helix.constant.Status; +import jakarta.persistence.*; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * Represents a role entity within the access control system. + *

+ * Roles define sets of permissions and responsibilities that can be assigned to users. They form + * the foundation of the role-based access control (RBAC) system, allowing for flexible and scalable + * permission management across the Helix application. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +@Entity +@Table(name = "roles") +public class Role { + + /** + * The unique identifier for the role. + *

+ * This serves as the primary key in the database and is used for all internal references to the + * role entity. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The human-readable name of the role. + *

+ * This field contains the display name of the role as it should appear in user interfaces and + * administrative panels. + */ + @Column(nullable = false, unique = true, length = 128) + private String name; + + /** + * The unique code identifier for the role. + *

+ * This field contains a system-level identifier that is typically used in code + * and configuration. It should be unique across all roles and follow a consistent + * naming convention. + */ + @Column(nullable = false, unique = true, length = 64) + private String code; + + /** + * The sort order for displaying roles. + *

+ * This field determines the order in which roles should be displayed in lists and + * selection interfaces. Lower values indicate higher priority in sorting. + */ + @Column(nullable = false) + private Integer sort; + + /** + * Indicates whether this role is assigned by default to new users. + *

+ * When set to true, this role will be automatically assigned to newly created user accounts. + * This is useful for defining baseline permissions that all users should have. + */ + @Column + private Boolean defaultValue; + + /** + * A detailed description of the role's purpose and permissions. + *

+ * This field provides additional context about what the role represents and what capabilities + * it grants to users who are assigned to it. + */ + @Column + private String description; + + /** + * The current status of the role. + *

+ * This field determines whether the role is active, inactive, or in any other state as defined + * by the {@link Status} enumeration. + */ + @Column(nullable = false) + @Enumerated + @JdbcType(PostgreSQLEnumJdbcType.class) + private Status status; + + /** + * The timestamp when this role record was created. + *

+ * This field is automatically set when the role entity is first persisted and provides audit + * information about when the role was established. + */ + @Column + private LocalDateTime createdAt; + + /** + * The timestamp when this role record was last updated. + *

+ * This field is automatically updated whenever any changes are made to the role entity and + * provides audit information about the most recent modification. + */ + @Column + private LocalDateTime updatedAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Integer getSort() { + return sort; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + public Boolean getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(Boolean defaultValue) { + this.defaultValue = defaultValue; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public Role() { + } + + public Role(Long id, String name, String code, Integer sort, Boolean defaultValue, String description, Status status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.name = name; + this.code = code; + this.sort = sort; + this.defaultValue = defaultValue; + this.description = description; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Role role = (Role) o; + return Objects.equals(id, role.id) && Objects.equals(name, role.name) && Objects.equals(code, role.code) && Objects.equals(sort, role.sort) && Objects.equals(defaultValue, role.defaultValue) && Objects.equals(description, role.description) && status == role.status && Objects.equals(createdAt, role.createdAt) && Objects.equals(updatedAt, role.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, code, sort, defaultValue, description, status, createdAt, updatedAt); + } + + @Override + public String toString() { + return "Role{" + + "id=" + id + + ", name='" + name + '\'' + + ", code='" + code + '\'' + + ", sort=" + sort + + ", defaultValue=" + defaultValue + + ", description='" + description + '\'' + + ", status=" + status + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + /** + * Creates a new Builder instance for constructing Role objects. + * + * @return a new RoleBuilder instance + */ + public static RoleBuilder builder() { + return new RoleBuilder(); + } + + /** + * Builder class for constructing Role instances with a fluent API. + *

+ * This builder provides a convenient way to construct Role objects with optional parameters, + * following the Builder pattern for improved readability and maintainability. + */ + public static class RoleBuilder { + private Long id; + private String name; + private String code; + private Integer sort; + private Boolean defaultValue; + private String description; + private Status status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private RoleBuilder() { + } + + public RoleBuilder id(Long id) { + this.id = id; + return this; + } + + public RoleBuilder name(String name) { + this.name = name; + return this; + } + + public RoleBuilder code(String code) { + this.code = code; + return this; + } + + public RoleBuilder sort(Integer sort) { + this.sort = sort; + return this; + } + + public RoleBuilder defaultValue(Boolean defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public RoleBuilder description(String description) { + this.description = description; + return this; + } + + public RoleBuilder status(Status status) { + this.status = status; + return this; + } + + public RoleBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public RoleBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + /** + * Builds and returns a new Role instance with the configured properties. + * + * @return a new Role instance + */ + public Role build() { + return new Role(id, name, code, sort, defaultValue, description, status, createdAt, updatedAt); + } + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java b/src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java new file mode 100644 index 0000000..ff876ea --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java @@ -0,0 +1,125 @@ +package com.onixbyte.helix.domain.entity; + +import com.onixbyte.helix.domain.entity.embeddable.RoleAuthorityId; +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * Represents the association entity between a Role and an Authority in a many-to-many relationship. + *

+ * This entity is mapped to the 'role_authorities' table and uses a composite primary key + * {@code @EmbeddedId} defined in the RoleAuthorityId class. It also includes the 'createdAt' + * auditing field. + */ +@Entity +@Table(name = "role_authorities") +public class RoleAuthority { + + /** + * The composite primary key of the association, mapped to the 'role_id' and 'authority_id' columns. + *

+ * This field embeds the RoleAuthorityId object which holds the foreign keys to the Role and Authority tables. + */ + @EmbeddedId + private RoleAuthorityId id; + + /** + * The timestamp when this role-authority association was created. + *

+ * This field is automatically set upon creation in the database. {@code @Column} maps the field + * to the 'created_at' column. + */ + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + public Long getRoleId() { + return this.id != null ? this.id.getRoleId() : null; + } + + public Long getAuthorityId() { + return this.id != null ? this.id.getAuthorityId() : null; + } + + public RoleAuthorityId getId() { + return id; + } + + public void setId(RoleAuthorityId id) { + this.id = id; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public RoleAuthority() { + // Initialise the ID for safety when created via JPA + this.id = new RoleAuthorityId(); + } + + public RoleAuthority(Long roleId, Long authorityId, LocalDateTime createdAt) { + this.id = new RoleAuthorityId(roleId, authorityId); + this.createdAt = createdAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RoleAuthority that = (RoleAuthority) o; + // Only check the primary key for entity equality + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + // Hash code based on the primary key + return Objects.hash(id); + } + + @Override + public String toString() { + return "RoleAuthority{" + + "roleId=" + (id != null ? id.getRoleId() : "null") + + ", authorityId=" + (id != null ? id.getAuthorityId() : "null") + + ", createdAt=" + createdAt + + '}'; + } + + public static RoleAuthorityBuilder builder() { + return new RoleAuthorityBuilder(); + } + + public static class RoleAuthorityBuilder { + private Long roleId; + private Long authorityId; + private LocalDateTime createdAt; + + private RoleAuthorityBuilder() { + } + + public RoleAuthorityBuilder roleId(Long roleId) { + this.roleId = roleId; + return this; + } + + public RoleAuthorityBuilder authorityId(Long authorityId) { + this.authorityId = authorityId; + return this; + } + + public RoleAuthorityBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public RoleAuthority build() { + return new RoleAuthority(roleId, authorityId, createdAt); + } + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Setting.java b/src/main/java/com/onixbyte/helix/domain/entity/Setting.java new file mode 100644 index 0000000..6bb6ce6 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/Setting.java @@ -0,0 +1,269 @@ +package com.onixbyte.helix.domain.entity; + +import com.onixbyte.helix.constant.SettingType; +import jakarta.persistence.*; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * Represents a hot-deployable application setting, stored in the 'settings' database table. + *

+ * This entity allows for dynamic configuration changes without application restarts. + */ +@Entity +@Table(name = "settings") +public class Setting { + + /** + * Setting unique identifier, mapped to the primary key 'id'. + *

+ * Uses BIGSERIAL (Long) and is set to auto-increment by the database. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * Setting name. Used as a unique key for retrieval. + */ + @Column(nullable = false, unique = true) + private String name; + + /** + * Setting description. + */ + @Column + private String description; + + /** + * The type of the value (e.g., BOOLEAN, INT, STRING). + *

+ * Mapped to the custom SQL type SETTING_TYPE, typically handled by JPA as an Enum. + */ + @Column(nullable = false) + @Enumerated + @JdbcType(PostgreSQLEnumJdbcType.class) + private SettingType type; + + /** + * Setting current value. Stored as a string regardless of the actual type. + */ + @Column + private String value; + + /** + * Setting default value. + */ + @Column(nullable = false) + private String defaultValue; + + /** + * The timestamp when this setting was created. + *

+ * Set only on creation and remains unchanged. + */ + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + /** + * The timestamp when this setting was last updated. + *

+ * Updated on every change to the entity. + */ + @Column(nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } + + public Setting() { + } + + public Setting(Long id, String name, String description, SettingType type, String value, String defaultValue, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.name = name; + this.description = description; + this.type = type; + this.value = value; + this.defaultValue = defaultValue; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public SettingType getType() { + return type; + } + + public void setType(SettingType type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public String fetchValueOrDefault() { + if (Objects.nonNull(value) && !value.isBlank()) { + return value; + } + return defaultValue; + } + + public Boolean asBoolean() { + var val = fetchValueOrDefault(); + if (type == SettingType.BOOLEAN) { + return Boolean.parseBoolean(val); + } else { + return null; + } + } + + public Integer asInt() { + try { + var val = fetchValueOrDefault(); + if (type == SettingType.INT) { + return Integer.parseInt(val); + } + return null; + } catch (NumberFormatException e) { + return null; + } + } + + public Long asLong() { + try { + var val = fetchValueOrDefault(); + if (type == SettingType.INT) { + return Long.parseLong(val); + } + return null; + } catch (NumberFormatException e) { + return null; + } + } + + public static SettingBuilder builder() { + return new SettingBuilder(); + } + + public static class SettingBuilder { + private Long id; + private String name; + private String description; + private SettingType type; + private String value; + private String defaultValue; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private SettingBuilder() { + } + + public SettingBuilder id(Long id) { + this.id = id; + return this; + } + + public SettingBuilder name(String name) { + this.name = name; + return this; + } + + public SettingBuilder description(String description) { + this.description = description; + return this; + } + + public SettingBuilder type(SettingType type) { + this.type = type; + return this; + } + + public SettingBuilder value(String value) { + this.value = value; + return this; + } + + public SettingBuilder defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public SettingBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public SettingBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + public Setting build() { + return new Setting(id, name, description, type, value, defaultValue, createdAt, updatedAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/entity/User.java b/src/main/java/com/onixbyte/helix/domain/entity/User.java new file mode 100644 index 0000000..eae7198 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/User.java @@ -0,0 +1,453 @@ +package com.onixbyte.helix.domain.entity; + +import com.onixbyte.helix.constant.UserStatus; +import jakarta.persistence.*; // 导入 Jakarta Persistence API +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * Represents a user entity in the Helix system. + *

+ * This entity encapsulates all user-related information including authentication credentials, + * personal details, contact information, and organisational associations. Users are the core + * entities that interact with the system and are associated with departments and positions within + * the organisational hierarchy. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +@Entity +@Table( + name = "users", + uniqueConstraints = { + @UniqueConstraint(name = "uidx_users_username", columnNames = {"username"}), + @UniqueConstraint(name = "uidx_users_email", columnNames = {"email"}), + @UniqueConstraint(name = "uidx_users_region_abbreviation_phone_number", columnNames = {"region_abbreviation", "phone_number"}) + }, + indexes = {@Index(name = "users_username_index", columnList = "username")} +) +public class User { + + /** + * The unique identifier for the user. + *

+ * This serves as the primary key in the database and is used for all internal references to the + * user entity. Since the SQL uses `BIGINT PRIMARY KEY` without `SERIAL`, we assume the ID is assigned manually or by an external service. + */ + @Id + @Column(nullable = false) + private Long id; + + /** + * The unique username for authentication purposes. + */ + @Column(nullable = false, length = 64) + private String username; + + /** + * The encrypted password for user authentication. + */ + @Column + private String password; + + /** + * The user's complete full name. + */ + @Column(nullable = false, length = 128) + private String fullName; + + /** + * The user's email address. + */ + @Column(length = 128) + private String email; + + /** + * The region abbreviation for the user's phone number. + */ + @Column(length = 10) + private String regionAbbreviation; + + /** + * The user's phone number without the country code. + */ + @Column(length = 32) + private String phoneNumber; + + /** + * The URL to the user's avatar image. + */ + @Column(columnDefinition = "TEXT") + private String avatarUrl; + + /** + * The current status of the user account. + *

+ * Mapped to the custom SQL type USER_STATUS. + */ + @Column(nullable = false) + @Enumerated + @JdbcType(PostgreSQLEnumJdbcType.class) + private UserStatus status; + + /** + * The identifier of the department to which this user belongs. + *

+ * {@code @ManyToOne} is the standard way to map a foreign key (department_id) in JPA. + * You might replace this with a direct {@code @ManyToOne} mapping to the Department entity later. + */ + @Column + private Long departmentId; + + /** + * The identifier of the position held by this user. + */ + @Column + private Long positionId; + + /** + * The timestamp when this user record was created. + */ + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + /** + * The timestamp when this user record was last updated. + */ + @Column(nullable = false) + private LocalDateTime updatedAt; + + // --- JPA Lifecycle Callbacks for Auditing --- + + @PrePersist + protected void onCreate() { + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + if (this.updatedAt == null) { + this.updatedAt = LocalDateTime.now(); + } + + // Ensure status default is applied if not set + if (this.status == null) { + this.status = UserStatus.ACTIVE; + } + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } + + // --- Constructors, Getters, Setters, and Builder (omitted for brevity) --- + + public User() { + } + + public User(Long id, String username, String password, String fullName, String email, String regionAbbreviation, String phoneNumber, String avatarUrl, UserStatus status, Long departmentId, Long positionId, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.username = username; + this.password = password; + this.fullName = fullName; + this.email = email; + this.regionAbbreviation = regionAbbreviation; + this.phoneNumber = phoneNumber; + this.avatarUrl = avatarUrl; + this.status = status; + this.departmentId = departmentId; + this.positionId = positionId; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + /** + * Gets the unique identifier for this user. + * + * @return the user's unique identifier + */ + public Long getId() { + return id; + } + + /** + * Sets the unique identifier for this user. + * + * @param id the user's unique identifier + */ + public void setId(Long id) { + this.id = id; + } + + /** + * Gets the username for authentication. + * + * @return the username + */ + public String getUsername() { + return username; + } + + /** + * Sets the username for authentication. + * + * @param username the username, must be unique across the system + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Gets the encrypted password. + * + * @return the encrypted password + */ + public String getPassword() { + return password; + } + + /** + * Sets the encrypted password. + * + * @param password the encrypted password (never plain text) + */ + public void setPassword(String password) { + this.password = password; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + /** + * Gets the user's email address. + * + * @return the email address + */ + public String getEmail() { + return email; + } + + /** + * Sets the user's email address. + * + * @param email the email address, must be unique across the system + */ + public void setEmail(String email) { + this.email = email; + } + + public String getRegionAbbreviation() { + return regionAbbreviation; + } + + public void setRegionAbbreviation(String countryCode) { + this.regionAbbreviation = countryCode; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + /** + * Gets the current status of the user account. + * + * @return the user status + */ + public UserStatus getStatus() { + return status; + } + + /** + * Sets the current status of the user account. + * + * @param status the user status + */ + public void setStatus(UserStatus status) { + this.status = status; + } + + public Long getDepartmentId() { + return departmentId; + } + + public void setDepartmentId(Long departmentId) { + this.departmentId = departmentId; + } + + public Long getPositionId() { + return positionId; + } + + public void setPositionId(Long positionId) { + this.positionId = positionId; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(id, user.id); // Typically only check the Primary Key for entity equality + } + + @Override + public int hashCode() { + return Objects.hash(id); // Hash code based on the Primary Key + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", username='" + username + '\'' + + ", fullName='" + fullName + '\'' + + ", email='" + email + '\'' + + ", status=" + status + + ", departmentId=" + departmentId + + ", positionId=" + positionId + + '}'; + } + + /** + * Creates a new Builder instance for constructing User objects. + * + * @return a new UserBuilder instance + */ + public static UserBuilder builder() { + return new UserBuilder(); + } + + /** + * Builder class for constructing User instances with a fluent API. + *

+ * This builder provides a convenient way to construct User objects with optional parameters, + * following the Builder pattern for improved readability and maintainability. + */ + public static class UserBuilder { + private Long id; + private String username; + private String password; + private String fullName; + private String email; + private String regionAbbreviation; + private String phoneNumber; + private String avatarUrl; + private UserStatus status; + private Long departmentId; + private Long positionId; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private UserBuilder() { + } + + public UserBuilder id(Long id) { + this.id = id; + return this; + } + + public UserBuilder username(String username) { + this.username = username; + return this; + } + + public UserBuilder password(String password) { + this.password = password; + return this; + } + + public UserBuilder fullName(String fullName) { + this.fullName = fullName; + return this; + } + + public UserBuilder email(String email) { + this.email = email; + return this; + } + + public UserBuilder regionAbbreviation(String regionAbbreviation) { + this.regionAbbreviation = regionAbbreviation; + return this; + } + + public UserBuilder phoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + return this; + } + + public UserBuilder avatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + return this; + } + + public UserBuilder status(UserStatus status) { + this.status = status; + return this; + } + + public UserBuilder departmentId(Long departmentId) { + this.departmentId = departmentId; + return this; + } + + public UserBuilder positionId(Long positionId) { + this.positionId = positionId; + return this; + } + + public UserBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public UserBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + /** + * Builds and returns a new User instance with the configured properties. + * + * @return a new User instance + */ + public User build() { + return new User(id, username, password, fullName, email, regionAbbreviation, phoneNumber, avatarUrl, status, departmentId, positionId, createdAt, updatedAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java b/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java new file mode 100644 index 0000000..caf8d02 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java @@ -0,0 +1,227 @@ +package com.onixbyte.helix.domain.entity; + +import com.onixbyte.helix.constant.IdentityProvider; +import com.onixbyte.helix.domain.entity.embeddable.UserIdentityId; +import jakarta.persistence.*; // 导入 Jakarta Persistence API +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * Represents an external identity mapping for a user. + *

+ * This entity manages the relationship between internal user accounts and external identity + * providers (such as OAuth providers, LDAP systems, or other authentication services). It enables + * users to authenticate using external credentials while maintaining a consistent internal user + * identity within the Helix system. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +@Entity +@Table(name = "user_identities") +public class UserIdentity { + + /** + * The composite primary key for the entity, composed of userId, provider, and externalId. + */ + @EmbeddedId + private UserIdentityId id; + + /** + * The timestamp when this identity mapping was created. + */ + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + /** + * The timestamp when this identity mapping was last updated. + */ + @Column(nullable = false) + private LocalDateTime updatedAt; + + // --- JPA Lifecycle Callbacks for Auditing --- + + @PrePersist + protected void onCreate() { + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + if (this.updatedAt == null) { + this.updatedAt = LocalDateTime.now(); + } + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } + + + // --- Getters and Setters (Delegating to EmbeddedId) --- + + /** + * Gets the identifier of the internal user account from the composite primary key. + * @return the user ID + */ + public Long getUserId() { + return this.id != null ? this.id.getUserId() : null; + } + + /** + * Sets the identifier of the internal user account within the composite primary key. + * @param userId the user ID + */ + public void setUserId(Long userId) { + if (this.id == null) this.id = new UserIdentityId(); + this.id.setUserId(userId); + } + + /** + * Gets the external identity provider from the composite primary key. + * @return the provider + */ + public IdentityProvider getProvider() { + return this.id != null ? this.id.getProvider() : null; + } + + /** + * Sets the external identity provider within the composite primary key. + * @param provider the provider + */ + public void setProvider(IdentityProvider provider) { + if (this.id == null) this.id = new UserIdentityId(); + this.id.setProvider(provider); + } + + /** + * Gets the unique identifier from the external provider from the composite primary key. + * @return the external ID + */ + public String getExternalId() { + return this.id != null ? this.id.getExternalId() : null; + } + + /** + * Sets the unique identifier from the external provider within the composite primary key. + * @param externalId the external ID + */ + public void setExternalId(String externalId) { + if (this.id == null) this.id = new UserIdentityId(); + this.id.setExternalId(externalId); + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + // --- Constructors (Adjusted for EmbeddedId) --- + + public UserIdentity() { + this.id = new UserIdentityId(); // Initialize ID object for safety + } + + public UserIdentity(Long userId, IdentityProvider provider, String externalId, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = new UserIdentityId(userId, provider, externalId); + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + // --- Overrides (Simplified to use the Id object for entity equality) --- + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserIdentity that = (UserIdentity) o; + return Objects.equals(id, that.id); // Entity equality based on primary key + } + + @Override + public int hashCode() { + return Objects.hash(id); // Hash code based on primary key + } + + @Override + public String toString() { + return "UserIdentity{" + + "userId=" + getUserId() + + ", provider=" + getProvider() + + ", externalId='" + getExternalId() + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + /** + * Creates a new Builder instance for constructing UserIdentity objects. + * + * @return a new UserIdentityBuilder instance + */ + public static UserIdentityBuilder builder() { + return new UserIdentityBuilder(); + } + + /** + * Builder class for constructing UserIdentity instances with a fluent API. + *

+ * This builder provides a convenient way to construct UserIdentity objects with optional parameters, + * following the Builder pattern for improved readability and maintainability. + */ + public static class UserIdentityBuilder { + private Long userId; + private IdentityProvider provider; + private String externalId; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private UserIdentityBuilder() { + } + + public UserIdentityBuilder userId(Long userId) { + this.userId = userId; + return this; + } + + public UserIdentityBuilder provider(IdentityProvider provider) { + this.provider = provider; + return this; + } + + public UserIdentityBuilder externalId(String externalId) { + this.externalId = externalId; + return this; + } + + public UserIdentityBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public UserIdentityBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + /** + * Builds and returns a new UserIdentity instance with the configured properties. + * + * @return a new UserIdentity instance + */ + public UserIdentity build() { + return new UserIdentity(userId, provider, externalId, createdAt, updatedAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/entity/UserRole.java b/src/main/java/com/onixbyte/helix/domain/entity/UserRole.java new file mode 100644 index 0000000..794bf40 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/UserRole.java @@ -0,0 +1,176 @@ +package com.onixbyte.helix.domain.entity; + +import com.onixbyte.helix.domain.entity.embeddable.UserRoleId; +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * Represents the association between users and roles in the access control system. + *

+ * This entity implements the many-to-many relationship between users and roles, allowing users to + * be assigned multiple roles and roles to be assigned to multiple users. It forms a fundamental + * part of the role-based access control (RBAC) system by defining which roles are assigned to + * each user, thereby determining their permissions and access levels within the system. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +@Entity +@Table(name = "user_roles") +public class UserRole { + + /** + * The composite primary key of the association, mapped to the 'user_id' and 'role_id' columns. + *

+ * This field embeds the UserRoleId object which holds the foreign keys to the User and Role tables. + */ + @EmbeddedId + private UserRoleId id; + + /** + * The timestamp when this user-role assignment was created. + *

+ * This field is automatically set upon creation in the database. + */ + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + // --- JPA Lifecycle Callbacks for Auditing --- + + @PrePersist + protected void onCreate() { + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + } + + // --- Getters and Setters (Delegating to EmbeddedId and fields) --- + + /** + * Gets the identifier of the role from the composite primary key. + * + * @return the role's unique identifier + */ + public Long getRoleId() { + return this.id != null ? this.id.getRoleId() : null; + } + + /** + * Sets the identifier of the role within the composite primary key. + * + * @param roleId the role's unique identifier + */ + public void setRoleId(Long roleId) { + if (this.id == null) this.id = new UserRoleId(); + this.id.setRoleId(roleId); + } + + /** + * Gets the identifier of the user from the composite primary key. + * + * @return the user's unique identifier + */ + public Long getUserId() { + return this.id != null ? this.id.getUserId() : null; + } + + /** + * Sets the identifier of the user within the composite primary key. + * + * @param userId the user's unique identifier + */ + public void setUserId(Long userId) { + if (this.id == null) this.id = new UserRoleId(); + this.id.setUserId(userId); + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public UserRole() { + this.id = new UserRoleId(); + } + + public UserRole(Long roleId, Long userId, LocalDateTime createdAt) { + this.id = new UserRoleId(userId, roleId); + this.createdAt = createdAt; + } + + // --- Overrides (Simplified to use the Id object for entity equality) --- + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserRole userRole = (UserRole) o; + return Objects.equals(id, userRole.id); // Entity equality based on primary key + } + + @Override + public int hashCode() { + return Objects.hash(id); // Hash code based on primary key + } + + @Override + public String toString() { + return "UserRole{" + + "roleId=" + getRoleId() + + ", userId=" + getUserId() + + ", createdAt=" + createdAt + + '}'; + } + + // --- Builder Class (Adjusted to build the Id object) --- + + /** + * Creates a new Builder instance for constructing UserRole objects. + * + * @return a new UserRoleBuilder instance + */ + public static UserRoleBuilder builder() { + return new UserRoleBuilder(); + } + + /** + * Builder class for constructing UserRole instances with a fluent API. + */ + public static class UserRoleBuilder { + private Long roleId; + private Long userId; + private LocalDateTime createdAt; + + private UserRoleBuilder() { + } + + public UserRoleBuilder roleId(Long roleId) { + this.roleId = roleId; + return this; + } + + public UserRoleBuilder userId(Long userId) { + this.userId = userId; + return this; + } + + public UserRoleBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + /** + * Builds and returns a new UserRole instance with the configured properties. + * + * @return a new UserRole instance + */ + public UserRole build() { + return new UserRole(roleId, userId, createdAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/entity/embeddable/RoleAuthorityId.java b/src/main/java/com/onixbyte/helix/domain/entity/embeddable/RoleAuthorityId.java new file mode 100644 index 0000000..0153899 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/embeddable/RoleAuthorityId.java @@ -0,0 +1,70 @@ +package com.onixbyte.helix.domain.entity.embeddable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; + +/** + * Represents the composite primary key for the RoleAuthority association entity. + */ +@Embeddable +public class RoleAuthorityId implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * The identifier of the role, mapping to the 'role_id' column. + *

+ * This field should match the corresponding field name in the RoleAuthority entity + * if the naming is non-standard, but typically matches the column name in the database. + */ + @Column(nullable = false) + private Long roleId; + + /** + * The identifier of the authority, mapping to the 'authority_id' column. + */ + @Column(nullable = false) + private Long authorityId; + + public RoleAuthorityId() { + } + + public RoleAuthorityId(Long roleId, Long authorityId) { + this.roleId = roleId; + this.authorityId = authorityId; + } + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } + + public Long getAuthorityId() { + return authorityId; + } + + public void setAuthorityId(Long authorityId) { + this.authorityId = authorityId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RoleAuthorityId that = (RoleAuthorityId) o; + return Objects.equals(roleId, that.roleId) && Objects.equals(authorityId, that.authorityId); + } + + @Override + public int hashCode() { + return Objects.hash(roleId, authorityId); + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserIdentityId.java b/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserIdentityId.java new file mode 100644 index 0000000..f400040 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserIdentityId.java @@ -0,0 +1,97 @@ +package com.onixbyte.helix.domain.entity.embeddable; + +import com.onixbyte.helix.constant.IdentityProvider; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import org.hibernate.annotations.JdbcType; +import org.hibernate.dialect.PostgreSQLEnumJdbcType; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; + +/** + * Represents the composite primary key for the UserIdentity entity. + *

+ * This key is composed of the internal user ID, the identity provider, and the external ID + * from that provider. + */ +@Embeddable +public class UserIdentityId implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * The identifier of the internal user account, corresponding to the 'user_id' column. + *

+ * This also serves as a foreign key reference to the User entity. + */ + @Column(nullable = false) + private Long userId; + + /** + * The external identity provider, corresponding to the 'provider' column. + */ + @Column(nullable = false) + @Enumerated + @JdbcType(PostgreSQLEnumJdbcType.class) + private IdentityProvider provider; + + /** + * The unique identifier from the external provider, corresponding to the 'external_id' column. + */ + @Column(nullable = false) + private String externalId; + + // --- Constructors --- + + public UserIdentityId() { + } + + public UserIdentityId(Long userId, IdentityProvider provider, String externalId) { + this.userId = userId; + this.provider = provider; + this.externalId = externalId; + } + + // --- Getters and Setters (Omitted for brevity, but should exist) --- + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public IdentityProvider getProvider() { + return provider; + } + + public void setProvider(IdentityProvider provider) { + this.provider = provider; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + // --- equals and hashCode (REQUIRED for composite keys) --- + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + UserIdentityId that = (UserIdentityId) o; + return Objects.equals(userId, that.userId) && provider == that.provider && Objects.equals(externalId, that.externalId); + } + + @Override + public int hashCode() { + return Objects.hash(userId, provider, externalId); + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserRoleId.java b/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserRoleId.java new file mode 100644 index 0000000..23de928 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserRoleId.java @@ -0,0 +1,72 @@ +package com.onixbyte.helix.domain.entity.embeddable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; + +/** + * Represents the composite primary key for the UserRole association entity. + *

+ * This class combines the userId and roleId to uniquely identify a user's role assignment. + */ +@Embeddable +public class UserRoleId implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * The identifier of the user in this association, mapping to the 'user_id' column. + */ + @Column(nullable = false) + private Long userId; + + /** + * The identifier of the role in this association, mapping to the 'role_id' column. + */ + @Column(nullable = false) + private Long roleId; + + // --- Constructors --- + public UserRoleId() { + } + + public UserRoleId(Long userId, Long roleId) { + this.userId = userId; + this.roleId = roleId; + } + + // --- Getters and Setters --- + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } + + // --- equals and hashCode (REQUIRED for composite keys) --- + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserRoleId that = (UserRoleId) o; + return Objects.equals(userId, that.userId) && Objects.equals(roleId, that.roleId); + } + + @Override + public int hashCode() { + return Objects.hash(userId, roleId); + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/model/TreeNode.java b/src/main/java/com/onixbyte/helix/domain/model/TreeNode.java new file mode 100644 index 0000000..3748a6b --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/model/TreeNode.java @@ -0,0 +1,18 @@ +package com.onixbyte.helix.domain.model; + +import java.util.ArrayList; +import java.util.List; + +public record TreeNode( + T item, + List> children +) { + + /** + * Helper constructor for building. + * @param item the item + */ + public TreeNode(T item) { + this(item, new ArrayList<>()); + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/view/AuthorityView.java b/src/main/java/com/onixbyte/helix/domain/view/AuthorityView.java new file mode 100644 index 0000000..1db4664 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/view/AuthorityView.java @@ -0,0 +1,264 @@ +package com.onixbyte.helix.domain.view; + +import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.domain.entity.Authority; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * View object for Authority entity. + *

+ * This class represents the view layer abstraction of an Authority, providing a simplified + * representation suitable for presentation in user interfaces and API responses. + * + * @author Zihlu Wang + * @version 1.0.0 + * @since 1.0.0 + */ +public class AuthorityView { + + /** + * The unique identifier for the authority. + */ + private Long id; + + /** + * The unique code identifier for the authority. + */ + private String code; + + /** + * The human-readable name of the authority. + */ + private String name; + + /** + * A detailed description of what this authority grants. + */ + private String description; + + /** + * The current status of the authority. + */ + private Status status; + + /** + * The timestamp when this authority record was created. + */ + private LocalDateTime createdAt; + + /** + * The timestamp when this authority record was last updated. + */ + private LocalDateTime updatedAt; + + /** + * Default constructor for serialisation frameworks. + */ + public AuthorityView() { + } + + /** + * Constructs a new AuthorityView with all fields specified. + * + * @param id the unique identifier + * @param code the unique code identifier + * @param name the human-readable name + * @param description the detailed description + * @param status the current status + * @param createdAt the creation timestamp + * @param updatedAt the last update timestamp + */ + public AuthorityView(Long id, String code, String name, String description, Status status, + LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.code = code; + this.name = name; + this.description = description; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + /** + * Creates an AuthorityView from an Authority entity. + * + * @param entity the Authority entity + * @return a new AuthorityView instance + */ + public static AuthorityView fromEntity(Authority entity) { + if (entity == null) { + return null; + } + return new AuthorityView( + entity.getId(), + entity.getCode(), + entity.getName(), + entity.getDescription(), + entity.getStatus(), + entity.getCreatedAt(), + entity.getUpdatedAt() + ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AuthorityView that = (AuthorityView) o; + return Objects.equals(id, that.id) && + Objects.equals(code, that.code) && + Objects.equals(name, that.name) && + Objects.equals(description, that.description) && + status == that.status && + Objects.equals(createdAt, that.createdAt) && + Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, code, name, description, status, createdAt, updatedAt); + } + + @Override + public String toString() { + return "AuthorityView{" + + "id=" + id + + ", code='" + code + '\'' + + ", name='" + name + '\'' + + ", description='" + description + '\'' + + ", status=" + status + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + /** + * Returns a builder for constructing AuthorityView instances. + * + * @return a new AuthorityViewBuilder instance + */ + public static AuthorityViewBuilder builder() { + return new AuthorityViewBuilder(); + } + + /** + * Builder class for constructing {@link AuthorityView} instances. + *

+ * This builder provides a fluent interface for creating AuthorityView objects with optional + * parameters, following the builder pattern for improved readability and maintainability. + */ + public static class AuthorityViewBuilder { + private Long id; + private String code; + private String name; + private String description; + private Status status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private AuthorityViewBuilder() { + } + + public AuthorityViewBuilder id(Long id) { + this.id = id; + return this; + } + + public AuthorityViewBuilder code(String code) { + this.code = code; + return this; + } + + public AuthorityViewBuilder name(String name) { + this.name = name; + return this; + } + + public AuthorityViewBuilder description(String description) { + this.description = description; + return this; + } + + public AuthorityViewBuilder status(Status status) { + this.status = status; + return this; + } + + public AuthorityViewBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public AuthorityViewBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + /** + * Builds and returns a new AuthorityView instance with the configured parameters. + * + * @return a new AuthorityView instance + */ + public AuthorityView build() { + return new AuthorityView(id, code, name, description, status, createdAt, updatedAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java b/src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java new file mode 100644 index 0000000..a72c58b --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java @@ -0,0 +1,270 @@ +package com.onixbyte.helix.domain.view; + +import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.domain.entity.Department; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * View object for Department entity. + *

+ * This class represents the view layer abstraction of a Department, providing a simplified + * representation suitable for presentation in user interfaces and API responses. + * + * @author Zihlu Wang + * @version 1.0.0 + * @since 1.0.0 + */ +public class DepartmentView { + + /** + * The unique identifier for the department. + */ + private Long id; + + /** + * The name of the department. + */ + private String name; + + /** + * The identifier of the parent department. + */ + private Long parentId; + + /** + * The sort order for displaying departments. + */ + private Integer sort; + + /** + * The current status of the department. + */ + private Status status; + + /** + * The timestamp when this department record was created. + */ + private LocalDateTime createdAt; + + /** + * The timestamp when this department record was last updated. + */ + private LocalDateTime updatedAt; + + /** + * Default constructor for serialisation frameworks. + */ + public DepartmentView() { + } + + /** + * Constructs a new DepartmentView with all fields specified. + * + * @param id the unique identifier + * @param name the name of the department + * @param parentId the identifier of the parent department + * @param sort the sort order for displaying departments + * @param status the current status + * @param createdAt the creation timestamp + * @param updatedAt the last update timestamp + */ + public DepartmentView(Long id, String name, Long parentId, Integer sort, + Status status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.name = name; + this.parentId = parentId; + this.sort = sort; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + /** + * Creates a DepartmentView from a Department entity. + * + * @param entity the Department entity + * @return a new DepartmentView instance + */ + public static DepartmentView fromEntity(Department entity) { + if (entity == null) { + return null; + } + return new DepartmentView( + entity.getId(), + entity.getName(), + entity.getParentId(), + entity.getSort(), + entity.getStatus(), + entity.getCreatedAt(), + entity.getUpdatedAt() + ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public Integer getSort() { + return sort; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DepartmentView that = (DepartmentView) o; + return Objects.equals(id, that.id) && + Objects.equals(name, that.name) && + Objects.equals(parentId, that.parentId) && + Objects.equals(sort, that.sort) && + status == that.status && + Objects.equals(createdAt, that.createdAt) && + Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, parentId, sort, status, createdAt, updatedAt); + } + + @Override + public String toString() { + return "DepartmentView{" + + "id=" + id + + ", name='" + name + '\'' + + ", parentId=" + parentId + + ", sort=" + sort + + ", status=" + status + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + /** + * Returns a builder for constructing DepartmentView instances. + * + * @return a new DepartmentViewBuilder instance + */ + public static DepartmentViewBuilder builder() { + return new DepartmentViewBuilder(); + } + + /** + * Builder class for constructing {@link DepartmentView} instances. + *

+ * This builder provides a fluent interface for creating DepartmentView objects with optional + * parameters, following the builder pattern for improved readability and maintainability. + */ + public static class DepartmentViewBuilder { + private Long id; + private String name; + private Long parentId; + private String treePath; + private Integer sort; + private Status status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private DepartmentViewBuilder() { + } + + public DepartmentViewBuilder id(Long id) { + this.id = id; + return this; + } + + public DepartmentViewBuilder name(String name) { + this.name = name; + return this; + } + + public DepartmentViewBuilder parentId(Long parentId) { + this.parentId = parentId; + return this; + } + + public DepartmentViewBuilder treePath(String treePath) { + this.treePath = treePath; + return this; + } + + public DepartmentViewBuilder sort(Integer sort) { + this.sort = sort; + return this; + } + + public DepartmentViewBuilder status(Status status) { + this.status = status; + return this; + } + + public DepartmentViewBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public DepartmentViewBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + /** + * Builds and returns a new DepartmentView instance with the configured parameters. + * + * @return a new DepartmentView instance + */ + public DepartmentView build() { + return new DepartmentView(id, name, parentId, sort, status, createdAt, updatedAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/view/PositionView.java b/src/main/java/com/onixbyte/helix/domain/view/PositionView.java new file mode 100644 index 0000000..8989f2b --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/view/PositionView.java @@ -0,0 +1,271 @@ +package com.onixbyte.helix.domain.view; + +import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.domain.entity.Position; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * View object for Position entity. + *

+ * This class represents a view of the Position entity, providing a data transfer object + * for position information in the organisational structure. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +public class PositionView { + + /** + * The unique identifier for the position. + */ + private Long id; + + /** + * The human-readable name of the position. + */ + private String name; + + /** + * The unique code identifier for the position. + */ + private String code; + + /** + * A detailed description of the position's responsibilities and requirements. + */ + private String description; + + /** + * The sort order for displaying positions. + */ + private Integer sort; + + /** + * The current status of the position. + */ + private Status status; + + /** + * The timestamp when this position record was created. + */ + private LocalDateTime createdAt; + + /** + * The timestamp when this position record was last updated. + */ + private LocalDateTime updatedAt; + + /** + * Default constructor. + */ + public PositionView() { + } + + /** + * Constructor with all fields. + */ + public PositionView(Long id, String name, String code, String description, Integer sort, + Status status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.name = name; + this.code = code; + this.description = description; + this.sort = sort; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + /** + * Creates a PositionView from a Position entity. + * + * @param position the Position entity + * @return the PositionView object + */ + public static PositionView fromEntity(Position position) { + if (position == null) { + return null; + } + return new PositionView( + position.getId(), + position.getName(), + position.getCode(), + position.getDescription(), + position.getSort(), + position.getStatus(), + position.getCreatedAt(), + position.getUpdatedAt() + ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getSort() { + return sort; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PositionView that = (PositionView) o; + return Objects.equals(id, that.id) && + Objects.equals(name, that.name) && + Objects.equals(code, that.code) && + Objects.equals(description, that.description) && + Objects.equals(sort, that.sort) && + status == that.status && + Objects.equals(createdAt, that.createdAt) && + Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, code, description, sort, status, createdAt, updatedAt); + } + + @Override + public String toString() { + return "PositionView{" + + "id=" + id + + ", name='" + name + '\'' + + ", code='" + code + '\'' + + ", description='" + description + '\'' + + ", sort=" + sort + + ", status=" + status + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + /** + * Creates a new builder instance. + * + * @return a new PositionViewBuilder + */ + public static PositionViewBuilder builder() { + return new PositionViewBuilder(); + } + + /** + * Builder class for PositionView. + */ + public static class PositionViewBuilder { + private Long id; + private String name; + private String code; + private String description; + private Integer sort; + private Status status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private PositionViewBuilder() { + } + + public PositionViewBuilder id(Long id) { + this.id = id; + return this; + } + + public PositionViewBuilder name(String name) { + this.name = name; + return this; + } + + public PositionViewBuilder code(String code) { + this.code = code; + return this; + } + + public PositionViewBuilder description(String description) { + this.description = description; + return this; + } + + public PositionViewBuilder sort(Integer sort) { + this.sort = sort; + return this; + } + + public PositionViewBuilder status(Status status) { + this.status = status; + return this; + } + + public PositionViewBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public PositionViewBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + public PositionView build() { + return new PositionView(id, name, code, description, sort, status, createdAt, updatedAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/view/RoleAuthorityView.java b/src/main/java/com/onixbyte/helix/domain/view/RoleAuthorityView.java new file mode 100644 index 0000000..f37e22c --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/view/RoleAuthorityView.java @@ -0,0 +1,154 @@ +package com.onixbyte.helix.domain.view; + +import com.onixbyte.helix.domain.entity.RoleAuthority; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * View object for RoleAuthority entity. + *

+ * This class represents a view of the RoleAuthority entity, providing a data transfer object + * for role-authority association information in the access control system. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +public class RoleAuthorityView { + + /** + * The identifier of the role in this association. + */ + private Long roleId; + + /** + * The identifier of the authority in this association. + */ + private Long authorityId; + + /** + * The timestamp when this role-authority association was created. + */ + private LocalDateTime createdAt; + + /** + * Default constructor. + */ + public RoleAuthorityView() { + } + + /** + * Constructor with all fields. + */ + public RoleAuthorityView(Long roleId, Long authorityId, LocalDateTime createdAt) { + this.roleId = roleId; + this.authorityId = authorityId; + this.createdAt = createdAt; + } + + /** + * Creates a RoleAuthorityView from a RoleAuthority entity. + * + * @param roleAuthority the RoleAuthority entity + * @return the RoleAuthorityView object + */ + public static RoleAuthorityView fromEntity(RoleAuthority roleAuthority) { + if (roleAuthority == null) { + return null; + } + return new RoleAuthorityView( + roleAuthority.getRoleId(), + roleAuthority.getAuthorityId(), + roleAuthority.getCreatedAt() + ); + } + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } + + public Long getAuthorityId() { + return authorityId; + } + + public void setAuthorityId(Long authorityId) { + this.authorityId = authorityId; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RoleAuthorityView that = (RoleAuthorityView) o; + return Objects.equals(roleId, that.roleId) && + Objects.equals(authorityId, that.authorityId) && + Objects.equals(createdAt, that.createdAt); + } + + @Override + public int hashCode() { + return Objects.hash(roleId, authorityId, createdAt); + } + + @Override + public String toString() { + return "RoleAuthorityView{" + + "roleId=" + roleId + + ", authorityId=" + authorityId + + ", createdAt=" + createdAt + + '}'; + } + + /** + * Creates a new builder instance. + * + * @return a new RoleAuthorityViewBuilder + */ + public static RoleAuthorityViewBuilder builder() { + return new RoleAuthorityViewBuilder(); + } + + /** + * Builder class for RoleAuthorityView. + */ + public static class RoleAuthorityViewBuilder { + private Long roleId; + private Long authorityId; + private LocalDateTime createdAt; + + private RoleAuthorityViewBuilder() { + } + + public RoleAuthorityViewBuilder roleId(Long roleId) { + this.roleId = roleId; + return this; + } + + public RoleAuthorityViewBuilder authorityId(Long authorityId) { + this.authorityId = authorityId; + return this; + } + + public RoleAuthorityViewBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public RoleAuthorityView build() { + return new RoleAuthorityView(roleId, authorityId, createdAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/view/RoleView.java b/src/main/java/com/onixbyte/helix/domain/view/RoleView.java new file mode 100644 index 0000000..dc0bc79 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/view/RoleView.java @@ -0,0 +1,294 @@ +package com.onixbyte.helix.domain.view; + +import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.domain.entity.Role; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * View object for Role entity. + *

+ * This class represents a view of the Role entity, providing a data transfer object + * for role information within the access control system. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +public class RoleView { + + /** + * The unique identifier for the role. + */ + private Long id; + + /** + * The human-readable name of the role. + */ + private String name; + + /** + * The unique code identifier for the role. + */ + private String code; + + /** + * The sort order for displaying roles. + */ + private Integer sort; + + /** + * Indicates whether this role is assigned by default to new users. + */ + private Boolean defaultValue; + + /** + * A detailed description of the role's purpose and permissions. + */ + private String description; + + /** + * The current status of the role. + */ + private Status status; + + /** + * The timestamp when this role record was created. + */ + private LocalDateTime createdAt; + + /** + * The timestamp when this role record was last updated. + */ + private LocalDateTime updatedAt; + + /** + * Default constructor. + */ + public RoleView() { + } + + /** + * Constructor with all fields. + */ + public RoleView(Long id, String name, String code, Integer sort, Boolean defaultValue, + String description, Status status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.name = name; + this.code = code; + this.sort = sort; + this.defaultValue = defaultValue; + this.description = description; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + /** + * Creates a RoleView from a Role entity. + * + * @param role the Role entity + * @return the RoleView object + */ + public static RoleView fromEntity(Role role) { + if (role == null) { + return null; + } + return new RoleView( + role.getId(), + role.getName(), + role.getCode(), + role.getSort(), + role.getDefaultValue(), + role.getDescription(), + role.getStatus(), + role.getCreatedAt(), + role.getUpdatedAt() + ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Integer getSort() { + return sort; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + public Boolean getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(Boolean defaultValue) { + this.defaultValue = defaultValue; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RoleView roleView = (RoleView) o; + return Objects.equals(id, roleView.id) && + Objects.equals(name, roleView.name) && + Objects.equals(code, roleView.code) && + Objects.equals(sort, roleView.sort) && + Objects.equals(defaultValue, roleView.defaultValue) && + Objects.equals(description, roleView.description) && + status == roleView.status && + Objects.equals(createdAt, roleView.createdAt) && + Objects.equals(updatedAt, roleView.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, code, sort, defaultValue, description, status, createdAt, updatedAt); + } + + @Override + public String toString() { + return "RoleView{" + + "id=" + id + + ", name='" + name + '\'' + + ", code='" + code + '\'' + + ", sort=" + sort + + ", defaultValue=" + defaultValue + + ", description='" + description + '\'' + + ", status=" + status + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + /** + * Creates a new builder instance. + * + * @return a new RoleViewBuilder + */ + public static RoleViewBuilder builder() { + return new RoleViewBuilder(); + } + + /** + * Builder class for RoleView. + */ + public static class RoleViewBuilder { + private Long id; + private String name; + private String code; + private Integer sort; + private Boolean defaultValue; + private String description; + private Status status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private RoleViewBuilder() { + } + + public RoleViewBuilder id(Long id) { + this.id = id; + return this; + } + + public RoleViewBuilder name(String name) { + this.name = name; + return this; + } + + public RoleViewBuilder code(String code) { + this.code = code; + return this; + } + + public RoleViewBuilder sort(Integer sort) { + this.sort = sort; + return this; + } + + public RoleViewBuilder defaultValue(Boolean defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public RoleViewBuilder description(String description) { + this.description = description; + return this; + } + + public RoleViewBuilder status(Status status) { + this.status = status; + return this; + } + + public RoleViewBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public RoleViewBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + public RoleView build() { + return new RoleView(id, name, code, sort, defaultValue, description, status, createdAt, updatedAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java b/src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java new file mode 100644 index 0000000..d261996 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java @@ -0,0 +1,202 @@ +package com.onixbyte.helix.domain.view; + +import com.onixbyte.helix.constant.IdentityProvider; +import com.onixbyte.helix.domain.entity.UserIdentity; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * View object for UserIdentity entity. + *

+ * This class represents a view of the UserIdentity entity, providing a data transfer object + * for external identity mapping information in the Helix system. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +public class UserIdentityView { + + /** + * The identifier of the internal user account. + */ + private Long userId; + + /** + * The external identity provider. + */ + private IdentityProvider provider; + + /** + * The unique identifier from the external provider. + */ + private String externalId; + + /** + * The timestamp when this identity mapping was created. + */ + private LocalDateTime createdAt; + + /** + * The timestamp when this identity mapping was last updated. + */ + private LocalDateTime updatedAt; + + /** + * Default constructor. + */ + public UserIdentityView() { + } + + /** + * Constructor with all fields. + */ + public UserIdentityView(Long userId, IdentityProvider provider, String externalId, + LocalDateTime createdAt, LocalDateTime updatedAt) { + this.userId = userId; + this.provider = provider; + this.externalId = externalId; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + /** + * Creates a UserIdentityView from a UserIdentity entity. + * + * @param userIdentity the UserIdentity entity + * @return the UserIdentityView object + */ + public static UserIdentityView fromEntity(UserIdentity userIdentity) { + if (userIdentity == null) { + return null; + } + return new UserIdentityView( + userIdentity.getUserId(), + userIdentity.getProvider(), + userIdentity.getExternalId(), + userIdentity.getCreatedAt(), + userIdentity.getUpdatedAt() + ); + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public IdentityProvider getProvider() { + return provider; + } + + public void setProvider(IdentityProvider provider) { + this.provider = provider; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserIdentityView that = (UserIdentityView) o; + return Objects.equals(userId, that.userId) && + provider == that.provider && + Objects.equals(externalId, that.externalId) && + Objects.equals(createdAt, that.createdAt) && + Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(userId, provider, externalId, createdAt, updatedAt); + } + + @Override + public String toString() { + return "UserIdentityView{" + + "userId=" + userId + + ", provider=" + provider + + ", externalId='" + externalId + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + /** + * Creates a new builder instance. + * + * @return a new UserIdentityViewBuilder + */ + public static UserIdentityViewBuilder builder() { + return new UserIdentityViewBuilder(); + } + + /** + * Builder class for UserIdentityView. + */ + public static class UserIdentityViewBuilder { + private Long userId; + private IdentityProvider provider; + private String externalId; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private UserIdentityViewBuilder() { + } + + public UserIdentityViewBuilder userId(Long userId) { + this.userId = userId; + return this; + } + + public UserIdentityViewBuilder provider(IdentityProvider provider) { + this.provider = provider; + return this; + } + + public UserIdentityViewBuilder externalId(String externalId) { + this.externalId = externalId; + return this; + } + + public UserIdentityViewBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public UserIdentityViewBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + public UserIdentityView build() { + return new UserIdentityView(userId, provider, externalId, createdAt, updatedAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/view/UserRoleView.java b/src/main/java/com/onixbyte/helix/domain/view/UserRoleView.java new file mode 100644 index 0000000..042528a --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/view/UserRoleView.java @@ -0,0 +1,154 @@ +package com.onixbyte.helix.domain.view; + +import com.onixbyte.helix.domain.entity.UserRole; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * View object for UserRole entity. + *

+ * This class represents a view of the UserRole entity, providing a data transfer object + * for user-role association information in the access control system. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +public class UserRoleView { + + /** + * The identifier of the role in this association. + */ + private Long roleId; + + /** + * The identifier of the user in this association. + */ + private Long userId; + + /** + * The timestamp when this user-role assignment was created. + */ + private LocalDateTime createdAt; + + /** + * Default constructor. + */ + public UserRoleView() { + } + + /** + * Constructor with all fields. + */ + public UserRoleView(Long roleId, Long userId, LocalDateTime createdAt) { + this.roleId = roleId; + this.userId = userId; + this.createdAt = createdAt; + } + + /** + * Creates a UserRoleView from a UserRole entity. + * + * @param userRole the UserRole entity + * @return the UserRoleView object + */ + public static UserRoleView fromEntity(UserRole userRole) { + if (userRole == null) { + return null; + } + return new UserRoleView( + userRole.getRoleId(), + userRole.getUserId(), + userRole.getCreatedAt() + ); + } + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserRoleView that = (UserRoleView) o; + return Objects.equals(roleId, that.roleId) && + Objects.equals(userId, that.userId) && + Objects.equals(createdAt, that.createdAt); + } + + @Override + public int hashCode() { + return Objects.hash(roleId, userId, createdAt); + } + + @Override + public String toString() { + return "UserRoleView{" + + "roleId=" + roleId + + ", userId=" + userId + + ", createdAt=" + createdAt + + '}'; + } + + /** + * Creates a new builder instance. + * + * @return a new UserRoleViewBuilder + */ + public static UserRoleViewBuilder builder() { + return new UserRoleViewBuilder(); + } + + /** + * Builder class for UserRoleView. + */ + public static class UserRoleViewBuilder { + private Long roleId; + private Long userId; + private LocalDateTime createdAt; + + private UserRoleViewBuilder() { + } + + public UserRoleViewBuilder roleId(Long roleId) { + this.roleId = roleId; + return this; + } + + public UserRoleViewBuilder userId(Long userId) { + this.userId = userId; + return this; + } + + public UserRoleViewBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public UserRoleView build() { + return new UserRoleView(roleId, userId, createdAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/view/UserView.java b/src/main/java/com/onixbyte/helix/domain/view/UserView.java new file mode 100644 index 0000000..df86f39 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/view/UserView.java @@ -0,0 +1,366 @@ +package com.onixbyte.helix.domain.view; + +import com.onixbyte.helix.constant.UserStatus; +import com.onixbyte.helix.domain.entity.User; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * View object for User entity. + *

+ * This class represents a view of the User entity, providing a data transfer object + * for user information in the Helix system. + * + * @author zihluwang + * @version 1.0 + * @since 1.0 + */ +public class UserView { + + /** + * The unique identifier for the user. + */ + private Long id; + + /** + * The unique username for authentication purposes. + */ + private String username; + + /** + * The user's complete full name. + */ + private String fullName; + + /** + * The user's email address. + */ + private String email; + + /** + * The country code for the user's phone number. + */ + private String regionAbbreviation; + + /** + * The user's phone number without the country code. + */ + private String phoneNumber; + + /** + * The URL to the user's avatar image. + */ + private String avatarUrl; + + /** + * The current status of the user account. + */ + private UserStatus status; + + /** + * The identifier of the department to which this user belongs. + */ + private Long departmentId; + + /** + * The identifier of the position held by this user. + */ + private Long positionId; + + /** + * The timestamp when this user record was created. + */ + private LocalDateTime createdAt; + + /** + * The timestamp when this user record was last updated. + */ + private LocalDateTime updatedAt; + + /** + * Default constructor. + */ + public UserView() { + } + + /** + * Constructor with all fields (excluding password for security). + */ + public UserView(Long id, String username, String fullName, String email, String regionAbbreviation, + String phoneNumber, String avatarUrl, UserStatus status, Long departmentId, + Long positionId, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.username = username; + this.fullName = fullName; + this.email = email; + this.regionAbbreviation = regionAbbreviation; + this.phoneNumber = phoneNumber; + this.avatarUrl = avatarUrl; + this.status = status; + this.departmentId = departmentId; + this.positionId = positionId; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + /** + * Creates a UserView from a User entity (excluding password for security). + * + * @param user the User entity + * @return the UserView object + */ + public static UserView fromEntity(User user) { + if (user == null) { + return null; + } + return new UserView( + user.getId(), + user.getUsername(), + user.getFullName(), + user.getEmail(), + user.getRegionAbbreviation(), + user.getPhoneNumber(), + user.getAvatarUrl(), + user.getStatus(), + user.getDepartmentId(), + user.getPositionId(), + user.getCreatedAt(), + user.getUpdatedAt() + ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getRegionAbbreviation() { + return regionAbbreviation; + } + + public void setRegionAbbreviation(String regionAbbreviation) { + this.regionAbbreviation = regionAbbreviation; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public UserStatus getStatus() { + return status; + } + + public void setStatus(UserStatus status) { + this.status = status; + } + + public Long getDepartmentId() { + return departmentId; + } + + public void setDepartmentId(Long departmentId) { + this.departmentId = departmentId; + } + + public Long getPositionId() { + return positionId; + } + + public void setPositionId(Long positionId) { + this.positionId = positionId; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserView userView = (UserView) o; + return Objects.equals(id, userView.id) && + Objects.equals(username, userView.username) && + Objects.equals(fullName, userView.fullName) && + Objects.equals(email, userView.email) && + Objects.equals(regionAbbreviation, userView.regionAbbreviation) && + Objects.equals(phoneNumber, userView.phoneNumber) && + Objects.equals(avatarUrl, userView.avatarUrl) && + status == userView.status && + Objects.equals(departmentId, userView.departmentId) && + Objects.equals(positionId, userView.positionId) && + Objects.equals(createdAt, userView.createdAt) && + Objects.equals(updatedAt, userView.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, username, fullName, email, regionAbbreviation, phoneNumber, avatarUrl, + status, departmentId, positionId, createdAt, updatedAt); + } + + @Override + public String toString() { + return "UserView{" + + "id=" + id + + ", username='" + username + '\'' + + ", fullName='" + fullName + '\'' + + ", email='" + email + '\'' + + ", regionAbbreviation='" + regionAbbreviation + '\'' + + ", phoneNumber='" + phoneNumber + '\'' + + ", avatarUrl='" + avatarUrl + '\'' + + ", status=" + status + + ", departmentId=" + departmentId + + ", positionId=" + positionId + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + /** + * Creates a new builder instance. + * + * @return a new UserViewBuilder + */ + public static UserViewBuilder builder() { + return new UserViewBuilder(); + } + + /** + * Builder class for UserView. + */ + public static class UserViewBuilder { + private Long id; + private String username; + private String fullName; + private String email; + private String regionAbbreviation; + private String phoneNumber; + private String avatarUrl; + private UserStatus status; + private Long departmentId; + private Long positionId; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private UserViewBuilder() { + } + + public UserViewBuilder id(Long id) { + this.id = id; + return this; + } + + public UserViewBuilder username(String username) { + this.username = username; + return this; + } + + public UserViewBuilder fullName(String fullName) { + this.fullName = fullName; + return this; + } + + public UserViewBuilder email(String email) { + this.email = email; + return this; + } + + public UserViewBuilder regionAbbreviation(String regionAbbreviation) { + this.regionAbbreviation = regionAbbreviation; + return this; + } + + public UserViewBuilder phoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + return this; + } + + public UserViewBuilder avatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + return this; + } + + public UserViewBuilder status(UserStatus status) { + this.status = status; + return this; + } + + public UserViewBuilder departmentId(Long departmentId) { + this.departmentId = departmentId; + return this; + } + + public UserViewBuilder positionId(Long positionId) { + this.positionId = positionId; + return this; + } + + public UserViewBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public UserViewBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + public UserView build() { + return new UserView(id, username, fullName, email, regionAbbreviation, phoneNumber, avatarUrl, + status, departmentId, positionId, createdAt, updatedAt); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java new file mode 100644 index 0000000..9162a5c --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java @@ -0,0 +1,24 @@ +package com.onixbyte.helix.domain.web.request; + +import com.onixbyte.helix.constant.UserStatus; +import jakarta.validation.constraints.NotBlank; + +import java.util.List; + +public record AddUserRequest( + @NotBlank(message = "Username cannot be empty.") + String username, + @NotBlank(message = "Password cannot be empty.") + String password, + @NotBlank(message = "Full name cannot be empty.") + String fullName, + String email, + String regionAbbreviation, + String phoneNumber, + String avatarUrl, + UserStatus status, + Long departmentId, + Long positionId, + List roleIds +) { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/QueryRoleRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/QueryRoleRequest.java new file mode 100644 index 0000000..6497c75 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/QueryRoleRequest.java @@ -0,0 +1,13 @@ +package com.onixbyte.helix.domain.web.request; + +import jakarta.validation.constraints.Pattern; + +public record QueryRoleRequest( + String name, + String code, + @Pattern( + regexp = "^(ACTIVE|INACTIVE)?$", + message = "状态仅可以是 ACTIVE、INACTIVE 其中之一") + String status +) { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/QueryUserRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/QueryUserRequest.java new file mode 100644 index 0000000..9d53f08 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/QueryUserRequest.java @@ -0,0 +1,19 @@ +package com.onixbyte.helix.domain.web.request; + +import jakarta.validation.constraints.Pattern; + +import java.time.LocalDateTime; + +public record QueryUserRequest( + Long departmentId, + String username, + String regionAbbreviation, + String phoneNumber, + @Pattern( + regexp = "^(ACTIVE|INACTIVE|LOCKED)?$", + message = "状态仅可以是 ACTIVE、INACTIVE 或 LOCKED 其中之一") + String status, + LocalDateTime createdAtStart, + LocalDateTime createdAtEnd +) { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java new file mode 100644 index 0000000..2ee0a27 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java @@ -0,0 +1,10 @@ +package com.onixbyte.helix.domain.web.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record ResetPasswordRequest( + @NotNull(message = "用户 ID 不能为空") Long id, + @NotBlank(message = "密码不能为空") String password +) { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/UpdateUserRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/UpdateUserRequest.java new file mode 100644 index 0000000..4c53655 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/UpdateUserRequest.java @@ -0,0 +1,22 @@ +package com.onixbyte.helix.domain.web.request; + +import com.onixbyte.helix.constant.UserStatus; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +import java.util.List; + +public record UpdateUserRequest( + @NotNull(message = "User ID cannot be null") + @Positive(message = "User ID must be positive") + Long id, + String fullName, + String email, + String regionAbbreviation, + String phoneNumber, + String avatarUrl, + UserStatus status, + Long departmentId, + Long positionId +) { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/UsernamePasswordLoginRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/UsernamePasswordLoginRequest.java new file mode 100644 index 0000000..8e95e11 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/UsernamePasswordLoginRequest.java @@ -0,0 +1,9 @@ +package com.onixbyte.helix.domain.web.request; + +public record UsernamePasswordLoginRequest( + String username, + String password, + String uuid, + String captcha +) { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/response/BizExceptionResponse.java b/src/main/java/com/onixbyte/helix/domain/web/response/BizExceptionResponse.java new file mode 100644 index 0000000..d7c4e07 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/response/BizExceptionResponse.java @@ -0,0 +1,38 @@ +package com.onixbyte.helix.domain.web.response; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * Record representing a standardised business exception response. + *

+ * This record encapsulates the essential information returned to clients when business logic + * exceptions occur within the Helix application. It provides a consistent structure for + * error communication, ensuring that all business exception responses follow the same format and + * contain the necessary information for client-side error handling. + *

+ * The response includes a timestamp indicating when the exception occurred and a human-readable + * message explaining the nature of the error. This standardised format enables consistent error + * handling across different client applications and API consumers. + *

+ * As a record, this class is immutable and provides automatic implementations of {@code equals()}, + * {@code hashCode()}, and {@code toString()} methods, making it suitable for use in functional + * programming patterns and ensuring thread safety in concurrent environments. + *

+ * This response entity is typically used by exception handlers and error processing components to + * communicate business rule violations, validation failures, and other application-specific errors + * to API clients. + * + * @param timestamp the timestamp when the exception occurred, providing temporal context for error + * tracking and debugging + * @param message a human-readable explanation of the exception, suitable for display to end users + * or logging purposes + * @author zihluwang + * @see Serializable + * @since 1.0.0 + */ +public record BizExceptionResponse( + LocalDateTime timestamp, + String message +) implements Serializable { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/response/CaptchaResponse.java b/src/main/java/com/onixbyte/helix/domain/web/response/CaptchaResponse.java new file mode 100644 index 0000000..9ff3265 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/response/CaptchaResponse.java @@ -0,0 +1,7 @@ +package com.onixbyte.helix.domain.web.response; + +public record CaptchaResponse( + String captcha, + String uuid +) { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/response/FileUploadResponse.java b/src/main/java/com/onixbyte/helix/domain/web/response/FileUploadResponse.java new file mode 100644 index 0000000..65e887c --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/response/FileUploadResponse.java @@ -0,0 +1,9 @@ +package com.onixbyte.helix.domain.web.response; + +public record FileUploadResponse( + String originalFileName, + String contentType, + Long size, + String url +) { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/response/LoginSuccessResponse.java b/src/main/java/com/onixbyte/helix/domain/web/response/LoginSuccessResponse.java new file mode 100644 index 0000000..94f1643 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/response/LoginSuccessResponse.java @@ -0,0 +1,13 @@ +package com.onixbyte.helix.domain.web.response; + +import com.onixbyte.helix.domain.entity.User; + +public record LoginSuccessResponse( + String accessToken, + User user +) { + + public LoginSuccessResponse { + user.setPassword(null); + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java b/src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java new file mode 100644 index 0000000..7c50647 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java @@ -0,0 +1,252 @@ +package com.onixbyte.helix.domain.web.response; + +import com.onixbyte.helix.constant.UserStatus; + +import java.time.LocalDateTime; + +public class UserDetailResponse { + private String id; + private String username; + private String fullName; + private String email; + private String regionAbbreviation; + private String phoneNumber; + private String avatarUrl; + private UserStatus status; + private Long departmentId; + private String departmentName; + private Long positionId; + private String positionName; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public UserDetailResponse() { + } + + public UserDetailResponse(String id, String username, String fullName, String email, String regionAbbreviation, String phoneNumber, String avatarUrl, UserStatus status, Long departmentId, String departmentName, Long positionId, String positionName, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.username = username; + this.fullName = fullName; + this.email = email; + this.regionAbbreviation = regionAbbreviation; + this.phoneNumber = phoneNumber; + this.avatarUrl = avatarUrl; + this.status = status; + this.departmentId = departmentId; + this.departmentName = departmentName; + this.positionId = positionId; + this.positionName = positionName; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getRegionAbbreviation() { + return regionAbbreviation; + } + + public void setRegionAbbreviation(String regionAbbreviation) { + this.regionAbbreviation = regionAbbreviation; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public UserStatus getStatus() { + return status; + } + + public void setStatus(UserStatus status) { + this.status = status; + } + + public Long getDepartmentId() { + return departmentId; + } + + public void setDepartmentId(Long departmentId) { + this.departmentId = departmentId; + } + + public String getDepartmentName() { + return departmentName; + } + + public void setDepartmentName(String departmentName) { + this.departmentName = departmentName; + } + + public Long getPositionId() { + return positionId; + } + + public void setPositionId(Long positionId) { + this.positionId = positionId; + } + + public String getPositionName() { + return positionName; + } + + public void setPositionName(String positionName) { + this.positionName = positionName; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public static UserDetailResponseBuilder builder() { + return new UserDetailResponseBuilder(); + } + + public static class UserDetailResponseBuilder { + private String id; + private String username; + private String fullName; + private String email; + private String regionAbbreviation; + private String phoneNumber; + private String avatarUrl; + private UserStatus status; + private Long departmentId; + private String departmentName; + private Long positionId; + private String positionName; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private UserDetailResponseBuilder() { + } + + public UserDetailResponseBuilder id(String id) { + this.id = id; + return this; + } + + public UserDetailResponseBuilder username(String username) { + this.username = username; + return this; + } + + public UserDetailResponseBuilder fullName(String fullName) { + this.fullName = fullName; + return this; + } + + public UserDetailResponseBuilder email(String email) { + this.email = email; + return this; + } + + public UserDetailResponseBuilder regionAbbreviation(String regionAbbreviation) { + this.regionAbbreviation = regionAbbreviation; + return this; + } + + public UserDetailResponseBuilder phoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + return this; + } + + public UserDetailResponseBuilder avatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + return this; + } + + public UserDetailResponseBuilder status(UserStatus status) { + this.status = status; + return this; + } + + public UserDetailResponseBuilder departmentId(Long departmentId) { + this.departmentId = departmentId; + return this; + } + + public UserDetailResponseBuilder departmentName(String departmentName) { + this.departmentName = departmentName; + return this; + } + + public UserDetailResponseBuilder positionId(Long positionId) { + this.positionId = positionId; + return this; + } + + public UserDetailResponseBuilder positionName(String positionName) { + this.positionName = positionName; + return this; + } + + public UserDetailResponseBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public UserDetailResponseBuilder updatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + public UserDetailResponse build() { + return new UserDetailResponse(id, username, fullName, email, regionAbbreviation, phoneNumber, avatarUrl, status, departmentId, departmentName, positionId, positionName, createdAt, updatedAt); + } + } +} diff --git a/src/main/java/com/onixbyte/helix/exception/BizException.java b/src/main/java/com/onixbyte/helix/exception/BizException.java new file mode 100644 index 0000000..f898daf --- /dev/null +++ b/src/main/java/com/onixbyte/helix/exception/BizException.java @@ -0,0 +1,74 @@ +package com.onixbyte.helix.exception; + +import org.springframework.http.HttpStatus; + +/** + * Custom runtime exception for business logic violations and application-specific errors. + *

+ * This exception is designed to handle business rule violations, validation failures, and other + * application-specific error conditions that occur during normal operation. Unlike + * system exceptions, business exceptions are expected and should be handled gracefully by the + * application's error handling mechanisms. + *

+ * Each business exception carries an HTTP status code that indicates the appropriate response + * status to return to clients when the exception occurs. This enables consistent error handling + * across REST API endpoints and provides meaningful HTTP responses to API consumers. + *

+ * Common use cases include: + *

    + *
  • Resource not found scenarios (404 Not Found)
  • + *
  • Validation failures (400 Bad Request)
  • + *
  • Authorisation violations (403 Forbidden)
  • + *
  • Business rule violations (422 Unprocessable Entity)
  • + *
  • Conflict situations (409 Conflict)
  • + *
+ *

+ * The exception integrates seamlessly with Spring Boot's exception handling framework and can be + * caught by global exception handlers to produce standardised error responses. + * + * @author zihluwang + * @see RuntimeException + * @see org.springframework.http.HttpStatus + * @since 1.0.0 + */ +public class BizException extends RuntimeException { + + /** + * The HTTP status code associated with this business exception. + *

+ * This status code indicates the appropriate HTTP response status that should be returned to + * clients when this exception occurs. It enables consistent error handling across + * REST API endpoints. + */ + private final HttpStatus status; + + /** + * Constructs a new business exception with the specified HTTP status and message. + * + * @param message the detailed error message explaining the business logic violation + */ + public BizException(String message) { + super(message); + this.status = HttpStatus.INTERNAL_SERVER_ERROR; + } + + /** + * Constructs a new business exception with the specified HTTP status and message. + * + * @param status the HTTP status code to associate with this exception + * @param message the detailed error message explaining the business logic violation + */ + public BizException(HttpStatus status, String message) { + super(message); + this.status = status; + } + + /** + * Returns the HTTP status code associated with this business exception. + * + * @return the HTTP status code that should be used in the error response + */ + public HttpStatus getStatus() { + return status; + } +} diff --git a/src/main/java/com/onixbyte/helix/extension/package-info.java b/src/main/java/com/onixbyte/helix/extension/package-info.java new file mode 100644 index 0000000..5bdf552 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/extension/package-info.java @@ -0,0 +1,43 @@ +/** + * Extension and plugin support package for the Helix application. + *

+ * This package is designed to contain extension points, plugin interfaces, + * and extensibility mechanisms that allow the Helix application to be + * extended with additional functionality without modifying core components. + *

+ * Intended Contents: + *

    + *
  • Extension Interfaces: Define contracts for pluggable components
  • + *
  • Plugin Loaders: Mechanisms for discovering and loading extensions
  • + *
  • + * Extension Points: Well-defined points where custom functionality can + * be injected + *
  • + *
  • Custom Annotations: Annotations for marking and configuring extensions
  • + *
+ *

+ * Design Principles: + *

    + *
  • + * Loose Coupling: Extensions should be loosely coupled to core functionality + *
  • + *
  • + * Service Provider Interface (SPI): Use SPI patterns for plugin discovery + *
  • + *
  • + * Configuration-Driven: Allow extensions to be configured through + * application properties + *
  • + *
  • + * Lifecycle Management: Provide proper initialisation and cleanup for extensions + *
  • + *
+ *

+ * This package follows the extensibility patterns commonly used in enterprise applications to + * support customisation and third-party integrations whilst maintaining system stability + * and performance. + * + * @author zihluwang + * @since 1.0.0 + */ +package com.onixbyte.helix.extension; \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/extension/redis/serializer/JacksonSerialiser.java b/src/main/java/com/onixbyte/helix/extension/redis/serializer/JacksonSerialiser.java new file mode 100644 index 0000000..01cef3b --- /dev/null +++ b/src/main/java/com/onixbyte/helix/extension/redis/serializer/JacksonSerialiser.java @@ -0,0 +1,21 @@ +package com.onixbyte.helix.extension.redis.serializer; + +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; + +public class JacksonSerialiser { + + public static final GenericJackson2JsonRedisSerializer INSTANCE = initialiseSerializer(); + + private static GenericJackson2JsonRedisSerializer initialiseSerializer() { + var serializer = new GenericJackson2JsonRedisSerializer(); + + serializer.configure((configurer) -> { + configurer.registerModule(new JavaTimeModule()); + configurer.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + }); + + return serializer; + } +} diff --git a/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java new file mode 100644 index 0000000..a22d041 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java @@ -0,0 +1,81 @@ +package com.onixbyte.helix.filter; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.onixbyte.helix.manager.AuthorityManager; +import com.onixbyte.helix.manager.UserManager; +import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Objects; + +@Component +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + private final static Logger log = LoggerFactory.getLogger(TokenAuthenticationFilter.class); + + private final Algorithm algorithm; + private final UserManager userManager; + private final AuthorityManager authorityManager; + + public TokenAuthenticationFilter(Algorithm algorithm, UserManager userManager, AuthorityManager authorityManager) { + this.algorithm = algorithm; + this.userManager = userManager; + this.authorityManager = authorityManager; + } + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + var token = request.getHeader("Authorization"); + if (Objects.isNull(token) || token.isBlank()) { + filterChain.doFilter(request, response); + return; + } + + if (!token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + token = token.substring(7); + var verifier = JWT.require(algorithm) + .withIssuer("Helix Server") + .build(); + + try { + var decodedToken = verifier.verify(token); + var username = decodedToken.getSubject(); + + var user = userManager.selectByUsername(username); + var authorities = authorityManager.queryByUserId(user.getId()) + .stream() + .map((authority) -> (GrantedAuthority) authority::getCode) + .toList(); + + user.setPassword(null); + + var authentication = UsernamePasswordAuthentication.authenticated(user, authorities); + SecurityContextHolder.getContext().setAuthentication(authentication); + filterChain.doFilter(request, response); + } catch (JWTVerificationException e) { + log.error("JWT verification failed.", e); + filterChain.doFilter(request, response); + } + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java b/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java new file mode 100644 index 0000000..d03f130 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java @@ -0,0 +1,18 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.properties.ApplicationProperties; +import org.springframework.stereotype.Component; + +@Component +public class ApplicationManager { + + private final ApplicationProperties applicationProperties; + + public ApplicationManager(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } + + public String getDefaultEmail() { + return applicationProperties.defaultEmail(); + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/AssetManager.java b/src/main/java/com/onixbyte/helix/manager/AssetManager.java new file mode 100644 index 0000000..b394abe --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/AssetManager.java @@ -0,0 +1,40 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.constant.CacheName; +import com.onixbyte.helix.domain.entity.Asset; +import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.mapper.AssetMapper; +import com.onixbyte.helix.repository.AssetRepository; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +@Component +public class AssetManager { + + private final AssetMapper assetMapper; + private final AssetRepository assetRepository; + + public AssetManager(AssetMapper assetMapper, AssetRepository assetRepository) { + this.assetMapper = assetMapper; + this.assetRepository = assetRepository; + } + + @CachePut(cacheNames = CacheName.ASSET, key = "#result.id", unless = "#result == null") + public Asset save(Asset asset) { + return assetRepository.save(asset); + } + + @Cacheable(cacheNames = CacheName.ASSET, key = "#assetId") + public Asset queryByAssetId(Long assetId) { + return assetRepository.findById(assetId) + .orElse(null); + } + + @CacheEvict(cacheNames = CacheName.ASSET, key = "#assetId") + public void deleteById(Long assetId) { + assetRepository.deleteById(assetId); + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java new file mode 100644 index 0000000..8b11921 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java @@ -0,0 +1,24 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.constant.CacheName; +import com.onixbyte.helix.domain.entity.Authority; +import com.onixbyte.helix.mapper.AuthorityMapper; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class AuthorityManager { + + private final AuthorityMapper authorityMapper; + + public AuthorityManager(AuthorityMapper authorityMapper) { + this.authorityMapper = authorityMapper; + } + + @Cacheable(cacheNames = CacheName.AUTHORITIES_OF_USER, key = "#userId") + public List queryByUserId(Long userId) { + return authorityMapper.selectByUserId(userId); + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/CaptchaManager.java b/src/main/java/com/onixbyte/helix/manager/CaptchaManager.java new file mode 100644 index 0000000..605918a --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/CaptchaManager.java @@ -0,0 +1,39 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.client.RedisClient; +import com.onixbyte.helix.constant.CacheName; +import com.onixbyte.helix.properties.CaptchaProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.Objects; + +@Component +public class CaptchaManager { + + private final RedisClient redisClient; + + @Autowired + public CaptchaManager(RedisClient redisClient) { + this.redisClient = redisClient; + } + + public void setCaptcha(String uuid, String captchaCode) { + redisClient.set(buildCacheKey(uuid), captchaCode, Duration.ofMinutes(5L)); + } + + public String getCaptcha(String uuid) { + var key = buildCacheKey(uuid); + var captcha = redisClient.get(key, String.class); + redisClient.delete(key); + return captcha; + } + + private String buildCacheKey(String uuid) { + return "captcha::" + uuid; + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java new file mode 100644 index 0000000..9967c5c --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java @@ -0,0 +1,26 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.domain.entity.Department; +import com.onixbyte.helix.mapper.DepartmentMapper; +import com.onixbyte.helix.repository.DepartmentRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Component +public class DepartmentManager { + + private final DepartmentMapper departmentMapper; + private final DepartmentRepository departmentRepository; + + public DepartmentManager(DepartmentMapper departmentMapper, DepartmentRepository departmentRepository) { + this.departmentMapper = departmentMapper; + this.departmentRepository = departmentRepository; + } + + public Page selectAll(Pageable pageable) { + return departmentRepository.findAll(pageable); + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/MenuManager.java b/src/main/java/com/onixbyte/helix/manager/MenuManager.java new file mode 100644 index 0000000..e22f356 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/MenuManager.java @@ -0,0 +1,23 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.domain.entity.Menu; +import com.onixbyte.helix.mapper.MenuMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class MenuManager { + private static final Logger log = LoggerFactory.getLogger(MenuManager.class); + private final MenuMapper menuMapper; + + public MenuManager(MenuMapper menuMapper) { + this.menuMapper = menuMapper; + } + + public List

selectActiveMenusByUserId(Long userId) { + return menuMapper.selectActiveMenusByUserId(userId); + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/PositionManager.java b/src/main/java/com/onixbyte/helix/manager/PositionManager.java new file mode 100644 index 0000000..d9c9454 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/PositionManager.java @@ -0,0 +1,25 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.domain.entity.Position; +import com.onixbyte.helix.mapper.PositionMapper; +import com.onixbyte.helix.repository.PositionRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +@Component +public class PositionManager { + + private final PositionMapper positionMapper; + private final PositionRepository positionRepository; + + public PositionManager(PositionMapper positionMapper, PositionRepository positionRepository) { + this.positionMapper = positionMapper; + this.positionRepository = positionRepository; + } + + public Page selectAll(Pageable pageable) { + return positionRepository.findAll(pageable); + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/RoleManager.java b/src/main/java/com/onixbyte/helix/manager/RoleManager.java new file mode 100644 index 0000000..3bc32e1 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/RoleManager.java @@ -0,0 +1,45 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.domain.database.query.wrapper.QueryRoleWrapper; +import com.onixbyte.helix.domain.entity.Role; +import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.mapper.RoleMapper; +import com.onixbyte.helix.repository.RoleRepository; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; + +@Component +public class RoleManager { + + private final RoleMapper roleMapper; + private final RoleRepository roleRepository; + + public RoleManager(RoleMapper roleMapper, RoleRepository roleRepository) { + this.roleMapper = roleMapper; + this.roleRepository = roleRepository; + } + + public void validateRoles(List roleIds) { + if (!roleMapper.areRolesExisted(roleIds)) { + throw new BizException(HttpStatus.BAD_REQUEST, "Role does not exist in database."); + } + } + + public Optional getRole(Role example) { + return roleRepository.findOne(Example.of(example)); + } + + public Page selectAll(Pageable pageable, QueryRoleWrapper wrapper) { + var records = roleMapper.selectAll(pageable, wrapper); + var total = roleMapper.count(wrapper); + + return new PageImpl<>(records, pageable, total); + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/SettingManager.java b/src/main/java/com/onixbyte/helix/manager/SettingManager.java new file mode 100644 index 0000000..c0ebc22 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/SettingManager.java @@ -0,0 +1,21 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.domain.entity.Setting; +import com.onixbyte.helix.repository.SettingRepository; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +@Component +public class SettingManager { + + private final SettingRepository settingRepository; + + public SettingManager(SettingRepository settingRepository) { + this.settingRepository = settingRepository; + } + + @Cacheable(cacheNames = "setting", key = "#name") + public Setting getSettingByName(String name) { + return settingRepository.getSettingByName(name); + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/UserManager.java b/src/main/java/com/onixbyte/helix/manager/UserManager.java new file mode 100644 index 0000000..3459404 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/UserManager.java @@ -0,0 +1,123 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.common.regex.Patterns; +import com.onixbyte.helix.constant.CacheName; +import com.onixbyte.helix.domain.database.query.wrapper.QueryUserWrapper; +import com.onixbyte.helix.domain.entity.User; +import com.onixbyte.helix.domain.web.response.UserDetailResponse; +import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.mapper.UserMapper; +import com.onixbyte.helix.repository.UserRepository; +import com.onixbyte.region.Region; +import org.apache.commons.lang3.StringUtils; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Component +public class UserManager { + + private final UserMapper userMapper; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public UserManager(UserMapper userMapper, UserRepository userRepository, PasswordEncoder passwordEncoder) { + this.userMapper = userMapper; + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + } + + /** + * Get user by username, and cache this user by username. + * + * @param username username + * @return user + */ + @Cacheable(cacheNames = CacheName.USER, key = "#username", unless = "#result == null") + public User selectByUsername(String username) { + return userRepository.findOne(Example.of(User.builder() + .username(username) + .build())) + .orElse(null); + } + + /** + * Query paginated users. + * + * @param pageable page request + * @return page result + */ + public Page selectUserDetailsPage(Pageable pageable, QueryUserWrapper wrapper) { + var result = userMapper.selectListWithDetails(pageable, wrapper); + var total = userMapper.count(wrapper); + return new PageImpl<>(result, pageable, total); + } + + @CachePut(cacheNames = CacheName.USER, key = "#user.username") + public User save(User user) { + return userRepository.save(user); + } + + public UserDetailResponse queryUserDetailByUserId(Long userId) { + return userMapper.selectWithDetailByUserId(userId); + } + + public void deleteById(Long userId) { + userRepository.deleteById(userId); + } + + @CachePut(cacheNames = CacheName.USER, key = "#result.username", unless = "#result == null") + @Transactional(rollbackFor = Throwable.class) + public User updateUser(User user) { + var userToUpdate = userRepository.findById(user.getId()) + .orElseThrow(() -> new BizException(HttpStatus.BAD_REQUEST, "找不到 ID 为" + user.getId() + "的用户信息")); + + Optional.ofNullable(user.getPassword()) + .filter(StringUtils::isNotBlank) + .map(passwordEncoder::encode) + .ifPresent(userToUpdate::setPassword); + + Optional.ofNullable(user.getFullName()) + .filter(StringUtils::isNotBlank) + .ifPresent(userToUpdate::setFullName); + + Optional.ofNullable(user.getEmail()) + .filter(StringUtils::isNotBlank) + .filter((email) -> Patterns.EMAIL.asPredicate().test(email)) + .ifPresent(userToUpdate::setEmail); + + Optional.ofNullable(user.getRegionAbbreviation()) + .filter(StringUtils::isNotBlank) + .filter(Region::isValidAbbreviation) + .ifPresent(userToUpdate::setRegionAbbreviation); + + Optional.ofNullable(user.getPhoneNumber()) + .filter(StringUtils::isNotBlank) + .ifPresent(userToUpdate::setPhoneNumber); + + Optional.ofNullable(user.getAvatarUrl()) + .filter(StringUtils::isNotBlank) + .filter((avatarUrl) -> Patterns.IMAGE_URL.asPredicate().test(avatarUrl) || Patterns.GRAVATAR_IMAGE_URL.asPredicate().test(avatarUrl)) + .ifPresent(userToUpdate::setAvatarUrl); + + Optional.ofNullable(user.getStatus()) + .ifPresent(userToUpdate::setStatus); + + Optional.ofNullable(user.getDepartmentId()) + .ifPresent(userToUpdate::setDepartmentId); + + Optional.ofNullable(user.getPositionId()) + .ifPresent(userToUpdate::setPositionId); + + return userToUpdate; + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java b/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java new file mode 100644 index 0000000..efe476b --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java @@ -0,0 +1,34 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.domain.entity.UserRole; +import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.mapper.UserRoleMapper; +import com.onixbyte.helix.repository.UserRoleRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class UserRoleManager { + + private static final Logger log = LoggerFactory.getLogger(UserRoleManager.class); + private final UserRoleMapper userRoleMapper; + private final UserRoleRepository userRoleRepository; + + public UserRoleManager(UserRoleMapper userRoleMapper, UserRoleRepository userRoleRepository) { + this.userRoleMapper = userRoleMapper; + this.userRoleRepository = userRoleRepository; + } + + public List saveBatch(List userRoles) { + return userRoleRepository.saveAll(userRoles); + } + + public void deleteByUserId(Long userId) { + var affectedRows = userRoleRepository.deleteByUserId(userId); + log.info("用户 {} 的角色关联被全部移除(共 {} 条)。", userId, affectedRows); + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/package-info.java b/src/main/java/com/onixbyte/helix/manager/package-info.java new file mode 100644 index 0000000..27b5fd9 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/package-info.java @@ -0,0 +1,60 @@ +/** + * Business logic management and orchestration package for the Helix application. + *

+ * This package is designed to contain manager classes that orchestrate complex business operations, + * coordinate between multiple services, and handle cross-cutting concerns that span multiple + * domain boundaries. + *

+ * Manager Pattern: Managers in this package serve as facades or coordinators that + * encapsulate complex business workflows, typically involving multiple services, repositories, + * or external systems. They provide a higher-level abstraction over individual service components. + *

+ * Intended Contents: + *

    + *
  • + * Workflow Managers: Orchestrate multi-step business processes + *
  • + *
  • + * Integration Managers: Coordinate interactions with external systems + *
  • + *
  • + * Transaction Managers: Handle complex transactional scenarios + *
  • + *
  • + * Cache Managers: Manage caching strategies and cache invalidation + *
  • + *
  • + * Event Managers: Coordinate event publishing and handling + *
  • + *
+ *

+ * Design Guidelines: + *

    + *
  • + * Single Responsibility: Each manager should focus on a specific business domain + * or cross-cutting concern + *
  • + *
  • + * Service Coordination: Managers should delegate to services rather than + * implementing business logic directly + *
  • + *
  • + * Transaction Boundaries: Define clear transactional boundaries for + * complex operations + *
  • + *
  • + * Error Handling: Provide comprehensive error handling and rollback mechanisms + * for complex workflows + *
  • + *
+ *

+ * Managers typically sit between the controller layer and the service layer, providing a + * coordination point for complex operations that require multiple service interactions or + * cross-cutting concerns. + * + * @author zihluwang + * @since 1.0.0 + * @see com.onixbyte.helix.service + * @see org.springframework.transaction.annotation.Transactional + */ +package com.onixbyte.helix.manager; \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/mapper/AssetMapper.java b/src/main/java/com/onixbyte/helix/mapper/AssetMapper.java new file mode 100644 index 0000000..55c1a36 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/mapper/AssetMapper.java @@ -0,0 +1,12 @@ +package com.onixbyte.helix.mapper; + +import org.apache.ibatis.annotations.Mapper; + +/** + * Asset related database operations. + * + * @author zihluwang + */ +@Mapper +public interface AssetMapper { +} diff --git a/src/main/java/com/onixbyte/helix/mapper/AuthorityMapper.java b/src/main/java/com/onixbyte/helix/mapper/AuthorityMapper.java new file mode 100644 index 0000000..84a3394 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/mapper/AuthorityMapper.java @@ -0,0 +1,19 @@ +package com.onixbyte.helix.mapper; + +import com.onixbyte.helix.domain.entity.Authority; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface AuthorityMapper { + + /** + * Select authorities that is granted to the specific user. + * + * @param userId user ID + * @return authorities + */ + List selectByUserId(@Param("userId") Long userId); +} diff --git a/src/main/java/com/onixbyte/helix/mapper/DepartmentMapper.java b/src/main/java/com/onixbyte/helix/mapper/DepartmentMapper.java new file mode 100644 index 0000000..eba2e85 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/mapper/DepartmentMapper.java @@ -0,0 +1,7 @@ +package com.onixbyte.helix.mapper; + +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DepartmentMapper { +} diff --git a/src/main/java/com/onixbyte/helix/mapper/MenuMapper.java b/src/main/java/com/onixbyte/helix/mapper/MenuMapper.java new file mode 100644 index 0000000..320067e --- /dev/null +++ b/src/main/java/com/onixbyte/helix/mapper/MenuMapper.java @@ -0,0 +1,12 @@ +package com.onixbyte.helix.mapper; + +import com.onixbyte.helix.domain.entity.Menu; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface MenuMapper { + List

selectActiveMenusByUserId(@Param("userId") Long userId); +} diff --git a/src/main/java/com/onixbyte/helix/mapper/PositionMapper.java b/src/main/java/com/onixbyte/helix/mapper/PositionMapper.java new file mode 100644 index 0000000..f7a8afb --- /dev/null +++ b/src/main/java/com/onixbyte/helix/mapper/PositionMapper.java @@ -0,0 +1,7 @@ +package com.onixbyte.helix.mapper; + +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PositionMapper { +} diff --git a/src/main/java/com/onixbyte/helix/mapper/RoleMapper.java b/src/main/java/com/onixbyte/helix/mapper/RoleMapper.java new file mode 100644 index 0000000..3cfdff8 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/mapper/RoleMapper.java @@ -0,0 +1,22 @@ +package com.onixbyte.helix.mapper; + +import com.onixbyte.helix.domain.database.query.wrapper.QueryRoleWrapper; +import com.onixbyte.helix.domain.entity.Role; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +@Mapper +public interface RoleMapper { + + boolean areRolesExisted(@Param("roleIds") List roleIds); + + List selectAll( + @Param("pageable") Pageable page, + @Param("wrapper") QueryRoleWrapper wrapper + ); + + Integer count(@Param("wrapper") QueryRoleWrapper wrapper); +} diff --git a/src/main/java/com/onixbyte/helix/mapper/SettingMapper.java b/src/main/java/com/onixbyte/helix/mapper/SettingMapper.java new file mode 100644 index 0000000..fcfaf41 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/mapper/SettingMapper.java @@ -0,0 +1,7 @@ +package com.onixbyte.helix.mapper; + +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SettingMapper { +} diff --git a/src/main/java/com/onixbyte/helix/mapper/UserMapper.java b/src/main/java/com/onixbyte/helix/mapper/UserMapper.java new file mode 100644 index 0000000..79ab8d7 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/mapper/UserMapper.java @@ -0,0 +1,22 @@ +package com.onixbyte.helix.mapper; + +import com.onixbyte.helix.domain.database.query.wrapper.QueryUserWrapper; +import com.onixbyte.helix.domain.web.response.UserDetailResponse; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +@Mapper +public interface UserMapper { + + int count(@Param("wrapper") QueryUserWrapper request); + + List selectListWithDetails( + @Param("pageable") Pageable pageable, + @Param("wrapper") QueryUserWrapper wrapper + ); + + UserDetailResponse selectWithDetailByUserId(@Param("id") Long userId); +} diff --git a/src/main/java/com/onixbyte/helix/mapper/UserRoleMapper.java b/src/main/java/com/onixbyte/helix/mapper/UserRoleMapper.java new file mode 100644 index 0000000..a825db2 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/mapper/UserRoleMapper.java @@ -0,0 +1,7 @@ +package com.onixbyte.helix.mapper; + +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserRoleMapper { +} diff --git a/src/main/java/com/onixbyte/helix/processor/package-info.java b/src/main/java/com/onixbyte/helix/processor/package-info.java new file mode 100644 index 0000000..7dec55b --- /dev/null +++ b/src/main/java/com/onixbyte/helix/processor/package-info.java @@ -0,0 +1,59 @@ +/** + * Data processing and transformation components for the Helix application. + *

+ * This package contains processors that handle data transformation, validation, and business + * logic processing. Processors in this package follow the Chain of Responsibility pattern and + * Strategy pattern to provide flexible and extensible data processing capabilities. + *

+ * Processing Architecture: The Helix application uses a processor-based + * architecture for complex data transformations and business rule applications. Processors can be + * chained together to create sophisticated data processing pipelines. + *

+ * Intended Contents: + *

    + *
  • Data Processors: Transform and validate incoming data
  • + *
  • Business Rule Processors: Apply business logic and rules
  • + *
  • Validation Processors: Perform complex validation operations
  • + *
  • Pipeline Processors: Orchestrate multiple processing steps
  • + *
  • Event Processors: Handle domain events and notifications
  • + *
+ *

+ * Design Principles: + *

    + *
  • + * Single Responsibility: Each processor handles one specific type of processing + * or transformation + *
  • + *
  • + * Composability: Processors can be combined to create complex + * processing workflows + *
  • + *
  • + * Immutability: Processors should not modify input data directly; instead, + * they should return processed results + *
  • + *
  • + * Error Handling: Processors should handle errors gracefully and provide + * meaningful error messages + *
  • + *
+ *

+ * Common Patterns: + *

    + *
  • + * Chain of Responsibility: Link processors together for sequential processing + *
  • + *
  • Strategy Pattern: Allow runtime selection of processing algorithms
  • + *
  • Template Method: Define processing skeleton with customisable steps
  • + *
  • Observer Pattern: Notify interested parties of processing events
  • + *
+ *

+ * All processors in this package should be stateless and thread-safe to support concurrent + * processing scenarios. + * + * @author zihluwang + * @since 1.0.0 + * @see com.onixbyte.helix.service + * @see com.onixbyte.helix.domain.entity + */ +package com.onixbyte.helix.processor; \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java b/src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java new file mode 100644 index 0000000..1ce7d27 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java @@ -0,0 +1,10 @@ +package com.onixbyte.helix.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; + +@ConfigurationProperties(prefix = "app.common") +public record ApplicationProperties( + @DefaultValue("default@helix.onixbyte.dev") String defaultEmail +) { +} diff --git a/src/main/java/com/onixbyte/helix/properties/AssetProperties.java b/src/main/java/com/onixbyte/helix/properties/AssetProperties.java new file mode 100644 index 0000000..538c475 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/properties/AssetProperties.java @@ -0,0 +1,41 @@ +package com.onixbyte.helix.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; + +/** + * Configuration properties for S3-compatible file storage services. + *

+ * This class encapsulates the essential settings required to connect to and utilise S3-based file + * storage providers. It supports custom endpoints, regions, and credentials, allowing integration + * with AWS S3 or compatible services.
+ *

+ * The configuration is intentionally minimal, with sensible defaults for common providers. Use this + * class to enable S3 storage, specify connection details, and select the provider as appropriate. + *

+ * Example usage in application.yml: + *

{@code
+ *
+ * }
+ * + * @param endpoint endpoint URL for the S3 service + * @param publicHost public host for access file anonymously + * @param pathStyle whether to enable path style for this s3 service + * @param bucket indicates which bucket is the target + * @param region region in which the S3 bucket is located + * @param accessKeyId access key ID used for authentication with the S3 provider + * @param secretAccessKey secret access key used for authentication with the S3 provider + * @author zihluwang + * @since 1.0.0 + */ +@ConfigurationProperties(prefix = "app.asset") +public record AssetProperties( + String endpoint, + String publicHost, + @DefaultValue("false") boolean pathStyle, + String bucket, + String region, + String accessKeyId, + String secretAccessKey +) { +} diff --git a/src/main/java/com/onixbyte/helix/properties/CaptchaProperties.java b/src/main/java/com/onixbyte/helix/properties/CaptchaProperties.java new file mode 100644 index 0000000..98eb110 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/properties/CaptchaProperties.java @@ -0,0 +1,11 @@ +package com.onixbyte.helix.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; + +@ConfigurationProperties(prefix = "app.captcha") +public record CaptchaProperties( + @DefaultValue("6") int length +) { +} + diff --git a/src/main/java/com/onixbyte/helix/properties/CorsProperties.java b/src/main/java/com/onixbyte/helix/properties/CorsProperties.java new file mode 100644 index 0000000..480be0c --- /dev/null +++ b/src/main/java/com/onixbyte/helix/properties/CorsProperties.java @@ -0,0 +1,82 @@ +package com.onixbyte.helix.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.http.HttpMethod; + +import java.time.Duration; + +/** + * Configuration properties class for Cross-Origin Resource Sharing (CORS) settings. + *

+ * This class provides a type-safe way to bind CORS-related configuration properties from + * application configuration files (such as {@code application.yml} or + * {@code application.properties}) to a Java object. All properties are prefixed with + * {@code app.cors} and are automatically mapped by Spring Boot's configuration property + * binding mechanism. + *

+ * CORS Overview: Cross-Origin Resource Sharing is a security feature implemented + * by web browsers that restricts web pages from making requests to a different domain than the one + * serving the web page. This class allows fine-grained control over CORS policies for the Helix + * application's REST APIs. + *

+ * Configuration Example: + *

{@code
+ * app:
+ *   cors:
+ *     allowed-origins:
+ *       - "https://example.com"
+ *       - "https://app.example.com"
+ *     allowed-methods:
+ *       - GET
+ *       - POST
+ *       - PUT
+ *       - DELETE
+ *     allowed-headers:
+ *       - "Content-Type"
+ *       - "Authorization"
+ *     allow-credentials: true
+ *     max-age: PT1H
+ * }
+ *

+ * Security Considerations: When configuring CORS, be mindful of + * security implications. Avoid using wildcard ({@code "*"}) for origins when + * {@code allowCredentials} is set to {@code true}, as this can expose the application to + * security vulnerabilities. + *

+ * The class follows JavaBean conventions with getter and setter methods for each property, + * making it compatible with Spring's property binding and validation frameworks. + * + * @param allowedHeaders HTTP header names that are permitted in the actual request + * @param allowedMethods HTTP methods (e.g., GET, POST, PUT) that are permitted for cross-origin requests + * @param allowedOrigins origins (domains) that are allowed to make cross-origin requests. Use "*" to + * allow all origins. + * @param allowCredentials indicating whether the client is allowed to send credentials (cookies, + * HTTP authentication) with the cross-origin request + * @param allowPrivateNetwork A flag indicating whether requests from a private network IP address + * to a public network IP address are allowed + * @param maxAge maximum amount of time for which the results of a preflight request + * can be cached by the client + * @param exposedHeaders HTTP header names that are allowed to be exposed to the client in a + * cross-origin response + * @author zihluwang + * @see ConfigurationProperties + * @see org.springframework.web.cors.CorsConfiguration + * @see org.springframework.web.servlet.config.annotation.CorsRegistry + * @since 1.0.0 + */ +@ConfigurationProperties(prefix = "app.cors") +public record CorsProperties( + @DefaultValue({"Content-Type", "Authorization"}) + String[] allowedHeaders, + @DefaultValue({"GET", "POST", "PUT", "PATCH", "DELETE"}) + HttpMethod[] allowedMethods, + String[] allowedOrigins, + boolean allowCredentials, + boolean allowPrivateNetwork, + @DefaultValue("PT1H30M") + Duration maxAge, + @DefaultValue({"Content-Type", "Authorization"}) + String[] exposedHeaders +) { +} diff --git a/src/main/java/com/onixbyte/helix/properties/MsalProperties.java b/src/main/java/com/onixbyte/helix/properties/MsalProperties.java new file mode 100644 index 0000000..5172c4c --- /dev/null +++ b/src/main/java/com/onixbyte/helix/properties/MsalProperties.java @@ -0,0 +1,49 @@ +package com.onixbyte.helix.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Microsoft Authentication Library (MSAL) integration. + *

+ * This class provides configuration binding for Microsoft Entra ID (formerly Azure Active Directory) + * authentication settings. It enables the Helix application to authenticate users through + * Microsoft's identity platform using the MSAL library, supporting single sign-on (SSO) + * and enterprise identity management scenarios. + *

+ * MSAL Integration: The Microsoft Authentication Library provides a unified + * way to authenticate users and acquire tokens for accessing Microsoft services and APIs. + * This configuration class specifically handles the core identifiers required for establishing + * a connection with Microsoft Entra ID. + *

+ * Configuration Example: + *

{@code
+ * app:
+ *   authentication:
+ *     msal:
+ *       client-id: "12345678-1234-1234-1234-123456789012"
+ *       tenant-id: "87654321-4321-4321-4321-210987654321"
+ * }
+ *

+ * Security Note: The client ID is considered public information and can be + * safely stored in configuration files. However, ensure that any client secrets (if used) + * are stored securely and not exposed in version control or logs. + *

+ * All properties are prefixed with {@code app.authentication.msal} and are automatically + * bound by Spring Boot's configuration property mechanism. + * + * @param tenantId directory (tenant) ID of the Microsoft Entra ID tenant where the application + * is registered + * @param clientId application (client) ID assigned to the application when registered in + * Microsoft Entra ID + * @author zihluwang + * @see ConfigurationProperties + * @see MSAL Overview + * @see Microsoft Entra ID Platform + * @since 1.0.0 + */ +@ConfigurationProperties(prefix = "app.authentication.msal") +public record MsalProperties( + String tenantId, + String clientId +) { +} diff --git a/src/main/java/com/onixbyte/helix/properties/TokenProperties.java b/src/main/java/com/onixbyte/helix/properties/TokenProperties.java new file mode 100644 index 0000000..63fc23d --- /dev/null +++ b/src/main/java/com/onixbyte/helix/properties/TokenProperties.java @@ -0,0 +1,57 @@ +package com.onixbyte.helix.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + +/** + * Configuration properties for JSON Web Token (JWT) generation and validation. + *

+ * This class provides configuration binding for JWT-related settings used throughout + * the Helix application for user authentication and authorisation. It encapsulates + * the essential parameters required for creating, signing, and validating JWT tokens + * in a secure and consistent manner. + *

+ * JWT Overview: JSON Web Tokens are a compact, URL-safe means of + * representing claims to be transferred between two parties. In the Helix application, + * JWTs are used to maintain user session state and carry authentication information + * across API requests without requiring server-side session storage. + *

+ * Configuration Example: + *

{@code
+ * app:
+ *   jwt:
+ *     issuer: "helix-server"
+ *     secret: "your-256-bit-secret-key-here"
+ *     valid-time: PT24H  # 24 hours
+ * }
+ *

+ * Security Considerations: + *

    + *
  • Secret Key: The secret must be kept confidential and should be + * at least 256 bits (32 characters) long for HS256 algorithm security.
  • + *
  • Token Expiry: Set appropriate validity periods to balance + * user experience with security requirements.
  • + *
  • Issuer Validation: The issuer claim helps prevent token + * misuse across different applications or environments.
  • + *
+ *

+ * All properties are prefixed with {@code app.jwt} and are automatically bound + * by Spring Boot's configuration property mechanism. + * + * @param issuer name of the issuer + * @param secret secret to sign a token + * @param validTime validity duration for JWT tokens + * @author zihluwang + * @see ConfigurationProperties + * @see RFC 7519 - JSON Web Token (JWT) + * @see JWT.io - JSON Web Tokens Introduction + * @since 1.0.0 + */ +@ConfigurationProperties(prefix = "app.jwt") +public record TokenProperties( + String issuer, + String secret, + Duration validTime +) { +} diff --git a/src/main/java/com/onixbyte/helix/repository/AssetRepository.java b/src/main/java/com/onixbyte/helix/repository/AssetRepository.java new file mode 100644 index 0000000..487ccc6 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/AssetRepository.java @@ -0,0 +1,9 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.Asset; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AssetRepository extends JpaRepository { +} diff --git a/src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java b/src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java new file mode 100644 index 0000000..96800c2 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java @@ -0,0 +1,9 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.Authority; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AuthorityRepository extends JpaRepository { +} diff --git a/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java b/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java new file mode 100644 index 0000000..70ceeab --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java @@ -0,0 +1,9 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.Department; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DepartmentRepository extends JpaRepository { +} diff --git a/src/main/java/com/onixbyte/helix/repository/MenuRepository.java b/src/main/java/com/onixbyte/helix/repository/MenuRepository.java new file mode 100644 index 0000000..d56903c --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/MenuRepository.java @@ -0,0 +1,9 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.Menu; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MenuRepository extends JpaRepository { +} diff --git a/src/main/java/com/onixbyte/helix/repository/PositionRepository.java b/src/main/java/com/onixbyte/helix/repository/PositionRepository.java new file mode 100644 index 0000000..59bd864 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/PositionRepository.java @@ -0,0 +1,9 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.Position; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PositionRepository extends JpaRepository { +} diff --git a/src/main/java/com/onixbyte/helix/repository/RoleAuthorityRepository.java b/src/main/java/com/onixbyte/helix/repository/RoleAuthorityRepository.java new file mode 100644 index 0000000..972639f --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/RoleAuthorityRepository.java @@ -0,0 +1,10 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.RoleAuthority; +import com.onixbyte.helix.domain.entity.embeddable.RoleAuthorityId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface RoleAuthorityRepository extends JpaRepository { +} diff --git a/src/main/java/com/onixbyte/helix/repository/RoleRepository.java b/src/main/java/com/onixbyte/helix/repository/RoleRepository.java new file mode 100644 index 0000000..57430e8 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/RoleRepository.java @@ -0,0 +1,9 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface RoleRepository extends JpaRepository { +} diff --git a/src/main/java/com/onixbyte/helix/repository/SettingRepository.java b/src/main/java/com/onixbyte/helix/repository/SettingRepository.java new file mode 100644 index 0000000..c7d9917 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/SettingRepository.java @@ -0,0 +1,10 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.Setting; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SettingRepository extends JpaRepository { + Setting getSettingByName(String name); +} diff --git a/src/main/java/com/onixbyte/helix/repository/UserIdentityRepository.java b/src/main/java/com/onixbyte/helix/repository/UserIdentityRepository.java new file mode 100644 index 0000000..932c91c --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/UserIdentityRepository.java @@ -0,0 +1,10 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.UserIdentity; +import com.onixbyte.helix.domain.entity.embeddable.UserIdentityId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserIdentityRepository extends JpaRepository { +} diff --git a/src/main/java/com/onixbyte/helix/repository/UserRepository.java b/src/main/java/com/onixbyte/helix/repository/UserRepository.java new file mode 100644 index 0000000..eced186 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { + User getUserById(Long id); +} diff --git a/src/main/java/com/onixbyte/helix/repository/UserRoleRepository.java b/src/main/java/com/onixbyte/helix/repository/UserRoleRepository.java new file mode 100644 index 0000000..8cd6a7b --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/UserRoleRepository.java @@ -0,0 +1,16 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.UserRole; +import com.onixbyte.helix.domain.entity.embeddable.UserRoleId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRoleRepository extends JpaRepository { + + @Modifying + @Query("DELETE FROM UserRole ur WHERE ur.id.userId = :userId") + int deleteByUserId(Long userId); +} diff --git a/src/main/java/com/onixbyte/helix/security/authentication/UsernamePasswordAuthentication.java b/src/main/java/com/onixbyte/helix/security/authentication/UsernamePasswordAuthentication.java new file mode 100644 index 0000000..2b90d25 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/security/authentication/UsernamePasswordAuthentication.java @@ -0,0 +1,80 @@ +package com.onixbyte.helix.security.authentication; + +import com.onixbyte.helix.domain.entity.User; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.CredentialsContainer; +import org.springframework.security.core.GrantedAuthority; +import java.util.Collection; +import java.util.List; + +public class UsernamePasswordAuthentication implements Authentication, CredentialsContainer { + private final String username; + private String password; + private boolean authenticated; + private User user; + private List authorities; + + private UsernamePasswordAuthentication(String username, String password, boolean authenticated, User user, List authorities) { + this.username = username; + this.password = password; + this.authenticated = authenticated; + this.user = user; + this.authorities = authorities; + } + + public static UsernamePasswordAuthentication unauthenticated(String username, String password) { + return new UsernamePasswordAuthentication(username, password, false, null, List.of()); + } + + public static UsernamePasswordAuthentication authenticated(User user, List authorities) { + return new UsernamePasswordAuthentication(user.getUsername(), null, true, user, authorities); + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getCredentials() { + return password; + } + + @Override + public User getDetails() { + return user; + } + + @Override + public String getPrincipal() { + return username; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } + + @Override + public void setAuthenticated(boolean authenticated) throws IllegalArgumentException { + this.authenticated = authenticated; + } + + @Override + public String getName() { + return username; + } + + @Override + public void eraseCredentials() { + this.password = null; + } + + public void setDetails(User user) { + this.user = user; + } + + public void setAuthorities(List authorities) { + this.authorities = authorities; + } +} diff --git a/src/main/java/com/onixbyte/helix/security/entrypoint/UnauthorisedAuthenticationEntryPoint.java b/src/main/java/com/onixbyte/helix/security/entrypoint/UnauthorisedAuthenticationEntryPoint.java new file mode 100644 index 0000000..4c3818d --- /dev/null +++ b/src/main/java/com/onixbyte/helix/security/entrypoint/UnauthorisedAuthenticationEntryPoint.java @@ -0,0 +1,36 @@ +package com.onixbyte.helix.security.entrypoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.onixbyte.helix.domain.web.response.BizExceptionResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.LocalDateTime; + +@Component +public class UnauthorisedAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + public UnauthorisedAuthenticationEntryPoint(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(new BizExceptionResponse( + LocalDateTime.now(), + authException.getMessage() + ))); + response.getWriter().flush(); + } +} + diff --git a/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java b/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java new file mode 100644 index 0000000..134ba66 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java @@ -0,0 +1,74 @@ +package com.onixbyte.helix.security.provider; + +import com.onixbyte.helix.domain.entity.Authority; +import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.manager.AuthorityManager; +import com.onixbyte.helix.manager.UserManager; +import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +@Component +public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider { + + private static final Logger log = LoggerFactory.getLogger(UsernamePasswordAuthenticationProvider.class); + private final UserManager userManager; + private final PasswordEncoder passwordEncoder; + private final AuthorityManager authorityManager; + + public UsernamePasswordAuthenticationProvider(UserManager userManager, PasswordEncoder passwordEncoder, AuthorityManager authorityManager) { + this.userManager = userManager; + this.passwordEncoder = passwordEncoder; + this.authorityManager = authorityManager; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (!(authentication instanceof UsernamePasswordAuthentication usernamePasswordAuthentication)) { + throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, "用户认证失败,请稍后再试。"); + } + + // get user from database + var user = userManager.selectByUsername(usernamePasswordAuthentication.getPrincipal()); + if (Objects.isNull(user)) { + log.error("User {} is trying to authenticate but no user found.", usernamePasswordAuthentication.getPrincipal()); + throw new BizException(HttpStatus.UNAUTHORIZED, "用户名或密码错误。"); + } + + // validate password + if (!passwordEncoder.matches(usernamePasswordAuthentication.getCredentials(), user.getPassword())) { + log.error("User {} is trying to authenticate but password is incorrect.", usernamePasswordAuthentication.getPrincipal()); + throw new BizException(HttpStatus.UNAUTHORIZED, "用户名或密码错误。"); + } + + // erase credentials + user.setPassword(null); + usernamePasswordAuthentication.eraseCredentials(); + + // get authorities + var authorities = authorityManager.queryByUserId(user.getId()) + .stream() + .map(Authority::asGrantedAuthority) + .toList(); + + // set values + usernamePasswordAuthentication.setAuthenticated(true); + usernamePasswordAuthentication.setDetails(user); + usernamePasswordAuthentication.setAuthorities(authorities); + + return usernamePasswordAuthentication; + } + + @Override + public boolean supports(Class authentication) { + return UsernamePasswordAuthentication.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/com/onixbyte/helix/service/AssetService.java b/src/main/java/com/onixbyte/helix/service/AssetService.java new file mode 100644 index 0000000..32bcfc8 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/service/AssetService.java @@ -0,0 +1,118 @@ +package com.onixbyte.helix.service; + +import com.onixbyte.helix.domain.entity.Asset; +import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.manager.AssetManager; +import com.onixbyte.helix.properties.AssetProperties; +import com.onixbyte.helix.utils.SecurityUtil; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * Service interface for file storage operations. + *

+ * Provides a unified interface for file upload, download, and deletion operations that can be + * implemented by different storage backends (local filesystem, S3-compatible services, etc.). + * + * @author zihluwang + * @since 1.0.0 + */ +@Service +public class AssetService { + + private final AssetProperties assetProperties; + private final S3Client s3Client; + private final AssetManager assetManager; + + public AssetService( + AssetProperties assetProperties, + S3Client s3Client, + AssetManager assetManager + ) { + this.assetProperties = assetProperties; + this.s3Client = s3Client; + this.assetManager = assetManager; + } + + /** + * Uploads a file to the configured storage backend. The file will be validated according to the + * storage configuration and stored with a unique filename to prevent conflicts. + * + * @param prefix prefix to the file, should not started with a {@code /} or {@code ..} + * @param file the multipart file to upload + * @return the URL or path that can be used to access the uploaded file + * @throws IllegalArgumentException if the file is invalid (empty, too large, wrong extension, + * etc.) + * @throws RuntimeException if the upload operation fails + */ + @Transactional(rollbackFor = {Throwable.class}) + public String uploadFile(String prefix, MultipartFile file) throws IOException { + if (Objects.isNull(prefix) || prefix.isBlank() || prefix.startsWith("/") || prefix.startsWith("..")) { + throw new BizException( + HttpStatus.INTERNAL_SERVER_ERROR, + "Prefix must not be empty, and should not start with '/' or '..'." + ); + } + + var currentUser = SecurityUtil.getCurrentUser(); + + var fullKey = buildFullKey(prefix, file.getOriginalFilename()); + + var request = PutObjectRequest.builder() + .bucket(assetProperties.bucket()) + .key(fullKey) + .contentType(file.getContentType()) + .contentLength(file.getSize()) + .build(); + + s3Client.putObject(request, RequestBody.fromInputStream(file.getInputStream(), file.getSize())); + + assetManager.save(Asset.builder() + .key(fullKey) + .uploadBy(currentUser.getId()) + .uploadTime(LocalDateTime.now()) + .build()); + + var fileUrlBuilder = new StringBuilder(assetProperties.publicHost()); + if (assetProperties.pathStyle()) { + fileUrlBuilder.append(assetProperties.bucket()); + } + fileUrlBuilder.append("/").append(fullKey); + return fileUrlBuilder.toString(); + } + + private String buildFullKey(String prefix, String fileName) { + return String.format("%s/%s", prefix, fileName); + } + + /** + * Delete file with given asset ID. + * + * @param assetId ID of the asset + */ + @Transactional(rollbackFor = Throwable.class) + public void deleteAsset(Long assetId) { + var currentUser = SecurityUtil.getCurrentUser(); + var asset = assetManager.queryByAssetId(assetId); + + if (!Objects.equals(currentUser.getId(), asset.getUploadBy())) { + throw new BizException(HttpStatus.FORBIDDEN, "You are not able to delete an asset that is not uploaded by you."); + } + + assetManager.deleteById(assetId); + s3Client.deleteObject(DeleteObjectRequest.builder() + .bucket(assetProperties.bucket()) + .key(asset.getKey()) + .build()); + } +} diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java new file mode 100644 index 0000000..1103f5e --- /dev/null +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -0,0 +1,94 @@ +package com.onixbyte.helix.service; + +import com.onixbyte.helix.client.TokenClient; +import com.onixbyte.helix.constant.SettingName; +import com.onixbyte.helix.domain.entity.Setting; +import com.onixbyte.helix.domain.web.request.UsernamePasswordLoginRequest; +import com.onixbyte.helix.domain.web.response.LoginSuccessResponse; +import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.manager.CaptchaManager; +import com.onixbyte.helix.manager.SettingManager; +import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.stereotype.Service; + +import java.util.Objects; +import java.util.Optional; + +@Service +public class AuthService { + + private static final Logger log = LoggerFactory.getLogger(AuthService.class); + + private final CaptchaManager captchaManager; + private final AuthenticationManager authenticationManager; + private final TokenClient tokenClient; + private final SettingManager settingManager; + + @Autowired + public AuthService( + CaptchaManager captchaManager, + AuthenticationManager authenticationManager, + TokenClient tokenClient, + SettingManager settingManager + ) { + this.captchaManager = captchaManager; + this.authenticationManager = authenticationManager; + this.tokenClient = tokenClient; + this.settingManager = settingManager; + } + + /** + * Perform user login. + * + * @param request user login request + * @return user information and user identity token + * @throws BizException if the user does not exist, or the username and password are incorrect + */ + public LoginSuccessResponse login(UsernamePasswordLoginRequest request) { + var captchaEnabled = Optional.ofNullable(settingManager.getSettingByName(SettingName.CAPTCHA_ENABLED)) + .map(Setting::asBoolean) + .orElse(false); + if (captchaEnabled) { + var uuid = request.uuid(); + var rawCaptcha = captchaManager.getCaptcha(uuid); + + if (Objects.isNull(rawCaptcha) || rawCaptcha.isBlank()) { + throw new BizException(HttpStatus.BAD_REQUEST, "未找到验证码"); + } + if (!rawCaptcha.equalsIgnoreCase(request.captcha())) { + throw new BizException(HttpStatus.BAD_REQUEST, "验证码错误"); + } + } + + var _authentication = authenticationManager.authenticate(UsernamePasswordAuthentication + .unauthenticated(request.username(), request.password())); + if (!(_authentication instanceof UsernamePasswordAuthentication authentication)) { + log.error( + "Type mismatched, required type is UsernamePasswordAuthentication but got {}.", + _authentication.getClass() + ); + throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, + "Cannot perform login due to server crashes."); + } + + var token = tokenClient.generateToken(authentication.getDetails()); + + return new LoginSuccessResponse(token, authentication.getDetails()); + } + + /** + * Get whether the registration function is turned on. + * + * @return {@code true} if registration is enabled, otherwise {@code false} + */ + public boolean getRegisterEnabled() { + return Optional.ofNullable(settingManager.getSettingByName(SettingName.REGISTER_ENABLED)) + .map(Setting::asBoolean) + .orElse(false); + } +} diff --git a/src/main/java/com/onixbyte/helix/service/CaptchaService.java b/src/main/java/com/onixbyte/helix/service/CaptchaService.java new file mode 100644 index 0000000..3160965 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/service/CaptchaService.java @@ -0,0 +1,76 @@ +package com.onixbyte.helix.service; + +import com.onixbyte.captcha.Producer; +import com.onixbyte.helix.constant.FileType; +import com.onixbyte.helix.constant.SettingName; +import com.onixbyte.helix.domain.entity.Setting; +import com.onixbyte.helix.domain.web.response.CaptchaResponse; +import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.manager.CaptchaManager; +import com.onixbyte.helix.manager.SettingManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.FastByteArrayOutputStream; + +import javax.imageio.ImageIO; +import java.io.IOException; +import java.util.Base64; +import java.util.Optional; +import java.util.UUID; + +@Service +public class CaptchaService { + + private final CaptchaManager captchaManager; + private final SettingManager settingManager; + + @Autowired + public CaptchaService(CaptchaManager captchaManager, SettingManager settingManager) { + this.captchaManager = captchaManager; + this.settingManager = settingManager; + } + + private Producer producer; + + @Autowired(required = false) + public void setProducer(Producer producer) { + this.producer = producer; + } + + /** + * Build captcha information. + * + * @return left value is data URL of captcha image, and right value is the identifier of + * the captcha code + */ + public CaptchaResponse buildCaptcha() { + var captchaEnabled = Optional.ofNullable(settingManager.getSettingByName(SettingName.CAPTCHA_ENABLED)) + .map(Setting::asBoolean) + .orElse(false); + if (!captchaEnabled) { + return null; + } + + // 生成 UUID 及验证码 + var uuid = UUID.randomUUID().toString().replaceAll("-", ""); + var captchaCode = producer.createText(); + + // 将验证码保存到缓存中 + captchaManager.setCaptcha(uuid, captchaCode); + + // 生成验证码图片 + var captchaImage = producer.createImage(captchaCode); + try (var byteArrayOutputStream = new FastByteArrayOutputStream()) { + ImageIO.write(captchaImage, FileType.Image.JPEG.getExtension(), byteArrayOutputStream); + var captchaDataUrl = "data:image/jpeg;base64," + + Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); + return new CaptchaResponse(captchaDataUrl, uuid); + } catch (IOException e) { + throw new BizException("无法生成验证码图片。"); + } + } + + public String getCaptcha(String uuid) { + return captchaManager.getCaptcha(uuid); + } +} diff --git a/src/main/java/com/onixbyte/helix/service/DepartmentService.java b/src/main/java/com/onixbyte/helix/service/DepartmentService.java new file mode 100644 index 0000000..15f03c4 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/service/DepartmentService.java @@ -0,0 +1,30 @@ +package com.onixbyte.helix.service; + +import com.onixbyte.helix.domain.entity.Department; +import com.onixbyte.helix.domain.model.TreeNode; +import com.onixbyte.helix.manager.DepartmentManager; +import com.onixbyte.helix.utils.TreeUtil; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class DepartmentService { + + private final DepartmentManager departmentManager; + + public DepartmentService(DepartmentManager departmentManager) { + this.departmentManager = departmentManager; + } + + public TreeNode getDepartmentTree() { + var departments = departmentManager.selectAll(Pageable.unpaged()); + return TreeUtil.buildTree(departments.getContent()); + } + + public List getDepartments() { + return departmentManager.selectAll(Pageable.unpaged()).getContent(); + } +} diff --git a/src/main/java/com/onixbyte/helix/service/MenuService.java b/src/main/java/com/onixbyte/helix/service/MenuService.java new file mode 100644 index 0000000..618ff89 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/service/MenuService.java @@ -0,0 +1,28 @@ +package com.onixbyte.helix.service; + +import com.onixbyte.helix.domain.entity.Menu; +import com.onixbyte.helix.domain.model.TreeNode; +import com.onixbyte.helix.manager.MenuManager; +import com.onixbyte.helix.utils.SecurityUtil; +import com.onixbyte.helix.utils.TreeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class MenuService { + private static final Logger log = LoggerFactory.getLogger(MenuService.class); + private final MenuManager menuManager; + + public MenuService(MenuManager menuManager) { + this.menuManager = menuManager; + } + + public List> getMenuTree() { + var user = SecurityUtil.getCurrentUser(); + var menus = menuManager.selectActiveMenusByUserId(user.getId()); + return TreeUtil.buildForest(menus); + } +} diff --git a/src/main/java/com/onixbyte/helix/service/PositionService.java b/src/main/java/com/onixbyte/helix/service/PositionService.java new file mode 100644 index 0000000..905231b --- /dev/null +++ b/src/main/java/com/onixbyte/helix/service/PositionService.java @@ -0,0 +1,23 @@ +package com.onixbyte.helix.service; + +import com.onixbyte.helix.domain.entity.Position; +import com.onixbyte.helix.manager.PositionManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class PositionService { + + private final PositionManager positionManager; + + public PositionService(PositionManager positionManager) { + this.positionManager = positionManager; + } + + public Page getPositions(Pageable pageable) { + return positionManager.selectAll(pageable); + } +} diff --git a/src/main/java/com/onixbyte/helix/service/RoleService.java b/src/main/java/com/onixbyte/helix/service/RoleService.java new file mode 100644 index 0000000..15a18c6 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/service/RoleService.java @@ -0,0 +1,42 @@ +package com.onixbyte.helix.service; + +import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.domain.database.query.wrapper.QueryRoleWrapper; +import com.onixbyte.helix.domain.entity.Role; +import com.onixbyte.helix.domain.web.request.QueryRoleRequest; +import com.onixbyte.helix.manager.RoleManager; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class RoleService { + + private final RoleManager roleManager; + + public RoleService(RoleManager roleManager) { + this.roleManager = roleManager; + } + + public Page getRoles(Pageable pageable, QueryRoleRequest request) { + QueryRoleWrapper wrapper = new QueryRoleWrapper(); + + Optional.ofNullable(request.name()) + .filter(StringUtils::isNotBlank) + .ifPresent(wrapper::setName); + + Optional.ofNullable(request.code()) + .filter(StringUtils::isNotBlank) + .ifPresent(wrapper::setCode); + + Optional.ofNullable(request.status()) + .filter(StringUtils::isNotBlank) + .map(Status::valueOf) + .ifPresent(wrapper::setStatus); + + return roleManager.selectAll(pageable, wrapper); + } +} diff --git a/src/main/java/com/onixbyte/helix/service/SettingService.java b/src/main/java/com/onixbyte/helix/service/SettingService.java new file mode 100644 index 0000000..9ddbbc0 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/service/SettingService.java @@ -0,0 +1,7 @@ +package com.onixbyte.helix.service; + +import org.springframework.stereotype.Service; + +@Service +public class SettingService { +} diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java new file mode 100644 index 0000000..a33343a --- /dev/null +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -0,0 +1,167 @@ +package com.onixbyte.helix.service; + +import com.onixbyte.common.util.HashUtil; +import com.onixbyte.helix.constant.UserStatus; +import com.onixbyte.helix.domain.database.query.wrapper.QueryUserWrapper; +import com.onixbyte.helix.domain.entity.Role; +import com.onixbyte.helix.domain.entity.User; +import com.onixbyte.helix.domain.entity.UserRole; +import com.onixbyte.helix.domain.web.request.AddUserRequest; +import com.onixbyte.helix.domain.web.request.QueryUserRequest; +import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; +import com.onixbyte.helix.domain.web.request.UpdateUserRequest; +import com.onixbyte.helix.domain.web.response.UserDetailResponse; +import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.manager.ApplicationManager; +import com.onixbyte.helix.manager.RoleManager; +import com.onixbyte.helix.manager.UserManager; +import com.onixbyte.helix.manager.UserRoleManager; +import com.onixbyte.identitygenerator.IdentityGenerator; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Service +public class UserService { + + private final UserManager userManager; + private final IdentityGenerator userIdentityGenerator; + private final RoleManager roleManager; + private final UserRoleManager userRoleManager; + private final PasswordEncoder passwordEncoder; + private final ApplicationManager applicationManager; + + public UserService( + UserManager userManager, + IdentityGenerator userIdentityGenerator, + RoleManager roleManager, + UserRoleManager userRoleManager, + PasswordEncoder passwordEncoder, + ApplicationManager applicationManager + ) { + this.userManager = userManager; + this.userIdentityGenerator = userIdentityGenerator; + this.roleManager = roleManager; + this.userRoleManager = userRoleManager; + this.passwordEncoder = passwordEncoder; + this.applicationManager = applicationManager; + } + + public Page queryUserDetailsPage(Pageable pageable, QueryUserRequest request) { + var wrapper = new QueryUserWrapper(); + wrapper.setDepartmentId(request.departmentId()); + wrapper.setUsername(request.username()); + wrapper.setRegionAbbreviation(request.regionAbbreviation()); + wrapper.setPhoneNumber(request.phoneNumber()); + wrapper.setStatus(Optional.ofNullable(request.status()) + .filter((status) -> !status.isBlank()) + .map(UserStatus::valueOf) + .orElse(null)); + wrapper.setCreatedAtStart(request.createdAtStart()); + wrapper.setCreatedAtEnd(request.createdAtEnd()); + + return userManager.selectUserDetailsPage(pageable, wrapper); + } + + @Transactional(rollbackFor = Throwable.class) + public void addUser(AddUserRequest request) { + var createTime = LocalDateTime.now(); + + // validate all roles are existed + if (CollectionUtils.isNotEmpty(request.roleIds())) { + roleManager.validateRoles(request.roleIds()); + } + + // Get user email or use default email address + var userEmail = Optional.ofNullable(request.email()) + .orElse(applicationManager.getDefaultEmail()); + + // Gravatar is used by default when user didn't set an avatar + var avatarUrl = Optional.ofNullable(request.avatarUrl()) + .orElseGet(() -> "https://gravatar.com/avatar/" + HashUtil.sha256(userEmail) + "/?d=identicon"); + + // Get user status, default to `ACTIVE` + var status = Optional.ofNullable(request.status()) + .orElse(UserStatus.ACTIVE); + + // Build user information + var user = userManager.save(User.builder() + .id(userIdentityGenerator.nextId()) + .username(request.username()) + .password(passwordEncoder.encode(request.password())) + .fullName(request.fullName()) + .email(request.email()) + .regionAbbreviation(request.regionAbbreviation()) + .phoneNumber(request.phoneNumber()) + .avatarUrl(avatarUrl) + .status(status) + .departmentId(request.departmentId()) + .positionId(request.positionId()) + .createdAt(createTime) + .updatedAt(createTime) + .build()); + + // Get role IDs + var roleIds = Optional.ofNullable(request.roleIds()) + .filter(CollectionUtils::isNotEmpty) + .orElseGet(() -> List.of(roleManager.getRole(Role.builder() + .defaultValue(true) + .build()) + .map(Role::getId) + .orElseThrow(() -> new BizException("No default role specified.")))); + + // Build bindings + var userRoleBindings = roleIds + .stream() + .map((roleId) -> UserRole.builder() + .userId(user.getId()) + .roleId(roleId) + .createdAt(createTime) + .build() + ) + .toList(); + + // Save user and role bindings + userRoleManager.saveBatch(userRoleBindings); + } + + @Transactional(rollbackFor = Throwable.class) + public void updateUser(UpdateUserRequest request) { + userManager.updateUser(User.builder() + .id(request.id()) + .fullName(request.fullName()) + .email(request.email()) + .regionAbbreviation(request.regionAbbreviation()) + .phoneNumber(request.phoneNumber()) + .avatarUrl(request.avatarUrl()) + .status(request.status()) + .departmentId(request.departmentId()) + .positionId(request.positionId()) + .build()); + } + + public UserDetailResponse getUserDetailByUserId(Long userId) { + return userManager.queryUserDetailByUserId(userId); + } + + @Transactional(rollbackFor = Throwable.class) + public void resetPassword(ResetPasswordRequest request) { + userManager.updateUser(User.builder() + .id(request.id()) + .password(request.password()) + .build()); + } + + @Transactional(rollbackFor = Throwable.class) + public void deleteUser(Long userId) { + userRoleManager.deleteByUserId(userId); + userManager.deleteById(userId); + } +} diff --git a/src/main/java/com/onixbyte/helix/utils/DateTimeUtil.java b/src/main/java/com/onixbyte/helix/utils/DateTimeUtil.java new file mode 100644 index 0000000..6517a4d --- /dev/null +++ b/src/main/java/com/onixbyte/helix/utils/DateTimeUtil.java @@ -0,0 +1,13 @@ +package com.onixbyte.helix.utils; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +public class DateTimeUtil { + + public static Instant asInstant(LocalDateTime localDateTime) { + return localDateTime.atZone(ZoneId.systemDefault()) + .toInstant(); + } +} diff --git a/src/main/java/com/onixbyte/helix/utils/EncodeUtil.java b/src/main/java/com/onixbyte/helix/utils/EncodeUtil.java new file mode 100644 index 0000000..5be5e06 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/utils/EncodeUtil.java @@ -0,0 +1,94 @@ +package com.onixbyte.helix.utils; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * Utility class providing encoding and decoding operations for web applications. + *

+ * This utility class offers methods for handling URL encoding operations that are commonly required + * in web applications, particularly for ensuring compatibility between Java server-side encoding + * and JavaScript client-side encoding standards. + *

+ * The primary focus is on providing encoding methods that precisely replicate JavaScript's built-in + * encoding functions, ensuring consistent behaviour across different platforms and environments. + * This is particularly important for applications that need to maintain encoding consistency + * between server-side Java code and client-side JavaScript code. + *

+ * All methods in this utility class are static and the class is designed to be used + * without instantiation. The encoding operations follow web standards and are optimised for + * performance and reliability. + * + * @author zihluwang + * @see java.net.URLEncoder + * @see java.nio.charset.StandardCharsets + * @since 1.0.0 + */ +public class EncodeUtil { + + /** + * Encodes a string to be used in a URI component, precisely replicating JavaScript's + * {@code encodeURIComponent} function. + *

+ * This method provides exact compatibility with JavaScript's {@code encodeURIComponent} + * function by first using Java's standard {@link URLEncoder} and then applying specific + * corrections to match JavaScript's encoding behaviour. + *

+ * The method handles the following character encoding differences: + *

    + *
  • Converts "+" to "{@code %20}" for proper space encoding
  • + *
  • Preserves "*" characters (decoded from "{@code %2A}")
  • + *
  • Preserves "'" characters (decoded from "{@code %27}")
  • + *
  • Preserves "(" characters (decoded from "{@code %28}")
  • + *
  • Preserves ")" characters (decoded from "{@code %29}")
  • + *
+ *

+ * Characters that remain unencoded in both implementations: {@code - _ . ! ~ * ' ( )} + *

+ * This method is particularly useful for applications that need to maintain encoding + * consistency between server-side Java operations and client-side JavaScript operations, such + * as when generating URLs or handling form data. + * + * @param content the string component to encode for URI usage + * @return a URI-component-safe encoded string that matches JavaScript's + * {@code encodeURIComponent} output + * @throws NullPointerException if the content parameter is null + * @see java.net.URLEncoder#encode(String, java.nio.charset.Charset) + * @see MDN encodeURIComponent Documentation + */ + public static String encodeUriComponent(String content) { + String encodedComponent; + + // First, we use the standard Java URLEncoder. + // + // It's crucial to specify the character encoding, with UTF-8 being the web standard. + encodedComponent = URLEncoder.encode(content, StandardCharsets.UTF_8); + + // Next, we must manually correct the output of URLEncoder to match encodeURIComponent's + // behaviour. + // + // JavaScript's encodeURIComponent does not encode these characters: - _ . ! ~ * ' ( ) + // URLEncoder, however, does encode some of them. + + // Replace "+" with "%20" (space character) + encodedComponent = encodedComponent.replace("+", "%20"); + + // URLEncoder encodes "*" as "%2A", but encodeURIComponent does not, so we decode it back. + encodedComponent = encodedComponent.replace("%2A", "*"); + + // URLEncoder encodes "'" as "%27", but encodeURIComponent does not. + encodedComponent = encodedComponent.replace("%27", "'"); + + // URLEncoder encodes "(" as "%28", but encodeURIComponent does not. + encodedComponent = encodedComponent.replace("%28", "("); + + // URLEncoder encodes ")" as "%29", but encodeURIComponent does not. + encodedComponent = encodedComponent.replace("%29", ")"); + + // Note: The characters '!', '~' are also not encoded by encodeURIComponent. + // + // Modern versions of URLEncoder (in recent JDKs) correctly leave '!' and '~' unencoded, + // so explicit replacements for "%21" and "%7E" are typically no longer necessary. + return encodedComponent; + } +} diff --git a/src/main/java/com/onixbyte/helix/utils/SecurityUtil.java b/src/main/java/com/onixbyte/helix/utils/SecurityUtil.java new file mode 100644 index 0000000..5165508 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/utils/SecurityUtil.java @@ -0,0 +1,26 @@ +package com.onixbyte.helix.utils; + +import com.onixbyte.helix.domain.entity.User; +import com.onixbyte.helix.exception.BizException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; + +public class SecurityUtil { + + private static final Logger log = LoggerFactory.getLogger(SecurityUtil.class); + + public static User getCurrentUser() { + var _details = SecurityContextHolder.getContext() + .getAuthentication() + .getDetails(); + + if (!(_details instanceof User user)) { + log.error("Authentication details is {}", _details); + throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, "Cannot retrieve user information from context."); + } + + return user; + } +} diff --git a/src/main/java/com/onixbyte/helix/utils/TreeUtil.java b/src/main/java/com/onixbyte/helix/utils/TreeUtil.java new file mode 100644 index 0000000..e5ba853 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/utils/TreeUtil.java @@ -0,0 +1,130 @@ +package com.onixbyte.helix.utils; + +import com.onixbyte.helix.domain.common.Treeable; +import com.onixbyte.helix.domain.model.TreeNode; +import com.onixbyte.helix.exception.BizException; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.http.HttpStatus; + +import java.util.*; +import java.util.stream.Collectors; + +public class TreeUtil { + + /** + * Builds a tree from a flat list of items that implement Treeable. + * + * @param items the flat list of treeable items + * @param the type extending Treeable + * @return a single root TreeNode + */ + public static > TreeNode buildTree(List items) { + if (CollectionUtils.isEmpty(items)) { + return null; + } + + // A map to quickly access the TreeNode corresponding to a node ID. + // This is crucial for building the hierarchical links between parent and child nodes. + var idToNodeMap = items.stream() + .collect(Collectors.toMap(Treeable::getId, (item) -> new TreeNode<>(item, new ArrayList<>()))); + + // Get root node + var rootItems = items.stream() + .filter((item) -> item.getParentId() == null) + .toList(); + + // Ensure only 1 root node is included + if (rootItems.size() > 1) { + throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, "Multiple root items found in given values."); + } + + // Get root item and build root node + var rootItem = rootItems.get(0); + var rootNode = new TreeNode(rootItem); + idToNodeMap.remove(rootItem.getId()); + + // Iterate through all departments to build the hierarchy. + for (var item : items) { + var currentNode = idToNodeMap.get(item.getId()); + // If it has a parentId, find its parent TreeNode and add the current node to its children. + var parentNode = Objects.equals(item.getParentId(), rootItem.getId()) ? rootNode : idToNodeMap.get(item.getParentId()); + if (parentNode != null) { // Ensure the parent actually exists in the provided list + parentNode.children().add(currentNode); + } + } + + sortChildrenRecursively(rootNode.children()); + + return rootNode; + } + + /** + * Builds a forest from a flat list of items that implement Treeable. + * + * @param items the flat list of treeable items + * @param the type extending Treeable + * @return a list of root TreeNodes + */ + public static > List> buildForest(List items) { + if (CollectionUtils.isEmpty(items)) { + return Collections.emptyList(); + } + + // A map to quickly access the TreeNode corresponding to a node ID. + // This is crucial for building the hierarchical links between parent and child nodes. + var idToNodeMap = items.stream() + .collect(Collectors.toMap(Treeable::getId, (item) -> new TreeNode<>(item, new ArrayList<>()))); + + // A list to store the top-level (root) departments. + var rootNodes = new ArrayList>(); + + // Iterate through all departments to build the hierarchy. + for (var item : items) { + var currentNode = idToNodeMap.get(item.getId()); + + if (Objects.isNull(item.getParentId())) { + // If a department has no parentId, it's a root department. + rootNodes.add(currentNode); + } else { + // If it has a parentId, find its parent TreeNode and add the current node to its children. + var parentNode = idToNodeMap.get(item.getParentId()); + if (parentNode != null) { // Ensure the parent actually exists in the provided list + parentNode.children().add(currentNode); + } + } + } + + // 4. Sort the children lists for all nodes, starting from the root nodes, to ensure consistent display order. + sortChildrenRecursively(rootNodes); + + return rootNodes; + } + + /** + * Recursively sorts the children list of each TreeNode based on the 'sort' field of its Department item. + * This method traverses the tree structure to apply sorting at every level. + * + * @param nodes The list of TreeNodes to sort (e.g., the initial list of root nodes, or a sub-list of children). + */ + private static > void sortChildrenRecursively(List> nodes) { + if (nodes == null || nodes.isEmpty()) { + return; + } + + // Define the comparator for sorting children based on the 'sort' field of the menu item. + var comparator = Comparator., Integer>comparing( + (node) -> node.item().getSort(), + Comparator.nullsLast(Comparator.naturalOrder()) // Handle null 'sort' values by placing them last. + ); + + // Sort the current list of nodes (e.g., root nodes, or a parent's immediate children). + nodes.sort(comparator); + + // Recursively apply sorting to the children of each node in the current list. + for (var node : nodes) { + if (node.children() != null && !node.children().isEmpty()) { + sortChildrenRecursively(node.children()); + } + } + } +} diff --git a/src/main/java/com/onixbyte/helix/validation/group/OnCreate.java b/src/main/java/com/onixbyte/helix/validation/group/OnCreate.java new file mode 100644 index 0000000..d9799f8 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/validation/group/OnCreate.java @@ -0,0 +1,64 @@ +package com.onixbyte.helix.validation.group; + +/** + * Validation group interface for create operations. + *

+ * This marker interface is used with Bean Validation (JSR-303/JSR-380) to define a validation group + * that is specifically applied during entity creation operations. By using validation groups, + * different validation rules can be applied depending on the context of the operation (create vs. + * update vs. other scenarios). + *

+ * Usage Pattern: This interface is typically used in conjunction with validation + * annotations such as {@code @NotNull}, {@code @NotBlank}, {@code @Valid}, etc., to specify that + * certain validation constraints should only be applied when creating new entities. + *

+ * Example Usage: + *

{@code
+ * public class User {
+ *     @NotNull(groups = OnCreate.class, message = "Username is required for new users")
+ *     @Size(min = 3, max = 50, groups = OnCreate.class)
+ *     private String username;
+ *     
+ *     @NotNull(groups = OnCreate.class, message = "Password is required for new users")
+ *     @Size(min = 8, groups = OnCreate.class)
+ *     private String password;
+ *     
+ *     // Other fields...
+ * }
+ * 
+ * // In a REST controller:
+ * @PostMapping("/users")
+ * public ResponseEntity createUser(
+ *     @Validated(OnCreate.class) @RequestBody User user) {
+ *     // Create user logic
+ * }
+ * }
+ *

+ * Benefits of Validation Groups: + *

    + *
  • + * Context-Specific Validation: Apply different validation rules for different + * operations (e.g., password required on create but not on update) + *
  • + *
  • + * Flexible Constraint Application: Enable or disable specific validations based + * on the operation context + *
  • + *
  • + * Cleaner Code: Avoid complex conditional validation logic by using declarative + * group-based validation + *
  • + *
+ *

+ * This interface contains no methods and serves purely as a marker for the Bean Validation + * framework to identify which constraints should be applied during create operations. + * + * @author zihluwang + * @since 1.0.0 + * @see OnUpdate + * @see jakarta.validation.groups.Default + * @see org.springframework.validation.annotation.Validated + * @see Bean Validation Specification - Validation Groups + */ +public interface OnCreate { +} diff --git a/src/main/java/com/onixbyte/helix/validation/group/OnUpdate.java b/src/main/java/com/onixbyte/helix/validation/group/OnUpdate.java new file mode 100644 index 0000000..dcb17c0 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/validation/group/OnUpdate.java @@ -0,0 +1,84 @@ +package com.onixbyte.helix.validation.group; + +/** + * Validation group interface for update operations. + *

+ * This marker interface is used with Bean Validation (JSR-303/JSR-380) to define a validation group + * that is specifically applied during entity update operations. By using validation groups, + * different validation rules can be applied depending on the context of the operation (update vs. + * create vs. other scenarios). + *

+ * Usage Pattern: This interface is typically used in conjunction with validation + * annotations such as {@code @NotNull}, {@code @NotBlank}, {@code @Valid}, etc., to specify that + * certain validation constraints should only be applied when updating existing entities. + *

+ * Example Usage: + *

{@code
+ * public class User {
+ *     @NotNull(groups = OnUpdate.class, message = "ID is required for updates")
+ *     @Positive(groups = OnUpdate.class)
+ *     private Long id;
+ *     
+ *     @Size(min = 3, max = 50, groups = OnUpdate.class)
+ *     private String username;
+ *     
+ *     // Password might be optional on update
+ *     @Size(min = 8, groups = OnUpdate.class)
+ *     private String password;
+ *     
+ *     // Other fields...
+ * }
+ * 
+ * // In a REST controller:
+ * @PutMapping("/users/{id}")
+ * public ResponseEntity updateUser(
+ *     @PathVariable Long id,
+ *     @Validated(OnUpdate.class) @RequestBody User user) {
+ *     // Update user logic
+ * }
+ * }
+ *

+ * Common Update Validation Scenarios: + *

    + *
  • + * ID Validation: Ensure entity ID is present and valid for updates + *
  • + *
  • + * Optional Fields: Allow certain fields to be optional during updates that were + * required during creation + *
  • + *
  • + * Partial Updates: Support PATCH operations where only modified fields + * need validation + *
  • + *
  • + * Version Control: Validate version fields for optimistic locking + *
  • + *
+ *

+ * Benefits of Update-Specific Validation: + *

    + *
  • + * Flexible Field Requirements: Different fields may be required or optional + * compared to create operations + *
  • + *
  • + * Identity Validation: Ensure proper entity identification for update operations + *
  • + *
  • + * Conditional Logic: Apply validation rules specific to modification scenarios + *
  • + *
+ *

+ * This interface contains no methods and serves purely as a marker for the Bean Validation + * framework to identify which constraints should be applied during update operations. + * + * @author zihluwang + * @since 1.0.0 + * @see OnCreate + * @see jakarta.validation.groups.Default + * @see org.springframework.validation.annotation.Validated + * @see Bean Validation Specification - Validation Groups + */ +public interface OnUpdate { +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..5d853f8 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,29 @@ +spring: + application: + name: helix-server + cache: + type: redis + redis: + time-to-live: PT1H30M + jpa: + properties: + hibernate: + transaction: + jta: + # No need to use distributed transaction manager for 1 datasource. + platform: org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform + hibernate: + ddl-auto: validate + open-in-view: false + datasource: + hikari: + minimum-idle: 1 + maximum-pool-size: 10 + +mybatis: + configuration: + log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl + map-underscore-to-camel-case: true + type-aliases-package: com.onixbyte.helix.domain.entity + type-handlers-package: com.onixbyte.helix.extension.mybatis.handler + mapper-locations: classpath:/mapper/*.xml diff --git a/src/main/resources/mapper/AssetMapper.xml b/src/main/resources/mapper/AssetMapper.xml new file mode 100644 index 0000000..a329395 --- /dev/null +++ b/src/main/resources/mapper/AssetMapper.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/AuthorityMapper.xml b/src/main/resources/mapper/AuthorityMapper.xml new file mode 100644 index 0000000..ea9d26b --- /dev/null +++ b/src/main/resources/mapper/AuthorityMapper.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/DepartmentMapper.xml b/src/main/resources/mapper/DepartmentMapper.xml new file mode 100644 index 0000000..1521ffa --- /dev/null +++ b/src/main/resources/mapper/DepartmentMapper.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/MenuMapper.xml b/src/main/resources/mapper/MenuMapper.xml new file mode 100644 index 0000000..f161b13 --- /dev/null +++ b/src/main/resources/mapper/MenuMapper.xml @@ -0,0 +1,38 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/PositionMapper.xml b/src/main/resources/mapper/PositionMapper.xml new file mode 100644 index 0000000..eba56ca --- /dev/null +++ b/src/main/resources/mapper/PositionMapper.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/RoleMapper.xml b/src/main/resources/mapper/RoleMapper.xml new file mode 100644 index 0000000..fff52e0 --- /dev/null +++ b/src/main/resources/mapper/RoleMapper.xml @@ -0,0 +1,70 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/SettingMapper.xml b/src/main/resources/mapper/SettingMapper.xml new file mode 100644 index 0000000..941f5ea --- /dev/null +++ b/src/main/resources/mapper/SettingMapper.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..bf5e209 --- /dev/null +++ b/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/UserRoleMapper.xml b/src/main/resources/mapper/UserRoleMapper.xml new file mode 100644 index 0000000..28ba798 --- /dev/null +++ b/src/main/resources/mapper/UserRoleMapper.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file From 28341089e1800534c2fefe5ac7cac0fdff389f6c Mon Sep 17 00:00:00 2001 From: siujamo Date: Fri, 26 Dec 2025 10:45:06 +0800 Subject: [PATCH 02/60] =?UTF-8?q?refactor:=20=E8=B0=83=E6=95=B4=20git=20?= =?UTF-8?q?=E5=BF=BD=E7=95=A5=E9=85=8D=E7=BD=AE=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20gradle-wrapper=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 35 +++++++++++++++--------------- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes 2 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 gradle/wrapper/gradle-wrapper.jar diff --git a/.gitignore b/.gitignore index 8d548fd..329367c 100644 --- a/.gitignore +++ b/.gitignore @@ -97,23 +97,6 @@ fabric.properties # Editor-based HTTP Client http-client.private.env.json -### Gradle -.gradle -**/build/ -!**/src/**/build/ - -!gradle/wrapper/ -!gradle/wrapper/gradle-wrapper.jar -!gradle/wrapper/gradle-wrapper.properties -gradle-app.setting -.gradletasknamecache - -# Eclipse Gradle plugin generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) -.classpath - ### Server # Compiled class file *.class @@ -147,3 +130,21 @@ config/*.properties # Tests test/ + +### Gradle +.gradle +**/build/ +!**/src/**/build/ + +!gradle/ +!gradle/wrapper +!gradle/wrapper/gradle-wrapper.jar +!gradle/wrapper/gradle-wrapper.properties +gradle-app.setting +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 zcma&Obx`DOvL%eWOXJW;V64viP??$)@wHcsJ68)>bJS6*&iHnskXE8MjvIPVl|FrmV}Npeql07fCw6`pw`0s zGauF(<*@v{3t!qoUU*=j)6;|-(yg@jvDx&fV^trtZt27?4Tkn729qrItVh@PMwG5$ z+oXHSPM??iHZ!cVP~gYact-CwV`}~Q+R}PPNRy+T-geK+>fHrijpllon_F4N{@b-} z1M0=a!VbVmJM8Xk@NRv)m&aRYN}FSJ{LS;}2ArQ5baSjfy40l@T5)1r-^0fAU6f_} zzScst%$Nd-^ElV~H0TetQhMc%S{}Q4lssln=|;LG?Ulo}*mhg8YvBAUY7YFdXs~vv zv~{duzVw%C#GxkBwX=TYp1Dh*Uaum2?RmsvPaLlzO^fIJ`L?&OV?Y&kKj~^kWC`Ly zfL-}J^4a0Ojuz9O{jUbIS;^JatJ5+YNNHe}6nG9Yd6P-lJiK2ms)A^xq^H2fKrTF) zp!6=`Ece~57>^9(RA4OB9;f1FAhV%zVss%#rDq$9ZW3N2cXC7dMz;|UcRFecBm`DA z1pCO!#6zKp#@mx{2>Qcme8y$Qg_gnA%(`Vtg3ccwgb~D(&@y8#Jg8nNYW*-P{_M#E zZ|wCsQoO1(iIKd-2B9xzI}?l#Q@G5d$m1Lfh0q;iS5FDQ&9_2X-H)VDKA*fa{b(sV zL--krNCXibi1+*C2;4qVjb0KWUVGjjRT{A}Q*!cFmj0tRip2ra>WYJ>ZK4C|V~RYs z6;~+*)5F^x^aQqk9tjh)L;DOLlD8j+0<>kHc8MN|68PxQV`tJFbgxSfq-}b(_h`luA0&;Vk<@51i0 z_cu6{_*=vlvYbKjDawLw+t^H?OV00_73Cn3goU5?})UYFuoSX6Xqw;TKcrsc|r# z$sMWYl@cs#SVopO$hpHZ)cdU-+Ui%z&Sa#lMI~zWW@vE%QDh@bTe0&V9nL>4Et9`N zGT8(X{l@A~loDx}BDz`m6@tLv@$mTlVJ;4MGuj!;9Y=%;;_kj#o8n5tX%@M)2I@}u z_{I!^7N1BxW9`g&Z+K#lZ@7_dXdsqp{W9_`)zgZ=sD~%WS5s$`7z#XR!Lfy(4se(m zR@a3twgMs19!-c4jh`PfpJOSU;vShBKD|I0@rmv_x|+ogqslnLLOepJpPMOxhRb*i zGHkwf#?ylQ@k9QJL?!}MY4i7joSzMcEhrDKJH&?2v{-tgCqJe+Y0njl7HYff z{&~M;JUXVR$qM1FPucIEY(IBAuCHC@^~QG6O!dAjzQBxDOR~lJEr4KS9R*idQ^p{D zS#%NQADGbAH~6wAt}(1=Uff-1O#ITe)31zCL$e9~{w)gx)g>?zFE{Bc9nJT6xR!i8 z)l)~9&~zSZTHk{?iQL^MQo$wLi}`B*qnvUy+Y*jEraZMnEhuj`Fu+>b5xD1_Tp z)8|wedv42#3AZUL7x&G@p@&zcUvPkvg=YJS6?1B7ZEXr4b>M+9Gli$gK-Sgh{O@>q7TUg+H zNJj`6q#O@>4HpPJEHvNij`sYW&u%#=215HKNg;C!0#hH1vlO5+dFq9& zS)8{5_%hz?#D#wn&nm@aB?1_|@kpA@{%jYcs{K%$a4W{k@F zPyTav?jb;F(|GaZhm6&M#g|`ckO+|mCtAU)5_(hn&Ogd z9Ku}orOMu@K^Ac>eRh3+0-y^F`j^noa*OkS3p^tLV`TY$F$cPXZJ48!xz1d7%vfA( zUx2+sDPqHfiD-_wJDb38K^LtpN2B0w=$A10z%F9f_P2aDX63w7zDG5CekVQJGy18I zB!tI`6rZr7TK10L(8bpiaQ>S@b7r_u@lh^vakd0e6USWw7W%d_Ob%M!a`K>#I3r-w zo2^+9Y)Sb?P9)x0iA#^ns+Kp{JFF|$09jb6ZS2}_<-=$?^#IUo5;g`4ICZknr!_aJ zd73%QP^e-$%Xjt|28xM}ftD|V@76V_qvNu#?Mt*A-OV{E4_zC4Ymo|(cb+w^`Wv== z>)c%_U0w`d$^`lZQp@midD89ta_qTJW~5lRrIVwjRG_9aRiQGug%f3p@;*%Y@J5uQ|#dJ+P{Omc`d2VR)DXM*=ukjVqIpkb<9gn9{*+&#p)Ek zN=4zwNWHF~=GqcLkd!q0p(S2_K=Q`$whZ}r@ec_cb9hhg9a z6CE=1n8Q;hC?;ujo0numJBSYY6)GTq^=kB~`-qE*h%*V6-ip=c4+Yqs*7C@@b4YAi zuLjsmD!5M7r7d5ZPe>4$;iv|zq=9=;B$lI|xuAJwi~j~^Wuv!Qj2iEPWjh9Z&#+G>lZQpZ@(xfBrhc{rlLwOC;optJZDj4Xfu3$u6rt_=YY0~lxoy~fq=*L_&RmD7dZWBUmY&12S;(Ui^y zBpHR0?Gk|`U&CooNm_(kkO~pK+cC%uVh^cnNn)MZjF@l{_bvn4`Jc}8QwC5_)k$zs zM2qW1Zda%bIgY^3NcfL)9ug`05r5c%8ck)J6{fluBQhVE>h+IA&Kb}~$55m-^c1S3 zJMXGlOk+01qTQUFlh5Jc3xq|7McY$nCs$5=`8Y;|il#Ypb{O9}GJZD8!kYh{TKqs@ z-mQn1K4q$yGeyMcryHQgD6Ra<6^5V(>6_qg`3uxbl|T&cJVA*M_+OC#>w(xL`RoPQ zf1ZCI3G%;o-x>RzO!mc}K!XX{1rih0$~9XeczHgHdPfL}4IPi~5EV#ZcT9 zdgkB3+NPbybS-d;{8%bZW^U+x@Ak+uw;a5JrZH!WbNvl!b~r4*vs#he^bqz`W93PkZna2oYO9dBrKh2QCWt{dGOw)%Su%1bIjtp4dKjZ^ zWfhb$M0MQiDa4)9rkip9DaH0_tv=XxNm>6MKeWv>`KNk@QVkp$Lhq_~>M6S$oliq2 zU6i7bK;TY)m>-}X7hDTie>cc$J|`*}t=MAMfWIALRh2=O{L57{#fA_9LMnrV(HrN6 zG0K_P5^#$eKt{J|#l~U0WN_3)p^LLY(XEqes0OvI?3)GTNY&S13X+9`6PLVFRf8K) z9x@c|2T72+-KOm|kZ@j4EDDec>03FdgQlJ!&FbUQQH+nU^=U3Jyrgu97&#-W4C*;_ z(WacjhBDp@&Yon<9(BWPb;Q?Kc0gR5ZH~aRNkPAWbDY!FiYVSu!~Ss^9067|JCrZk z-{Rn2KEBR|Wti_iy) zXnh2wiU5Yz2L!W{{_#LwNWXeNPHkF=jjXmHC@n*oiz zIoM~Wvo^T@@t!QQW?Ujql-GBOlnB|HjN@x~K8z)c(X}%%5Zcux09vC8=@tvgY>czq z3D(U&FiETaN9aP}FDP3ZSIXIffq>M3{~eTB{uauL07oYiM=~K(XA{SN!rJLyXeC+Y zOdeebgHOc2aCIgC=8>-Q>zfuXV*=a&gp{l#E@K|{qft@YtO>xaF>O7sZz%8);e86? z+jJlFB{0fu6%8ew^_<+v>>%6eB8|t*_v7gb{x=vLLQYJKo;p7^o9!9A1)fZZ8i#ZU z<|E?bZakjkEV8xGi?n+{Xh3EgFKdM^;4D;5fHmc04PI>6oU>>WuLy6jgpPhf8$K4M zjJo*MbN0rZbZ!5DmoC^@hbqXiP^1l7I5;Wtp2i9Jkh+KtDJoXP0O8qmN;Sp(+%upX zAxXs*qlr(ck+-QG_mMx?hQNXVV~LT{$Q$ShX+&x?Q7v z@8t|UDylH6@RZ?WsMVd3B0z5zf50BP6U<&X_}+y3uJ0c5OD}+J&2T8}A%2Hu#Nt_4 zoOoTI$A!hQ<2pk5wfZDv+7Z{yo+Etqry=$!*pvYyS+kA4xnJ~3b~TBmA8Qd){w_bE zqDaLIjnU8m$wG#&T!}{e0qmHHipA{$j`%KN{&#_Kmjd&#X-hQN+ju$5Ms$iHj4r?) z&5m8tI}L$ih&95AjQ9EDfPKSmMj-@j?Q+h~C3<|Lg2zVtfKz=ft{YaQ1i6Om&EMll zzov%MsjSg=u^%EfnO+W}@)O6u0LwoX709h3Cxdc2Rwgjd%LLTChQvHZ+y<1q6kbJXj3_pq1&MBE{8 zd;aFotyW>4WHB{JSD8Z9M@jBitC1RF;!B8;Rf-B4nOiVbGlh9w51(8WjL&e{_iXN( zAvuMDIm_>L?rJPxc>S`bqC|W$njA0MKWa?V$u6mN@PLKYqak!bR!b%c^ze(M`ec(x zv500337YCT4gO3+9>oVIJLv$pkf`01S(DUM+4u!HQob|IFHJHm#>eb#eB1X5;bMc| z>QA4Zv}$S?fWg~31?Lr(C>MKhZg>gplRm`2WZ--iw%&&YlneQYY|PXl;_4*>vkp;I z$VYTZq|B*(3(y17#@ud@o)XUZPYN*rStQg5U1Sm2gM}7hf_G<>*T%6ebK*tF(kbJc zNPH4*xMnJNgw!ff{YXrhL&V$6`ylY={qT_xg9znQWw9>PlG~IbhnpsG_94Kk_(V-o&v7#F znra%uD-}KOX2dkak**hJnZZQyp#ERyyV^lNe!Qrg=VHiyr7*%j#PMvZMuYNE8o;JM zGrnDWmGGy)(UX{rLzJ*QEBd(VwMBXnJ@>*F8eOFy|FK*Vi0tYDw;#E zu#6eS;%Nm2KY+7dHGT3m{TM7sl=z8|V0e!DzEkY-RG8vTWDdSQFE|?+&FYA146@|y zV(JP>LWL;TSL6rao@W5fWqM1-xr$gRci#RQV2DX-x4@`w{uEUgoH4G|`J%H!N?*Qn zy~rjzuf(E7E!A9R2bSF|{{U(zO+;e29K_dGmC^p7MCP!=Bzq@}&AdF5=rtCwka zTT1A?5o}i*sXCsRXBt)`?nOL$zxuP3i*rm3Gmbmr6}9HCLvL*45d|(zP;q&(v%}S5yBmRVdYQQ24zh z6qL2<2>StU$_Ft29IyF!6=!@;tW=o8vNzVy*hh}XhZhUbxa&;9~woye<_YmkUZ)S?PW{7t; zmr%({tBlRLx=ffLd60`e{PQR3NUniWN2W^~7Sy~MPJ>A#!6PLnlw7O0(`=PgA}JLZ ztqhiNcKvobCcBel2 z-N82?4-()eGOisnWcQ9Wp23|ybG?*g!2j#>m3~0__IX1o%dG4b;VF@^B+mRgKx|ij zWr5G4jiRy}5n*(qu!W`y54Y*t8g`$YrjSunUmOsqykYB4-D(*(A~?QpuFWh;)A;5= zPl|=x+-w&H9B7EZGjUMqXT}MkcSfF}bHeRFLttu!vHD{Aq)3HVhvtZY^&-lxYb2%` zDXk7>V#WzPfJs6u{?ZhXpsMdm3kZscOc<^P&e&684Rc1-d=+=VOB)NR;{?0NjTl~D z1MXak$#X4{VNJyD$b;U~Q@;zlGoPc@ny!u7Pe;N2l4;i8Q=8>R3H{>HU(z z%hV2?rSinAg6&wuv1DmXok`5@a3@H0BrqsF~L$pRYHNEXXuRIWom0l zR9hrZpn1LoYc+G@q@VsFyMDNX;>_Vf%4>6$Y@j;KSK#g)TZRmjJxB!_NmUMTY(cAV zmewn7H{z`M3^Z& z2O$pWlDuZHAQJ{xjA}B;fuojAj8WxhO}_9>qd0|p0nBXS6IIRMX|8Qa!YDD{9NYYK z%JZrk2!Ss(Ra@NRW<7U#%8SZdWMFDU@;q<}%F{|6n#Y|?FaBgV$7!@|=NSVoxlJI4G-G(rn}bh|?mKkaBF$-Yr zA;t0r?^5Nz;u6gwxURapQ0$(-su(S+24Ffmx-aP(@8d>GhMtC5x*iEXIKthE*mk$` zOj!Uri|EAb4>03C1xaC#(q_I<;t}U7;1JqISVHz3tO{) zD(Yu@=>I9FDmDtUiWt81;BeaU{_=es^#QI7>uYl@e$$lGeZ~Q(f$?^3>$<<{n`Bn$ zn8bamZlL@6r^RZHV_c5WV7m2(G6X|OI!+04eAnNA5=0v1Z3lxml2#p~Zo57ri;4>;#16sSXXEK#QlH>=b$inEH0`G#<_ zvp;{+iY)BgX$R!`HmB{S&1TrS=V;*5SB$7*&%4rf_2wQS2ed2E%Wtz@y$4ecq4w<) z-?1vz_&u>s?BMrCQG6t9;t&gvYz;@K@$k!Zi=`tgpw*v-#U1Pxy%S9%52`uf$XMv~ zU}7FR5L4F<#9i%$P=t29nX9VBVv)-y7S$ZW;gmMVBvT$BT8d}B#XV^@;wXErJ-W2A zA=JftQRL>vNO(!n4mcd3O27bHYZD!a0kI)6b4hzzL9)l-OqWn)a~{VP;=Uo|D~?AY z#8grAAASNOkFMbRDdlqVUfB;GIS-B-_YXNlT_8~a|LvRMVXf!<^uy;)d$^OR(u)!) zHHH=FqJF-*BXif9uP~`SXlt0pYx|W&7jQnCbjy|8b-i>NWb@!6bx;1L&$v&+!%9BZ z0nN-l`&}xvv|wwxmC-ZmoFT_B#BzgQZxtm|4N+|;+(YW&Jtj^g!)iqPG++Z%x0LmqnF875%Ry&2QcCamx!T@FgE@H zN39P6e#I5y6Yl&K4eUP{^biV`u9{&CiCG#U6xgGRQr)zew;Z%x+ z-gC>y%gvx|dM=OrO`N@P+h2klPtbYvjS!mNnk4yE0+I&YrSRi?F^plh}hIp_+OKd#o7ID;b;%*c0ES z!J))9D&YufGIvNVwT|qsGWiZAwFODugFQ$VsNS%gMi8OJ#i${a4!E3<-4Jj<9SdSY z&xe|D0V1c`dZv+$8>(}RE|zL{E3 z-$5Anhp#7}oO(xm#}tF+W=KE*3(xxKxhBt-uuJP}`_K#0A< zE%rhMg?=b$ot^i@BhE3&)bNBpt1V*O`g?8hhcsV-n#=|9wGCOYt8`^#T&H7{U`yt2 z{l9Xl5CVsE=`)w4A^%PbIR6uG_5Ww9k`=q<@t9Bu662;o{8PTjDBzzbY#tL;$wrpjONqZ{^Ds4oanFm~uyPm#y1Ll3(H57YDWk9TlC zq;kebC!e=`FU&q2ojmz~GeLxaJHfs0#F%c(i+~gg$#$XOHIi@1mA72g2pFEdZSvp}m0zgQb5u2?tSRp#oo!bp`FP}< zaK4iuMpH+Jg{bb7n9N6eR*NZfgL7QiLxI zk6{uKr>xxJ42sR%bJ%m8QgrL|fzo9@?9eQiMW8O`j3teoO_R8cXPe_XiLnlYkE3U4 zN!^F)Z4ZWcA8gekEPLtFqX-Q~)te`LZnJK_pgdKs)Dp50 zdUq)JjlJeELskKg^6KY!sIou-HUnSFRsqG^lsHuRs`Z{f(Ti9eyd3cwu*Kxp?Ws7l z3cN>hGPXTnQK@qBgqz(n*qdJ2wbafELi?b90fK~+#XIkFGU4+HihnWq;{{)1J zv*Txl@GlnIMOjzjA1z%g?GsB2(6Zb-8fooT*8b0KF2CdsIw}~Hir$d3TdVHRx1m3c z4C3#h@1Xi@{t4zge-#B6jo*ChO%s-R%+9%-E|y<*4;L>$766RiygaLR?X%izyqMXA zb|N=Z-0PSFeH;W6aQ3(5VZWVC>5Ibgi&cj*c%_3=o#VyUJv* zM&bjyFOzlaFq;ZW(q?|yyi|_zS%oIuH^T*MZ6NNXBj;&yM3eQ7!CqXY?`7+*+GN47 zNR#%*ZH<^x{(0@hS8l{seisY~IE*)BD+R6^OJX}<2HRzo^fC$n>#yTOAZbk4%=Bei=JEe=o$jm`or0YDw*G?d> z=i$eEL7^}_?UI^9$;1Tn9b>$KOM@NAnvWrcru)r`?LodV%lz55O3y(%FqN;cKgj7t zlJ7BmLTQ*NDX#uelGbCY>k+&H*iSK?x-{w;f5G%%!^e4QT9z<_0vHbXW^MLR} zeC*jezrU|{*_F`I0mi)9=sUj^G03i@MjXx@ePv@(Udt2CCXVOJhRh4yp~fpn>ssHZ z?k(C>2uOMWKW5FVsBo#Nk!oqYbL`?#i~#!{3w^qmCto05uS|hKkT+iPrC-}hU_nbL zO622#mJupB21nChpime}&M1+whF2XM?prT-Vv)|EjWYK(yGYwJLRRMCkx;nMSpu?0 zNwa*{0n+Yg6=SR3-S&;vq=-lRqN`s9~#)OOaIcy3GZ&~l4g@2h| zThAN#=dh{3UN7Xil;nb8@%)wx5t!l z0RSe_yJQ+_y#qEYy$B)m2yDlul^|m9V2Ia$1CKi6Q19~GTbzqk*{y4;ew=_B4V8zw zScDH&QedBl&M*-S+bH}@IZUSkUfleyM45G>CnYY{hx8J9q}ME?Iv%XK`#DJRNmAYt zk2uY?A*uyBA=nlYjkcNPMGi*552=*Q>%l?gDK_XYh*Rya_c)ve{=ps`QYE0n!n!)_$TrGi_}J|>1v}(VE7I~aP-wns#?>Y zu+O7`5kq32zM4mAQpJ50vJsUDT_^s&^k-llQMy9!@wRnxw@~kXV6{;z_wLu3i=F3m z&eVsJmuauY)8(<=pNUM5!!fQ4uA6hBkJoElL1asWNkYE#qaP?a+biwWw~vB48PRS7 zY;DSHvgbIB$)!uJU)xA!yLE*kP0owzYo`v@wfdux#~f!dv#uNc_$SF@Qq9#3q5R zfuQnPPN_(z;#X#nRHTV>TWL_Q%}5N-a=PhkQ^GL+$=QYfoDr2JO-zo#j;mCsZVUQ) zJ96e^OqdLW6b-T@CW@eQg)EgIS9*k`xr$1yDa1NWqQ|gF^2pn#dP}3NjfRYx$pTrb zwGrf8=bQAjXx*8?du*?rlH2x~^pXjiEmj^XwQo{`NMonBN=Q@Y21!H)D( zA~%|VhiTjaRQ%|#Q9d*K4j~JDXOa4wmHb0L)hn*;Eq#*GI}@#ux4}bt+olS(M4$>c z=v8x74V_5~xH$sP+LZCTrMxi)VC%(Dg!2)KvW|Wwj@pwmH6%8zd*x0rUUe$e(Z%AW z@Q{4LL9#(A-9QaY2*+q8Yq2P`pbk3!V3mJkh3uH~uN)+p?67d(r|Vo0CebgR#u}i? zBxa^w%U|7QytN%L9bKaeYhwdg7(z=AoMeP0)M3XZA)NnyqL%D_x-(jXp&tp*`%Qsx z6}=lGr;^m1<{;e=QQZ!FNxvLcvJVGPkJ63at5%*`W?46!6|5FHYV0qhizSMT>Zoe8 zsJ48kb2@=*txGRe;?~KhZgr-ZZ&c0rNV7eK+h$I-UvQ=552@psVrvj#Ys@EU4p8`3 zsNqJu-o=#@9N!Pq`}<=|((u)>^r0k^*%r<{YTMm+mOPL>EoSREuQc-e2~C#ZQ&Xve zZ}OUzmE4{N-7cqhJiUoO_V#(nHX11fdfVZJT>|6CJGX5RQ+Ng$Nq9xs-C86-)~`>p zW--X53J`O~vS{WWjsAuGq{K#8f#2iz` zzSSNIf6;?5sXrHig%X(}0q^Y=eYwvh{TWK-fT>($8Ex>!vo_oGFw#ncr{vmERi^m7lRi%8Imph})ZopLoIWt*eFWSPuBK zu>;Pu2B#+e_W|IZ0_Q9E9(s@0>C*1ft`V{*UWz^K<0Ispxi@4umgGXW!j%7n+NC~* zBDhZ~k6sS44(G}*zg||X#9Weto;u*Ty;fP!+v*7be%cYG|yEOBomch#m8Np!Sw`L)q+T` zmrTMf2^}7j=RPwgpO9@eXfb{Q>GW#{X=+xt`AwTl!=TgYm)aS2x5*`FSUaaP_I{Xi zA#irF%G33Bw>t?^1YqX%czv|JF0+@Pzi%!KJ?z!u$A`Catug*tYPO`_Zho5iip0@! z;`rR0-|Ao!YUO3yaujlSQ+j-@*{m9dHLtve!sY1Xq_T2L3&=8N;n!!Eb8P0Z^p4PL zQDdZ?An2uzbIakOpC|d@=xEA}v-srucnX3Ym{~I#Ghl~JZU(a~Ppo9Gy1oZH&Wh%y zI=KH_s!Lm%lAY&`_KGm*Ht)j*C{-t}Nn71drvS!o|I|g>ZKjE3&Mq0TCs6}W;p>%M zQ(e!h*U~b;rsZ1OPigud>ej=&hRzs@b>>sq6@Yjhnw?M26YLnDH_Wt#*7S$-BtL08 zVyIKBm$}^vp?ILpIJetMkW1VtIc&7P3z0M|{y5gA!Yi5x4}UNz5C0Wdh02!h zNS>923}vrkzl07CX`hi)nj-B?#n?BJ2Vk0zOGsF<~{Fo7OMCN_85daxhk*pO}x_8;-h>}pcw26V6CqR-=x2vRL?GB#y%tYqi;J}kvxaz}*iFO6YO0ha6!fHU9#UI2Nv z_(`F#QU1B+P;E!t#Lb)^KaQYYSewj4L!_w$RH%@IL-M($?DV@lGj%3ZgVdHe^q>n(x zyd5PDpGbvR-&p*eU9$#e5#g3-W_Z@loCSz}f~{94>k6VRG`e5lI=SE0AJ7Z_+=nnE zTuHEW)W|a8{fJS>2TaX zuRoa=LCP~kP)kx4L+OqTjtJOtXiF=y;*eUFgCn^Y@`gtyp?n14PvWF=zhNGGsM{R- z^DsGxtoDtx+g^hZi@E2Y(msb-hm{dWiHdoQvdX88EdM>^DS#f}&kCGpPFDu*KjEpv$FZtLpeT>@)mf|z#ZWEsueeW~hF78Hu zfY9a+Gp?<)s{Poh_qdcSATV2oZJo$OH~K@QzE2kCADZ@xX(; z)0i=kcAi%nvlsYagvUp(z0>3`39iKG9WBDu3z)h38p|hLGdD+Khk394PF3qkX!02H z#rNE`T~P9vwNQ_pNe0toMCRCBHuJUmNUl)KFn6Gu2je+p>{<9^oZ4Gfb!)rLZ3CR3 z-o&b;Bh>51JOt=)$-9+Z!P}c@cKev_4F1ZZGs$I(A{*PoK!6j@ZJrAt zv2LxN#p1z2_0Ox|Q8PVblp9N${kXkpsNVa^tNWhof)8x8&VxywcJz#7&P&d8vvxn` zt75mu>yV=Dl#SuiV!^1BPh5R)`}k@Nr2+s8VGp?%Le>+fa{3&(XYi~{k{ z-u4#CgYIdhp~GxLC+_wT%I*)tm4=w;ErgmAt<5i6c~)7JD2olIaK8by{u-!tZWT#RQddptXRfEZxmfpt|@bs<*uh?Y_< zD>W09Iy4iM@@80&!e^~gj!N`3lZwosC!!ydvJtc0nH==K)v#ta_I}4Tar|;TLb|+) zSF(;=?$Z0?ZFdG6>Qz)6oPM}y1&zx_Mf`A&chb znSERvt9%wdPDBIU(07X+CY74u`J{@SSgesGy~)!Mqr#yV6$=w-dO;C`JDmv=YciTH zvcrN1kVvq|(3O)NNdth>X?ftc`W2X|FGnWV%s})+uV*bw>aoJ#0|$pIqK6K0Lw!@- z3pkPbzd`ljS=H2Bt0NYe)u+%kU%DWwWa>^vKo=lzDZHr>ruL5Ky&#q7davj-_$C6J z>V8D-XJ}0cL$8}Xud{T_{19#W5y}D9HT~$&YY-@=Th219U+#nT{tu=d|B)3K`pL53 zf7`I*|L@^dPEIDJkI3_oA9vsH7n7O}JaR{G~8 zfi$?kmKvu20(l`dV7=0S43VwVKvtF!7njv1Q{Ju#ysj=|dASq&iTE8ZTbd-iiu|2& zmll%Ee1|M?n9pf~?_tdQ<7%JA53!ulo1b^h#s|Su2S4r{TH7BRB3iIOiX5|vc^;5( zKfE1+ah18YA9o1EPT(AhBtve5(%GMbspXV)|1wf5VdvzeYt8GVGt0e*3|ELBhwRaO zE|yMhl;Bm?8Ju3-;DNnxM3Roelg`^!S%e({t)jvYtJCKPqN`LmMg^V&S z$9OIFLF$%Py~{l?#ReyMzpWixvm(n(Y^Am*#>atEZ8#YD&?>NUU=zLxOdSh0m6mL? z_twklB0SjM!3+7U^>-vV=KyQZI-6<(EZiwmNBzGy;Sjc#hQk%D;bay$v#zczt%mFCHL*817X4R;E$~N5(N$1Tv{VZh7d4mhu?HgkE>O+^-C*R@ zR0ima8PsEV*WFvz`NaB+lhX3&LUZcWWJJrG7ZjQrOWD%_jxv=)`cbCk zMgelcftZ%1-p9u!I-Zf_LLz{hcn5NRbxkWby@sj2XmYfAV?iw^0?hM<$&ZDctdC`; zsL|C-7d;w$z2Gt0@hsltNlytoPnK&$>ksr(=>!7}Vk#;)Hp)LuA7(2(Hh(y3LcxRY zim!`~j6`~B+sRBv4 z<#B{@38kH;sLB4eH2+8IPWklhd25r5j2VR}YK$lpZ%7eVF5CBr#~=kUp`i zlb+>Z%i%BJH}5dmfg1>h7U5Q(-F{1d=aHDbMv9TugohX5lq#szPAvPE|HaokMQIi_ zTcTNsO53(oX=hg2w!XA&+qP}nwr$(C)pgG8emS@Mf7m0&*kiA!wPLS`88c=aD$niJ zp?3j%NI^uy|5*MzF`k4hFbsyQZ@wu!*IY+U&&9PwumdmyfL(S0#!2RFfmtzD3m9V7 zsNOw9RQofl-XBfKBF^~~{oUVouka#r3EqRf=SnleD=r1Hm@~`y8U7R)w16fgHvK-6?-TFth)f3WlklbZh+}0 zx*}7oDF4U^1tX4^$qd%987I}g;+o0*$Gsd=J>~Uae~XY6UtbdF)J8TzJXoSrqHVC) zJ@pMgE#;zmuz?N2MIC+{&)tx=7A%$yq-{GAzyz zLzZLf=%2Jqy8wGHD;>^x57VG)sDZxU+EMfe0L{@1DtxrFOp)=zKY1i%HUf~Dro#8} zUw_Mj10K7iDsX}+fThqhb@&GI7PwONx!5z;`yLmB_92z0sBd#HiqTzDvAsTdx+%W{ z2YL#U=9r!@3pNXMp_nvximh+@HV3psUaVa-lOBekVuMf1RUd26~P*|MLouQrb}XM-bEw(UgQxMI6M&l3Nha z{MBcV=tl(b_4}oFdAo}WX$~$Mj-z70FowdoB{TN|h2BdYs?$imcj{IQpEf9q z)rzpttc0?iwopSmEoB&V!1aoZqEWEeO-MKMx(4iK7&Fhc(94c zdy}SOnSCOHX+A8q@i>gB@mQ~Anv|yiUsW!bO9hb&5JqTfDit9X6xDEz*mQEiNu$ay zwqkTV%WLat|Ar+xCOfYs0UQNM`sdsnn*zJr>5T=qOU4#Z(d90!IL76DaHIZeWKyE1 zqwN%9+~lPf2d7)vN2*Q?En?DEPcM+GQwvA<#;X3v=fqsxmjYtLJpc3)A8~*g(KqFx zZEnqqruFDnEagXUM>TC7ngwKMjc2Gx%#Ll#=N4qkOuK|;>4%=0Xl7k`E69@QJ-*Vq zk9p5!+Ek#bjuPa<@Xv7ku4uiWo|_wy)6tIr`aO!)h>m5zaMS-@{HGIXJ0UilA7*I} z?|NZ!Tp8@o-lnyde*H+@8IHME8VTQOGh96&XX3E+}OB zA>VLAGW+urF&J{H{9Gj3&u+Gyn?JAVW84_XBeGs1;mm?2SQm9^!3UE@(_FiMwgkJI zZ*caE={wMm`7>9R?z3Ewg!{PdFDrbzCmz=RF<@(yQJ_A6?PCd_MdUf5vv6G#9Mf)i#G z($OxDT~8RNZ>1R-vw|nN699a}MQN4gJE_9gA-0%>a?Q<9;f3ymgoi$OI!=aE6Elw z2I`l!qe-1J$T$X&x9Zz#;3!P$I);jdOgYY1nqny-k=4|Q4F!mkqACSN`blRji>z1` zc8M57`~1lgL+Ha%@V9_G($HFBXH%k;Swyr>EsQvg%6rNi){Tr&+NAMga2;@85531V z_h+h{jdB&-l+%aY{$oy2hQfx`d{&?#psJ78iXrhrO)McOFt-o80(W^LKM{Zw93O}m z;}G!51qE?hi=Gk2VRUL2kYOBRuAzktql%_KYF4>944&lJKfbr+uo@)hklCHkC=i)E zE*%WbWr@9zoNjumq|kT<9Hm*%&ahcQ)|TCjp@uymEU!&mqqgS;d|v)QlBsE0Jw|+^ zFi9xty2hOk?rlGYT3)Q7i4k65@$RJ-d<38o<`}3KsOR}t8sAShiVWevR8z^Si4>dS z)$&ILfZ9?H#H&lumngpj7`|rKQQ`|tmMmFR+y-9PP`;-425w+#PRKKnx7o-Rw8;}*Ctyw zKh~1oJ5+0hNZ79!1fb(t7IqD8*O1I_hM;o*V~vd_LKqu7c_thyLalEF8Y3oAV=ODv z$F_m(Z>ucO(@?+g_vZ`S9+=~Msu6W-V5I-V6h7->50nQ@+TELlpl{SIfYYNvS6T6D z`9cq=at#zEZUmTfTiM3*vUamr!OB~g$#?9$&QiwDMbSaEmciWf3O2E8?oE0ApScg38hb&iN%K+kvRt#d))-tr^ zD+%!d`i!OOE3in0Q_HzNXE!JcZ<0;cu6P_@;_TIyMZ@Wv!J z)HSXAYKE%-oBk`Ye@W3ShYu-bfCAZ}1|J16hFnLy z?Bmg2_kLhlZ*?`5R8(1%Y?{O?xT)IMv{-)VWa9#1pKH|oVRm4!lLmls=u}Lxs44@g^Zwa0Z_h>Rk<(_mHN47=Id4oba zQ-=qXGz^cNX(b*=NT0<^23+hpS&#OXzzVO@$Z2)D`@oS=#(s+eQ@+FSQcpXD@9npp zlxNC&q-PFU6|!;RiM`?o&Sj&)<4xG3#ozRyQxcW4=EE;E)wcZ&zUG*5elg;{9!j}I z9slay#_bb<)N!IKO16`n3^@w=Y%duKA-{8q``*!w9SW|SRbxcNl50{k&CsV@b`5Xg zWGZ1lX)zs_M65Yt&lO%mG0^IFxzE_CL_6$rDFc&#xX5EXEKbV8E2FOAt>Ka@e0aHQ zMBf>J$FLrCGL@$VgPKSbRkkqo>sOXmU!Yx+Dp7E3SRfT`v~!mjU3qj-*!!YjgI*^) z+*05x78FVnVwSGKr^A|FW*0B|HYgc{c;e3Ld}z4rMI7hVBKaiJRL_e$rxDW^8!nGLdJ<7ex9dFoyj|EkODflJ#Xl`j&bTO%=$v)c+gJsLK_%H3}A_} z6%rfG?a7+k7Bl(HW;wQ7BwY=YFMSR3J43?!;#~E&)-RV_L!|S%XEPYl&#`s!LcF>l zn&K8eemu&CJp2hOHJKaYU#hxEutr+O161ze&=j3w12)UKS%+LAwbjqR8sDoZHnD=m0(p62!zg zxt!Sj65S?6WPmm zL&U9c`6G}T`irf=NcOiZ!V)qhnvMNOPjVkyO2^CGJ+dKTnNAPa?!AxZEpO7yL_LkB zWpolpaDfSaO-&Uv=dj7`03^BT3_HJOAjn~X;wz-}03kNs@D^()_{*BD|0mII!J>5p z1h06PTyM#3BWzAz1FPewjtrQfvecWhkRR=^gKeFDe$rmaYAo!np6iuio3>$w?az$E zwGH|zy@OgvuXok}C)o1_&N6B3P7ZX&-yimXc1hAbXr!K&vclCL%hjVF$yHpK6i_Wa z*CMg1RAH1(EuuA01@lA$sMfe*s@9- z$jNWqM;a%d3?(>Hzp*MiOUM*?8eJ$=(0fYFis!YA;0m8s^Q=M0Hx4ai3eLn%CBm14 zOb8lfI!^UAu_RkuHmKA-8gx8Z;##oCpZV{{NlNSe<i;9!MfIN!&;JI-{|n{(A19|s z9oiGesENcLf@NN^9R0uIrgg(46r%kjR{0SbnjBqPq()wDJ@LC2{kUu_j$VR=l`#RdaRe zxx;b7bu+@IntWaV$si1_nrQpo*IWGLBhhMS13qH zTy4NpK<-3aVc;M)5v(8JeksSAGQJ%6(PXGnQ-g^GQPh|xCop?zVXlFz>42%rbP@jg z)n)% zM9anq5(R=uo4tq~W7wES$g|Ko z1iNIw@-{x@xKxSXAuTx@SEcw(%E49+JJCpT(y=d+n9PO0Gv1SmHkYbcxPgDHF}4iY zkXU4rkqkwVBz<{mcv~A0K|{zpX}aJcty9s(u-$je2&=1u(e#Q~UA{gA!f;0EAaDzdQ=}x7g(9gWrWYe~ zV98=VkHbI!5Rr;+SM;*#tOgYNlfr7;nLU~MD^jSdSpn@gYOa$TQPv+e8DyJ&>aInB zDk>JmjH=}<4H4N4z&QeFx>1VPY8GU&^1c&71T*@2#dINft%ibtY(bAm%<2YwPL?J0Mt{ z7l7BR718o5=v|jB!<7PDBafdL>?cCdVmKC;)MCOobo5edt%RTWiReAMaIU5X9h`@El0sR&Z z7Ed+FiyA+QAyWn zf7=%(8XpcS*C4^-L24TBUu%0;@s!Nzy{e95qjgkzElf0#ou`sYng<}wG1M|L? zKl6ITA1X9mt6o@S(#R3B{uwJI8O$&<3{+A?T~t>Kapx6#QJDol6%?i-{b1aRu?&9B z*W@$T*o&IQ&5Kc*4LK_)MK-f&Ys^OJ9FfE?0SDbAPd(RB)Oju#S(LK)?EVandS1qb#KR;OP|86J?;TqI%E8`vszd&-kS%&~;1Als=NaLzRNnj4q=+ zu5H#z)BDKHo1EJTC?Cd_oq0qEqNAF8PwU7fK!-WwVEp4~4g z3SEmE3-$ddli))xY9KN$lxEIfyLzup@utHn=Q{OCoz9?>u%L^JjClW$M8OB`txg4r6Q-6UlVx3tR%%Z!VMb6#|BKRL`I))#g zij8#9gk|p&Iwv+4s+=XRDW7VQrI(+9>DikEq!_6vIX8$>poDjSYIPcju%=qluSS&j zI-~+ztl1f71O-B+s7Hf>AZ#}DNSf`7C7*)%(Xzf|ps6Dr7IOGSR417xsU=Rxb z1pgk9vv${17h7mZ{)*R{mc%R=!i}8EFV9pl8V=nXCZruBff`$cqN3tpB&RK^$yH!A8RL zJ5KltH$&5%xC7pLZD}6wjD2-uq3&XL8CM$@V9jqalF{mvZ)c4Vn?xXbvkB(q%xbSdjoXJXanVN@I;8I`)XlBX@6BjuQKD28Jrg05} z^ImmK-Ux*QMn_A|1ionE#AurP8Vi?x)7jG?v#YyVe_9^up@6^t_Zy^T1yKW*t* z&Z0+0Eo(==98ig=^`he&G^K$I!F~1l~gq}%o5#pR6?T+ zLmZu&_ekx%^nys<^tC@)s$kD`^r8)1^tUazRkWEYPw0P)=%cqnyeFo3nW zyV$^0DXPKn5^QiOtOi4MIX^#3wBPJjenU#2OIAgCHPKXv$OY=e;yf7+_vI7KcjKq% z?RVzC24ekYp2lEhIE^J$l&wNX0<}1Poir8PjM`m#zwk-AL0w6WvltT}*JN8WFmtP_ z6#rK7$6S!nS!}PSFTG6AF7giGJw5%A%14ECde3x95(%>&W3zUF!8x5%*h-zk8b@Bz zh`7@ixoCVCZ&$$*YUJpur90Yg0X-P82>c~NMzDy7@Ed|6(#`;{)%t7#Yb>*DBiXC3 zUFq(UDFjrgOsc%0KJ_L;WQKF0q!MINpQzSsqwv?#Wg+-NO; z84#4nk$+3C{2f#}TrRhin=Erdfs77TqBSvmxm0P?01Tn@V(}gI_ltHRzQKPyvQ2=M zX#i1-a(>FPaESNx+wZ6J{^m_q3i})1n~JG80c<%-Ky!ZdTs8cn{qWY%x%X^27-Or_ z`KjiUE$OG9K4lWS16+?aak__C*)XA{ z6HmS*8#t_3dl}4;7ZZgn4|Tyy1lOEM1~6Qgl(|BgfQF{Mfjktch zB5kc~4NeehRYO%)3Z!FFHhUVVcV@uEX$eft5Qn&V3g;}hScW_d)K_h5i)vxjKCxcf zL>XlZ^*pQNuX*RJQn)b6;blT3<7@Ap)55)aK3n-H08GIx65W zO9B%gE%`!fyT`)hKjm-&=on)l&!i-QH+mXQ&lbXg0d|F{Ac#U;6b$pqQcpqWSgAPo zmr$gOoE*0r#7J=cu1$5YZE%uylM!i3L{;GW{ae9uy)+EaV>GqW6QJ)*B2)-W`|kLL z)EeeBtpgm;79U_1;Ni5!c^0RbG8yZ0W98JiG~TC8rjFRjGc6Zi8BtoC);q1@8h7UV zFa&LRzYsq%6d!o5-yrqyjXi>jg&c8bu}{Bz9F2D(B%nnuVAz74zmBGv)PAdFXS2(A z=Z?uupM2f-ar0!A)C6l2o8a|+uT*~huH)!h3i!&$ zr>76mt|lwexD(W_+5R{e@2SwR15lGxsnEy|gbS-s5?U}l*kcfQlfnQKo5=LZXizrL zM=0ty+$#f_qGGri-*t@LfGS?%7&LigUIU#JXvwEdJZvIgPCWFBTPT`@Re5z%%tRDO zkMlJCoqf2A=hkU7Ih=IxmPF~fEL90)u76nfFRQwe{m7b&Ww$pnk~$4Lx#s9|($Cvt ze|p{Xozhb^g1MNh-PqS_dLY|Fex4|rhM#lmzq&mhebD$5P>M$eqLoV|z=VQY{)7&sR#tW zl(S1i!!Rrg7kv+V@EL51PGpm511he%MbX2-Jl+DtyYA(0gZyZQjPZP@`SAH{n&25@ zd)emg(p2T3$A!Nmzo|%=z%AhLX)W4hsZNFhmd4<1l6?b3&Fg)G(Zh%J{Cf8Q;?_++ zgO7O<(-)H|Es@QqUgcXNJEfC-BCB~#dhi6ADVZtL!)Mx|u7>ukD052z!QZ5UC-+rd zYXWNRpCmdM{&?M9OMa;OiN{Y#0+F>lBQ=W@M;OXq;-7v3niC$pM8p!agNmq7F04;| z@s-_98JJB&s`Pr6o$KZ=8}qO*7m6SMp7kVmmh$jfnG{r@O(auI7Z^jj!x}NTLS9>k zdo}&Qc2m4Ws3)5qFw#<$h=g%+QUKiYog33bE)e4*H~6tfd42q+|FT5+vmr6Y$6HGC zV!!q>B`1Ho|6E|D<2tYE;4`8WRfm2#AVBBn%_W)mi(~x@g;uyQV3_)~!#A6kmFy0p zY~#!R1%h5E{5;rehP%-#kjMLt*{g((o@0-9*8lKVu+t~CtnOxuaMgo2ssI6@kX09{ zkn~q8Gx<6T)l}7tWYS#q0&~x|-3ho@l}qIr79qOJQcm&Kfr7H54=BQto0)vd1A_*V z)8b2{xa5O^u95~TS=HcJF5b9gMV%&M6uaj<>E zPNM~qGjJ~xbg%QTy#(hPtfc46^nN=Y_GmPYY_hTL{q`W3NedZyRL^kgU@Q$_KMAjEzz*eip`3u6AhPDcWXzR=Io5EtZRPme>#K9 z4lN&87i%YYjoCKN_z9YK+{fJu{yrriba#oGM|2l$ir017UH86Eoig3x+;bz32R*;n zt)Eyg#PhQbbGr^naCv0?H<=@+Poz)Xw*3Gn00qdSL|zGiyYKOA0CP%qk=rBAlt~hr zEvd3Z4nfW%g|c`_sfK$z8fWsXTQm@@eI-FpLGrW<^PIjYw)XC-xFk+M<6>MfG;WJr zuN}7b;p^`uc0j(73^=XJcw;|D4B(`)Flm|qEbB?>qBBv2V?`mWA?Q3yRdLkK7b}y& z+!3!JBI{+&`~;%Pj#n&&y+<;IQzw5SvqlbC+V=kLZLAHOQb zS{{8E&JXy1p|B&$K!T*GKtSV^{|Uk;`oE*F;?@q1dX|>|KWb@|Dy*lbGV0Gx;gpA$ z*N16`v*gQ?6Skw(f^|SL;;^ox6jf2AQ$Zl?gvEV&H|-ep*hIS@0TmGu1X1ZmEPY&f zKCrV{UgRAiNU*=+Uw%gjIQhTAC@67m)6(_D+N>)(^gK74F%M2NUpWpho}aq|Kxh$3 zz#DWOmQV4Lg&}`XTU41Z|P~5;wN2c?2L{a=)Xi~!m#*=22c~&AW zgG#yc!_p##fI&E{xQD9l#^x|9`wSyCMxXe<3^kDIkS0N>=oAz7b`@M>aT?e$IGZR; zS;I{gnr4cS^u$#>D(sjkh^T6_$s=*o%vNLC5+6J=HA$&0v6(Y1lm|RDn&v|^CTV{= zjVrg_S}WZ|k=zzp>DX08AtfT@LhW&}!rv^);ds7|mKc5^zge_Li>FTNFoA8dbk@K$ zuuzmDQRL1leikp%m}2_`A7*7=1p2!HBlj0KjPC|WT?5{_aa%}rQ+9MqcfXI0NtjvXz1U)|H>0{6^JpHspI4MfXjV%1Tc1O!tdvd{!IpO+@ z!nh()i-J3`AXow^MP!oVLVhVW&!CDaQxlD9b|Zsc%IzsZ@d~OfMvTFXoEQg9Nj|_L zI+^=(GK9!FGck+y8!KF!nzw8ZCX>?kQr=p@7EL_^;2Mlu1e7@ixfZQ#pqpyCJ```(m;la2NpJNoLQR};i4E;hd+|QBL@GdQy(Cc zTSgZ)4O~hXj86x<7&ho5ePzDrVD`XL7{7PjjNM1|6d5>*1hFPY!E(XDMA+AS;_%E~ z(dOs)vy29&I`5_yEw0x{8Adg%wvmoW&Q;x?5`HJFB@KtmS+o0ZFkE@f)v>YYh-z&m z#>ze?@JK4oE7kFRFD%MPC@x$^p{aW}*CH9Y_(oJ~St#(2)4e-b34D>VG6giMGFA83 zpZTHM2I*c8HE}5G;?Y7RXMA2k{Y?RxHb2 zZFQv?!*Kr_q;jt3`{?B5Wf}_a7`roT&m1BN9{;5Vqo6JPh*gnN(gj}#=A$-F(SRJj zUih_ce0f%K19VLXi5(VBGOFbc(YF zLvvOJl+W<}>_6_4O?LhD>MRGlrk;~J{S#Q;Q9F^;Cu@>EgZAH=-5fp02(VND(v#7n zK-`CfxEdonk!!65?3Ry(s$=|CvNV}u$5YpUf?9kZl8h@M!AMR7RG<9#=`_@qF@})d ztJDH>=F!5I+h!4#^DN6C$pd6^)_;0Bz7|#^edb9_qFg&eI}x{Roovml5^Yf5;=ehZ zGqz-x{I`J$ejkmGTFipKrUbv-+1S_Yga=)I2ZsO16_ye@!%&Op^6;#*Bm;=I^#F;? z27Sz-pXm4x-ykSW*3`)y4$89wy6dNOP$(@VYuPfb97XPDTY2FE{Z+{6=}LLA23mAc zskjZJ05>b)I7^SfVc)LnKW(&*(kP*jBnj>jtph`ZD@&30362cnQpZW8juUWcDnghc zy|tN1T6m?R7E8iyrL%)53`ymXX~_;#r${G`4Q(&7=m7b#jN%wdLlS0lb~r9RMdSuU zJ{~>>zGA5N`^QmrzaqDJ(=9y*?@HZyE!yLFONJO!8q5Up#2v>fR6CkquE$PEcvw5q zC8FZX!15JgSn{Gqft&>A9r0e#be^C<%)psE*nyW^e>tsc8s4Q}OIm})rOhuc{3o)g1r>Q^w5mas) zDlZQyjQefhl0PmH%cK05*&v{-M1QCiK=rAP%c#pdCq_StgDW}mmw$S&K6ASE=`u4+ z5wcmtrP27nAlQCc4qazffZoFV7*l2=Va}SVJD6CgRY^=5Ul=VYLGqR7H^LHA;H^1g}ekn=4K8SPRCT+pel*@jUXnLz+AIePjz@mUsslCN2 z({jl?BWf&DS+FlE5Xwp%5zXC7{!C=k9oQLP5B;sLQxd`pg+B@qPRqZ6FU(k~QkQu{ zF~5P=kLhs+D}8qqa|CQo2=cv$wkqAzBRmz_HL9(HRBj&73T@+B{(zZahlkkJ>EQmQ zenp59dy+L;sSWYde!z_W+I~-+2Xnm;c;wI_wH=RTgxpMlCW@;Us*0}L74J#E z8XbDWJGpBscw?W$&ZxZNxUq(*DKDwNzW7_}AIw$HF6Ix|;AJ3t6lN=v(c9=?n9;Y0 zK9A0uW4Ib9|Mp-itnzS#5in=Ny+XhGO8#(1_H4%Z6yEBciBiHfn*h;^r9gWb^$UB4 zJtN8^++GfT`1!WfQt#3sXGi-p<~gIVdMM<#ZZ0e_kdPG%Q5s20NNt3Jj^t$(?5cJ$ zGZ#FT(Lt>-0fP4b5V3az4_byF12k%}Spc$WsRydi&H|9H5u1RbfPC#lq=z#a9W(r1 z!*}KST!Yhsem0tO#r!z`znSL-=NnP~f(pw-sE+Z$e7i7t9nBP^5ts1~WFmW+j+<@7 zIh@^zKO{1%Lpx^$w8-S+T_59v;%N;EZtJzcfN%&@(Ux5 z@YzX^MwbbXESD*d(&qT7-eOHD6iaH-^N>p2sVdq&(`C$;?#mgBANIc5$r| z^A$r)@c{Z}N%sbfo?T`tTHz9-YpiMW?6>kr&W9t$Cuk{q^g1<$I~L zo++o2!!$;|U93cI#p4hyc!_Mv2QKXxv419}Ej#w#%N+YIBDdnn8;35!f2QZkUG?8O zpP47Wf9rnoI^^!9!dy~XsZ&!DU4bVTAi3Fc<9$_krGR&3TI=Az9uMgYU5dd~ksx+} zP+bs9y+NgEL>c@l>H1R%@>5SWg2k&@QZL(qNUI4XwDl6(=!Q^U%o984{|0e|mR$p+ z9BcwttR#7?As?@Q{+j?K6H7R71PuiA^Dl$=f47nUKL|koCwutc_P<-m{|Al3C~o7w z=4S=}s5LcJFT1zjS)+10X_r$74`K78pz!nGGH%JV%w75!YSIt#hT7}}K>+@{{a+Im z5p#6%^X*txY?}|T17xWW*sa^?G2QHt#@tlcw0GIcy;|NR2vaCBDvn=`h)1il7E5Rx z%)mA4$`$OZx)NF5vXZnaJ1)*cA6ryx6Ll~t!LzhxvcTedxT;>JS&e=?-&DXUPaQ2~ zH*69ezE`hgV{K-|0z|m~ld}=X^-Ob={wpex&}*+Rz{gx)G}gn!C_VN{UN=>^EV=Xc zr$-HO09cW&p4^M}V3yBjTP_xrVcc8iU_^Y-JD~(bgw*@GXGB1gYKz5DWO+O`>})|N zWrC)MR93yA)3{&27-M)TJB6Ml3~?zZg#mYsF=#OSTaw&K z@hBftpt+2l@)YK@|3DvTjl(8wZtpLp9Ik!6G$CSL_idZ$Ti?R)4toe8bb)l|)lNb}?K;O2K9vyn1QG zd=v#y-Ld49UVkmfRU>Egc+(Y$^-;6vW;3Lcu*6~etz}0|@+b|+!UCal)DEYGLbHWJ zll5Wi^$Y<6@S%^y%hdjRh6&{!z1Py|lZ|q&Wub3l41uN2zEF8E&5H5?PL*&V}?*a}Lp% zCYi{ghjpRNT^^B+_U59No50Ghih5qn(W5`RkrsDWr{~A1dgtv{sRkH4RU2^A{jb&0 zxVRnrm|u<;$iI;M6A>$POP)TWGU-gSjAERk*EGmVT(aw$!XUSe~7Ql-oRA54^4V(JWS6Q1mG?!vZ zx+pE!FEtvqr|Xrcb3oR`%LHFLmU_&{=p%mGy6MRe2Yz_5WJ8p@IgU2 zdVvvhhQtiQkChK%*&PsiPCBL9oDOoJX8!$S(V>R}+1M}wzK*U*A{KJ`r=lM;mPrKU zQDqqN(W*u-5-?$(SIk<6A0E}34y&@-IVC%S!a1F4kz<3bIKjlyD)ooO_7ftl%S_(6w`!vX&1PZ!K`@D@L6JR)6zO@Dl!YF{RY}d3HZ7?Q5E>w=$ ze)H_)48Ds*Ov4?zoGb2fe3}{!5Ooc|KCIni1o)(Gj+CO?`*7jsV`hIv@8J(22o4Q? zu?Bvi)zDG(me?7XKeL|iF9ZRgZdT*}Ffsl62Cu;{Gv9j6dO zPt*H2GqC)-C`V`ceuu=tM{7!2yTEj=*5+T~5DYiZ)Hy)*PARYI6R2lZXoOj;v8M4W z*O-NX(7_~Q&A3>Oaw&1lBH_H%SwmISX-i3)HfHvBOeVwTT{LUM3}ZuZmg<(>)KE;d zbs2!0v6>J;1nQ0UJkUxnkE@Ibi~Q}M=-=Rk;hcOnxO$luOKEVxZc|!XECgex(2`}T z3Y;Q_6rL)e+SrOZhQj5_e}Lv>w7n*Pep$yWZNQl>ubBgb_NIWWDn3kNpn+MPQXV;8 zV|_Ba5jsQ(w&Ey^IM|@|y!AqcJ#3m0#Q6_qvgCG~eoF#mnGmbO(;DP+bW%_aOs1R_ z@9p#7X2UA^--#Nwx_Hvk2l1`eO{P*#j@q2UELtH|Uh6hxR`h_847wIJo0=5CQQ`6it|%a-I$^&a@we1rc&*;QIu5Ck^?) zx*5eSd*mG#=6Hi(5!;5uUi&{HfnT1S8X-)?gE5CZ6KWoqM5|CyrULmuFBKOU8SOp* z{IB1$OCcq`S-k*xs;4fmhKsIGZ;GYAY*%(@875NxhMq|j*m4CNLI(Vho|N|F);!E0cS5y^$H^Izje?z}oTgyr`9x9G&rlJZw&uqIoBMtz zzhU0(9;w02?m#0!)cFi*r+8YvooQ;(s2lLVvyLqAE%Xqe!vtWbIs!l1Bpp(FIht-Z zPn#CN-2C|J*GhA2fuHqYQ2mJiXlGTzD}mkr2;ia8Wp}h^;OS7+N^Mw|en!1${vN6 z-x{8N*4UekA~`IV2&K-GzhAqau|}d*pEQ$1MH$cFi03OG^1NetZ_jW^STaEzr&Xho zB452St%v3ez2#TFm~`gZh$vi=in+y2d!z<{OZ~Kty-5bQ;0O=k_ESi8Nx9{*T`LJy6jqR>&|+>OZ;+=0hA04 zE25t^sE9HG)3^KKR_A5WDkqispweP9!I-@dCO&N!JrD@i{WBHnfQ z95o8;d$`AFnca3;N-0iX-CmbbAp5yQ!GoH;h7Cn?m{ammZJI8igP{U73lFnl2&gCs zqJ4(Vo~^j`{zOAzScL5B_Sm?Mjtek1d(A6X5ObcZi$;aOYy|g$}BY z$GEP3#i60Ju_&3SHzryH!gUFwC9-295u??cf+aYRQ1$+!rc#42YNattd6mZEFI@?C zqFM>6+zxEunIHDZ>{Z15u##>N(28Dw!>G(k*dB{NHvip@aP}f`@=Q;!o;zRMWo{Cx zo?kyzh8n7#f1g0&g>Cd>O-2g?uPwy8sy8hZbHSsXPmU;@l=HL=zm7mN(=@*|D$i+u zs~TllkCTvD$f&-#b9B?}#Lg*-ibK13R_a$RyoN3m5`10tdhAq{+VW)K#Bht-ra1*J z+n$N%V>u0rVtx`aKJDwXXrxaD7nS<>$=c82v7@KVx^S@vT;h=SZE37K>iahpx3;VDzEr9GY=2(%uaqM;^76eSP0QLzo4sI z>p_Eei*T$K;|qK`sq;?Hesp}(@VvX2Q4sAMYAJ}b&d$htDMC{FG-$o4k9ApECi1$a zXdamjiOGKHBh(4M<3(2x6n-CrmZMCknkQxdSS!qlis#I}btfX;J`JU3RlvtLdrymP zG0ZzrsGXVFiq+Wk1=BFay&9ZiCE#(`h~CL+c-Hs@iGTU@YxM%vlg;)`Tf~IknA^02 zXkN#Txo6aR{j$wP5T#|UH#5AP2{rSY8p?jKFv zG3kn3y`FaV!*Jq%m39_TQEhD>M@l*bhEPGe1{ft3q#K5AknT=F2_=T^l#ou5ln@D# z5Tzs(kRG@qNDa~HLNvfv7Z0g=bSlb?`QAx|Gfoni|iHJ%K0cy z;~Nsaa+{8HP_qrb{nj+xzkdYhSI@W4N_1`z(eSGIkbDP)!Ko|M%}Rqp(~KI2hl~eE zvJ!j4m6iwMgKy>fkCLC)`M$z9EV}B+sq1}}kVf$(ig0pWTY?rHz1Sm=4srTGNb^JG z=2$9wz-C@aZZZ2!HY#HNejqZRmE=pN(D$Kui$NpfhU`!y_s{@MIxiJdHb1|{6xb`> zE74_@QtgtG{4=3P1$^vn&m}7Aw8!1DnT$2thO#~44wl(N#ao8S0@t@m+Z!KD2CfK; z)n5DAPKV_etmH1aLDK$?`;sL91iVt$D z*SG}=-LIAg(*+JON!-5ivqOMQ1S!OQUgHglDsKik&Mwg;vva523`JwQH6SRz9eTY# zTIi23145~kc3r1mSWC_RzD%hs$S#!pkI9!BU80jJCJcwo*FZolQG$q`8C1d9pP@ND zG^&-ZraIvhg_FDVSfKGwkcI=avIan%2sK4coUs~Nr8jC*&!G0#?}_^s3r-c}-uAqi zM-Lw>Y}I``T;IS%Y|qH;s{F*ZefM!4{I5awr!K+T@uPd*Vu*iPWI}>(-D{zxsN>LG z=@747a_Rb2>q?y8xYf?dq2HM5tFO8Y5e4N;Y=xy8yAhI zsm>oy%R5;7)7T3V_b2%`aH^tNlsQpFxIFW#iV#8?{6{^cGr{A0@1bA)|K z>MMTuZD(pd2t|7vmHtywGXb%%=)S<`OG~}U+jm#xd%H8 z$v8-C%F?ah3$;hn?{G3(LT!SgvCVi$vwsZssAQvUwT`Q%qSw!LSd!(I!64w1=%Sc1Mck)q1@pZ@)=SY zoX}d+L3-RA|c?G3_BQNm&( z!i$AZ7cI(z7q|e9VM##6T3Xorj1JG(9os$;(I$y%mBy(#8{|3l4|x*oBAQL^XhZ0g zy1FR1teRrpKq{uLAibTLx#n({qwjlkOvR{OdSAeT5ah4-sNN)n4Clg1T9lzF)&yj; zyal1%+s4n1IG;^VPWJ;#olpk8Z42Gj-tjFeQ&PlxB)`oCNoUYKj4U$AeG8rYiD{pK zndDf&2;2;)D|KvOZP+e7fcPU9k4M2sfhr@vC~Ly0?S-4dz)ZGAYpCsAhChgbxLd4g zhTrbIPkO5SEp_kD>Ha0m12h5n3s;mE8kn515&nzSf+^D= zyE{JnJ;43l&BH55CL<=W%CF;6iUI)V5C*6!`**KqvzR2=Fj*3Y4`HYwx}TYD445(K z-QtXwtL?m*(F=LVH*H4oM>dXHBW=38q_dZ-_Vr&qpEPxd9Fs95P5W~@Z|Rt+WZP6l zPSQ}~Dh4V?Pp1g&Hk*Px?lm16C@X6M29Vrk%Rw@E||E-v~$ zb_E~{z<}#8i`Mx9mkqtd#Z1lZ-E_J8I+2oumc#x1)jdvh{W76NKm6x-RYpM~v!P8$ zw3e|YVf|}Hse9~oC@N7^j}Fi$hNpyaYnu1}bdXsD=^oI*%WKvbme|BI}$G3>smu#6y)ls|j? zF7Bhu9Z)j)C;3cZb+I>0stSK^WLOYV^U{pUYkgv>?+Nt^5j*CUB=eGw-CvU&40>y~ zGoHLXxY^7k5Xgv62{iQy|5jJQuq0|LU`}lE@flQ2Z*Zn*VWcQjm4FTb>LSVox^S4q zLn`LfS@mrjKCmg$nb^af?d?0&$aX6#2u(JyzIJvuJ*lwPrh|0~aEnSACCTezSdG%h zmSQg`17j@$Iq)r1&?+eR@1nlX|H`<}_!?BQSF&N+QQnvEAqZe+mIFui!0V49R?|9*$ zv!K1A01{8xq;L()Tv*Qk0-$Oj6+vCT*TUD{HvxO@3JjxBwM!4g3ydy&eaJw4CoQBF zJtULJ!YxgNR7_Ls%LmogyI7uIs=!B&?=MYY^yX+v;j@D_xGeZg>eZk0C;4e|HRNSi z6KlD9>q=3v-$4Zik&^ZDhNm1X)+7LCH1k!s+T3tn zUn@={1U&NJLq@K?~w|(=Y<4W{ucX}FdRr6pLw(l2$iK)At%t3gYBMlJz#(K0Nqm;=KAML!&MMSNz=%k=j*zh77r34Rs37iCY` z=_kva_41bdrj(b=4Wc5MO0~q^z#pIWJ>)vDSgIQF=3JVJe1iDy%h)8oNy{s_r&;m` zL{DYKSB_5xRb9xKNOS{qAY3qv5sSXVrrf%~*q5HO|CQ&lbKMePa$M5D{vlJcoGrCZ zD?fKbZN$6rWwz)w7`9h4DAmh1ij2}EO|bO#A9L0_RW6l*$sPPUJrUbhLC75L9%W5iO$Iw5~Yut-qBeu~hF|xD7-eQ%l z412vpq_;t%^F*pYDk%Q35c-erK|6Ve=FxQbAv~ikZ4c9$Y4;ee#ciOD9{yRqf55Qk zumv}#+JciT|Gj$uFOxBUze)=?l{B}qaC0_7m`t82<$K53!4Xvi9Tr)ADp3Off?O8o zVDG0Yx|tfn@r((m?Nxrh(b0DGjg)$;DfO&$6uY;4&F!4jnxkhP}Y3x zS?WFFt>=HWzqlQhffVfvM$Ta8Sg*r3j!Eo&rUOW7SCL2~lG7<+XZ;+{&8h5g8ElI+P>>yR2U%S93NN!Xhm|C682t6ysH-=o1=Bd*N*VlnG%l+KZFtjG`UkL;%65qn0UYQ`h zh0{9jDQx(`aBe7J0Aj3Z)4}`A|4OMM0a;?{j}qkYwi)~O8$9D}ITiMH2buiU>ixYp zhL${nwj6X($*OwmpVG`y5b6v45tX*J8?og}Qju6eJ9H}`X87iEd%BUo7<`2q(HJx+ zMR}d-J4oAf{V1W^a2~`M-YAdZ81dd4o6NPO{cmZaAS@RS4ir#Sr zfFZO-VIL|VN<%nEXr2` z$0FK2L#8O_f1w~c@G70JrB@N}r(gJ!Vmkk6{r68w!o$qO?HrFcjeU0_3F5;*!E2%( zTx>4?gP8w z1B?3UVZmz^%d_dIps>>0{cB~mp3{9UoPR6uQFecVq&} zY{ebB?AlPAD_}(ll{fK99;Wh1cgRbnw)maD^F>*J!R}eHM*W0VYN1TADWMy9H=$00 z5bHY${oDgwX7(W9LZw?}{!8(_{JB~Xkje6{0x4fgC4kUmpfJ+LT1DYD*TWu4#h{Y7 zFLronmc=hS=W=j1ar3r1JNjQoWo2hMWsqW*e?TF%#&{GpsaLp}iN~$)ar+7Ti}E&X z-nq~+Gkp(`qF0F_4A22>VZn-x>I$?PDZSeG8h_ifoWf^DxIb5%T7UytYo3}F|4#RC zUHpg$=)qVqD~=m(!~?XwocuxU1u}9qhhM7d^eqmJPi_e-!IO`*{u7A zbu*?L$Mbj-X9n3G2>+Kc#l`@d8}Xb9{l*IN{#M*d;s+3Pdr8FO$EBELR=8{ zd?LJbSv9fI`{OqTH)5{b?WulgMb)psp+W|@cSp=jtl-&5C}9lw@*0H+gEW(}mAWNz zf{~U;;N}|wdSaphgqnH{FWUy!{y3^=AC*c?RJ5Eb<^ zCgH_v7^axIUVmHSFL^zlj2R$zow$|y#7>%#U7d#Vp_ezcp3lefMyd5ES=q$>4pWyA zp_Zso^^NP~lu2=S6nD(3Z5u=Uy&B&F1i$J*3;3KhEkD_lgscHGR*;T;U!9vgQa(hI}oh9IzEf_PU_8F+i77t-~gDX z490Sb)LyVZmf18N6w{+37$aO<2!Av0 ztLaPOv^J<2@p{WnMiDudoghX_`luFZt_4eNU}*~cF5i%eEcNLs;D>QVIwr8mH;=dc z09`}JV;aaF;13@&iS(w>Jc=k~|d_1hcpM(l|O zu>!@}me%isTT$xT#hNUvh(ATd0wT4fbv=6htcHNEZIw9%E6wlYmwfu2{j0kh1y=$;Yf!|NldgB9ul zB{dbE&LfRnr8ITm@;-68wo#VV?8lG3ed&9k1}QBS3}WGV9%26?A1rBkkDR9Z3o+g+ z)eQg8BY3y(Dh5&z?VLLNdDV`C=muUvCPpGg!oYxIgOI3^%4>5d7jTh~ni!Fg2;fhx z(*c%H6Je84kmQh;5tC3*l~7khLxK-e|Cz?FLh!yYe7g|*LwqU?2wv^_ZyKT$fYVkGJo@AK0$+ml?}zJeB~deT2WL1vz}dxB z)y??t!}%M@)u$_IyW~)6u1SttJ!awd6N5lx|xBrmyrBh>tb&D*=C+Z3nPfq$1%WgY0bY*?PZ#Hk|=xn zGM#0*w4CaB^y0G(J4q=;5NeM@m-P}#mv7QZNF)M!dK^w{mk_!n0`+Y3PQutu-%NBt zzgPXug?JLEbUL{e_dk;Vd896&yPe(hliVK!lj%5+@BKdcrEZ2Nc_*i@ve*2lB>u~{ zFozd2FM|_0+nAGR4TLNHanQn_Oeb!JrUcvzJ?7p9TTNB}ocO3j$7ij!li8#k6 z@2tSd1>K03K9A#_-MIq)S;T#oE^;>U$)&}okIvDf3lm?kI{d80$>~xKUoS!%q1Pi?WpsUUt(tI ztjNjY*y&Rm9(S(DC2GuPHBJs@5M{RGm`c1z<6nwyN^)rMo-AS{M2$oM9|y%fM|}G~ DHx0+F literal 0 HcmV?d00001 From 826f51926bf6c5cb76368f080dd288b8aed35c98 Mon Sep 17 00:00:00 2001 From: siujamo Date: Fri, 26 Dec 2025 14:59:10 +0800 Subject: [PATCH 03/60] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helix/controller/RoleController.java | 8 +++++++ .../onixbyte/helix/domain/entity/Role.java | 13 ++++++++++++ .../domain/web/request/AddRoleRequest.java | 21 +++++++++++++++++++ .../onixbyte/helix/manager/RoleManager.java | 4 ++++ .../onixbyte/helix/service/RoleService.java | 20 ++++++++++++++++++ 5 files changed, 66 insertions(+) create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/AddRoleRequest.java diff --git a/src/main/java/com/onixbyte/helix/controller/RoleController.java b/src/main/java/com/onixbyte/helix/controller/RoleController.java index 8f25535..7d16f6a 100644 --- a/src/main/java/com/onixbyte/helix/controller/RoleController.java +++ b/src/main/java/com/onixbyte/helix/controller/RoleController.java @@ -1,11 +1,13 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Role; +import com.onixbyte.helix.domain.web.request.AddRoleRequest; import com.onixbyte.helix.domain.web.request.QueryRoleRequest; import com.onixbyte.helix.service.RoleService; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -28,4 +30,10 @@ public class RoleController { var pageRequest = PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Order.asc("id"))); return roleService.getRoles(pageRequest, request); } + + @PostMapping + public ResponseEntity addRole(@Validated @RequestBody AddRoleRequest request) { + roleService.addRole(request); + return ResponseEntity.ok(null); + } } diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Role.java b/src/main/java/com/onixbyte/helix/domain/entity/Role.java index d000fba..5633d13 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Role.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Role.java @@ -305,4 +305,17 @@ public class Role { return new Role(id, name, code, sort, defaultValue, description, status, createdAt, updatedAt); } } + + @PrePersist + private void onInsert() { + var currentTime = LocalDateTime.now(); + this.createdAt = currentTime; + this.updatedAt = currentTime; + } + + @PreUpdate + private void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } + } diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AddRoleRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/AddRoleRequest.java new file mode 100644 index 0000000..1127ba2 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/AddRoleRequest.java @@ -0,0 +1,21 @@ +package com.onixbyte.helix.domain.web.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +public record AddRoleRequest( + @NotBlank(message = "角色名称不能为空") + String name, + @NotBlank(message = "角色编码不能为空") + String code, + @NotNull(message = "排序编号不能为空") + Integer sort, + Boolean defaultValue, + String description, + @Pattern( + regexp = "^(ACTIVE|INACTIVE)?$", + message = "状态仅可以是 ACTIVE、INACTIVE 其中之一") + String status +) { +} diff --git a/src/main/java/com/onixbyte/helix/manager/RoleManager.java b/src/main/java/com/onixbyte/helix/manager/RoleManager.java index 3bc32e1..0046f9f 100644 --- a/src/main/java/com/onixbyte/helix/manager/RoleManager.java +++ b/src/main/java/com/onixbyte/helix/manager/RoleManager.java @@ -42,4 +42,8 @@ public class RoleManager { return new PageImpl<>(records, pageable, total); } + + public Role save(Role role) { + return roleRepository.save(role); + } } diff --git a/src/main/java/com/onixbyte/helix/service/RoleService.java b/src/main/java/com/onixbyte/helix/service/RoleService.java index 15a18c6..3cef3b8 100644 --- a/src/main/java/com/onixbyte/helix/service/RoleService.java +++ b/src/main/java/com/onixbyte/helix/service/RoleService.java @@ -3,6 +3,7 @@ package com.onixbyte.helix.service; import com.onixbyte.helix.constant.Status; import com.onixbyte.helix.domain.database.query.wrapper.QueryRoleWrapper; import com.onixbyte.helix.domain.entity.Role; +import com.onixbyte.helix.domain.web.request.AddRoleRequest; import com.onixbyte.helix.domain.web.request.QueryRoleRequest; import com.onixbyte.helix.manager.RoleManager; import org.apache.commons.lang3.StringUtils; @@ -39,4 +40,23 @@ public class RoleService { return roleManager.selectAll(pageable, wrapper); } + + public Role addRole(AddRoleRequest request) { + var isDefaultRole = Optional.ofNullable(request.defaultValue()) + .orElse(false); + var status = Optional.ofNullable(request.status()) + .map(Status::valueOf) + .orElse(Status.ACTIVE); + + var role = Role.builder() + .name(request.name()) + .code(request.code()) + .sort(request.sort()) + .defaultValue(isDefaultRole) + .description(request.description()) + .status(status) + .build(); + + return roleManager.save(role); + } } From 3f1c320a48e56e9c2dcf10da1a4580237acc870a Mon Sep 17 00:00:00 2001 From: siujamo Date: Fri, 26 Dec 2025 17:48:52 +0800 Subject: [PATCH 04/60] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helix/controller/RoleController.java | 7 ++++ .../domain/web/request/EditRoleRequest.java | 20 +++++++++++ .../onixbyte/helix/manager/RoleManager.java | 35 +++++++++++++++---- .../onixbyte/helix/service/RoleService.java | 17 +++++++++ .../onixbyte/helix/service/UserService.java | 7 ++-- 5 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java diff --git a/src/main/java/com/onixbyte/helix/controller/RoleController.java b/src/main/java/com/onixbyte/helix/controller/RoleController.java index 7d16f6a..79a7505 100644 --- a/src/main/java/com/onixbyte/helix/controller/RoleController.java +++ b/src/main/java/com/onixbyte/helix/controller/RoleController.java @@ -2,6 +2,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Role; import com.onixbyte.helix.domain.web.request.AddRoleRequest; +import com.onixbyte.helix.domain.web.request.EditRoleRequest; import com.onixbyte.helix.domain.web.request.QueryRoleRequest; import com.onixbyte.helix.service.RoleService; import org.springframework.data.domain.Page; @@ -36,4 +37,10 @@ public class RoleController { roleService.addRole(request); return ResponseEntity.ok(null); } + + @PutMapping + public ResponseEntity editRole(@Validated @RequestBody EditRoleRequest request) { + roleService.editRole(request); + return ResponseEntity.ok(null); + } } diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java new file mode 100644 index 0000000..9392181 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java @@ -0,0 +1,20 @@ +package com.onixbyte.helix.domain.web.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +public record EditRoleRequest( + @NotNull(message = "角色 ID 不能为空") + Long id, + String name, + String code, + Integer sort, + Boolean defaultValue, + String description, + @Pattern( + regexp = "^(ACTIVE|INACTIVE)?$", + message = "状态仅可以是 ACTIVE、INACTIVE 其中之一") + String status +) { +} diff --git a/src/main/java/com/onixbyte/helix/manager/RoleManager.java b/src/main/java/com/onixbyte/helix/manager/RoleManager.java index 0046f9f..bd7f735 100644 --- a/src/main/java/com/onixbyte/helix/manager/RoleManager.java +++ b/src/main/java/com/onixbyte/helix/manager/RoleManager.java @@ -1,16 +1,15 @@ package com.onixbyte.helix.manager; +import com.onixbyte.helix.constant.Status; import com.onixbyte.helix.domain.database.query.wrapper.QueryRoleWrapper; import com.onixbyte.helix.domain.entity.Role; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.mapper.RoleMapper; import com.onixbyte.helix.repository.RoleRepository; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.*; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @@ -32,8 +31,8 @@ public class RoleManager { } } - public Optional getRole(Role example) { - return roleRepository.findOne(Example.of(example)); + public List getRoles(Role example) { + return roleRepository.findAll(Example.of(example), Sort.by(Sort.Order.asc("id"))); } public Page selectAll(Pageable pageable, QueryRoleWrapper wrapper) { @@ -46,4 +45,28 @@ public class RoleManager { public Role save(Role role) { return roleRepository.save(role); } + + public Optional getRoleById(Long id) { + return roleRepository.findById(id); + } + + @Transactional + public void updateRole(Role role) { + var roleToUpdate = roleRepository.findById(role.getId()) + .orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, "找不到指定的角色信息。")); + + Optional.ofNullable(role.getName()) + .ifPresent(roleToUpdate::setName); + + Optional.ofNullable(role.getCode()) + .ifPresent(roleToUpdate::setCode); + + Optional.ofNullable(role.getSort()) + .ifPresent(roleToUpdate::setSort); + + roleToUpdate.setDescription(role.getDescription()); + + Optional.ofNullable(role.getStatus()) + .ifPresent(roleToUpdate::setStatus); + } } diff --git a/src/main/java/com/onixbyte/helix/service/RoleService.java b/src/main/java/com/onixbyte/helix/service/RoleService.java index 3cef3b8..615db80 100644 --- a/src/main/java/com/onixbyte/helix/service/RoleService.java +++ b/src/main/java/com/onixbyte/helix/service/RoleService.java @@ -4,8 +4,10 @@ import com.onixbyte.helix.constant.Status; import com.onixbyte.helix.domain.database.query.wrapper.QueryRoleWrapper; import com.onixbyte.helix.domain.entity.Role; import com.onixbyte.helix.domain.web.request.AddRoleRequest; +import com.onixbyte.helix.domain.web.request.EditRoleRequest; import com.onixbyte.helix.domain.web.request.QueryRoleRequest; import com.onixbyte.helix.manager.RoleManager; +import jakarta.transaction.Transactional; import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -59,4 +61,19 @@ public class RoleService { return roleManager.save(role); } + + @Transactional + public void editRole(EditRoleRequest request) { + roleManager.updateRole(Role.builder() + .id(request.id()) + .name(request.name()) + .code(request.code()) + .sort(request.sort()) + .defaultValue(request.defaultValue()) + .description(request.description()) + .status(Optional.ofNullable(request.status()) + .map(Status::valueOf) + .orElse(null)) + .build()); + } } diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index a33343a..bd3a1f0 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -11,7 +11,6 @@ import com.onixbyte.helix.domain.web.request.QueryUserRequest; import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; import com.onixbyte.helix.domain.web.request.UpdateUserRequest; import com.onixbyte.helix.domain.web.response.UserDetailResponse; -import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.ApplicationManager; import com.onixbyte.helix.manager.RoleManager; import com.onixbyte.helix.manager.UserManager; @@ -25,7 +24,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.List; import java.util.Optional; @Service @@ -111,11 +109,12 @@ public class UserService { // Get role IDs var roleIds = Optional.ofNullable(request.roleIds()) .filter(CollectionUtils::isNotEmpty) - .orElseGet(() -> List.of(roleManager.getRole(Role.builder() + .orElseGet(() -> roleManager.getRoles(Role.builder() .defaultValue(true) .build()) + .stream() .map(Role::getId) - .orElseThrow(() -> new BizException("No default role specified.")))); + .toList()); // Build bindings var userRoleBindings = roleIds From aa19c9146529cc71341bc7c3ea4934314e76d526 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Sat, 27 Dec 2025 11:36:28 +0800 Subject: [PATCH 05/60] =?UTF-8?q?refactor:=20=E8=A1=A5=E5=85=85=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onixbyte/helix/client/TokenClient.java | 2 + .../helix/controller/AssetController.java | 2 + .../helix/controller/AuthController.java | 2 + .../helix/controller/CaptchaController.java | 2 + .../controller/DepartmentController.java | 2 + .../helix/controller/ExceptionController.java | 1 - .../helix/controller/MenuController.java | 2 + .../helix/controller/PositionController.java | 2 + .../helix/controller/RoleController.java | 2 + .../helix/controller/UserController.java | 3 +- .../helix/manager/ApplicationManager.java | 2 + .../onixbyte/helix/manager/AssetManager.java | 4 +- .../helix/manager/AuthorityManager.java | 2 + .../helix/manager/DepartmentManager.java | 2 + .../onixbyte/helix/manager/MenuManager.java | 2 + .../helix/manager/PositionManager.java | 3 +- .../onixbyte/helix/manager/RoleManager.java | 3 +- .../helix/manager/SettingManager.java | 2 + .../onixbyte/helix/manager/UserManager.java | 8 ++- .../helix/manager/UserRoleManager.java | 4 +- .../onixbyte/helix/manager/package-info.java | 60 ------------------- ...sernamePasswordAuthenticationProvider.java | 8 ++- .../onixbyte/helix/service/AssetService.java | 2 + .../helix/service/CaptchaService.java | 10 +--- .../helix/service/DepartmentService.java | 3 +- .../onixbyte/helix/service/MenuService.java | 2 + .../helix/service/PositionService.java | 4 +- .../onixbyte/helix/service/RoleService.java | 2 + .../onixbyte/helix/service/UserService.java | 2 + .../onixbyte/helix/utils/DateTimeUtil.java | 11 ++++ .../onixbyte/helix/utils/SecurityUtil.java | 14 +++++ .../com/onixbyte/helix/utils/TreeUtil.java | 10 +++- 32 files changed, 99 insertions(+), 81 deletions(-) delete mode 100644 src/main/java/com/onixbyte/helix/manager/package-info.java diff --git a/src/main/java/com/onixbyte/helix/client/TokenClient.java b/src/main/java/com/onixbyte/helix/client/TokenClient.java index b6af92f..3c11f4b 100644 --- a/src/main/java/com/onixbyte/helix/client/TokenClient.java +++ b/src/main/java/com/onixbyte/helix/client/TokenClient.java @@ -5,6 +5,7 @@ import com.auth0.jwt.algorithms.Algorithm; import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.properties.TokenProperties; import com.onixbyte.helix.utils.DateTimeUtil; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @@ -31,6 +32,7 @@ public class TokenClient { * @param tokenProperties the configuration properties for the token, such as issuer and * validity period */ + @Autowired public TokenClient(Algorithm algorithm, TokenProperties tokenProperties) { this.algorithm = algorithm; this.tokenProperties = tokenProperties; diff --git a/src/main/java/com/onixbyte/helix/controller/AssetController.java b/src/main/java/com/onixbyte/helix/controller/AssetController.java index be1f185..c545530 100644 --- a/src/main/java/com/onixbyte/helix/controller/AssetController.java +++ b/src/main/java/com/onixbyte/helix/controller/AssetController.java @@ -6,6 +6,7 @@ import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.service.AssetService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -31,6 +32,7 @@ public class AssetController { * * @param assetService the file service to use for file operations */ + @Autowired public AssetController(AssetService assetService) { this.assetService = assetService; } diff --git a/src/main/java/com/onixbyte/helix/controller/AuthController.java b/src/main/java/com/onixbyte/helix/controller/AuthController.java index dbb49b6..defeb02 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthController.java @@ -5,6 +5,7 @@ import com.onixbyte.helix.domain.web.response.LoginSuccessResponse; import com.onixbyte.helix.service.AuthService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -15,6 +16,7 @@ public class AuthController { private static final Logger log = LoggerFactory.getLogger(AuthController.class); private final AuthService authService; + @Autowired public AuthController(AuthService authService) { this.authService = authService; } diff --git a/src/main/java/com/onixbyte/helix/controller/CaptchaController.java b/src/main/java/com/onixbyte/helix/controller/CaptchaController.java index 8afd721..a06808c 100644 --- a/src/main/java/com/onixbyte/helix/controller/CaptchaController.java +++ b/src/main/java/com/onixbyte/helix/controller/CaptchaController.java @@ -2,6 +2,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.web.response.CaptchaResponse; import com.onixbyte.helix.service.CaptchaService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -15,6 +16,7 @@ public class CaptchaController { private final CaptchaService captchaService; + @Autowired public CaptchaController(CaptchaService captchaService) { this.captchaService = captchaService; } diff --git a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java index 09565cd..338a06c 100644 --- a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java +++ b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java @@ -3,6 +3,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Department; import com.onixbyte.helix.domain.model.TreeNode; import com.onixbyte.helix.service.DepartmentService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -15,6 +16,7 @@ public class DepartmentController { private final DepartmentService departmentService; + @Autowired public DepartmentController(DepartmentService departmentService) { this.departmentService = departmentService; } diff --git a/src/main/java/com/onixbyte/helix/controller/ExceptionController.java b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java index e47df94..9a9b789 100644 --- a/src/main/java/com/onixbyte/helix/controller/ExceptionController.java +++ b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java @@ -37,7 +37,6 @@ import java.util.stream.Collectors; @RestControllerAdvice public class ExceptionController { - /** * Handles business logic exceptions thrown throughout the application. *

diff --git a/src/main/java/com/onixbyte/helix/controller/MenuController.java b/src/main/java/com/onixbyte/helix/controller/MenuController.java index e7b218f..26e0aa6 100644 --- a/src/main/java/com/onixbyte/helix/controller/MenuController.java +++ b/src/main/java/com/onixbyte/helix/controller/MenuController.java @@ -3,6 +3,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Menu; import com.onixbyte.helix.domain.model.TreeNode; import com.onixbyte.helix.service.MenuService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -15,6 +16,7 @@ public class MenuController { private final MenuService menuService; + @Autowired public MenuController(MenuService menuService) { this.menuService = menuService; } diff --git a/src/main/java/com/onixbyte/helix/controller/PositionController.java b/src/main/java/com/onixbyte/helix/controller/PositionController.java index 25d8681..3ac6463 100644 --- a/src/main/java/com/onixbyte/helix/controller/PositionController.java +++ b/src/main/java/com/onixbyte/helix/controller/PositionController.java @@ -2,6 +2,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Position; import com.onixbyte.helix.service.PositionService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -16,6 +17,7 @@ public class PositionController { private final PositionService positionService; + @Autowired public PositionController(PositionService positionService) { this.positionService = positionService; } diff --git a/src/main/java/com/onixbyte/helix/controller/RoleController.java b/src/main/java/com/onixbyte/helix/controller/RoleController.java index 79a7505..b1ca22d 100644 --- a/src/main/java/com/onixbyte/helix/controller/RoleController.java +++ b/src/main/java/com/onixbyte/helix/controller/RoleController.java @@ -5,6 +5,7 @@ import com.onixbyte.helix.domain.web.request.AddRoleRequest; import com.onixbyte.helix.domain.web.request.EditRoleRequest; import com.onixbyte.helix.domain.web.request.QueryRoleRequest; import com.onixbyte.helix.service.RoleService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -18,6 +19,7 @@ public class RoleController { private final RoleService roleService; + @Autowired public RoleController(RoleService roleService) { this.roleService = roleService; } diff --git a/src/main/java/com/onixbyte/helix/controller/UserController.java b/src/main/java/com/onixbyte/helix/controller/UserController.java index 30d9f8e..e64aed2 100644 --- a/src/main/java/com/onixbyte/helix/controller/UserController.java +++ b/src/main/java/com/onixbyte/helix/controller/UserController.java @@ -1,12 +1,12 @@ package com.onixbyte.helix.controller; -import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.domain.web.request.AddUserRequest; import com.onixbyte.helix.domain.web.request.QueryUserRequest; import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; import com.onixbyte.helix.domain.web.request.UpdateUserRequest; import com.onixbyte.helix.domain.web.response.UserDetailResponse; import com.onixbyte.helix.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -21,6 +21,7 @@ public class UserController { private final UserService userService; + @Autowired public UserController(UserService userService) { this.userService = userService; } diff --git a/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java b/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java index d03f130..813cf8d 100644 --- a/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java +++ b/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java @@ -1,6 +1,7 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.properties.ApplicationProperties; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component @@ -8,6 +9,7 @@ public class ApplicationManager { private final ApplicationProperties applicationProperties; + @Autowired public ApplicationManager(ApplicationProperties applicationProperties) { this.applicationProperties = applicationProperties; } diff --git a/src/main/java/com/onixbyte/helix/manager/AssetManager.java b/src/main/java/com/onixbyte/helix/manager/AssetManager.java index b394abe..b9f1d59 100644 --- a/src/main/java/com/onixbyte/helix/manager/AssetManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AssetManager.java @@ -2,13 +2,12 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.constant.CacheName; import com.onixbyte.helix.domain.entity.Asset; -import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.mapper.AssetMapper; import com.onixbyte.helix.repository.AssetRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; @Component @@ -17,6 +16,7 @@ public class AssetManager { private final AssetMapper assetMapper; private final AssetRepository assetRepository; + @Autowired public AssetManager(AssetMapper assetMapper, AssetRepository assetRepository) { this.assetMapper = assetMapper; this.assetRepository = assetRepository; diff --git a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java index 8b11921..7d76d3d 100644 --- a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java @@ -3,6 +3,7 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.constant.CacheName; import com.onixbyte.helix.domain.entity.Authority; import com.onixbyte.helix.mapper.AuthorityMapper; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; @@ -13,6 +14,7 @@ public class AuthorityManager { private final AuthorityMapper authorityMapper; + @Autowired public AuthorityManager(AuthorityMapper authorityMapper) { this.authorityMapper = authorityMapper; } diff --git a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java index 9967c5c..bc678b7 100644 --- a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java +++ b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java @@ -3,6 +3,7 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.entity.Department; import com.onixbyte.helix.mapper.DepartmentMapper; import com.onixbyte.helix.repository.DepartmentRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; @@ -15,6 +16,7 @@ public class DepartmentManager { private final DepartmentMapper departmentMapper; private final DepartmentRepository departmentRepository; + @Autowired public DepartmentManager(DepartmentMapper departmentMapper, DepartmentRepository departmentRepository) { this.departmentMapper = departmentMapper; this.departmentRepository = departmentRepository; diff --git a/src/main/java/com/onixbyte/helix/manager/MenuManager.java b/src/main/java/com/onixbyte/helix/manager/MenuManager.java index e22f356..b4d491b 100644 --- a/src/main/java/com/onixbyte/helix/manager/MenuManager.java +++ b/src/main/java/com/onixbyte/helix/manager/MenuManager.java @@ -4,6 +4,7 @@ import com.onixbyte.helix.domain.entity.Menu; import com.onixbyte.helix.mapper.MenuMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.List; @@ -13,6 +14,7 @@ public class MenuManager { private static final Logger log = LoggerFactory.getLogger(MenuManager.class); private final MenuMapper menuMapper; + @Autowired public MenuManager(MenuMapper menuMapper) { this.menuMapper = menuMapper; } diff --git a/src/main/java/com/onixbyte/helix/manager/PositionManager.java b/src/main/java/com/onixbyte/helix/manager/PositionManager.java index d9c9454..3f4c46f 100644 --- a/src/main/java/com/onixbyte/helix/manager/PositionManager.java +++ b/src/main/java/com/onixbyte/helix/manager/PositionManager.java @@ -3,8 +3,8 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.entity.Position; import com.onixbyte.helix.mapper.PositionMapper; import com.onixbyte.helix.repository.PositionRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; @@ -14,6 +14,7 @@ public class PositionManager { private final PositionMapper positionMapper; private final PositionRepository positionRepository; + @Autowired public PositionManager(PositionMapper positionMapper, PositionRepository positionRepository) { this.positionMapper = positionMapper; this.positionRepository = positionRepository; diff --git a/src/main/java/com/onixbyte/helix/manager/RoleManager.java b/src/main/java/com/onixbyte/helix/manager/RoleManager.java index bd7f735..4544fe3 100644 --- a/src/main/java/com/onixbyte/helix/manager/RoleManager.java +++ b/src/main/java/com/onixbyte/helix/manager/RoleManager.java @@ -1,11 +1,11 @@ package com.onixbyte.helix.manager; -import com.onixbyte.helix.constant.Status; import com.onixbyte.helix.domain.database.query.wrapper.QueryRoleWrapper; import com.onixbyte.helix.domain.entity.Role; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.mapper.RoleMapper; import com.onixbyte.helix.repository.RoleRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.*; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; @@ -20,6 +20,7 @@ public class RoleManager { private final RoleMapper roleMapper; private final RoleRepository roleRepository; + @Autowired public RoleManager(RoleMapper roleMapper, RoleRepository roleRepository) { this.roleMapper = roleMapper; this.roleRepository = roleRepository; diff --git a/src/main/java/com/onixbyte/helix/manager/SettingManager.java b/src/main/java/com/onixbyte/helix/manager/SettingManager.java index c0ebc22..4003a5f 100644 --- a/src/main/java/com/onixbyte/helix/manager/SettingManager.java +++ b/src/main/java/com/onixbyte/helix/manager/SettingManager.java @@ -2,6 +2,7 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.entity.Setting; import com.onixbyte.helix.repository.SettingRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; @@ -10,6 +11,7 @@ public class SettingManager { private final SettingRepository settingRepository; + @Autowired public SettingManager(SettingRepository settingRepository) { this.settingRepository = settingRepository; } diff --git a/src/main/java/com/onixbyte/helix/manager/UserManager.java b/src/main/java/com/onixbyte/helix/manager/UserManager.java index 3459404..942fb11 100644 --- a/src/main/java/com/onixbyte/helix/manager/UserManager.java +++ b/src/main/java/com/onixbyte/helix/manager/UserManager.java @@ -10,6 +10,7 @@ import com.onixbyte.helix.mapper.UserMapper; import com.onixbyte.helix.repository.UserRepository; import com.onixbyte.region.Region; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Example; @@ -30,7 +31,12 @@ public class UserManager { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - public UserManager(UserMapper userMapper, UserRepository userRepository, PasswordEncoder passwordEncoder) { + @Autowired + public UserManager( + UserMapper userMapper, + UserRepository userRepository, + PasswordEncoder passwordEncoder + ) { this.userMapper = userMapper; this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; diff --git a/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java b/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java index efe476b..c03800c 100644 --- a/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java +++ b/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java @@ -1,12 +1,11 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.entity.UserRole; -import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.mapper.UserRoleMapper; import com.onixbyte.helix.repository.UserRoleRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.List; @@ -18,6 +17,7 @@ public class UserRoleManager { private final UserRoleMapper userRoleMapper; private final UserRoleRepository userRoleRepository; + @Autowired public UserRoleManager(UserRoleMapper userRoleMapper, UserRoleRepository userRoleRepository) { this.userRoleMapper = userRoleMapper; this.userRoleRepository = userRoleRepository; diff --git a/src/main/java/com/onixbyte/helix/manager/package-info.java b/src/main/java/com/onixbyte/helix/manager/package-info.java deleted file mode 100644 index 27b5fd9..0000000 --- a/src/main/java/com/onixbyte/helix/manager/package-info.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Business logic management and orchestration package for the Helix application. - *

- * This package is designed to contain manager classes that orchestrate complex business operations, - * coordinate between multiple services, and handle cross-cutting concerns that span multiple - * domain boundaries. - *

- * Manager Pattern: Managers in this package serve as facades or coordinators that - * encapsulate complex business workflows, typically involving multiple services, repositories, - * or external systems. They provide a higher-level abstraction over individual service components. - *

- * Intended Contents: - *

    - *
  • - * Workflow Managers: Orchestrate multi-step business processes - *
  • - *
  • - * Integration Managers: Coordinate interactions with external systems - *
  • - *
  • - * Transaction Managers: Handle complex transactional scenarios - *
  • - *
  • - * Cache Managers: Manage caching strategies and cache invalidation - *
  • - *
  • - * Event Managers: Coordinate event publishing and handling - *
  • - *
- *

- * Design Guidelines: - *

    - *
  • - * Single Responsibility: Each manager should focus on a specific business domain - * or cross-cutting concern - *
  • - *
  • - * Service Coordination: Managers should delegate to services rather than - * implementing business logic directly - *
  • - *
  • - * Transaction Boundaries: Define clear transactional boundaries for - * complex operations - *
  • - *
  • - * Error Handling: Provide comprehensive error handling and rollback mechanisms - * for complex workflows - *
  • - *
- *

- * Managers typically sit between the controller layer and the service layer, providing a - * coordination point for complex operations that require multiple service interactions or - * cross-cutting concerns. - * - * @author zihluwang - * @since 1.0.0 - * @see com.onixbyte.helix.service - * @see org.springframework.transaction.annotation.Transactional - */ -package com.onixbyte.helix.manager; \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java b/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java index 134ba66..dc20d46 100644 --- a/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java +++ b/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java @@ -7,6 +7,7 @@ import com.onixbyte.helix.manager.UserManager; import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; @@ -24,7 +25,12 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro private final PasswordEncoder passwordEncoder; private final AuthorityManager authorityManager; - public UsernamePasswordAuthenticationProvider(UserManager userManager, PasswordEncoder passwordEncoder, AuthorityManager authorityManager) { + @Autowired + public UsernamePasswordAuthenticationProvider( + UserManager userManager, + PasswordEncoder passwordEncoder, + AuthorityManager authorityManager + ) { this.userManager = userManager; this.passwordEncoder = passwordEncoder; this.authorityManager = authorityManager; diff --git a/src/main/java/com/onixbyte/helix/service/AssetService.java b/src/main/java/com/onixbyte/helix/service/AssetService.java index 32bcfc8..33f1dc0 100644 --- a/src/main/java/com/onixbyte/helix/service/AssetService.java +++ b/src/main/java/com/onixbyte/helix/service/AssetService.java @@ -5,6 +5,7 @@ import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.AssetManager; import com.onixbyte.helix.properties.AssetProperties; import com.onixbyte.helix.utils.SecurityUtil; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,6 +35,7 @@ public class AssetService { private final S3Client s3Client; private final AssetManager assetManager; + @Autowired public AssetService( AssetProperties assetProperties, S3Client s3Client, diff --git a/src/main/java/com/onixbyte/helix/service/CaptchaService.java b/src/main/java/com/onixbyte/helix/service/CaptchaService.java index 3160965..866d1a9 100644 --- a/src/main/java/com/onixbyte/helix/service/CaptchaService.java +++ b/src/main/java/com/onixbyte/helix/service/CaptchaService.java @@ -51,14 +51,14 @@ public class CaptchaService { return null; } - // 生成 UUID 及验证码 + // Generate UUID and captcha var uuid = UUID.randomUUID().toString().replaceAll("-", ""); var captchaCode = producer.createText(); - // 将验证码保存到缓存中 + // Store UUID and captcha to cache captchaManager.setCaptcha(uuid, captchaCode); - // 生成验证码图片 + // Generate captcha image var captchaImage = producer.createImage(captchaCode); try (var byteArrayOutputStream = new FastByteArrayOutputStream()) { ImageIO.write(captchaImage, FileType.Image.JPEG.getExtension(), byteArrayOutputStream); @@ -69,8 +69,4 @@ public class CaptchaService { throw new BizException("无法生成验证码图片。"); } } - - public String getCaptcha(String uuid) { - return captchaManager.getCaptcha(uuid); - } } diff --git a/src/main/java/com/onixbyte/helix/service/DepartmentService.java b/src/main/java/com/onixbyte/helix/service/DepartmentService.java index 15f03c4..4eb231d 100644 --- a/src/main/java/com/onixbyte/helix/service/DepartmentService.java +++ b/src/main/java/com/onixbyte/helix/service/DepartmentService.java @@ -4,7 +4,7 @@ import com.onixbyte.helix.domain.entity.Department; import com.onixbyte.helix.domain.model.TreeNode; import com.onixbyte.helix.manager.DepartmentManager; import com.onixbyte.helix.utils.TreeUtil; -import org.springframework.data.domain.PageRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -15,6 +15,7 @@ public class DepartmentService { private final DepartmentManager departmentManager; + @Autowired public DepartmentService(DepartmentManager departmentManager) { this.departmentManager = departmentManager; } diff --git a/src/main/java/com/onixbyte/helix/service/MenuService.java b/src/main/java/com/onixbyte/helix/service/MenuService.java index 618ff89..2eabe09 100644 --- a/src/main/java/com/onixbyte/helix/service/MenuService.java +++ b/src/main/java/com/onixbyte/helix/service/MenuService.java @@ -7,6 +7,7 @@ import com.onixbyte.helix.utils.SecurityUtil; import com.onixbyte.helix.utils.TreeUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @@ -16,6 +17,7 @@ public class MenuService { private static final Logger log = LoggerFactory.getLogger(MenuService.class); private final MenuManager menuManager; + @Autowired public MenuService(MenuManager menuManager) { this.menuManager = menuManager; } diff --git a/src/main/java/com/onixbyte/helix/service/PositionService.java b/src/main/java/com/onixbyte/helix/service/PositionService.java index 905231b..2ec0aaa 100644 --- a/src/main/java/com/onixbyte/helix/service/PositionService.java +++ b/src/main/java/com/onixbyte/helix/service/PositionService.java @@ -2,17 +2,17 @@ package com.onixbyte.helix.service; import com.onixbyte.helix.domain.entity.Position; import com.onixbyte.helix.manager.PositionManager; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import java.util.List; - @Service public class PositionService { private final PositionManager positionManager; + @Autowired public PositionService(PositionManager positionManager) { this.positionManager = positionManager; } diff --git a/src/main/java/com/onixbyte/helix/service/RoleService.java b/src/main/java/com/onixbyte/helix/service/RoleService.java index 615db80..08e8e05 100644 --- a/src/main/java/com/onixbyte/helix/service/RoleService.java +++ b/src/main/java/com/onixbyte/helix/service/RoleService.java @@ -9,6 +9,7 @@ import com.onixbyte.helix.domain.web.request.QueryRoleRequest; import com.onixbyte.helix.manager.RoleManager; import jakarta.transaction.Transactional; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -20,6 +21,7 @@ public class RoleService { private final RoleManager roleManager; + @Autowired public RoleService(RoleManager roleManager) { this.roleManager = roleManager; } diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index bd3a1f0..f565764 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -17,6 +17,7 @@ import com.onixbyte.helix.manager.UserManager; import com.onixbyte.helix.manager.UserRoleManager; import com.onixbyte.identitygenerator.IdentityGenerator; import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.security.crypto.password.PasswordEncoder; @@ -36,6 +37,7 @@ public class UserService { private final PasswordEncoder passwordEncoder; private final ApplicationManager applicationManager; + @Autowired public UserService( UserManager userManager, IdentityGenerator userIdentityGenerator, diff --git a/src/main/java/com/onixbyte/helix/utils/DateTimeUtil.java b/src/main/java/com/onixbyte/helix/utils/DateTimeUtil.java index 6517a4d..9e63b65 100644 --- a/src/main/java/com/onixbyte/helix/utils/DateTimeUtil.java +++ b/src/main/java/com/onixbyte/helix/utils/DateTimeUtil.java @@ -4,8 +4,19 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; +/** + * DateTimeUtil helps to convert date and time between different types. + * + * @author zihluwang + */ public class DateTimeUtil { + /** + * Convert {@link LocalDateTime} to {@link Instant}. + * + * @param localDateTime local date time to be converted + * @return converted instant + */ public static Instant asInstant(LocalDateTime localDateTime) { return localDateTime.atZone(ZoneId.systemDefault()) .toInstant(); diff --git a/src/main/java/com/onixbyte/helix/utils/SecurityUtil.java b/src/main/java/com/onixbyte/helix/utils/SecurityUtil.java index 5165508..acb59be 100644 --- a/src/main/java/com/onixbyte/helix/utils/SecurityUtil.java +++ b/src/main/java/com/onixbyte/helix/utils/SecurityUtil.java @@ -7,10 +7,24 @@ import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.security.core.context.SecurityContextHolder; +/** + * SecurityUtil helps to manipulate Spring Security. + * + * @author zihluwang + */ public class SecurityUtil { private static final Logger log = LoggerFactory.getLogger(SecurityUtil.class); + private SecurityUtil() { + } + + /** + * Get user information from {@link SecurityContextHolder}. + * + * @return user information + * @throws BizException if user information is not saved in {@link SecurityContextHolder} + */ public static User getCurrentUser() { var _details = SecurityContextHolder.getContext() .getAuthentication() diff --git a/src/main/java/com/onixbyte/helix/utils/TreeUtil.java b/src/main/java/com/onixbyte/helix/utils/TreeUtil.java index e5ba853..15f23fe 100644 --- a/src/main/java/com/onixbyte/helix/utils/TreeUtil.java +++ b/src/main/java/com/onixbyte/helix/utils/TreeUtil.java @@ -9,8 +9,16 @@ import org.springframework.http.HttpStatus; import java.util.*; import java.util.stream.Collectors; +/** + * TreeUtil help to build a tree or forest of specified data. + * + * @author zihluwang + */ public class TreeUtil { + private TreeUtil() { + } + /** * Builds a tree from a flat list of items that implement Treeable. * @@ -40,7 +48,7 @@ public class TreeUtil { // Get root item and build root node var rootItem = rootItems.get(0); - var rootNode = new TreeNode(rootItem); + var rootNode = new TreeNode<>(rootItem); idToNodeMap.remove(rootItem.getId()); // Iterate through all departments to build the hierarchy. From 670e54aefb5e705db349e5ad9327a6f01073840c Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 29 Dec 2025 10:36:50 +0800 Subject: [PATCH 06/60] =?UTF-8?q?chore(deps):=20=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E6=95=B0=E5=AD=A6=E8=AE=A1=E7=AE=97=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + gradle/libs.versions.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 6a9e9ae..0c6d6b0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(platform(libs.onixbyte.versionCatalogue)) implementation(libs.onixbyte.tuple) implementation(libs.onixbyte.commonToolbox) + implementation(libs.onixbyte.mathToolbox) implementation(libs.onixbyte.identityGenerator) implementation(libs.onixbyte.captcha) implementation(libs.onixbyte.regions) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 744b7f0..8c094a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher onixbyte-versionCatalogue = { group = "com.onixbyte", name = "version-catalogue", version.ref = "onixbyteVersion" } onixbyte-tuple = { group = "com.onixbyte", name = "tuple", version.ref = "onixbyteVersion" } onixbyte-commonToolbox = { group = "com.onixbyte", name = "common-toolbox", version.ref = "onixbyteVersion" } +onixbyte-mathToolbox = { group = "com.onixbyte", name = "math-toolbox", version.ref = "onixbyteVersion" } onixbyte-identityGenerator = { group = "com.onixbyte", name = "identity-generator", version.ref = "onixbyteVersion" } onixbyte-captcha = { group = "com.onixbyte", name = "captcha", version.ref = "onixbyteCaptcha" } onixbyte-regions = { group = "com.onixbyte", name = "regions4j", version.ref = "onixbyteRegions" } From 05b6bbca79234b39766be362ce4ff172eac94f06 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 29 Dec 2025 12:32:40 +0800 Subject: [PATCH 07/60] =?UTF-8?q?chore(deps):=20=E5=8D=87=E7=BA=A7=20JDK?= =?UTF-8?q?=2021?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 0c6d6b0..7f43d91 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,10 +14,10 @@ tasks.withType { } java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } } From f62e26d4416352393118b0130b2fd5718d192479 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 29 Dec 2025 12:33:01 +0800 Subject: [PATCH 08/60] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helix/controller/RoleController.java | 6 +++++ .../helix/manager/RoleAuthorityManager.java | 22 +++++++++++++++++++ .../onixbyte/helix/manager/RoleManager.java | 9 +++++++- .../helix/manager/UserRoleManager.java | 6 +++++ .../helix/mapper/RoleAuthorityMapper.java | 10 +++++++++ .../repository/RoleAuthorityRepository.java | 5 +++++ .../helix/repository/UserRoleRepository.java | 4 ++++ .../onixbyte/helix/service/RoleService.java | 20 ++++++++++++++++- .../resources/mapper/RoleAuthorityMapper.xml | 10 +++++++++ 9 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java create mode 100644 src/main/java/com/onixbyte/helix/mapper/RoleAuthorityMapper.java create mode 100644 src/main/resources/mapper/RoleAuthorityMapper.xml diff --git a/src/main/java/com/onixbyte/helix/controller/RoleController.java b/src/main/java/com/onixbyte/helix/controller/RoleController.java index b1ca22d..adbf5c2 100644 --- a/src/main/java/com/onixbyte/helix/controller/RoleController.java +++ b/src/main/java/com/onixbyte/helix/controller/RoleController.java @@ -45,4 +45,10 @@ public class RoleController { roleService.editRole(request); return ResponseEntity.ok(null); } + + @DeleteMapping("/{id:\\d+}") + public ResponseEntity deleteRole(@PathVariable Long id) { + roleService.deleteRole(id); + return ResponseEntity.ok(null); + } } diff --git a/src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java new file mode 100644 index 0000000..684698d --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java @@ -0,0 +1,22 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.repository.RoleAuthorityRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class RoleAuthorityManager { + + private static final Logger log = LoggerFactory.getLogger(RoleAuthorityManager.class); + private final RoleAuthorityRepository roleAuthorityRepository; + + public RoleAuthorityManager(RoleAuthorityRepository roleAuthorityRepository) { + this.roleAuthorityRepository = roleAuthorityRepository; + } + + public void deleteByRoleId(Long roleId) { + var affectedRows = roleAuthorityRepository.deleteByRoleId(roleId); + log.info("角色 {} 关联的权限绑定已全部移除(共 {} 条)", roleId, affectedRows); + } +} diff --git a/src/main/java/com/onixbyte/helix/manager/RoleManager.java b/src/main/java/com/onixbyte/helix/manager/RoleManager.java index 4544fe3..685a794 100644 --- a/src/main/java/com/onixbyte/helix/manager/RoleManager.java +++ b/src/main/java/com/onixbyte/helix/manager/RoleManager.java @@ -3,6 +3,7 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.database.query.wrapper.QueryRoleWrapper; import com.onixbyte.helix.domain.entity.Role; import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.mapper.RoleAuthorityMapper; import com.onixbyte.helix.mapper.RoleMapper; import com.onixbyte.helix.repository.RoleRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -19,11 +20,13 @@ public class RoleManager { private final RoleMapper roleMapper; private final RoleRepository roleRepository; + private final RoleAuthorityMapper roleAuthorityMapper; @Autowired - public RoleManager(RoleMapper roleMapper, RoleRepository roleRepository) { + public RoleManager(RoleMapper roleMapper, RoleRepository roleRepository, RoleAuthorityMapper roleAuthorityMapper) { this.roleMapper = roleMapper; this.roleRepository = roleRepository; + this.roleAuthorityMapper = roleAuthorityMapper; } public void validateRoles(List roleIds) { @@ -70,4 +73,8 @@ public class RoleManager { Optional.ofNullable(role.getStatus()) .ifPresent(roleToUpdate::setStatus); } + + public void deleteRole(Long id) { + roleRepository.deleteById(id); + } } diff --git a/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java b/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java index c03800c..0350bff 100644 --- a/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java +++ b/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java @@ -1,6 +1,7 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.entity.UserRole; +import com.onixbyte.helix.domain.entity.embeddable.UserRoleId; import com.onixbyte.helix.mapper.UserRoleMapper; import com.onixbyte.helix.repository.UserRoleRepository; import org.slf4j.Logger; @@ -31,4 +32,9 @@ public class UserRoleManager { var affectedRows = userRoleRepository.deleteByUserId(userId); log.info("用户 {} 的角色关联被全部移除(共 {} 条)。", userId, affectedRows); } + + public void deleteByRoleId(Long roleId) { + var affectedRows = userRoleRepository.deleteByRoleId(roleId); + log.info("角色 {} 的用户关联被全部移除(共 {} 条)。", roleId, affectedRows); + } } diff --git a/src/main/java/com/onixbyte/helix/mapper/RoleAuthorityMapper.java b/src/main/java/com/onixbyte/helix/mapper/RoleAuthorityMapper.java new file mode 100644 index 0000000..72bf389 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/mapper/RoleAuthorityMapper.java @@ -0,0 +1,10 @@ +package com.onixbyte.helix.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface RoleAuthorityMapper { + + int deleteByRoleId(@Param("roleId") Long roleId); +} diff --git a/src/main/java/com/onixbyte/helix/repository/RoleAuthorityRepository.java b/src/main/java/com/onixbyte/helix/repository/RoleAuthorityRepository.java index 972639f..1148c16 100644 --- a/src/main/java/com/onixbyte/helix/repository/RoleAuthorityRepository.java +++ b/src/main/java/com/onixbyte/helix/repository/RoleAuthorityRepository.java @@ -3,8 +3,13 @@ package com.onixbyte.helix.repository; import com.onixbyte.helix.domain.entity.RoleAuthority; import com.onixbyte.helix.domain.entity.embeddable.RoleAuthorityId; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface RoleAuthorityRepository extends JpaRepository { + @Modifying + @Query("DELETE FROM RoleAuthority ra WHERE ra.id.roleId = :roleId") + int deleteByRoleId(Long roleId); } diff --git a/src/main/java/com/onixbyte/helix/repository/UserRoleRepository.java b/src/main/java/com/onixbyte/helix/repository/UserRoleRepository.java index 8cd6a7b..3bac5ef 100644 --- a/src/main/java/com/onixbyte/helix/repository/UserRoleRepository.java +++ b/src/main/java/com/onixbyte/helix/repository/UserRoleRepository.java @@ -13,4 +13,8 @@ public interface UserRoleRepository extends JpaRepository @Modifying @Query("DELETE FROM UserRole ur WHERE ur.id.userId = :userId") int deleteByUserId(Long userId); + + @Modifying + @Query("DELETE FROM UserRole ur WHERE ur.id.roleId = :roleId") + int deleteByRoleId(Long roleId); } diff --git a/src/main/java/com/onixbyte/helix/service/RoleService.java b/src/main/java/com/onixbyte/helix/service/RoleService.java index 08e8e05..f09ad05 100644 --- a/src/main/java/com/onixbyte/helix/service/RoleService.java +++ b/src/main/java/com/onixbyte/helix/service/RoleService.java @@ -6,7 +6,10 @@ import com.onixbyte.helix.domain.entity.Role; import com.onixbyte.helix.domain.web.request.AddRoleRequest; import com.onixbyte.helix.domain.web.request.EditRoleRequest; import com.onixbyte.helix.domain.web.request.QueryRoleRequest; +import com.onixbyte.helix.manager.RoleAuthorityManager; import com.onixbyte.helix.manager.RoleManager; +import com.onixbyte.helix.manager.UserRoleManager; +import com.onixbyte.helix.repository.UserRoleRepository; import jakarta.transaction.Transactional; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -20,10 +23,18 @@ import java.util.Optional; public class RoleService { private final RoleManager roleManager; + private final RoleAuthorityManager roleAuthorityManager; + private final UserRoleManager userRoleManager; @Autowired - public RoleService(RoleManager roleManager) { + public RoleService( + RoleManager roleManager, + RoleAuthorityManager roleAuthorityManager, + UserRoleManager userRoleManager + ) { this.roleManager = roleManager; + this.roleAuthorityManager = roleAuthorityManager; + this.userRoleManager = userRoleManager; } public Page getRoles(Pageable pageable, QueryRoleRequest request) { @@ -78,4 +89,11 @@ public class RoleService { .orElse(null)) .build()); } + + @Transactional + public void deleteRole(Long id) { + roleAuthorityManager.deleteByRoleId(id); + userRoleManager.deleteByRoleId(id); + roleManager.deleteRole(id); + } } diff --git a/src/main/resources/mapper/RoleAuthorityMapper.xml b/src/main/resources/mapper/RoleAuthorityMapper.xml new file mode 100644 index 0000000..9e32c8f --- /dev/null +++ b/src/main/resources/mapper/RoleAuthorityMapper.xml @@ -0,0 +1,10 @@ + + + + + DELETE + FROM role_authorities + WHERE role_id = #{roleId} + + \ No newline at end of file From 659d123f2ba50fec38abd13b705c9442a3160450 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 09:29:33 +0800 Subject: [PATCH 09/60] =?UTF-8?q?refactor:=20=E8=B0=83=E6=95=B4=E5=8C=85?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/onixbyte/helix/controller/DepartmentController.java | 2 +- src/main/java/com/onixbyte/helix/controller/MenuController.java | 2 +- .../com/onixbyte/helix/domain/{model => common}/TreeNode.java | 2 +- src/main/java/com/onixbyte/helix/service/DepartmentService.java | 2 +- src/main/java/com/onixbyte/helix/service/MenuService.java | 2 +- src/main/java/com/onixbyte/helix/utils/TreeUtil.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename src/main/java/com/onixbyte/helix/domain/{model => common}/TreeNode.java (87%) diff --git a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java index 338a06c..f8716eb 100644 --- a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java +++ b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java @@ -1,7 +1,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Department; -import com.onixbyte.helix.domain.model.TreeNode; +import com.onixbyte.helix.domain.common.TreeNode; import com.onixbyte.helix.service.DepartmentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/onixbyte/helix/controller/MenuController.java b/src/main/java/com/onixbyte/helix/controller/MenuController.java index 26e0aa6..4523651 100644 --- a/src/main/java/com/onixbyte/helix/controller/MenuController.java +++ b/src/main/java/com/onixbyte/helix/controller/MenuController.java @@ -1,7 +1,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Menu; -import com.onixbyte.helix.domain.model.TreeNode; +import com.onixbyte.helix.domain.common.TreeNode; import com.onixbyte.helix.service.MenuService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/onixbyte/helix/domain/model/TreeNode.java b/src/main/java/com/onixbyte/helix/domain/common/TreeNode.java similarity index 87% rename from src/main/java/com/onixbyte/helix/domain/model/TreeNode.java rename to src/main/java/com/onixbyte/helix/domain/common/TreeNode.java index 3748a6b..ab3505e 100644 --- a/src/main/java/com/onixbyte/helix/domain/model/TreeNode.java +++ b/src/main/java/com/onixbyte/helix/domain/common/TreeNode.java @@ -1,4 +1,4 @@ -package com.onixbyte.helix.domain.model; +package com.onixbyte.helix.domain.common; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/onixbyte/helix/service/DepartmentService.java b/src/main/java/com/onixbyte/helix/service/DepartmentService.java index 4eb231d..0cae30a 100644 --- a/src/main/java/com/onixbyte/helix/service/DepartmentService.java +++ b/src/main/java/com/onixbyte/helix/service/DepartmentService.java @@ -1,7 +1,7 @@ package com.onixbyte.helix.service; import com.onixbyte.helix.domain.entity.Department; -import com.onixbyte.helix.domain.model.TreeNode; +import com.onixbyte.helix.domain.common.TreeNode; import com.onixbyte.helix.manager.DepartmentManager; import com.onixbyte.helix.utils.TreeUtil; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/java/com/onixbyte/helix/service/MenuService.java b/src/main/java/com/onixbyte/helix/service/MenuService.java index 2eabe09..ad5a57f 100644 --- a/src/main/java/com/onixbyte/helix/service/MenuService.java +++ b/src/main/java/com/onixbyte/helix/service/MenuService.java @@ -1,7 +1,7 @@ package com.onixbyte.helix.service; import com.onixbyte.helix.domain.entity.Menu; -import com.onixbyte.helix.domain.model.TreeNode; +import com.onixbyte.helix.domain.common.TreeNode; import com.onixbyte.helix.manager.MenuManager; import com.onixbyte.helix.utils.SecurityUtil; import com.onixbyte.helix.utils.TreeUtil; diff --git a/src/main/java/com/onixbyte/helix/utils/TreeUtil.java b/src/main/java/com/onixbyte/helix/utils/TreeUtil.java index 15f23fe..848c65b 100644 --- a/src/main/java/com/onixbyte/helix/utils/TreeUtil.java +++ b/src/main/java/com/onixbyte/helix/utils/TreeUtil.java @@ -1,7 +1,7 @@ package com.onixbyte.helix.utils; import com.onixbyte.helix.domain.common.Treeable; -import com.onixbyte.helix.domain.model.TreeNode; +import com.onixbyte.helix.domain.common.TreeNode; import com.onixbyte.helix.exception.BizException; import org.apache.commons.collections4.CollectionUtils; import org.springframework.http.HttpStatus; From c74a67cdc6401b8c688ea563343dee226d260673 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 09:46:28 +0800 Subject: [PATCH 10/60] =?UTF-8?q?perf:=20=E6=80=A7=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用全局唯一 JWT Verifier 实例 --- .../onixbyte/helix/client/TokenClient.java | 19 ++++++++++++++++++- .../onixbyte/helix/config/SecurityConfig.java | 9 +++++++++ .../filter/TokenAuthenticationFilter.java | 17 +++++++++-------- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/client/TokenClient.java b/src/main/java/com/onixbyte/helix/client/TokenClient.java index 3c11f4b..7e13e0d 100644 --- a/src/main/java/com/onixbyte/helix/client/TokenClient.java +++ b/src/main/java/com/onixbyte/helix/client/TokenClient.java @@ -1,7 +1,9 @@ package com.onixbyte.helix.client; import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.properties.TokenProperties; import com.onixbyte.helix.utils.DateTimeUtil; @@ -24,6 +26,7 @@ public class TokenClient { private final Algorithm algorithm; private final TokenProperties tokenProperties; + private final JWTVerifier verifier; /** * Constructs a new TokenClient with the necessary algorithm and token properties. @@ -33,9 +36,10 @@ public class TokenClient { * validity period */ @Autowired - public TokenClient(Algorithm algorithm, TokenProperties tokenProperties) { + public TokenClient(Algorithm algorithm, TokenProperties tokenProperties, JWTVerifier verifier) { this.algorithm = algorithm; this.tokenProperties = tokenProperties; + this.verifier = verifier; } /** @@ -56,4 +60,17 @@ public class TokenClient { .withExpiresAt(DateTimeUtil.asInstant(expiresAt)) .sign(algorithm); } + + /** + * Verify and decode token. + * + * @param token a JWT token + * @return information included in the given token + * @throws com.auth0.jwt.exceptions.JWTVerificationException if the token is invalid, such as + * expired, or not signed by + * specific server + */ + public DecodedJWT verifyToken(String token) { + return verifier.verify(token); + } } \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/config/SecurityConfig.java b/src/main/java/com/onixbyte/helix/config/SecurityConfig.java index 7d80d9d..9ed2ffc 100644 --- a/src/main/java/com/onixbyte/helix/config/SecurityConfig.java +++ b/src/main/java/com/onixbyte/helix/config/SecurityConfig.java @@ -1,5 +1,7 @@ package com.onixbyte.helix.config; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.onixbyte.helix.filter.TokenAuthenticationFilter; import com.onixbyte.helix.properties.CorsProperties; @@ -206,4 +208,11 @@ public class SecurityConfig { public Algorithm algorithm(TokenProperties properties) { return Algorithm.HMAC256(properties.secret()); } + + @Bean + public JWTVerifier verifier(Algorithm algorithm, TokenProperties tokenProperties) { + return JWT.require(algorithm) + .withIssuer(tokenProperties.issuer()) + .build(); + } } diff --git a/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java index a22d041..99d123b 100644 --- a/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java +++ b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java @@ -3,6 +3,7 @@ package com.onixbyte.helix.filter; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; +import com.onixbyte.helix.client.TokenClient; import com.onixbyte.helix.manager.AuthorityManager; import com.onixbyte.helix.manager.UserManager; import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; @@ -26,14 +27,18 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { private final static Logger log = LoggerFactory.getLogger(TokenAuthenticationFilter.class); - private final Algorithm algorithm; private final UserManager userManager; private final AuthorityManager authorityManager; + private final TokenClient tokenClient; - public TokenAuthenticationFilter(Algorithm algorithm, UserManager userManager, AuthorityManager authorityManager) { - this.algorithm = algorithm; + public TokenAuthenticationFilter( + UserManager userManager, + AuthorityManager authorityManager, + TokenClient tokenClient + ) { this.userManager = userManager; this.authorityManager = authorityManager; + this.tokenClient = tokenClient; } @Override @@ -54,12 +59,8 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { } token = token.substring(7); - var verifier = JWT.require(algorithm) - .withIssuer("Helix Server") - .build(); - try { - var decodedToken = verifier.verify(token); + var decodedToken = tokenClient.verifyToken(token); var username = decodedToken.getSubject(); var user = userManager.selectByUsername(username); From d2d0ccef1cd8e04af2b1eb97fa80aa92d1ff046a Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 09:54:12 +0800 Subject: [PATCH 11/60] =?UTF-8?q?style:=20=E7=A7=BB=E9=99=A4=E9=AD=94?= =?UTF-8?q?=E6=B3=95=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/onixbyte/helix/constant/SecurityConstant.java | 10 ++++++++++ .../helix/filter/TokenAuthenticationFilter.java | 9 ++++----- 2 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/onixbyte/helix/constant/SecurityConstant.java diff --git a/src/main/java/com/onixbyte/helix/constant/SecurityConstant.java b/src/main/java/com/onixbyte/helix/constant/SecurityConstant.java new file mode 100644 index 0000000..08c3bb5 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/constant/SecurityConstant.java @@ -0,0 +1,10 @@ +package com.onixbyte.helix.constant; + +public class SecurityConstant { + + public static final String TOKEN_HEADER_NAME = "Authorization"; + + public static final String TOKEN_PREFIX = "Bearer "; + + public static final int TOKEN_PREFIX_LENGTH = TOKEN_PREFIX.length(); +} diff --git a/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java index 99d123b..63990d1 100644 --- a/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java +++ b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java @@ -1,9 +1,8 @@ package com.onixbyte.helix.filter; -import com.auth0.jwt.JWT; -import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.onixbyte.helix.client.TokenClient; +import com.onixbyte.helix.constant.SecurityConstant; import com.onixbyte.helix.manager.AuthorityManager; import com.onixbyte.helix.manager.UserManager; import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; @@ -47,18 +46,18 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { - var token = request.getHeader("Authorization"); + var token = request.getHeader(SecurityConstant.TOKEN_HEADER_NAME); if (Objects.isNull(token) || token.isBlank()) { filterChain.doFilter(request, response); return; } - if (!token.startsWith("Bearer ")) { + if (!token.startsWith(SecurityConstant.TOKEN_PREFIX)) { filterChain.doFilter(request, response); return; } - token = token.substring(7); + token = token.substring(SecurityConstant.TOKEN_PREFIX_LENGTH); try { var decodedToken = tokenClient.verifyToken(token); var username = decodedToken.getSubject(); From b24a6dc809d87c07e0b36f91c2e6e28ac2273c53 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 11:14:10 +0800 Subject: [PATCH 12/60] =?UTF-8?q?refactor:=20=E8=B0=83=E6=95=B4=E5=8C=85?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/onixbyte/helix/controller/AssetController.java | 2 +- .../domain/database/query/wrapper/QueryRoleWrapper.java | 2 +- .../domain/database/query/wrapper/QueryUserWrapper.java | 2 +- .../java/com/onixbyte/helix/domain/entity/Authority.java | 3 +-- .../java/com/onixbyte/helix/domain/entity/Department.java | 2 +- src/main/java/com/onixbyte/helix/domain/entity/Menu.java | 2 +- .../java/com/onixbyte/helix/domain/entity/Position.java | 2 +- src/main/java/com/onixbyte/helix/domain/entity/Role.java | 2 +- .../java/com/onixbyte/helix/domain/entity/Setting.java | 2 +- src/main/java/com/onixbyte/helix/domain/entity/User.java | 2 +- .../com/onixbyte/helix/domain/entity/UserIdentity.java | 2 +- .../helix/domain/entity/embeddable/UserIdentityId.java | 3 +-- .../com/onixbyte/helix/domain/view/AuthorityView.java | 2 +- .../com/onixbyte/helix/domain/view/DepartmentView.java | 2 +- .../java/com/onixbyte/helix/domain/view/PositionView.java | 2 +- .../java/com/onixbyte/helix/domain/view/RoleView.java | 2 +- .../com/onixbyte/helix/domain/view/UserIdentityView.java | 2 +- .../java/com/onixbyte/helix/domain/view/UserView.java | 2 +- .../onixbyte/helix/domain/web/request/AddUserRequest.java | 2 +- .../helix/domain/web/request/UpdateUserRequest.java | 4 +--- .../helix/domain/web/response/UserDetailResponse.java | 2 +- .../helix/{constant => enumeration}/ExternalHost.java | 2 +- .../helix/{constant => enumeration}/IdentityProvider.java | 2 +- .../helix/{constant => enumeration}/SettingType.java | 2 +- .../onixbyte/helix/{constant => enumeration}/Status.java | 2 +- .../helix/{constant => enumeration}/UserStatus.java | 2 +- .../onixbyte/helix/filter/TokenAuthenticationFilter.java | 8 ++++---- .../java/com/onixbyte/helix/manager/AssetManager.java | 2 +- .../java/com/onixbyte/helix/manager/AuthorityManager.java | 2 +- .../java/com/onixbyte/helix/manager/CaptchaManager.java | 6 ------ src/main/java/com/onixbyte/helix/manager/UserManager.java | 2 +- src/main/java/com/onixbyte/helix/service/AuthService.java | 2 +- .../java/com/onixbyte/helix/service/CaptchaService.java | 4 ++-- src/main/java/com/onixbyte/helix/service/RoleService.java | 3 +-- src/main/java/com/onixbyte/helix/service/UserService.java | 2 +- .../onixbyte/helix/{constant => shared}/AssetPrefix.java | 2 +- .../onixbyte/helix/{constant => shared}/CacheName.java | 2 +- .../com/onixbyte/helix/{constant => shared}/FileType.java | 2 +- .../onixbyte/helix/{constant => shared}/SettingName.java | 2 +- .../SecurityConstant.java => shared/TokenConstant.java} | 4 ++-- 40 files changed, 44 insertions(+), 55 deletions(-) rename src/main/java/com/onixbyte/helix/{constant => enumeration}/ExternalHost.java (95%) rename src/main/java/com/onixbyte/helix/{constant => enumeration}/IdentityProvider.java (97%) rename src/main/java/com/onixbyte/helix/{constant => enumeration}/SettingType.java (63%) rename src/main/java/com/onixbyte/helix/{constant => enumeration}/Status.java (97%) rename src/main/java/com/onixbyte/helix/{constant => enumeration}/UserStatus.java (98%) rename src/main/java/com/onixbyte/helix/{constant => shared}/AssetPrefix.java (83%) rename src/main/java/com/onixbyte/helix/{constant => shared}/CacheName.java (92%) rename src/main/java/com/onixbyte/helix/{constant => shared}/FileType.java (91%) rename src/main/java/com/onixbyte/helix/{constant => shared}/SettingName.java (82%) rename src/main/java/com/onixbyte/helix/{constant/SecurityConstant.java => shared/TokenConstant.java} (74%) diff --git a/src/main/java/com/onixbyte/helix/controller/AssetController.java b/src/main/java/com/onixbyte/helix/controller/AssetController.java index c545530..3ef06d7 100644 --- a/src/main/java/com/onixbyte/helix/controller/AssetController.java +++ b/src/main/java/com/onixbyte/helix/controller/AssetController.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.controller; -import com.onixbyte.helix.constant.AssetPrefix; +import com.onixbyte.helix.shared.AssetPrefix; import com.onixbyte.helix.domain.web.response.FileUploadResponse; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.service.AssetService; diff --git a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java index 29a2a8d..51366b4 100644 --- a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java +++ b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.database.query.wrapper; -import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.enumeration.Status; public class QueryRoleWrapper { private String name; diff --git a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java index 1ee80ae..695a704 100644 --- a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java +++ b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.database.query.wrapper; -import com.onixbyte.helix.constant.UserStatus; +import com.onixbyte.helix.enumeration.UserStatus; import java.time.LocalDateTime; diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Authority.java b/src/main/java/com/onixbyte/helix/domain/entity/Authority.java index 1851591..eab02f1 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Authority.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Authority.java @@ -1,9 +1,8 @@ package com.onixbyte.helix.domain.entity; -import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.enumeration.Status; import jakarta.persistence.*; import org.hibernate.annotations.JdbcType; -import org.hibernate.annotations.Type; import org.hibernate.dialect.PostgreSQLEnumJdbcType; import org.springframework.security.core.GrantedAuthority; import java.time.LocalDateTime; diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Department.java b/src/main/java/com/onixbyte/helix/domain/entity/Department.java index d4f0d40..1a68320 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Department.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Department.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.entity; -import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.domain.common.Treeable; import jakarta.persistence.*; import org.hibernate.annotations.JdbcType; diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Menu.java b/src/main/java/com/onixbyte/helix/domain/entity/Menu.java index c45dd3b..5702892 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Menu.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Menu.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.entity; -import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.domain.common.Treeable; import jakarta.persistence.*; import org.hibernate.annotations.JdbcType; diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Position.java b/src/main/java/com/onixbyte/helix/domain/entity/Position.java index fc0733a..65db827 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Position.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Position.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.entity; -import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.enumeration.Status; import jakarta.persistence.*; import org.hibernate.annotations.JdbcType; import org.hibernate.dialect.PostgreSQLEnumJdbcType; diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Role.java b/src/main/java/com/onixbyte/helix/domain/entity/Role.java index 5633d13..5fa4176 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Role.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Role.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.entity; -import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.enumeration.Status; import jakarta.persistence.*; import org.hibernate.annotations.JdbcType; import org.hibernate.dialect.PostgreSQLEnumJdbcType; diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Setting.java b/src/main/java/com/onixbyte/helix/domain/entity/Setting.java index 6bb6ce6..69f5046 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Setting.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Setting.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.entity; -import com.onixbyte.helix.constant.SettingType; +import com.onixbyte.helix.enumeration.SettingType; import jakarta.persistence.*; import org.hibernate.annotations.JdbcType; import org.hibernate.dialect.PostgreSQLEnumJdbcType; diff --git a/src/main/java/com/onixbyte/helix/domain/entity/User.java b/src/main/java/com/onixbyte/helix/domain/entity/User.java index eae7198..5d48206 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/User.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/User.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.entity; -import com.onixbyte.helix.constant.UserStatus; +import com.onixbyte.helix.enumeration.UserStatus; import jakarta.persistence.*; // 导入 Jakarta Persistence API import org.hibernate.annotations.JdbcType; import org.hibernate.dialect.PostgreSQLEnumJdbcType; diff --git a/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java b/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java index caf8d02..87d3e45 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.entity; -import com.onixbyte.helix.constant.IdentityProvider; +import com.onixbyte.helix.enumeration.IdentityProvider; import com.onixbyte.helix.domain.entity.embeddable.UserIdentityId; import jakarta.persistence.*; // 导入 Jakarta Persistence API import java.time.LocalDateTime; diff --git a/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserIdentityId.java b/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserIdentityId.java index f400040..3ec828f 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserIdentityId.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserIdentityId.java @@ -1,9 +1,8 @@ package com.onixbyte.helix.domain.entity.embeddable; -import com.onixbyte.helix.constant.IdentityProvider; +import com.onixbyte.helix.enumeration.IdentityProvider; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import org.hibernate.annotations.JdbcType; import org.hibernate.dialect.PostgreSQLEnumJdbcType; diff --git a/src/main/java/com/onixbyte/helix/domain/view/AuthorityView.java b/src/main/java/com/onixbyte/helix/domain/view/AuthorityView.java index 1db4664..c5562c4 100644 --- a/src/main/java/com/onixbyte/helix/domain/view/AuthorityView.java +++ b/src/main/java/com/onixbyte/helix/domain/view/AuthorityView.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.view; -import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.domain.entity.Authority; import java.time.LocalDateTime; diff --git a/src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java b/src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java index a72c58b..4edf143 100644 --- a/src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java +++ b/src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.view; -import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.domain.entity.Department; import java.time.LocalDateTime; diff --git a/src/main/java/com/onixbyte/helix/domain/view/PositionView.java b/src/main/java/com/onixbyte/helix/domain/view/PositionView.java index 8989f2b..8b9d3a6 100644 --- a/src/main/java/com/onixbyte/helix/domain/view/PositionView.java +++ b/src/main/java/com/onixbyte/helix/domain/view/PositionView.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.view; -import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.domain.entity.Position; import java.time.LocalDateTime; diff --git a/src/main/java/com/onixbyte/helix/domain/view/RoleView.java b/src/main/java/com/onixbyte/helix/domain/view/RoleView.java index dc0bc79..ee00803 100644 --- a/src/main/java/com/onixbyte/helix/domain/view/RoleView.java +++ b/src/main/java/com/onixbyte/helix/domain/view/RoleView.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.view; -import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.domain.entity.Role; import java.time.LocalDateTime; diff --git a/src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java b/src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java index d261996..aeab9d4 100644 --- a/src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java +++ b/src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.view; -import com.onixbyte.helix.constant.IdentityProvider; +import com.onixbyte.helix.enumeration.IdentityProvider; import com.onixbyte.helix.domain.entity.UserIdentity; import java.time.LocalDateTime; diff --git a/src/main/java/com/onixbyte/helix/domain/view/UserView.java b/src/main/java/com/onixbyte/helix/domain/view/UserView.java index df86f39..dc1b439 100644 --- a/src/main/java/com/onixbyte/helix/domain/view/UserView.java +++ b/src/main/java/com/onixbyte/helix/domain/view/UserView.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.view; -import com.onixbyte.helix.constant.UserStatus; +import com.onixbyte.helix.enumeration.UserStatus; import com.onixbyte.helix.domain.entity.User; import java.time.LocalDateTime; diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java index 9162a5c..df7fca0 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.web.request; -import com.onixbyte.helix.constant.UserStatus; +import com.onixbyte.helix.enumeration.UserStatus; import jakarta.validation.constraints.NotBlank; import java.util.List; diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/UpdateUserRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/UpdateUserRequest.java index 4c53655..55bfb05 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/UpdateUserRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/UpdateUserRequest.java @@ -1,11 +1,9 @@ package com.onixbyte.helix.domain.web.request; -import com.onixbyte.helix.constant.UserStatus; +import com.onixbyte.helix.enumeration.UserStatus; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; -import java.util.List; - public record UpdateUserRequest( @NotNull(message = "User ID cannot be null") @Positive(message = "User ID must be positive") diff --git a/src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java b/src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java index 7c50647..a709218 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java +++ b/src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.web.response; -import com.onixbyte.helix.constant.UserStatus; +import com.onixbyte.helix.enumeration.UserStatus; import java.time.LocalDateTime; diff --git a/src/main/java/com/onixbyte/helix/constant/ExternalHost.java b/src/main/java/com/onixbyte/helix/enumeration/ExternalHost.java similarity index 95% rename from src/main/java/com/onixbyte/helix/constant/ExternalHost.java rename to src/main/java/com/onixbyte/helix/enumeration/ExternalHost.java index 5512434..af7ebd9 100644 --- a/src/main/java/com/onixbyte/helix/constant/ExternalHost.java +++ b/src/main/java/com/onixbyte/helix/enumeration/ExternalHost.java @@ -1,4 +1,4 @@ -package com.onixbyte.helix.constant; +package com.onixbyte.helix.enumeration; /** * Constants for external host configurations and endpoints. diff --git a/src/main/java/com/onixbyte/helix/constant/IdentityProvider.java b/src/main/java/com/onixbyte/helix/enumeration/IdentityProvider.java similarity index 97% rename from src/main/java/com/onixbyte/helix/constant/IdentityProvider.java rename to src/main/java/com/onixbyte/helix/enumeration/IdentityProvider.java index 9e06844..068ee13 100644 --- a/src/main/java/com/onixbyte/helix/constant/IdentityProvider.java +++ b/src/main/java/com/onixbyte/helix/enumeration/IdentityProvider.java @@ -1,4 +1,4 @@ -package com.onixbyte.helix.constant; +package com.onixbyte.helix.enumeration; import com.onixbyte.helix.config.AuthenticationConfig; diff --git a/src/main/java/com/onixbyte/helix/constant/SettingType.java b/src/main/java/com/onixbyte/helix/enumeration/SettingType.java similarity index 63% rename from src/main/java/com/onixbyte/helix/constant/SettingType.java rename to src/main/java/com/onixbyte/helix/enumeration/SettingType.java index a4dfdd3..47facf9 100644 --- a/src/main/java/com/onixbyte/helix/constant/SettingType.java +++ b/src/main/java/com/onixbyte/helix/enumeration/SettingType.java @@ -1,4 +1,4 @@ -package com.onixbyte.helix.constant; +package com.onixbyte.helix.enumeration; public enum SettingType { diff --git a/src/main/java/com/onixbyte/helix/constant/Status.java b/src/main/java/com/onixbyte/helix/enumeration/Status.java similarity index 97% rename from src/main/java/com/onixbyte/helix/constant/Status.java rename to src/main/java/com/onixbyte/helix/enumeration/Status.java index 5b2a80d..eb078c8 100644 --- a/src/main/java/com/onixbyte/helix/constant/Status.java +++ b/src/main/java/com/onixbyte/helix/enumeration/Status.java @@ -1,4 +1,4 @@ -package com.onixbyte.helix.constant; +package com.onixbyte.helix.enumeration; /** * Enumeration representing general status states for system entities. diff --git a/src/main/java/com/onixbyte/helix/constant/UserStatus.java b/src/main/java/com/onixbyte/helix/enumeration/UserStatus.java similarity index 98% rename from src/main/java/com/onixbyte/helix/constant/UserStatus.java rename to src/main/java/com/onixbyte/helix/enumeration/UserStatus.java index bbc05a3..957f8ba 100644 --- a/src/main/java/com/onixbyte/helix/constant/UserStatus.java +++ b/src/main/java/com/onixbyte/helix/enumeration/UserStatus.java @@ -1,4 +1,4 @@ -package com.onixbyte.helix.constant; +package com.onixbyte.helix.enumeration; /** * Enumeration representing the various states of user accounts within the system. diff --git a/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java index 63990d1..63b3c2b 100644 --- a/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java +++ b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java @@ -2,10 +2,10 @@ package com.onixbyte.helix.filter; import com.auth0.jwt.exceptions.JWTVerificationException; import com.onixbyte.helix.client.TokenClient; -import com.onixbyte.helix.constant.SecurityConstant; import com.onixbyte.helix.manager.AuthorityManager; import com.onixbyte.helix.manager.UserManager; import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; +import com.onixbyte.helix.shared.TokenConstant; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -46,18 +46,18 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { - var token = request.getHeader(SecurityConstant.TOKEN_HEADER_NAME); + var token = request.getHeader(TokenConstant.TOKEN_HEADER_NAME); if (Objects.isNull(token) || token.isBlank()) { filterChain.doFilter(request, response); return; } - if (!token.startsWith(SecurityConstant.TOKEN_PREFIX)) { + if (!token.startsWith(TokenConstant.TOKEN_PREFIX)) { filterChain.doFilter(request, response); return; } - token = token.substring(SecurityConstant.TOKEN_PREFIX_LENGTH); + token = token.substring(TokenConstant.TOKEN_PREFIX_LENGTH); try { var decodedToken = tokenClient.verifyToken(token); var username = decodedToken.getSubject(); diff --git a/src/main/java/com/onixbyte/helix/manager/AssetManager.java b/src/main/java/com/onixbyte/helix/manager/AssetManager.java index b9f1d59..d899fd8 100644 --- a/src/main/java/com/onixbyte/helix/manager/AssetManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AssetManager.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.manager; -import com.onixbyte.helix.constant.CacheName; +import com.onixbyte.helix.shared.CacheName; import com.onixbyte.helix.domain.entity.Asset; import com.onixbyte.helix.mapper.AssetMapper; import com.onixbyte.helix.repository.AssetRepository; diff --git a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java index 7d76d3d..55cc481 100644 --- a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.manager; -import com.onixbyte.helix.constant.CacheName; +import com.onixbyte.helix.shared.CacheName; import com.onixbyte.helix.domain.entity.Authority; import com.onixbyte.helix.mapper.AuthorityMapper; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/java/com/onixbyte/helix/manager/CaptchaManager.java b/src/main/java/com/onixbyte/helix/manager/CaptchaManager.java index 605918a..2f8ad32 100644 --- a/src/main/java/com/onixbyte/helix/manager/CaptchaManager.java +++ b/src/main/java/com/onixbyte/helix/manager/CaptchaManager.java @@ -1,16 +1,10 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.client.RedisClient; -import com.onixbyte.helix.constant.CacheName; -import com.onixbyte.helix.properties.CaptchaProperties; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.Cache; -import org.springframework.data.redis.cache.RedisCacheManager; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.time.Duration; -import java.util.Objects; @Component public class CaptchaManager { diff --git a/src/main/java/com/onixbyte/helix/manager/UserManager.java b/src/main/java/com/onixbyte/helix/manager/UserManager.java index 942fb11..2bf0e72 100644 --- a/src/main/java/com/onixbyte/helix/manager/UserManager.java +++ b/src/main/java/com/onixbyte/helix/manager/UserManager.java @@ -1,7 +1,7 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.common.regex.Patterns; -import com.onixbyte.helix.constant.CacheName; +import com.onixbyte.helix.shared.CacheName; import com.onixbyte.helix.domain.database.query.wrapper.QueryUserWrapper; import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.domain.web.response.UserDetailResponse; diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index 1103f5e..d272cec 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -1,7 +1,7 @@ package com.onixbyte.helix.service; import com.onixbyte.helix.client.TokenClient; -import com.onixbyte.helix.constant.SettingName; +import com.onixbyte.helix.shared.SettingName; import com.onixbyte.helix.domain.entity.Setting; import com.onixbyte.helix.domain.web.request.UsernamePasswordLoginRequest; import com.onixbyte.helix.domain.web.response.LoginSuccessResponse; diff --git a/src/main/java/com/onixbyte/helix/service/CaptchaService.java b/src/main/java/com/onixbyte/helix/service/CaptchaService.java index 866d1a9..cda2d08 100644 --- a/src/main/java/com/onixbyte/helix/service/CaptchaService.java +++ b/src/main/java/com/onixbyte/helix/service/CaptchaService.java @@ -1,8 +1,8 @@ package com.onixbyte.helix.service; import com.onixbyte.captcha.Producer; -import com.onixbyte.helix.constant.FileType; -import com.onixbyte.helix.constant.SettingName; +import com.onixbyte.helix.shared.FileType; +import com.onixbyte.helix.shared.SettingName; import com.onixbyte.helix.domain.entity.Setting; import com.onixbyte.helix.domain.web.response.CaptchaResponse; import com.onixbyte.helix.exception.BizException; diff --git a/src/main/java/com/onixbyte/helix/service/RoleService.java b/src/main/java/com/onixbyte/helix/service/RoleService.java index f09ad05..8deedd0 100644 --- a/src/main/java/com/onixbyte/helix/service/RoleService.java +++ b/src/main/java/com/onixbyte/helix/service/RoleService.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.service; -import com.onixbyte.helix.constant.Status; +import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.domain.database.query.wrapper.QueryRoleWrapper; import com.onixbyte.helix.domain.entity.Role; import com.onixbyte.helix.domain.web.request.AddRoleRequest; @@ -9,7 +9,6 @@ import com.onixbyte.helix.domain.web.request.QueryRoleRequest; import com.onixbyte.helix.manager.RoleAuthorityManager; import com.onixbyte.helix.manager.RoleManager; import com.onixbyte.helix.manager.UserRoleManager; -import com.onixbyte.helix.repository.UserRoleRepository; import jakarta.transaction.Transactional; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index f565764..8667811 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -1,7 +1,7 @@ package com.onixbyte.helix.service; import com.onixbyte.common.util.HashUtil; -import com.onixbyte.helix.constant.UserStatus; +import com.onixbyte.helix.enumeration.UserStatus; import com.onixbyte.helix.domain.database.query.wrapper.QueryUserWrapper; import com.onixbyte.helix.domain.entity.Role; import com.onixbyte.helix.domain.entity.User; diff --git a/src/main/java/com/onixbyte/helix/constant/AssetPrefix.java b/src/main/java/com/onixbyte/helix/shared/AssetPrefix.java similarity index 83% rename from src/main/java/com/onixbyte/helix/constant/AssetPrefix.java rename to src/main/java/com/onixbyte/helix/shared/AssetPrefix.java index 09c9612..2591d2e 100644 --- a/src/main/java/com/onixbyte/helix/constant/AssetPrefix.java +++ b/src/main/java/com/onixbyte/helix/shared/AssetPrefix.java @@ -1,4 +1,4 @@ -package com.onixbyte.helix.constant; +package com.onixbyte.helix.shared; public class AssetPrefix { diff --git a/src/main/java/com/onixbyte/helix/constant/CacheName.java b/src/main/java/com/onixbyte/helix/shared/CacheName.java similarity index 92% rename from src/main/java/com/onixbyte/helix/constant/CacheName.java rename to src/main/java/com/onixbyte/helix/shared/CacheName.java index 8c60a6e..607d6d7 100644 --- a/src/main/java/com/onixbyte/helix/constant/CacheName.java +++ b/src/main/java/com/onixbyte/helix/shared/CacheName.java @@ -1,4 +1,4 @@ -package com.onixbyte.helix.constant; +package com.onixbyte.helix.shared; public class CacheName { diff --git a/src/main/java/com/onixbyte/helix/constant/FileType.java b/src/main/java/com/onixbyte/helix/shared/FileType.java similarity index 91% rename from src/main/java/com/onixbyte/helix/constant/FileType.java rename to src/main/java/com/onixbyte/helix/shared/FileType.java index 45f55a4..1f9267d 100644 --- a/src/main/java/com/onixbyte/helix/constant/FileType.java +++ b/src/main/java/com/onixbyte/helix/shared/FileType.java @@ -1,4 +1,4 @@ -package com.onixbyte.helix.constant; +package com.onixbyte.helix.shared; public interface FileType { diff --git a/src/main/java/com/onixbyte/helix/constant/SettingName.java b/src/main/java/com/onixbyte/helix/shared/SettingName.java similarity index 82% rename from src/main/java/com/onixbyte/helix/constant/SettingName.java rename to src/main/java/com/onixbyte/helix/shared/SettingName.java index b539d96..9d92b64 100644 --- a/src/main/java/com/onixbyte/helix/constant/SettingName.java +++ b/src/main/java/com/onixbyte/helix/shared/SettingName.java @@ -1,4 +1,4 @@ -package com.onixbyte.helix.constant; +package com.onixbyte.helix.shared; public class SettingName { diff --git a/src/main/java/com/onixbyte/helix/constant/SecurityConstant.java b/src/main/java/com/onixbyte/helix/shared/TokenConstant.java similarity index 74% rename from src/main/java/com/onixbyte/helix/constant/SecurityConstant.java rename to src/main/java/com/onixbyte/helix/shared/TokenConstant.java index 08c3bb5..7dbd3e8 100644 --- a/src/main/java/com/onixbyte/helix/constant/SecurityConstant.java +++ b/src/main/java/com/onixbyte/helix/shared/TokenConstant.java @@ -1,6 +1,6 @@ -package com.onixbyte.helix.constant; +package com.onixbyte.helix.shared; -public class SecurityConstant { +public class TokenConstant { public static final String TOKEN_HEADER_NAME = "Authorization"; From 5406d810a64d9aaf16743a26a884151337a15092 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 11:19:48 +0800 Subject: [PATCH 13/60] =?UTF-8?q?docs:=20=E7=A7=BB=E9=99=A4=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=9A=84=20@see=20=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/onixbyte/helix/enumeration/UserStatus.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/onixbyte/helix/enumeration/UserStatus.java b/src/main/java/com/onixbyte/helix/enumeration/UserStatus.java index 957f8ba..708e623 100644 --- a/src/main/java/com/onixbyte/helix/enumeration/UserStatus.java +++ b/src/main/java/com/onixbyte/helix/enumeration/UserStatus.java @@ -14,7 +14,6 @@ package com.onixbyte.helix.enumeration; * * @author zihluwang * @since 1.0.0 - * @see com.onixbyte.helix.config.SecurityConfiguration */ public enum UserStatus { From 4ca7791e26efce0a22a61d461266978bc418ac12 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 14:19:51 +0800 Subject: [PATCH 14/60] =?UTF-8?q?refactor:=20=E5=B0=86=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E5=B8=B8=E9=87=8F=E7=B1=BB=E7=9A=84=E6=9E=84=E9=80=A0=E5=99=A8?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E4=B8=BA=E7=A7=81=E6=9C=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helix/enumeration/ExternalHost.java | 26 ------------------- .../onixbyte/helix/shared/AssetPrefix.java | 5 +++- .../com/onixbyte/helix/shared/CacheName.java | 7 +++-- .../onixbyte/helix/shared/ExternalHost.java | 13 ++++++++++ .../onixbyte/helix/shared/SettingName.java | 5 +++- .../onixbyte/helix/shared/TokenConstant.java | 5 +++- 6 files changed, 28 insertions(+), 33 deletions(-) delete mode 100644 src/main/java/com/onixbyte/helix/enumeration/ExternalHost.java create mode 100644 src/main/java/com/onixbyte/helix/shared/ExternalHost.java diff --git a/src/main/java/com/onixbyte/helix/enumeration/ExternalHost.java b/src/main/java/com/onixbyte/helix/enumeration/ExternalHost.java deleted file mode 100644 index af7ebd9..0000000 --- a/src/main/java/com/onixbyte/helix/enumeration/ExternalHost.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.onixbyte.helix.enumeration; - -/** - * Constants for external host configurations and endpoints. - *

- * This utility class provides centralised definitions for external service hosts, API endpoints, - * and third-party integration points used throughout the Helix application. It serves as a single - * source of truth for external service configurations, promoting maintainability and consistency. - *

- * The class is designed to hold static final constants representing: - *

    - *
  • External API base URLs
  • - *
  • Third-party service endpoints
  • - *
  • Integration service hosts
  • - *
  • External resource locations
  • - *
- *

- * This class cannot be instantiated as it serves purely as a constant container. - * - * @author zihluwang - * @since 1.0.0 - */ -public final class ExternalHost { - - -} diff --git a/src/main/java/com/onixbyte/helix/shared/AssetPrefix.java b/src/main/java/com/onixbyte/helix/shared/AssetPrefix.java index 2591d2e..2a44c0f 100644 --- a/src/main/java/com/onixbyte/helix/shared/AssetPrefix.java +++ b/src/main/java/com/onixbyte/helix/shared/AssetPrefix.java @@ -1,10 +1,13 @@ package com.onixbyte.helix.shared; -public class AssetPrefix { +public final class AssetPrefix { public static final String UPLOADS = "uploads"; public static final String AVATARS = "avatars"; public static final String PROFILES = "profiles"; + + private AssetPrefix() { + } } diff --git a/src/main/java/com/onixbyte/helix/shared/CacheName.java b/src/main/java/com/onixbyte/helix/shared/CacheName.java index 607d6d7..8babf0f 100644 --- a/src/main/java/com/onixbyte/helix/shared/CacheName.java +++ b/src/main/java/com/onixbyte/helix/shared/CacheName.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.shared; -public class CacheName { +public final class CacheName { public static final String USER = "user"; @@ -12,7 +12,6 @@ public class CacheName { public static final String CAPTCHA = "captcha"; - public static final String CAPTCHA_SETTING = "captcha-setting"; - - public static final String AUTH_SETTING = "auth-setting"; + private CacheName() { + } } diff --git a/src/main/java/com/onixbyte/helix/shared/ExternalHost.java b/src/main/java/com/onixbyte/helix/shared/ExternalHost.java new file mode 100644 index 0000000..5911e7b --- /dev/null +++ b/src/main/java/com/onixbyte/helix/shared/ExternalHost.java @@ -0,0 +1,13 @@ +package com.onixbyte.helix.shared; + +/** + * Constants for external host configurations and endpoints. + * + * @author zihluwang + * @since 1.0.0 + */ +public final class ExternalHost { + + private ExternalHost() { + } +} diff --git a/src/main/java/com/onixbyte/helix/shared/SettingName.java b/src/main/java/com/onixbyte/helix/shared/SettingName.java index 9d92b64..80ee4b3 100644 --- a/src/main/java/com/onixbyte/helix/shared/SettingName.java +++ b/src/main/java/com/onixbyte/helix/shared/SettingName.java @@ -1,8 +1,11 @@ package com.onixbyte.helix.shared; -public class SettingName { +public final class SettingName { public static final String CAPTCHA_ENABLED = "captcha-enabled"; public static final String REGISTER_ENABLED = "register-enabled"; + + private SettingName() { + } } diff --git a/src/main/java/com/onixbyte/helix/shared/TokenConstant.java b/src/main/java/com/onixbyte/helix/shared/TokenConstant.java index 7dbd3e8..739ad25 100644 --- a/src/main/java/com/onixbyte/helix/shared/TokenConstant.java +++ b/src/main/java/com/onixbyte/helix/shared/TokenConstant.java @@ -1,10 +1,13 @@ package com.onixbyte.helix.shared; -public class TokenConstant { +public final class TokenConstant { public static final String TOKEN_HEADER_NAME = "Authorization"; public static final String TOKEN_PREFIX = "Bearer "; public static final int TOKEN_PREFIX_LENGTH = TOKEN_PREFIX.length(); + + private TokenConstant() { + } } From d25c754bf023b52e5349fea25ec98fdbacdc526f Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 14:47:27 +0800 Subject: [PATCH 15/60] =?UTF-8?q?feat:=20=E6=9F=A5=E8=AF=A2=E6=9D=83?= =?UTF-8?q?=E9=99=90=E4=BF=A1=E6=81=AF=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helix/controller/AuthorityController.java | 33 +++++++++++++++++++ .../web/request/QueryAuthorityRequest.java | 4 +++ .../helix/manager/AuthorityManager.java | 11 ++++++- .../helix/service/AuthorityService.java | 24 ++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/onixbyte/helix/controller/AuthorityController.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/QueryAuthorityRequest.java create mode 100644 src/main/java/com/onixbyte/helix/service/AuthorityService.java diff --git a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java new file mode 100644 index 0000000..e21111d --- /dev/null +++ b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java @@ -0,0 +1,33 @@ +package com.onixbyte.helix.controller; + +import com.onixbyte.helix.domain.entity.Authority; +import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; +import com.onixbyte.helix.service.AuthorityService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/authorities") +public class AuthorityController { + + private final AuthorityService authorityService; + + public AuthorityController(AuthorityService authorityService) { + this.authorityService = authorityService; + } + + @GetMapping + public Page getAuthorities( + @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @RequestParam(required = false, defaultValue = "10") Integer pageSize, + @Validated @ModelAttribute QueryAuthorityRequest request + ) { + var pageRequest = PageRequest.of(pageNum - 1, pageSize); + return authorityService.getAuthorities(pageRequest, request); + } +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/QueryAuthorityRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/QueryAuthorityRequest.java new file mode 100644 index 0000000..27e41fb --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/QueryAuthorityRequest.java @@ -0,0 +1,4 @@ +package com.onixbyte.helix.domain.web.request; + +public record QueryAuthorityRequest() { +} diff --git a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java index 55cc481..99beb7a 100644 --- a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java @@ -1,10 +1,13 @@ package com.onixbyte.helix.manager; +import com.onixbyte.helix.repository.AuthorityRepository; import com.onixbyte.helix.shared.CacheName; import com.onixbyte.helix.domain.entity.Authority; import com.onixbyte.helix.mapper.AuthorityMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; import java.util.List; @@ -13,14 +16,20 @@ import java.util.List; public class AuthorityManager { private final AuthorityMapper authorityMapper; + private final AuthorityRepository authorityRepository; @Autowired - public AuthorityManager(AuthorityMapper authorityMapper) { + public AuthorityManager(AuthorityMapper authorityMapper, AuthorityRepository authorityRepository) { this.authorityMapper = authorityMapper; + this.authorityRepository = authorityRepository; } @Cacheable(cacheNames = CacheName.AUTHORITIES_OF_USER, key = "#userId") public List queryByUserId(Long userId) { return authorityMapper.selectByUserId(userId); } + + public Page selectAll(Pageable pageable) { + return authorityRepository.findAll(pageable); + } } diff --git a/src/main/java/com/onixbyte/helix/service/AuthorityService.java b/src/main/java/com/onixbyte/helix/service/AuthorityService.java new file mode 100644 index 0000000..eb3a8ba --- /dev/null +++ b/src/main/java/com/onixbyte/helix/service/AuthorityService.java @@ -0,0 +1,24 @@ +package com.onixbyte.helix.service; + +import com.onixbyte.helix.domain.entity.Authority; +import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; +import com.onixbyte.helix.manager.AuthorityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AuthorityService { + + private final AuthorityManager authorityManager; + + public AuthorityService(AuthorityManager authorityManager) { + this.authorityManager = authorityManager; + } + + public Page getAuthorities(Pageable pageable, QueryAuthorityRequest request) { + return authorityManager.selectAll(pageable); + } +} From 382a6c177f0f09fb90cebe0907982cfa64ad218a Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 14:55:11 +0800 Subject: [PATCH 16/60] =?UTF-8?q?refactor:=20=E4=B8=BA=20JPA=20=E5=AE=9E?= =?UTF-8?q?=E4=BD=93=E6=B7=BB=E5=8A=A0=20life=20cycle=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helix/domain/entity/Authority.java | 13 +++++++ .../helix/domain/entity/Department.java | 12 +++++++ .../onixbyte/helix/domain/entity/Menu.java | 12 +++++++ .../helix/domain/entity/Position.java | 12 +++++++ .../onixbyte/helix/domain/entity/Role.java | 8 ++--- .../helix/domain/entity/RoleAuthority.java | 5 +++ .../onixbyte/helix/domain/entity/Setting.java | 23 ++++++------ .../onixbyte/helix/domain/entity/User.java | 36 +++++++------------ .../helix/domain/entity/UserIdentity.java | 32 +++++++---------- .../helix/domain/entity/UserRole.java | 16 +++------ 10 files changed, 99 insertions(+), 70 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Authority.java b/src/main/java/com/onixbyte/helix/domain/entity/Authority.java index eab02f1..3e3240f 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Authority.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Authority.java @@ -5,6 +5,7 @@ import jakarta.persistence.*; import org.hibernate.annotations.JdbcType; import org.hibernate.dialect.PostgreSQLEnumJdbcType; import org.springframework.security.core.GrantedAuthority; + import java.time.LocalDateTime; import java.util.Objects; @@ -261,4 +262,16 @@ public class Authority { public GrantedAuthority asGrantedAuthority() { return this::getCode; } + + @PrePersist + protected void onCreate() { + var createTime = LocalDateTime.now(); + this.createdAt = createTime; + this.updatedAt = createTime; + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Department.java b/src/main/java/com/onixbyte/helix/domain/entity/Department.java index 1a68320..9a1bb1b 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Department.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Department.java @@ -287,4 +287,16 @@ public class Department implements Treeable { return new Department(id, name, parentId, sort, status, createdAt, updatedAt); } } + + @PrePersist + protected void onCreate() { + var createTime = LocalDateTime.now(); + this.createdAt = createTime; + this.updatedAt = createTime; + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Menu.java b/src/main/java/com/onixbyte/helix/domain/entity/Menu.java index 5702892..ec7bed6 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Menu.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Menu.java @@ -224,4 +224,16 @@ public class Menu implements Treeable { ", updatedAt=" + updatedAt + '}'; } + + @PrePersist + protected void onCreate() { + var createTime = LocalDateTime.now(); + this.createdAt = createTime; + this.updatedAt = createTime; + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Position.java b/src/main/java/com/onixbyte/helix/domain/entity/Position.java index 65db827..8f9b80b 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Position.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Position.java @@ -281,4 +281,16 @@ public class Position { return new Position(id, name, code, description, sort, status, createdAt, updatedAt); } } + + @PrePersist + protected void onCreate() { + var createTime = LocalDateTime.now(); + this.createdAt = createTime; + this.updatedAt = createTime; + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Role.java b/src/main/java/com/onixbyte/helix/domain/entity/Role.java index 5fa4176..1896e98 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Role.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Role.java @@ -307,10 +307,10 @@ public class Role { } @PrePersist - private void onInsert() { - var currentTime = LocalDateTime.now(); - this.createdAt = currentTime; - this.updatedAt = currentTime; + private void onCreate() { + var createTime = LocalDateTime.now(); + this.createdAt = createTime; + this.updatedAt = createTime; } @PreUpdate diff --git a/src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java b/src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java index ff876ea..bcd00d8 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java @@ -122,4 +122,9 @@ public class RoleAuthority { return new RoleAuthority(roleId, authorityId, createdAt); } } + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Setting.java b/src/main/java/com/onixbyte/helix/domain/entity/Setting.java index 69f5046..967794a 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Setting.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Setting.java @@ -76,17 +76,6 @@ public class Setting { @Column(nullable = false) private LocalDateTime updatedAt; - @PrePersist - protected void onCreate() { - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); - } - - @PreUpdate - protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); - } - public Setting() { } @@ -266,4 +255,16 @@ public class Setting { return new Setting(id, name, description, type, value, defaultValue, createdAt, updatedAt); } } + + @PrePersist + protected void onCreate() { + var createTime = LocalDateTime.now(); + this.createdAt = createTime; + this.updatedAt = createTime; + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } } \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/entity/User.java b/src/main/java/com/onixbyte/helix/domain/entity/User.java index 5d48206..f391a42 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/User.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/User.java @@ -121,30 +121,6 @@ public class User { @Column(nullable = false) private LocalDateTime updatedAt; - // --- JPA Lifecycle Callbacks for Auditing --- - - @PrePersist - protected void onCreate() { - if (this.createdAt == null) { - this.createdAt = LocalDateTime.now(); - } - if (this.updatedAt == null) { - this.updatedAt = LocalDateTime.now(); - } - - // Ensure status default is applied if not set - if (this.status == null) { - this.status = UserStatus.ACTIVE; - } - } - - @PreUpdate - protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); - } - - // --- Constructors, Getters, Setters, and Builder (omitted for brevity) --- - public User() { } @@ -450,4 +426,16 @@ public class User { return new User(id, username, password, fullName, email, regionAbbreviation, phoneNumber, avatarUrl, status, departmentId, positionId, createdAt, updatedAt); } } + + @PrePersist + protected void onCreate() { + var createTime = LocalDateTime.now(); + this.createdAt = createTime; + this.updatedAt = createTime; + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } } \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java b/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java index 87d3e45..8181256 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java @@ -40,26 +40,6 @@ public class UserIdentity { @Column(nullable = false) private LocalDateTime updatedAt; - // --- JPA Lifecycle Callbacks for Auditing --- - - @PrePersist - protected void onCreate() { - if (this.createdAt == null) { - this.createdAt = LocalDateTime.now(); - } - if (this.updatedAt == null) { - this.updatedAt = LocalDateTime.now(); - } - } - - @PreUpdate - protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); - } - - - // --- Getters and Setters (Delegating to EmbeddedId) --- - /** * Gets the identifier of the internal user account from the composite primary key. * @return the user ID @@ -224,4 +204,16 @@ public class UserIdentity { return new UserIdentity(userId, provider, externalId, createdAt, updatedAt); } } + + @PrePersist + protected void onCreate() { + var createTime = LocalDateTime.now(); + this.createdAt = createTime; + this.updatedAt = createTime; + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } } \ No newline at end of file diff --git a/src/main/java/com/onixbyte/helix/domain/entity/UserRole.java b/src/main/java/com/onixbyte/helix/domain/entity/UserRole.java index 794bf40..ec22695 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/UserRole.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/UserRole.java @@ -37,17 +37,6 @@ public class UserRole { @Column(nullable = false, updatable = false) private LocalDateTime createdAt; - // --- JPA Lifecycle Callbacks for Auditing --- - - @PrePersist - protected void onCreate() { - if (this.createdAt == null) { - this.createdAt = LocalDateTime.now(); - } - } - - // --- Getters and Setters (Delegating to EmbeddedId and fields) --- - /** * Gets the identifier of the role from the composite primary key. * @@ -173,4 +162,9 @@ public class UserRole { return new UserRole(roleId, userId, createdAt); } } + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + } } \ No newline at end of file From 9b8d276e0b73a87720df3824a8f1eda88afd688a Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 16:42:08 +0800 Subject: [PATCH 17/60] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9D=83?= =?UTF-8?q?=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helix/controller/AuthorityController.java | 6 ++++ .../query/wrapper/QueryAuthorityWrapper.java | 4 +++ .../web/request/AddAuthorityRequest.java | 15 ++++++++++ .../helix/manager/AuthorityManager.java | 23 +++++++++++++-- .../helix/service/AuthorityService.java | 28 +++++++++++++++++-- 5 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryAuthorityWrapper.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/AddAuthorityRequest.java diff --git a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java index e21111d..0a8fe29 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java @@ -1,6 +1,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Authority; +import com.onixbyte.helix.domain.web.request.AddAuthorityRequest; import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; import com.onixbyte.helix.service.AuthorityService; import org.springframework.data.domain.Page; @@ -30,4 +31,9 @@ public class AuthorityController { var pageRequest = PageRequest.of(pageNum - 1, pageSize); return authorityService.getAuthorities(pageRequest, request); } + + @PostMapping + public Authority addAuthority(@Validated @RequestBody AddAuthorityRequest request) { + return authorityService.addAuthority(request); + } } diff --git a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryAuthorityWrapper.java b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryAuthorityWrapper.java new file mode 100644 index 0000000..330bd5c --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryAuthorityWrapper.java @@ -0,0 +1,4 @@ +package com.onixbyte.helix.domain.database.query.wrapper; + +public class QueryAuthorityWrapper { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AddAuthorityRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/AddAuthorityRequest.java new file mode 100644 index 0000000..1431245 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/AddAuthorityRequest.java @@ -0,0 +1,15 @@ +package com.onixbyte.helix.domain.web.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +public record AddAuthorityRequest( + @NotNull @NotBlank(message = "权限编码不能为空") String code, + @NotNull @NotBlank(message = "权限名称不能为空") String name, + String description, + @Pattern( + regexp = "^(ACTIVE|INACTIVE)?$", + message = "状态错误") String status +) { +} diff --git a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java index 99beb7a..25971d7 100644 --- a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java @@ -1,11 +1,15 @@ package com.onixbyte.helix.manager; -import com.onixbyte.helix.repository.AuthorityRepository; -import com.onixbyte.helix.shared.CacheName; +import com.onixbyte.helix.domain.database.query.wrapper.QueryAuthorityWrapper; import com.onixbyte.helix.domain.entity.Authority; import com.onixbyte.helix.mapper.AuthorityMapper; +import com.onixbyte.helix.repository.AuthorityRepository; +import com.onixbyte.helix.shared.CacheName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; @@ -15,6 +19,7 @@ import java.util.List; @Component public class AuthorityManager { + private static final Logger log = LoggerFactory.getLogger(AuthorityManager.class); private final AuthorityMapper authorityMapper; private final AuthorityRepository authorityRepository; @@ -29,7 +34,19 @@ public class AuthorityManager { return authorityMapper.selectByUserId(userId); } - public Page selectAll(Pageable pageable) { + public Page selectAll(Pageable pageable, QueryAuthorityWrapper wrapper) { return authorityRepository.findAll(pageable); } + + public boolean existsByCode(Authority authority) { + return authorityRepository.exists(Example.of(Authority + .builder() + .code(authority.getCode()) + .build() + )); + } + + public Authority save(Authority authority) { + return authorityRepository.save(authority); + } } diff --git a/src/main/java/com/onixbyte/helix/service/AuthorityService.java b/src/main/java/com/onixbyte/helix/service/AuthorityService.java index eb3a8ba..b6eb3ce 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthorityService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthorityService.java @@ -1,13 +1,18 @@ package com.onixbyte.helix.service; +import com.onixbyte.helix.domain.database.query.wrapper.QueryAuthorityWrapper; import com.onixbyte.helix.domain.entity.Authority; +import com.onixbyte.helix.domain.web.request.AddAuthorityRequest; import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; +import com.onixbyte.helix.enumeration.Status; +import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.AuthorityManager; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import java.util.List; +import java.util.Optional; @Service public class AuthorityService { @@ -19,6 +24,25 @@ public class AuthorityService { } public Page getAuthorities(Pageable pageable, QueryAuthorityRequest request) { - return authorityManager.selectAll(pageable); + var wrapper = new QueryAuthorityWrapper(); + + return authorityManager.selectAll(pageable, wrapper); + } + + public Authority addAuthority(AddAuthorityRequest request) { + var authority = Authority.builder() + .code(request.code()) + .name(request.name()) + .description(request.description()) + .status(Optional.ofNullable(request.status()) + .map(Status::valueOf) + .orElse(Status.ACTIVE)) + .build(); + + if (authorityManager.existsByCode(authority)) { + throw new BizException(HttpStatus.CONFLICT, "权限编码 `" + authority.getCode() + "` 已被使用"); + } + + return authorityManager.save(authority); } } From a5da09cf0943a756b56327e1405a91d108ff3382 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 17:00:45 +0800 Subject: [PATCH 18/60] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E6=9D=83?= =?UTF-8?q?=E9=99=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helix/controller/AuthController.java | 4 ++-- .../helix/controller/AuthorityController.java | 6 +++++ .../helix/controller/UserController.java | 4 ++-- .../web/request/EditAuthorityRequest.java | 14 +++++++++++ ...eUserRequest.java => EditUserRequest.java} | 2 +- .../domain/web/request/LoginRequest.java | 17 ++++++++++++++ .../request/UsernamePasswordLoginRequest.java | 9 -------- .../helix/manager/AuthorityManager.java | 23 +++++++++++++++++++ .../onixbyte/helix/service/AuthService.java | 4 ++-- .../helix/service/AuthorityService.java | 5 ++++ .../onixbyte/helix/service/UserService.java | 4 ++-- 11 files changed, 74 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/EditAuthorityRequest.java rename src/main/java/com/onixbyte/helix/domain/web/request/{UpdateUserRequest.java => EditUserRequest.java} (94%) create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/LoginRequest.java delete mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/UsernamePasswordLoginRequest.java diff --git a/src/main/java/com/onixbyte/helix/controller/AuthController.java b/src/main/java/com/onixbyte/helix/controller/AuthController.java index defeb02..36d4ed2 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthController.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.controller; -import com.onixbyte.helix.domain.web.request.UsernamePasswordLoginRequest; +import com.onixbyte.helix.domain.web.request.LoginRequest; import com.onixbyte.helix.domain.web.response.LoginSuccessResponse; import com.onixbyte.helix.service.AuthService; import org.slf4j.Logger; @@ -29,7 +29,7 @@ public class AuthController { */ @PostMapping("/login") public LoginSuccessResponse loginWithUsernameAndPassword( - @Validated @RequestBody UsernamePasswordLoginRequest request + @Validated @RequestBody LoginRequest request ) { return authService.login(request); } diff --git a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java index 0a8fe29..31e2a5a 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java @@ -2,6 +2,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Authority; import com.onixbyte.helix.domain.web.request.AddAuthorityRequest; +import com.onixbyte.helix.domain.web.request.EditAuthorityRequest; import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; import com.onixbyte.helix.service.AuthorityService; import org.springframework.data.domain.Page; @@ -36,4 +37,9 @@ public class AuthorityController { public Authority addAuthority(@Validated @RequestBody AddAuthorityRequest request) { return authorityService.addAuthority(request); } + + @PutMapping + public Authority editAuthority(@Validated @RequestBody EditAuthorityRequest request) { + return authorityService.editAuthority(request); + } } diff --git a/src/main/java/com/onixbyte/helix/controller/UserController.java b/src/main/java/com/onixbyte/helix/controller/UserController.java index e64aed2..6ab71f0 100644 --- a/src/main/java/com/onixbyte/helix/controller/UserController.java +++ b/src/main/java/com/onixbyte/helix/controller/UserController.java @@ -3,7 +3,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.web.request.AddUserRequest; import com.onixbyte.helix.domain.web.request.QueryUserRequest; import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; -import com.onixbyte.helix.domain.web.request.UpdateUserRequest; +import com.onixbyte.helix.domain.web.request.EditUserRequest; import com.onixbyte.helix.domain.web.response.UserDetailResponse; import com.onixbyte.helix.service.UserService; import org.springframework.beans.factory.annotation.Autowired; @@ -64,7 +64,7 @@ public class UserController { } @PutMapping - public ResponseEntity editUser(@Validated @RequestBody UpdateUserRequest request) { + public ResponseEntity editUser(@Validated @RequestBody EditUserRequest request) { userService.updateUser(request); return ResponseEntity.ok(null); } diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/EditAuthorityRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/EditAuthorityRequest.java new file mode 100644 index 0000000..d71e437 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/EditAuthorityRequest.java @@ -0,0 +1,14 @@ +package com.onixbyte.helix.domain.web.request; + +import jakarta.validation.constraints.Pattern; + +public record EditAuthorityRequest( + Long id, + String name, + String description, + @Pattern( + regexp = "^(ACTIVE|INACTIVE)?$", + message = "状态参数错误") + String status +) { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/UpdateUserRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/EditUserRequest.java similarity index 94% rename from src/main/java/com/onixbyte/helix/domain/web/request/UpdateUserRequest.java rename to src/main/java/com/onixbyte/helix/domain/web/request/EditUserRequest.java index 55bfb05..c294023 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/UpdateUserRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/EditUserRequest.java @@ -4,7 +4,7 @@ import com.onixbyte.helix.enumeration.UserStatus; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; -public record UpdateUserRequest( +public record EditUserRequest( @NotNull(message = "User ID cannot be null") @Positive(message = "User ID must be positive") Long id, diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/LoginRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/LoginRequest.java new file mode 100644 index 0000000..49a7bfb --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/LoginRequest.java @@ -0,0 +1,17 @@ +package com.onixbyte.helix.domain.web.request; + +/** + * Login data. + * + * @param username username + * @param password password + * @param uuid captcha uuid + * @param captcha captcha code + */ +public record LoginRequest( + String username, + String password, + String uuid, + String captcha +) { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/UsernamePasswordLoginRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/UsernamePasswordLoginRequest.java deleted file mode 100644 index 8e95e11..0000000 --- a/src/main/java/com/onixbyte/helix/domain/web/request/UsernamePasswordLoginRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.onixbyte.helix.domain.web.request; - -public record UsernamePasswordLoginRequest( - String username, - String password, - String uuid, - String captcha -) { -} diff --git a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java index 25971d7..b064e86 100644 --- a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java @@ -2,6 +2,9 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.database.query.wrapper.QueryAuthorityWrapper; import com.onixbyte.helix.domain.entity.Authority; +import com.onixbyte.helix.domain.web.request.EditAuthorityRequest; +import com.onixbyte.helix.enumeration.Status; +import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.mapper.AuthorityMapper; import com.onixbyte.helix.repository.AuthorityRepository; import com.onixbyte.helix.shared.CacheName; @@ -12,9 +15,12 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Optional; @Component public class AuthorityManager { @@ -49,4 +55,21 @@ public class AuthorityManager { public Authority save(Authority authority) { return authorityRepository.save(authority); } + + @Transactional + public Authority update(EditAuthorityRequest request) { + var authority = authorityRepository.findById(request.id()) + .orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, "找不到指定的权限信息")); + + Optional.ofNullable(request.name()) + .ifPresent(authority::setName); + + authority.setDescription(request.description()); + + Optional.ofNullable(request.status()) + .map(Status::valueOf) + .ifPresent(authority::setStatus); + + return authority; + } } diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index d272cec..ff972c9 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -3,7 +3,7 @@ package com.onixbyte.helix.service; import com.onixbyte.helix.client.TokenClient; import com.onixbyte.helix.shared.SettingName; import com.onixbyte.helix.domain.entity.Setting; -import com.onixbyte.helix.domain.web.request.UsernamePasswordLoginRequest; +import com.onixbyte.helix.domain.web.request.LoginRequest; import com.onixbyte.helix.domain.web.response.LoginSuccessResponse; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.CaptchaManager; @@ -49,7 +49,7 @@ public class AuthService { * @return user information and user identity token * @throws BizException if the user does not exist, or the username and password are incorrect */ - public LoginSuccessResponse login(UsernamePasswordLoginRequest request) { + public LoginSuccessResponse login(LoginRequest request) { var captchaEnabled = Optional.ofNullable(settingManager.getSettingByName(SettingName.CAPTCHA_ENABLED)) .map(Setting::asBoolean) .orElse(false); diff --git a/src/main/java/com/onixbyte/helix/service/AuthorityService.java b/src/main/java/com/onixbyte/helix/service/AuthorityService.java index b6eb3ce..d8d1bdb 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthorityService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthorityService.java @@ -3,6 +3,7 @@ package com.onixbyte.helix.service; import com.onixbyte.helix.domain.database.query.wrapper.QueryAuthorityWrapper; import com.onixbyte.helix.domain.entity.Authority; import com.onixbyte.helix.domain.web.request.AddAuthorityRequest; +import com.onixbyte.helix.domain.web.request.EditAuthorityRequest; import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.exception.BizException; @@ -45,4 +46,8 @@ public class AuthorityService { return authorityManager.save(authority); } + + public Authority editAuthority(EditAuthorityRequest request) { + return authorityManager.update(request); + } } diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index 8667811..5284a1e 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -9,7 +9,7 @@ import com.onixbyte.helix.domain.entity.UserRole; import com.onixbyte.helix.domain.web.request.AddUserRequest; import com.onixbyte.helix.domain.web.request.QueryUserRequest; import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; -import com.onixbyte.helix.domain.web.request.UpdateUserRequest; +import com.onixbyte.helix.domain.web.request.EditUserRequest; import com.onixbyte.helix.domain.web.response.UserDetailResponse; import com.onixbyte.helix.manager.ApplicationManager; import com.onixbyte.helix.manager.RoleManager; @@ -134,7 +134,7 @@ public class UserService { } @Transactional(rollbackFor = Throwable.class) - public void updateUser(UpdateUserRequest request) { + public void updateUser(EditUserRequest request) { userManager.updateUser(User.builder() .id(request.id()) .fullName(request.fullName()) From 2db2b594f6e021e4f224b013c7595c2adc9a9772 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 17:47:34 +0800 Subject: [PATCH 19/60] =?UTF-8?q?refactor:=20=E4=B8=BA=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=8A=9F=E8=83=BD=E6=B7=BB=E5=8A=A0=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helix/controller/UserController.java | 5 ++-- .../web/response/UserDetailResponse.java | 17 ++++++++++++ .../helix/manager/DepartmentManager.java | 6 +++-- .../helix/manager/PositionManager.java | 4 +++ .../onixbyte/helix/service/UserService.java | 26 ++++++++++++++----- 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/controller/UserController.java b/src/main/java/com/onixbyte/helix/controller/UserController.java index 6ab71f0..00f9743 100644 --- a/src/main/java/com/onixbyte/helix/controller/UserController.java +++ b/src/main/java/com/onixbyte/helix/controller/UserController.java @@ -58,9 +58,8 @@ public class UserController { @PostMapping @PreAuthorize("hasAnyAuthority('system:user:write')") - public ResponseEntity addUser(@Validated @RequestBody AddUserRequest request) { - userService.addUser(request); - return ResponseEntity.ok(null); + public UserDetailResponse addUser(@Validated @RequestBody AddUserRequest request) { + return userService.addUser(request); } @PutMapping diff --git a/src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java b/src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java index a709218..78dd131 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java +++ b/src/main/java/com/onixbyte/helix/domain/web/response/UserDetailResponse.java @@ -1,5 +1,6 @@ package com.onixbyte.helix.domain.web.response; +import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.enumeration.UserStatus; import java.time.LocalDateTime; @@ -175,6 +176,22 @@ public class UserDetailResponse { private UserDetailResponseBuilder() { } + public UserDetailResponseBuilder user(User user) { + this.id = String.valueOf(user.getId()); + this.username = user.getUsername(); + this.fullName = user.getFullName(); + this.email = user.getEmail(); + this.regionAbbreviation = user.getRegionAbbreviation(); + this.phoneNumber = user.getPhoneNumber(); + this.avatarUrl = user.getAvatarUrl(); + this.status = user.getStatus(); + this.departmentId = user.getDepartmentId(); + this.positionId = user.getPositionId(); + this.createdAt = user.getCreatedAt(); + this.updatedAt = user.getUpdatedAt(); + return this; + } + public UserDetailResponseBuilder id(String id) { this.id = id; return this; diff --git a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java index bc678b7..1c88c20 100644 --- a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java +++ b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java @@ -8,8 +8,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; -import java.util.*; - @Component public class DepartmentManager { @@ -25,4 +23,8 @@ public class DepartmentManager { public Page selectAll(Pageable pageable) { return departmentRepository.findAll(pageable); } + + public Department selectById(Long id) { + return departmentRepository.findById(id).orElse(null); + } } diff --git a/src/main/java/com/onixbyte/helix/manager/PositionManager.java b/src/main/java/com/onixbyte/helix/manager/PositionManager.java index 3f4c46f..c3a0349 100644 --- a/src/main/java/com/onixbyte/helix/manager/PositionManager.java +++ b/src/main/java/com/onixbyte/helix/manager/PositionManager.java @@ -23,4 +23,8 @@ public class PositionManager { public Page selectAll(Pageable pageable) { return positionRepository.findAll(pageable); } + + public Position selectById(Long id) { + return positionRepository.findById(id).orElse(null); + } } diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index 5284a1e..1e9bc65 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -11,10 +11,7 @@ import com.onixbyte.helix.domain.web.request.QueryUserRequest; import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; import com.onixbyte.helix.domain.web.request.EditUserRequest; import com.onixbyte.helix.domain.web.response.UserDetailResponse; -import com.onixbyte.helix.manager.ApplicationManager; -import com.onixbyte.helix.manager.RoleManager; -import com.onixbyte.helix.manager.UserManager; -import com.onixbyte.helix.manager.UserRoleManager; +import com.onixbyte.helix.manager.*; import com.onixbyte.identitygenerator.IdentityGenerator; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -36,6 +33,8 @@ public class UserService { private final UserRoleManager userRoleManager; private final PasswordEncoder passwordEncoder; private final ApplicationManager applicationManager; + private final DepartmentManager departmentManager; + private final PositionManager positionManager; @Autowired public UserService( @@ -44,14 +43,16 @@ public class UserService { RoleManager roleManager, UserRoleManager userRoleManager, PasswordEncoder passwordEncoder, - ApplicationManager applicationManager - ) { + ApplicationManager applicationManager, + DepartmentManager departmentManager, PositionManager positionManager) { this.userManager = userManager; this.userIdentityGenerator = userIdentityGenerator; this.roleManager = roleManager; this.userRoleManager = userRoleManager; this.passwordEncoder = passwordEncoder; this.applicationManager = applicationManager; + this.departmentManager = departmentManager; + this.positionManager = positionManager; } public Page queryUserDetailsPage(Pageable pageable, QueryUserRequest request) { @@ -71,7 +72,7 @@ public class UserService { } @Transactional(rollbackFor = Throwable.class) - public void addUser(AddUserRequest request) { + public UserDetailResponse addUser(AddUserRequest request) { var createTime = LocalDateTime.now(); // validate all roles are existed @@ -131,6 +132,17 @@ public class UserService { // Save user and role bindings userRoleManager.saveBatch(userRoleBindings); + + // Get department and position + var department = departmentManager.selectById(user.getDepartmentId()); + var position = positionManager.selectById(user.getPositionId()); + + // Build response and return + return UserDetailResponse.builder() + .user(user) + .departmentName(department.getName()) + .positionName(position.getName()) + .build(); } @Transactional(rollbackFor = Throwable.class) From 8d148f88d0f742124e651abd0314496c7df57255 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 31 Dec 2025 17:48:28 +0800 Subject: [PATCH 20/60] =?UTF-8?q?style:=20=E4=BF=AE=E6=94=B9=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/onixbyte/helix/service/UserService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index 1e9bc65..b9f13a6 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -44,7 +44,9 @@ public class UserService { UserRoleManager userRoleManager, PasswordEncoder passwordEncoder, ApplicationManager applicationManager, - DepartmentManager departmentManager, PositionManager positionManager) { + DepartmentManager departmentManager, + PositionManager positionManager + ) { this.userManager = userManager; this.userIdentityGenerator = userIdentityGenerator; this.roleManager = roleManager; From 0a10c64278a16f2324f563692c73406d3a976f23 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Mon, 5 Jan 2026 23:53:31 +0800 Subject: [PATCH 21/60] =?UTF-8?q?refactor:=20=E8=B0=83=E6=95=B4=20Token=20?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1 --- .../helix/config/AuthenticationConfig.java | 9 +++-- .../helix/controller/AuthController.java | 29 ++++++++++++--- .../helix/manager/ApplicationManager.java | 21 ++++++++++- .../properties/ApplicationProperties.java | 3 +- .../properties/AuthenticationProperties.java | 11 ++++++ .../onixbyte/helix/service/AuthService.java | 36 ++++++++++++------- .../onixbyte/helix/service/TokenService.java | 24 +++++++++++++ .../onixbyte/helix/service/UserService.java | 11 ++++++ .../onixbyte/helix/shared/TokenConstant.java | 2 ++ 9 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/onixbyte/helix/properties/AuthenticationProperties.java create mode 100644 src/main/java/com/onixbyte/helix/service/TokenService.java diff --git a/src/main/java/com/onixbyte/helix/config/AuthenticationConfig.java b/src/main/java/com/onixbyte/helix/config/AuthenticationConfig.java index d5aff8a..3fb80d9 100644 --- a/src/main/java/com/onixbyte/helix/config/AuthenticationConfig.java +++ b/src/main/java/com/onixbyte/helix/config/AuthenticationConfig.java @@ -1,6 +1,7 @@ package com.onixbyte.helix.config; import com.onixbyte.helix.properties.ApplicationProperties; +import com.onixbyte.helix.properties.AuthenticationProperties; import com.onixbyte.helix.properties.MsalProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -18,11 +19,15 @@ import org.springframework.context.annotation.Configuration; * application context. * * @author zihluwang - * @since 1.0.0 * @see MsalProperties * @see EnableConfigurationProperties + * @since 1.0.0 */ @Configuration -@EnableConfigurationProperties({MsalProperties.class, ApplicationProperties.class}) +@EnableConfigurationProperties({ + MsalProperties.class, + AuthenticationProperties.class, + ApplicationProperties.class +}) public class AuthenticationConfig { } diff --git a/src/main/java/com/onixbyte/helix/controller/AuthController.java b/src/main/java/com/onixbyte/helix/controller/AuthController.java index 36d4ed2..4a3153a 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthController.java @@ -1,11 +1,17 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.web.request.LoginRequest; -import com.onixbyte.helix.domain.web.response.LoginSuccessResponse; +import com.onixbyte.helix.domain.web.response.UserDetailResponse; import com.onixbyte.helix.service.AuthService; +import com.onixbyte.helix.service.TokenService; +import com.onixbyte.helix.service.UserService; +import com.onixbyte.helix.shared.TokenConstant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -15,10 +21,18 @@ public class AuthController { private static final Logger log = LoggerFactory.getLogger(AuthController.class); private final AuthService authService; + private final TokenService tokenService; + private final UserService userService; @Autowired - public AuthController(AuthService authService) { + public AuthController( + AuthService authService, + TokenService tokenService, + UserService userService + ) { this.authService = authService; + this.tokenService = tokenService; + this.userService = userService; } /** @@ -28,10 +42,17 @@ public class AuthController { * @return detailed user info and authentication token */ @PostMapping("/login") - public LoginSuccessResponse loginWithUsernameAndPassword( + public ResponseEntity loginWithUsernameAndPassword( @Validated @RequestBody LoginRequest request ) { - return authService.login(request); + var user = authService.login(request); + var token = tokenService.generateToken(user); + + var cookie = authService.buildCookie(TokenConstant.TOKEN_NAME, token); + + return ResponseEntity.status(HttpStatus.OK) + .header(HttpHeaders.SET_COOKIE, cookie.toString()) + .body(userService.getDetail(user)); } @GetMapping("/register-enabled") diff --git a/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java b/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java index 813cf8d..d1b3a27 100644 --- a/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java +++ b/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java @@ -1,20 +1,39 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.properties.ApplicationProperties; +import com.onixbyte.helix.properties.AuthenticationProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.Optional; + @Component public class ApplicationManager { private final ApplicationProperties applicationProperties; + private final AuthenticationProperties authenticationProperties; @Autowired - public ApplicationManager(ApplicationProperties applicationProperties) { + public ApplicationManager(ApplicationProperties applicationProperties, AuthenticationProperties authenticationProperties) { this.applicationProperties = applicationProperties; + this.authenticationProperties = authenticationProperties; } public String getDefaultEmail() { return applicationProperties.defaultEmail(); } + + public String getExternalHost() { + return applicationProperties.externalHost(); + } + + public boolean isSslEnabled() { + return Optional.ofNullable(authenticationProperties.sslEnabled()) + .orElse(false); + } + + public boolean isSecureCookieEnabled() { + return Optional.ofNullable(authenticationProperties.secureCookieEnabled()) + .orElse(false); + } } diff --git a/src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java b/src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java index 1ce7d27..2a725a4 100644 --- a/src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java +++ b/src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java @@ -5,6 +5,7 @@ import org.springframework.boot.context.properties.bind.DefaultValue; @ConfigurationProperties(prefix = "app.common") public record ApplicationProperties( - @DefaultValue("default@helix.onixbyte.dev") String defaultEmail + @DefaultValue("default@helix.onixbyte.dev") String defaultEmail, + @DefaultValue("helix.onixbyte.dev") String externalHost ) { } diff --git a/src/main/java/com/onixbyte/helix/properties/AuthenticationProperties.java b/src/main/java/com/onixbyte/helix/properties/AuthenticationProperties.java new file mode 100644 index 0000000..fca1673 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/properties/AuthenticationProperties.java @@ -0,0 +1,11 @@ +package com.onixbyte.helix.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; + +@ConfigurationProperties(prefix = "app.auth") +public record AuthenticationProperties( + @DefaultValue("true") Boolean sslEnabled, + @DefaultValue("true") Boolean secureCookieEnabled +) { +} diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index ff972c9..c6ebcc1 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -1,18 +1,17 @@ package com.onixbyte.helix.service; -import com.onixbyte.helix.client.TokenClient; -import com.onixbyte.helix.shared.SettingName; import com.onixbyte.helix.domain.entity.Setting; +import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.domain.web.request.LoginRequest; -import com.onixbyte.helix.domain.web.response.LoginSuccessResponse; import com.onixbyte.helix.exception.BizException; -import com.onixbyte.helix.manager.CaptchaManager; -import com.onixbyte.helix.manager.SettingManager; +import com.onixbyte.helix.manager.*; import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; +import com.onixbyte.helix.shared.SettingName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.stereotype.Service; @@ -26,20 +25,20 @@ public class AuthService { private final CaptchaManager captchaManager; private final AuthenticationManager authenticationManager; - private final TokenClient tokenClient; private final SettingManager settingManager; + private final ApplicationManager applicationManager; @Autowired public AuthService( CaptchaManager captchaManager, AuthenticationManager authenticationManager, - TokenClient tokenClient, - SettingManager settingManager + SettingManager settingManager, + ApplicationManager applicationManager ) { this.captchaManager = captchaManager; this.authenticationManager = authenticationManager; - this.tokenClient = tokenClient; this.settingManager = settingManager; + this.applicationManager = applicationManager; } /** @@ -49,7 +48,7 @@ public class AuthService { * @return user information and user identity token * @throws BizException if the user does not exist, or the username and password are incorrect */ - public LoginSuccessResponse login(LoginRequest request) { + public User login(LoginRequest request) { var captchaEnabled = Optional.ofNullable(settingManager.getSettingByName(SettingName.CAPTCHA_ENABLED)) .map(Setting::asBoolean) .orElse(false); @@ -76,9 +75,7 @@ public class AuthService { "Cannot perform login due to server crashes."); } - var token = tokenClient.generateToken(authentication.getDetails()); - - return new LoginSuccessResponse(token, authentication.getDetails()); + return authentication.getDetails(); } /** @@ -91,4 +88,17 @@ public class AuthService { .map(Setting::asBoolean) .orElse(false); } + + public ResponseCookie buildCookie(String cookieName, String token) { + var cookieBuilder = ResponseCookie.from(cookieName, token) + .httpOnly(true) + .secure(applicationManager.isSslEnabled()) + .path("/"); + + if (applicationManager.isSecureCookieEnabled()) { + cookieBuilder.domain(applicationManager.getExternalHost()); + } + + return cookieBuilder.build(); + } } diff --git a/src/main/java/com/onixbyte/helix/service/TokenService.java b/src/main/java/com/onixbyte/helix/service/TokenService.java new file mode 100644 index 0000000..5c5a214 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/service/TokenService.java @@ -0,0 +1,24 @@ +package com.onixbyte.helix.service; + +import com.auth0.jwt.interfaces.DecodedJWT; +import com.onixbyte.helix.client.TokenClient; +import com.onixbyte.helix.domain.entity.User; +import org.springframework.stereotype.Service; + +@Service +public class TokenService { + + private final TokenClient tokenClient; + + public TokenService(TokenClient tokenClient) { + this.tokenClient = tokenClient; + } + + public String generateToken(User user) { + return tokenClient.generateToken(user); + } + + public DecodedJWT verifyToken(String token) { + return tokenClient.verifyToken(token); + } +} diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index b9f13a6..361aa7d 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -179,4 +179,15 @@ public class UserService { userRoleManager.deleteByUserId(userId); userManager.deleteById(userId); } + + public UserDetailResponse getDetail(User user) { + var department = departmentManager.selectById(user.getDepartmentId()); + var position = positionManager.selectById(user.getPositionId()); + + return UserDetailResponse.builder() + .user(user) + .departmentName(department.getName()) + .positionName(position.getName()) + .build(); + } } diff --git a/src/main/java/com/onixbyte/helix/shared/TokenConstant.java b/src/main/java/com/onixbyte/helix/shared/TokenConstant.java index 739ad25..c4e8d59 100644 --- a/src/main/java/com/onixbyte/helix/shared/TokenConstant.java +++ b/src/main/java/com/onixbyte/helix/shared/TokenConstant.java @@ -4,6 +4,8 @@ public final class TokenConstant { public static final String TOKEN_HEADER_NAME = "Authorization"; + public static final String TOKEN_NAME = "AccessToken"; + public static final String TOKEN_PREFIX = "Bearer "; public static final int TOKEN_PREFIX_LENGTH = TOKEN_PREFIX.length(); From b9fee3787aa7b4b74f509a31b330aa0ff361ba39 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Mon, 5 Jan 2026 23:59:05 +0800 Subject: [PATCH 22/60] =?UTF-8?q?feat:=20=E5=BF=BD=E7=95=A5=20cookie=20sec?= =?UTF-8?q?ure=20=E5=8F=8A=20domain=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/onixbyte/helix/service/AuthService.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index c6ebcc1..42a3de0 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -92,13 +92,8 @@ public class AuthService { public ResponseCookie buildCookie(String cookieName, String token) { var cookieBuilder = ResponseCookie.from(cookieName, token) .httpOnly(true) - .secure(applicationManager.isSslEnabled()) .path("/"); - if (applicationManager.isSecureCookieEnabled()) { - cookieBuilder.domain(applicationManager.getExternalHost()); - } - return cookieBuilder.build(); } } From a526ac795d5909d354a500b29f0bc8b971842314 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Tue, 6 Jan 2026 00:07:15 +0800 Subject: [PATCH 23/60] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=80=80?= =?UTF-8?q?=E5=87=BA=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helix/controller/AuthController.java | 10 ++++++++ .../helix/manager/SecurityManager.java | 20 +++++++++++++++ .../onixbyte/helix/service/AuthService.java | 25 ++++++++++++++++--- 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/onixbyte/helix/manager/SecurityManager.java diff --git a/src/main/java/com/onixbyte/helix/controller/AuthController.java b/src/main/java/com/onixbyte/helix/controller/AuthController.java index 4a3153a..98b4d90 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthController.java @@ -15,6 +15,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.time.Duration; + @RestController @RequestMapping("/auth") public class AuthController { @@ -59,4 +61,12 @@ public class AuthController { public boolean getRegisterEnabled() { return authService.getRegisterEnabled(); } + + @GetMapping("/logout") + public ResponseEntity logout() { + var cookie = authService.buildCookie(TokenConstant.TOKEN_NAME, "", Duration.ZERO); + return ResponseEntity.status(HttpStatus.OK) + .header(HttpHeaders.SET_COOKIE, cookie.toString()) + .body(null); + } } diff --git a/src/main/java/com/onixbyte/helix/manager/SecurityManager.java b/src/main/java/com/onixbyte/helix/manager/SecurityManager.java new file mode 100644 index 0000000..f10b9fb --- /dev/null +++ b/src/main/java/com/onixbyte/helix/manager/SecurityManager.java @@ -0,0 +1,20 @@ +package com.onixbyte.helix.manager; + +import com.onixbyte.helix.properties.TokenProperties; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +public class SecurityManager { + + private final TokenProperties tokenProperties; + + public SecurityManager(TokenProperties tokenProperties) { + this.tokenProperties = tokenProperties; + } + + public Duration getTokenValidDuration() { + return tokenProperties.validTime(); + } +} diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index 42a3de0..c36ae7f 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -1,10 +1,14 @@ package com.onixbyte.helix.service; +import com.onixbyte.helix.client.TokenClient; import com.onixbyte.helix.domain.entity.Setting; import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.domain.web.request.LoginRequest; import com.onixbyte.helix.exception.BizException; -import com.onixbyte.helix.manager.*; +import com.onixbyte.helix.manager.ApplicationManager; +import com.onixbyte.helix.manager.CaptchaManager; +import com.onixbyte.helix.manager.SecurityManager; +import com.onixbyte.helix.manager.SettingManager; import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; import com.onixbyte.helix.shared.SettingName; import org.slf4j.Logger; @@ -15,6 +19,7 @@ import org.springframework.http.ResponseCookie; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.stereotype.Service; +import java.time.Duration; import java.util.Objects; import java.util.Optional; @@ -27,18 +32,22 @@ public class AuthService { private final AuthenticationManager authenticationManager; private final SettingManager settingManager; private final ApplicationManager applicationManager; + private final TokenClient tokenClient; + private final SecurityManager securityManager; @Autowired public AuthService( CaptchaManager captchaManager, AuthenticationManager authenticationManager, SettingManager settingManager, - ApplicationManager applicationManager - ) { + ApplicationManager applicationManager, + TokenClient tokenClient, SecurityManager securityManager) { this.captchaManager = captchaManager; this.authenticationManager = authenticationManager; this.settingManager = settingManager; this.applicationManager = applicationManager; + this.tokenClient = tokenClient; + this.securityManager = securityManager; } /** @@ -92,6 +101,16 @@ public class AuthService { public ResponseCookie buildCookie(String cookieName, String token) { var cookieBuilder = ResponseCookie.from(cookieName, token) .httpOnly(true) + .maxAge(securityManager.getTokenValidity()) + .path("/"); + + return cookieBuilder.build(); + } + + public ResponseCookie buildCookie(String cookieName, String token, Duration validDuration) { + var cookieBuilder = ResponseCookie.from(cookieName, token) + .httpOnly(true) + .maxAge(validDuration) .path("/"); return cookieBuilder.build(); From a8c349bd751ff959d2029d501a3c932f6e037c0a Mon Sep 17 00:00:00 2001 From: zihluwang Date: Fri, 9 Jan 2026 09:17:01 +0800 Subject: [PATCH 24/60] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20Javadoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/application-prod.yml.example | 8 +-- .../onixbyte/helix/config/WebFluxConfig.java | 57 ------------------- .../helix/controller/AssetController.java | 5 +- .../helix/controller/AuthController.java | 16 ++++++ .../helix/controller/AuthorityController.java | 29 +++++++++- .../helix/controller/CaptchaController.java | 11 ++++ .../controller/DepartmentController.java | 6 ++ .../helix/controller/ExceptionController.java | 16 +----- .../helix/controller/MenuController.java | 11 ++++ .../helix/controller/PositionController.java | 13 +++++ .../helix/controller/RoleController.java | 6 ++ .../helix/controller/SettingController.java | 9 ++- .../helix/controller/UserController.java | 41 +++++++++++-- .../domain/web/response/ActionResponse.java | 15 +++++ 14 files changed, 154 insertions(+), 89 deletions(-) delete mode 100644 src/main/java/com/onixbyte/helix/config/WebFluxConfig.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/response/ActionResponse.java diff --git a/config/application-prod.yml.example b/config/application-prod.yml.example index 101e908..f5b3e37 100644 --- a/config/application-prod.yml.example +++ b/config/application-prod.yml.example @@ -26,9 +26,9 @@ app: # 是否开启 S3 文件存储服务 enabled: true # S3 服务端点(若使用非 AWS 提供的 S3 兼容 API,请添加该配置) -# endpoint: https://endpoint.s3.service + # endpoint: https://endpoint.s3.service # S3 服务区域(详情请见 S3 服务提供商) -# region: apac + # region: apac # 公开域名 public-host: https://s3.my.app # 是否开启 Path Style @@ -64,8 +64,8 @@ app: # 允许的请求头列表 allowed-headers: Content-Type # 允许的请求方法列表(Ref org.springframework.http.HttpMethod) - # 2025.11.6注 - # 由于 Spring 解析问题,在此处使用小写的情况下会导致在请求头中存在 Origin 时出现 Invalid CORS Request 的问题,请务必使用大写 + # Spring HttpMethod 并非 enum class,因此在此处使用小写请求方式会导致在请求头中存在 Origin 时出现 Invalid + # CORS Request 的问题,请务必使用大写 allowed-methods: - GET - POST diff --git a/src/main/java/com/onixbyte/helix/config/WebFluxConfig.java b/src/main/java/com/onixbyte/helix/config/WebFluxConfig.java deleted file mode 100644 index 6dae4eb..0000000 --- a/src/main/java/com/onixbyte/helix/config/WebFluxConfig.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.onixbyte.helix.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * Configuration class for Spring WebFlux reactive web components. - *

- * This configuration class provides beans for reactive web programming components within the Helix - * application. It configures WebClient instances and other reactive web-related components that - * enable non-blocking, asynchronous HTTP communication with external services. - *

- * The configuration supports: - *

    - *
  • Reactive HTTP client configuration
  • - *
  • Non-blocking I/O operations
  • - *
  • Asynchronous request/response handling
  • - *
- * - * @author zihluwang - * @since 1.0.0 - * @see WebClient - * @see Configuration - */ -@Configuration -public class WebFluxConfig { - - /** - * Creates a reactive WebClient for HTTP communication with external services. - *

- * This method configures a {@link WebClient} instance that provides a modern, reactive approach - * to HTTP client operations. The WebClient supports non-blocking I/O and is built on top of - * Reactor Netty, making it suitable for high-performance, scalable applications. - *

- * The client is configured with default settings and can be used throughout the application for - * making HTTP requests to external APIs, microservices, or other web resources in a reactive, - * non-blocking manner. - *

- * Key features: - *

    - *
  • Non-blocking I/O operations
  • - *
  • Reactive streams support
  • - *
  • Built-in support for JSON serialisation/deserialisation
  • - *
  • Configurable timeouts and retry mechanisms
  • - *
- * - * @return a configured {@link WebClient} instance for reactive HTTP operations - * @see WebClient - * @see WebClient.Builder - */ - @Bean - public WebClient webClient() { - return WebClient.builder() - .build(); - } -} diff --git a/src/main/java/com/onixbyte/helix/controller/AssetController.java b/src/main/java/com/onixbyte/helix/controller/AssetController.java index 3ef06d7..c413f38 100644 --- a/src/main/java/com/onixbyte/helix/controller/AssetController.java +++ b/src/main/java/com/onixbyte/helix/controller/AssetController.java @@ -13,11 +13,10 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; /** - * REST controller for file storage operations. Provides endpoints for uploading, downloading, and - * deleting assets using the configured storage service. + * This controller provides entry points that manipulates assets. * * @author zihluwang - * @since 1.0.0 + * @author siujamo */ @RestController @RequestMapping("/assets") diff --git a/src/main/java/com/onixbyte/helix/controller/AuthController.java b/src/main/java/com/onixbyte/helix/controller/AuthController.java index 98b4d90..1bef319 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthController.java @@ -17,6 +17,12 @@ import org.springframework.web.bind.annotation.*; import java.time.Duration; +/** + * This controller provides entry points making user authorised. + * + * @author zihluwang + * @author siujamo + */ @RestController @RequestMapping("/auth") public class AuthController { @@ -57,11 +63,21 @@ public class AuthController { .body(userService.getDetail(user)); } + /** + * Get whether the registration function is enabled. + * + * @return {@code true} if registration function is enabled, otherwise {@code false} + */ @GetMapping("/register-enabled") public boolean getRegisterEnabled() { return authService.getRegisterEnabled(); } + /** + * Perform log out. + * + * @return a response that remove the authentication from cookie + */ @GetMapping("/logout") public ResponseEntity logout() { var cookie = authService.buildCookie(TokenConstant.TOKEN_NAME, "", Duration.ZERO); diff --git a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java index 31e2a5a..5363712 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java @@ -7,12 +7,15 @@ import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; import com.onixbyte.helix.service.AuthorityService; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; -import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.util.List; - +/** + * This controller provides entry points for manipulate authorities. + * + * @author zihluwang + * @author siujamo + */ @RestController @RequestMapping("/authorities") public class AuthorityController { @@ -23,6 +26,14 @@ public class AuthorityController { this.authorityService = authorityService; } + /** + * Get authorities by page. + * + * @param pageNum current page num + * @param pageSize page size + * @param request query parameters + * @return a page contains authority data of the specified page + */ @GetMapping public Page getAuthorities( @RequestParam(required = false, defaultValue = "1") Integer pageNum, @@ -33,11 +44,23 @@ public class AuthorityController { return authorityService.getAuthorities(pageRequest, request); } + /** + * Add an authority. + * + * @param request authority specs + * @return created authority + */ @PostMapping public Authority addAuthority(@Validated @RequestBody AddAuthorityRequest request) { return authorityService.addAuthority(request); } + /** + * Edit an authority. + * + * @param request authority specs + * @return edited authority + */ @PutMapping public Authority editAuthority(@Validated @RequestBody EditAuthorityRequest request) { return authorityService.editAuthority(request); diff --git a/src/main/java/com/onixbyte/helix/controller/CaptchaController.java b/src/main/java/com/onixbyte/helix/controller/CaptchaController.java index a06808c..0e63f23 100644 --- a/src/main/java/com/onixbyte/helix/controller/CaptchaController.java +++ b/src/main/java/com/onixbyte/helix/controller/CaptchaController.java @@ -10,6 +10,12 @@ import org.springframework.web.bind.annotation.RestController; import java.util.Optional; +/** + * This controller provides entry points to get captcha images. + * + * @author zihluwang + * @author siujamo + */ @RestController @RequestMapping("/captcha") public class CaptchaController { @@ -21,6 +27,11 @@ public class CaptchaController { this.captchaService = captchaService; } + /** + * Get captcha image and captcha uuid. + * + * @return captcha response, contains the uuid of the captcha and image BASE64 + */ @GetMapping public ResponseEntity getCaptcha() { var captchaTuple = captchaService.buildCaptcha(); diff --git a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java index f8716eb..34c5a5e 100644 --- a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java +++ b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java @@ -10,6 +10,12 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +/** + * This controller provides entry points that manipulates departments. + * + * @author zihluwang + * @author siujamo + */ @RestController @RequestMapping("/departments") public class DepartmentController { diff --git a/src/main/java/com/onixbyte/helix/controller/ExceptionController.java b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java index 9a9b789..f8a6677 100644 --- a/src/main/java/com/onixbyte/helix/controller/ExceptionController.java +++ b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java @@ -13,22 +13,10 @@ import java.time.LocalDateTime; import java.util.stream.Collectors; /** - * Global exception handler for the Helix application. - *

- * This controller advice provides centralised exception handling across all controllers in - * the application. It intercepts exceptions thrown during request processing and converts them into - * appropriate HTTP responses with standardised error formats. - *

- * The controller handles various types of exceptions including: - *

    - *
  • Business logic exceptions ({@link BizException})
  • - *
  • Bean validation constraint violations ({@link ConstraintViolationException})
  • - *
- *

- * All error responses are formatted consistently using {@link BizExceptionResponse} to provide a - * uniform API error structure for client applications. + * This controller advise will catch some business exception which produced when performing actions. * * @author zihluwang + * @author siujamo * @see BizException * @see BizExceptionResponse * @see RestControllerAdvice diff --git a/src/main/java/com/onixbyte/helix/controller/MenuController.java b/src/main/java/com/onixbyte/helix/controller/MenuController.java index 4523651..ea260a6 100644 --- a/src/main/java/com/onixbyte/helix/controller/MenuController.java +++ b/src/main/java/com/onixbyte/helix/controller/MenuController.java @@ -10,6 +10,12 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +/** + * This controller provides entry points to manipulate menus. + * + * @author zihluwang + * @author siujamo + */ @RestController @RequestMapping("/menus") public class MenuController { @@ -21,6 +27,11 @@ public class MenuController { this.menuService = menuService; } + /** + * Get menu tree. + * + * @return available menu tree for the current user + */ @GetMapping public List> getMenuTree() { return menuService.getMenuTree(); diff --git a/src/main/java/com/onixbyte/helix/controller/PositionController.java b/src/main/java/com/onixbyte/helix/controller/PositionController.java index 3ac6463..eb5cc06 100644 --- a/src/main/java/com/onixbyte/helix/controller/PositionController.java +++ b/src/main/java/com/onixbyte/helix/controller/PositionController.java @@ -11,6 +11,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +/** + * This controller provides entry points to manipulate positions. + * + * @author zihluwang + * @author siujamo + */ @RestController @RequestMapping("/positions") public class PositionController { @@ -22,6 +28,13 @@ public class PositionController { this.positionService = positionService; } + /** + * Get position data paginated. + * + * @param pageNum current page num + * @param pageSize page size + * @return paginated position data + */ @GetMapping public Page getPositions( @RequestParam(required = false, defaultValue = "1") Integer pageNum, diff --git a/src/main/java/com/onixbyte/helix/controller/RoleController.java b/src/main/java/com/onixbyte/helix/controller/RoleController.java index adbf5c2..f24068d 100644 --- a/src/main/java/com/onixbyte/helix/controller/RoleController.java +++ b/src/main/java/com/onixbyte/helix/controller/RoleController.java @@ -13,6 +13,12 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +/** + * This controller provides entry points to manipulate roles. + * + * @author zihluwang + * @author siujamo + */ @RestController @RequestMapping("/roles") public class RoleController { diff --git a/src/main/java/com/onixbyte/helix/controller/SettingController.java b/src/main/java/com/onixbyte/helix/controller/SettingController.java index 8adb7d3..06320c5 100644 --- a/src/main/java/com/onixbyte/helix/controller/SettingController.java +++ b/src/main/java/com/onixbyte/helix/controller/SettingController.java @@ -1,11 +1,14 @@ package com.onixbyte.helix.controller; -import com.onixbyte.helix.domain.entity.Setting; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * This controller provides entry points to manipulate settings. + * + * @author zihluwang + * @author siujamo + */ @RestController @RequestMapping public class SettingController { diff --git a/src/main/java/com/onixbyte/helix/controller/UserController.java b/src/main/java/com/onixbyte/helix/controller/UserController.java index 00f9743..642d31f 100644 --- a/src/main/java/com/onixbyte/helix/controller/UserController.java +++ b/src/main/java/com/onixbyte/helix/controller/UserController.java @@ -1,9 +1,10 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.web.request.AddUserRequest; +import com.onixbyte.helix.domain.web.request.EditUserRequest; import com.onixbyte.helix.domain.web.request.QueryUserRequest; import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; -import com.onixbyte.helix.domain.web.request.EditUserRequest; +import com.onixbyte.helix.domain.web.response.ActionResponse; import com.onixbyte.helix.domain.web.response.UserDetailResponse; import com.onixbyte.helix.service.UserService; import org.springframework.beans.factory.annotation.Autowired; @@ -15,6 +16,12 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +/** + * This controller provides entry points to manipulate users. + * + * @author zihluwang + * @author siujamo + */ @RestController @RequestMapping("/users") public class UserController { @@ -56,29 +63,53 @@ public class UserController { return userService.getUserDetailByUserId(userId); } + /** + * Add a new user. + * + * @param request user to be added + * @return added user + */ @PostMapping @PreAuthorize("hasAnyAuthority('system:user:write')") public UserDetailResponse addUser(@Validated @RequestBody AddUserRequest request) { return userService.addUser(request); } + /** + * Edit a user. + * + * @param request user to be edited + * @return edited user + */ @PutMapping public ResponseEntity editUser(@Validated @RequestBody EditUserRequest request) { userService.updateUser(request); return ResponseEntity.ok(null); } + /** + * Reset user's password. + * + * @param request reset password request, contains ID of the user and new password + * @return action response + */ @PreAuthorize("hasAnyAuthority('system:user:reset-password')") @PatchMapping("/reset-password") - public ResponseEntity resetPassword(@Validated @RequestBody ResetPasswordRequest request) { + public ActionResponse resetPassword(@Validated @RequestBody ResetPasswordRequest request) { userService.resetPassword(request); - return ResponseEntity.ok(null); + return ActionResponse.success("密码修改成功"); } + /** + * Delete a user. + * + * @param userId ID of the user to be deleted + * @return action response + */ @PreAuthorize("hasAnyAuthority('system:user:write')") @DeleteMapping("/{userId:\\d+}") - public ResponseEntity deleteUser(@PathVariable Long userId) { + public ActionResponse deleteUser(@PathVariable Long userId) { userService.deleteUser(userId); - return ResponseEntity.ok(null); + return ActionResponse.success("删除成功"); } } diff --git a/src/main/java/com/onixbyte/helix/domain/web/response/ActionResponse.java b/src/main/java/com/onixbyte/helix/domain/web/response/ActionResponse.java new file mode 100644 index 0000000..8568960 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/response/ActionResponse.java @@ -0,0 +1,15 @@ +package com.onixbyte.helix.domain.web.response; + +public record ActionResponse( + String message, + boolean success +) { + + public static ActionResponse success(String message) { + return new ActionResponse(message, true); + } + + public static ActionResponse failed(String message) { + return new ActionResponse(message, false); + } +} From c13520bd8badb577ebc07436bdd82f8833473387 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Fri, 9 Jan 2026 10:49:54 +0800 Subject: [PATCH 25/60] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20Javadoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../database/query/wrapper/QueryAuthorityWrapper.java | 8 ++++++++ .../database/query/wrapper/QueryRoleWrapper.java | 10 ++++++++-- .../database/query/wrapper/QueryUserWrapper.java | 8 ++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryAuthorityWrapper.java b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryAuthorityWrapper.java index 330bd5c..9a77b62 100644 --- a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryAuthorityWrapper.java +++ b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryAuthorityWrapper.java @@ -1,4 +1,12 @@ package com.onixbyte.helix.domain.database.query.wrapper; +/** + * Wrapper class for authority query criteria. + *

+ * This class encapsulates query parameters used for filtering and searching authority entities. + * It is used in conjunction with repository methods to provide flexible querying capabilities. + * + * @author zihluwang + */ public class QueryAuthorityWrapper { } diff --git a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java index 51366b4..75f008e 100644 --- a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java +++ b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryRoleWrapper.java @@ -2,6 +2,14 @@ package com.onixbyte.helix.domain.database.query.wrapper; import com.onixbyte.helix.enumeration.Status; +/** + * Wrapper class for role query criteria. + *

+ * This class encapsulates query parameters used for filtering and searching role entities. It is + * used in conjunction with mapper methods to provide flexible querying capabilities. + * + * @author zihluwang + */ public class QueryRoleWrapper { private String name; private String code; @@ -39,6 +47,4 @@ public class QueryRoleWrapper { public void setStatus(Status status) { this.status = status; } - - } diff --git a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java index 695a704..745446f 100644 --- a/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java +++ b/src/main/java/com/onixbyte/helix/domain/database/query/wrapper/QueryUserWrapper.java @@ -4,6 +4,14 @@ import com.onixbyte.helix.enumeration.UserStatus; import java.time.LocalDateTime; +/** + * Wrapper class for user query criteria. + *

+ * This class encapsulates query parameters used for filtering and searching user entities. It is + * used in conjunction with mapper methods to provide flexible querying capabilities. + * + * @author zihluwang + */ public class QueryUserWrapper { private Long departmentId; private String username; From 4dc9ed341d16e704c474f6b4e0a8ebfa728799a3 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Fri, 9 Jan 2026 14:22:05 +0800 Subject: [PATCH 26/60] =?UTF-8?q?feat:=20=E4=BB=8E=20Cookie=20=E4=B8=AD?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=20AccessToken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helix/filter/TokenAuthenticationFilter.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java index 63b3c2b..9f3e9e6 100644 --- a/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java +++ b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java @@ -8,6 +8,7 @@ import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication import com.onixbyte.helix.shared.TokenConstant; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.jspecify.annotations.NonNull; @@ -17,9 +18,11 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.WebUtils; import java.io.IOException; import java.util.Objects; +import java.util.Optional; @Component public class TokenAuthenticationFilter extends OncePerRequestFilter { @@ -46,18 +49,14 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { - var token = request.getHeader(TokenConstant.TOKEN_HEADER_NAME); + var token = Optional.ofNullable(WebUtils.getCookie(request, TokenConstant.TOKEN_NAME)) + .map(Cookie::getValue) + .orElse(null); if (Objects.isNull(token) || token.isBlank()) { filterChain.doFilter(request, response); return; } - if (!token.startsWith(TokenConstant.TOKEN_PREFIX)) { - filterChain.doFilter(request, response); - return; - } - - token = token.substring(TokenConstant.TOKEN_PREFIX_LENGTH); try { var decodedToken = tokenClient.verifyToken(token); var username = decodedToken.getSubject(); From f6096c5ab7f05bf56c3d5634f80cb55f9b93f4d7 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Tue, 27 Jan 2026 13:42:52 +0800 Subject: [PATCH 27/60] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20API=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/authentication/folder.bru | 7 ++++++ api/authentication/login.bru | 23 +++++++++++++++++++ api/bruno.json | 9 ++++++++ api/collection.bru | 3 +++ api/environments/dev.bru | 3 +++ .../onixbyte/helix/service/AuthService.java | 12 +++------- 6 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 api/authentication/folder.bru create mode 100644 api/authentication/login.bru create mode 100644 api/bruno.json create mode 100644 api/collection.bru create mode 100644 api/environments/dev.bru diff --git a/api/authentication/folder.bru b/api/authentication/folder.bru new file mode 100644 index 0000000..e859f1f --- /dev/null +++ b/api/authentication/folder.bru @@ -0,0 +1,7 @@ +meta { + name: Authentication +} + +auth { + mode: none +} diff --git a/api/authentication/login.bru b/api/authentication/login.bru new file mode 100644 index 0000000..ea9a5b7 --- /dev/null +++ b/api/authentication/login.bru @@ -0,0 +1,23 @@ +meta { + name: Login + type: http + seq: 1 +} + +post { + url: {{BaseURL}}/auth/login + body: json + auth: inherit +} + +body:json { + { + "username": "helix", + "password": "123456" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/api/bruno.json b/api/bruno.json new file mode 100644 index 0000000..d233235 --- /dev/null +++ b/api/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "Helix API", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/api/collection.bru b/api/collection.bru new file mode 100644 index 0000000..732ab0c --- /dev/null +++ b/api/collection.bru @@ -0,0 +1,3 @@ +auth { + mode: none +} diff --git a/api/environments/dev.bru b/api/environments/dev.bru new file mode 100644 index 0000000..3beeb49 --- /dev/null +++ b/api/environments/dev.bru @@ -0,0 +1,3 @@ +vars { + BaseURL: http://localhost:8080 +} diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index c36ae7f..fa71d9d 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -1,11 +1,9 @@ package com.onixbyte.helix.service; -import com.onixbyte.helix.client.TokenClient; import com.onixbyte.helix.domain.entity.Setting; import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.domain.web.request.LoginRequest; import com.onixbyte.helix.exception.BizException; -import com.onixbyte.helix.manager.ApplicationManager; import com.onixbyte.helix.manager.CaptchaManager; import com.onixbyte.helix.manager.SecurityManager; import com.onixbyte.helix.manager.SettingManager; @@ -31,8 +29,6 @@ public class AuthService { private final CaptchaManager captchaManager; private final AuthenticationManager authenticationManager; private final SettingManager settingManager; - private final ApplicationManager applicationManager; - private final TokenClient tokenClient; private final SecurityManager securityManager; @Autowired @@ -40,13 +36,11 @@ public class AuthService { CaptchaManager captchaManager, AuthenticationManager authenticationManager, SettingManager settingManager, - ApplicationManager applicationManager, - TokenClient tokenClient, SecurityManager securityManager) { + SecurityManager securityManager + ) { this.captchaManager = captchaManager; this.authenticationManager = authenticationManager; this.settingManager = settingManager; - this.applicationManager = applicationManager; - this.tokenClient = tokenClient; this.securityManager = securityManager; } @@ -101,7 +95,7 @@ public class AuthService { public ResponseCookie buildCookie(String cookieName, String token) { var cookieBuilder = ResponseCookie.from(cookieName, token) .httpOnly(true) - .maxAge(securityManager.getTokenValidity()) + .maxAge(securityManager.getTokenValidDuration()) .path("/"); return cookieBuilder.build(); From ba2ed6d22cca74aa14bf86c9cdceff3af539682e Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 28 Jan 2026 11:23:44 +0800 Subject: [PATCH 28/60] refactor: categorise dependencies --- gradle/libs.versions.toml | 59 ++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c094a5..09e23a4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,38 +19,51 @@ jacksonVersion = "2.19.2" hypersistenceVersion = "3.14.0" [libraries] -jwt-core = { group = "com.auth0", name = "java-jwt", version.ref = "javaJwtVersion" } +# General Utilities +jspecify-core = { group = "org.jspecify", name = "jspecify", version.ref = "jspecifyVersion" } +commons-io = { group = "commons-io", name = "commons-io", version.ref = "commonsIoVersion" } +commons-collections = { group = "org.apache.commons", name = "commons-collections4", version.ref = "commonsCollections" } +commons-lang = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commonsLangVersion" } +jackson-jsr310 = { group = "com.fasterxml.jackson.datatype", name = "jackson-datatype-jsr310", version.ref = "jacksonVersion" } + +# Onixbyte Ecosystem +onixbyte-versionCatalogue = { group = "com.onixbyte", name = "version-catalogue", version.ref = "onixbyteVersion" } +onixbyte-tuple = { group = "com.onixbyte", name = "tuple" } +onixbyte-commonToolbox = { group = "com.onixbyte", name = "common-toolbox" } +onixbyte-mathToolbox = { group = "com.onixbyte", name = "math-toolbox" } +onixbyte-identityGenerator = { group = "com.onixbyte", name = "identity-generator" } +onixbyte-captcha = { group = "com.onixbyte", name = "captcha", version.ref = "onixbyteCaptcha" } +onixbyte-regions = { group = "com.onixbyte", name = "regions4j", version.ref = "onixbyteRegions" } + +# Persistence & Database +mybatis-starter-core = { group = "org.mybatis.spring.boot", name = "mybatis-spring-boot-starter", version.ref = "mybatisVersion" } +spring-boot-starter-jpa = { group = "org.springframework.boot", name = "spring-boot-starter-data-jpa" } +hypersistence-core = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version.ref = "hypersistenceVersion" } postgres-driver = { group = "org.postgresql", name = "postgresql", version.ref = "postgresDriverVersion" } h2-database = { group = "com.h2database", name = "h2", version.ref = "h2Version" } -spring-boot-configurationProcessor = { group = "org.springframework.boot", name = "spring-boot-configuration-processor", version.ref = "springBootVersion" } -spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web", version.ref = "springBootVersion" } -spring-boot-starter-webFlux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux", version.ref = "springBootVersion" } -spring-boot-starter-validation = { group = "org.springframework.boot", name = "spring-boot-starter-validation", version.ref = "springBootVersion" } spring-boot-starter-redis = { group = "org.springframework.boot", name = "spring-boot-starter-data-redis", version.ref = "springBootVersion" } -spring-boot-starter-cache = { group = "org.springframework.boot", name = "spring-boot-starter-cache", version.ref = "springBootVersion" } + +# Spring Boot Core & Web +spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" } +spring-boot-starter-webFlux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" } +spring-boot-starter-validation = { group = "org.springframework.boot", name = "spring-boot-starter-validation" } +spring-boot-starter-cache = { group = "org.springframework.boot", name = "spring-boot-starter-cache" } +spring-boot-configurationProcessor = { group = "org.springframework.boot", name = "spring-boot-configuration-processor" } + +# Security & Auth spring-boot-starter-security = { group = "org.springframework.boot", name = "spring-boot-starter-security", version.ref = "springBootVersion" } +jwt-core = { group = "com.auth0", name = "java-jwt", version.ref = "javaJwtVersion" } + +# Cloud Services +aws-sdk-bom = { group = "software.amazon.awssdk", name = "bom", version.ref = "awsSdkVersion" } +aws-sdk-s3 = { group = "software.amazon.awssdk", name = "s3" } + +# Testing spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test", version.ref = "springBootVersion" } spring-security-test = { group = "org.springframework.security", name = "spring-security-test", version.ref = "springSecurityVersion" } reactor-test = { group = "io.projectreactor", name = "reactor-test", version.ref = "reactorVersion" } junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junitPlatformVersion" } -onixbyte-versionCatalogue = { group = "com.onixbyte", name = "version-catalogue", version.ref = "onixbyteVersion" } -onixbyte-tuple = { group = "com.onixbyte", name = "tuple", version.ref = "onixbyteVersion" } -onixbyte-commonToolbox = { group = "com.onixbyte", name = "common-toolbox", version.ref = "onixbyteVersion" } -onixbyte-mathToolbox = { group = "com.onixbyte", name = "math-toolbox", version.ref = "onixbyteVersion" } -onixbyte-identityGenerator = { group = "com.onixbyte", name = "identity-generator", version.ref = "onixbyteVersion" } -onixbyte-captcha = { group = "com.onixbyte", name = "captcha", version.ref = "onixbyteCaptcha" } -onixbyte-regions = { group = "com.onixbyte", name = "regions4j", version.ref = "onixbyteRegions" } -aws-sdk-bom = { group = "software.amazon.awssdk", name = "bom", version.ref = "awsSdkVersion" } -aws-sdk-s3 = { group = "software.amazon.awssdk", name = "s3" } -commons-io = { group = "commons-io", name = "commons-io", version.ref = "commonsIoVersion" } -commons-collections = { group = "org.apache.commons", name = "commons-collections4", version.ref = "commonsCollections" } -mybatis-starter-core = { group = "org.mybatis.spring.boot", name = "mybatis-spring-boot-starter", version.ref = "mybatisVersion" } mybatis-starter-test = { group = "org.mybatis.spring.boot", name = "mybatis-spring-boot-starter-test", version.ref = "mybatisVersion" } -jackson-jsr310 = { group = "com.fasterxml.jackson.datatype", name = "jackson-datatype-jsr310", version.ref = "jacksonVersion" } -spring-boot-starter-jpa = { group = "org.springframework.boot", name = "spring-boot-starter-data-jpa" } -hypersistence-core = { group = "io.hypersistence", name = "hypersistence-utils-hibernate-63", version.ref = "hypersistenceVersion" } -commons-lang = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commonsLangVersion" } -jspecify-core = { group = "org.jspecify", name = "jspecify", version.ref = "jspecifyVersion" } [plugins] From d6944957c892a31621cfc8115de6087d557ae49a Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 28 Jan 2026 14:32:29 +0800 Subject: [PATCH 29/60] refactor: optimise dependencies --- build.gradle.kts | 6 +++--- gradle/libs.versions.toml | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7f43d91..c0ee94a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { - id("java") - id("org.springframework.boot") version "3.5.4" - id("io.spring.dependency-management") version "1.1.7" + java + alias(libs.plugins.spring.boot) + alias(libs.plugins.spring.dependency.management) } val artefactVersion: String by project diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 09e23a4..e4d20bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,11 +3,11 @@ jspecifyVersion = "1.0.0" javaJwtVersion = "4.5.0" postgresDriverVersion = "42.7.7" h2Version = "2.2.224" -springSecurityVersion = "6.5.2" springBootVersion = "3.5.4" +springSecurityVersion = "6.5.2" reactorVersion = "3.7.8" junitPlatformVersion = "1.12.2" -onixbyteVersion = "3.2.0" +onixbyteVersion = "3.3.0" onixbyteCaptcha = "1.1.0" onixbyteRegions = "2025.12.0" awsSdkVersion = "2.25.48" @@ -17,6 +17,7 @@ commonsLangVersion = "3.20.0" mybatisVersion = "3.0.5" jacksonVersion = "2.19.2" hypersistenceVersion = "3.14.0" +springDependencyManagementVersion = "1.1.7" [libraries] # General Utilities @@ -66,4 +67,5 @@ junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher mybatis-starter-test = { group = "org.mybatis.spring.boot", name = "mybatis-spring-boot-starter-test", version.ref = "mybatisVersion" } [plugins] - +spring-boot = { id = "org.springframework.boot", version.ref = "springBootVersion" } +spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagementVersion" } From 3095e3416489b819e777929b3d1fbc57f7cb1aa2 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 28 Jan 2026 14:44:10 +0800 Subject: [PATCH 30/60] build: add docker build script --- .dockerignore | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 27 +++++++++ 2 files changed, 179 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0cd419a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,152 @@ +### macOS +# General +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# Metadata left by Dolphin file manager, which comes with KDE Plasma +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# Log files created by default by the nohup command +nohup.out + +### Windows + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### JetBrains IDE +# Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea + +# Gradle and Maven with auto-import +*.iml +*.ipr + +# File-based project format +*.iws + +# IntelliJ +out/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based HTTP Client +http-client.private.env.json + +### Server +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +*.jar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +# Config +config/*.yml +config/*.yaml +config/*.properties + +# Tests +test/ + +### Gradle +.gradle +**/build/ +!**/src/**/build/ + +!gradle/ +!gradle/wrapper +!gradle/wrapper/gradle-wrapper.jar +!gradle/wrapper/gradle-wrapper.properties +gradle-app.setting +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +api/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..564f182 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM amazoncorretto:21 AS build + +ARG ARTEFACT_VERSION='1.0.0' +WORKDIR /home/app + +COPY gradlew . +COPY gradle gradle +COPY build.gradle.kts . +COPY settings.gradle.kts . +COPY gradle/libs.versions.toml gradle/libs.versions.toml + +RUN chmod +x gradlew && ./gradlew dependencies --no-daemon + +COPY src src +RUN ./gradlew -PartefactVersion=${ARTEFACT_VERSION} bootJar --no-daemon + +FROM amazoncorretto:21-alpine + +WORKDIR /app + +COPY --from=build /home/app/build/libs/helix-server-${ARTEFACT_VERSION}.jar app.jar + +EXPOSE 8080 + +ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" + +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] \ No newline at end of file From 891b7f92807158a1307cb6894a3ead8e16b42800 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 28 Jan 2026 15:44:54 +0800 Subject: [PATCH 31/60] build: optimise docker build script --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 564f182..e1182c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ COPY gradle/libs.versions.toml gradle/libs.versions.toml RUN chmod +x gradlew && ./gradlew dependencies --no-daemon COPY src src -RUN ./gradlew -PartefactVersion=${ARTEFACT_VERSION} bootJar --no-daemon +RUN ./gradlew bootJar --no-daemon -PartefactVersion=${ARTEFACT_VERSION} FROM amazoncorretto:21-alpine From e9c6732888268707d269bc827ad37b83e0e2b0cb Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 28 Jan 2026 15:45:13 +0800 Subject: [PATCH 32/60] chore: upgrade postgres driver --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4d20bd..a70c101 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] jspecifyVersion = "1.0.0" javaJwtVersion = "4.5.0" -postgresDriverVersion = "42.7.7" +postgresDriverVersion = "42.7.9" h2Version = "2.2.224" springBootVersion = "3.5.4" springSecurityVersion = "6.5.2" From 64be3a79ea6882366f5b8ba683ca9b6917692629 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 28 Jan 2026 15:45:45 +0800 Subject: [PATCH 33/60] refactor: update init.sql to current schema --- database/init.d/init-en_GB.sql | 63 ++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/database/init.d/init-en_GB.sql b/database/init.d/init-en_GB.sql index 629473c..44e9727 100644 --- a/database/init.d/init-en_GB.sql +++ b/database/init.d/init-en_GB.sql @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + /** * IMPORTANT NOTE ON DATABASE CREATION: * * If you intend to create the database using a user other than the one specified @@ -85,31 +107,28 @@ VALUES (1, 'HR Manager', 'HR-MGR', DROP TABLE IF EXISTS users CASCADE; CREATE TABLE users ( - id BIGINT PRIMARY KEY, - username VARCHAR(64) UNIQUE NOT NULL, - password VARCHAR(255), - full_name VARCHAR(128) NOT NULL, - email VARCHAR(128) UNIQUE, - region_code VARCHAR(10), - phone_number VARCHAR(32), - avatar_url TEXT, - status USER_STATUS NOT NULL DEFAULT 'ACTIVE', - department_id BIGINT REFERENCES departments (id), - position_id BIGINT REFERENCES positions (id), - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + id BIGINT PRIMARY KEY, + username VARCHAR(64) UNIQUE NOT NULL, + password VARCHAR(255), + full_name VARCHAR(128) NOT NULL, + email VARCHAR(128) NULL UNIQUE, + region_abbreviation VARCHAR(10) NULL, + phone_number VARCHAR(32) NULL, + avatar_url TEXT, + status USER_STATUS NOT NULL DEFAULT 'ACTIVE', + department_id BIGINT REFERENCES departments (id), + position_id BIGINT REFERENCES positions (id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (region_abbreviation, phone_number) ); CREATE INDEX users_username_index ON users (username); ---- Users Table Indexes --- -CREATE UNIQUE INDEX uidx_users_region_abbreviation_phone_number - ON users (region_abbreviation, phone_number); - --- Users Data Insertion --- -- NOTE: All phone numbers are generated by ChatGPT, they should not be connected any real person. -INSERT INTO users(id, username, password, full_name, email, region_abbreviation, phone_number, avatar_url, - department_id, position_id, created_at, updated_at) +INSERT INTO users(id, username, password, full_name, email, region_abbreviation, phone_number, + avatar_url, department_id, position_id, created_at, updated_at) VALUES (1, 'helix', null, 'Helix Admin', 'admin@helix.onixbyte.dev', 'GB', '7000000000', 'https://gravatar.com/avatar/6ef4c4033f6aa8e43d06bd5e462a6173cc2a960633473721a6f1289cd1b5146f', 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), @@ -310,14 +329,14 @@ CREATE TABLE menus status STATUS NOT NULL DEFAULT 'ACTIVE'::STATUS, authority_code VARCHAR(128) NULL DEFAULT NULL, icon VARCHAR(128) NULL DEFAULT NULL, - create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - update_time TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX menus_code_uindex ON menus (code); INSERT INTO menus(id, name, parent_id, code, sort, path, is_external_link, is_visible, status, - authority_code, icon, create_time, update_time) + authority_code, icon, created_at, updated_at) VALUES (1, '系统管理', NULL, 'system-manage', 99, NULL, FALSE, TRUE, 'ACTIVE'::STATUS, NULL, NULL, NOW(), NOW()), (2, '用户管理', 1, 'user-manage', 1, '/users', FALSE, TRUE, 'ACTIVE'::STATUS, From 9b48869f3fb2044a38587480d98b008649a29cb5 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 28 Jan 2026 16:08:51 +0800 Subject: [PATCH 34/60] refactor: move caching configuration to CacheConfig and update application.yml settings --- .../java/com/onixbyte/helix/HelixApplication.java | 4 ---- .../java/com/onixbyte/helix/config/CacheConfig.java | 2 ++ src/main/resources/application.yml | 12 +++++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/HelixApplication.java b/src/main/java/com/onixbyte/helix/HelixApplication.java index 4bf85ae..b4d15d1 100644 --- a/src/main/java/com/onixbyte/helix/HelixApplication.java +++ b/src/main/java/com/onixbyte/helix/HelixApplication.java @@ -1,20 +1,16 @@ package com.onixbyte.helix; -import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cache.annotation.EnableCaching; /** * Application entrance. * * @author zihluwang * @see SpringBootApplication - * @see EnableCaching * @see SpringApplication * @since 1.0.0 */ -@EnableCaching @SpringBootApplication public class HelixApplication { diff --git a/src/main/java/com/onixbyte/helix/config/CacheConfig.java b/src/main/java/com/onixbyte/helix/config/CacheConfig.java index 97b4aff..c103445 100644 --- a/src/main/java/com/onixbyte/helix/config/CacheConfig.java +++ b/src/main/java/com/onixbyte/helix/config/CacheConfig.java @@ -1,6 +1,7 @@ package com.onixbyte.helix.config; import com.onixbyte.helix.extension.redis.serializer.JacksonSerialiser; +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; @@ -34,6 +35,7 @@ import java.time.Duration; * @since 1.0.0 */ @Configuration +@EnableCaching public class CacheConfig { /** diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5d853f8..41411b7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,7 +4,7 @@ spring: cache: type: redis redis: - time-to-live: PT1H30M + time-to-live: PT2H jpa: properties: hibernate: @@ -19,6 +19,11 @@ spring: hikari: minimum-idle: 1 maximum-pool-size: 10 + data: + redis: + repositories: + # Disable redis repositories + enabled: false mybatis: configuration: @@ -27,3 +32,8 @@ mybatis: type-aliases-package: com.onixbyte.helix.domain.entity type-handlers-package: com.onixbyte.helix.extension.mybatis.handler mapper-locations: classpath:/mapper/*.xml + +logging: + level: + org.hibernate: + orm.connections.pooling: off From d9bd1a1b0eda4563f91866e9a445ab8acf264a93 Mon Sep 17 00:00:00 2001 From: siujamo Date: Wed, 28 Jan 2026 16:15:17 +0800 Subject: [PATCH 35/60] chore: disable redis repositories and JTA support in application.yml --- src/main/resources/application.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 41411b7..761998a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,6 +5,14 @@ spring: type: redis redis: time-to-live: PT2H + data: + redis: + repositories: + # Disable redis repositories + enabled: false + jta: + # Disable JTA support + enabled: false jpa: properties: hibernate: @@ -19,11 +27,7 @@ spring: hikari: minimum-idle: 1 maximum-pool-size: 10 - data: - redis: - repositories: - # Disable redis repositories - enabled: false + mybatis: configuration: From e5ae5f61b59f2bec8a512cae46ce2eb158004e8f Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 29 Jan 2026 09:15:56 +0800 Subject: [PATCH 36/60] style: reformat init.sql --- database/init.d/init-en_GB.sql | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/database/init.d/init-en_GB.sql b/database/init.d/init-en_GB.sql index 44e9727..231b269 100644 --- a/database/init.d/init-en_GB.sql +++ b/database/init.d/init-en_GB.sql @@ -335,9 +335,30 @@ CREATE TABLE menus CREATE UNIQUE INDEX menus_code_uindex ON menus (code); -INSERT INTO menus(id, name, parent_id, code, sort, path, is_external_link, is_visible, status, - authority_code, icon, created_at, updated_at) -VALUES (1, '系统管理', NULL, 'system-manage', 99, NULL, FALSE, TRUE, 'ACTIVE'::STATUS, NULL, NULL, - NOW(), NOW()), - (2, '用户管理', 1, 'user-manage', 1, '/users', FALSE, TRUE, 'ACTIVE'::STATUS, - 'system:user:write', NULL, NOW(), NOW()); \ No newline at end of file +WITH system_menu AS ( + INSERT INTO menus (name, parent_id, code, sort, path, is_external_link, is_visible, status, + authority_code, icon, created_at, updated_at) + VALUES ('系统管理', NULL, 'system-mgmt', 99, NULL, FALSE, TRUE, 'ACTIVE'::STATUS, NULL, + NULL, NOW(), NOW()) + RETURNING id) +INSERT +INTO menus(name, parent_id, code, sort, path, is_external_link, is_visible, status, + authority_code, icon, created_at, updated_at) +SELECT '用户管理', + system_menu.id, + 'user-mgmt', + 1, + '/users', + FALSE, + TRUE, + 'ACTIVE'::STATUS, + 'system:user:write', + NULL, + NOW(), + NOW() +FROM system_menu; + +INSERT INTO menus (name, parent_id, code, sort, path, is_external_link, is_visible, status, + authority_code, icon, created_at, updated_at) +VALUES ('Helix 官网', null, 'helix-official-site', 100, 'https://helix.onixbyte.com', true, true, + 'ACTIVE'::STATUS, null, null, NOW(), NOW()); \ No newline at end of file From d2b086ce17f124968105c6816fb177022c0427f9 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 29 Jan 2026 09:27:28 +0800 Subject: [PATCH 37/60] refactor: enhance department and user data insertion with CTEs and streamline role authority assignments --- database/init.d/init-en_GB.sql | 143 ++++++++++++++++++++++----------- 1 file changed, 97 insertions(+), 46 deletions(-) diff --git a/database/init.d/init-en_GB.sql b/database/init.d/init-en_GB.sql index 231b269..21c2396 100644 --- a/database/init.d/init-en_GB.sql +++ b/database/init.d/init-en_GB.sql @@ -59,14 +59,41 @@ CREATE INDEX departments_name_index ON departments (name); CREATE INDEX departments_parent_id_index ON departments (parent_id); --- Departments Data Insertion --- -INSERT INTO departments (id, name, parent_id, sort, status) -VALUES (1, 'Company HQ', NULL, 1, 'ACTIVE'::STATUS), - (2, 'Human Resources', 1, 1, 'ACTIVE'::STATUS), - (3, 'Finance', 1, 2, 'ACTIVE'::STATUS), - (4, 'Technology', 1, 3, 'ACTIVE'::STATUS), - (5, 'IT Support', 4, 1, 'ACTIVE'::STATUS), - (6, 'Software Development', 4, 2, 'ACTIVE'::STATUS), - (7, 'Operations', 1, 4, 'INACTIVE'::STATUS); +WITH company_hq AS ( + INSERT INTO departments (name, parent_id, sort, status) + VALUES ('Company HQ', NULL, 1, 'ACTIVE'::STATUS) + RETURNING id), + human_resources AS ( + INSERT INTO departments (name, parent_id, sort, status) + SELECT 'Human Resources', company_hq.id, 1, 'ACTIVE'::STATUS + FROM company_hq + RETURNING id), + finance AS ( + INSERT INTO departments (name, parent_id, sort, status) + SELECT 'Finance', company_hq.id, 2, 'ACTIVE'::STATUS + FROM company_hq + RETURNING id), + technology AS ( + INSERT INTO departments (name, parent_id, sort, status) + SELECT 'Technology', company_hq.id, 3, 'ACTIVE'::STATUS + FROM company_hq + RETURNING id), + it_support AS ( + INSERT INTO departments (name, parent_id, sort, status) + SELECT 'IT Support', technology.id, 1, 'ACTIVE'::STATUS + FROM technology + RETURNING id), + software_development AS ( + INSERT INTO departments (name, parent_id, sort, status) + SELECT 'Software Development', technology.id, 2, 'ACTIVE'::STATUS + FROM technology + RETURNING id), + operations AS ( + INSERT INTO departments (name, parent_id, sort, status) + SELECT 'Operations', company_hq.id, 4, 'INACTIVE'::STATUS + FROM company_hq + RETURNING id) +SELECT NULL; --- Positions Table --- DROP TABLE IF EXISTS positions CASCADE; @@ -87,27 +114,30 @@ CREATE INDEX positions_code_index ON positions (code); CREATE INDEX positions_name_code_index ON positions (name, code); --- Positions Data Insertion --- -INSERT INTO positions (id, name, code, description, sort, status) -VALUES (1, 'HR Manager', 'HR-MGR', +INSERT INTO positions (name, code, description, sort, status) +VALUES ('HR Manager', 'HR-MGR', 'Responsible for overseeing recruitment, employee relations, and staff wellbeing.', 1, 'ACTIVE'), - (2, 'Finance Officer', 'FIN-OFC', + ('Finance Officer', 'FIN-OFC', 'Handles accounts, prepares financial statements, and ensures compliance with regulations.', 2, 'ACTIVE'), - (3, 'IT Support Specialist', 'IT-SPT', + ('IT Support Specialist', 'IT-SPT', 'Provides technical assistance, manages helpdesk queries, and maintains computer systems.', 3, 'ACTIVE'), - (4, 'Software Engineer', 'SWE-ENG', + ('Software Engineer', 'SWE-ENG', 'Develops and maintains in-house applications, ensuring code quality and system reliability.', 4, 'ACTIVE'), - (5, 'Operations Coordinator', 'OPS-CRD', + ('Operations Coordinator', 'OPS-CRD', 'Assists with day-to-day logistics, procurement, and office organisation.', 5, 'INACTIVE'); --- Users Table --- DROP TABLE IF EXISTS users CASCADE; +DROP SEQUENCE IF EXISTS users_id_seq CASCADE; +CREATE SEQUENCE users_id_seq START WITH 1; + CREATE TABLE users ( - id BIGINT PRIMARY KEY, + id BIGINT PRIMARY KEY DEFAULT nextval('users_id_seq'), username VARCHAR(64) UNIQUE NOT NULL, password VARCHAR(255), full_name VARCHAR(128) NOT NULL, @@ -127,14 +157,33 @@ CREATE INDEX users_username_index ON users (username); --- Users Data Insertion --- -- NOTE: All phone numbers are generated by ChatGPT, they should not be connected any real person. -INSERT INTO users(id, username, password, full_name, email, region_abbreviation, phone_number, +INSERT INTO users(username, password, full_name, email, region_abbreviation, phone_number, avatar_url, department_id, position_id, created_at, updated_at) -VALUES (1, 'helix', null, 'Helix Admin', 'admin@helix.onixbyte.dev', 'GB', '7000000000', - 'https://gravatar.com/avatar/6ef4c4033f6aa8e43d06bd5e462a6173cc2a960633473721a6f1289cd1b5146f', - 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - (2, 'johndoe', null, 'John Doe', 'johndoe@helix.onixbyte.dev', 'GB', '7000000001', - 'https://gravatar.com/avatar/41bcebddd573747d1bd35ef7fae72ebefd6b47f077d42442a2510d35b0c2db92', - 6, 4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); +SELECT 'helix', + null, + 'Helix Admin', + 'admin@helix.onixbyte.dev', + 'GB', + '7000000000', + 'https://gravatar.com/avatar/6ef4c4033f6aa8e43d06bd5e462a6173cc2a960633473721a6f1289cd1b5146f', + (SELECT id FROM departments WHERE name = 'Company HQ'), + (SELECT id FROM positions WHERE code = 'HR-MGR'), + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP; + +INSERT INTO users(username, password, full_name, email, region_abbreviation, phone_number, + avatar_url, department_id, position_id, created_at, updated_at) +SELECT 'johndoe', + null, + 'John Doe', + 'johndoe@helix.onixbyte.dev', + 'GB', + '7000000001', + 'https://gravatar.com/avatar/41bcebddd573747d1bd35ef7fae72ebefd6b47f077d42442a2510d35b0c2db92', + (SELECT id FROM departments WHERE name = 'Software Development'), + (SELECT id FROM positions WHERE code = 'SWE-ENG'), + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP; --- User Identities Table --- DROP TABLE IF EXISTS user_identities CASCADE; @@ -187,9 +236,12 @@ CREATE TABLE user_roles ); --- User Roles Data Insertion --- -INSERT INTO user_roles -VALUES (1, 1), - (2, 2); +INSERT INTO user_roles (user_id, role_id) +SELECT u.id, r.id +FROM users u + CROSS JOIN roles r +WHERE (u.username = 'helix' AND r.code = 'admin') + OR (u.username = 'johndoe' AND r.code = 'user'); --- Authorities Table --- DROP TABLE IF EXISTS authorities CASCADE; @@ -247,27 +299,26 @@ CREATE TABLE role_authorities ); --- Role Authorities Data Insertion --- -INSERT INTO role_authorities -VALUES (1, 1, CURRENT_TIMESTAMP), - (1, 2, CURRENT_TIMESTAMP), - (1, 3, CURRENT_TIMESTAMP), - (1, 4, CURRENT_TIMESTAMP), - (1, 5, CURRENT_TIMESTAMP), - (1, 6, CURRENT_TIMESTAMP), - (1, 7, CURRENT_TIMESTAMP), - (1, 8, CURRENT_TIMESTAMP), - (1, 9, CURRENT_TIMESTAMP), - (1, 10, CURRENT_TIMESTAMP), - (1, 11, CURRENT_TIMESTAMP), - (1, 12, CURRENT_TIMESTAMP), - (1, 13, CURRENT_TIMESTAMP), - (2, 1, CURRENT_TIMESTAMP), - (2, 2, CURRENT_TIMESTAMP), - (2, 3, CURRENT_TIMESTAMP), - (2, 5, CURRENT_TIMESTAMP), - (2, 7, CURRENT_TIMESTAMP), - (2, 9, CURRENT_TIMESTAMP), - (2, 11, CURRENT_TIMESTAMP); +-- Admin role gets all authorities +INSERT INTO role_authorities (role_id, authority_id, created_at) +SELECT r.id, a.id, CURRENT_TIMESTAMP +FROM roles r + CROSS JOIN authorities a +WHERE r.code = 'admin'; + +-- Normal user role gets specific authorities +INSERT INTO role_authorities (role_id, authority_id, created_at) +SELECT r.id, a.id, CURRENT_TIMESTAMP +FROM roles r + CROSS JOIN authorities a +WHERE r.code = 'user' + AND a.code IN ('system:dashboard:read', + 'system:user:read', + 'system:user_detail:read', + 'system:department:read', + 'system:role:read', + 'system:authority:read', + 'system:audit_log:read'); DROP TABLE IF EXISTS assets; CREATE TABLE assets From 2de2cb029dbdf256bd0d22ad4bb797b997c1c9b9 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 12 Mar 2026 13:54:31 +0800 Subject: [PATCH 38/60] feat: add Spring Boot Actuator dependency and update Java JWT version --- build.gradle.kts | 1 + gradle/libs.versions.toml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index c0ee94a..662fcac 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(libs.onixbyte.regions) implementation(libs.jwt.core) implementation(libs.spring.boot.configurationProcessor) + implementation(libs.spring.boot.actuator) implementation(libs.spring.boot.starter.web) implementation(libs.spring.boot.starter.webFlux) implementation(libs.spring.boot.starter.validation) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a70c101..afbf926 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] jspecifyVersion = "1.0.0" -javaJwtVersion = "4.5.0" +javaJwtVersion = "4.5.1" postgresDriverVersion = "42.7.9" h2Version = "2.2.224" springBootVersion = "3.5.4" @@ -50,6 +50,7 @@ spring-boot-starter-webFlux = { group = "org.springframework.boot", name = "spri spring-boot-starter-validation = { group = "org.springframework.boot", name = "spring-boot-starter-validation" } spring-boot-starter-cache = { group = "org.springframework.boot", name = "spring-boot-starter-cache" } spring-boot-configurationProcessor = { group = "org.springframework.boot", name = "spring-boot-configuration-processor" } +spring-boot-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" } # Security & Auth spring-boot-starter-security = { group = "org.springframework.boot", name = "spring-boot-starter-security", version.ref = "springBootVersion" } From e83f1358e3a88f933fcbd8676792df40d1979efc Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 12 Mar 2026 13:55:22 +0800 Subject: [PATCH 39/60] chore: remove bruno api files --- api/authentication/folder.bru | 7 ------- api/authentication/login.bru | 23 ----------------------- api/bruno.json | 9 --------- api/collection.bru | 3 --- api/environments/dev.bru | 3 --- 5 files changed, 45 deletions(-) delete mode 100644 api/authentication/folder.bru delete mode 100644 api/authentication/login.bru delete mode 100644 api/bruno.json delete mode 100644 api/collection.bru delete mode 100644 api/environments/dev.bru diff --git a/api/authentication/folder.bru b/api/authentication/folder.bru deleted file mode 100644 index e859f1f..0000000 --- a/api/authentication/folder.bru +++ /dev/null @@ -1,7 +0,0 @@ -meta { - name: Authentication -} - -auth { - mode: none -} diff --git a/api/authentication/login.bru b/api/authentication/login.bru deleted file mode 100644 index ea9a5b7..0000000 --- a/api/authentication/login.bru +++ /dev/null @@ -1,23 +0,0 @@ -meta { - name: Login - type: http - seq: 1 -} - -post { - url: {{BaseURL}}/auth/login - body: json - auth: inherit -} - -body:json { - { - "username": "helix", - "password": "123456" - } -} - -settings { - encodeUrl: true - timeout: 0 -} diff --git a/api/bruno.json b/api/bruno.json deleted file mode 100644 index d233235..0000000 --- a/api/bruno.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "version": "1", - "name": "Helix API", - "type": "collection", - "ignore": [ - "node_modules", - ".git" - ] -} \ No newline at end of file diff --git a/api/collection.bru b/api/collection.bru deleted file mode 100644 index 732ab0c..0000000 --- a/api/collection.bru +++ /dev/null @@ -1,3 +0,0 @@ -auth { - mode: none -} diff --git a/api/environments/dev.bru b/api/environments/dev.bru deleted file mode 100644 index 3beeb49..0000000 --- a/api/environments/dev.bru +++ /dev/null @@ -1,3 +0,0 @@ -vars { - BaseURL: http://localhost:8080 -} From 0562c8548bea56a560e721e6cfbbc2419e3de46e Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 12 Mar 2026 13:55:53 +0800 Subject: [PATCH 40/60] feat: introduce ApplicationMode enumeration and integrate it into ApplicationProperties and ApplicationManager --- .../helix/enumeration/ApplicationMode.java | 30 +++++++++++++++++++ .../helix/manager/ApplicationManager.java | 5 ++++ .../properties/ApplicationProperties.java | 4 ++- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/onixbyte/helix/enumeration/ApplicationMode.java diff --git a/src/main/java/com/onixbyte/helix/enumeration/ApplicationMode.java b/src/main/java/com/onixbyte/helix/enumeration/ApplicationMode.java new file mode 100644 index 0000000..a1250a3 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/enumeration/ApplicationMode.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.enumeration; + +public enum ApplicationMode { + + DEVELOPMENT, + PRODUCTION, + ; +} diff --git a/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java b/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java index d1b3a27..9b509fa 100644 --- a/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java +++ b/src/main/java/com/onixbyte/helix/manager/ApplicationManager.java @@ -1,5 +1,6 @@ package com.onixbyte.helix.manager; +import com.onixbyte.helix.enumeration.ApplicationMode; import com.onixbyte.helix.properties.ApplicationProperties; import com.onixbyte.helix.properties.AuthenticationProperties; import org.springframework.beans.factory.annotation.Autowired; @@ -36,4 +37,8 @@ public class ApplicationManager { return Optional.ofNullable(authenticationProperties.secureCookieEnabled()) .orElse(false); } + + public ApplicationMode getApplicationMode() { + return applicationProperties.mode(); + } } diff --git a/src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java b/src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java index 2a725a4..2abea04 100644 --- a/src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java +++ b/src/main/java/com/onixbyte/helix/properties/ApplicationProperties.java @@ -1,11 +1,13 @@ package com.onixbyte.helix.properties; +import com.onixbyte.helix.enumeration.ApplicationMode; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.DefaultValue; @ConfigurationProperties(prefix = "app.common") public record ApplicationProperties( @DefaultValue("default@helix.onixbyte.dev") String defaultEmail, - @DefaultValue("helix.onixbyte.dev") String externalHost + @DefaultValue("helix.onixbyte.dev") String externalHost, + @DefaultValue("prod") ApplicationMode mode ) { } From 7ce241cc1656a16eb44ef91f330c3650e8d71782 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 12 Mar 2026 13:56:06 +0800 Subject: [PATCH 41/60] feat: implement user credential management with new UserCredential entity and related components --- database/init.d/init-en_GB.sql | 20 ++++---- .../onixbyte/helix/domain/entity/User.java | 35 +------------ ...{UserIdentity.java => UserCredential.java} | 50 +++++++++---------- ...rIdentityId.java => UserCredentialId.java} | 32 ++++++------ .../helix/domain/view/UserIdentityView.java | 34 ++++++------- .../web/response/LoginSuccessResponse.java | 4 -- ...yProvider.java => CredentialProvider.java} | 24 ++++++++- .../filter/TokenAuthenticationFilter.java | 2 - .../onixbyte/helix/manager/UserManager.java | 20 +++++--- .../helix/mapper/UserCredentialMapper.java | 35 +++++++++++++ .../repository/UserCredentialRepository.java | 10 ++++ .../repository/UserIdentityRepository.java | 10 ---- ...sernamePasswordAuthenticationProvider.java | 19 +++++-- .../onixbyte/helix/service/AuthService.java | 22 ++++++-- .../onixbyte/helix/service/UserService.java | 16 +++--- .../resources/mapper/UserCredentialMapper.xml | 33 ++++++++++++ 16 files changed, 229 insertions(+), 137 deletions(-) rename src/main/java/com/onixbyte/helix/domain/entity/{UserIdentity.java => UserCredential.java} (78%) rename src/main/java/com/onixbyte/helix/domain/entity/embeddable/{UserIdentityId.java => UserCredentialId.java} (70%) rename src/main/java/com/onixbyte/helix/enumeration/{IdentityProvider.java => CredentialProvider.java} (58%) create mode 100644 src/main/java/com/onixbyte/helix/mapper/UserCredentialMapper.java create mode 100644 src/main/java/com/onixbyte/helix/repository/UserCredentialRepository.java delete mode 100644 src/main/java/com/onixbyte/helix/repository/UserIdentityRepository.java create mode 100644 src/main/resources/mapper/UserCredentialMapper.xml diff --git a/database/init.d/init-en_GB.sql b/database/init.d/init-en_GB.sql index 21c2396..9be7216 100644 --- a/database/init.d/init-en_GB.sql +++ b/database/init.d/init-en_GB.sql @@ -34,12 +34,12 @@ --- Type Definitions --- DROP TYPE IF EXISTS USER_STATUS CASCADE; DROP TYPE IF EXISTS STATUS CASCADE; -DROP TYPE IF EXISTS IDENTITY_PROVIDER CASCADE; +DROP TYPE IF EXISTS CREDENTIAL_PROVIDER CASCADE; DROP TYPE IF EXISTS SETTING_TYPE CASCADE; CREATE TYPE USER_STATUS AS ENUM ('ACTIVE', 'INACTIVE', 'LOCKED'); CREATE TYPE STATUS AS ENUM ('ACTIVE', 'INACTIVE'); -CREATE TYPE IDENTITY_PROVIDER AS ENUM ('LOCAL', 'OIDC', 'MICROSOFT_ENTRA_ID', 'GOOGLE_OIDC', 'SAML'); +CREATE TYPE CREDENTIAL_PROVIDER AS ENUM ('LOCAL', 'OIDC', 'MICROSOFT_ENTRA_ID', 'GOOGLE_OIDC', 'SAML'); CREATE TYPE SETTING_TYPE AS ENUM ('STRING', 'BOOLEAN', 'INT'); --- Departments Table --- @@ -186,15 +186,15 @@ SELECT 'johndoe', CURRENT_TIMESTAMP; --- User Identities Table --- -DROP TABLE IF EXISTS user_identities CASCADE; -CREATE TABLE user_identities +DROP TABLE IF EXISTS user_credentials CASCADE; +CREATE TABLE user_credentials ( - user_id BIGINT NOT NULL REFERENCES users (id), - provider IDENTITY_PROVIDER NOT NULL, - external_id VARCHAR(255) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (user_id, provider, external_id) + user_id BIGINT NOT NULL REFERENCES users (id), + provider CREDENTIAL_PROVIDER NOT NULL, + credential VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, provider, credential) ); --- Roles Table --- diff --git a/src/main/java/com/onixbyte/helix/domain/entity/User.java b/src/main/java/com/onixbyte/helix/domain/entity/User.java index f391a42..f6c60c4 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/User.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/User.java @@ -48,12 +48,6 @@ public class User { @Column(nullable = false, length = 64) private String username; - /** - * The encrypted password for user authentication. - */ - @Column - private String password; - /** * The user's complete full name. */ @@ -124,10 +118,9 @@ public class User { public User() { } - public User(Long id, String username, String password, String fullName, String email, String regionAbbreviation, String phoneNumber, String avatarUrl, UserStatus status, Long departmentId, Long positionId, LocalDateTime createdAt, LocalDateTime updatedAt) { + public User(Long id, String username, String fullName, String email, String regionAbbreviation, String phoneNumber, String avatarUrl, UserStatus status, Long departmentId, Long positionId, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.username = username; - this.password = password; this.fullName = fullName; this.email = email; this.regionAbbreviation = regionAbbreviation; @@ -176,24 +169,6 @@ public class User { this.username = username; } - /** - * Gets the encrypted password. - * - * @return the encrypted password - */ - public String getPassword() { - return password; - } - - /** - * Sets the encrypted password. - * - * @param password the encrypted password (never plain text) - */ - public void setPassword(String password) { - this.password = password; - } - public String getFullName() { return fullName; } @@ -337,7 +312,6 @@ public class User { public static class UserBuilder { private Long id; private String username; - private String password; private String fullName; private String email; private String regionAbbreviation; @@ -362,11 +336,6 @@ public class User { return this; } - public UserBuilder password(String password) { - this.password = password; - return this; - } - public UserBuilder fullName(String fullName) { this.fullName = fullName; return this; @@ -423,7 +392,7 @@ public class User { * @return a new User instance */ public User build() { - return new User(id, username, password, fullName, email, regionAbbreviation, phoneNumber, avatarUrl, status, departmentId, positionId, createdAt, updatedAt); + return new User(id, username, fullName, email, regionAbbreviation, phoneNumber, avatarUrl, status, departmentId, positionId, createdAt, updatedAt); } } diff --git a/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java b/src/main/java/com/onixbyte/helix/domain/entity/UserCredential.java similarity index 78% rename from src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java rename to src/main/java/com/onixbyte/helix/domain/entity/UserCredential.java index 8181256..243a3aa 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/UserIdentity.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/UserCredential.java @@ -1,7 +1,7 @@ package com.onixbyte.helix.domain.entity; -import com.onixbyte.helix.enumeration.IdentityProvider; -import com.onixbyte.helix.domain.entity.embeddable.UserIdentityId; +import com.onixbyte.helix.enumeration.CredentialProvider; +import com.onixbyte.helix.domain.entity.embeddable.UserCredentialId; import jakarta.persistence.*; // 导入 Jakarta Persistence API import java.time.LocalDateTime; import java.util.Objects; @@ -19,14 +19,14 @@ import java.util.Objects; * @since 1.0 */ @Entity -@Table(name = "user_identities") -public class UserIdentity { +@Table(name = "user_credentials") +public class UserCredential { /** * The composite primary key for the entity, composed of userId, provider, and externalId. */ @EmbeddedId - private UserIdentityId id; + private UserCredentialId id; /** * The timestamp when this identity mapping was created. @@ -53,7 +53,7 @@ public class UserIdentity { * @param userId the user ID */ public void setUserId(Long userId) { - if (this.id == null) this.id = new UserIdentityId(); + if (this.id == null) this.id = new UserCredentialId(); this.id.setUserId(userId); } @@ -61,7 +61,7 @@ public class UserIdentity { * Gets the external identity provider from the composite primary key. * @return the provider */ - public IdentityProvider getProvider() { + public CredentialProvider getProvider() { return this.id != null ? this.id.getProvider() : null; } @@ -69,8 +69,8 @@ public class UserIdentity { * Sets the external identity provider within the composite primary key. * @param provider the provider */ - public void setProvider(IdentityProvider provider) { - if (this.id == null) this.id = new UserIdentityId(); + public void setProvider(CredentialProvider provider) { + if (this.id == null) this.id = new UserCredentialId(); this.id.setProvider(provider); } @@ -78,17 +78,17 @@ public class UserIdentity { * Gets the unique identifier from the external provider from the composite primary key. * @return the external ID */ - public String getExternalId() { - return this.id != null ? this.id.getExternalId() : null; + public String getCredential() { + return this.id != null ? this.id.getCredential() : null; } /** * Sets the unique identifier from the external provider within the composite primary key. - * @param externalId the external ID + * @param credential the external ID */ - public void setExternalId(String externalId) { - if (this.id == null) this.id = new UserIdentityId(); - this.id.setExternalId(externalId); + public void setCredential(String credential) { + if (this.id == null) this.id = new UserCredentialId(); + this.id.setCredential(credential); } public LocalDateTime getCreatedAt() { @@ -109,12 +109,12 @@ public class UserIdentity { // --- Constructors (Adjusted for EmbeddedId) --- - public UserIdentity() { - this.id = new UserIdentityId(); // Initialize ID object for safety + public UserCredential() { + this.id = new UserCredentialId(); // Initialize ID object for safety } - public UserIdentity(Long userId, IdentityProvider provider, String externalId, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.id = new UserIdentityId(userId, provider, externalId); + public UserCredential(Long userId, CredentialProvider provider, String externalId, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = new UserCredentialId(userId, provider, externalId); this.createdAt = createdAt; this.updatedAt = updatedAt; } @@ -125,7 +125,7 @@ public class UserIdentity { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - UserIdentity that = (UserIdentity) o; + UserCredential that = (UserCredential) o; return Objects.equals(id, that.id); // Entity equality based on primary key } @@ -139,7 +139,7 @@ public class UserIdentity { return "UserIdentity{" + "userId=" + getUserId() + ", provider=" + getProvider() + - ", externalId='" + getExternalId() + '\'' + + ", externalId='" + getCredential() + '\'' + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + '}'; @@ -162,7 +162,7 @@ public class UserIdentity { */ public static class UserIdentityBuilder { private Long userId; - private IdentityProvider provider; + private CredentialProvider provider; private String externalId; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -175,7 +175,7 @@ public class UserIdentity { return this; } - public UserIdentityBuilder provider(IdentityProvider provider) { + public UserIdentityBuilder provider(CredentialProvider provider) { this.provider = provider; return this; } @@ -200,8 +200,8 @@ public class UserIdentity { * * @return a new UserIdentity instance */ - public UserIdentity build() { - return new UserIdentity(userId, provider, externalId, createdAt, updatedAt); + public UserCredential build() { + return new UserCredential(userId, provider, externalId, createdAt, updatedAt); } } diff --git a/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserIdentityId.java b/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserCredentialId.java similarity index 70% rename from src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserIdentityId.java rename to src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserCredentialId.java index 3ec828f..ff3c994 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserIdentityId.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/embeddable/UserCredentialId.java @@ -1,6 +1,6 @@ package com.onixbyte.helix.domain.entity.embeddable; -import com.onixbyte.helix.enumeration.IdentityProvider; +import com.onixbyte.helix.enumeration.CredentialProvider; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import jakarta.persistence.Enumerated; @@ -18,7 +18,7 @@ import java.util.Objects; * from that provider. */ @Embeddable -public class UserIdentityId implements Serializable { +public class UserCredentialId implements Serializable { @Serial private static final long serialVersionUID = 1L; @@ -37,23 +37,23 @@ public class UserIdentityId implements Serializable { @Column(nullable = false) @Enumerated @JdbcType(PostgreSQLEnumJdbcType.class) - private IdentityProvider provider; + private CredentialProvider provider; /** * The unique identifier from the external provider, corresponding to the 'external_id' column. */ @Column(nullable = false) - private String externalId; + private String credential; // --- Constructors --- - public UserIdentityId() { + public UserCredentialId() { } - public UserIdentityId(Long userId, IdentityProvider provider, String externalId) { + public UserCredentialId(Long userId, CredentialProvider provider, String credential) { this.userId = userId; this.provider = provider; - this.externalId = externalId; + this.credential = credential; } // --- Getters and Setters (Omitted for brevity, but should exist) --- @@ -65,32 +65,32 @@ public class UserIdentityId implements Serializable { this.userId = userId; } - public IdentityProvider getProvider() { + public CredentialProvider getProvider() { return provider; } - public void setProvider(IdentityProvider provider) { + public void setProvider(CredentialProvider provider) { this.provider = provider; } - public String getExternalId() { - return externalId; + public String getCredential() { + return credential; } - public void setExternalId(String externalId) { - this.externalId = externalId; + public void setCredential(String credential) { + this.credential = credential; } // --- equals and hashCode (REQUIRED for composite keys) --- @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; - UserIdentityId that = (UserIdentityId) o; - return Objects.equals(userId, that.userId) && provider == that.provider && Objects.equals(externalId, that.externalId); + UserCredentialId that = (UserCredentialId) o; + return Objects.equals(userId, that.userId) && provider == that.provider && Objects.equals(credential, that.credential); } @Override public int hashCode() { - return Objects.hash(userId, provider, externalId); + return Objects.hash(userId, provider, credential); } } diff --git a/src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java b/src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java index aeab9d4..2abfa03 100644 --- a/src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java +++ b/src/main/java/com/onixbyte/helix/domain/view/UserIdentityView.java @@ -1,7 +1,7 @@ package com.onixbyte.helix.domain.view; -import com.onixbyte.helix.enumeration.IdentityProvider; -import com.onixbyte.helix.domain.entity.UserIdentity; +import com.onixbyte.helix.enumeration.CredentialProvider; +import com.onixbyte.helix.domain.entity.UserCredential; import java.time.LocalDateTime; import java.util.Objects; @@ -26,7 +26,7 @@ public class UserIdentityView { /** * The external identity provider. */ - private IdentityProvider provider; + private CredentialProvider provider; /** * The unique identifier from the external provider. @@ -52,8 +52,8 @@ public class UserIdentityView { /** * Constructor with all fields. */ - public UserIdentityView(Long userId, IdentityProvider provider, String externalId, - LocalDateTime createdAt, LocalDateTime updatedAt) { + public UserIdentityView(Long userId, CredentialProvider provider, String externalId, + LocalDateTime createdAt, LocalDateTime updatedAt) { this.userId = userId; this.provider = provider; this.externalId = externalId; @@ -64,19 +64,19 @@ public class UserIdentityView { /** * Creates a UserIdentityView from a UserIdentity entity. * - * @param userIdentity the UserIdentity entity + * @param userCredential the UserIdentity entity * @return the UserIdentityView object */ - public static UserIdentityView fromEntity(UserIdentity userIdentity) { - if (userIdentity == null) { + public static UserIdentityView fromEntity(UserCredential userCredential) { + if (userCredential == null) { return null; } return new UserIdentityView( - userIdentity.getUserId(), - userIdentity.getProvider(), - userIdentity.getExternalId(), - userIdentity.getCreatedAt(), - userIdentity.getUpdatedAt() + userCredential.getUserId(), + userCredential.getProvider(), + userCredential.getCredential(), + userCredential.getCreatedAt(), + userCredential.getUpdatedAt() ); } @@ -88,11 +88,11 @@ public class UserIdentityView { this.userId = userId; } - public IdentityProvider getProvider() { + public CredentialProvider getProvider() { return provider; } - public void setProvider(IdentityProvider provider) { + public void setProvider(CredentialProvider provider) { this.provider = provider; } @@ -162,7 +162,7 @@ public class UserIdentityView { */ public static class UserIdentityViewBuilder { private Long userId; - private IdentityProvider provider; + private CredentialProvider provider; private String externalId; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -175,7 +175,7 @@ public class UserIdentityView { return this; } - public UserIdentityViewBuilder provider(IdentityProvider provider) { + public UserIdentityViewBuilder provider(CredentialProvider provider) { this.provider = provider; return this; } diff --git a/src/main/java/com/onixbyte/helix/domain/web/response/LoginSuccessResponse.java b/src/main/java/com/onixbyte/helix/domain/web/response/LoginSuccessResponse.java index 94f1643..997c742 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/response/LoginSuccessResponse.java +++ b/src/main/java/com/onixbyte/helix/domain/web/response/LoginSuccessResponse.java @@ -6,8 +6,4 @@ public record LoginSuccessResponse( String accessToken, User user ) { - - public LoginSuccessResponse { - user.setPassword(null); - } } diff --git a/src/main/java/com/onixbyte/helix/enumeration/IdentityProvider.java b/src/main/java/com/onixbyte/helix/enumeration/CredentialProvider.java similarity index 58% rename from src/main/java/com/onixbyte/helix/enumeration/IdentityProvider.java rename to src/main/java/com/onixbyte/helix/enumeration/CredentialProvider.java index 068ee13..50cc99b 100644 --- a/src/main/java/com/onixbyte/helix/enumeration/IdentityProvider.java +++ b/src/main/java/com/onixbyte/helix/enumeration/CredentialProvider.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + package com.onixbyte.helix.enumeration; import com.onixbyte.helix.config.AuthenticationConfig; @@ -18,7 +40,7 @@ import com.onixbyte.helix.config.AuthenticationConfig; * @since 1.0.0 * @see AuthenticationConfig */ -public enum IdentityProvider { +public enum CredentialProvider { /** * Local identity provider using the application's internal user database. diff --git a/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java index 9f3e9e6..2ad79e9 100644 --- a/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java +++ b/src/main/java/com/onixbyte/helix/filter/TokenAuthenticationFilter.java @@ -67,8 +67,6 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { .map((authority) -> (GrantedAuthority) authority::getCode) .toList(); - user.setPassword(null); - var authentication = UsernamePasswordAuthentication.authenticated(user, authorities); SecurityContextHolder.getContext().setAuthentication(authentication); filterChain.doFilter(request, response); diff --git a/src/main/java/com/onixbyte/helix/manager/UserManager.java b/src/main/java/com/onixbyte/helix/manager/UserManager.java index 2bf0e72..80dcbc1 100644 --- a/src/main/java/com/onixbyte/helix/manager/UserManager.java +++ b/src/main/java/com/onixbyte/helix/manager/UserManager.java @@ -1,6 +1,8 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.common.regex.Patterns; +import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; +import com.onixbyte.helix.mapper.UserCredentialMapper; import com.onixbyte.helix.shared.CacheName; import com.onixbyte.helix.domain.database.query.wrapper.QueryUserWrapper; import com.onixbyte.helix.domain.entity.User; @@ -30,16 +32,19 @@ public class UserManager { private final UserMapper userMapper; private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final UserCredentialMapper userCredentialMapper; @Autowired public UserManager( UserMapper userMapper, UserRepository userRepository, - PasswordEncoder passwordEncoder + PasswordEncoder passwordEncoder, + UserCredentialMapper userCredentialMapper ) { this.userMapper = userMapper; this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; + this.userCredentialMapper = userCredentialMapper; } /** @@ -87,11 +92,6 @@ public class UserManager { var userToUpdate = userRepository.findById(user.getId()) .orElseThrow(() -> new BizException(HttpStatus.BAD_REQUEST, "找不到 ID 为" + user.getId() + "的用户信息")); - Optional.ofNullable(user.getPassword()) - .filter(StringUtils::isNotBlank) - .map(passwordEncoder::encode) - .ifPresent(userToUpdate::setPassword); - Optional.ofNullable(user.getFullName()) .filter(StringUtils::isNotBlank) .ifPresent(userToUpdate::setFullName); @@ -126,4 +126,12 @@ public class UserManager { return userToUpdate; } + + @Transactional(rollbackFor = Throwable.class) + public void updateUserPassword(ResetPasswordRequest request) { + userCredentialMapper.updateUserCredential( + request.id(), + passwordEncoder.encode(request.password()) + ); + } } diff --git a/src/main/java/com/onixbyte/helix/mapper/UserCredentialMapper.java b/src/main/java/com/onixbyte/helix/mapper/UserCredentialMapper.java new file mode 100644 index 0000000..23099c6 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/mapper/UserCredentialMapper.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.mapper; + +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserCredentialMapper { + + int updateUserCredential( + @Param("userId") Long userId, + @Param("encodedPassword") String encodedPassword + ); +} diff --git a/src/main/java/com/onixbyte/helix/repository/UserCredentialRepository.java b/src/main/java/com/onixbyte/helix/repository/UserCredentialRepository.java new file mode 100644 index 0000000..99825eb --- /dev/null +++ b/src/main/java/com/onixbyte/helix/repository/UserCredentialRepository.java @@ -0,0 +1,10 @@ +package com.onixbyte.helix.repository; + +import com.onixbyte.helix.domain.entity.UserCredential; +import com.onixbyte.helix.domain.entity.embeddable.UserCredentialId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserCredentialRepository extends JpaRepository { +} diff --git a/src/main/java/com/onixbyte/helix/repository/UserIdentityRepository.java b/src/main/java/com/onixbyte/helix/repository/UserIdentityRepository.java deleted file mode 100644 index 932c91c..0000000 --- a/src/main/java/com/onixbyte/helix/repository/UserIdentityRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.onixbyte.helix.repository; - -import com.onixbyte.helix.domain.entity.UserIdentity; -import com.onixbyte.helix.domain.entity.embeddable.UserIdentityId; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface UserIdentityRepository extends JpaRepository { -} diff --git a/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java b/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java index dc20d46..35f72fc 100644 --- a/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java +++ b/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java @@ -1,13 +1,17 @@ package com.onixbyte.helix.security.provider; import com.onixbyte.helix.domain.entity.Authority; +import com.onixbyte.helix.domain.entity.UserCredential; +import com.onixbyte.helix.enumeration.CredentialProvider; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.AuthorityManager; import com.onixbyte.helix.manager.UserManager; +import com.onixbyte.helix.repository.UserCredentialRepository; import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; @@ -24,16 +28,19 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro private final UserManager userManager; private final PasswordEncoder passwordEncoder; private final AuthorityManager authorityManager; + private final UserCredentialRepository userCredentialRepository; @Autowired public UsernamePasswordAuthenticationProvider( UserManager userManager, PasswordEncoder passwordEncoder, - AuthorityManager authorityManager + AuthorityManager authorityManager, + UserCredentialRepository userCredentialRepository ) { this.userManager = userManager; this.passwordEncoder = passwordEncoder; this.authorityManager = authorityManager; + this.userCredentialRepository = userCredentialRepository; } @Override @@ -49,14 +56,20 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro throw new BizException(HttpStatus.UNAUTHORIZED, "用户名或密码错误。"); } + // get user credentials from database + var userCredentials = userCredentialRepository.findOne(Example.of(UserCredential.builder() + .provider(CredentialProvider.LOCAL) + .userId(user.getId()) + .build())) + .orElseThrow(() -> new BizException(HttpStatus.UNAUTHORIZED, "您还没有配置密码,请使用第三方账号登录")); + // validate password - if (!passwordEncoder.matches(usernamePasswordAuthentication.getCredentials(), user.getPassword())) { + if (!passwordEncoder.matches(usernamePasswordAuthentication.getCredentials(), userCredentials.getCredential())) { log.error("User {} is trying to authenticate but password is incorrect.", usernamePasswordAuthentication.getPrincipal()); throw new BizException(HttpStatus.UNAUTHORIZED, "用户名或密码错误。"); } // erase credentials - user.setPassword(null); usernamePasswordAuthentication.eraseCredentials(); // get authorities diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index fa71d9d..1837d30 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -3,7 +3,9 @@ package com.onixbyte.helix.service; import com.onixbyte.helix.domain.entity.Setting; import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.domain.web.request.LoginRequest; +import com.onixbyte.helix.enumeration.ApplicationMode; import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.manager.ApplicationManager; import com.onixbyte.helix.manager.CaptchaManager; import com.onixbyte.helix.manager.SecurityManager; import com.onixbyte.helix.manager.SettingManager; @@ -30,18 +32,20 @@ public class AuthService { private final AuthenticationManager authenticationManager; private final SettingManager settingManager; private final SecurityManager securityManager; + private final ApplicationManager applicationManager; @Autowired public AuthService( CaptchaManager captchaManager, AuthenticationManager authenticationManager, SettingManager settingManager, - SecurityManager securityManager - ) { + SecurityManager securityManager, + ApplicationManager applicationManager) { this.captchaManager = captchaManager; this.authenticationManager = authenticationManager; this.settingManager = settingManager; this.securityManager = securityManager; + this.applicationManager = applicationManager; } /** @@ -94,10 +98,22 @@ public class AuthService { public ResponseCookie buildCookie(String cookieName, String token) { var cookieBuilder = ResponseCookie.from(cookieName, token) - .httpOnly(true) .maxAge(securityManager.getTokenValidDuration()) + .secure(true) .path("/"); + var applicationMode = applicationManager.getApplicationMode(); + switch (applicationMode) { + case PRODUCTION -> { + cookieBuilder.httpOnly(true); + } + case DEVELOPMENT -> { + cookieBuilder.sameSite("NONE"); + } + case null, default -> { + } + } + return cookieBuilder.build(); } diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index 361aa7d..d536cd5 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -12,6 +12,8 @@ import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; import com.onixbyte.helix.domain.web.request.EditUserRequest; import com.onixbyte.helix.domain.web.response.UserDetailResponse; import com.onixbyte.helix.manager.*; +import com.onixbyte.helix.mapper.UserCredentialMapper; +import com.onixbyte.helix.repository.UserCredentialRepository; import com.onixbyte.identitygenerator.IdentityGenerator; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -35,6 +37,8 @@ public class UserService { private final ApplicationManager applicationManager; private final DepartmentManager departmentManager; private final PositionManager positionManager; + private final UserCredentialRepository userCredentialRepository; + private final UserCredentialMapper userCredentialMapper; @Autowired public UserService( @@ -45,8 +49,8 @@ public class UserService { PasswordEncoder passwordEncoder, ApplicationManager applicationManager, DepartmentManager departmentManager, - PositionManager positionManager - ) { + PositionManager positionManager, + UserCredentialRepository userCredentialRepository, UserCredentialMapper userCredentialMapper) { this.userManager = userManager; this.userIdentityGenerator = userIdentityGenerator; this.roleManager = roleManager; @@ -55,6 +59,8 @@ public class UserService { this.applicationManager = applicationManager; this.departmentManager = departmentManager; this.positionManager = positionManager; + this.userCredentialRepository = userCredentialRepository; + this.userCredentialMapper = userCredentialMapper; } public Page queryUserDetailsPage(Pageable pageable, QueryUserRequest request) { @@ -98,7 +104,6 @@ public class UserService { var user = userManager.save(User.builder() .id(userIdentityGenerator.nextId()) .username(request.username()) - .password(passwordEncoder.encode(request.password())) .fullName(request.fullName()) .email(request.email()) .regionAbbreviation(request.regionAbbreviation()) @@ -168,10 +173,7 @@ public class UserService { @Transactional(rollbackFor = Throwable.class) public void resetPassword(ResetPasswordRequest request) { - userManager.updateUser(User.builder() - .id(request.id()) - .password(request.password()) - .build()); + userManager.updateUserPassword(request); } @Transactional(rollbackFor = Throwable.class) diff --git a/src/main/resources/mapper/UserCredentialMapper.xml b/src/main/resources/mapper/UserCredentialMapper.xml new file mode 100644 index 0000000..1166a0e --- /dev/null +++ b/src/main/resources/mapper/UserCredentialMapper.xml @@ -0,0 +1,33 @@ + + + + + + + UPDATE user_credentials + SET credential = #{encodedPassword} + WHERE user_id = #{userId} + AND provider = 'LOCAL'::credential_provider + + \ No newline at end of file From 14e53740b0a5a167941cd3aad2fce8afa58810ec Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 12 Mar 2026 14:38:06 +0800 Subject: [PATCH 42/60] refactor: remove unused imports and fields --- .../onixbyte/helix/domain/view/DepartmentView.java | 6 ------ .../UnauthorisedAuthenticationEntryPoint.java | 1 - .../com/onixbyte/helix/service/AuthService.java | 9 ++------- .../com/onixbyte/helix/service/UserService.java | 14 ++------------ 4 files changed, 4 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java b/src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java index 4edf143..d5f7b6b 100644 --- a/src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java +++ b/src/main/java/com/onixbyte/helix/domain/view/DepartmentView.java @@ -209,7 +209,6 @@ public class DepartmentView { private Long id; private String name; private Long parentId; - private String treePath; private Integer sort; private Status status; private LocalDateTime createdAt; @@ -233,11 +232,6 @@ public class DepartmentView { return this; } - public DepartmentViewBuilder treePath(String treePath) { - this.treePath = treePath; - return this; - } - public DepartmentViewBuilder sort(Integer sort) { this.sort = sort; return this; diff --git a/src/main/java/com/onixbyte/helix/security/entrypoint/UnauthorisedAuthenticationEntryPoint.java b/src/main/java/com/onixbyte/helix/security/entrypoint/UnauthorisedAuthenticationEntryPoint.java index 4c3818d..2a2a9b0 100644 --- a/src/main/java/com/onixbyte/helix/security/entrypoint/UnauthorisedAuthenticationEntryPoint.java +++ b/src/main/java/com/onixbyte/helix/security/entrypoint/UnauthorisedAuthenticationEntryPoint.java @@ -2,7 +2,6 @@ package com.onixbyte.helix.security.entrypoint; import com.fasterxml.jackson.databind.ObjectMapper; import com.onixbyte.helix.domain.web.response.BizExceptionResponse; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index 1837d30..37408bc 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -3,7 +3,6 @@ package com.onixbyte.helix.service; import com.onixbyte.helix.domain.entity.Setting; import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.domain.web.request.LoginRequest; -import com.onixbyte.helix.enumeration.ApplicationMode; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.ApplicationManager; import com.onixbyte.helix.manager.CaptchaManager; @@ -104,12 +103,8 @@ public class AuthService { var applicationMode = applicationManager.getApplicationMode(); switch (applicationMode) { - case PRODUCTION -> { - cookieBuilder.httpOnly(true); - } - case DEVELOPMENT -> { - cookieBuilder.sameSite("NONE"); - } + case PRODUCTION -> cookieBuilder.httpOnly(true); + case DEVELOPMENT -> cookieBuilder.sameSite("NONE"); case null, default -> { } } diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index d536cd5..42fcf65 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -12,14 +12,11 @@ import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; import com.onixbyte.helix.domain.web.request.EditUserRequest; import com.onixbyte.helix.domain.web.response.UserDetailResponse; import com.onixbyte.helix.manager.*; -import com.onixbyte.helix.mapper.UserCredentialMapper; -import com.onixbyte.helix.repository.UserCredentialRepository; import com.onixbyte.identitygenerator.IdentityGenerator; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,12 +30,9 @@ public class UserService { private final IdentityGenerator userIdentityGenerator; private final RoleManager roleManager; private final UserRoleManager userRoleManager; - private final PasswordEncoder passwordEncoder; private final ApplicationManager applicationManager; private final DepartmentManager departmentManager; private final PositionManager positionManager; - private final UserCredentialRepository userCredentialRepository; - private final UserCredentialMapper userCredentialMapper; @Autowired public UserService( @@ -46,21 +40,17 @@ public class UserService { IdentityGenerator userIdentityGenerator, RoleManager roleManager, UserRoleManager userRoleManager, - PasswordEncoder passwordEncoder, ApplicationManager applicationManager, DepartmentManager departmentManager, - PositionManager positionManager, - UserCredentialRepository userCredentialRepository, UserCredentialMapper userCredentialMapper) { + PositionManager positionManager + ) { this.userManager = userManager; this.userIdentityGenerator = userIdentityGenerator; this.roleManager = roleManager; this.userRoleManager = userRoleManager; - this.passwordEncoder = passwordEncoder; this.applicationManager = applicationManager; this.departmentManager = departmentManager; this.positionManager = positionManager; - this.userCredentialRepository = userCredentialRepository; - this.userCredentialMapper = userCredentialMapper; } public Page queryUserDetailsPage(Pageable pageable, QueryUserRequest request) { From bd597fbc6415cc62175e9d2c4307aa51472c2409 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 12 Mar 2026 14:44:59 +0800 Subject: [PATCH 43/60] refactor: extract build cookie --- .../onixbyte/helix/service/AuthService.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index 37408bc..82ea596 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -95,29 +95,29 @@ public class AuthService { .orElse(false); } - public ResponseCookie buildCookie(String cookieName, String token) { - var cookieBuilder = ResponseCookie.from(cookieName, token) - .maxAge(securityManager.getTokenValidDuration()) - .secure(true) - .path("/"); - - var applicationMode = applicationManager.getApplicationMode(); - switch (applicationMode) { - case PRODUCTION -> cookieBuilder.httpOnly(true); - case DEVELOPMENT -> cookieBuilder.sameSite("NONE"); - case null, default -> { - } - } - - return cookieBuilder.build(); + public ResponseCookie buildCookie(String cookieName, String value) { + return buildCookieInternal(cookieName, value, securityManager.getTokenValidDuration()) + .build(); } - public ResponseCookie buildCookie(String cookieName, String token, Duration validDuration) { - var cookieBuilder = ResponseCookie.from(cookieName, token) - .httpOnly(true) + public ResponseCookie buildCookie(String cookieName, String value, Duration validDuration) { + return buildCookieInternal(cookieName, value, validDuration) + .build(); + } + + protected ResponseCookie.ResponseCookieBuilder buildCookieInternal(String cookieName, String value, Duration validDuration) { + var applicationMode = applicationManager.getApplicationMode(); + + var cookieBuilder = ResponseCookie.from(cookieName, value) + .maxAge(securityManager.getTokenValidDuration()) + .secure(true) .maxAge(validDuration) .path("/"); - return cookieBuilder.build(); + return switch (applicationMode) { + case PRODUCTION -> cookieBuilder.httpOnly(true); + case DEVELOPMENT -> cookieBuilder.sameSite("NONE"); + case null -> cookieBuilder; + }; } } From abbcb6bf5c6cce5fbfe383a43c10c4587e7e8bfc Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 12 Mar 2026 15:37:51 +0800 Subject: [PATCH 44/60] feat: enhance cookie creation method with detailed documentation --- .../com/onixbyte/helix/service/AuthService.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index 82ea596..d7304c8 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -105,10 +105,22 @@ public class AuthService { .build(); } - protected ResponseCookie.ResponseCookieBuilder buildCookieInternal(String cookieName, String value, Duration validDuration) { + /** + * Creates a response cookie builder with specified name, value and valid duration. + * + * @param name name of the cookie + * @param value value of the cookie + * @param validDuration valid duration of the cookie + * @return cookie builder + */ + protected ResponseCookie.ResponseCookieBuilder buildCookieInternal( + String name, + String value, + Duration validDuration + ) { var applicationMode = applicationManager.getApplicationMode(); - var cookieBuilder = ResponseCookie.from(cookieName, value) + var cookieBuilder = ResponseCookie.from(name, value) .maxAge(securityManager.getTokenValidDuration()) .secure(true) .maxAge(validDuration) From bfa0690065523eb56673a22cc79390ca4d328ffc Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 12 Mar 2026 17:25:19 +0800 Subject: [PATCH 45/60] fix: update cookie settings for production and development modes --- src/main/java/com/onixbyte/helix/service/AuthService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index d7304c8..1411a56 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -127,8 +127,11 @@ public class AuthService { .path("/"); return switch (applicationMode) { - case PRODUCTION -> cookieBuilder.httpOnly(true); - case DEVELOPMENT -> cookieBuilder.sameSite("NONE"); + case PRODUCTION -> cookieBuilder + .httpOnly(true) + .sameSite("LAX"); + case DEVELOPMENT -> cookieBuilder + .sameSite("NONE"); case null -> cookieBuilder; }; } From 7c9f9c35f97409ebb69125d7d71997617f0507ed Mon Sep 17 00:00:00 2001 From: zihluwang Date: Sat, 14 Mar 2026 08:28:43 +0800 Subject: [PATCH 46/60] chore: rename settings --- database/init.d/init-en_GB.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/init.d/init-en_GB.sql b/database/init.d/init-en_GB.sql index 9be7216..7bfd7c0 100644 --- a/database/init.d/init-en_GB.sql +++ b/database/init.d/init-en_GB.sql @@ -361,9 +361,9 @@ COMMENT ON COLUMN settings.value IS 'Setting current value.'; COMMENT ON COLUMN settings.default_value IS 'Setting default value.'; INSERT INTO settings(name, description, type, value, default_value) -VALUES ('captcha-setting::enabled', 'Whether captcha is enabled.', 'BOOLEAN'::SETTING_TYPE, 'true', +VALUES ('captcha-enabled', 'Whether captcha is enabled.', 'BOOLEAN'::SETTING_TYPE, 'true', 'false'), - ('auth-setting::register-enabled', 'Whether register is enabled', 'BOOLEAN'::SETTING_TYPE, + ('register-enabled', 'Whether register is enabled', 'BOOLEAN'::SETTING_TYPE, 'true', 'false'); DROP TABLE IF EXISTS menus; From 484a9f4a71fb893f470f1ba342e39714b6c4c7c6 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 19 Mar 2026 10:54:50 +0800 Subject: [PATCH 47/60] feat: update database schema and configuration for Flyway integration and refactor table names --- build.gradle.kts | 2 + gradle/libs.versions.toml | 2 + src/main/resources/application.yml | 7 +- .../resources/db/migrations/V1__init_db.sql | 213 +++++++++--------- src/main/resources/mapper/AuthorityMapper.xml | 6 +- src/main/resources/mapper/MenuMapper.xml | 10 +- .../resources/mapper/RoleAuthorityMapper.xml | 2 +- src/main/resources/mapper/RoleMapper.xml | 6 +- .../resources/mapper/UserCredentialMapper.xml | 2 +- src/main/resources/mapper/UserMapper.xml | 37 ++- 10 files changed, 145 insertions(+), 142 deletions(-) rename database/init.d/init-en_GB.sql => src/main/resources/db/migrations/V1__init_db.sql (67%) diff --git a/build.gradle.kts b/build.gradle.kts index 662fcac..17ddcac 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,6 +47,8 @@ dependencies { implementation(libs.spring.boot.starter.security) implementation(libs.spring.boot.starter.jpa) implementation(libs.mybatis.starter.core) + implementation(libs.flyway.core) + implementation(libs.flyway.postgresql) implementation(libs.jackson.jsr310) testImplementation(libs.spring.boot.starter.test) testImplementation(libs.reactor.test) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index afbf926..2a01562 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,6 +43,8 @@ hypersistence-core = { group = "io.hypersistence", name = "hypersistence-utils-h postgres-driver = { group = "org.postgresql", name = "postgresql", version.ref = "postgresDriverVersion" } h2-database = { group = "com.h2database", name = "h2", version.ref = "h2Version" } spring-boot-starter-redis = { group = "org.springframework.boot", name = "spring-boot-starter-data-redis", version.ref = "springBootVersion" } +flyway-core = { group = "org.flywaydb", name = "flyway-core" } +flyway-postgresql = { group = "org.flywaydb", name = "flyway-database-postgresql" } # Spring Boot Core & Web spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 761998a..dc7bbad 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,12 +21,17 @@ spring: # No need to use distributed transaction manager for 1 datasource. platform: org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform hibernate: - ddl-auto: validate + ddl-auto: none open-in-view: false datasource: hikari: minimum-idle: 1 maximum-pool-size: 10 + flyway: + enabled: true + baseline-on-migrate: true + locations: classpath:db/migrations + baseline-version: 0 mybatis: diff --git a/database/init.d/init-en_GB.sql b/src/main/resources/db/migrations/V1__init_db.sql similarity index 67% rename from database/init.d/init-en_GB.sql rename to src/main/resources/db/migrations/V1__init_db.sql index 7bfd7c0..5073696 100644 --- a/database/init.d/init-en_GB.sql +++ b/src/main/resources/db/migrations/V1__init_db.sql @@ -27,7 +27,7 @@ * please ensure this SQL file is executed by the **corresponding database user**. * * This is crucial for correctly setting up ownership and default privileges. * * Example: CREATE DATABASE my_database OWNER app_user; - * * Ensure all necessary user roles and permissions are in place **prior** to + * * Ensure all necessary user role and permissions are in place **prior** to * running this initialisation script. */ @@ -43,61 +43,61 @@ CREATE TYPE CREDENTIAL_PROVIDER AS ENUM ('LOCAL', 'OIDC', 'MICROSOFT_ENTRA_ID', CREATE TYPE SETTING_TYPE AS ENUM ('STRING', 'BOOLEAN', 'INT'); --- Departments Table --- -DROP TABLE IF EXISTS departments CASCADE; -CREATE TABLE departments +DROP TABLE IF EXISTS department CASCADE; +CREATE TABLE department ( id BIGSERIAL PRIMARY KEY, name VARCHAR(128) NOT NULL UNIQUE, - parent_id BIGINT NULL REFERENCES departments (id), + parent_id BIGINT NULL REFERENCES department (id), sort INT NOT NULL DEFAULT NULL, status STATUS NOT NULL DEFAULT 'ACTIVE'::STATUS, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX departments_name_index ON departments (name); -CREATE INDEX departments_parent_id_index ON departments (parent_id); +CREATE INDEX department_name_index ON department (name); +CREATE INDEX department_parent_id_index ON department (parent_id); --- Departments Data Insertion --- WITH company_hq AS ( - INSERT INTO departments (name, parent_id, sort, status) + INSERT INTO department (name, parent_id, sort, status) VALUES ('Company HQ', NULL, 1, 'ACTIVE'::STATUS) RETURNING id), human_resources AS ( - INSERT INTO departments (name, parent_id, sort, status) + INSERT INTO department (name, parent_id, sort, status) SELECT 'Human Resources', company_hq.id, 1, 'ACTIVE'::STATUS FROM company_hq RETURNING id), finance AS ( - INSERT INTO departments (name, parent_id, sort, status) + INSERT INTO department (name, parent_id, sort, status) SELECT 'Finance', company_hq.id, 2, 'ACTIVE'::STATUS FROM company_hq RETURNING id), technology AS ( - INSERT INTO departments (name, parent_id, sort, status) + INSERT INTO department (name, parent_id, sort, status) SELECT 'Technology', company_hq.id, 3, 'ACTIVE'::STATUS FROM company_hq RETURNING id), it_support AS ( - INSERT INTO departments (name, parent_id, sort, status) + INSERT INTO department (name, parent_id, sort, status) SELECT 'IT Support', technology.id, 1, 'ACTIVE'::STATUS FROM technology RETURNING id), software_development AS ( - INSERT INTO departments (name, parent_id, sort, status) + INSERT INTO department (name, parent_id, sort, status) SELECT 'Software Development', technology.id, 2, 'ACTIVE'::STATUS FROM technology RETURNING id), operations AS ( - INSERT INTO departments (name, parent_id, sort, status) + INSERT INTO department (name, parent_id, sort, status) SELECT 'Operations', company_hq.id, 4, 'INACTIVE'::STATUS FROM company_hq RETURNING id) SELECT NULL; --- Positions Table --- -DROP TABLE IF EXISTS positions CASCADE; -CREATE TABLE positions +DROP TABLE IF EXISTS position CASCADE; +CREATE TABLE position ( id BIGSERIAL PRIMARY KEY, name VARCHAR(128) NOT NULL UNIQUE, @@ -109,12 +109,12 @@ CREATE TABLE positions updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX positions_name_index ON positions (name); -CREATE INDEX positions_code_index ON positions (code); -CREATE INDEX positions_name_code_index ON positions (name, code); +CREATE INDEX position_name_index ON position (name); +CREATE INDEX position_code_index ON position (code); +CREATE INDEX position_name_code_index ON position (name, code); --- Positions Data Insertion --- -INSERT INTO positions (name, code, description, sort, status) +INSERT INTO position (name, code, description, sort, status) VALUES ('HR Manager', 'HR-MGR', 'Responsible for overseeing recruitment, employee relations, and staff wellbeing.', 1, 'ACTIVE'), @@ -131,65 +131,62 @@ VALUES ('HR Manager', 'HR-MGR', 'Assists with day-to-day logistics, procurement, and office organisation.', 5, 'INACTIVE'); --- Users Table --- -DROP TABLE IF EXISTS users CASCADE; -DROP SEQUENCE IF EXISTS users_id_seq CASCADE; -CREATE SEQUENCE users_id_seq START WITH 1; +DROP TABLE IF EXISTS "user" CASCADE; +DROP SEQUENCE IF EXISTS user_id_seq CASCADE; +CREATE SEQUENCE user_id_seq START WITH 1; -CREATE TABLE users +CREATE TABLE "user" ( - id BIGINT PRIMARY KEY DEFAULT nextval('users_id_seq'), + id BIGSERIAL PRIMARY KEY, username VARCHAR(64) UNIQUE NOT NULL, - password VARCHAR(255), full_name VARCHAR(128) NOT NULL, email VARCHAR(128) NULL UNIQUE, region_abbreviation VARCHAR(10) NULL, phone_number VARCHAR(32) NULL, avatar_url TEXT, status USER_STATUS NOT NULL DEFAULT 'ACTIVE', - department_id BIGINT REFERENCES departments (id), - position_id BIGINT REFERENCES positions (id), + department_id BIGINT REFERENCES department (id), + position_id BIGINT REFERENCES position (id), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (region_abbreviation, phone_number) ); -CREATE INDEX users_username_index ON users (username); +CREATE INDEX user_username_index ON "user" (username); --- Users Data Insertion --- -- NOTE: All phone numbers are generated by ChatGPT, they should not be connected any real person. -INSERT INTO users(username, password, full_name, email, region_abbreviation, phone_number, - avatar_url, department_id, position_id, created_at, updated_at) +INSERT INTO "user"(username, full_name, email, region_abbreviation, phone_number, + avatar_url, department_id, position_id, created_at, updated_at) SELECT 'helix', - null, 'Helix Admin', 'admin@helix.onixbyte.dev', 'GB', '7000000000', 'https://gravatar.com/avatar/6ef4c4033f6aa8e43d06bd5e462a6173cc2a960633473721a6f1289cd1b5146f', - (SELECT id FROM departments WHERE name = 'Company HQ'), - (SELECT id FROM positions WHERE code = 'HR-MGR'), + (SELECT id FROM department WHERE name = 'Company HQ'), + (SELECT id FROM position WHERE code = 'HR-MGR'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP; -INSERT INTO users(username, password, full_name, email, region_abbreviation, phone_number, - avatar_url, department_id, position_id, created_at, updated_at) +INSERT INTO "user"(username, full_name, email, region_abbreviation, phone_number, + avatar_url, department_id, position_id, created_at, updated_at) SELECT 'johndoe', - null, 'John Doe', 'johndoe@helix.onixbyte.dev', 'GB', '7000000001', 'https://gravatar.com/avatar/41bcebddd573747d1bd35ef7fae72ebefd6b47f077d42442a2510d35b0c2db92', - (SELECT id FROM departments WHERE name = 'Software Development'), - (SELECT id FROM positions WHERE code = 'SWE-ENG'), + (SELECT id FROM department WHERE name = 'Software Development'), + (SELECT id FROM position WHERE code = 'SWE-ENG'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP; --- User Identities Table --- -DROP TABLE IF EXISTS user_credentials CASCADE; -CREATE TABLE user_credentials +DROP TABLE IF EXISTS user_credential CASCADE; +CREATE TABLE user_credential ( - user_id BIGINT NOT NULL REFERENCES users (id), + user_id BIGINT NOT NULL REFERENCES "user" (id), provider CREDENTIAL_PROVIDER NOT NULL, credential VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -198,8 +195,8 @@ CREATE TABLE user_credentials ); --- Roles Table --- -DROP TABLE IF EXISTS roles CASCADE; -CREATE TABLE roles +DROP TABLE IF EXISTS role CASCADE; +CREATE TABLE role ( id BIGSERIAL PRIMARY KEY, name VARCHAR(128) NOT NULL UNIQUE, @@ -212,40 +209,40 @@ CREATE TABLE roles updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX roles_name_index ON roles (name); -CREATE INDEX roles_code_index ON roles (code); -CREATE INDEX roles_name_code_index ON roles (name, code); +CREATE INDEX role_name_index ON role (name); +CREATE INDEX role_code_index ON role (code); +CREATE INDEX role_name_code_index ON role (name, code); --- Roles Data Insertion --- -INSERT INTO roles (name, code, sort, default_value, description, status, created_at, updated_at) +INSERT INTO role (name, code, sort, default_value, description, status, created_at, updated_at) VALUES ('Admin', 'admin', 1, FALSE, 'Administrator of this system.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('Normal User', 'user', 2, TRUE, 'Normal user of this system.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); --- User Roles Table --- -DROP TABLE IF EXISTS user_roles CASCADE; -CREATE TABLE user_roles +DROP TABLE IF EXISTS user_role CASCADE; +CREATE TABLE user_role ( user_id BIGINT NOT NULL - CONSTRAINT user_roles_users_id_fk REFERENCES users, + CONSTRAINT user_role_user_id_fk REFERENCES "user", role_id BIGINT NOT NULL - CONSTRAINT user_roles_roles_id_fk REFERENCES roles, + CONSTRAINT user_role_role_id_fk REFERENCES role, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - CONSTRAINT user_roles_pk PRIMARY KEY (user_id, role_id) + CONSTRAINT user_role_pk PRIMARY KEY (user_id, role_id) ); --- User Roles Data Insertion --- -INSERT INTO user_roles (user_id, role_id) +INSERT INTO user_role (user_id, role_id) SELECT u.id, r.id -FROM users u - CROSS JOIN roles r +FROM "user" u + CROSS JOIN role r WHERE (u.username = 'helix' AND r.code = 'admin') OR (u.username = 'johndoe' AND r.code = 'user'); --- Authorities Table --- -DROP TABLE IF EXISTS authorities CASCADE; -CREATE TABLE authorities +DROP TABLE IF EXISTS authority CASCADE; +CREATE TABLE authority ( id BIGSERIAL PRIMARY KEY, code VARCHAR(128) NOT NULL UNIQUE, @@ -256,10 +253,10 @@ CREATE TABLE authorities updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX authorities_code_index ON authorities (code); +CREATE INDEX authority_code_index ON authority (code); --- Authorities Data Insertion --- -INSERT INTO authorities(code, name, description, status, created_at, updated_at) +INSERT INTO authority(code, name, description, status, created_at, updated_at) VALUES ('system:dashboard:read', 'Read Dashboard', 'Read dashboard.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('system:user:read', 'Read User', 'Read user.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, @@ -268,49 +265,49 @@ VALUES ('system:dashboard:read', 'Read Dashboard', 'Read dashboard.', 'ACTIVE':: CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('system:user:write', 'Write User', 'Write user, such as add, edit or delete.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - ('system:department:read', 'Read Department', 'Read departments', 'ACTIVE'::STATUS, + ('system:department:read', 'Read Department', 'Read department', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - ('system:department:write', 'Write Department', 'Write departments.', 'ACTIVE'::STATUS, + ('system:department:write', 'Write Department', 'Write department.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - ('system:role:read', 'Read Roles', 'Read roles.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, + ('system:role:read', 'Read Roles', 'Read role.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - ('system:role:write', 'Write Roles', 'Write roles.', 'ACTIVE'::STATUS, + ('system:role:write', 'Write Roles', 'Write role.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - ('system:authority:read', 'Read Authorities', 'Read authorities.', 'ACTIVE'::STATUS, + ('system:authority:read', 'Read Authorities', 'Read authority.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - ('system:authority:write', 'Write Authorities', 'Write authorities.', + ('system:authority:write', 'Write Authorities', 'Write authority.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('system:audit_log:read', 'Read Audit Logs', 'Read audit logs.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), ('system:sso:write', 'Manage SSO', 'Manage SSO configurations (such as Microsoft Entra ID, etc.).', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), - ('system:setting:write', 'Write System Settings', 'Write system settings.', + ('system:setting:write', 'Write System Settings', 'Write system setting.', 'ACTIVE'::STATUS, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); --- Role Authorities Table --- -DROP TABLE IF EXISTS role_authorities CASCADE; -CREATE TABLE role_authorities +DROP TABLE IF EXISTS role_authority CASCADE; +CREATE TABLE role_authority ( - role_id BIGINT NOT NULL REFERENCES roles (id), - authority_id BIGINT NOT NULL REFERENCES authorities (id), + role_id BIGINT NOT NULL REFERENCES role (id), + authority_id BIGINT NOT NULL REFERENCES authority (id), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (role_id, authority_id) ); --- Role Authorities Data Insertion --- --- Admin role gets all authorities -INSERT INTO role_authorities (role_id, authority_id, created_at) +-- Admin role gets all authority +INSERT INTO role_authority (role_id, authority_id, created_at) SELECT r.id, a.id, CURRENT_TIMESTAMP -FROM roles r - CROSS JOIN authorities a +FROM role r + CROSS JOIN authority a WHERE r.code = 'admin'; --- Normal user role gets specific authorities -INSERT INTO role_authorities (role_id, authority_id, created_at) +-- Normal user role gets specific authority +INSERT INTO role_authority (role_id, authority_id, created_at) SELECT r.id, a.id, CURRENT_TIMESTAMP -FROM roles r - CROSS JOIN authorities a +FROM role r + CROSS JOIN authority a WHERE r.code = 'user' AND a.code IN ('system:dashboard:read', 'system:user:read', @@ -320,25 +317,25 @@ WHERE r.code = 'user' 'system:authority:read', 'system:audit_log:read'); -DROP TABLE IF EXISTS assets; -CREATE TABLE assets +DROP TABLE IF EXISTS asset; +CREATE TABLE asset ( id BIGSERIAL NOT NULL PRIMARY KEY, key VARCHAR(255) NOT NULL UNIQUE, - upload_by BIGINT NOT NULL REFERENCES users (id), + upload_by BIGINT NOT NULL REFERENCES "user" (id), upload_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX assets_key_index ON assets (key); +CREATE INDEX asset_key_index ON asset (key); -COMMENT ON TABLE assets IS 'Stores metadata for files or other digital assets within the system.'; -COMMENT ON COLUMN assets.id IS 'The unique identifier for the asset, automatically generated by the database.'; -COMMENT ON COLUMN assets.key IS 'The unique key or path of the asset within the storage system.'; -COMMENT ON COLUMN assets.upload_by IS 'The unique ID of the user who uploaded this asset, referencing the ID in the users table.'; -COMMENT ON COLUMN assets.upload_time IS 'The timestamp indicating when the asset was uploaded to the system.'; +COMMENT ON TABLE asset IS 'Stores metadata for files or other digital asset within the system.'; +COMMENT ON COLUMN asset.id IS 'The unique identifier for the asset, automatically generated by the database.'; +COMMENT ON COLUMN asset.key IS 'The unique key or path of the asset within the storage system.'; +COMMENT ON COLUMN asset.upload_by IS 'The unique ID of the user who uploaded this asset, referencing the ID in the user table.'; +COMMENT ON COLUMN asset.upload_time IS 'The timestamp indicating when the asset was uploaded to the system.'; -DROP TABLE IF EXISTS settings; -CREATE TABLE settings +DROP TABLE IF EXISTS setting; +CREATE TABLE setting ( id BIGSERIAL NOT NULL PRIMARY KEY, name VARCHAR(255) NOT NULL, @@ -350,24 +347,24 @@ CREATE TABLE settings updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX settings_name_index ON settings (name); +CREATE INDEX setting_name_index ON setting (name); -COMMENT ON TABLE settings IS 'Hot-deployable application settings.'; -COMMENT ON COLUMN settings.id IS 'Setting unique identifier.'; -COMMENT ON COLUMN settings.name IS 'Setting name.'; -COMMENT ON COLUMN settings.description IS 'Setting description.'; -COMMENT ON COLUMN settings.type IS 'The type of the value.'; -COMMENT ON COLUMN settings.value IS 'Setting current value.'; -COMMENT ON COLUMN settings.default_value IS 'Setting default value.'; +COMMENT ON TABLE setting IS 'Hot-deployable application setting.'; +COMMENT ON COLUMN setting.id IS 'Setting unique identifier.'; +COMMENT ON COLUMN setting.name IS 'Setting name.'; +COMMENT ON COLUMN setting.description IS 'Setting description.'; +COMMENT ON COLUMN setting.type IS 'The type of the value.'; +COMMENT ON COLUMN setting.value IS 'Setting current value.'; +COMMENT ON COLUMN setting.default_value IS 'Setting default value.'; -INSERT INTO settings(name, description, type, value, default_value) +INSERT INTO setting(name, description, type, value, default_value) VALUES ('captcha-enabled', 'Whether captcha is enabled.', 'BOOLEAN'::SETTING_TYPE, 'true', 'false'), ('register-enabled', 'Whether register is enabled', 'BOOLEAN'::SETTING_TYPE, 'true', 'false'); -DROP TABLE IF EXISTS menus; -CREATE TABLE menus +DROP TABLE IF EXISTS menu; +CREATE TABLE menu ( id BIGSERIAL NOT NULL PRIMARY KEY, name VARCHAR(50) NOT NULL, @@ -384,22 +381,22 @@ CREATE TABLE menus updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE UNIQUE INDEX menus_code_uindex ON menus (code); +CREATE UNIQUE INDEX menu_code_uindex ON menu (code); WITH system_menu AS ( - INSERT INTO menus (name, parent_id, code, sort, path, is_external_link, is_visible, status, - authority_code, icon, created_at, updated_at) + INSERT INTO menu (name, parent_id, code, sort, path, is_external_link, is_visible, status, + authority_code, icon, created_at, updated_at) VALUES ('系统管理', NULL, 'system-mgmt', 99, NULL, FALSE, TRUE, 'ACTIVE'::STATUS, NULL, NULL, NOW(), NOW()) RETURNING id) INSERT -INTO menus(name, parent_id, code, sort, path, is_external_link, is_visible, status, - authority_code, icon, created_at, updated_at) +INTO menu(name, parent_id, code, sort, path, is_external_link, is_visible, status, + authority_code, icon, created_at, updated_at) SELECT '用户管理', system_menu.id, 'user-mgmt', 1, - '/users', + '/user', FALSE, TRUE, 'ACTIVE'::STATUS, @@ -409,7 +406,7 @@ SELECT '用户管理', NOW() FROM system_menu; -INSERT INTO menus (name, parent_id, code, sort, path, is_external_link, is_visible, status, - authority_code, icon, created_at, updated_at) +INSERT INTO menu (name, parent_id, code, sort, path, is_external_link, is_visible, status, + authority_code, icon, created_at, updated_at) VALUES ('Helix 官网', null, 'helix-official-site', 100, 'https://helix.onixbyte.com', true, true, 'ACTIVE'::STATUS, null, null, NOW(), NOW()); \ No newline at end of file diff --git a/src/main/resources/mapper/AuthorityMapper.xml b/src/main/resources/mapper/AuthorityMapper.xml index ea9d26b..8b64eaf 100644 --- a/src/main/resources/mapper/AuthorityMapper.xml +++ b/src/main/resources/mapper/AuthorityMapper.xml @@ -4,9 +4,9 @@ \ No newline at end of file diff --git a/src/main/resources/mapper/MenuMapper.xml b/src/main/resources/mapper/MenuMapper.xml index f161b13..5c4c759 100644 --- a/src/main/resources/mapper/MenuMapper.xml +++ b/src/main/resources/mapper/MenuMapper.xml @@ -5,10 +5,10 @@ SELECT COUNT(*) - FROM roles + FROM role AND name LIKE '%' || #{wrapper.name} || '%' diff --git a/src/main/resources/mapper/UserCredentialMapper.xml b/src/main/resources/mapper/UserCredentialMapper.xml index 1166a0e..ea82989 100644 --- a/src/main/resources/mapper/UserCredentialMapper.xml +++ b/src/main/resources/mapper/UserCredentialMapper.xml @@ -25,7 +25,7 @@ "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> - UPDATE user_credentials + UPDATE user_credential SET credential = #{encodedPassword} WHERE user_id = #{userId} AND provider = 'LOCAL'::credential_provider diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml index bf5e209..508c02c 100644 --- a/src/main/resources/mapper/UserMapper.xml +++ b/src/main/resources/mapper/UserMapper.xml @@ -5,7 +5,6 @@ @@ -24,7 +23,6 @@ resultType="user"> SELECT id, username, - password, full_name, email, region_abbreviation, @@ -35,7 +33,7 @@ position_id, created_at, updated_at - FROM users + FROM "user" ORDER BY @@ -47,13 +45,12 @@ SELECT COUNT(*) - FROM users + FROM "user" AND department_id IN (WITH RECURSIVE SubDepartments AS ( SELECT id - FROM departments + FROM department WHERE id = #{wrapper.departmentId} UNION ALL SELECT d.id - FROM departments d + FROM department d INNER JOIN SubDepartments sd ON d.parent_id = sd.id) SELECT id @@ -160,9 +157,9 @@ p.name AS position_name, u.created_at, u.updated_at - FROM users u - LEFT JOIN departments d ON u.department_id = d.id - LEFT JOIN positions p ON u.position_id = p.id + FROM "user" u + LEFT JOIN department d ON u.department_id = d.id + LEFT JOIN position p ON u.position_id = p.id WHERE u.id = #{id} @@ -181,18 +178,18 @@ p.name AS position_name, u.created_at, u.updated_at - FROM users u - LEFT JOIN departments d ON u.department_id = d.id - LEFT JOIN positions p ON u.position_id = p.id + FROM "user" u + LEFT JOIN department d ON u.department_id = d.id + LEFT JOIN position p ON u.position_id = p.id AND department_id IN (WITH RECURSIVE SubDepartments AS ( SELECT id - FROM departments + FROM department WHERE id = #{wrapper.departmentId} UNION ALL SELECT d.id - FROM departments d + FROM department d INNER JOIN SubDepartments sd ON d.parent_id = sd.id) SELECT id From d33f4f2dbfc88188045c5540088e1c41775db038 Mon Sep 17 00:00:00 2001 From: siujamo Date: Fri, 20 Mar 2026 09:24:51 +0800 Subject: [PATCH 48/60] refactor: rename database table names for consistency --- src/main/java/com/onixbyte/helix/domain/entity/Asset.java | 2 +- src/main/java/com/onixbyte/helix/domain/entity/Authority.java | 2 +- src/main/java/com/onixbyte/helix/domain/entity/Department.java | 2 +- src/main/java/com/onixbyte/helix/domain/entity/Menu.java | 2 +- src/main/java/com/onixbyte/helix/domain/entity/Position.java | 2 +- src/main/java/com/onixbyte/helix/domain/entity/Role.java | 2 +- .../java/com/onixbyte/helix/domain/entity/RoleAuthority.java | 2 +- src/main/java/com/onixbyte/helix/domain/entity/Setting.java | 2 +- src/main/java/com/onixbyte/helix/domain/entity/User.java | 2 +- .../java/com/onixbyte/helix/domain/entity/UserCredential.java | 2 +- src/main/java/com/onixbyte/helix/domain/entity/UserRole.java | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Asset.java b/src/main/java/com/onixbyte/helix/domain/entity/Asset.java index c133dc5..e723a2b 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Asset.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Asset.java @@ -6,7 +6,7 @@ import java.time.LocalDateTime; import java.util.Objects; @Entity -@Table(name = "assets") +@Table(name = "asset") public class Asset { @Id diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Authority.java b/src/main/java/com/onixbyte/helix/domain/entity/Authority.java index 3e3240f..5d47016 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Authority.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Authority.java @@ -22,7 +22,7 @@ import java.util.Objects; * @since 1.0 */ @Entity -@Table(name = "authorities") +@Table(name = "authority") public class Authority { /** diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Department.java b/src/main/java/com/onixbyte/helix/domain/entity/Department.java index 9a1bb1b..ca30293 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Department.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Department.java @@ -23,7 +23,7 @@ import java.util.Objects; * @since 1.0 */ @Entity -@Table(name = "departments") +@Table(name = "department") public class Department implements Treeable { /** diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Menu.java b/src/main/java/com/onixbyte/helix/domain/entity/Menu.java index ec7bed6..cfb33b3 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Menu.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Menu.java @@ -10,7 +10,7 @@ import java.time.LocalDateTime; import java.util.Objects; @Entity -@Table(name = "menus") +@Table(name = "menu") public class Menu implements Treeable { @Id diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Position.java b/src/main/java/com/onixbyte/helix/domain/entity/Position.java index 8f9b80b..aadc209 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Position.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Position.java @@ -21,7 +21,7 @@ import java.util.Objects; * @since 1.0 */ @Entity -@Table(name = "positions") +@Table(name = "position") public class Position { /** diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Role.java b/src/main/java/com/onixbyte/helix/domain/entity/Role.java index 1896e98..a061424 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Role.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Role.java @@ -20,7 +20,7 @@ import java.util.Objects; * @since 1.0 */ @Entity -@Table(name = "roles") +@Table(name = "role") public class Role { /** diff --git a/src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java b/src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java index bcd00d8..b87bead 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/RoleAuthority.java @@ -13,7 +13,7 @@ import java.util.Objects; * auditing field. */ @Entity -@Table(name = "role_authorities") +@Table(name = "role_authority") public class RoleAuthority { /** diff --git a/src/main/java/com/onixbyte/helix/domain/entity/Setting.java b/src/main/java/com/onixbyte/helix/domain/entity/Setting.java index 967794a..4911ea0 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/Setting.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/Setting.java @@ -14,7 +14,7 @@ import java.util.Objects; * This entity allows for dynamic configuration changes without application restarts. */ @Entity -@Table(name = "settings") +@Table(name = "setting") public class Setting { /** diff --git a/src/main/java/com/onixbyte/helix/domain/entity/User.java b/src/main/java/com/onixbyte/helix/domain/entity/User.java index f6c60c4..dae750e 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/User.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/User.java @@ -22,7 +22,7 @@ import java.util.Objects; */ @Entity @Table( - name = "users", + name = "user", uniqueConstraints = { @UniqueConstraint(name = "uidx_users_username", columnNames = {"username"}), @UniqueConstraint(name = "uidx_users_email", columnNames = {"email"}), diff --git a/src/main/java/com/onixbyte/helix/domain/entity/UserCredential.java b/src/main/java/com/onixbyte/helix/domain/entity/UserCredential.java index 243a3aa..8c45909 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/UserCredential.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/UserCredential.java @@ -19,7 +19,7 @@ import java.util.Objects; * @since 1.0 */ @Entity -@Table(name = "user_credentials") +@Table(name = "user_credential") public class UserCredential { /** diff --git a/src/main/java/com/onixbyte/helix/domain/entity/UserRole.java b/src/main/java/com/onixbyte/helix/domain/entity/UserRole.java index ec22695..ae67249 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/UserRole.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/UserRole.java @@ -18,7 +18,7 @@ import java.util.Objects; * @since 1.0 */ @Entity -@Table(name = "user_roles") +@Table(name = "user_role") public class UserRole { /** From 7b9849c311a2225d5994b6f08637239babec16ee Mon Sep 17 00:00:00 2001 From: siujamo Date: Fri, 20 Mar 2026 10:33:16 +0800 Subject: [PATCH 49/60] feat: implement custom validation framework with dynamic rules --- .../com/onixbyte/helix/HelixApplication.java | 7 - .../com/onixbyte/helix/shared/CacheName.java | 2 +- .../annotation/CustomValidation.java | 76 +++++++++++ .../executor/CustomValidationExecutor.java | 127 ++++++++++++++++++ .../helix/validation/group/OnCreate.java | 62 +-------- .../helix/validation/group/OnUpdate.java | 82 +---------- .../validator/DynamicValidator.java | 64 +++++++++ 7 files changed, 278 insertions(+), 142 deletions(-) create mode 100644 src/main/java/com/onixbyte/helix/validation/annotation/CustomValidation.java create mode 100644 src/main/java/com/onixbyte/helix/validation/executor/CustomValidationExecutor.java create mode 100644 src/main/java/com/onixbyte/helix/validation/validator/DynamicValidator.java diff --git a/src/main/java/com/onixbyte/helix/HelixApplication.java b/src/main/java/com/onixbyte/helix/HelixApplication.java index b4d15d1..bfd4063 100644 --- a/src/main/java/com/onixbyte/helix/HelixApplication.java +++ b/src/main/java/com/onixbyte/helix/HelixApplication.java @@ -14,14 +14,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class HelixApplication { - /** - * Main method that serves as the entry point for the Helix application. - * - * @param args command-line arguments passed to the application, which can be used to override - * default configuration properties or specify runtime options - */ public static void main(String[] args) { SpringApplication.run(HelixApplication.class, args); } - } diff --git a/src/main/java/com/onixbyte/helix/shared/CacheName.java b/src/main/java/com/onixbyte/helix/shared/CacheName.java index 8babf0f..5a61ff9 100644 --- a/src/main/java/com/onixbyte/helix/shared/CacheName.java +++ b/src/main/java/com/onixbyte/helix/shared/CacheName.java @@ -4,7 +4,7 @@ public final class CacheName { public static final String USER = "user"; - public static final String AUTHORITIES_OF_USER = "user-authorities"; + public static final String AUTHORITIES_OF_USER = "user-authority"; public static final String ASSET = "asset"; diff --git a/src/main/java/com/onixbyte/helix/validation/annotation/CustomValidation.java b/src/main/java/com/onixbyte/helix/validation/annotation/CustomValidation.java new file mode 100644 index 0000000..637271a --- /dev/null +++ b/src/main/java/com/onixbyte/helix/validation/annotation/CustomValidation.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.validation.annotation; + +import com.onixbyte.helix.validation.executor.CustomValidationExecutor; +import com.onixbyte.helix.validation.validator.DynamicValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * CustomValidation is a custom validation annotation that allows for dynamic validation rules to be + * applied to fields or classes. It accepts an array of DynamicValidator classes that will be + * executed to validate the annotated element. + * + * @author siujamo + */ +@Target({ElementType.FIELD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = CustomValidationExecutor.class) +public @interface CustomValidation { + /** + * An array of DynamicValidator classes that will be executed to validate the annotated element. + * + * @return an array of DynamicValidator classes + */ + Class>[] rules(); + + /** + * The default validation message that will be used if any of the specified validators fail. + * + * @return the default validation message + */ + String message() default "Validation failed for the annotated element."; + + /** + * Groups can be used to specify different validation scenarios. This allows for more flexible + * validation logic based on the context of the operation (e.g., creation vs. update). + * + * @return an array of validation groups + */ + Class[] groups() default {}; + + /** + * Payload can be used by clients of the Bean Validation API to assign custom payload objects + * to a constraint. This is not commonly used but can be helpful for advanced validation + * scenarios. + * + * @return an array of payload classes + */ + Class[] payload() default {}; +} diff --git a/src/main/java/com/onixbyte/helix/validation/executor/CustomValidationExecutor.java b/src/main/java/com/onixbyte/helix/validation/executor/CustomValidationExecutor.java new file mode 100644 index 0000000..717d7e5 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/validation/executor/CustomValidationExecutor.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.validation.executor; + +import com.onixbyte.helix.validation.annotation.CustomValidation; +import com.onixbyte.helix.validation.validator.DynamicValidator; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.jspecify.annotations.NonNull; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * CustomValidationExecutor is a dynamic validation executor that processes multiple validation + * rules specified by the @CustomValidation annotation. + * + * @author siujamo + */ +@Component +public class CustomValidationExecutor + implements ConstraintValidator, ApplicationContextAware { + + private ApplicationContext applicationContext; + private Class>[] validatorClasses; + + @Override + public void initialize(CustomValidation constraintAnnotation) { + this.validatorClasses = constraintAnnotation.rules(); + } + + /** + * Validates the given value against all specified validators. If any validator fails, + * the entire validation fails. + * + * @param value the value to validate + * @param context the constraint validator context for adding violation messages if needed + * @return true if the value is valid according to all validators, false otherwise + */ + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + if (Objects.isNull(value)) return true; + + // Iterate through each specified validator class and perform validation + for (Class> clazz : validatorClasses) { + DynamicValidator validator = getValidatorInstance(clazz); + + // Ensure the validator supports the type of the value being validated + if (!validator.getSupportedType().isInstance(value)) { + return false; + } + + // Perform validation using the current validator + if (!invokeValidator(validator, value, context)) { + return false; + } + } + + return true; // All validators passed + } + + /** + * Invokes the validation logic of the given validator on the provided value. + * + * @param validator the dynamic validator to invoke + * @param value the value to validate + * @param context the constraint validator context for adding violation messages if needed + * @return true if the value is valid according to the validator, false otherwise + */ + private boolean invokeValidator(DynamicValidator validator, Object value, ConstraintValidatorContext context) { + var supportedType = validator.getSupportedType(); + if (!supportedType.isInstance(value)) { + return false; + } + return validator.isValid(supportedType.cast(value), context); + } + + /** + * Retrieves an instance of the specified DynamicValidator class from the Spring application context. + * + * @param clazz the class of the DynamicValidator to retrieve + * @return an instance of the specified DynamicValidator + * @throws IllegalStateException if the validator cannot be instantiated + */ + private DynamicValidator getValidatorInstance(Class> clazz) { + try { + return applicationContext.getBean(clazz); + } catch (NoSuchBeanDefinitionException e) { + throw new IllegalStateException("Failed to instantiate the validator: " + clazz.getName(), e); + } + } + + /** + * Sets the ApplicationContext that this object runs in. This method is called by the + * Spring framework during bean initialisation to provide the application context to + * this validator. + * + * @param applicationContext the ApplicationContext object to be used by this validator + */ + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } +} diff --git a/src/main/java/com/onixbyte/helix/validation/group/OnCreate.java b/src/main/java/com/onixbyte/helix/validation/group/OnCreate.java index d9799f8..6c16f2c 100644 --- a/src/main/java/com/onixbyte/helix/validation/group/OnCreate.java +++ b/src/main/java/com/onixbyte/helix/validation/group/OnCreate.java @@ -1,64 +1,12 @@ package com.onixbyte.helix.validation.group; /** - * Validation group interface for create operations. - *

- * This marker interface is used with Bean Validation (JSR-303/JSR-380) to define a validation group - * that is specifically applied during entity creation operations. By using validation groups, - * different validation rules can be applied depending on the context of the operation (create vs. - * update vs. other scenarios). - *

- * Usage Pattern: This interface is typically used in conjunction with validation - * annotations such as {@code @NotNull}, {@code @NotBlank}, {@code @Valid}, etc., to specify that - * certain validation constraints should only be applied when creating new entities. - *

- * Example Usage: - *

{@code
- * public class User {
- *     @NotNull(groups = OnCreate.class, message = "Username is required for new users")
- *     @Size(min = 3, max = 50, groups = OnCreate.class)
- *     private String username;
- *     
- *     @NotNull(groups = OnCreate.class, message = "Password is required for new users")
- *     @Size(min = 8, groups = OnCreate.class)
- *     private String password;
- *     
- *     // Other fields...
- * }
- * 
- * // In a REST controller:
- * @PostMapping("/users")
- * public ResponseEntity createUser(
- *     @Validated(OnCreate.class) @RequestBody User user) {
- *     // Create user logic
- * }
- * }
- *

- * Benefits of Validation Groups: - *

    - *
  • - * Context-Specific Validation: Apply different validation rules for different - * operations (e.g., password required on create but not on update) - *
  • - *
  • - * Flexible Constraint Application: Enable or disable specific validations based - * on the operation context - *
  • - *
  • - * Cleaner Code: Avoid complex conditional validation logic by using declarative - * group-based validation - *
  • - *
- *

- * This interface contains no methods and serves purely as a marker for the Bean Validation - * framework to identify which constraints should be applied during create operations. + * OnCreate is a validation group used to specify that certain validation rules should only be + * applied when creating a new entity. This allows for different validation logic to be executed + * during creation and update operations, ensuring that the appropriate constraints are enforced + * based on the context of the operation. * - * @author zihluwang - * @since 1.0.0 - * @see OnUpdate - * @see jakarta.validation.groups.Default - * @see org.springframework.validation.annotation.Validated - * @see Bean Validation Specification - Validation Groups + * @author siujamo */ public interface OnCreate { } diff --git a/src/main/java/com/onixbyte/helix/validation/group/OnUpdate.java b/src/main/java/com/onixbyte/helix/validation/group/OnUpdate.java index dcb17c0..a7737fa 100644 --- a/src/main/java/com/onixbyte/helix/validation/group/OnUpdate.java +++ b/src/main/java/com/onixbyte/helix/validation/group/OnUpdate.java @@ -1,84 +1,12 @@ package com.onixbyte.helix.validation.group; /** - * Validation group interface for update operations. - *

- * This marker interface is used with Bean Validation (JSR-303/JSR-380) to define a validation group - * that is specifically applied during entity update operations. By using validation groups, - * different validation rules can be applied depending on the context of the operation (update vs. - * create vs. other scenarios). - *

- * Usage Pattern: This interface is typically used in conjunction with validation - * annotations such as {@code @NotNull}, {@code @NotBlank}, {@code @Valid}, etc., to specify that - * certain validation constraints should only be applied when updating existing entities. - *

- * Example Usage: - *

{@code
- * public class User {
- *     @NotNull(groups = OnUpdate.class, message = "ID is required for updates")
- *     @Positive(groups = OnUpdate.class)
- *     private Long id;
- *     
- *     @Size(min = 3, max = 50, groups = OnUpdate.class)
- *     private String username;
- *     
- *     // Password might be optional on update
- *     @Size(min = 8, groups = OnUpdate.class)
- *     private String password;
- *     
- *     // Other fields...
- * }
- * 
- * // In a REST controller:
- * @PutMapping("/users/{id}")
- * public ResponseEntity updateUser(
- *     @PathVariable Long id,
- *     @Validated(OnUpdate.class) @RequestBody User user) {
- *     // Update user logic
- * }
- * }
- *

- * Common Update Validation Scenarios: - *

    - *
  • - * ID Validation: Ensure entity ID is present and valid for updates - *
  • - *
  • - * Optional Fields: Allow certain fields to be optional during updates that were - * required during creation - *
  • - *
  • - * Partial Updates: Support PATCH operations where only modified fields - * need validation - *
  • - *
  • - * Version Control: Validate version fields for optimistic locking - *
  • - *
- *

- * Benefits of Update-Specific Validation: - *

    - *
  • - * Flexible Field Requirements: Different fields may be required or optional - * compared to create operations - *
  • - *
  • - * Identity Validation: Ensure proper entity identification for update operations - *
  • - *
  • - * Conditional Logic: Apply validation rules specific to modification scenarios - *
  • - *
- *

- * This interface contains no methods and serves purely as a marker for the Bean Validation - * framework to identify which constraints should be applied during update operations. + * OnUpdate is a validation group used to specify that certain validation rules should only be + * applied when updating an existing entity. This allows for different validation logic to be + * executed during creation and update operations, ensuring that the appropriate constraints are + * enforced based on the context of the operation. * - * @author zihluwang - * @since 1.0.0 - * @see OnCreate - * @see jakarta.validation.groups.Default - * @see org.springframework.validation.annotation.Validated - * @see Bean Validation Specification - Validation Groups + * @author siujamo */ public interface OnUpdate { } diff --git a/src/main/java/com/onixbyte/helix/validation/validator/DynamicValidator.java b/src/main/java/com/onixbyte/helix/validation/validator/DynamicValidator.java new file mode 100644 index 0000000..c9ff8f0 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/validation/validator/DynamicValidator.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.validation.validator; + +import jakarta.validation.ConstraintValidatorContext; + +/** + * DynamicValidator is an interface for implementing dynamic validation logic that can be applied + * to various types of data. + * + * @param + * @author siujamo + */ +public interface DynamicValidator { + + /** + * Get the supported type of this validator. + * + * @return the supported type class + */ + Class getSupportedType(); + + /** + * Validate the given value and return whether it is valid according to the implemented logic. + * + * @param value the value to validate + * @param context the constraint validator context for adding violation messages if needed + * @return true if the value is valid, false otherwise + */ + boolean isValid(T value, ConstraintValidatorContext context); + + /** + * A helper method to add a violation message to the context. + * + * @param context the constraint validator context + * @param message the violation message to add + * @return the updated constraint validator context with the violation message added + */ + default ConstraintValidatorContext addViolationMessage(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + return context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + } +} From aebb693ee7ac0ffb5b9147a6cda9a3f67133d553 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 23 Mar 2026 15:13:23 +0800 Subject: [PATCH 50/60] feat: add department creation endpoint and request validation --- .../controller/DepartmentController.java | 11 ++-- .../helix/controller/ExceptionController.java | 52 ++++++------------- .../onixbyte/helix/domain/entity/User.java | 2 +- .../web/request/AddDepartmentRequest.java | 37 +++++++++++++ .../helix/manager/DepartmentManager.java | 10 ++++ .../onixbyte/helix/manager/UserManager.java | 22 ++++++++ .../repository/DepartmentRepository.java | 9 ++++ .../helix/service/DepartmentService.java | 25 ++++++++- .../resources/db/migrations/V1__init_db.sql | 19 ++++++- 9 files changed, 146 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/AddDepartmentRequest.java diff --git a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java index 34c5a5e..24117e5 100644 --- a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java +++ b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java @@ -2,11 +2,11 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Department; import com.onixbyte.helix.domain.common.TreeNode; +import com.onixbyte.helix.domain.web.request.AddDepartmentRequest; import com.onixbyte.helix.service.DepartmentService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -36,4 +36,9 @@ public class DepartmentController { public List getDepartments() { return departmentService.getDepartments(); } + + @PostMapping + public Department addDepartment(@Validated @RequestBody AddDepartmentRequest request) { + return departmentService.addDepartment(request); + } } diff --git a/src/main/java/com/onixbyte/helix/controller/ExceptionController.java b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java index f8a6677..dbec193 100644 --- a/src/main/java/com/onixbyte/helix/controller/ExceptionController.java +++ b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java @@ -3,8 +3,10 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.web.response.BizExceptionResponse; import com.onixbyte.helix.exception.BizException; import jakarta.validation.ConstraintViolationException; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -25,22 +27,6 @@ import java.util.stream.Collectors; @RestControllerAdvice public class ExceptionController { - /** - * Handles business logic exceptions thrown throughout the application. - *

- * This method intercepts {@link BizException} instances and converts them into appropriate HTTP - * responses. The HTTP status code is determined by the exception's status property, whilst the - * error message is extracted from the exception and included in the response body. - *

- * The response includes a timestamp indicating when the error occurred and the specific error - * message describing the business logic violation. - * - * @param ex the business exception that was thrown - * @return a {@link ResponseEntity} containing the error response with appropriate HTTP status - * and {@link BizExceptionResponse} body - * @see BizException - * @see BizExceptionResponse - */ @ExceptionHandler(BizException.class) public ResponseEntity handleBizException(BizException ex) { return ResponseEntity.status(ex.getStatus()) @@ -50,25 +36,6 @@ public class ExceptionController { ); } - /** - * Handles bean validation constraint violation exceptions. - *

- * This method processes {@link ConstraintViolationException} instances that occur when bean - * validation constraints are violated during request processing. It extracts all constraint - * violations, formats them into a readable error message, and returns a standardised - * error response. - *

- * The error message includes the property path and violation message for each constraint that - * was violated, separated by commas for multiple violations. The response is automatically - * assigned a {@code 400 Bad Request} status. - * - * @param ex the constraint violation exception containing validation errors - * @return a {@link BizExceptionResponse} containing the formatted validation - * error messages and timestamp - * @see ConstraintViolationException - * @see BizExceptionResponse - * @see jakarta.validation.constraints - */ @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public BizExceptionResponse handleConstraintViolation(ConstraintViolationException ex) { @@ -81,4 +48,19 @@ public class ExceptionController { errorMessage ); } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public BizExceptionResponse handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { + var errorMessage = ex.getBindingResult() + .getFieldErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining("; ")); + + return new BizExceptionResponse( + LocalDateTime.now(), + errorMessage + ); + } } diff --git a/src/main/java/com/onixbyte/helix/domain/entity/User.java b/src/main/java/com/onixbyte/helix/domain/entity/User.java index dae750e..3523650 100644 --- a/src/main/java/com/onixbyte/helix/domain/entity/User.java +++ b/src/main/java/com/onixbyte/helix/domain/entity/User.java @@ -22,7 +22,7 @@ import java.util.Objects; */ @Entity @Table( - name = "user", + name = "\"user\"", uniqueConstraints = { @UniqueConstraint(name = "uidx_users_username", columnNames = {"username"}), @UniqueConstraint(name = "uidx_users_email", columnNames = {"email"}), diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AddDepartmentRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/AddDepartmentRequest.java new file mode 100644 index 0000000..7b31ee9 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/AddDepartmentRequest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.domain.web.request; + +import com.onixbyte.helix.enumeration.Status; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record AddDepartmentRequest( + @NotNull(message = "Name of the department should not be null") + @NotBlank(message = "Name of the department should not be null") + String name, + Long parentId, + Integer sort, + Status status +) { +} diff --git a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java index 1c88c20..db9524e 100644 --- a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java +++ b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java @@ -8,6 +8,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; +import java.util.Optional; + @Component public class DepartmentManager { @@ -27,4 +29,12 @@ public class DepartmentManager { public Department selectById(Long id) { return departmentRepository.findById(id).orElse(null); } + + public Integer getNextSort(Long parentId) { + return Optional.ofNullable(departmentRepository.findMaxSort(parentId)).orElse(0) + 1; + } + + public Department save(Department department) { + return departmentRepository.save(department); + } } diff --git a/src/main/java/com/onixbyte/helix/manager/UserManager.java b/src/main/java/com/onixbyte/helix/manager/UserManager.java index 80dcbc1..4c986b3 100644 --- a/src/main/java/com/onixbyte/helix/manager/UserManager.java +++ b/src/main/java/com/onixbyte/helix/manager/UserManager.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + package com.onixbyte.helix.manager; import com.onixbyte.helix.common.regex.Patterns; diff --git a/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java b/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java index 70ceeab..79a1849 100644 --- a/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java +++ b/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java @@ -2,8 +2,17 @@ package com.onixbyte.helix.repository; import com.onixbyte.helix.domain.entity.Department; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface DepartmentRepository extends JpaRepository { + + @Query(""" + select max(d.sort) + from Department d + where (:parentId is null and d.parentId is null) + or d.parentId = :parentId + """) + Integer findMaxSort(Long parentId); } diff --git a/src/main/java/com/onixbyte/helix/service/DepartmentService.java b/src/main/java/com/onixbyte/helix/service/DepartmentService.java index 0cae30a..41e5a8c 100644 --- a/src/main/java/com/onixbyte/helix/service/DepartmentService.java +++ b/src/main/java/com/onixbyte/helix/service/DepartmentService.java @@ -1,14 +1,19 @@ package com.onixbyte.helix.service; -import com.onixbyte.helix.domain.entity.Department; import com.onixbyte.helix.domain.common.TreeNode; +import com.onixbyte.helix.domain.entity.Department; +import com.onixbyte.helix.domain.web.request.AddDepartmentRequest; +import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.manager.DepartmentManager; import com.onixbyte.helix.utils.TreeUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; @Service public class DepartmentService { @@ -28,4 +33,22 @@ public class DepartmentService { public List getDepartments() { return departmentManager.selectAll(Pageable.unpaged()).getContent(); } + + @Transactional(rollbackFor = Throwable.class) + public Department addDepartment(AddDepartmentRequest request) { + var createdAt = LocalDateTime.now(); + + var parentId = request.parentId(); + var sort = Optional.ofNullable(request.sort()) + .orElseGet(() -> departmentManager.getNextSort(parentId)); + + return departmentManager.save(Department.builder() + .name(request.name()) + .parentId(parentId) + .sort(sort) + .status(Optional.ofNullable(request.status()).orElse(Status.ACTIVE)) + .createdAt(createdAt) + .updatedAt(createdAt) + .build()); + } } diff --git a/src/main/resources/db/migrations/V1__init_db.sql b/src/main/resources/db/migrations/V1__init_db.sql index 5073696..61213ee 100644 --- a/src/main/resources/db/migrations/V1__init_db.sql +++ b/src/main/resources/db/migrations/V1__init_db.sql @@ -409,4 +409,21 @@ FROM system_menu; INSERT INTO menu (name, parent_id, code, sort, path, is_external_link, is_visible, status, authority_code, icon, created_at, updated_at) VALUES ('Helix 官网', null, 'helix-official-site', 100, 'https://helix.onixbyte.com', true, true, - 'ACTIVE'::STATUS, null, null, NOW(), NOW()); \ No newline at end of file + 'ACTIVE'::STATUS, null, null, NOW(), NOW()); + +DO +$$ + DECLARE + v_user_id BIGINT; + v_now TIMESTAMP; + BEGIN + SELECT id INTO v_user_id FROM "user" WHERE username = 'helix'; + v_now := CURRENT_TIMESTAMP; + + RAISE NOTICE 'User ID: %, Timestamp: %', v_user_id, v_now; + + -- Default password is '123456' + INSERT INTO "user_credential" + VALUES (v_user_id, 'LOCAL'::credential_provider, '$2a$10$1LoatLVvHL3LFK0pnNXcM.eiPK6.UdA9cl9IDwanWHBAAILn1xe0K', v_now, v_now); + END; +$$; \ No newline at end of file From 69e3f84becb1eae9f29aa9e2ecdac83f1d8d5689 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 23 Mar 2026 15:39:44 +0800 Subject: [PATCH 51/60] feat: implement authority deletion functionality and related repository methods --- .../helix/controller/AuthorityController.java | 7 +++++++ .../helix/manager/AuthorityManager.java | 8 +++++++ .../helix/manager/RoleAuthorityManager.java | 15 ++++++++++--- .../helix/mapper/RoleAuthorityMapper.java | 2 ++ .../helix/repository/AuthorityRepository.java | 9 ++++++++ .../helix/service/AuthorityService.java | 21 ++++++++++++++++++- src/main/resources/mapper/AuthorityMapper.xml | 2 +- .../resources/mapper/RoleAuthorityMapper.xml | 6 ++++++ src/main/resources/mapper/RoleMapper.xml | 2 +- 9 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java index 5363712..bfefede 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java @@ -4,6 +4,7 @@ import com.onixbyte.helix.domain.entity.Authority; import com.onixbyte.helix.domain.web.request.AddAuthorityRequest; import com.onixbyte.helix.domain.web.request.EditAuthorityRequest; import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; +import com.onixbyte.helix.domain.web.response.ActionResponse; import com.onixbyte.helix.service.AuthorityService; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -65,4 +66,10 @@ public class AuthorityController { public Authority editAuthority(@Validated @RequestBody EditAuthorityRequest request) { return authorityService.editAuthority(request); } + + @DeleteMapping("/{authorityId:\\d+}") + public ActionResponse deleteAuthority(@PathVariable Long authorityId) { + var name = authorityService.deleteAuthority(authorityId); + return ActionResponse.success("Authority [%s] deleted.".formatted(name)); + } } diff --git a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java index b064e86..e52cd8a 100644 --- a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java @@ -72,4 +72,12 @@ public class AuthorityManager { return authority; } + + public String findAuthorityNameById(Long authorityId) { + return authorityRepository.findAuthorityNameById(authorityId); + } + + public void deleteById(Long authorityId) { + authorityRepository.deleteById(authorityId); + } } diff --git a/src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java index 684698d..d29e80f 100644 --- a/src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java +++ b/src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java @@ -1,5 +1,6 @@ package com.onixbyte.helix.manager; +import com.onixbyte.helix.mapper.RoleAuthorityMapper; import com.onixbyte.helix.repository.RoleAuthorityRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,13 +11,21 @@ public class RoleAuthorityManager { private static final Logger log = LoggerFactory.getLogger(RoleAuthorityManager.class); private final RoleAuthorityRepository roleAuthorityRepository; + private final RoleAuthorityMapper roleAuthorityMapper; - public RoleAuthorityManager(RoleAuthorityRepository roleAuthorityRepository) { + public RoleAuthorityManager(RoleAuthorityRepository roleAuthorityRepository, RoleAuthorityMapper roleAuthorityMapper) { this.roleAuthorityRepository = roleAuthorityRepository; + this.roleAuthorityMapper = roleAuthorityMapper; } public void deleteByRoleId(Long roleId) { - var affectedRows = roleAuthorityRepository.deleteByRoleId(roleId); - log.info("角色 {} 关联的权限绑定已全部移除(共 {} 条)", roleId, affectedRows); + var affectedRows = roleAuthorityMapper.deleteByRoleId(roleId); + log.info("A total of {} authorities linked to Role ID: {} have been successfully cleared.", + affectedRows, roleId); + } + + public void deleteByAuthorityId(Long authorityId) { + var affectedRows = roleAuthorityMapper.deleteByAuthorityId(authorityId); + log.info("The binding between {} authorities and the role has been cleared.", affectedRows); } } diff --git a/src/main/java/com/onixbyte/helix/mapper/RoleAuthorityMapper.java b/src/main/java/com/onixbyte/helix/mapper/RoleAuthorityMapper.java index 72bf389..86daeca 100644 --- a/src/main/java/com/onixbyte/helix/mapper/RoleAuthorityMapper.java +++ b/src/main/java/com/onixbyte/helix/mapper/RoleAuthorityMapper.java @@ -7,4 +7,6 @@ import org.apache.ibatis.annotations.Param; public interface RoleAuthorityMapper { int deleteByRoleId(@Param("roleId") Long roleId); + + int deleteByAuthorityId(@Param("authorityId") Long authorityId); } diff --git a/src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java b/src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java index 96800c2..ce52bb9 100644 --- a/src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java +++ b/src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java @@ -2,8 +2,17 @@ package com.onixbyte.helix.repository; import com.onixbyte.helix.domain.entity.Authority; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface AuthorityRepository extends JpaRepository { + + @Query(""" + select a.name + from Authority a + where a.id = :authorityId + """) + String findAuthorityNameById(Long authorityId); } diff --git a/src/main/java/com/onixbyte/helix/service/AuthorityService.java b/src/main/java/com/onixbyte/helix/service/AuthorityService.java index d8d1bdb..551582f 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthorityService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthorityService.java @@ -8,10 +8,13 @@ import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.AuthorityManager; +import com.onixbyte.helix.manager.RoleAuthorityManager; +import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @@ -19,9 +22,11 @@ import java.util.Optional; public class AuthorityService { private final AuthorityManager authorityManager; + private final RoleAuthorityManager roleAuthorityManager; - public AuthorityService(AuthorityManager authorityManager) { + public AuthorityService(AuthorityManager authorityManager, RoleAuthorityManager roleAuthorityManager) { this.authorityManager = authorityManager; + this.roleAuthorityManager = roleAuthorityManager; } public Page getAuthorities(Pageable pageable, QueryAuthorityRequest request) { @@ -50,4 +55,18 @@ public class AuthorityService { public Authority editAuthority(EditAuthorityRequest request) { return authorityManager.update(request); } + + @Transactional(rollbackFor = Throwable.class) + public String deleteAuthority(Long authorityId) { + var authorityName = authorityManager.findAuthorityNameById(authorityId); + + if (StringUtils.isBlank(authorityName)) { + throw new BizException(HttpStatus.NOT_FOUND, "Authority with ID '%d' not found.".formatted(authorityId)); + } + + roleAuthorityManager.deleteByAuthorityId(authorityId); + authorityManager.deleteById(authorityId); + + return authorityName; + } } diff --git a/src/main/resources/mapper/AuthorityMapper.xml b/src/main/resources/mapper/AuthorityMapper.xml index 8b64eaf..02b9e72 100644 --- a/src/main/resources/mapper/AuthorityMapper.xml +++ b/src/main/resources/mapper/AuthorityMapper.xml @@ -2,7 +2,7 @@ - SELECT DISTINCT a.id, a.code, a.name, a.description, a.status, a.created_at, a.updated_at FROM authority a JOIN role_authority ra ON a.id = ra.authority_id diff --git a/src/main/resources/mapper/RoleAuthorityMapper.xml b/src/main/resources/mapper/RoleAuthorityMapper.xml index 9885de6..e4776a1 100644 --- a/src/main/resources/mapper/RoleAuthorityMapper.xml +++ b/src/main/resources/mapper/RoleAuthorityMapper.xml @@ -7,4 +7,10 @@ FROM role_authority WHERE role_id = #{roleId} + + + DELETE + FROM role_authority + WHERE authority_id = #{authorityId} + \ No newline at end of file diff --git a/src/main/resources/mapper/RoleMapper.xml b/src/main/resources/mapper/RoleMapper.xml index 4f14892..dd17648 100644 --- a/src/main/resources/mapper/RoleMapper.xml +++ b/src/main/resources/mapper/RoleMapper.xml @@ -52,7 +52,7 @@ - SELECT COUNT(*) FROM role From 8b343af4e815d3d96685cdac2b514314a954c7ab Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 23 Mar 2026 15:45:11 +0800 Subject: [PATCH 52/60] refactor: remove unused logger imports and related fields from manager and controller classes --- .../java/com/onixbyte/helix/controller/AuthController.java | 3 --- .../onixbyte/helix/domain/web/request/EditRoleRequest.java | 1 - src/main/java/com/onixbyte/helix/manager/AssetManager.java | 5 +---- .../java/com/onixbyte/helix/manager/AuthorityManager.java | 3 --- .../java/com/onixbyte/helix/manager/DepartmentManager.java | 5 +---- src/main/java/com/onixbyte/helix/manager/MenuManager.java | 4 +--- .../java/com/onixbyte/helix/manager/PositionManager.java | 5 +---- .../com/onixbyte/helix/manager/RoleAuthorityManager.java | 5 +---- src/main/java/com/onixbyte/helix/manager/RoleManager.java | 5 +---- .../java/com/onixbyte/helix/manager/UserRoleManager.java | 6 +----- .../com/onixbyte/helix/repository/AuthorityRepository.java | 1 - src/main/java/com/onixbyte/helix/service/MenuService.java | 4 +--- 12 files changed, 8 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/controller/AuthController.java b/src/main/java/com/onixbyte/helix/controller/AuthController.java index 1bef319..2b193d4 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthController.java @@ -6,8 +6,6 @@ import com.onixbyte.helix.service.AuthService; import com.onixbyte.helix.service.TokenService; import com.onixbyte.helix.service.UserService; import com.onixbyte.helix.shared.TokenConstant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -27,7 +25,6 @@ import java.time.Duration; @RequestMapping("/auth") public class AuthController { - private static final Logger log = LoggerFactory.getLogger(AuthController.class); private final AuthService authService; private final TokenService tokenService; private final UserService userService; diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java index 9392181..293311e 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java @@ -1,6 +1,5 @@ package com.onixbyte.helix.domain.web.request; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; diff --git a/src/main/java/com/onixbyte/helix/manager/AssetManager.java b/src/main/java/com/onixbyte/helix/manager/AssetManager.java index d899fd8..c4b89ec 100644 --- a/src/main/java/com/onixbyte/helix/manager/AssetManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AssetManager.java @@ -2,7 +2,6 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.shared.CacheName; import com.onixbyte.helix.domain.entity.Asset; -import com.onixbyte.helix.mapper.AssetMapper; import com.onixbyte.helix.repository.AssetRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; @@ -13,12 +12,10 @@ import org.springframework.stereotype.Component; @Component public class AssetManager { - private final AssetMapper assetMapper; private final AssetRepository assetRepository; @Autowired - public AssetManager(AssetMapper assetMapper, AssetRepository assetRepository) { - this.assetMapper = assetMapper; + public AssetManager(AssetRepository assetRepository) { this.assetRepository = assetRepository; } diff --git a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java index e52cd8a..975a647 100644 --- a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java @@ -8,8 +8,6 @@ import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.mapper.AuthorityMapper; import com.onixbyte.helix.repository.AuthorityRepository; import com.onixbyte.helix.shared.CacheName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Example; @@ -25,7 +23,6 @@ import java.util.Optional; @Component public class AuthorityManager { - private static final Logger log = LoggerFactory.getLogger(AuthorityManager.class); private final AuthorityMapper authorityMapper; private final AuthorityRepository authorityRepository; diff --git a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java index db9524e..25fbe50 100644 --- a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java +++ b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java @@ -1,7 +1,6 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.entity.Department; -import com.onixbyte.helix.mapper.DepartmentMapper; import com.onixbyte.helix.repository.DepartmentRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -13,12 +12,10 @@ import java.util.Optional; @Component public class DepartmentManager { - private final DepartmentMapper departmentMapper; private final DepartmentRepository departmentRepository; @Autowired - public DepartmentManager(DepartmentMapper departmentMapper, DepartmentRepository departmentRepository) { - this.departmentMapper = departmentMapper; + public DepartmentManager(DepartmentRepository departmentRepository) { this.departmentRepository = departmentRepository; } diff --git a/src/main/java/com/onixbyte/helix/manager/MenuManager.java b/src/main/java/com/onixbyte/helix/manager/MenuManager.java index b4d491b..dcca73d 100644 --- a/src/main/java/com/onixbyte/helix/manager/MenuManager.java +++ b/src/main/java/com/onixbyte/helix/manager/MenuManager.java @@ -2,8 +2,6 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.entity.Menu; import com.onixbyte.helix.mapper.MenuMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -11,7 +9,7 @@ import java.util.List; @Component public class MenuManager { - private static final Logger log = LoggerFactory.getLogger(MenuManager.class); + private final MenuMapper menuMapper; @Autowired diff --git a/src/main/java/com/onixbyte/helix/manager/PositionManager.java b/src/main/java/com/onixbyte/helix/manager/PositionManager.java index c3a0349..ed8f23f 100644 --- a/src/main/java/com/onixbyte/helix/manager/PositionManager.java +++ b/src/main/java/com/onixbyte/helix/manager/PositionManager.java @@ -1,7 +1,6 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.entity.Position; -import com.onixbyte.helix.mapper.PositionMapper; import com.onixbyte.helix.repository.PositionRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -11,12 +10,10 @@ import org.springframework.stereotype.Component; @Component public class PositionManager { - private final PositionMapper positionMapper; private final PositionRepository positionRepository; @Autowired - public PositionManager(PositionMapper positionMapper, PositionRepository positionRepository) { - this.positionMapper = positionMapper; + public PositionManager(PositionRepository positionRepository) { this.positionRepository = positionRepository; } diff --git a/src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java index d29e80f..300ef05 100644 --- a/src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java +++ b/src/main/java/com/onixbyte/helix/manager/RoleAuthorityManager.java @@ -1,7 +1,6 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.mapper.RoleAuthorityMapper; -import com.onixbyte.helix.repository.RoleAuthorityRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -10,11 +9,9 @@ import org.springframework.stereotype.Component; public class RoleAuthorityManager { private static final Logger log = LoggerFactory.getLogger(RoleAuthorityManager.class); - private final RoleAuthorityRepository roleAuthorityRepository; private final RoleAuthorityMapper roleAuthorityMapper; - public RoleAuthorityManager(RoleAuthorityRepository roleAuthorityRepository, RoleAuthorityMapper roleAuthorityMapper) { - this.roleAuthorityRepository = roleAuthorityRepository; + public RoleAuthorityManager(RoleAuthorityMapper roleAuthorityMapper) { this.roleAuthorityMapper = roleAuthorityMapper; } diff --git a/src/main/java/com/onixbyte/helix/manager/RoleManager.java b/src/main/java/com/onixbyte/helix/manager/RoleManager.java index 685a794..926549d 100644 --- a/src/main/java/com/onixbyte/helix/manager/RoleManager.java +++ b/src/main/java/com/onixbyte/helix/manager/RoleManager.java @@ -3,7 +3,6 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.database.query.wrapper.QueryRoleWrapper; import com.onixbyte.helix.domain.entity.Role; import com.onixbyte.helix.exception.BizException; -import com.onixbyte.helix.mapper.RoleAuthorityMapper; import com.onixbyte.helix.mapper.RoleMapper; import com.onixbyte.helix.repository.RoleRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -20,13 +19,11 @@ public class RoleManager { private final RoleMapper roleMapper; private final RoleRepository roleRepository; - private final RoleAuthorityMapper roleAuthorityMapper; @Autowired - public RoleManager(RoleMapper roleMapper, RoleRepository roleRepository, RoleAuthorityMapper roleAuthorityMapper) { + public RoleManager(RoleMapper roleMapper, RoleRepository roleRepository) { this.roleMapper = roleMapper; this.roleRepository = roleRepository; - this.roleAuthorityMapper = roleAuthorityMapper; } public void validateRoles(List roleIds) { diff --git a/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java b/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java index 0350bff..8a4ba8a 100644 --- a/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java +++ b/src/main/java/com/onixbyte/helix/manager/UserRoleManager.java @@ -1,8 +1,6 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.entity.UserRole; -import com.onixbyte.helix.domain.entity.embeddable.UserRoleId; -import com.onixbyte.helix.mapper.UserRoleMapper; import com.onixbyte.helix.repository.UserRoleRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,12 +13,10 @@ import java.util.List; public class UserRoleManager { private static final Logger log = LoggerFactory.getLogger(UserRoleManager.class); - private final UserRoleMapper userRoleMapper; private final UserRoleRepository userRoleRepository; @Autowired - public UserRoleManager(UserRoleMapper userRoleMapper, UserRoleRepository userRoleRepository) { - this.userRoleMapper = userRoleMapper; + public UserRoleManager(UserRoleRepository userRoleRepository) { this.userRoleRepository = userRoleRepository; } diff --git a/src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java b/src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java index ce52bb9..4007991 100644 --- a/src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java +++ b/src/main/java/com/onixbyte/helix/repository/AuthorityRepository.java @@ -3,7 +3,6 @@ package com.onixbyte.helix.repository; import com.onixbyte.helix.domain.entity.Authority; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository diff --git a/src/main/java/com/onixbyte/helix/service/MenuService.java b/src/main/java/com/onixbyte/helix/service/MenuService.java index ad5a57f..cba7625 100644 --- a/src/main/java/com/onixbyte/helix/service/MenuService.java +++ b/src/main/java/com/onixbyte/helix/service/MenuService.java @@ -5,8 +5,6 @@ import com.onixbyte.helix.domain.common.TreeNode; import com.onixbyte.helix.manager.MenuManager; import com.onixbyte.helix.utils.SecurityUtil; import com.onixbyte.helix.utils.TreeUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -14,7 +12,7 @@ import java.util.List; @Service public class MenuService { - private static final Logger log = LoggerFactory.getLogger(MenuService.class); + private final MenuManager menuManager; @Autowired From 712a6753254462041cca636afa316ec7c75f9382 Mon Sep 17 00:00:00 2001 From: siujamo Date: Mon, 23 Mar 2026 15:45:45 +0800 Subject: [PATCH 53/60] refactor: remove unused imports from GuidConfig and JacksonConfig classes --- .../com/onixbyte/helix/config/GuidConfig.java | 19 ------------------- .../onixbyte/helix/config/JacksonConfig.java | 1 - 2 files changed, 20 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/config/GuidConfig.java b/src/main/java/com/onixbyte/helix/config/GuidConfig.java index 690b6ca..8ee9c29 100644 --- a/src/main/java/com/onixbyte/helix/config/GuidConfig.java +++ b/src/main/java/com/onixbyte/helix/config/GuidConfig.java @@ -1,7 +1,6 @@ package com.onixbyte.helix.config; import com.onixbyte.identitygenerator.IdentityGenerator; -import com.onixbyte.identitygenerator.impl.SequentialUuidGenerator; import com.onixbyte.identitygenerator.impl.SnowflakeIdentityGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,24 +27,6 @@ import org.springframework.context.annotation.Configuration; @Configuration public class GuidConfig { - /** - * Creates a Snowflake-based identity generator for user IDs. - *

- * This method configures a {@link SnowflakeIdentityGenerator} with machine ID and data centre - * ID both set to 0. The generator produces unique 64-bit Long identifiers suitable for user - * entity primary keys in distributed environments. - *

- * The generated IDs are: - *

    - *
  • Globally unique across all instances
  • - *
  • Time-ordered (newer IDs have higher values)
  • - *
  • Highly performant with minimal coordination overhead
  • - *
- * - * @return a configured {@link SnowflakeIdentityGenerator} instance for generating user IDs - * @see SnowflakeIdentityGenerator - * @see IdentityGenerator - */ @Bean public IdentityGenerator userIdentityGenerator() { return new SnowflakeIdentityGenerator(0x0, 0x0); diff --git a/src/main/java/com/onixbyte/helix/config/JacksonConfig.java b/src/main/java/com/onixbyte/helix/config/JacksonConfig.java index cc686d3..fc5f8b2 100644 --- a/src/main/java/com/onixbyte/helix/config/JacksonConfig.java +++ b/src/main/java/com/onixbyte/helix/config/JacksonConfig.java @@ -1,7 +1,6 @@ package com.onixbyte.helix.config; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.onixbyte.helix.common.jackson.JacksonModules; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; From e1ad5cdfd8a8118ffeb72c26d9c7092b3168df64 Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 24 Mar 2026 10:07:37 +0800 Subject: [PATCH 54/60] feat: rename AddDepartmentRequest to DepartmentRequest and implement department editing functionality --- .../controller/DepartmentController.java | 12 +++++-- ...entRequest.java => DepartmentRequest.java} | 2 +- .../helix/manager/DepartmentManager.java | 34 +++++++++++++++++++ .../helix/service/DepartmentService.java | 9 +++-- 4 files changed, 52 insertions(+), 5 deletions(-) rename src/main/java/com/onixbyte/helix/domain/web/request/{AddDepartmentRequest.java => DepartmentRequest.java} (97%) diff --git a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java index 24117e5..2b4442b 100644 --- a/src/main/java/com/onixbyte/helix/controller/DepartmentController.java +++ b/src/main/java/com/onixbyte/helix/controller/DepartmentController.java @@ -2,7 +2,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Department; import com.onixbyte.helix.domain.common.TreeNode; -import com.onixbyte.helix.domain.web.request.AddDepartmentRequest; +import com.onixbyte.helix.domain.web.request.DepartmentRequest; import com.onixbyte.helix.service.DepartmentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; @@ -38,7 +38,15 @@ public class DepartmentController { } @PostMapping - public Department addDepartment(@Validated @RequestBody AddDepartmentRequest request) { + public Department addDepartment(@Validated @RequestBody DepartmentRequest request) { return departmentService.addDepartment(request); } + + @PutMapping("/{id:\\d+}") + public Department editDepartment( + @PathVariable Long id, + @Validated @RequestBody DepartmentRequest request + ) { + return departmentService.editDepartment(id, request); + } } diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AddDepartmentRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/DepartmentRequest.java similarity index 97% rename from src/main/java/com/onixbyte/helix/domain/web/request/AddDepartmentRequest.java rename to src/main/java/com/onixbyte/helix/domain/web/request/DepartmentRequest.java index 7b31ee9..6b737a6 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/AddDepartmentRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/DepartmentRequest.java @@ -26,7 +26,7 @@ import com.onixbyte.helix.enumeration.Status; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -public record AddDepartmentRequest( +public record DepartmentRequest( @NotNull(message = "Name of the department should not be null") @NotBlank(message = "Name of the department should not be null") String name, diff --git a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java index 25fbe50..c6f7b3a 100644 --- a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java +++ b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java @@ -1,12 +1,16 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.entity.Department; +import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.repository.DepartmentRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.Optional; @Component @@ -34,4 +38,34 @@ public class DepartmentManager { public Department save(Department department) { return departmentRepository.save(department); } + + /** + * Fully updates an existing department by ID. + *

+ * The method loads the target department, replaces mutable fields ({@code name}, + * {@code parentId}, {@code sort}, {@code status}), and refreshes {@code updatedAt} to the + * current time. + * + * @param id the ID of the department to update + * @param department the source data carrying new field values + * @return the managed and updated {@link Department} entity + * @throws BizException if the target department does not exist + */ + @Transactional + public Department fullUpdateById(Long id, Department department) { + var updatedAt = LocalDateTime.now(); + + var departmentToEdit = departmentRepository.findById(id) + .orElseThrow(() -> new BizException( + HttpStatus.NOT_FOUND, + "Department (ID: %d) to be edited not found.".formatted(department.getId())) + ); + + departmentToEdit.setName(department.getName()); + departmentToEdit.setParentId(department.getParentId()); + departmentToEdit.setSort(department.getSort()); + departmentToEdit.setStatus(department.getStatus()); + departmentToEdit.setUpdatedAt(updatedAt); + return departmentToEdit; + } } diff --git a/src/main/java/com/onixbyte/helix/service/DepartmentService.java b/src/main/java/com/onixbyte/helix/service/DepartmentService.java index 41e5a8c..c13793b 100644 --- a/src/main/java/com/onixbyte/helix/service/DepartmentService.java +++ b/src/main/java/com/onixbyte/helix/service/DepartmentService.java @@ -2,7 +2,7 @@ package com.onixbyte.helix.service; import com.onixbyte.helix.domain.common.TreeNode; import com.onixbyte.helix.domain.entity.Department; -import com.onixbyte.helix.domain.web.request.AddDepartmentRequest; +import com.onixbyte.helix.domain.web.request.DepartmentRequest; import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.manager.DepartmentManager; import com.onixbyte.helix.utils.TreeUtil; @@ -35,7 +35,7 @@ public class DepartmentService { } @Transactional(rollbackFor = Throwable.class) - public Department addDepartment(AddDepartmentRequest request) { + public Department addDepartment(DepartmentRequest request) { var createdAt = LocalDateTime.now(); var parentId = request.parentId(); @@ -51,4 +51,9 @@ public class DepartmentService { .updatedAt(createdAt) .build()); } + + public Department editDepartment(Long id, DepartmentRequest request) { + return departmentManager.fullUpdateById(id, Department.builder() + .build()); + } } From a3596ad086fe3aad4d299527170d51d7089e5ba0 Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 24 Mar 2026 10:27:52 +0800 Subject: [PATCH 55/60] feat: refactor authority request handling and update methods to use AuthorityRequest --- .../helix/controller/AuthorityController.java | 14 ++++---- .../web/request/AddAuthorityRequest.java | 15 --------- .../domain/web/request/AuthorityRequest.java | 20 +++++++++++ .../web/request/EditAuthorityRequest.java | 14 -------- .../helix/manager/AuthorityManager.java | 33 ++++++++++++------- .../helix/service/AuthorityService.java | 20 +++++------ 6 files changed, 59 insertions(+), 57 deletions(-) delete mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/AddAuthorityRequest.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/AuthorityRequest.java delete mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/EditAuthorityRequest.java diff --git a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java index bfefede..01071af 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java @@ -1,8 +1,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Authority; -import com.onixbyte.helix.domain.web.request.AddAuthorityRequest; -import com.onixbyte.helix.domain.web.request.EditAuthorityRequest; +import com.onixbyte.helix.domain.web.request.AuthorityRequest; import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; import com.onixbyte.helix.domain.web.response.ActionResponse; import com.onixbyte.helix.service.AuthorityService; @@ -52,7 +51,7 @@ public class AuthorityController { * @return created authority */ @PostMapping - public Authority addAuthority(@Validated @RequestBody AddAuthorityRequest request) { + public Authority addAuthority(@Validated @RequestBody AuthorityRequest request) { return authorityService.addAuthority(request); } @@ -62,9 +61,12 @@ public class AuthorityController { * @param request authority specs * @return edited authority */ - @PutMapping - public Authority editAuthority(@Validated @RequestBody EditAuthorityRequest request) { - return authorityService.editAuthority(request); + @PutMapping("/{id:\\d+}") + public Authority editAuthority( + @PathVariable Long id, + @Validated @RequestBody AuthorityRequest request + ) { + return authorityService.editAuthority(id, request); } @DeleteMapping("/{authorityId:\\d+}") diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AddAuthorityRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/AddAuthorityRequest.java deleted file mode 100644 index 1431245..0000000 --- a/src/main/java/com/onixbyte/helix/domain/web/request/AddAuthorityRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.onixbyte.helix.domain.web.request; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; - -public record AddAuthorityRequest( - @NotNull @NotBlank(message = "权限编码不能为空") String code, - @NotNull @NotBlank(message = "权限名称不能为空") String name, - String description, - @Pattern( - regexp = "^(ACTIVE|INACTIVE)?$", - message = "状态错误") String status -) { -} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AuthorityRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/AuthorityRequest.java new file mode 100644 index 0000000..844118b --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/AuthorityRequest.java @@ -0,0 +1,20 @@ +package com.onixbyte.helix.domain.web.request; + +import com.onixbyte.helix.enumeration.Status; +import com.onixbyte.helix.validation.group.OnCreate; +import com.onixbyte.helix.validation.group.OnUpdate; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Null; + +public record AuthorityRequest( + @Null(groups = {OnUpdate.class}, message = "Code cannot be edited.") + @NotNull(groups = {OnCreate.class}, message = "Code cannot be null.") + String code, + @NotNull(message = "Name of the authority cannot be null") + @NotBlank(message = "Name of the authority cannot be null") + String name, + String description, + Status status +) { +} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/EditAuthorityRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/EditAuthorityRequest.java deleted file mode 100644 index d71e437..0000000 --- a/src/main/java/com/onixbyte/helix/domain/web/request/EditAuthorityRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.onixbyte.helix.domain.web.request; - -import jakarta.validation.constraints.Pattern; - -public record EditAuthorityRequest( - Long id, - String name, - String description, - @Pattern( - regexp = "^(ACTIVE|INACTIVE)?$", - message = "状态参数错误") - String status -) { -} diff --git a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java index 975a647..0d5b774 100644 --- a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java @@ -2,7 +2,7 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.database.query.wrapper.QueryAuthorityWrapper; import com.onixbyte.helix.domain.entity.Authority; -import com.onixbyte.helix.domain.web.request.EditAuthorityRequest; +import com.onixbyte.helix.domain.web.request.AuthorityRequest; import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.mapper.AuthorityMapper; @@ -17,6 +17,7 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -53,19 +54,29 @@ public class AuthorityManager { return authorityRepository.save(authority); } + /** + * Fully updates an existing authority by ID. + *

+ * The method loads the target authority, replaces mutable fields ({@code name}, + * {@code description}, {@code status}), and refreshes {@code updatedAt} to the current time. + * The update runs within a transactional context. + * + * @param id the ID of the authority to update + * @param authority the source data carrying new field values + * @return the supplied {@link Authority} object + * @throws BizException if the target authority does not exist + */ @Transactional - public Authority update(EditAuthorityRequest request) { - var authority = authorityRepository.findById(request.id()) + public Authority fullUpdateById(Long id, Authority authority) { + var updatedAt = LocalDateTime.now(); + + var authorityToUpdate = authorityRepository.findById(id) .orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, "找不到指定的权限信息")); - Optional.ofNullable(request.name()) - .ifPresent(authority::setName); - - authority.setDescription(request.description()); - - Optional.ofNullable(request.status()) - .map(Status::valueOf) - .ifPresent(authority::setStatus); + authorityToUpdate.setName(authority.getName()); + authorityToUpdate.setDescription(authority.getDescription()); + authorityToUpdate.setStatus(authority.getStatus()); + authorityToUpdate.setUpdatedAt(updatedAt); return authority; } diff --git a/src/main/java/com/onixbyte/helix/service/AuthorityService.java b/src/main/java/com/onixbyte/helix/service/AuthorityService.java index 551582f..d63328e 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthorityService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthorityService.java @@ -2,10 +2,8 @@ package com.onixbyte.helix.service; import com.onixbyte.helix.domain.database.query.wrapper.QueryAuthorityWrapper; import com.onixbyte.helix.domain.entity.Authority; -import com.onixbyte.helix.domain.web.request.AddAuthorityRequest; -import com.onixbyte.helix.domain.web.request.EditAuthorityRequest; +import com.onixbyte.helix.domain.web.request.AuthorityRequest; import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; -import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.AuthorityManager; import com.onixbyte.helix.manager.RoleAuthorityManager; @@ -16,8 +14,6 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Service public class AuthorityService { @@ -35,14 +31,12 @@ public class AuthorityService { return authorityManager.selectAll(pageable, wrapper); } - public Authority addAuthority(AddAuthorityRequest request) { + public Authority addAuthority(AuthorityRequest request) { var authority = Authority.builder() .code(request.code()) .name(request.name()) .description(request.description()) - .status(Optional.ofNullable(request.status()) - .map(Status::valueOf) - .orElse(Status.ACTIVE)) + .status(request.status()) .build(); if (authorityManager.existsByCode(authority)) { @@ -52,8 +46,12 @@ public class AuthorityService { return authorityManager.save(authority); } - public Authority editAuthority(EditAuthorityRequest request) { - return authorityManager.update(request); + public Authority editAuthority(Long id, AuthorityRequest request) { + return authorityManager.fullUpdateById(id, Authority.builder() + .name(request.name()) + .description(request.description()) + .status(request.status()) + .build()); } @Transactional(rollbackFor = Throwable.class) From 776ddd28c1e2019977d0ec2c7e3dd18da1d13aa9 Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 24 Mar 2026 10:49:33 +0800 Subject: [PATCH 56/60] feat: implement internationalisation support with message utility and localisation configuration --- .../com/onixbyte/helix/config/I18nConfig.java | 41 ++++++++++++++++ .../helix/config/ValidationConfig.java | 47 +++++++++++++++++++ .../helix/controller/AssetController.java | 16 ++++--- .../helix/controller/AuthorityController.java | 11 ++++- .../helix/controller/UserController.java | 19 +++++--- .../web/request/ResetPasswordRequest.java | 1 - .../onixbyte/helix/manager/UserManager.java | 4 +- .../onixbyte/helix/service/UserService.java | 4 +- .../com/onixbyte/helix/shared/Message.java | 33 +++++++++++++ .../com/onixbyte/helix/utils/MessageUtil.java | 42 +++++++++++++++++ src/main/resources/i18n/messages.properties | 28 +++++++++++ .../resources/i18n/messages_en_GB.properties | 28 +++++++++++ .../resources/i18n/messages_zh_CN.properties | 28 +++++++++++ 13 files changed, 282 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/onixbyte/helix/config/I18nConfig.java create mode 100644 src/main/java/com/onixbyte/helix/config/ValidationConfig.java create mode 100644 src/main/java/com/onixbyte/helix/shared/Message.java create mode 100644 src/main/java/com/onixbyte/helix/utils/MessageUtil.java create mode 100644 src/main/resources/i18n/messages.properties create mode 100644 src/main/resources/i18n/messages_en_GB.properties create mode 100644 src/main/resources/i18n/messages_zh_CN.properties diff --git a/src/main/java/com/onixbyte/helix/config/I18nConfig.java b/src/main/java/com/onixbyte/helix/config/I18nConfig.java new file mode 100644 index 0000000..a6a0322 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/I18nConfig.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.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 I18nConfig { + + @Bean + public LocaleResolver localeResolver() { + var slr = new AcceptHeaderLocaleResolver(); + slr.setDefaultLocale(Locale.UK); + return slr; + } +} diff --git a/src/main/java/com/onixbyte/helix/config/ValidationConfig.java b/src/main/java/com/onixbyte/helix/config/ValidationConfig.java new file mode 100644 index 0000000..4462d47 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/config/ValidationConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +@Configuration +public class ValidationConfig { + + private final MessageSource messageSource; + + @Autowired + public ValidationConfig(MessageSource messageSource) { + this.messageSource = messageSource; + } + + @Bean + public LocalValidatorFactoryBean getValidator() { + var factoryBean = new LocalValidatorFactoryBean(); + factoryBean.setValidationMessageSource(messageSource); + return factoryBean; + } +} diff --git a/src/main/java/com/onixbyte/helix/controller/AssetController.java b/src/main/java/com/onixbyte/helix/controller/AssetController.java index c413f38..270bfeb 100644 --- a/src/main/java/com/onixbyte/helix/controller/AssetController.java +++ b/src/main/java/com/onixbyte/helix/controller/AssetController.java @@ -4,6 +4,8 @@ import com.onixbyte.helix.shared.AssetPrefix; import com.onixbyte.helix.domain.web.response.FileUploadResponse; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.service.AssetService; +import com.onixbyte.helix.shared.Message; +import com.onixbyte.helix.utils.MessageUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -25,15 +27,15 @@ public class AssetController { private static final Logger log = LoggerFactory.getLogger(AssetController.class); private final AssetService assetService; + private final MessageUtil messageUtil; - /** - * Constructs a new FileController with the specified file service. - * - * @param assetService the file service to use for file operations - */ @Autowired - public AssetController(AssetService assetService) { + public AssetController( + AssetService assetService, + MessageUtil messageUtil + ) { this.assetService = assetService; + this.messageUtil = messageUtil; } /** @@ -48,7 +50,7 @@ public class AssetController { ) { try { if (file.isEmpty()) { - throw new BizException(HttpStatus.BAD_REQUEST, "File cannot be empty."); + throw new BizException(HttpStatus.BAD_REQUEST, messageUtil.getMessage(Message.ASSET_NOT_EMPTY)); } var fileUrl = assetService.uploadFile(AssetPrefix.UPLOADS, file); diff --git a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java index 01071af..ee5dd4f 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java @@ -5,6 +5,8 @@ import com.onixbyte.helix.domain.web.request.AuthorityRequest; import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; import com.onixbyte.helix.domain.web.response.ActionResponse; import com.onixbyte.helix.service.AuthorityService; +import com.onixbyte.helix.shared.Message; +import com.onixbyte.helix.utils.MessageUtil; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.validation.annotation.Validated; @@ -21,9 +23,14 @@ import org.springframework.web.bind.annotation.*; public class AuthorityController { private final AuthorityService authorityService; + private final MessageUtil messageUtil; - public AuthorityController(AuthorityService authorityService) { + public AuthorityController( + AuthorityService authorityService, + MessageUtil messageUtil + ) { this.authorityService = authorityService; + this.messageUtil = messageUtil; } /** @@ -72,6 +79,6 @@ public class AuthorityController { @DeleteMapping("/{authorityId:\\d+}") public ActionResponse deleteAuthority(@PathVariable Long authorityId) { var name = authorityService.deleteAuthority(authorityId); - return ActionResponse.success("Authority [%s] deleted.".formatted(name)); + return ActionResponse.success(messageUtil.getMessage(Message.AUTHORITY_DELETED, name)); } } diff --git a/src/main/java/com/onixbyte/helix/controller/UserController.java b/src/main/java/com/onixbyte/helix/controller/UserController.java index 642d31f..a2bf304 100644 --- a/src/main/java/com/onixbyte/helix/controller/UserController.java +++ b/src/main/java/com/onixbyte/helix/controller/UserController.java @@ -7,6 +7,8 @@ import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; import com.onixbyte.helix.domain.web.response.ActionResponse; import com.onixbyte.helix.domain.web.response.UserDetailResponse; import com.onixbyte.helix.service.UserService; +import com.onixbyte.helix.shared.Message; +import com.onixbyte.helix.utils.MessageUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -27,10 +29,12 @@ import org.springframework.web.bind.annotation.*; public class UserController { private final UserService userService; + private final MessageUtil messageUtil; @Autowired - public UserController(UserService userService) { + public UserController(UserService userService, MessageUtil messageUtil) { this.userService = userService; + this.messageUtil = messageUtil; } /** @@ -94,10 +98,13 @@ public class UserController { * @return action response */ @PreAuthorize("hasAnyAuthority('system:user:reset-password')") - @PatchMapping("/reset-password") - public ActionResponse resetPassword(@Validated @RequestBody ResetPasswordRequest request) { - userService.resetPassword(request); - return ActionResponse.success("密码修改成功"); + @PatchMapping("/reset-password/{id:\\d+}") + public ActionResponse resetPassword( + @PathVariable Long id, + @Validated @RequestBody ResetPasswordRequest request + ) { + userService.resetPassword(id, request); + return ActionResponse.success(messageUtil.getMessage(Message.USER_PASSWORD_RESET_SUCCESS)); } /** @@ -110,6 +117,6 @@ public class UserController { @DeleteMapping("/{userId:\\d+}") public ActionResponse deleteUser(@PathVariable Long userId) { userService.deleteUser(userId); - return ActionResponse.success("删除成功"); + return ActionResponse.success(messageUtil.getMessage(Message.USER_DELETED)); } } diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java index 2ee0a27..d1ac2d3 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public record ResetPasswordRequest( - @NotNull(message = "用户 ID 不能为空") Long id, @NotBlank(message = "密码不能为空") String password ) { } diff --git a/src/main/java/com/onixbyte/helix/manager/UserManager.java b/src/main/java/com/onixbyte/helix/manager/UserManager.java index 4c986b3..3b19dee 100644 --- a/src/main/java/com/onixbyte/helix/manager/UserManager.java +++ b/src/main/java/com/onixbyte/helix/manager/UserManager.java @@ -150,9 +150,9 @@ public class UserManager { } @Transactional(rollbackFor = Throwable.class) - public void updateUserPassword(ResetPasswordRequest request) { + public void updatePasswordById(Long id, ResetPasswordRequest request) { userCredentialMapper.updateUserCredential( - request.id(), + id, passwordEncoder.encode(request.password()) ); } diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index 42fcf65..adda0b7 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -162,8 +162,8 @@ public class UserService { } @Transactional(rollbackFor = Throwable.class) - public void resetPassword(ResetPasswordRequest request) { - userManager.updateUserPassword(request); + public void resetPassword(Long id, ResetPasswordRequest request) { + userManager.updatePasswordById(id, request); } @Transactional(rollbackFor = Throwable.class) diff --git a/src/main/java/com/onixbyte/helix/shared/Message.java b/src/main/java/com/onixbyte/helix/shared/Message.java new file mode 100644 index 0000000..0111874 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/shared/Message.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.shared; + +public class Message { + + public static final String ASSET_NOT_EMPTY = "asset.not-empty"; + + public static final String AUTHORITY_DELETED = "authority.deleted"; + + public static final String USER_PASSWORD_RESET_SUCCESS = "user.password-reset-success"; + public static final String USER_DELETED = "user.deleted"; +} diff --git a/src/main/java/com/onixbyte/helix/utils/MessageUtil.java b/src/main/java/com/onixbyte/helix/utils/MessageUtil.java new file mode 100644 index 0000000..9e9ede8 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/utils/MessageUtil.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.utils; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class MessageUtil { + + private final MessageSource messageSource; + + public MessageUtil(MessageSource messageSource) { + this.messageSource = messageSource; + } + + public String getMessage(String code, Object... args) { + var locale = LocaleContextHolder.getLocale(); + return messageSource.getMessage(code, args, locale); + } +} diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..f9a18f6 --- /dev/null +++ b/src/main/resources/i18n/messages.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2024-2026 OnixByte +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +asset.not-empty = File cannot be empty. + +authority.deleted = Authority [{0}] deleted. + +user.password-reset-success = Password has been reset. +user.deleted = User [{0}] deleted. \ No newline at end of file diff --git a/src/main/resources/i18n/messages_en_GB.properties b/src/main/resources/i18n/messages_en_GB.properties new file mode 100644 index 0000000..f9a18f6 --- /dev/null +++ b/src/main/resources/i18n/messages_en_GB.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2024-2026 OnixByte +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +asset.not-empty = File cannot be empty. + +authority.deleted = Authority [{0}] deleted. + +user.password-reset-success = Password has been reset. +user.deleted = User [{0}] deleted. \ No newline at end of file diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties new file mode 100644 index 0000000..009ffa2 --- /dev/null +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2024-2026 OnixByte +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +asset.not-empty = 文件不能为空。 + +authority.deleted = 权限【{0}】删除成功。 + +user.password-reset-success = 密码修改成功。 +user.deleted = 用户【{0}】删除成功。 \ No newline at end of file From 08c18fea90745a88b3ea0ea4211e9c12fa90561c Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 24 Mar 2026 12:23:13 +0800 Subject: [PATCH 57/60] feat: refactor message handling to use MessageName constants for improved internationalisation support --- .../helix/controller/AssetController.java | 17 ++-- .../helix/controller/AuthorityController.java | 4 +- .../helix/controller/ExceptionController.java | 13 ++- .../helix/controller/RoleController.java | 32 ++++---- .../helix/controller/UserController.java | 6 +- .../domain/web/request/AddRoleRequest.java | 21 ----- .../domain/web/request/AddUserRequest.java | 7 +- .../domain/web/request/AuthorityRequest.java | 9 +- .../domain/web/request/DepartmentRequest.java | 5 +- .../domain/web/request/EditRoleRequest.java | 5 +- .../domain/web/request/EditUserRequest.java | 5 +- .../domain/web/request/QueryRoleRequest.java | 3 +- .../domain/web/request/QueryUserRequest.java | 3 +- .../web/request/ResetPasswordRequest.java | 3 +- .../helix/domain/web/request/RoleRequest.java | 20 +++++ .../helix/exception/BizException.java | 28 +++++++ .../helix/manager/AuthorityManager.java | 3 +- .../onixbyte/helix/manager/RoleManager.java | 36 ++++---- .../onixbyte/helix/manager/UserManager.java | 3 +- ...sernamePasswordAuthenticationProvider.java | 9 +- .../onixbyte/helix/service/AssetService.java | 5 +- .../onixbyte/helix/service/AuthService.java | 7 +- .../helix/service/AuthorityService.java | 5 +- .../helix/service/CaptchaService.java | 7 +- .../onixbyte/helix/service/RoleService.java | 42 ++++++---- .../com/onixbyte/helix/shared/Message.java | 33 -------- .../onixbyte/helix/shared/MessageName.java | 82 +++++++++++++++++++ .../onixbyte/helix/utils/SecurityUtil.java | 3 +- .../com/onixbyte/helix/utils/TreeUtil.java | 3 +- src/main/resources/application.yml | 2 + src/main/resources/i18n/messages.properties | 22 ++++- .../resources/i18n/messages_en_GB.properties | 50 ++++++++++- .../resources/i18n/messages_zh_CN.properties | 50 ++++++++++- 33 files changed, 391 insertions(+), 152 deletions(-) delete mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/AddRoleRequest.java create mode 100644 src/main/java/com/onixbyte/helix/domain/web/request/RoleRequest.java delete mode 100644 src/main/java/com/onixbyte/helix/shared/Message.java create mode 100644 src/main/java/com/onixbyte/helix/shared/MessageName.java diff --git a/src/main/java/com/onixbyte/helix/controller/AssetController.java b/src/main/java/com/onixbyte/helix/controller/AssetController.java index 270bfeb..eb2f566 100644 --- a/src/main/java/com/onixbyte/helix/controller/AssetController.java +++ b/src/main/java/com/onixbyte/helix/controller/AssetController.java @@ -4,8 +4,7 @@ import com.onixbyte.helix.shared.AssetPrefix; import com.onixbyte.helix.domain.web.response.FileUploadResponse; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.service.AssetService; -import com.onixbyte.helix.shared.Message; -import com.onixbyte.helix.utils.MessageUtil; +import com.onixbyte.helix.shared.MessageName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -27,15 +26,10 @@ public class AssetController { private static final Logger log = LoggerFactory.getLogger(AssetController.class); private final AssetService assetService; - private final MessageUtil messageUtil; @Autowired - public AssetController( - AssetService assetService, - MessageUtil messageUtil - ) { + public AssetController(AssetService assetService) { this.assetService = assetService; - this.messageUtil = messageUtil; } /** @@ -50,7 +44,7 @@ public class AssetController { ) { try { if (file.isEmpty()) { - throw new BizException(HttpStatus.BAD_REQUEST, messageUtil.getMessage(Message.ASSET_NOT_EMPTY)); + throw new BizException(HttpStatus.BAD_REQUEST, MessageName.ASSET_NOT_EMPTY); } var fileUrl = assetService.uploadFile(AssetPrefix.UPLOADS, file); @@ -63,10 +57,13 @@ public class AssetController { file.getSize(), fileUrl )); + } catch (BizException ex) { + throw ex; } catch (Exception e) { log.error("File upload failed: {}", e.getMessage(), e); throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, - "Failed upload file: " + e.getMessage()); + MessageName.ASSET_UPLOAD_FAILED, + e.getMessage()); } } diff --git a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java index ee5dd4f..127fd19 100644 --- a/src/main/java/com/onixbyte/helix/controller/AuthorityController.java +++ b/src/main/java/com/onixbyte/helix/controller/AuthorityController.java @@ -5,7 +5,7 @@ import com.onixbyte.helix.domain.web.request.AuthorityRequest; import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; import com.onixbyte.helix.domain.web.response.ActionResponse; import com.onixbyte.helix.service.AuthorityService; -import com.onixbyte.helix.shared.Message; +import com.onixbyte.helix.shared.MessageName; import com.onixbyte.helix.utils.MessageUtil; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -79,6 +79,6 @@ public class AuthorityController { @DeleteMapping("/{authorityId:\\d+}") public ActionResponse deleteAuthority(@PathVariable Long authorityId) { var name = authorityService.deleteAuthority(authorityId); - return ActionResponse.success(messageUtil.getMessage(Message.AUTHORITY_DELETED, name)); + return ActionResponse.success(messageUtil.getMessage(MessageName.AUTHORITY_DELETED, name)); } } diff --git a/src/main/java/com/onixbyte/helix/controller/ExceptionController.java b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java index dbec193..2e27cee 100644 --- a/src/main/java/com/onixbyte/helix/controller/ExceptionController.java +++ b/src/main/java/com/onixbyte/helix/controller/ExceptionController.java @@ -2,6 +2,7 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.web.response.BizExceptionResponse; import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.utils.MessageUtil; import jakarta.validation.ConstraintViolationException; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpStatus; @@ -27,12 +28,22 @@ import java.util.stream.Collectors; @RestControllerAdvice public class ExceptionController { + private final MessageUtil messageUtil; + + public ExceptionController(MessageUtil messageUtil) { + this.messageUtil = messageUtil; + } + @ExceptionHandler(BizException.class) public ResponseEntity handleBizException(BizException ex) { + var message = ex.getMessageCode() == null + ? ex.getMessage() + : messageUtil.getMessage(ex.getMessageCode(), ex.getMessageArgs()); + return ResponseEntity.status(ex.getStatus()) .body(new BizExceptionResponse( LocalDateTime.now(), - ex.getMessage()) + message) ); } diff --git a/src/main/java/com/onixbyte/helix/controller/RoleController.java b/src/main/java/com/onixbyte/helix/controller/RoleController.java index f24068d..7350dee 100644 --- a/src/main/java/com/onixbyte/helix/controller/RoleController.java +++ b/src/main/java/com/onixbyte/helix/controller/RoleController.java @@ -1,15 +1,16 @@ package com.onixbyte.helix.controller; import com.onixbyte.helix.domain.entity.Role; -import com.onixbyte.helix.domain.web.request.AddRoleRequest; -import com.onixbyte.helix.domain.web.request.EditRoleRequest; import com.onixbyte.helix.domain.web.request.QueryRoleRequest; +import com.onixbyte.helix.domain.web.request.RoleRequest; +import com.onixbyte.helix.domain.web.response.ActionResponse; import com.onixbyte.helix.service.RoleService; +import com.onixbyte.helix.shared.MessageName; +import com.onixbyte.helix.utils.MessageUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -24,10 +25,12 @@ import org.springframework.web.bind.annotation.*; public class RoleController { private final RoleService roleService; + private final MessageUtil messageUtil; @Autowired - public RoleController(RoleService roleService) { + public RoleController(RoleService roleService, MessageUtil messageUtil) { this.roleService = roleService; + this.messageUtil = messageUtil; } @GetMapping @@ -41,20 +44,21 @@ public class RoleController { } @PostMapping - public ResponseEntity addRole(@Validated @RequestBody AddRoleRequest request) { - roleService.addRole(request); - return ResponseEntity.ok(null); + public Role addRole(@Validated @RequestBody RoleRequest request) { + return roleService.addRole(request); } - @PutMapping - public ResponseEntity editRole(@Validated @RequestBody EditRoleRequest request) { - roleService.editRole(request); - return ResponseEntity.ok(null); + @PutMapping("/{id:\\d+}") + public Role editRole( + @PathVariable Long id, + @Validated @RequestBody RoleRequest request + ) { + return roleService.editRole(id, request); } @DeleteMapping("/{id:\\d+}") - public ResponseEntity deleteRole(@PathVariable Long id) { - roleService.deleteRole(id); - return ResponseEntity.ok(null); + public ActionResponse deleteRole(@PathVariable Long id) { + var name = roleService.deleteRole(id); + return ActionResponse.success(messageUtil.getMessage(MessageName.ROLE_DELETED, name)); } } diff --git a/src/main/java/com/onixbyte/helix/controller/UserController.java b/src/main/java/com/onixbyte/helix/controller/UserController.java index a2bf304..9a2dc1f 100644 --- a/src/main/java/com/onixbyte/helix/controller/UserController.java +++ b/src/main/java/com/onixbyte/helix/controller/UserController.java @@ -7,7 +7,7 @@ import com.onixbyte.helix.domain.web.request.ResetPasswordRequest; import com.onixbyte.helix.domain.web.response.ActionResponse; import com.onixbyte.helix.domain.web.response.UserDetailResponse; import com.onixbyte.helix.service.UserService; -import com.onixbyte.helix.shared.Message; +import com.onixbyte.helix.shared.MessageName; import com.onixbyte.helix.utils.MessageUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -104,7 +104,7 @@ public class UserController { @Validated @RequestBody ResetPasswordRequest request ) { userService.resetPassword(id, request); - return ActionResponse.success(messageUtil.getMessage(Message.USER_PASSWORD_RESET_SUCCESS)); + return ActionResponse.success(messageUtil.getMessage(MessageName.USER_PASSWORD_RESET_SUCCESS)); } /** @@ -117,6 +117,6 @@ public class UserController { @DeleteMapping("/{userId:\\d+}") public ActionResponse deleteUser(@PathVariable Long userId) { userService.deleteUser(userId); - return ActionResponse.success(messageUtil.getMessage(Message.USER_DELETED)); + return ActionResponse.success(messageUtil.getMessage(MessageName.USER_DELETED, userId)); } } diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AddRoleRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/AddRoleRequest.java deleted file mode 100644 index 1127ba2..0000000 --- a/src/main/java/com/onixbyte/helix/domain/web/request/AddRoleRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.onixbyte.helix.domain.web.request; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; - -public record AddRoleRequest( - @NotBlank(message = "角色名称不能为空") - String name, - @NotBlank(message = "角色编码不能为空") - String code, - @NotNull(message = "排序编号不能为空") - Integer sort, - Boolean defaultValue, - String description, - @Pattern( - regexp = "^(ACTIVE|INACTIVE)?$", - message = "状态仅可以是 ACTIVE、INACTIVE 其中之一") - String status -) { -} diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java index df7fca0..21a9ed8 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/AddUserRequest.java @@ -1,16 +1,17 @@ package com.onixbyte.helix.domain.web.request; import com.onixbyte.helix.enumeration.UserStatus; +import com.onixbyte.helix.shared.MessageName; import jakarta.validation.constraints.NotBlank; import java.util.List; public record AddUserRequest( - @NotBlank(message = "Username cannot be empty.") + @NotBlank(message = "{" + MessageName.REQUEST_ADD_USER_USERNAME_NOT_EMPTY + "}") String username, - @NotBlank(message = "Password cannot be empty.") + @NotBlank(message = "{" + MessageName.REQUEST_ADD_USER_PASSWORD_NOT_EMPTY + "}") String password, - @NotBlank(message = "Full name cannot be empty.") + @NotBlank(message = "{" + MessageName.REQUEST_ADD_USER_FULL_NAME_NOT_EMPTY + "}") String fullName, String email, String regionAbbreviation, diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/AuthorityRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/AuthorityRequest.java index 844118b..e169516 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/AuthorityRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/AuthorityRequest.java @@ -1,6 +1,7 @@ package com.onixbyte.helix.domain.web.request; import com.onixbyte.helix.enumeration.Status; +import com.onixbyte.helix.shared.MessageName; import com.onixbyte.helix.validation.group.OnCreate; import com.onixbyte.helix.validation.group.OnUpdate; import jakarta.validation.constraints.NotBlank; @@ -8,11 +9,11 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Null; public record AuthorityRequest( - @Null(groups = {OnUpdate.class}, message = "Code cannot be edited.") - @NotNull(groups = {OnCreate.class}, message = "Code cannot be null.") + @Null(groups = {OnUpdate.class}, message = "{" + MessageName.REQUEST_AUTHORITY_CODE_NOT_EDITABLE + "}") + @NotNull(groups = {OnCreate.class}, message = "{" + MessageName.REQUEST_AUTHORITY_CODE_NOT_NULL + "}") String code, - @NotNull(message = "Name of the authority cannot be null") - @NotBlank(message = "Name of the authority cannot be null") + @NotNull(message = "{" + MessageName.REQUEST_AUTHORITY_NAME_NOT_NULL + "}") + @NotBlank(message = "{" + MessageName.REQUEST_AUTHORITY_NAME_NOT_NULL + "}") String name, String description, Status status diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/DepartmentRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/DepartmentRequest.java index 6b737a6..4937369 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/DepartmentRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/DepartmentRequest.java @@ -23,12 +23,13 @@ package com.onixbyte.helix.domain.web.request; import com.onixbyte.helix.enumeration.Status; +import com.onixbyte.helix.shared.MessageName; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public record DepartmentRequest( - @NotNull(message = "Name of the department should not be null") - @NotBlank(message = "Name of the department should not be null") + @NotNull(message = "{" + MessageName.REQUEST_DEPARTMENT_NAME_NOT_NULL + "}") + @NotBlank(message = "{" + MessageName.REQUEST_DEPARTMENT_NAME_NOT_NULL + "}") String name, Long parentId, Integer sort, diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java index 293311e..266dfea 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/EditRoleRequest.java @@ -1,10 +1,11 @@ package com.onixbyte.helix.domain.web.request; +import com.onixbyte.helix.shared.MessageName; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; public record EditRoleRequest( - @NotNull(message = "角色 ID 不能为空") + @NotNull(message = "{" + MessageName.REQUEST_EDIT_ROLE_ID_NOT_NULL + "}") Long id, String name, String code, @@ -13,7 +14,7 @@ public record EditRoleRequest( String description, @Pattern( regexp = "^(ACTIVE|INACTIVE)?$", - message = "状态仅可以是 ACTIVE、INACTIVE 其中之一") + message = "{" + MessageName.REQUEST_EDIT_ROLE_STATUS_INVALID + "}") String status ) { } diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/EditUserRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/EditUserRequest.java index c294023..b5da25e 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/EditUserRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/EditUserRequest.java @@ -1,12 +1,13 @@ package com.onixbyte.helix.domain.web.request; import com.onixbyte.helix.enumeration.UserStatus; +import com.onixbyte.helix.shared.MessageName; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; public record EditUserRequest( - @NotNull(message = "User ID cannot be null") - @Positive(message = "User ID must be positive") + @NotNull(message = "{" + MessageName.REQUEST_EDIT_USER_ID_NOT_NULL + "}") + @Positive(message = "{" + MessageName.REQUEST_EDIT_USER_ID_POSITIVE + "}") Long id, String fullName, String email, diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/QueryRoleRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/QueryRoleRequest.java index 6497c75..20ebe90 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/QueryRoleRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/QueryRoleRequest.java @@ -1,5 +1,6 @@ package com.onixbyte.helix.domain.web.request; +import com.onixbyte.helix.shared.MessageName; import jakarta.validation.constraints.Pattern; public record QueryRoleRequest( @@ -7,7 +8,7 @@ public record QueryRoleRequest( String code, @Pattern( regexp = "^(ACTIVE|INACTIVE)?$", - message = "状态仅可以是 ACTIVE、INACTIVE 其中之一") + message = "{" + MessageName.REQUEST_QUERY_ROLE_STATUS_INVALID + "}") String status ) { } diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/QueryUserRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/QueryUserRequest.java index 9d53f08..7b73b72 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/QueryUserRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/QueryUserRequest.java @@ -1,5 +1,6 @@ package com.onixbyte.helix.domain.web.request; +import com.onixbyte.helix.shared.MessageName; import jakarta.validation.constraints.Pattern; import java.time.LocalDateTime; @@ -11,7 +12,7 @@ public record QueryUserRequest( String phoneNumber, @Pattern( regexp = "^(ACTIVE|INACTIVE|LOCKED)?$", - message = "状态仅可以是 ACTIVE、INACTIVE 或 LOCKED 其中之一") + message = "{" + MessageName.REQUEST_QUERY_USER_STATUS_INVALID + "}") String status, LocalDateTime createdAtStart, LocalDateTime createdAtEnd diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java index d1ac2d3..1d835bd 100644 --- a/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java +++ b/src/main/java/com/onixbyte/helix/domain/web/request/ResetPasswordRequest.java @@ -1,9 +1,10 @@ package com.onixbyte.helix.domain.web.request; +import com.onixbyte.helix.shared.MessageName; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public record ResetPasswordRequest( - @NotBlank(message = "密码不能为空") String password + @NotBlank(message = "{" + MessageName.REQUEST_RESET_PASSWORD_NOT_EMPTY + "}") String password ) { } diff --git a/src/main/java/com/onixbyte/helix/domain/web/request/RoleRequest.java b/src/main/java/com/onixbyte/helix/domain/web/request/RoleRequest.java new file mode 100644 index 0000000..e101a2f --- /dev/null +++ b/src/main/java/com/onixbyte/helix/domain/web/request/RoleRequest.java @@ -0,0 +1,20 @@ +package com.onixbyte.helix.domain.web.request; + +import com.onixbyte.helix.enumeration.Status; +import com.onixbyte.helix.shared.MessageName; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +public record RoleRequest( + @NotBlank(message = "{" + MessageName.REQUEST_ROLE_NAME_NOT_EMPTY + "}") + String name, + @NotBlank(message = "{" + MessageName.REQUEST_ROLE_CODE_NOT_EMPTY + "}") + String code, + @NotNull(message = "{" + MessageName.REQUEST_ROLE_SORT_NOT_NULL + "}") + Integer sort, + Boolean defaultValue, + String description, + Status status +) { +} diff --git a/src/main/java/com/onixbyte/helix/exception/BizException.java b/src/main/java/com/onixbyte/helix/exception/BizException.java index f898daf..8e99c74 100644 --- a/src/main/java/com/onixbyte/helix/exception/BizException.java +++ b/src/main/java/com/onixbyte/helix/exception/BizException.java @@ -41,6 +41,8 @@ public class BizException extends RuntimeException { * REST API endpoints. */ private final HttpStatus status; + private final String messageCode; + private final Object[] messageArgs; /** * Constructs a new business exception with the specified HTTP status and message. @@ -50,6 +52,8 @@ public class BizException extends RuntimeException { public BizException(String message) { super(message); this.status = HttpStatus.INTERNAL_SERVER_ERROR; + this.messageCode = null; + this.messageArgs = new Object[0]; } /** @@ -61,6 +65,22 @@ public class BizException extends RuntimeException { public BizException(HttpStatus status, String message) { super(message); this.status = status; + this.messageCode = null; + this.messageArgs = new Object[0]; + } + + public BizException(String messageCode, Object... messageArgs) { + super(messageCode); + this.status = HttpStatus.INTERNAL_SERVER_ERROR; + this.messageCode = messageCode; + this.messageArgs = messageArgs == null ? new Object[0] : messageArgs; + } + + public BizException(HttpStatus status, String messageCode, Object... messageArgs) { + super(messageCode); + this.status = status; + this.messageCode = messageCode; + this.messageArgs = messageArgs == null ? new Object[0] : messageArgs; } /** @@ -71,4 +91,12 @@ public class BizException extends RuntimeException { public HttpStatus getStatus() { return status; } + + public String getMessageCode() { + return messageCode; + } + + public Object[] getMessageArgs() { + return messageArgs; + } } diff --git a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java index 0d5b774..dc1c2c5 100644 --- a/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java +++ b/src/main/java/com/onixbyte/helix/manager/AuthorityManager.java @@ -8,6 +8,7 @@ import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.mapper.AuthorityMapper; import com.onixbyte.helix.repository.AuthorityRepository; import com.onixbyte.helix.shared.CacheName; +import com.onixbyte.helix.shared.MessageName; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Example; @@ -71,7 +72,7 @@ public class AuthorityManager { var updatedAt = LocalDateTime.now(); var authorityToUpdate = authorityRepository.findById(id) - .orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, "找不到指定的权限信息")); + .orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, MessageName.AUTHORITY_NOT_FOUND, id)); authorityToUpdate.setName(authority.getName()); authorityToUpdate.setDescription(authority.getDescription()); diff --git a/src/main/java/com/onixbyte/helix/manager/RoleManager.java b/src/main/java/com/onixbyte/helix/manager/RoleManager.java index 926549d..ad55450 100644 --- a/src/main/java/com/onixbyte/helix/manager/RoleManager.java +++ b/src/main/java/com/onixbyte/helix/manager/RoleManager.java @@ -5,12 +5,15 @@ import com.onixbyte.helix.domain.entity.Role; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.mapper.RoleMapper; import com.onixbyte.helix.repository.RoleRepository; +import com.onixbyte.helix.shared.MessageName; +import com.onixbyte.helix.utils.MessageUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.*; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -19,16 +22,18 @@ public class RoleManager { private final RoleMapper roleMapper; private final RoleRepository roleRepository; + private final MessageUtil messageUtil; @Autowired - public RoleManager(RoleMapper roleMapper, RoleRepository roleRepository) { + public RoleManager(RoleMapper roleMapper, RoleRepository roleRepository, MessageUtil messageUtil) { this.roleMapper = roleMapper; this.roleRepository = roleRepository; + this.messageUtil = messageUtil; } public void validateRoles(List roleIds) { if (!roleMapper.areRolesExisted(roleIds)) { - throw new BizException(HttpStatus.BAD_REQUEST, "Role does not exist in database."); + throw new BizException(HttpStatus.BAD_REQUEST, MessageName.ROLE_NOT_EXISTS); } } @@ -52,23 +57,24 @@ public class RoleManager { } @Transactional - public void updateRole(Role role) { - var roleToUpdate = roleRepository.findById(role.getId()) - .orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, "找不到指定的角色信息。")); + public Role fullUpdateById(Long id, Role role) { + var updatedAt = LocalDateTime.now(); - Optional.ofNullable(role.getName()) - .ifPresent(roleToUpdate::setName); - - Optional.ofNullable(role.getCode()) - .ifPresent(roleToUpdate::setCode); - - Optional.ofNullable(role.getSort()) - .ifPresent(roleToUpdate::setSort); + var roleToUpdate = roleRepository.findById(id) + .orElseThrow(() -> new BizException( + HttpStatus.NOT_FOUND, + messageUtil.getMessage(MessageName.ROLE_NOT_FOUND, id)) + ); + roleToUpdate.setName(role.getName()); + roleToUpdate.setCode(role.getCode()); + roleToUpdate.setSort(role.getSort()); + roleToUpdate.setDefaultValue(role.getDefaultValue()); roleToUpdate.setDescription(role.getDescription()); + roleToUpdate.setStatus(role.getStatus()); + roleToUpdate.setUpdatedAt(updatedAt); - Optional.ofNullable(role.getStatus()) - .ifPresent(roleToUpdate::setStatus); + return role; } public void deleteRole(Long id) { diff --git a/src/main/java/com/onixbyte/helix/manager/UserManager.java b/src/main/java/com/onixbyte/helix/manager/UserManager.java index 3b19dee..2f5121b 100644 --- a/src/main/java/com/onixbyte/helix/manager/UserManager.java +++ b/src/main/java/com/onixbyte/helix/manager/UserManager.java @@ -33,6 +33,7 @@ import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.mapper.UserMapper; import com.onixbyte.helix.repository.UserRepository; import com.onixbyte.region.Region; +import com.onixbyte.helix.shared.MessageName; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CachePut; @@ -112,7 +113,7 @@ public class UserManager { @Transactional(rollbackFor = Throwable.class) public User updateUser(User user) { var userToUpdate = userRepository.findById(user.getId()) - .orElseThrow(() -> new BizException(HttpStatus.BAD_REQUEST, "找不到 ID 为" + user.getId() + "的用户信息")); + .orElseThrow(() -> new BizException(HttpStatus.BAD_REQUEST, MessageName.USER_NOT_FOUND, user.getId())); Optional.ofNullable(user.getFullName()) .filter(StringUtils::isNotBlank) diff --git a/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java b/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java index 35f72fc..254a844 100644 --- a/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java +++ b/src/main/java/com/onixbyte/helix/security/provider/UsernamePasswordAuthenticationProvider.java @@ -8,6 +8,7 @@ import com.onixbyte.helix.manager.AuthorityManager; import com.onixbyte.helix.manager.UserManager; import com.onixbyte.helix.repository.UserCredentialRepository; import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; +import com.onixbyte.helix.shared.MessageName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -46,14 +47,14 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (!(authentication instanceof UsernamePasswordAuthentication usernamePasswordAuthentication)) { - throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, "用户认证失败,请稍后再试。"); + throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, MessageName.AUTH_PROVIDER_FAILED); } // get user from database var user = userManager.selectByUsername(usernamePasswordAuthentication.getPrincipal()); if (Objects.isNull(user)) { log.error("User {} is trying to authenticate but no user found.", usernamePasswordAuthentication.getPrincipal()); - throw new BizException(HttpStatus.UNAUTHORIZED, "用户名或密码错误。"); + throw new BizException(HttpStatus.UNAUTHORIZED, MessageName.AUTH_PROVIDER_BAD_CREDENTIALS); } // get user credentials from database @@ -61,12 +62,12 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro .provider(CredentialProvider.LOCAL) .userId(user.getId()) .build())) - .orElseThrow(() -> new BizException(HttpStatus.UNAUTHORIZED, "您还没有配置密码,请使用第三方账号登录")); + .orElseThrow(() -> new BizException(HttpStatus.UNAUTHORIZED, MessageName.AUTH_PROVIDER_PASSWORD_NOT_CONFIGURED)); // validate password if (!passwordEncoder.matches(usernamePasswordAuthentication.getCredentials(), userCredentials.getCredential())) { log.error("User {} is trying to authenticate but password is incorrect.", usernamePasswordAuthentication.getPrincipal()); - throw new BizException(HttpStatus.UNAUTHORIZED, "用户名或密码错误。"); + throw new BizException(HttpStatus.UNAUTHORIZED, MessageName.AUTH_PROVIDER_BAD_CREDENTIALS); } // erase credentials diff --git a/src/main/java/com/onixbyte/helix/service/AssetService.java b/src/main/java/com/onixbyte/helix/service/AssetService.java index 33f1dc0..95346e4 100644 --- a/src/main/java/com/onixbyte/helix/service/AssetService.java +++ b/src/main/java/com/onixbyte/helix/service/AssetService.java @@ -4,6 +4,7 @@ import com.onixbyte.helix.domain.entity.Asset; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.AssetManager; import com.onixbyte.helix.properties.AssetProperties; +import com.onixbyte.helix.shared.MessageName; import com.onixbyte.helix.utils.SecurityUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -62,7 +63,7 @@ public class AssetService { if (Objects.isNull(prefix) || prefix.isBlank() || prefix.startsWith("/") || prefix.startsWith("..")) { throw new BizException( HttpStatus.INTERNAL_SERVER_ERROR, - "Prefix must not be empty, and should not start with '/' or '..'." + MessageName.ASSET_INVALID_PREFIX ); } @@ -108,7 +109,7 @@ public class AssetService { var asset = assetManager.queryByAssetId(assetId); if (!Objects.equals(currentUser.getId(), asset.getUploadBy())) { - throw new BizException(HttpStatus.FORBIDDEN, "You are not able to delete an asset that is not uploaded by you."); + throw new BizException(HttpStatus.FORBIDDEN, MessageName.ASSET_DELETE_FORBIDDEN); } assetManager.deleteById(assetId); diff --git a/src/main/java/com/onixbyte/helix/service/AuthService.java b/src/main/java/com/onixbyte/helix/service/AuthService.java index 1411a56..cc62efc 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthService.java @@ -9,6 +9,7 @@ import com.onixbyte.helix.manager.CaptchaManager; import com.onixbyte.helix.manager.SecurityManager; import com.onixbyte.helix.manager.SettingManager; import com.onixbyte.helix.security.authentication.UsernamePasswordAuthentication; +import com.onixbyte.helix.shared.MessageName; import com.onixbyte.helix.shared.SettingName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,10 +64,10 @@ public class AuthService { var rawCaptcha = captchaManager.getCaptcha(uuid); if (Objects.isNull(rawCaptcha) || rawCaptcha.isBlank()) { - throw new BizException(HttpStatus.BAD_REQUEST, "未找到验证码"); + throw new BizException(HttpStatus.BAD_REQUEST, MessageName.AUTH_LOGIN_CAPTCHA_NOT_FOUND); } if (!rawCaptcha.equalsIgnoreCase(request.captcha())) { - throw new BizException(HttpStatus.BAD_REQUEST, "验证码错误"); + throw new BizException(HttpStatus.BAD_REQUEST, MessageName.AUTH_LOGIN_CAPTCHA_INCORRECT); } } @@ -78,7 +79,7 @@ public class AuthService { _authentication.getClass() ); throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, - "Cannot perform login due to server crashes."); + MessageName.AUTH_LOGIN_FAILED); } return authentication.getDetails(); diff --git a/src/main/java/com/onixbyte/helix/service/AuthorityService.java b/src/main/java/com/onixbyte/helix/service/AuthorityService.java index d63328e..189f5c2 100644 --- a/src/main/java/com/onixbyte/helix/service/AuthorityService.java +++ b/src/main/java/com/onixbyte/helix/service/AuthorityService.java @@ -7,6 +7,7 @@ import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.AuthorityManager; import com.onixbyte.helix.manager.RoleAuthorityManager; +import com.onixbyte.helix.shared.MessageName; import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -40,7 +41,7 @@ public class AuthorityService { .build(); if (authorityManager.existsByCode(authority)) { - throw new BizException(HttpStatus.CONFLICT, "权限编码 `" + authority.getCode() + "` 已被使用"); + throw new BizException(HttpStatus.CONFLICT, MessageName.AUTHORITY_CODE_USED, authority.getCode()); } return authorityManager.save(authority); @@ -59,7 +60,7 @@ public class AuthorityService { var authorityName = authorityManager.findAuthorityNameById(authorityId); if (StringUtils.isBlank(authorityName)) { - throw new BizException(HttpStatus.NOT_FOUND, "Authority with ID '%d' not found.".formatted(authorityId)); + throw new BizException(HttpStatus.NOT_FOUND, MessageName.AUTHORITY_NOT_FOUND, authorityId); } roleAuthorityManager.deleteByAuthorityId(authorityId); diff --git a/src/main/java/com/onixbyte/helix/service/CaptchaService.java b/src/main/java/com/onixbyte/helix/service/CaptchaService.java index cda2d08..7f17a73 100644 --- a/src/main/java/com/onixbyte/helix/service/CaptchaService.java +++ b/src/main/java/com/onixbyte/helix/service/CaptchaService.java @@ -1,13 +1,14 @@ package com.onixbyte.helix.service; import com.onixbyte.captcha.Producer; -import com.onixbyte.helix.shared.FileType; -import com.onixbyte.helix.shared.SettingName; import com.onixbyte.helix.domain.entity.Setting; import com.onixbyte.helix.domain.web.response.CaptchaResponse; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.CaptchaManager; import com.onixbyte.helix.manager.SettingManager; +import com.onixbyte.helix.shared.FileType; +import com.onixbyte.helix.shared.MessageName; +import com.onixbyte.helix.shared.SettingName; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.FastByteArrayOutputStream; @@ -66,7 +67,7 @@ public class CaptchaService { Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); return new CaptchaResponse(captchaDataUrl, uuid); } catch (IOException e) { - throw new BizException("无法生成验证码图片。"); + throw new BizException(MessageName.CAPTCHA_GENERATE_FAILED); } } } diff --git a/src/main/java/com/onixbyte/helix/service/RoleService.java b/src/main/java/com/onixbyte/helix/service/RoleService.java index 8deedd0..5717c83 100644 --- a/src/main/java/com/onixbyte/helix/service/RoleService.java +++ b/src/main/java/com/onixbyte/helix/service/RoleService.java @@ -1,19 +1,22 @@ package com.onixbyte.helix.service; -import com.onixbyte.helix.enumeration.Status; import com.onixbyte.helix.domain.database.query.wrapper.QueryRoleWrapper; import com.onixbyte.helix.domain.entity.Role; -import com.onixbyte.helix.domain.web.request.AddRoleRequest; -import com.onixbyte.helix.domain.web.request.EditRoleRequest; import com.onixbyte.helix.domain.web.request.QueryRoleRequest; +import com.onixbyte.helix.domain.web.request.RoleRequest; +import com.onixbyte.helix.enumeration.Status; +import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.RoleAuthorityManager; import com.onixbyte.helix.manager.RoleManager; import com.onixbyte.helix.manager.UserRoleManager; +import com.onixbyte.helix.shared.MessageName; +import com.onixbyte.helix.utils.MessageUtil; import jakarta.transaction.Transactional; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import java.util.Optional; @@ -24,16 +27,19 @@ public class RoleService { private final RoleManager roleManager; private final RoleAuthorityManager roleAuthorityManager; private final UserRoleManager userRoleManager; + private final MessageUtil messageUtil; @Autowired public RoleService( RoleManager roleManager, RoleAuthorityManager roleAuthorityManager, - UserRoleManager userRoleManager + UserRoleManager userRoleManager, + MessageUtil messageUtil ) { this.roleManager = roleManager; this.roleAuthorityManager = roleAuthorityManager; this.userRoleManager = userRoleManager; + this.messageUtil = messageUtil; } public Page getRoles(Pageable pageable, QueryRoleRequest request) { @@ -55,11 +61,12 @@ public class RoleService { return roleManager.selectAll(pageable, wrapper); } - public Role addRole(AddRoleRequest request) { - var isDefaultRole = Optional.ofNullable(request.defaultValue()) + public Role addRole(RoleRequest request) { + var isDefaultRole = Optional.of(request) + .map(RoleRequest::defaultValue) .orElse(false); - var status = Optional.ofNullable(request.status()) - .map(Status::valueOf) + var status = Optional.of(request) + .map(RoleRequest::status) .orElse(Status.ACTIVE); var role = Role.builder() @@ -75,24 +82,29 @@ public class RoleService { } @Transactional - public void editRole(EditRoleRequest request) { - roleManager.updateRole(Role.builder() - .id(request.id()) + public Role editRole(Long id, RoleRequest request) { + return roleManager.fullUpdateById(id, Role.builder() .name(request.name()) .code(request.code()) .sort(request.sort()) .defaultValue(request.defaultValue()) .description(request.description()) - .status(Optional.ofNullable(request.status()) - .map(Status::valueOf) - .orElse(null)) + .status(request.status()) .build()); } @Transactional - public void deleteRole(Long id) { + public String deleteRole(Long id) { + var role = roleManager.getRoleById(id) + .orElseThrow(() -> new BizException( + HttpStatus.NOT_FOUND, + messageUtil.getMessage(MessageName.ROLE_NOT_FOUND, id)) + ); + roleAuthorityManager.deleteByRoleId(id); userRoleManager.deleteByRoleId(id); roleManager.deleteRole(id); + + return role.getName(); } } diff --git a/src/main/java/com/onixbyte/helix/shared/Message.java b/src/main/java/com/onixbyte/helix/shared/Message.java deleted file mode 100644 index 0111874..0000000 --- a/src/main/java/com/onixbyte/helix/shared/Message.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2024-2026 OnixByte - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package com.onixbyte.helix.shared; - -public class Message { - - public static final String ASSET_NOT_EMPTY = "asset.not-empty"; - - public static final String AUTHORITY_DELETED = "authority.deleted"; - - public static final String USER_PASSWORD_RESET_SUCCESS = "user.password-reset-success"; - public static final String USER_DELETED = "user.deleted"; -} diff --git a/src/main/java/com/onixbyte/helix/shared/MessageName.java b/src/main/java/com/onixbyte/helix/shared/MessageName.java new file mode 100644 index 0000000..c872f02 --- /dev/null +++ b/src/main/java/com/onixbyte/helix/shared/MessageName.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024-2026 OnixByte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.onixbyte.helix.shared; + +public class MessageName { + + public static final String ASSET_NOT_EMPTY = "asset.not-empty"; + public static final String ASSET_INVALID_PREFIX = "asset.invalid-prefix"; + public static final String ASSET_DELETE_FORBIDDEN = "asset.delete-forbidden"; + public static final String ASSET_UPLOAD_FAILED = "asset.upload-failed"; + + public static final String AUTHORITY_DELETED = "authority.deleted"; + public static final String AUTHORITY_CODE_USED = "authority.code-used"; + public static final String AUTHORITY_NOT_FOUND = "authority.not-found"; + + public static final String AUTH_LOGIN_CAPTCHA_NOT_FOUND = "auth.login.captcha-not-found"; + public static final String AUTH_LOGIN_CAPTCHA_INCORRECT = "auth.login.captcha-incorrect"; + public static final String AUTH_LOGIN_FAILED = "auth.login.failed"; + + public static final String AUTH_PROVIDER_FAILED = "auth.provider.failed"; + public static final String AUTH_PROVIDER_BAD_CREDENTIALS = "auth.provider.bad-credentials"; + public static final String AUTH_PROVIDER_PASSWORD_NOT_CONFIGURED = "auth.provider.password-not-configured"; + + public static final String CAPTCHA_GENERATE_FAILED = "captcha.generate-failed"; + + public static final String ROLE_NOT_EXISTS = "role.not-exists"; + + public static final String USER_PASSWORD_RESET_SUCCESS = "user.password-reset-success"; + public static final String USER_DELETED = "user.deleted"; + public static final String USER_NOT_FOUND = "user.not-found"; + + public static final String SECURITY_CONTEXT_USER_NOT_FOUND = "security.context-user-not-found"; + public static final String TREE_MULTIPLE_ROOTS = "tree.multiple-roots"; + + public static final String ROLE_NOT_FOUND = "role.not-found"; + public static final String ROLE_DELETED = "role.deleted"; + + public static final String REQUEST_ADD_USER_USERNAME_NOT_EMPTY = "request.add-user.username.not-empty"; + public static final String REQUEST_ADD_USER_PASSWORD_NOT_EMPTY = "request.add-user.password.not-empty"; + public static final String REQUEST_ADD_USER_FULL_NAME_NOT_EMPTY = "request.add-user.full-name.not-empty"; + + public static final String REQUEST_AUTHORITY_CODE_NOT_EDITABLE = "request.authority.code.not-editable"; + public static final String REQUEST_AUTHORITY_CODE_NOT_NULL = "request.authority.code.not-null"; + public static final String REQUEST_AUTHORITY_NAME_NOT_NULL = "request.authority.name.not-null"; + + public static final String REQUEST_DEPARTMENT_NAME_NOT_NULL = "request.department.name.not-null"; + + public static final String REQUEST_EDIT_ROLE_ID_NOT_NULL = "request.edit-role.id.not-null"; + public static final String REQUEST_EDIT_ROLE_STATUS_INVALID = "request.edit-role.status.invalid"; + + public static final String REQUEST_EDIT_USER_ID_NOT_NULL = "request.edit-user.id.not-null"; + public static final String REQUEST_EDIT_USER_ID_POSITIVE = "request.edit-user.id.positive"; + + public static final String REQUEST_RESET_PASSWORD_NOT_EMPTY = "request.reset-password.password.not-empty"; + + public static final String REQUEST_ROLE_NAME_NOT_EMPTY = "request.role.name.not-empty"; + public static final String REQUEST_ROLE_CODE_NOT_EMPTY = "request.role.code.not-empty"; + public static final String REQUEST_ROLE_SORT_NOT_NULL = "request.role.sort.not-null"; + + public static final String REQUEST_QUERY_ROLE_STATUS_INVALID = "request.query-role.status.invalid"; + public static final String REQUEST_QUERY_USER_STATUS_INVALID = "request.query-user.status.invalid"; +} diff --git a/src/main/java/com/onixbyte/helix/utils/SecurityUtil.java b/src/main/java/com/onixbyte/helix/utils/SecurityUtil.java index acb59be..18b3ff9 100644 --- a/src/main/java/com/onixbyte/helix/utils/SecurityUtil.java +++ b/src/main/java/com/onixbyte/helix/utils/SecurityUtil.java @@ -2,6 +2,7 @@ package com.onixbyte.helix.utils; import com.onixbyte.helix.domain.entity.User; import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.shared.MessageName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -32,7 +33,7 @@ public class SecurityUtil { if (!(_details instanceof User user)) { log.error("Authentication details is {}", _details); - throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, "Cannot retrieve user information from context."); + throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, MessageName.SECURITY_CONTEXT_USER_NOT_FOUND); } return user; diff --git a/src/main/java/com/onixbyte/helix/utils/TreeUtil.java b/src/main/java/com/onixbyte/helix/utils/TreeUtil.java index 848c65b..adc6514 100644 --- a/src/main/java/com/onixbyte/helix/utils/TreeUtil.java +++ b/src/main/java/com/onixbyte/helix/utils/TreeUtil.java @@ -3,6 +3,7 @@ package com.onixbyte.helix.utils; import com.onixbyte.helix.domain.common.Treeable; import com.onixbyte.helix.domain.common.TreeNode; import com.onixbyte.helix.exception.BizException; +import com.onixbyte.helix.shared.MessageName; import org.apache.commons.collections4.CollectionUtils; import org.springframework.http.HttpStatus; @@ -43,7 +44,7 @@ public class TreeUtil { // Ensure only 1 root node is included if (rootItems.size() > 1) { - throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, "Multiple root items found in given values."); + throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR, MessageName.TREE_MULTIPLE_ROOTS); } // Get root item and build root node diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dc7bbad..cd88c9e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,6 +27,8 @@ spring: hikari: minimum-idle: 1 maximum-pool-size: 10 + messages: + basename: i18n/messages flyway: enabled: true baseline-on-migrate: true diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index f9a18f6..a204e38 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -19,10 +19,28 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # - +tree.multiple-roots= +user.not-found=User with ID [{0}] not found. +security.context-user-not-found=Cannot retrieve user information from security context. +role.not-exists=Role does not exist in database. +captcha.generate-failed= +auth.provider.password-not-configured=Username or password is incorrect. +auth.provider.bad-credentials=Username or password is incorrect. +auth.provider.failed=Authentication failed, please try again later. +auth.login.failed=Cannot perform login due to server error. +auth.login.captcha-incorrect=Captcha is incorrect. +auth.login.captcha-not-found=Captcha not found. +authority.not-found=Authority with ID [{0}] not found. +authority.code-used=Authority code [{0}] is already in use. +asset.upload-failed=Failed to upload file: {0} +asset.delete-forbidden=You are not able to delete an asset that is not uploaded by you. +asset.invalid-prefix=Prefix must not be empty and must not start with '/' or '..'. asset.not-empty = File cannot be empty. authority.deleted = Authority [{0}] deleted. user.password-reset-success = Password has been reset. -user.deleted = User [{0}] deleted. \ No newline at end of file +user.deleted = User [{0}] deleted. +role.not-found=Role with ID {0} not found. +role.deleted=Role [{0}] deleted. +request.role.name.not-empty=Name of the role cannot be empty. \ No newline at end of file diff --git a/src/main/resources/i18n/messages_en_GB.properties b/src/main/resources/i18n/messages_en_GB.properties index f9a18f6..b5a19db 100644 --- a/src/main/resources/i18n/messages_en_GB.properties +++ b/src/main/resources/i18n/messages_en_GB.properties @@ -21,8 +21,56 @@ # asset.not-empty = File cannot be empty. +asset.invalid-prefix = Prefix must not be empty and must not start with '/' or '..'. +asset.delete-forbidden = You are not able to delete an asset that is not uploaded by you. +asset.upload-failed = Failed to upload file: {0} authority.deleted = Authority [{0}] deleted. +authority.code-used = Authority code [{0}] is already in use. +authority.not-found = Authority with ID [{0}] not found. + +auth.login.captcha-not-found = Captcha not found. +auth.login.captcha-incorrect = Captcha is incorrect. +auth.login.failed = Cannot perform login due to server error. + +auth.provider.failed = Authentication failed, please try again later. +auth.provider.bad-credentials = Username or password is incorrect. +auth.provider.password-not-configured = Password is not configured. Please sign in with a third-party account. + +captcha.generate-failed = Unable to generate captcha image. + +role.not-exists = Role does not exist in database. +role.not-found = Role with ID {0} not found. +role.deleted = Role [{0}] deleted. user.password-reset-success = Password has been reset. -user.deleted = User [{0}] deleted. \ No newline at end of file +user.deleted = User [{0}] deleted. +user.not-found = User with ID [{0}] not found. + +security.context-user-not-found = Cannot retrieve user information from security context. +tree.multiple-roots = Multiple root items found in given values. + +request.add-user.username.not-empty = Username cannot be empty. +request.add-user.password.not-empty = Password cannot be empty. +request.add-user.full-name.not-empty = Full name cannot be empty. + +request.authority.code.not-editable = Code cannot be edited. +request.authority.code.not-null = Code cannot be null. +request.authority.name.not-null = Name of the authority cannot be null. + +request.department.name.not-null = Name of the department should not be null. + +request.edit-role.id.not-null = Role ID cannot be null. +request.edit-role.status.invalid = Status can only be ACTIVE or INACTIVE. + +request.edit-user.id.not-null = User ID cannot be null. +request.edit-user.id.positive = User ID must be positive. + +request.reset-password.password.not-empty = Password cannot be empty. + +request.role.name.not-empty = Name of the role cannot be empty. +request.role.code.not-empty = Code of the role cannot be empty. +request.role.sort.not-null = Sort number cannot be null. + +request.query-role.status.invalid = Status can only be ACTIVE or INACTIVE. +request.query-user.status.invalid = Status can only be ACTIVE, INACTIVE, or LOCKED. diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 009ffa2..9c61320 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -21,8 +21,56 @@ # asset.not-empty = 文件不能为空。 +asset.invalid-prefix = 前缀不能为空,且不能以 '/' 或 '..' 开头。 +asset.delete-forbidden = 你不能删除非自己上传的文件。 +asset.upload-failed = 文件上传失败:{0} authority.deleted = 权限【{0}】删除成功。 +authority.code-used = 权限编码【{0}】已被使用。 +authority.not-found = 找不到 ID 为【{0}】的权限。 + +auth.login.captcha-not-found = 未找到验证码。 +auth.login.captcha-incorrect = 验证码错误。 +auth.login.failed = 登录失败,服务器出现错误。 + +auth.provider.failed = 用户认证失败,请稍后再试。 +auth.provider.bad-credentials = 用户名或密码错误。 +auth.provider.password-not-configured = 你还没有配置密码,请使用第三方账号登录。 + +captcha.generate-failed = 无法生成验证码图片。 + +role.not-exists = 角色不存在。 +role.not-found = 找不到 ID 为 {0} 的角色。 +role.deleted = 角色【{0}】删除成功。 user.password-reset-success = 密码修改成功。 -user.deleted = 用户【{0}】删除成功。 \ No newline at end of file +user.deleted = 用户【{0}】删除成功。 +user.not-found = 找不到 ID 为【{0}】的用户信息。 + +security.context-user-not-found = 无法从安全上下文中获取用户信息。 +tree.multiple-roots = 数据中包含多个根节点。 + +request.add-user.username.not-empty = 用户名不能为空。 +request.add-user.password.not-empty = 密码不能为空。 +request.add-user.full-name.not-empty = 姓名不能为空。 + +request.authority.code.not-editable = 权限编码不可修改。 +request.authority.code.not-null = 权限编码不能为空。 +request.authority.name.not-null = 权限名称不能为空。 + +request.department.name.not-null = 部门名称不能为空。 + +request.edit-role.id.not-null = 角色 ID 不能为空。 +request.edit-role.status.invalid = 状态只能是 ACTIVE 或 INACTIVE。 + +request.edit-user.id.not-null = 用户 ID 不能为空。 +request.edit-user.id.positive = 用户 ID 必须为正数。 + +request.reset-password.password.not-empty = 密码不能为空。 + +request.role.name.not-empty = 角色名称不能为空。 +request.role.code.not-empty = 角色编码不能为空。 +request.role.sort.not-null = 排序编号不能为空。 + +request.query-role.status.invalid = 状态只能是 ACTIVE 或 INACTIVE。 +request.query-user.status.invalid = 状态只能是 ACTIVE、INACTIVE 或 LOCKED。 From 6f29904349c1607f664f6161c87ffd8ddcae8a94 Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 24 Mar 2026 12:30:36 +0800 Subject: [PATCH 58/60] feat: update user editing functionality to include user ID in the request --- .../onixbyte/helix/controller/UserController.java | 12 +++++++----- .../java/com/onixbyte/helix/service/UserService.java | 6 ++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/controller/UserController.java b/src/main/java/com/onixbyte/helix/controller/UserController.java index 9a2dc1f..c0d43e9 100644 --- a/src/main/java/com/onixbyte/helix/controller/UserController.java +++ b/src/main/java/com/onixbyte/helix/controller/UserController.java @@ -13,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -82,13 +81,16 @@ public class UserController { /** * Edit a user. * + * @param id user ID * @param request user to be edited * @return edited user */ - @PutMapping - public ResponseEntity editUser(@Validated @RequestBody EditUserRequest request) { - userService.updateUser(request); - return ResponseEntity.ok(null); + @PutMapping("/{id:\\d+}") + public UserDetailResponse editUser( + @PathVariable Long id, + @Validated @RequestBody EditUserRequest request + ) { + return userService.updateUser(id, request); } /** diff --git a/src/main/java/com/onixbyte/helix/service/UserService.java b/src/main/java/com/onixbyte/helix/service/UserService.java index adda0b7..88af91c 100644 --- a/src/main/java/com/onixbyte/helix/service/UserService.java +++ b/src/main/java/com/onixbyte/helix/service/UserService.java @@ -143,9 +143,9 @@ public class UserService { } @Transactional(rollbackFor = Throwable.class) - public void updateUser(EditUserRequest request) { + public UserDetailResponse updateUser(Long id, EditUserRequest request) { userManager.updateUser(User.builder() - .id(request.id()) + .id(id) .fullName(request.fullName()) .email(request.email()) .regionAbbreviation(request.regionAbbreviation()) @@ -155,6 +155,8 @@ public class UserService { .departmentId(request.departmentId()) .positionId(request.positionId()) .build()); + + return getUserDetailByUserId(id); } public UserDetailResponse getUserDetailByUserId(Long userId) { From 9d2fc024ea6817b494b77ceda159ad3bd911b6c4 Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 24 Mar 2026 15:49:48 +0800 Subject: [PATCH 59/60] feat: add department name uniqueness check and corresponding error message --- .../helix/exception/BizException.java | 27 -------- .../helix/manager/DepartmentManager.java | 6 ++ .../repository/DepartmentRepository.java | 2 + .../helix/service/DepartmentService.java | 7 ++ .../onixbyte/helix/shared/MessageName.java | 2 + src/main/resources/i18n/messages.properties | 68 +++++++++++++------ .../resources/i18n/messages_en_GB.properties | 1 + .../resources/i18n/messages_zh_CN.properties | 1 + 8 files changed, 68 insertions(+), 46 deletions(-) diff --git a/src/main/java/com/onixbyte/helix/exception/BizException.java b/src/main/java/com/onixbyte/helix/exception/BizException.java index 8e99c74..d864077 100644 --- a/src/main/java/com/onixbyte/helix/exception/BizException.java +++ b/src/main/java/com/onixbyte/helix/exception/BizException.java @@ -44,40 +44,13 @@ public class BizException extends RuntimeException { private final String messageCode; private final Object[] messageArgs; - /** - * Constructs a new business exception with the specified HTTP status and message. - * - * @param message the detailed error message explaining the business logic violation - */ - public BizException(String message) { - super(message); - this.status = HttpStatus.INTERNAL_SERVER_ERROR; - this.messageCode = null; - this.messageArgs = new Object[0]; - } - - /** - * Constructs a new business exception with the specified HTTP status and message. - * - * @param status the HTTP status code to associate with this exception - * @param message the detailed error message explaining the business logic violation - */ - public BizException(HttpStatus status, String message) { - super(message); - this.status = status; - this.messageCode = null; - this.messageArgs = new Object[0]; - } - public BizException(String messageCode, Object... messageArgs) { - super(messageCode); this.status = HttpStatus.INTERNAL_SERVER_ERROR; this.messageCode = messageCode; this.messageArgs = messageArgs == null ? new Object[0] : messageArgs; } public BizException(HttpStatus status, String messageCode, Object... messageArgs) { - super(messageCode); this.status = status; this.messageCode = messageCode; this.messageArgs = messageArgs == null ? new Object[0] : messageArgs; diff --git a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java index c6f7b3a..1f07468 100644 --- a/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java +++ b/src/main/java/com/onixbyte/helix/manager/DepartmentManager.java @@ -3,6 +3,8 @@ package com.onixbyte.helix.manager; import com.onixbyte.helix.domain.entity.Department; import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.repository.DepartmentRepository; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -68,4 +70,8 @@ public class DepartmentManager { departmentToEdit.setUpdatedAt(updatedAt); return departmentToEdit; } + + public boolean existsByName(String name) { + return departmentRepository.existsByName(name); + } } diff --git a/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java b/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java index 79a1849..49ad5c2 100644 --- a/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java +++ b/src/main/java/com/onixbyte/helix/repository/DepartmentRepository.java @@ -15,4 +15,6 @@ public interface DepartmentRepository extends JpaRepository { or d.parentId = :parentId """) Integer findMaxSort(Long parentId); + + boolean existsByName(String name); } diff --git a/src/main/java/com/onixbyte/helix/service/DepartmentService.java b/src/main/java/com/onixbyte/helix/service/DepartmentService.java index c13793b..5400e5d 100644 --- a/src/main/java/com/onixbyte/helix/service/DepartmentService.java +++ b/src/main/java/com/onixbyte/helix/service/DepartmentService.java @@ -4,10 +4,13 @@ import com.onixbyte.helix.domain.common.TreeNode; import com.onixbyte.helix.domain.entity.Department; import com.onixbyte.helix.domain.web.request.DepartmentRequest; import com.onixbyte.helix.enumeration.Status; +import com.onixbyte.helix.exception.BizException; import com.onixbyte.helix.manager.DepartmentManager; +import com.onixbyte.helix.shared.MessageName; import com.onixbyte.helix.utils.TreeUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,6 +41,10 @@ public class DepartmentService { public Department addDepartment(DepartmentRequest request) { var createdAt = LocalDateTime.now(); + if (departmentManager.existsByName(request.name())) { + throw new BizException(HttpStatus.CONFLICT, MessageName.REQUEST_CREATE_DEPARTMENT_NAME_DUPLICATED); + } + var parentId = request.parentId(); var sort = Optional.ofNullable(request.sort()) .orElseGet(() -> departmentManager.getNextSort(parentId)); diff --git a/src/main/java/com/onixbyte/helix/shared/MessageName.java b/src/main/java/com/onixbyte/helix/shared/MessageName.java index c872f02..7c33f66 100644 --- a/src/main/java/com/onixbyte/helix/shared/MessageName.java +++ b/src/main/java/com/onixbyte/helix/shared/MessageName.java @@ -79,4 +79,6 @@ public class MessageName { public static final String REQUEST_QUERY_ROLE_STATUS_INVALID = "request.query-role.status.invalid"; public static final String REQUEST_QUERY_USER_STATUS_INVALID = "request.query-user.status.invalid"; + + public static final String REQUEST_CREATE_DEPARTMENT_NAME_DUPLICATED = "request.create-department.name-duplicated"; } diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index a204e38..f4a9f5b 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -19,28 +19,58 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # -tree.multiple-roots= -user.not-found=User with ID [{0}] not found. -security.context-user-not-found=Cannot retrieve user information from security context. -role.not-exists=Role does not exist in database. -captcha.generate-failed= -auth.provider.password-not-configured=Username or password is incorrect. -auth.provider.bad-credentials=Username or password is incorrect. -auth.provider.failed=Authentication failed, please try again later. -auth.login.failed=Cannot perform login due to server error. -auth.login.captcha-incorrect=Captcha is incorrect. -auth.login.captcha-not-found=Captcha not found. -authority.not-found=Authority with ID [{0}] not found. -authority.code-used=Authority code [{0}] is already in use. -asset.upload-failed=Failed to upload file: {0} -asset.delete-forbidden=You are not able to delete an asset that is not uploaded by you. -asset.invalid-prefix=Prefix must not be empty and must not start with '/' or '..'. + asset.not-empty = File cannot be empty. +asset.invalid-prefix = Prefix must not be empty and must not start with '/' or '..'. +asset.delete-forbidden = You are not able to delete an asset that is not uploaded by you. +asset.upload-failed = Failed to upload file: {0} authority.deleted = Authority [{0}] deleted. +authority.code-used = Authority code [{0}] is already in use. +authority.not-found = Authority with ID [{0}] not found. + +auth.login.captcha-not-found = Captcha not found. +auth.login.captcha-incorrect = Captcha is incorrect. +auth.login.failed = Cannot perform login due to server error. + +auth.provider.failed = Authentication failed, please try again later. +auth.provider.bad-credentials = Username or password is incorrect. +auth.provider.password-not-configured=Username or password is incorrect. + +captcha.generate-failed = Unable to generate captcha image. +role.not-exists=Name of the authority cannot be null. +role.not-found = Role with ID {0} not found. +role.deleted = Role [{0}] deleted. user.password-reset-success = Password has been reset. user.deleted = User [{0}] deleted. -role.not-found=Role with ID {0} not found. -role.deleted=Role [{0}] deleted. -request.role.name.not-empty=Name of the role cannot be empty. \ No newline at end of file +user.not-found = User with ID [{0}] not found. + +security.context-user-not-found = Cannot retrieve user information from security context. +tree.multiple-roots = Multiple root items found in given values. + +request.add-user.username.not-empty = Username cannot be empty. +request.add-user.password.not-empty = Password cannot be empty. +request.add-user.full-name.not-empty = Full name cannot be empty. + +request.authority.code.not-editable = Code cannot be edited. +request.authority.code.not-null = Code cannot be null. +request.authority.name.not-null = Name of the authority cannot be null. + +request.department.name.not-null = Name of the department should not be null. + +request.edit-role.id.not-null = Role ID cannot be null. +request.edit-role.status.invalid = Status can only be ACTIVE or INACTIVE. + +request.edit-user.id.not-null = User ID cannot be null. +request.edit-user.id.positive = User ID must be positive. + +request.reset-password.password.not-empty = Password cannot be empty. + +request.role.name.not-empty = Name of the role cannot be empty. +request.role.code.not-empty = Code of the role cannot be empty. +request.role.sort.not-null = Sort number cannot be null. + +request.query-role.status.invalid = Status can only be ACTIVE or INACTIVE. +request.query-user.status.invalid = Status can only be ACTIVE, INACTIVE, or LOCKED. +request.create-department.name-duplicated=Name of the department already exists. diff --git a/src/main/resources/i18n/messages_en_GB.properties b/src/main/resources/i18n/messages_en_GB.properties index b5a19db..ef1634a 100644 --- a/src/main/resources/i18n/messages_en_GB.properties +++ b/src/main/resources/i18n/messages_en_GB.properties @@ -74,3 +74,4 @@ request.role.sort.not-null = Sort number cannot be null. request.query-role.status.invalid = Status can only be ACTIVE or INACTIVE. request.query-user.status.invalid = Status can only be ACTIVE, INACTIVE, or LOCKED. +request.create-department.name-duplicated=Name of the department already exists. diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 9c61320..7bb0fdb 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -74,3 +74,4 @@ request.role.sort.not-null = 排序编号不能为空。 request.query-role.status.invalid = 状态只能是 ACTIVE 或 INACTIVE。 request.query-user.status.invalid = 状态只能是 ACTIVE、INACTIVE 或 LOCKED。 +request.create-department.name-duplicated=部门名称已存在。 From dedf95865ecdf6eedbb2fd90a3c82be0ebf143c4 Mon Sep 17 00:00:00 2001 From: siujamo Date: Tue, 24 Mar 2026 17:29:03 +0800 Subject: [PATCH 60/60] feat: add department name uniqueness check and corresponding error message --- .../com/onixbyte/helix/service/DepartmentService.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/onixbyte/helix/service/DepartmentService.java b/src/main/java/com/onixbyte/helix/service/DepartmentService.java index 5400e5d..901d2f7 100644 --- a/src/main/java/com/onixbyte/helix/service/DepartmentService.java +++ b/src/main/java/com/onixbyte/helix/service/DepartmentService.java @@ -60,7 +60,15 @@ public class DepartmentService { } public Department editDepartment(Long id, DepartmentRequest request) { + if (departmentManager.existsByName(request.name())) { + throw new BizException(HttpStatus.CONFLICT, MessageName.REQUEST_CREATE_DEPARTMENT_NAME_DUPLICATED); + } + return departmentManager.fullUpdateById(id, Department.builder() + .name(request.name()) + .parentId(request.parentId()) + .sort(request.sort()) + .status(request.status()) .build()); } }