Compare commits

...
25 Commits
Author SHA1 Message Date
siujamo 06fd31576a docs: reframe Helix as a quick-start boilerplate for customisation 2026-09-01 11:41:16 +08:00
siujamo 965b92b47d chore: restructure into monorepo and add open-source documentation
- Rename helix-server -> server and helix-web -> web
- Remove helix-api (bruno collection and docs site)
- Merge per-module .gitignore files into the root .gitignore
- Add README, AGENTS, CONTRIBUTING and MIT LICENCE
- Add full-stack docker-compose (postgres, redis, server, web via Caddy)
  with web Dockerfile, Caddyfile and Compose server config
- Keep the Gradle wrapper jar tracked after the .gitignore merge
- Copy gradle.properties into the server Docker build image
2026-09-01 10:48:45 +08:00
siujamo 112d0f3be4 chore: add helix-api and workspace config to monorepo 2026-09-01 10:11:07 +08:00
siujamo c732624580 merge: import helix-web history into helix-web/
git-subtree-dir: helix-web
git-subtree-mainline: 21f14f8c05
git-subtree-split: 83f3120fdc
2026-09-01 10:05:58 +08:00
siujamo 21f14f8c05 merge: import helix-server history into helix-server/
git-subtree-dir: helix-server
git-subtree-mainline: 1c675c9f53
git-subtree-split: dedf95865e
2026-09-01 10:05:53 +08:00
siujamo 1c675c9f53 chore: initialise helix monorepo 2026-09-01 09:57:05 +08:00
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 83f3120fdc fix: add rel attribute to external link for security 2026-03-12 13:56:23 +08:00
zihluwang 9b642c85d0 chore(deps): update dependencies 2026-01-28 22:02:25 +08:00
siujamo d5074a92a2 feat: 支持多因素认证登录,更新相关类型定义 2026-01-28 17:05:38 +08:00
siujamo d5109e1333 refactor: 移除 Token 配置 2026-01-06 09:21:43 +08:00
siujamo 0d3b0bd28d refactor: 调整 axios 配置 2026-01-05 10:10:51 +08:00
siujamo a00488be5d refactor: 优化报错信息 2026-01-05 09:59:53 +08:00
siujamo 29ce5e45f1 refactor: 使用 useApp hook 获取 modal 上下文 2025-12-31 11:15:08 +08:00
siujamo d3723ba485 feat: 添加左上角应用名称自定义功能 2025-12-29 14:33:08 +08:00
siujamo 944d176240 feat: 完成角色删除功能对接 2025-12-29 14:20:38 +08:00
siujamo 1ae17823ef refactor: 优化错误处理逻辑 2025-12-29 11:49:44 +08:00
siujamo b0ccd8f832 feat: 完成角色编辑功能 2025-12-26 17:53:09 +08:00
siujamo 3b0bd56001 fix: 修复编辑表单打开后展示数据错误的问题 2025-12-26 17:49:56 +08:00
siujamo 8aaad677b6 feat: 完成创建角色功能 2025-12-26 16:00:40 +08:00
siujamo 9139d0c451 docs: 添加默认地区配置 2025-12-26 14:28:16 +08:00
siujamo faff32475f feat: 初始提交 2025-12-25 16:12:01 +08:00
281 changed files with 7555 additions and 261 deletions

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
# Docker Compose configuration for Helix.
# Copy to `.env` and adjust: cp .env.example .env
# PostgreSQL
PG_DATABASE=helix
PG_USER=helix
PG_PASSWORD=helix
# Redis (keep empty for a local no-password Redis)
REDIS_PASSWORD=
# JWT signing secret — change this in any non-local deployment
TOKEN_SECRET=please-change-this-secret
# Microsoft Entra ID single sign-on (optional for local runs)
MSAL_CLIENT_ID=
MSAL_TENANT_ID=
# S3-compatible object storage (optional — requires enabling `app.asset.enabled`
# in docker/application-prod.yml and providing the credentials below)
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
# Host ports
SERVER_PORT=8080
WEB_PORT=80
+25 -7
View File
@@ -124,9 +124,9 @@ hs_err_pid*
replay_pid*
# Config
config/*.yml
config/*.yaml
config/*.properties
**/config/*.yml
**/config/*.yaml
**/config/*.properties
# Tests
test/
@@ -136,10 +136,11 @@ test/
**/build/
!**/src/**/build/
!gradle/
!gradle/wrapper
!gradle/wrapper/gradle-wrapper.jar
!gradle/wrapper/gradle-wrapper.properties
# Keep the Gradle wrapper (checked in per Gradle convention) tracked at any depth
!**/gradle/
!**/gradle/wrapper
!**/gradle/wrapper/gradle-wrapper.jar
!**/gradle/wrapper/gradle-wrapper.properties
gradle-app.setting
.gradletasknamecache
@@ -148,3 +149,20 @@ gradle-app.setting
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### Frontend
# Node libraries
node_modules/
# Config
.env.local
.env.*.local
# Claude Code local tooling
.claude/
### Docker Compose
# Root .env holds Compose secrets; use .env.example as a template
/.env
+4
View File
@@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "disabled",
"java.configuration.updateBuildConfiguration": "automatic"
}
+103
View File
@@ -0,0 +1,103 @@
# AGENTS.md
Guidance for AI coding agents and contributors working in this repository.
It complements [README.md](README.md); read that first for the project overview.
> **Scope — this is a quick-start boilerplate.** Helix is a starting point for
> custom development, not a finished product. The baseline modules
> (authentication, RBAC, user/role/department/position/menu management) are
> intentionally kept minimal and exist to be extended. When adding features,
> build on the existing patterns below; do not introduce parallel
> conventions.
## Repository overview
Helix is a monorepo containing a backend API and a frontend SPA:
- **`server/`** — Spring Boot 3.5 REST API. Java 21, Gradle, Spring Security,
Spring Data JPA + MyBatis, Flyway, PostgreSQL, Redis, Auth0 java-jwt,
AWS SDK (S3). Package root: `com.onixbyte.helix`.
- **`web/`** — React 19 SPA. TypeScript, Vite 7, Ant Design 6, Tailwind CSS 4,
Redux Toolkit, React Router 7, Axios, MSAL.
## Build and test
Server (requires JDK 21):
```bash
cd server
./gradlew build # compiles and runs the JUnit 5 test suite
./gradlew bootRun # starts the API on port 8080
```
Web (requires Node 22+ and pnpm):
```bash
cd web
pnpm install
pnpm dev # Vite dev server
pnpm build # tsc -b && vite build
pnpm lint # ESLint
```
## Server conventions
- **Layering.** Follow the strict call chain
`Controller → Service → Manager → Repository` (MyBatis Mappers sit alongside
JPA repositories for query-heavy reads). Controllers stay thin: parse and
validate input, delegate to a service, return a response DTO. Business rules
live in services; transactional use-case orchestration lives in managers.
- **Validation.** Use the custom validation framework in
`com.onixbyte.helix.validation` (`annotation` / `validator` / `executor` /
`group`) instead of ad-hoc checks where possible.
- **Errors and i18n.** Throw `BizException(HttpStatus, MessageName.X, args...)`.
Message keys are constants declared in `com.onixbyte.helix.shared.MessageName`
and resolved from the bundles in `src/main/resources/i18n`
(`messages.properties`, `messages_en_GB.properties`, `messages_zh_CN.properties`).
Always add a new key to **all** bundles, in the default language and in Chinese.
- **Authentication.** Spring Security with a JWT stored in a cookie.
`TokenAuthenticationFilter` (under `com.onixbyte.helix.filter`) validates the
token; the `X-Authorisation` header carries the exchanged token. Sign-in
providers (Entra ID/MSAL, WeCom) live under `service/auth`. MFA is supported.
- **Persistence.** The schema is owned by Flyway — add migrations under
`src/main/resources/db/migrations/`; `ddl-auto` is disabled. MyBatis XML
mappers live in `src/main/resources/mapper/`. JPA entities go in
`com.onixbyte.helix.domain.entity`.
- **Shared utilities.** Prefer the Onixbyte libraries already on the classpath
(`identity-generator`, `tuple`, `common-toolbox`, `math-toolbox`) over
reimplementing them.
- **Style.** UTF-8 source, Javadoc on public APIs, constructor injection with
`final` fields, builder/record patterns for DTOs. Match the surrounding code.
- **Configuration.** Runtime config is externalised via environment variables
(reference: `config/application-prod.yml.example`). Never hardcode secrets.
Local `config/*.yml` files are gitignored.
## Web conventions
- **State.** Redux Toolkit slices in `src/store` (e.g. `auth-slice`);
consume via `react-redux` hooks.
- **API calls.** Typed per-domain clients in `src/api` (`auth`, `user`, `role`,
`department`, `position`, `menu`). They use the shared Axios instance in
`src/client/web-client/index.ts`, whose `baseURL` comes from
`VITE_API_BASE_URL` and which sends credentials (`withCredentials`).
- **Types.** Request/response types live in `src/types`.
- **Routing.** `react-router` 7 routes are declared in `src/router/index.tsx`.
- **UI.** Ant Design 6 components styled with Tailwind utility classes.
- **Environment.** `VITE_*` variables (see `.env.example`). The Vite dev server
has no proxy — for local development point `VITE_API_BASE_URL` at the backend
directly (e.g. `http://localhost:8080`).
## General guidelines
- **Custom development.** When extending the boilerplate, use the existing
extension points rather than parallel patterns: add Flyway migrations for
schema changes, register message keys in every i18n bundle, implement features
through the standard `Controller → Service → Manager → Repository` chain, and
expose typed API clients in `web/src/api`. Customise the baseline — do not
re-architect it.
- **Commits.** Use Conventional Commits (`feat:`, `fix:`, `refactor:`, `chore:`,
`docs:`, `perf:`, `style:`, `test:`). Match the language of the surrounding
history (the log is a mix of English and Chinese).
- **Secrets.** Never commit `.env` files, local config, or credentials.
`web/.dockerignore` excludes local env files from the build context — keep it
that way.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+83
View File
@@ -0,0 +1,83 @@
# Contributing to Helix
Thanks for your interest in contributing! Helix is a quick-start boilerplate for
custom development, open source under the [MIT licence](LICENCE). This guide
explains how to set up your environment, make changes and get them merged.
## What to contribute
Contributions should make the **boilerplate itself** more useful to everyone who
forks it:
- New or improved baseline capabilities (reusable, not business-specific)
- Bug fixes and hardening
- Documentation and tests
Changes that encode one particular organisation's custom rules are usually
better kept in your own fork. If you are not sure whether something belongs in
the boilerplate, open an issue first and ask.
## Getting started
1. Fork the repository and clone your fork.
2. Install the prerequisites:
- Backend: JDK 21 and the Gradle wrapper (bundled in `server/`)
- Frontend: Node.js 22+ and [pnpm](https://pnpm.io/)
- Optional: Docker + Docker Compose for the full-stack run
3. Follow the [Quick start](README.md#quick-start) in the README to run the app
locally.
## Development workflow
- Create a feature branch from `main`, using a short prefix for clarity:
`feat/`, `fix/`, `refactor/`, `chore/` or `docs/` (e.g. `feat/role-audit-log`).
- Keep pull requests small and focused on a single concern.
- Write or update tests for the code you touch.
- Open a pull request against `main` and describe what changed and why.
## Code style
The full conventions are documented in [AGENTS.md](AGENTS.md). The essentials:
**Backend (`server/`)**
- Follow the `Controller → Service → Manager → Repository` layering.
- Use the custom validation framework for input validation.
- Throw `BizException` with a `MessageName` constant and add the message key to
every bundle under `src/main/resources/i18n`.
- Add Flyway migrations for schema changes under `src/main/resources/db/migrations/`.
- Run `./gradlew build` before opening a PR.
**Frontend (`web/`)**
- Use ESLint and Prettier formatting; run `pnpm lint`.
- Keep API clients in `src/api` and state in Redux Toolkit slices.
- Never commit local `.env` files.
## Commit messages
Use [Conventional Commits](https://www.conventionalcommits.org/):
```
feat: support TOTP multi-factor authentication
fix: resolve department name uniqueness check
refactor: extract build cookie
docs: update deployment guide
chore(deps): upgrade spring boot to 3.5.4
```
## Reporting issues
When opening an issue, include:
- What you expected to happen and what actually happened
- Steps to reproduce
- Environment details (OS, Java/Node versions, deployment mode)
Security issues: please report them privately to the maintainers rather than
opening a public issue.
## License
By contributing, you agree that your contributions are licensed under the
[MIT licence](LICENCE).
+21
View File
@@ -0,0 +1,21 @@
MIT License
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.
+176
View File
@@ -0,0 +1,176 @@
# Helix
Helix is a **quick-start boilerplate** for building custom identity and organisation
management systems. It ships with a working full-stack baseline — authentication,
role-based access control, and user/role/department/position/menu management — so
you can start from a runnable application and customise it to your own
requirements instead of starting from zero.
The boilerplate is organised as a monorepo:
- **`server/`** — Spring Boot 3 REST API (Java 21, PostgreSQL, Redis, Flyway)
- **`web/`** — React 19 single-page application (Vite, Ant Design, Tailwind CSS, Redux Toolkit)
- **`docker/`** — deployment support files used by Docker Compose
> Helix is a starting point, not a finished product. Every baseline module is
> meant to be extended or replaced as part of your custom development.
## Baseline capabilities
Out of the box the boilerplate provides:
- **Authentication & single sign-on** — username/password login with captcha,
Microsoft Entra ID (MSAL) sign-in, WeCom (WeChat Work) login and MFA
- **User management** — create, edit and deactivate users; assign roles, departments and positions
- **Role-based access control** — roles with fine-grained authorities, enforced across all endpoints
- **Organisation management** — departments, positions and custom navigation menus
- **Asset storage** — optional S3-compatible object storage for uploaded files
- **Internationalisation** — English (`en-GB`) and Chinese (`zh-CN`) message bundles
- **Responsive admin UI** — Ant Design + Tailwind CSS front end
## Tech stack
| Layer | Technologies |
| ----------- | ------------ |
| Backend | Java 21 · Spring Boot 3.5 · Spring Security · Spring Data JPA · MyBatis · Flyway · PostgreSQL · Redis · Auth0 java-jwt · AWS SDK for Java (S3) |
| Frontend | React 19 · TypeScript · Vite 7 · Ant Design 6 · Tailwind CSS 4 · Redux Toolkit · React Router 7 · Axios · MSAL |
| Infra | Docker · Docker Compose · Caddy |
## Repository layout
```
.
├── server/ # Spring Boot REST API
│ ├── src/main/java/com/onixbyte/helix/
│ │ ├── controller/ # HTTP entry points
│ │ ├── service/ # business logic
│ │ ├── manager/ # use-case orchestration
│ │ ├── repository/ # JPA repositories
│ │ ├── mapper/ # MyBatis mappers
│ │ ├── validation/ # custom validation framework
│ │ └── security/ # JWT filter, security configuration
│ ├── src/main/resources/
│ │ ├── db/migrations/ # Flyway SQL migrations
│ │ ├── i18n/ # message bundles (en-GB, zh-CN)
│ │ └── mapper/ # MyBatis XML mappers
│ ├── config/ # runtime config (see application-prod.yml.example)
│ └── Dockerfile
├── web/ # React SPA
│ ├── src/api/ # typed API clients (auth, user, role, …)
│ ├── src/page/ # page components
│ ├── src/store/ # Redux slices
│ ├── src/router/ # react-router routes
│ ├── Dockerfile # builds the SPA and serves it with Caddy
│ └── Caddyfile # static files + /api reverse proxy
├── docker/ # Compose support files
│ └── application-prod.yml # server config mounted into the container
├── docker-compose.yaml # full-stack: postgres + redis + server + web
└── .env.example # Compose environment template
```
## Customising the boilerplate
Helix is designed to be forked and extended. The recommended approach:
1. **Fork (or copy) the repository** and rename the project to your own.
2. **Change the branding** — application title (`VITE_APP_TITLE`), regions
(`VITE_DEFAULT_REGION_ABBREVIATION`), JWT issuer, S3 buckets and so on.
3. **Build on the baseline modules.** The existing patterns — custom validation
framework, `BizException` + `MessageName` error handling, JWT authentication,
Redux slices and typed API clients — are the extension points for your own
features. Follow the conventions in [AGENTS.md](AGENTS.md).
4. **Add schema changes as Flyway migrations** under
`server/src/main/resources/db/migrations/` and add new message keys to
**all** i18n bundles.
## Quick start
### Full stack with Docker Compose
Requires [Docker](https://www.docker.com/) with the Compose plugin.
```bash
cp .env.example .env # optional: adjust ports, secrets and integrations
docker compose up -d --build
```
Once the containers are healthy:
| Service | URL |
| ------------------ | ---------------------------------- |
| Web UI | <http://localhost> |
| REST API | <http://localhost:8080> |
| Health check | <http://localhost:8080/actuator/health> |
Login works out of the box with username/password + captcha. Microsoft Entra ID
and WeCom sign-in require configuring their credentials (see below).
### Manual development
#### Server (JDK 21)
```bash
cd server
cp config/application-prod.yml.example config/application-prod.yml # edit to match your environment
SPRING_PROFILES_ACTIVE=prod ./gradlew bootRun
```
The API starts on <http://localhost:8080>.
#### Web (Node 22+ with pnpm)
```bash
cd web
pnpm install
cp .env.example .env.development.local # point the API base URL at your dev server
pnpm dev
```
Set `VITE_API_BASE_URL=http://localhost:8080` in `.env.development.local` to call
the locally running API directly. The Vite dev server listens on
<http://localhost:5173>.
## Configuration
Runtime configuration is externalised through environment variables (see
`server/config/application-prod.yml.example` for the complete reference).
### Docker Compose (`.env`)
| Variable | Default | Description |
| -------------------------- | ------------------------- | ---------------------------------------------- |
| `PG_DATABASE` | `helix` | PostgreSQL database name |
| `PG_USER` | `helix` | PostgreSQL user |
| `PG_PASSWORD` | `helix` | PostgreSQL password |
| `REDIS_PASSWORD` | *(empty)* | Redis password (leave empty for local Redis) |
| `TOKEN_SECRET` | *(see example)* | JWT signing secret — change in production |
| `MSAL_CLIENT_ID` | *(empty)* | Microsoft Entra ID application (client) ID |
| `MSAL_TENANT_ID` | *(empty)* | Microsoft Entra ID tenant ID |
| `S3_ACCESS_KEY_ID` | *(empty)* | S3 access key (requires enabling asset storage)|
| `S3_SECRET_ACCESS_KEY` | *(empty)* | S3 secret key |
| `SERVER_PORT` | `8080` | Host port mapped to the server container |
| `WEB_PORT` | `80` | Host port mapped to the Caddy container |
Asset storage is disabled in `docker/application-prod.yml` by default. To enable
it, set `app.asset.enabled: true` there and provide the `S3_*` variables.
### Web (`.env`)
| Variable | Default | Description |
| ------------------------------- | ---------------------------------- | ---------------------------------- |
| `VITE_API_BASE_URL` | `/api` | Backend base URL |
| `VITE_MSAL_CLIENT_ID` | *(empty)* | Microsoft Entra ID client ID |
| `VITE_MSAL_TENANT_ID` | *(empty)* | Microsoft Entra ID tenant ID |
| `VITE_DEFAULT_REGION_ABBREVIATION` | `GB` | Default user region (ISO 3166-1 alpha-2) |
| `VITE_APP_TITLE` | `Onixbyte Technology Co., Ltd` | Application title shown in the UI |
## Development
- Architecture and conventions are documented in [AGENTS.md](AGENTS.md).
- See [CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute.
- Database schema is managed with Flyway — add migrations under
`server/src/main/resources/db/migrations/` rather than relying on JPA `ddl-auto`.
## Licence
[MIT](LICENCE) © OnixByte
+81
View File
@@ -0,0 +1,81 @@
name: helix
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${PG_DATABASE:-helix}
POSTGRES_USER: ${PG_USER:-helix}
POSTGRES_PASSWORD: ${PG_PASSWORD:-helix}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${PG_USER:-helix} -d ${PG_DATABASE:-helix}"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
server:
build:
context: ./server
args:
ARTEFACT_VERSION: ${ARTEFACT_VERSION:-0.0.1}
environment:
SPRING_PROFILES_ACTIVE: prod
PG_HOST: postgres
PG_PORT: "5432"
PG_DATABASE: ${PG_DATABASE:-helix}
PG_USER: ${PG_USER:-helix}
PG_PASSWORD: ${PG_PASSWORD:-helix}
REDIS_HOST: redis
REDIS_PORT: "6379"
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
TOKEN_SECRET: ${TOKEN_SECRET:-please-change-this-secret}
MSAL_CLIENT_ID: ${MSAL_CLIENT_ID:-}
MSAL_TENANT_ID: ${MSAL_TENANT_ID:-}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-}
volumes:
# Mounts the Compose-specific server configuration (Spring Boot picks up
# config/application-{profile}.yml next to the working directory).
- ./docker/application-prod.yml:/app/config/application-prod.yml:ro
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "${SERVER_PORT:-8080}:8080"
restart: unless-stopped
web:
build:
context: ./web
args:
VITE_API_BASE_URL: ${VITE_API_BASE_URL:-/api}
VITE_MSAL_CLIENT_ID: ${VITE_MSAL_CLIENT_ID:-}
VITE_MSAL_TENANT_ID: ${VITE_MSAL_TENANT_ID:-}
VITE_APP_TITLE: ${VITE_APP_TITLE:-Helix}
VITE_DEFAULT_REGION_ABBREVIATION: ${VITE_DEFAULT_REGION_ABBREVIATION:-GB}
depends_on:
- server
ports:
- "${WEB_PORT:-80}:80"
restart: unless-stopped
volumes:
postgres-data:
redis-data:
+77
View File
@@ -0,0 +1,77 @@
# Server configuration used by docker-compose.yaml (mounted into the server
# container at /app/config/application-prod.yml).
#
# All values resolve from environment variables set by the Compose services.
# For a standalone deployment, base your own config on
# server/config/application-prod.yml.example instead.
spring:
data:
redis:
# Redis 主机
host: ${REDIS_HOST:-localhost}
# Redis 端口
port: ${REDIS_PORT:-6379}
# Redis 数据库索引
database: ${REDIS_DATABASE_INDEX:-0}
# Redis 数据库密码
password: ${REDIS_PASSWORD:-}
datasource:
# 数据库连接池类型
type: com.zaxxer.hikari.HikariDataSource
# 数据库驱动类
driver-class-name: org.postgresql.Driver
# 数据库 URL
url: jdbc:postgresql://${PG_HOST:-localhost}:${PG_PORT:-5432}/${PG_DATABASE:-helix}
# 数据库用户
username: ${PG_USER:-helix}
# 数据库密码
password: ${PG_PASSWORD:-helix}
app:
asset:
# Disabled by default for a local full-stack run.
# Set `enabled: true` and provide S3_* environment variables to enable
# S3-compatible object storage.
enabled: false
# 鉴权配置
authentication:
# Microsoft Entra ID 鉴权配置
msal:
# Microsoft Entra ID 提供的客户端 ID
client-id: ${MSAL_CLIENT_ID:-}
# Microsoft Entra ID 提供的租户 ID
tenant-id: ${MSAL_TENANT_ID:-}
# 令牌配置
jwt:
# 令牌签发人
issuer: Helix Server
# 令牌机密
secret: ${TOKEN_SECRET:-please-change-this-secret}
# 令牌有效期(Ref java.time.Duration)
valid-time: PT2H
# 跨域配置
cors:
# 是否允许身份验证
allow-credentials: true
# 是否允许私有网络
allow-private-network: true
# 允许的请求头列表
allowed-headers: Content-Type
# 允许的请求方法列表(Ref org.springframework.http.HttpMethod)
allowed-methods:
- GET
- POST
- PUT
- PATCH
- DELETE
# 允许的来源域名
allowed-origins: '*'
# 要对外暴露的响应头列表
exposed-headers: X-Authorisation
# 跨域缓存时长
max-age: PT2H
# Captcha 配置
captcha:
# Captcha 长度
length: 6
File renamed without changes.
+1
View File
@@ -5,6 +5,7 @@ WORKDIR /home/app
COPY gradlew .
COPY gradle gradle
COPY gradle.properties .
COPY build.gradle.kts .
COPY settings.gradle.kts .
COPY gradle/libs.versions.toml gradle/libs.versions.toml
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
View File
File renamed without changes.
View File
File renamed without changes.
File renamed without changes.
@@ -4,8 +4,7 @@ import com.onixbyte.helix.shared.AssetPrefix;
import com.onixbyte.helix.domain.web.response.FileUploadResponse;
import com.onixbyte.helix.exception.BizException;
import com.onixbyte.helix.service.AssetService;
import com.onixbyte.helix.shared.Message;
import com.onixbyte.helix.utils.MessageUtil;
import com.onixbyte.helix.shared.MessageName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -27,15 +26,10 @@ public class AssetController {
private static final Logger log = LoggerFactory.getLogger(AssetController.class);
private final AssetService assetService;
private final MessageUtil messageUtil;
@Autowired
public AssetController(
AssetService assetService,
MessageUtil messageUtil
) {
public AssetController(AssetService assetService) {
this.assetService = assetService;
this.messageUtil = messageUtil;
}
/**
@@ -50,7 +44,7 @@ public class AssetController {
) {
try {
if (file.isEmpty()) {
throw new BizException(HttpStatus.BAD_REQUEST, messageUtil.getMessage(Message.ASSET_NOT_EMPTY));
throw new BizException(HttpStatus.BAD_REQUEST, MessageName.ASSET_NOT_EMPTY);
}
var fileUrl = assetService.uploadFile(AssetPrefix.UPLOADS, file);
@@ -63,10 +57,13 @@ public class AssetController {
file.getSize(),
fileUrl
));
} catch (BizException ex) {
throw ex;
} catch (Exception e) {
log.error("File upload failed: {}", e.getMessage(), e);
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR,
"Failed upload file: " + e.getMessage());
MessageName.ASSET_UPLOAD_FAILED,
e.getMessage());
}
}
@@ -5,7 +5,7 @@ import com.onixbyte.helix.domain.web.request.AuthorityRequest;
import com.onixbyte.helix.domain.web.request.QueryAuthorityRequest;
import com.onixbyte.helix.domain.web.response.ActionResponse;
import com.onixbyte.helix.service.AuthorityService;
import com.onixbyte.helix.shared.Message;
import com.onixbyte.helix.shared.MessageName;
import com.onixbyte.helix.utils.MessageUtil;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -79,6 +79,6 @@ public class AuthorityController {
@DeleteMapping("/{authorityId:\\d+}")
public ActionResponse deleteAuthority(@PathVariable Long authorityId) {
var name = authorityService.deleteAuthority(authorityId);
return ActionResponse.success(messageUtil.getMessage(Message.AUTHORITY_DELETED, name));
return ActionResponse.success(messageUtil.getMessage(MessageName.AUTHORITY_DELETED, name));
}
}
@@ -2,6 +2,7 @@ package com.onixbyte.helix.controller;
import com.onixbyte.helix.domain.web.response.BizExceptionResponse;
import com.onixbyte.helix.exception.BizException;
import com.onixbyte.helix.utils.MessageUtil;
import jakarta.validation.ConstraintViolationException;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
@@ -27,12 +28,22 @@ import java.util.stream.Collectors;
@RestControllerAdvice
public class ExceptionController {
private final MessageUtil messageUtil;
public ExceptionController(MessageUtil messageUtil) {
this.messageUtil = messageUtil;
}
@ExceptionHandler(BizException.class)
public ResponseEntity<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)
);
}
@@ -1,15 +1,16 @@
package com.onixbyte.helix.controller;
import com.onixbyte.helix.domain.entity.Role;
import com.onixbyte.helix.domain.web.request.AddRoleRequest;
import com.onixbyte.helix.domain.web.request.EditRoleRequest;
import com.onixbyte.helix.domain.web.request.QueryRoleRequest;
import com.onixbyte.helix.domain.web.request.RoleRequest;
import com.onixbyte.helix.domain.web.response.ActionResponse;
import com.onixbyte.helix.service.RoleService;
import com.onixbyte.helix.shared.MessageName;
import com.onixbyte.helix.utils.MessageUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -24,10 +25,12 @@ import org.springframework.web.bind.annotation.*;
public class RoleController {
private final RoleService roleService;
private final MessageUtil messageUtil;
@Autowired
public RoleController(RoleService roleService) {
public RoleController(RoleService roleService, MessageUtil messageUtil) {
this.roleService = roleService;
this.messageUtil = messageUtil;
}
@GetMapping
@@ -41,20 +44,21 @@ public class RoleController {
}
@PostMapping
public ResponseEntity<Void> addRole(@Validated @RequestBody AddRoleRequest request) {
roleService.addRole(request);
return ResponseEntity.ok(null);
public Role addRole(@Validated @RequestBody RoleRequest request) {
return roleService.addRole(request);
}
@PutMapping
public ResponseEntity<Void> editRole(@Validated @RequestBody EditRoleRequest request) {
roleService.editRole(request);
return ResponseEntity.ok(null);
@PutMapping("/{id:\\d+}")
public Role editRole(
@PathVariable Long id,
@Validated @RequestBody RoleRequest request
) {
return roleService.editRole(id, request);
}
@DeleteMapping("/{id:\\d+}")
public ResponseEntity<Void> deleteRole(@PathVariable Long id) {
roleService.deleteRole(id);
return ResponseEntity.ok(null);
public ActionResponse deleteRole(@PathVariable Long id) {
var name = roleService.deleteRole(id);
return ActionResponse.success(messageUtil.getMessage(MessageName.ROLE_DELETED, name));
}
}
@@ -7,13 +7,12 @@ import com.onixbyte.helix.domain.web.request.ResetPasswordRequest;
import com.onixbyte.helix.domain.web.response.ActionResponse;
import com.onixbyte.helix.domain.web.response.UserDetailResponse;
import com.onixbyte.helix.service.UserService;
import com.onixbyte.helix.shared.Message;
import com.onixbyte.helix.shared.MessageName;
import com.onixbyte.helix.utils.MessageUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -82,13 +81,16 @@ public class UserController {
/**
* Edit a user.
*
* @param id user ID
* @param request user to be edited
* @return edited user
*/
@PutMapping
public ResponseEntity<Void> editUser(@Validated @RequestBody EditUserRequest request) {
userService.updateUser(request);
return ResponseEntity.ok(null);
@PutMapping("/{id:\\d+}")
public UserDetailResponse editUser(
@PathVariable Long id,
@Validated @RequestBody EditUserRequest request
) {
return userService.updateUser(id, request);
}
/**
@@ -104,7 +106,7 @@ public class UserController {
@Validated @RequestBody ResetPasswordRequest request
) {
userService.resetPassword(id, request);
return ActionResponse.success(messageUtil.getMessage(Message.USER_PASSWORD_RESET_SUCCESS));
return ActionResponse.success(messageUtil.getMessage(MessageName.USER_PASSWORD_RESET_SUCCESS));
}
/**
@@ -117,6 +119,6 @@ public class UserController {
@DeleteMapping("/{userId:\\d+}")
public ActionResponse deleteUser(@PathVariable Long userId) {
userService.deleteUser(userId);
return ActionResponse.success(messageUtil.getMessage(Message.USER_DELETED));
return ActionResponse.success(messageUtil.getMessage(MessageName.USER_DELETED, userId));
}
}
@@ -1,16 +1,17 @@
package com.onixbyte.helix.domain.web.request;
import com.onixbyte.helix.enumeration.UserStatus;
import com.onixbyte.helix.shared.MessageName;
import jakarta.validation.constraints.NotBlank;
import java.util.List;
public record AddUserRequest(
@NotBlank(message = "Username cannot be empty.")
@NotBlank(message = "{" + MessageName.REQUEST_ADD_USER_USERNAME_NOT_EMPTY + "}")
String username,
@NotBlank(message = "Password cannot be empty.")
@NotBlank(message = "{" + MessageName.REQUEST_ADD_USER_PASSWORD_NOT_EMPTY + "}")
String password,
@NotBlank(message = "Full name cannot be empty.")
@NotBlank(message = "{" + MessageName.REQUEST_ADD_USER_FULL_NAME_NOT_EMPTY + "}")
String fullName,
String email,
String regionAbbreviation,
@@ -1,6 +1,7 @@
package com.onixbyte.helix.domain.web.request;
import com.onixbyte.helix.enumeration.Status;
import com.onixbyte.helix.shared.MessageName;
import com.onixbyte.helix.validation.group.OnCreate;
import com.onixbyte.helix.validation.group.OnUpdate;
import jakarta.validation.constraints.NotBlank;
@@ -8,11 +9,11 @@ import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null;
public record AuthorityRequest(
@Null(groups = {OnUpdate.class}, message = "Code cannot be edited.")
@NotNull(groups = {OnCreate.class}, message = "Code cannot be null.")
@Null(groups = {OnUpdate.class}, message = "{" + MessageName.REQUEST_AUTHORITY_CODE_NOT_EDITABLE + "}")
@NotNull(groups = {OnCreate.class}, message = "{" + MessageName.REQUEST_AUTHORITY_CODE_NOT_NULL + "}")
String code,
@NotNull(message = "Name of the authority cannot be null")
@NotBlank(message = "Name of the authority cannot be null")
@NotNull(message = "{" + MessageName.REQUEST_AUTHORITY_NAME_NOT_NULL + "}")
@NotBlank(message = "{" + MessageName.REQUEST_AUTHORITY_NAME_NOT_NULL + "}")
String name,
String description,
Status status
@@ -23,12 +23,13 @@
package com.onixbyte.helix.domain.web.request;
import com.onixbyte.helix.enumeration.Status;
import com.onixbyte.helix.shared.MessageName;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public record DepartmentRequest(
@NotNull(message = "Name of the department should not be null")
@NotBlank(message = "Name of the department should not be null")
@NotNull(message = "{" + MessageName.REQUEST_DEPARTMENT_NAME_NOT_NULL + "}")
@NotBlank(message = "{" + MessageName.REQUEST_DEPARTMENT_NAME_NOT_NULL + "}")
String name,
Long parentId,
Integer sort,
@@ -1,10 +1,11 @@
package com.onixbyte.helix.domain.web.request;
import com.onixbyte.helix.shared.MessageName;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
public record EditRoleRequest(
@NotNull(message = "角色 ID 不能为空")
@NotNull(message = "{" + MessageName.REQUEST_EDIT_ROLE_ID_NOT_NULL + "}")
Long id,
String name,
String code,
@@ -13,7 +14,7 @@ public record EditRoleRequest(
String description,
@Pattern(
regexp = "^(ACTIVE|INACTIVE)?$",
message = "状态仅可以是 ACTIVE、INACTIVE 其中之一")
message = "{" + MessageName.REQUEST_EDIT_ROLE_STATUS_INVALID + "}")
String status
) {
}
@@ -1,12 +1,13 @@
package com.onixbyte.helix.domain.web.request;
import com.onixbyte.helix.enumeration.UserStatus;
import com.onixbyte.helix.shared.MessageName;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
public record EditUserRequest(
@NotNull(message = "User ID cannot be null")
@Positive(message = "User ID must be positive")
@NotNull(message = "{" + MessageName.REQUEST_EDIT_USER_ID_NOT_NULL + "}")
@Positive(message = "{" + MessageName.REQUEST_EDIT_USER_ID_POSITIVE + "}")
Long id,
String fullName,
String email,
@@ -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,9 +1,10 @@
package com.onixbyte.helix.domain.web.request;
import com.onixbyte.helix.shared.MessageName;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public record ResetPasswordRequest(
@NotBlank(message = "密码不能为空") String password
@NotBlank(message = "{" + MessageName.REQUEST_RESET_PASSWORD_NOT_EMPTY + "}") String password
) {
}
@@ -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
) {
}
@@ -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;
}
}
Loaded 100 of 281 files, more files were not shown because too many files have changed in this diff. Show more