Compare commits

...

58 Commits

Author SHA1 Message Date
siujamo dedf95865e feat: add department name uniqueness check and corresponding error message 2026-03-24 17:29:03 +08:00
siujamo 9d2fc024ea feat: add department name uniqueness check and corresponding error message 2026-03-24 15:49:48 +08:00
siujamo 6f29904349 feat: update user editing functionality to include user ID in the request 2026-03-24 12:30:36 +08:00
siujamo 08c18fea90 feat: refactor message handling to use MessageName constants for improved internationalisation support 2026-03-24 12:23:13 +08:00
siujamo 776ddd28c1 feat: implement internationalisation support with message utility and localisation configuration 2026-03-24 10:49:33 +08:00
siujamo a3596ad086 feat: refactor authority request handling and update methods to use AuthorityRequest 2026-03-24 10:27:52 +08:00
siujamo e1ad5cdfd8 feat: rename AddDepartmentRequest to DepartmentRequest and implement department editing functionality 2026-03-24 10:07:37 +08:00
siujamo 712a675325 refactor: remove unused imports from GuidConfig and JacksonConfig classes 2026-03-23 15:45:45 +08:00
siujamo 8b343af4e8 refactor: remove unused logger imports and related fields from manager and controller classes 2026-03-23 15:45:11 +08:00
siujamo 69e3f84bec feat: implement authority deletion functionality and related repository methods 2026-03-23 15:39:44 +08:00
siujamo aebb693ee7 feat: add department creation endpoint and request validation 2026-03-23 15:13:23 +08:00
siujamo 7b9849c311 feat: implement custom validation framework with dynamic rules 2026-03-20 10:33:16 +08:00
siujamo d33f4f2dbf refactor: rename database table names for consistency 2026-03-20 09:24:51 +08:00
siujamo 484a9f4a71 feat: update database schema and configuration for Flyway integration and refactor table names 2026-03-19 10:54:50 +08:00
zihluwang 7c9f9c35f9 chore: rename settings 2026-03-14 08:28:43 +08:00
siujamo bfa0690065 fix: update cookie settings for production and development modes 2026-03-12 17:25:19 +08:00
siujamo abbcb6bf5c feat: enhance cookie creation method with detailed documentation 2026-03-12 15:37:51 +08:00
siujamo bd597fbc64 refactor: extract build cookie 2026-03-12 14:44:59 +08:00
siujamo 14e53740b0 refactor: remove unused imports and fields 2026-03-12 14:38:06 +08:00
siujamo 7ce241cc16 feat: implement user credential management with new UserCredential entity and related components 2026-03-12 13:56:06 +08:00
siujamo 0562c8548b feat: introduce ApplicationMode enumeration and integrate it into ApplicationProperties and ApplicationManager 2026-03-12 13:55:53 +08:00
siujamo e83f1358e3 chore: remove bruno api files 2026-03-12 13:55:22 +08:00
siujamo 2de2cb029d feat: add Spring Boot Actuator dependency and update Java JWT version 2026-03-12 13:54:31 +08:00
siujamo d2b086ce17 refactor: enhance department and user data insertion with CTEs and streamline role authority assignments 2026-01-29 09:27:28 +08:00
siujamo e5ae5f61b5 style: reformat init.sql 2026-01-29 09:15:56 +08:00
siujamo d9bd1a1b0e chore: disable redis repositories and JTA support in application.yml 2026-01-28 16:15:17 +08:00
siujamo 9b48869f3f refactor: move caching configuration to CacheConfig and update application.yml settings 2026-01-28 16:08:51 +08:00
siujamo 64be3a79ea refactor: update init.sql to current schema 2026-01-28 15:45:45 +08:00
siujamo e9c6732888 chore: upgrade postgres driver 2026-01-28 15:45:13 +08:00
siujamo 891b7f9280 build: optimise docker build script 2026-01-28 15:44:54 +08:00
siujamo 3095e34164 build: add docker build script 2026-01-28 14:44:10 +08:00
siujamo d6944957c8 refactor: optimise dependencies 2026-01-28 14:32:29 +08:00
siujamo ba2ed6d22c refactor: categorise dependencies 2026-01-28 11:23:44 +08:00
zihluwang f6096c5ab7 docs: 添加 API docs 2026-01-27 13:42:52 +08:00
zihluwang 4dc9ed341d feat: 从 Cookie 中获取 AccessToken 2026-01-09 14:22:05 +08:00
zihluwang c13520bd8b docs: 添加 Javadoc 2026-01-09 10:49:54 +08:00
zihluwang a8c349bd75 docs: 添加 Javadoc 2026-01-09 09:17:01 +08:00
zihluwang a526ac795d feat: 添加退出接口 2026-01-06 00:07:15 +08:00
zihluwang b9fee3787a feat: 忽略 cookie secure 及 domain 配置 2026-01-05 23:59:05 +08:00
zihluwang 0a10c64278 refactor: 调整 Token 返回方式
Closes #1
2026-01-05 23:53:31 +08:00
siujamo 8d148f88d0 style: 修改代码风格 2025-12-31 17:48:28 +08:00
siujamo 2db2b594f6 refactor: 为添加用户功能添加返回值 2025-12-31 17:47:34 +08:00
siujamo a5da09cf09 feat: 修改权限功能 2025-12-31 17:00:45 +08:00
siujamo 9b8d276e0b feat: 添加权限 2025-12-31 16:42:08 +08:00
siujamo 382a6c177f refactor: 为 JPA 实体添加 life cycle 配置 2025-12-31 14:55:11 +08:00
siujamo d25c754bf0 feat: 查询权限信息列表 2025-12-31 14:47:27 +08:00
siujamo 4ca7791e26 refactor: 将共享常量类的构造器设置为私有 2025-12-31 14:19:51 +08:00
siujamo 5406d810a6 docs: 移除错误的 @see 引用 2025-12-31 11:19:48 +08:00
siujamo b24a6dc809 refactor: 调整包结构 2025-12-31 11:14:10 +08:00
siujamo d2d0ccef1c style: 移除魔法变量 2025-12-31 09:54:12 +08:00
siujamo c74a67cdc6 perf: 性能优化
使用全局唯一 JWT Verifier 实例
2025-12-31 09:46:28 +08:00
siujamo 659d123f2b refactor: 调整包结构 2025-12-31 09:29:33 +08:00
siujamo f62e26d441 feat: 删除角色功能 2025-12-29 12:33:01 +08:00
siujamo 05b6bbca79 chore(deps): 升级 JDK 21 2025-12-29 12:32:40 +08:00
siujamo 670e54aefb chore(deps): 导入数学计算模块 2025-12-29 10:36:50 +08:00
zihluwang aa19c91465 refactor: 补充文档并优化代码 2025-12-27 11:36:28 +08:00
siujamo 3f1c320a48 feat: 修改角色功能 2025-12-26 17:48:52 +08:00
siujamo 826f51926b feat: 创建角色功能 2025-12-26 14:59:10 +08:00
135 changed files with 3211 additions and 1209 deletions
+152
View File
@@ -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/
+27
View File
@@ -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 bootJar --no-daemon -PartefactVersion=${ARTEFACT_VERSION}
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"]
+10 -6
View File
@@ -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
@@ -14,10 +14,10 @@ tasks.withType<JavaCompile> {
}
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)
}
}
@@ -32,11 +32,13 @@ 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)
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)
@@ -45,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)
+4 -4
View File
@@ -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
-324
View File
@@ -1,324 +0,0 @@
/**
* 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());
+46 -27
View File
@@ -1,13 +1,13 @@
[versions]
jspecifyVersion = "1.0.0"
javaJwtVersion = "4.5.0"
postgresDriverVersion = "42.7.7"
javaJwtVersion = "4.5.1"
postgresDriverVersion = "42.7.9"
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,39 +17,58 @@ commonsLangVersion = "3.20.0"
mybatisVersion = "3.0.5"
jacksonVersion = "2.19.2"
hypersistenceVersion = "3.14.0"
springDependencyManagementVersion = "1.1.7"
[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" }
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" }
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" }
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" }
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-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]
spring-boot = { id = "org.springframework.boot", version.ref = "springBootVersion" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagementVersion" }
@@ -1,31 +1,20 @@
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);
}
}
@@ -1,10 +1,13 @@
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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@@ -23,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.
@@ -31,9 +35,11 @@ public class TokenClient {
* @param tokenProperties the configuration properties for the token, such as issuer and
* validity period
*/
public TokenClient(Algorithm algorithm, TokenProperties tokenProperties) {
@Autowired
public TokenClient(Algorithm algorithm, TokenProperties tokenProperties, JWTVerifier verifier) {
this.algorithm = algorithm;
this.tokenProperties = tokenProperties;
this.verifier = verifier;
}
/**
@@ -54,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);
}
}
@@ -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 {
}
@@ -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 {
/**
@@ -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.
* <p>
* 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.
* <p>
* The generated IDs are:
* <ul>
* <li>Globally unique across all instances</li>
* <li>Time-ordered (newer IDs have higher values)</li>
* <li>Highly performant with minimal coordination overhead</li>
* </ul>
*
* @return a configured {@link SnowflakeIdentityGenerator} instance for generating user IDs
* @see SnowflakeIdentityGenerator
* @see IdentityGenerator
*/
@Bean
public IdentityGenerator<Long> userIdentityGenerator() {
return new SnowflakeIdentityGenerator(0x0, 0x0);
@@ -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;
}
}
@@ -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;
@@ -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();
}
}
@@ -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;
}
}
@@ -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.
* <p>
* 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.
* <p>
* The configuration supports:
* <ul>
* <li>Reactive HTTP client configuration</li>
* <li>Non-blocking I/O operations</li>
* <li>Asynchronous request/response handling</li>
* </ul>
*
* @author zihluwang
* @since 1.0.0
* @see WebClient
* @see Configuration
*/
@Configuration
public class WebFluxConfig {
/**
* Creates a reactive WebClient for HTTP communication with external services.
* <p>
* 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.
* <p>
* 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.
* <p>
* Key features:
* <ul>
* <li>Non-blocking I/O operations</li>
* <li>Reactive streams support</li>
* <li>Built-in support for JSON serialisation/deserialisation</li>
* <li>Configurable timeouts and retry mechanisms</li>
* </ul>
*
* @return a configured {@link WebClient} instance for reactive HTTP operations
* @see WebClient
* @see WebClient.Builder
*/
@Bean
public WebClient webClient() {
return WebClient.builder()
.build();
}
}
@@ -1,26 +0,0 @@
package com.onixbyte.helix.constant;
/**
* Constants for external host configurations and endpoints.
* <p>
* 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.
* <p>
* The class is designed to hold static final constants representing:
* <ul>
* <li>External API base URLs</li>
* <li>Third-party service endpoints</li>
* <li>Integration service hosts</li>
* <li>External resource locations</li>
* </ul>
* <p>
* This class cannot be instantiated as it serves purely as a constant container.
*
* @author zihluwang
* @since 1.0.0
*/
public final class ExternalHost {
}
@@ -1,22 +1,23 @@
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;
import com.onixbyte.helix.shared.MessageName;
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.*;
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")
@@ -26,11 +27,7 @@ public class AssetController {
private final AssetService assetService;
/**
* Constructs a new FileController with the specified file service.
*
* @param assetService the file service to use for file operations
*/
@Autowired
public AssetController(AssetService assetService) {
this.assetService = assetService;
}
@@ -47,7 +44,7 @@ public class AssetController {
) {
try {
if (file.isEmpty()) {
throw new BizException(HttpStatus.BAD_REQUEST, "File cannot be empty.");
throw new BizException(HttpStatus.BAD_REQUEST, MessageName.ASSET_NOT_EMPTY);
}
var fileUrl = assetService.uploadFile(AssetPrefix.UPLOADS, file);
@@ -60,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());
}
}
@@ -1,22 +1,43 @@
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.domain.web.request.LoginRequest;
import com.onixbyte.helix.domain.web.response.UserDetailResponse;
import com.onixbyte.helix.service.AuthService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.onixbyte.helix.service.TokenService;
import com.onixbyte.helix.service.UserService;
import com.onixbyte.helix.shared.TokenConstant;
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.*;
import java.time.Duration;
/**
* This controller provides entry points making user authorised.
*
* @author zihluwang
* @author siujamo
*/
@RestController
@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;
public AuthController(AuthService authService) {
@Autowired
public AuthController(
AuthService authService,
TokenService tokenService,
UserService userService
) {
this.authService = authService;
this.tokenService = tokenService;
this.userService = userService;
}
/**
@@ -26,14 +47,39 @@ public class AuthController {
* @return detailed user info and authentication token
*/
@PostMapping("/login")
public LoginSuccessResponse loginWithUsernameAndPassword(
@Validated @RequestBody UsernamePasswordLoginRequest request
public ResponseEntity<UserDetailResponse> 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));
}
/**
* 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<Void> logout() {
var cookie = authService.buildCookie(TokenConstant.TOKEN_NAME, "", Duration.ZERO);
return ResponseEntity.status(HttpStatus.OK)
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(null);
}
}
@@ -0,0 +1,84 @@
package com.onixbyte.helix.controller;
import com.onixbyte.helix.domain.entity.Authority;
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.MessageName;
import com.onixbyte.helix.utils.MessageUtil;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* This controller provides entry points for manipulate authorities.
*
* @author zihluwang
* @author siujamo
*/
@RestController
@RequestMapping("/authorities")
public class AuthorityController {
private final AuthorityService authorityService;
private final MessageUtil messageUtil;
public AuthorityController(
AuthorityService authorityService,
MessageUtil messageUtil
) {
this.authorityService = authorityService;
this.messageUtil = messageUtil;
}
/**
* 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<Authority> 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);
}
/**
* Add an authority.
*
* @param request authority specs
* @return created authority
*/
@PostMapping
public Authority addAuthority(@Validated @RequestBody AuthorityRequest request) {
return authorityService.addAuthority(request);
}
/**
* Edit an authority.
*
* @param request authority specs
* @return edited authority
*/
@PutMapping("/{id:\\d+}")
public Authority editAuthority(
@PathVariable Long id,
@Validated @RequestBody AuthorityRequest request
) {
return authorityService.editAuthority(id, request);
}
@DeleteMapping("/{authorityId:\\d+}")
public ActionResponse deleteAuthority(@PathVariable Long authorityId) {
var name = authorityService.deleteAuthority(authorityId);
return ActionResponse.success(messageUtil.getMessage(MessageName.AUTHORITY_DELETED, name));
}
}
@@ -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;
@@ -9,16 +10,28 @@ 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 {
private final CaptchaService captchaService;
@Autowired
public CaptchaController(CaptchaService captchaService) {
this.captchaService = captchaService;
}
/**
* Get captcha image and captcha uuid.
*
* @return captcha response, contains the uuid of the captcha and image BASE64
*/
@GetMapping
public ResponseEntity<CaptchaResponse> getCaptcha() {
var captchaTuple = captchaService.buildCaptcha();
@@ -1,20 +1,28 @@
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.domain.web.request.DepartmentRequest;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* This controller provides entry points that manipulates departments.
*
* @author zihluwang
* @author siujamo
*/
@RestController
@RequestMapping("/departments")
public class DepartmentController {
private final DepartmentService departmentService;
@Autowired
public DepartmentController(DepartmentService departmentService) {
this.departmentService = departmentService;
}
@@ -28,4 +36,17 @@ public class DepartmentController {
public List<Department> getDepartments() {
return departmentService.getDepartments();
}
@PostMapping
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);
}
}
@@ -2,9 +2,12 @@ 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;
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;
@@ -13,22 +16,10 @@ import java.time.LocalDateTime;
import java.util.stream.Collectors;
/**
* Global exception handler for the Helix application.
* <p>
* 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.
* <p>
* The controller handles various types of exceptions including:
* <ul>
* <li>Business logic exceptions ({@link BizException})</li>
* <li>Bean validation constraint violations ({@link ConstraintViolationException})</li>
* </ul>
* <p>
* 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
@@ -37,51 +28,25 @@ import java.util.stream.Collectors;
@RestControllerAdvice
public class ExceptionController {
private final MessageUtil messageUtil;
public ExceptionController(MessageUtil messageUtil) {
this.messageUtil = messageUtil;
}
/**
* Handles business logic exceptions thrown throughout the application.
* <p>
* 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.
* <p>
* 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<BizExceptionResponse> 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)
);
}
/**
* Handles bean validation constraint violation exceptions.
* <p>
* 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.
* <p>
* 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) {
@@ -94,4 +59,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
);
}
}
@@ -1,24 +1,37 @@
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;
import org.springframework.web.bind.annotation.RequestMapping;
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 {
private final MenuService menuService;
@Autowired
public MenuController(MenuService menuService) {
this.menuService = menuService;
}
/**
* Get menu tree.
*
* @return available menu tree for the current user
*/
@GetMapping
public List<TreeNode<Menu>> getMenuTree() {
return menuService.getMenuTree();
@@ -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;
@@ -10,16 +11,30 @@ 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 {
private final PositionService positionService;
@Autowired
public PositionController(PositionService positionService) {
this.positionService = positionService;
}
/**
* Get position data paginated.
*
* @param pageNum current page num
* @param pageSize page size
* @return paginated position data
*/
@GetMapping
public Page<Position> getPositions(
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@@ -2,21 +2,35 @@ package com.onixbyte.helix.controller;
import com.onixbyte.helix.domain.entity.Role;
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.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 {
private final RoleService roleService;
private final MessageUtil messageUtil;
public RoleController(RoleService roleService) {
@Autowired
public RoleController(RoleService roleService, MessageUtil messageUtil) {
this.roleService = roleService;
this.messageUtil = messageUtil;
}
@GetMapping
@@ -28,4 +42,23 @@ public class RoleController {
var pageRequest = PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Order.asc("id")));
return roleService.getRoles(pageRequest, request);
}
@PostMapping
public Role addRole(@Validated @RequestBody RoleRequest request) {
return roleService.addRole(request);
}
@PutMapping("/{id:\\d+}")
public Role editRole(
@PathVariable Long id,
@Validated @RequestBody RoleRequest request
) {
return roleService.editRole(id, request);
}
@DeleteMapping("/{id:\\d+}")
public ActionResponse deleteRole(@PathVariable Long id) {
var name = roleService.deleteRole(id);
return ActionResponse.success(messageUtil.getMessage(MessageName.ROLE_DELETED, name));
}
}
@@ -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 {
@@ -1,28 +1,39 @@
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.EditUserRequest;
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.ActionResponse;
import com.onixbyte.helix.domain.web.response.UserDetailResponse;
import com.onixbyte.helix.service.UserService;
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.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 {
private final UserService userService;
private final MessageUtil messageUtil;
public UserController(UserService userService) {
@Autowired
public UserController(UserService userService, MessageUtil messageUtil) {
this.userService = userService;
this.messageUtil = messageUtil;
}
/**
@@ -55,30 +66,59 @@ 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 ResponseEntity<Void> addUser(@Validated @RequestBody AddUserRequest request) {
userService.addUser(request);
return ResponseEntity.ok(null);
public UserDetailResponse addUser(@Validated @RequestBody AddUserRequest request) {
return userService.addUser(request);
}
@PutMapping
public ResponseEntity<Void> editUser(@Validated @RequestBody UpdateUserRequest request) {
userService.updateUser(request);
return ResponseEntity.ok(null);
/**
* Edit a user.
*
* @param id user ID
* @param request user to be edited
* @return edited user
*/
@PutMapping("/{id:\\d+}")
public UserDetailResponse editUser(
@PathVariable Long id,
@Validated @RequestBody EditUserRequest request
) {
return userService.updateUser(id, request);
}
/**
* 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<Void> resetPassword(@Validated @RequestBody ResetPasswordRequest request) {
userService.resetPassword(request);
return ResponseEntity.ok(null);
@PatchMapping("/reset-password/{id:\\d+}")
public ActionResponse resetPassword(
@PathVariable Long id,
@Validated @RequestBody ResetPasswordRequest request
) {
userService.resetPassword(id, request);
return ActionResponse.success(messageUtil.getMessage(MessageName.USER_PASSWORD_RESET_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<Void> deleteUser(@PathVariable Long userId) {
public ActionResponse deleteUser(@PathVariable Long userId) {
userService.deleteUser(userId);
return ResponseEntity.ok(null);
return ActionResponse.success(messageUtil.getMessage(MessageName.USER_DELETED, userId));
}
}
@@ -1,4 +1,4 @@
package com.onixbyte.helix.domain.model;
package com.onixbyte.helix.domain.common;
import java.util.ArrayList;
import java.util.List;
@@ -0,0 +1,12 @@
package com.onixbyte.helix.domain.database.query.wrapper;
/**
* Wrapper class for authority query criteria.
* <p>
* 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 {
}
@@ -1,7 +1,15 @@
package com.onixbyte.helix.domain.database.query.wrapper;
import com.onixbyte.helix.constant.Status;
import com.onixbyte.helix.enumeration.Status;
/**
* Wrapper class for role query criteria.
* <p>
* 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;
}
}
@@ -1,9 +1,17 @@
package com.onixbyte.helix.domain.database.query.wrapper;
import com.onixbyte.helix.constant.UserStatus;
import com.onixbyte.helix.enumeration.UserStatus;
import java.time.LocalDateTime;
/**
* Wrapper class for user query criteria.
* <p>
* 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;
@@ -6,7 +6,7 @@ import java.time.LocalDateTime;
import java.util.Objects;
@Entity
@Table(name = "assets")
@Table(name = "asset")
public class Asset {
@Id
@@ -1,11 +1,11 @@
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;
import java.util.Objects;
@@ -22,7 +22,7 @@ import java.util.Objects;
* @since 1.0
*/
@Entity
@Table(name = "authorities")
@Table(name = "authority")
public class Authority {
/**
@@ -262,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();
}
}
@@ -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;
@@ -23,7 +23,7 @@ import java.util.Objects;
* @since 1.0
*/
@Entity
@Table(name = "departments")
@Table(name = "department")
public class Department implements Treeable<Long> {
/**
@@ -287,4 +287,16 @@ public class Department implements Treeable<Long> {
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();
}
}
@@ -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;
@@ -10,7 +10,7 @@ import java.time.LocalDateTime;
import java.util.Objects;
@Entity
@Table(name = "menus")
@Table(name = "menu")
public class Menu implements Treeable<Long> {
@Id
@@ -224,4 +224,16 @@ public class Menu implements Treeable<Long> {
", updatedAt=" + updatedAt +
'}';
}
@PrePersist
protected void onCreate() {
var createTime = LocalDateTime.now();
this.createdAt = createTime;
this.updatedAt = createTime;
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
@@ -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;
@@ -21,7 +21,7 @@ import java.util.Objects;
* @since 1.0
*/
@Entity
@Table(name = "positions")
@Table(name = "position")
public class Position {
/**
@@ -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();
}
}
@@ -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;
@@ -20,7 +20,7 @@ import java.util.Objects;
* @since 1.0
*/
@Entity
@Table(name = "roles")
@Table(name = "role")
public class Role {
/**
@@ -305,4 +305,17 @@ public class Role {
return new Role(id, name, code, sort, defaultValue, description, status, createdAt, updatedAt);
}
}
@PrePersist
private void onCreate() {
var createTime = LocalDateTime.now();
this.createdAt = createTime;
this.updatedAt = createTime;
}
@PreUpdate
private void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
@@ -13,7 +13,7 @@ import java.util.Objects;
* auditing field.
*/
@Entity
@Table(name = "role_authorities")
@Table(name = "role_authority")
public class RoleAuthority {
/**
@@ -122,4 +122,9 @@ public class RoleAuthority {
return new RoleAuthority(roleId, authorityId, createdAt);
}
}
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
}
}
@@ -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;
@@ -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 {
/**
@@ -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();
}
}
@@ -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;
@@ -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"}),
@@ -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.
*/
@@ -121,37 +115,12 @@ 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() {
}
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;
@@ -200,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;
}
@@ -361,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;
@@ -386,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;
@@ -447,7 +392,19 @@ 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);
}
}
@PrePersist
protected void onCreate() {
var createTime = LocalDateTime.now();
this.createdAt = createTime;
this.updatedAt = createTime;
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
@@ -1,7 +1,7 @@
package com.onixbyte.helix.domain.entity;
import com.onixbyte.helix.constant.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_credential")
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.
@@ -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
@@ -73,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);
}
@@ -81,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;
}
@@ -89,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);
}
@@ -98,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() {
@@ -129,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;
}
@@ -145,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
}
@@ -159,7 +139,7 @@ public class UserIdentity {
return "UserIdentity{" +
"userId=" + getUserId() +
", provider=" + getProvider() +
", externalId='" + getExternalId() + '\'' +
", externalId='" + getCredential() + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';
@@ -182,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;
@@ -195,7 +175,7 @@ public class UserIdentity {
return this;
}
public UserIdentityBuilder provider(IdentityProvider provider) {
public UserIdentityBuilder provider(CredentialProvider provider) {
this.provider = provider;
return this;
}
@@ -220,8 +200,20 @@ 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);
}
}
@PrePersist
protected void onCreate() {
var createTime = LocalDateTime.now();
this.createdAt = createTime;
this.updatedAt = createTime;
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
@@ -18,7 +18,7 @@ import java.util.Objects;
* @since 1.0
*/
@Entity
@Table(name = "user_roles")
@Table(name = "user_role")
public class UserRole {
/**
@@ -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();
}
}
@@ -1,9 +1,8 @@
package com.onixbyte.helix.domain.entity.embeddable;
import com.onixbyte.helix.constant.IdentityProvider;
import com.onixbyte.helix.enumeration.CredentialProvider;
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;
@@ -19,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;
@@ -38,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) ---
@@ -66,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);
}
}
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -1,7 +1,7 @@
package com.onixbyte.helix.domain.view;
import com.onixbyte.helix.constant.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;
}
@@ -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;
@@ -1,16 +1,17 @@
package com.onixbyte.helix.domain.web.request;
import com.onixbyte.helix.constant.UserStatus;
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,
@@ -0,0 +1,21 @@
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;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null;
public record AuthorityRequest(
@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 = "{" + MessageName.REQUEST_AUTHORITY_NAME_NOT_NULL + "}")
@NotBlank(message = "{" + MessageName.REQUEST_AUTHORITY_NAME_NOT_NULL + "}")
String name,
String description,
Status status
) {
}
@@ -0,0 +1,38 @@
/*
* 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 com.onixbyte.helix.shared.MessageName;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public record DepartmentRequest(
@NotNull(message = "{" + MessageName.REQUEST_DEPARTMENT_NAME_NOT_NULL + "}")
@NotBlank(message = "{" + MessageName.REQUEST_DEPARTMENT_NAME_NOT_NULL + "}")
String name,
Long parentId,
Integer sort,
Status status
) {
}
@@ -0,0 +1,20 @@
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 = "{" + MessageName.REQUEST_EDIT_ROLE_ID_NOT_NULL + "}")
Long id,
String name,
String code,
Integer sort,
Boolean defaultValue,
String description,
@Pattern(
regexp = "^(ACTIVE|INACTIVE)?$",
message = "{" + MessageName.REQUEST_EDIT_ROLE_STATUS_INVALID + "}")
String status
) {
}
@@ -1,14 +1,13 @@
package com.onixbyte.helix.domain.web.request;
import com.onixbyte.helix.constant.UserStatus;
import com.onixbyte.helix.enumeration.UserStatus;
import com.onixbyte.helix.shared.MessageName;
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")
public record EditUserRequest(
@NotNull(message = "{" + MessageName.REQUEST_EDIT_USER_ID_NOT_NULL + "}")
@Positive(message = "{" + MessageName.REQUEST_EDIT_USER_ID_POSITIVE + "}")
Long id,
String fullName,
String email,
@@ -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
) {
}
@@ -0,0 +1,4 @@
package com.onixbyte.helix.domain.web.request;
public record QueryAuthorityRequest() {
}
@@ -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
) {
}
@@ -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
@@ -1,10 +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(
@NotNull(message = "用户 ID 不能为空") Long id,
@NotBlank(message = "密码不能为空") String password
@NotBlank(message = "{" + MessageName.REQUEST_RESET_PASSWORD_NOT_EMPTY + "}") String password
) {
}
@@ -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
) {
}
@@ -1,9 +0,0 @@
package com.onixbyte.helix.domain.web.request;
public record UsernamePasswordLoginRequest(
String username,
String password,
String uuid,
String captcha
) {
}
@@ -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);
}
}
@@ -6,8 +6,4 @@ public record LoginSuccessResponse(
String accessToken,
User user
) {
public LoginSuccessResponse {
user.setPassword(null);
}
}
@@ -1,6 +1,7 @@
package com.onixbyte.helix.domain.web.response;
import com.onixbyte.helix.constant.UserStatus;
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;
@@ -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,
;
}
@@ -1,4 +1,26 @@
package com.onixbyte.helix.constant;
/*
* 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.
@@ -1,4 +1,4 @@
package com.onixbyte.helix.constant;
package com.onixbyte.helix.enumeration;
public enum SettingType {
@@ -1,4 +1,4 @@
package com.onixbyte.helix.constant;
package com.onixbyte.helix.enumeration;
/**
* Enumeration representing general status states for system entities.
@@ -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.
@@ -14,7 +14,6 @@ package com.onixbyte.helix.constant;
*
* @author zihluwang
* @since 1.0.0
* @see com.onixbyte.helix.config.SecurityConfiguration
*/
public enum UserStatus {
@@ -41,26 +41,19 @@ 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.
*
* @param message the detailed error message explaining the business logic violation
*/
public BizException(String message) {
super(message);
public BizException(String messageCode, Object... messageArgs) {
this.status = HttpStatus.INTERNAL_SERVER_ERROR;
this.messageCode = messageCode;
this.messageArgs = messageArgs == null ? new Object[0] : messageArgs;
}
/**
* 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);
public BizException(HttpStatus status, String messageCode, Object... messageArgs) {
this.status = status;
this.messageCode = messageCode;
this.messageArgs = messageArgs == null ? new Object[0] : messageArgs;
}
/**
@@ -71,4 +64,12 @@ public class BizException extends RuntimeException {
public HttpStatus getStatus() {
return status;
}
public String getMessageCode() {
return messageCode;
}
public Object[] getMessageArgs() {
return messageArgs;
}
}
@@ -1,13 +1,14 @@
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;
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,23 +18,29 @@ 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 {
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
@@ -42,24 +49,16 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
var token = request.getHeader("Authorization");
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("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 decodedToken = tokenClient.verifyToken(token);
var username = decodedToken.getSubject();
var user = userManager.selectByUsername(username);
@@ -68,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);
@@ -1,18 +1,44 @@
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;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class ApplicationManager {
private final ApplicationProperties applicationProperties;
private final AuthenticationProperties authenticationProperties;
public ApplicationManager(ApplicationProperties applicationProperties) {
@Autowired
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);
}
public ApplicationMode getApplicationMode() {
return applicationProperties.mode();
}
}
@@ -1,24 +1,21 @@
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.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
public class AssetManager {
private final AssetMapper assetMapper;
private final AssetRepository assetRepository;
public AssetManager(AssetMapper assetMapper, AssetRepository assetRepository) {
this.assetMapper = assetMapper;
@Autowired
public AssetManager(AssetRepository assetRepository) {
this.assetRepository = assetRepository;
}
@@ -1,24 +1,92 @@
package com.onixbyte.helix.manager;
import com.onixbyte.helix.constant.CacheName;
import com.onixbyte.helix.domain.database.query.wrapper.QueryAuthorityWrapper;
import com.onixbyte.helix.domain.entity.Authority;
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;
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;
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.List;
import java.util.Optional;
@Component
public class AuthorityManager {
private final AuthorityMapper authorityMapper;
private final AuthorityRepository authorityRepository;
public AuthorityManager(AuthorityMapper authorityMapper) {
@Autowired
public AuthorityManager(AuthorityMapper authorityMapper, AuthorityRepository authorityRepository) {
this.authorityMapper = authorityMapper;
this.authorityRepository = authorityRepository;
}
@Cacheable(cacheNames = CacheName.AUTHORITIES_OF_USER, key = "#userId")
public List<Authority> queryByUserId(Long userId) {
return authorityMapper.selectByUserId(userId);
}
public Page<Authority> 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);
}
/**
* Fully updates an existing authority by ID.
* <p>
* 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 fullUpdateById(Long id, Authority authority) {
var updatedAt = LocalDateTime.now();
var authorityToUpdate = authorityRepository.findById(id)
.orElseThrow(() -> new BizException(HttpStatus.NOT_FOUND, MessageName.AUTHORITY_NOT_FOUND, id));
authorityToUpdate.setName(authority.getName());
authorityToUpdate.setDescription(authority.getDescription());
authorityToUpdate.setStatus(authority.getStatus());
authorityToUpdate.setUpdatedAt(updatedAt);
return authority;
}
public String findAuthorityNameById(Long authorityId) {
return authorityRepository.findAuthorityNameById(authorityId);
}
public void deleteById(Long authorityId) {
authorityRepository.deleteById(authorityId);
}
}
@@ -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 {
@@ -1,26 +1,77 @@
package com.onixbyte.helix.manager;
import com.onixbyte.helix.domain.entity.Department;
import com.onixbyte.helix.mapper.DepartmentMapper;
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;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.time.LocalDateTime;
import java.util.Optional;
@Component
public class DepartmentManager {
private final DepartmentMapper departmentMapper;
private final DepartmentRepository departmentRepository;
public DepartmentManager(DepartmentMapper departmentMapper, DepartmentRepository departmentRepository) {
this.departmentMapper = departmentMapper;
@Autowired
public DepartmentManager(DepartmentRepository departmentRepository) {
this.departmentRepository = departmentRepository;
}
public Page<Department> selectAll(Pageable pageable) {
return departmentRepository.findAll(pageable);
}
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);
}
/**
* Fully updates an existing department by ID.
* <p>
* 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;
}
public boolean existsByName(String name) {
return departmentRepository.existsByName(name);
}
}
@@ -2,17 +2,17 @@ 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;
import java.util.List;
@Component
public class MenuManager {
private static final Logger log = LoggerFactory.getLogger(MenuManager.class);
private final MenuMapper menuMapper;
@Autowired
public MenuManager(MenuMapper menuMapper) {
this.menuMapper = menuMapper;
}
@@ -1,25 +1,27 @@
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;
@Component
public class PositionManager {
private final PositionMapper positionMapper;
private final PositionRepository positionRepository;
public PositionManager(PositionMapper positionMapper, PositionRepository positionRepository) {
this.positionMapper = positionMapper;
@Autowired
public PositionManager(PositionRepository positionRepository) {
this.positionRepository = positionRepository;
}
public Page<Position> selectAll(Pageable pageable) {
return positionRepository.findAll(pageable);
}
public Position selectById(Long id) {
return positionRepository.findById(id).orElse(null);
}
}
@@ -0,0 +1,28 @@
package com.onixbyte.helix.manager;
import com.onixbyte.helix.mapper.RoleAuthorityMapper;
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 RoleAuthorityMapper roleAuthorityMapper;
public RoleAuthorityManager(RoleAuthorityMapper roleAuthorityMapper) {
this.roleAuthorityMapper = roleAuthorityMapper;
}
public void deleteByRoleId(Long roleId) {
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);
}
}
@@ -5,13 +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 org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
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;
@@ -20,20 +22,23 @@ public class RoleManager {
private final RoleMapper roleMapper;
private final RoleRepository roleRepository;
private final MessageUtil messageUtil;
public RoleManager(RoleMapper roleMapper, RoleRepository roleRepository) {
@Autowired
public RoleManager(RoleMapper roleMapper, RoleRepository roleRepository, MessageUtil messageUtil) {
this.roleMapper = roleMapper;
this.roleRepository = roleRepository;
this.messageUtil = messageUtil;
}
public void validateRoles(List<Long> 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);
}
}
public Optional<Role> getRole(Role example) {
return roleRepository.findOne(Example.of(example));
public List<Role> getRoles(Role example) {
return roleRepository.findAll(Example.of(example), Sort.by(Sort.Order.asc("id")));
}
public Page<Role> selectAll(Pageable pageable, QueryRoleWrapper wrapper) {
@@ -42,4 +47,37 @@ public class RoleManager {
return new PageImpl<>(records, pageable, total);
}
public Role save(Role role) {
return roleRepository.save(role);
}
public Optional<Role> getRoleById(Long id) {
return roleRepository.findById(id);
}
@Transactional
public Role fullUpdateById(Long id, Role role) {
var updatedAt = LocalDateTime.now();
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);
return role;
}
public void deleteRole(Long id) {
roleRepository.deleteById(id);
}
}
@@ -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();
}
}
@@ -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;
}
@@ -1,7 +1,31 @@
/*
* 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;
import com.onixbyte.helix.constant.CacheName;
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;
import com.onixbyte.helix.domain.web.response.UserDetailResponse;
@@ -9,7 +33,9 @@ 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;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Example;
@@ -29,11 +55,19 @@ public class UserManager {
private final UserMapper userMapper;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserCredentialMapper userCredentialMapper;
public UserManager(UserMapper userMapper, UserRepository userRepository, PasswordEncoder passwordEncoder) {
@Autowired
public UserManager(
UserMapper userMapper,
UserRepository userRepository,
PasswordEncoder passwordEncoder,
UserCredentialMapper userCredentialMapper
) {
this.userMapper = userMapper;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.userCredentialMapper = userCredentialMapper;
}
/**
@@ -79,12 +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() + "的用户信息"));
Optional.ofNullable(user.getPassword())
.filter(StringUtils::isNotBlank)
.map(passwordEncoder::encode)
.ifPresent(userToUpdate::setPassword);
.orElseThrow(() -> new BizException(HttpStatus.BAD_REQUEST, MessageName.USER_NOT_FOUND, user.getId()));
Optional.ofNullable(user.getFullName())
.filter(StringUtils::isNotBlank)
@@ -120,4 +149,12 @@ public class UserManager {
return userToUpdate;
}
@Transactional(rollbackFor = Throwable.class)
public void updatePasswordById(Long id, ResetPasswordRequest request) {
userCredentialMapper.updateUserCredential(
id,
passwordEncoder.encode(request.password())
);
}
}
@@ -1,12 +1,10 @@
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;
@@ -15,11 +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;
public UserRoleManager(UserRoleMapper userRoleMapper, UserRoleRepository userRoleRepository) {
this.userRoleMapper = userRoleMapper;
@Autowired
public UserRoleManager(UserRoleRepository userRoleRepository) {
this.userRoleRepository = userRoleRepository;
}
@@ -31,4 +28,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);
}
}
@@ -1,60 +0,0 @@
/**
* Business logic management and orchestration package for the Helix application.
* <p>
* 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.
* <p>
* <strong>Manager Pattern:</strong> 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.
* <p>
* <strong>Intended Contents:</strong>
* <ul>
* <li>
* <strong>Workflow Managers:</strong> Orchestrate multi-step business processes
* </li>
* <li>
* <strong>Integration Managers:</strong> Coordinate interactions with external systems
* </li>
* <li>
* <strong>Transaction Managers:</strong> Handle complex transactional scenarios
* </li>
* <li>
* <strong>Cache Managers:</strong> Manage caching strategies and cache invalidation
* </li>
* <li>
* <strong>Event Managers:</strong> Coordinate event publishing and handling
* </li>
* </ul>
* <p>
* <strong>Design Guidelines:</strong>
* <ul>
* <li>
* <strong>Single Responsibility:</strong> Each manager should focus on a specific business domain
* or cross-cutting concern
* </li>
* <li>
* <strong>Service Coordination:</strong> Managers should delegate to services rather than
* implementing business logic directly
* </li>
* <li>
* <strong>Transaction Boundaries:</strong> Define clear transactional boundaries for
* complex operations
* </li>
* <li>
* <strong>Error Handling:</strong> Provide comprehensive error handling and rollback mechanisms
* for complex workflows
* </li>
* </ul>
* <p>
* 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;
@@ -0,0 +1,12 @@
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);
int deleteByAuthorityId(@Param("authorityId") Long authorityId);
}
@@ -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
);
}
@@ -1,10 +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("default@helix.onixbyte.dev") String defaultEmail,
@DefaultValue("helix.onixbyte.dev") String externalHost,
@DefaultValue("prod") ApplicationMode mode
) {
}
@@ -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
) {
}
@@ -2,8 +2,16 @@ 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.stereotype.Repository;
@Repository
public interface AuthorityRepository extends JpaRepository<Authority, Long> {
@Query("""
select a.name
from Authority a
where a.id = :authorityId
""")
String findAuthorityNameById(Long authorityId);
}
@@ -2,8 +2,19 @@ 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<Department, Long> {
@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);
boolean existsByName(String name);
}
@@ -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<RoleAuthority, RoleAuthorityId> {
@Modifying
@Query("DELETE FROM RoleAuthority ra WHERE ra.id.roleId = :roleId")
int deleteByRoleId(Long roleId);
}
@@ -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<UserCredential, UserCredentialId> {
}
@@ -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<UserIdentity, UserIdentityId> {
}
@@ -13,4 +13,8 @@ public interface UserRoleRepository extends JpaRepository<UserRole, UserRoleId>
@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);
}
@@ -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;
@@ -1,12 +1,18 @@
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 com.onixbyte.helix.shared.MessageName;
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;
@@ -23,34 +29,48 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro
private final UserManager userManager;
private final PasswordEncoder passwordEncoder;
private final AuthorityManager authorityManager;
private final UserCredentialRepository userCredentialRepository;
public UsernamePasswordAuthenticationProvider(UserManager userManager, PasswordEncoder passwordEncoder, AuthorityManager authorityManager) {
@Autowired
public UsernamePasswordAuthenticationProvider(
UserManager userManager,
PasswordEncoder passwordEncoder,
AuthorityManager authorityManager,
UserCredentialRepository userCredentialRepository
) {
this.userManager = userManager;
this.passwordEncoder = passwordEncoder;
this.authorityManager = authorityManager;
this.userCredentialRepository = userCredentialRepository;
}
@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
var userCredentials = userCredentialRepository.findOne(Example.of(UserCredential.builder()
.provider(CredentialProvider.LOCAL)
.userId(user.getId())
.build()))
.orElseThrow(() -> new BizException(HttpStatus.UNAUTHORIZED, MessageName.AUTH_PROVIDER_PASSWORD_NOT_CONFIGURED));
// 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, "用户名或密码错误。");
throw new BizException(HttpStatus.UNAUTHORIZED, MessageName.AUTH_PROVIDER_BAD_CREDENTIALS);
}
// erase credentials
user.setPassword(null);
usernamePasswordAuthentication.eraseCredentials();
// get authorities
@@ -4,7 +4,9 @@ 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;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -34,6 +36,7 @@ public class AssetService {
private final S3Client s3Client;
private final AssetManager assetManager;
@Autowired
public AssetService(
AssetProperties assetProperties,
S3Client s3Client,
@@ -60,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
);
}
@@ -106,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);
@@ -1,21 +1,25 @@
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.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;
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;
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;
import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
@@ -26,20 +30,22 @@ public class AuthService {
private final CaptchaManager captchaManager;
private final AuthenticationManager authenticationManager;
private final TokenClient tokenClient;
private final SettingManager settingManager;
private final SecurityManager securityManager;
private final ApplicationManager applicationManager;
@Autowired
public AuthService(
CaptchaManager captchaManager,
AuthenticationManager authenticationManager,
TokenClient tokenClient,
SettingManager settingManager
) {
SettingManager settingManager,
SecurityManager securityManager,
ApplicationManager applicationManager) {
this.captchaManager = captchaManager;
this.authenticationManager = authenticationManager;
this.tokenClient = tokenClient;
this.settingManager = settingManager;
this.securityManager = securityManager;
this.applicationManager = applicationManager;
}
/**
@@ -49,7 +55,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 User login(LoginRequest request) {
var captchaEnabled = Optional.ofNullable(settingManager.getSettingByName(SettingName.CAPTCHA_ENABLED))
.map(Setting::asBoolean)
.orElse(false);
@@ -58,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);
}
}
@@ -73,12 +79,10 @@ public class AuthService {
_authentication.getClass()
);
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR,
"Cannot perform login due to server crashes.");
MessageName.AUTH_LOGIN_FAILED);
}
var token = tokenClient.generateToken(authentication.getDetails());
return new LoginSuccessResponse(token, authentication.getDetails());
return authentication.getDetails();
}
/**
@@ -91,4 +95,45 @@ public class AuthService {
.map(Setting::asBoolean)
.orElse(false);
}
public ResponseCookie buildCookie(String cookieName, String value) {
return buildCookieInternal(cookieName, value, securityManager.getTokenValidDuration())
.build();
}
public ResponseCookie buildCookie(String cookieName, String value, Duration validDuration) {
return buildCookieInternal(cookieName, value, validDuration)
.build();
}
/**
* 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(name, value)
.maxAge(securityManager.getTokenValidDuration())
.secure(true)
.maxAge(validDuration)
.path("/");
return switch (applicationMode) {
case PRODUCTION -> cookieBuilder
.httpOnly(true)
.sameSite("LAX");
case DEVELOPMENT -> cookieBuilder
.sameSite("NONE");
case null -> cookieBuilder;
};
}
}

Some files were not shown because too many files have changed in this diff Show More