feat: add UMS CLI for binary packaging and system initialization
- Add Cobra-based CLI with ums init, ums serve, ums version commands - ums init supports interactive prompts and non-interactive flags - Generates secure JWT secrets and config.yaml automatically - Extract server.Serve() function for reuse - Add cross-platform build targets to Makefile - Update README with CLI installation and usage instructions New files: - cmd/ums/main.go - CLI entry point - cmd/ums/cmd/root.go - Root command - cmd/ums/cmd/init.go - Interactive/non-interactive init - cmd/ums/cmd/serve.go - Server command - cmd/ums/cmd/version.go - Version command - internal/server/server.go - Extracted Serve function
This commit is contained in:
30
Makefile
30
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: help build run test clean vet tidy check run-check db-dir
|
.PHONY: help build build-cli build-cli-all run test clean vet tidy check run-check db-dir
|
||||||
|
|
||||||
help: ## 显示帮助信息
|
help: ## 显示帮助信息
|
||||||
@echo "======================================"
|
@echo "======================================"
|
||||||
@@ -7,6 +7,8 @@ help: ## 显示帮助信息
|
|||||||
@echo "可用命令:"
|
@echo "可用命令:"
|
||||||
@echo " make check - 全面检查(依赖+vet+编译+测试)"
|
@echo " make check - 全面检查(依赖+vet+编译+测试)"
|
||||||
@echo " make build - 构建应用"
|
@echo " make build - 构建应用"
|
||||||
|
@echo " make build-cli - 构建 UMS CLI"
|
||||||
|
@echo " make build-cli-all - 交叉编译所有平台"
|
||||||
@echo " make run - 运行应用"
|
@echo " make run - 运行应用"
|
||||||
@echo " make test - 运行测试"
|
@echo " make test - 运行测试"
|
||||||
@echo " make vet - 代码静态检查"
|
@echo " make vet - 代码静态检查"
|
||||||
@@ -15,7 +17,17 @@ help: ## 显示帮助信息
|
|||||||
@echo " make clean - 清理构建文件"
|
@echo " make clean - 清理构建文件"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
check: tidy vet build test ## 全面检查:依赖+静态检查+编译+测试
|
# CLI 构建配置
|
||||||
|
CLI_NAME = ums
|
||||||
|
VERSION = 1.0.0
|
||||||
|
COMMIT = $(shell git rev-parse --short HEAD 2>/dev/null || echo "dev")
|
||||||
|
BUILD_DATE = $(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||||
|
LDFLAGS = -ldflags "-X github.com/user-management-system/cmd/ums/cmd.Version=$(VERSION) -X github.com/user-management-system/cmd/ums/cmd.Commit=$(COMMIT) -X github.com/user-management-system/cmd/ums/cmd.BuildDate=$(BUILD_DATE)"
|
||||||
|
|
||||||
|
# 平台列表
|
||||||
|
PLATFORMS = darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64
|
||||||
|
|
||||||
|
check: tidy vet build build-cli test ## 全面检查:依赖+静态检查+编译+测试
|
||||||
|
|
||||||
tidy: ## 整理Go模块依赖
|
tidy: ## 整理Go模块依赖
|
||||||
@echo "整理依赖..."
|
@echo "整理依赖..."
|
||||||
@@ -30,6 +42,20 @@ build: db-dir ## 构建应用
|
|||||||
@echo "构建应用..."
|
@echo "构建应用..."
|
||||||
go build -o bin/server cmd/server/main.go
|
go build -o bin/server cmd/server/main.go
|
||||||
|
|
||||||
|
build-cli: ## 构建 UMS CLI(当前平台)
|
||||||
|
@echo "构建 UMS CLI..."
|
||||||
|
CGO_ENABLED=0 go build $(LDFLAGS) -o bin/$(CLI_NAME) cmd/ums/main.go
|
||||||
|
|
||||||
|
build-cli-all: $(PLATFORMS) ## 构建所有平台的 CLI
|
||||||
|
@echo "所有平台构建完成"
|
||||||
|
|
||||||
|
build-cli-%:
|
||||||
|
@platform=$(patsubst %/%,%,$@); \
|
||||||
|
os=$(platform%%/*); \
|
||||||
|
arch=$(platform##*/); \
|
||||||
|
echo "Building for $$os/$$arch"; \
|
||||||
|
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch go build $(LDFLAGS) -o bin/$(CLI_NAME)-$$os-$$arch cmd/ums/main.go
|
||||||
|
|
||||||
run: db-dir ## 运行应用
|
run: db-dir ## 运行应用
|
||||||
@echo "运行应用..."
|
@echo "运行应用..."
|
||||||
go run cmd/server/main.go
|
go run cmd/server/main.go
|
||||||
|
|||||||
143
README.md
143
README.md
@@ -4,26 +4,50 @@
|
|||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 前置依赖
|
### 安装 UMS CLI
|
||||||
|
|
||||||
- Go 1.21+
|
|
||||||
- Node.js 18+
|
|
||||||
- SQLite(默认,无需安装)
|
|
||||||
|
|
||||||
### 启动后端
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 复制环境配置
|
# 下载对应平台的二进制文件
|
||||||
cp .env.example .env
|
# Linux/macOS
|
||||||
# 编辑 .env 填入必要配置(JWT_SECRET, DEFAULT_ADMIN_PASSWORD 等)
|
curl -L -o ums https://github.com/user-management-system/ums/releases/latest/download/ums-linux-amd64
|
||||||
|
chmod +x ums
|
||||||
|
|
||||||
# 启动服务
|
# Windows
|
||||||
go run ./cmd/server
|
curl -L -o ums.exe https://github.com/user-management-system/ums/releases/latest/download/ums-windows-amd64.exe
|
||||||
|
|
||||||
|
# 或使用 Go 安装
|
||||||
|
go install github.com/user-management-system/cmd/ums@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
服务启动后访问 `http://localhost:8080/api/v1/auth/bootstrap` 初始化管理员账号。
|
### 初始化系统
|
||||||
|
|
||||||
### 启动前端
|
```bash
|
||||||
|
# 交互式初始化(推荐)
|
||||||
|
ums init
|
||||||
|
|
||||||
|
# 非交互式初始化
|
||||||
|
ums init \
|
||||||
|
--admin-user admin \
|
||||||
|
--admin-pass MySecretPassword123 \
|
||||||
|
--admin-email admin@example.com \
|
||||||
|
--cors-origin http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
初始化命令会:
|
||||||
|
1. 生成安全的 JWT 密钥
|
||||||
|
2. 创建配置文件 `config.yaml` 和 `.env`
|
||||||
|
3. 创建数据库目录并执行迁移
|
||||||
|
4. 初始化默认角色、权限和管理员账号
|
||||||
|
|
||||||
|
### 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ums serve
|
||||||
|
```
|
||||||
|
|
||||||
|
服务启动后访问 `http://localhost:8080/health` 确认服务正常运行。
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend/admin
|
cd frontend/admin
|
||||||
@@ -35,13 +59,20 @@ npm run dev
|
|||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── cmd/server/ # 后端入口
|
├── cmd/
|
||||||
|
│ ├── ums/ # UMS CLI 入口
|
||||||
|
│ │ └── cmd/ # CLI 子命令
|
||||||
|
│ │ ├── init.go # 初始化命令
|
||||||
|
│ │ ├── serve.go # 服务启动命令
|
||||||
|
│ │ └── version.go # 版本命令
|
||||||
|
│ └── server/ # 后端服务入口
|
||||||
├── internal/ # 后端代码
|
├── internal/ # 后端代码
|
||||||
│ ├── api/handler/ # HTTP 处理器
|
│ ├── api/handler/ # HTTP 处理器
|
||||||
│ ├── api/middleware/ # 中间件(认证、权限、限流)
|
│ ├── api/middleware/ # 中间件(认证、权限、限流)
|
||||||
│ ├── auth/ # 认证服务(JWT/SSO)
|
│ ├── auth/ # 认证服务(JWT/SSO)
|
||||||
│ ├── repository/ # 数据访问层
|
│ ├── repository/ # 数据访问层
|
||||||
│ ├── service/ # 业务逻辑层
|
│ ├── service/ # 业务逻辑层
|
||||||
|
│ ├── server/ # 服务器核心逻辑
|
||||||
│ └── domain/ # 领域模型
|
│ └── domain/ # 领域模型
|
||||||
├── frontend/admin/ # 管理后台前端
|
├── frontend/admin/ # 管理后台前端
|
||||||
├── configs/ # 配置文件
|
├── configs/ # 配置文件
|
||||||
@@ -77,49 +108,73 @@ npm run dev
|
|||||||
| OAuth context 正确传播 | ✅ 已修复 |
|
| OAuth context 正确传播 | ✅ 已修复 |
|
||||||
| 密码修改后 Token 失效(PCE) | ✅ 已修复 |
|
| 密码修改后 Token 失效(PCE) | ✅ 已修复 |
|
||||||
|
|
||||||
## 环境变量
|
## CLI 命令
|
||||||
|
|
||||||
关键配置项(详见 `.env.example`):
|
```bash
|
||||||
|
ums init # 初始化系统(交互式或非交互式)
|
||||||
|
ums serve # 启动服务器
|
||||||
|
ums version # 显示版本信息
|
||||||
|
|
||||||
| 变量 | 说明 | 必填 |
|
# ums serve 选项
|
||||||
|------|------|------|
|
ums serve --port 8080 # 指定端口
|
||||||
| `JWT_SECRET` | JWT 签名密钥 | 是 |
|
ums serve --config ./prod.yaml # 指定配置文件
|
||||||
| `DEFAULT_ADMIN_EMAIL` | 初始管理员邮箱 | 是 |
|
|
||||||
| `DEFAULT_ADMIN_PASSWORD` | 初始管理员密码 | 是 |
|
|
||||||
| `SMTP_*` | 邮件服务配置 | 是(邮件功能)|
|
|
||||||
| `SMS_*` | 短信服务配置 | 否 |
|
|
||||||
|
|
||||||
## API 文档
|
# ums init 选项
|
||||||
|
ums init --db-type sqlite # 数据库类型
|
||||||
完整 API 规范:`docs/API.md`
|
ums init --db-path ./data/ums.db # 数据库路径
|
||||||
|
ums init --redis-enable # 启用 Redis
|
||||||
认证流程:
|
ums init --redis-host localhost # Redis 地址
|
||||||
|
ums init --admin-user admin # 管理员用户名
|
||||||
|
ums init --admin-pass MyPassword123 # 管理员密码
|
||||||
|
ums init --admin-email admin@example.com # 管理员邮箱
|
||||||
|
ums init --port 8080 # 服务端口
|
||||||
|
ums init --cors-origin http://example.com # CORS 域名
|
||||||
|
ums init --yes # 跳过确认
|
||||||
```
|
```
|
||||||
1. POST /api/v1/auth/register # 注册用户
|
|
||||||
2. POST /api/v1/auth/login # 登录获取 Token
|
## 配置文件
|
||||||
3. POST /api/v1/auth/refresh # 刷新 Token
|
|
||||||
|
初始化后生成以下配置文件:
|
||||||
|
|
||||||
|
**config.yaml** - 主配置文件
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
mode: release
|
||||||
|
database:
|
||||||
|
type: sqlite
|
||||||
|
dbname: "./data/user_management.db"
|
||||||
|
jwt:
|
||||||
|
secret: "<自动生成的密钥>"
|
||||||
|
redis:
|
||||||
|
enabled: false
|
||||||
|
```
|
||||||
|
|
||||||
|
**.env** - 环境变量(包含敏感信息,请勿提交)
|
||||||
|
```bash
|
||||||
|
JWT_SECRET=<自动生成的密钥>
|
||||||
|
BOOTSTRAP_SECRET=<自动生成的密钥>
|
||||||
|
DEFAULT_ADMIN_EMAIL=admin@example.com
|
||||||
|
DEFAULT_ADMIN_PASSWORD=<您设置的密码>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 开发命令
|
## 开发命令
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 构建
|
# 构建 CLI
|
||||||
|
make build-cli
|
||||||
|
|
||||||
|
# 构建所有平台 CLI
|
||||||
|
make build-cli-all
|
||||||
|
|
||||||
|
# 构建服务器
|
||||||
go build ./cmd/server
|
go build ./cmd/server
|
||||||
|
|
||||||
# 测试(跳过大规模性能测试)
|
# 测试
|
||||||
go test ./internal/... -skip TestScale -count=1
|
go test ./internal/... -skip TestScale -count=1
|
||||||
|
|
||||||
# 前端构建
|
# 前端构建
|
||||||
cd frontend/admin && npm run build
|
cd frontend/admin && npm run build
|
||||||
|
|
||||||
# 前端测试
|
|
||||||
cd frontend/admin && npm test
|
|
||||||
|
|
||||||
# 前端 lint
|
|
||||||
cd frontend/admin && npm run lint
|
|
||||||
|
|
||||||
# Docker 构建
|
|
||||||
docker build -t ums .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
@@ -144,4 +199,4 @@ docker build -t ums .
|
|||||||
|
|
||||||
完整项目状态:`docs/status/REAL_PROJECT_STATUS.md`
|
完整项目状态:`docs/status/REAL_PROJECT_STATUS.md`
|
||||||
|
|
||||||
**2026-04-18 最新状态:** 所有 P0/P1/P2 安全和质量修复已全部完成并验证通过。
|
**2026-04-19 最新状态:** CLI 打包和系统初始化优化已完成,支持单一二进制文件部署和交互式/非交互式初始化。
|
||||||
|
|||||||
@@ -1,28 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
"github.com/user-management-system/internal/api/handler"
|
|
||||||
"github.com/user-management-system/internal/api/middleware"
|
|
||||||
"github.com/user-management-system/internal/api/router"
|
|
||||||
"github.com/user-management-system/internal/auth"
|
|
||||||
"github.com/user-management-system/internal/cache"
|
|
||||||
"github.com/user-management-system/internal/config"
|
"github.com/user-management-system/internal/config"
|
||||||
"github.com/user-management-system/internal/database"
|
"github.com/user-management-system/internal/server"
|
||||||
"github.com/user-management-system/internal/monitoring"
|
|
||||||
"github.com/user-management-system/internal/repository"
|
|
||||||
"github.com/user-management-system/internal/security"
|
|
||||||
"github.com/user-management-system/internal/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -32,240 +14,8 @@ func main() {
|
|||||||
log.Fatalf("load config failed: %v", err)
|
log.Fatalf("load config failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置 Gin 模式
|
|
||||||
gin.SetMode(resolveGinMode(cfg.Server.Mode))
|
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
db, err := database.NewDB(cfg)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("connect database failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行数据库迁移
|
|
||||||
if err := db.AutoMigrate(cfg); err != nil {
|
|
||||||
log.Fatalf("auto migrate failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// P1-3:Argon2id 启动时自适应校准
|
|
||||||
// 在当前机器上测量哈希耗时,超出 500ms 预算则自动降低参数,确保登录接口 P99 < 1000ms。
|
|
||||||
// 此操作仅在启动阶段执行一次,耗时约 1-3s(正常情况下与默认参数一致则跳过)。
|
|
||||||
auth.CalibrateArgon2id(500 * time.Millisecond)
|
|
||||||
|
|
||||||
// 初始化 JWT 管理器
|
|
||||||
jwtManager, err := auth.NewJWTWithOptions(auth.JWTOptions{
|
|
||||||
HS256Secret: cfg.JWT.Secret,
|
|
||||||
AccessTokenExpire: time.Duration(cfg.JWT.AccessTokenExpireMinutes) * time.Minute,
|
|
||||||
RefreshTokenExpire: time.Duration(cfg.JWT.RefreshTokenExpireDays) * 24 * time.Hour,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("create jwt manager failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化缓存
|
|
||||||
// Redis 智能探测:有 Redis 则启用 L2 分布式缓存,无 Redis 则降级到纯 L1 内存缓存。
|
|
||||||
// 两种模式下系统功能完全等价,区别仅在于多实例场景的缓存共享能力。
|
|
||||||
// 如需禁用 Redis 探测(即使 Redis 可达也不启用),可将配置中 redis.host 留空。
|
|
||||||
l1Cache := cache.NewL1Cache()
|
|
||||||
redisAddr := fmt.Sprintf("%s:%d", cfg.Redis.Host, cfg.Redis.Port)
|
|
||||||
redisEnabled := cfg.Redis.Host != "" && cache.ProbeRedis(redisAddr, cfg.Redis.Password, cfg.Redis.DB)
|
|
||||||
if !redisEnabled {
|
|
||||||
log.Printf("cache: running in memory-only mode (Redis unreachable or not configured)")
|
|
||||||
}
|
|
||||||
l2Cache := cache.NewRedisCacheWithConfig(cache.RedisCacheConfig{
|
|
||||||
Enabled: redisEnabled,
|
|
||||||
Addr: redisAddr,
|
|
||||||
Password: cfg.Redis.Password,
|
|
||||||
DB: cfg.Redis.DB,
|
|
||||||
})
|
|
||||||
defer l2Cache.Close()
|
|
||||||
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
|
||||||
|
|
||||||
// 初始化 Repository
|
|
||||||
userRepo := repository.NewUserRepository(db.DB)
|
|
||||||
roleRepo := repository.NewRoleRepository(db.DB)
|
|
||||||
permissionRepo := repository.NewPermissionRepository(db.DB)
|
|
||||||
userRoleRepo := repository.NewUserRoleRepository(db.DB)
|
|
||||||
rolePermissionRepo := repository.NewRolePermissionRepository(db.DB)
|
|
||||||
deviceRepo := repository.NewDeviceRepository(db.DB)
|
|
||||||
loginLogRepo := repository.NewLoginLogRepository(db.DB)
|
|
||||||
operationLogRepo := repository.NewOperationLogRepository(db.DB)
|
|
||||||
customFieldRepo := repository.NewCustomFieldRepository(db.DB)
|
|
||||||
userCustomFieldValueRepo := repository.NewUserCustomFieldValueRepository(db.DB)
|
|
||||||
themeRepo := repository.NewThemeConfigRepository(db.DB)
|
|
||||||
socialRepo, err := repository.NewSocialAccountRepository(db.DB)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("initialize social account repository failed: %v", err)
|
|
||||||
}
|
|
||||||
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB)
|
|
||||||
|
|
||||||
// 初始化 Service
|
|
||||||
deviceService := service.NewDeviceService(deviceRepo, userRepo)
|
|
||||||
authService := service.NewAuthService(
|
|
||||||
userRepo,
|
|
||||||
socialRepo,
|
|
||||||
jwtManager,
|
|
||||||
cacheManager,
|
|
||||||
8, // passwordMinLength
|
|
||||||
5, // maxLoginAttempts
|
|
||||||
15*time.Minute, // loginLockDuration
|
|
||||||
)
|
|
||||||
authService.SetRoleRepositories(userRoleRepo, roleRepo)
|
|
||||||
authService.SetLoginLogRepository(loginLogRepo)
|
|
||||||
authService.SetDeviceService(deviceService)
|
|
||||||
|
|
||||||
// IP 过滤中间件
|
|
||||||
var ipFilterMiddleware *middleware.IPFilterMiddleware
|
|
||||||
ipFilter := security.NewIPFilter()
|
|
||||||
if ipFilter != nil {
|
|
||||||
ipFilterMiddleware = middleware.NewIPFilterMiddleware(ipFilter, middleware.IPFilterConfig{
|
|
||||||
TrustProxy: cfg.CORS.AllowCredentials,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化异常检测器并注入
|
|
||||||
anomalyDetector := security.NewAnomalyDetector(security.DefaultAnomalyConfig, ipFilter)
|
|
||||||
authService.SetAnomalyDetector(anomalyDetector)
|
|
||||||
log.Println("anomaly detector initialized")
|
|
||||||
|
|
||||||
userService := service.NewUserService(userRepo, userRoleRepo, roleRepo, passwordHistoryRepo)
|
|
||||||
roleService := service.NewRoleService(roleRepo, rolePermissionRepo)
|
|
||||||
permissionService := service.NewPermissionService(permissionRepo)
|
|
||||||
loginLogService := service.NewLoginLogService(loginLogRepo)
|
|
||||||
operationLogService := service.NewOperationLogService(operationLogRepo)
|
|
||||||
captchaService := service.NewCaptchaService(cacheManager)
|
|
||||||
totpService := service.NewTOTPService(userRepo)
|
|
||||||
|
|
||||||
passwordResetConfig := service.DefaultPasswordResetConfig()
|
|
||||||
passwordResetService := service.NewPasswordResetService(userRepo, cacheManager, passwordResetConfig).
|
|
||||||
WithPasswordHistoryRepo(passwordHistoryRepo)
|
|
||||||
|
|
||||||
webhookService := service.NewWebhookService(db.DB, service.WebhookServiceConfig{
|
|
||||||
Enabled: false,
|
|
||||||
})
|
|
||||||
exportService := service.NewExportService(userRepo, roleRepo)
|
|
||||||
statsService := service.NewStatsService(userRepo, loginLogRepo)
|
|
||||||
customFieldService := service.NewCustomFieldService(customFieldRepo, userCustomFieldValueRepo)
|
|
||||||
themeService := service.NewThemeService(themeRepo)
|
|
||||||
|
|
||||||
// 设置 CORS 配置
|
|
||||||
middleware.SetCORSConfig(cfg.CORS)
|
|
||||||
|
|
||||||
// 初始化中间件
|
|
||||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware(cfg.RateLimit)
|
|
||||||
authMiddleware := middleware.NewAuthMiddleware(
|
|
||||||
jwtManager,
|
|
||||||
userRepo,
|
|
||||||
userRoleRepo,
|
|
||||||
l1Cache,
|
|
||||||
)
|
|
||||||
authMiddleware.SetCacheManager(cacheManager)
|
|
||||||
|
|
||||||
opLogMiddleware := middleware.NewOperationLogMiddleware(operationLogRepo)
|
|
||||||
|
|
||||||
// 初始化 Handler
|
|
||||||
authHandler := handler.NewAuthHandler(authService)
|
|
||||||
userHandler := handler.NewUserHandler(userService)
|
|
||||||
roleHandler := handler.NewRoleHandler(roleService)
|
|
||||||
permissionHandler := handler.NewPermissionHandler(permissionService)
|
|
||||||
deviceHandler := handler.NewDeviceHandler(deviceService)
|
|
||||||
logHandler := handler.NewLogHandler(loginLogService, operationLogService)
|
|
||||||
captchaHandler := handler.NewCaptchaHandler(captchaService)
|
|
||||||
totpHandler := handler.NewTOTPHandler(authService, totpService)
|
|
||||||
webhookHandler := handler.NewWebhookHandler(webhookService)
|
|
||||||
exportHandler := handler.NewExportHandler(exportService)
|
|
||||||
statsHandler := handler.NewStatsHandler(statsService)
|
|
||||||
passwordResetHandler := handler.NewPasswordResetHandler(passwordResetService)
|
|
||||||
smsHandler := handler.NewSMSHandler(authService, nil)
|
|
||||||
avatarHandler := handler.NewAvatarHandler(userRepo)
|
|
||||||
customFieldHandler := handler.NewCustomFieldHandler(customFieldService)
|
|
||||||
themeHandler := handler.NewThemeHandler(themeService)
|
|
||||||
|
|
||||||
// 初始化 SSO 管理器
|
|
||||||
ssoManager := auth.NewSSOManager()
|
|
||||||
ssoClientsStore := auth.NewDefaultSSOClientsStore()
|
|
||||||
ssoHandler := handler.NewSSOHandler(ssoManager, ssoClientsStore)
|
|
||||||
|
|
||||||
// 系统设置服务
|
|
||||||
settingsService := service.NewSettingsService()
|
|
||||||
settingsHandler := handler.NewSettingsHandler(settingsService)
|
|
||||||
|
|
||||||
// SSO 会话清理 context(随服务器关闭而取消)
|
|
||||||
ssoCtx, ssoCancel := context.WithCancel(context.Background())
|
|
||||||
defer ssoCancel()
|
|
||||||
ssoManager.StartCleanup(ssoCtx)
|
|
||||||
|
|
||||||
// 初始化监控指标(CRIT-01/02 修复:确保指标被初始化并挂载)
|
|
||||||
metrics := monitoring.GetGlobalMetrics()
|
|
||||||
sloMetrics := monitoring.GetGlobalSLOMetrics()
|
|
||||||
|
|
||||||
// CRIT-03 修复:启动后台 goroutine 定期采集系统指标(runtime + DB 连接池)
|
|
||||||
metricsCtx, metricsCancel := context.WithCancel(context.Background())
|
|
||||||
defer metricsCancel()
|
|
||||||
go monitoring.StartSystemMetricsCollector(metricsCtx, metrics, sloMetrics, db.DB)
|
|
||||||
|
|
||||||
// 设置路由
|
|
||||||
r := router.NewRouter(
|
|
||||||
authHandler, userHandler, roleHandler, permissionHandler, deviceHandler,
|
|
||||||
logHandler, authMiddleware, rateLimitMiddleware, opLogMiddleware,
|
|
||||||
passwordResetHandler, captchaHandler, totpHandler, webhookHandler,
|
|
||||||
ipFilterMiddleware, exportHandler, statsHandler, smsHandler, customFieldHandler, themeHandler, ssoHandler,
|
|
||||||
settingsHandler, metrics, avatarHandler,
|
|
||||||
)
|
|
||||||
engine := r.Setup()
|
|
||||||
|
|
||||||
// 健康检查(增强版:存活/就绪分离,检查数据库连接)
|
|
||||||
healthCheck := monitoring.NewHealthCheck(db.DB)
|
|
||||||
engine.GET("/health", healthCheck.Handler)
|
|
||||||
engine.GET("/health/live", healthCheck.LivenessHandler)
|
|
||||||
engine.GET("/health/ready", healthCheck.ReadinessHandler)
|
|
||||||
|
|
||||||
// 启动服务器
|
// 启动服务器
|
||||||
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
if err := server.Serve(cfg); err != nil {
|
||||||
srv := &http.Server{
|
log.Fatalf("server failed: %v", err)
|
||||||
Addr: addr,
|
|
||||||
Handler: engine,
|
|
||||||
ReadTimeout: 30 * time.Second,
|
|
||||||
WriteTimeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Printf("server listening on %s", addr)
|
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
log.Fatalf("listen failed: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待中断信号
|
|
||||||
quit := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-quit
|
|
||||||
|
|
||||||
log.Println("shutting down server...")
|
|
||||||
|
|
||||||
// 关闭 Webhook 服务,等待投递任务完成
|
|
||||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer shutdownCancel()
|
|
||||||
if err := webhookService.Shutdown(shutdownCtx); err != nil {
|
|
||||||
log.Printf("webhook service shutdown: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
|
||||||
log.Fatalf("server forced to shutdown: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("server exited")
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveGinMode(mode string) string {
|
|
||||||
switch mode {
|
|
||||||
case "debug":
|
|
||||||
return gin.DebugMode
|
|
||||||
case "test":
|
|
||||||
return gin.TestMode
|
|
||||||
default:
|
|
||||||
return gin.ReleaseMode
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
500
cmd/ums/cmd/init.go
Normal file
500
cmd/ums/cmd/init.go
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
|
|
||||||
|
"github.com/user-management-system/internal/config"
|
||||||
|
"github.com/user-management-system/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// initCmd is the init subcommand
|
||||||
|
var initCmd = &cobra.Command{
|
||||||
|
Use: "init",
|
||||||
|
Short: "Initialize the User Management System",
|
||||||
|
Long: `Interactively initialize UMS: generate secrets, configure database,
|
||||||
|
bootstrap admin user, and create configuration files.
|
||||||
|
|
||||||
|
This command can run in two modes:
|
||||||
|
1. Interactive mode (no flags): Prompts for all required values
|
||||||
|
2. Non-interactive mode (with flags): Uses provided values directly
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Interactive mode
|
||||||
|
ums init
|
||||||
|
|
||||||
|
# Non-interactive mode
|
||||||
|
ums init --admin-user admin --admin-pass MySecret123 --admin-email admin@example.com`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runInit(cmd)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
initDBType string
|
||||||
|
initDBPath string
|
||||||
|
initRedisEnable bool
|
||||||
|
initRedisHost string
|
||||||
|
initRedisPort int
|
||||||
|
initRedisPassword string
|
||||||
|
adminUser string
|
||||||
|
adminPass string
|
||||||
|
adminEmail string
|
||||||
|
initPort int
|
||||||
|
initCorsOrigin string
|
||||||
|
yesFlag bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initCmd.Flags().StringVar(&initDBType, "db-type", "sqlite", "database type (sqlite, postgresql, mysql)")
|
||||||
|
initCmd.Flags().StringVar(&initDBPath, "db-path", "./data/user_management.db", "database path or connection string")
|
||||||
|
initCmd.Flags().BoolVar(&initRedisEnable, "redis-enable", false, "enable Redis")
|
||||||
|
initCmd.Flags().StringVar(&initRedisHost, "redis-host", "localhost", "Redis host")
|
||||||
|
initCmd.Flags().IntVar(&initRedisPort, "redis-port", 6379, "Redis port")
|
||||||
|
initCmd.Flags().StringVar(&initRedisPassword, "redis-password", "", "Redis password")
|
||||||
|
initCmd.Flags().StringVar(&adminUser, "admin-user", "", "admin username")
|
||||||
|
initCmd.Flags().StringVar(&adminPass, "admin-pass", "", "admin password")
|
||||||
|
initCmd.Flags().StringVar(&adminEmail, "admin-email", "", "admin email")
|
||||||
|
initCmd.Flags().IntVar(&initPort, "port", 8080, "server port")
|
||||||
|
initCmd.Flags().StringVar(&initCorsOrigin, "cors-origin", "http://localhost:3000", "CORS allowed origin")
|
||||||
|
initCmd.Flags().BoolVar(&yesFlag, "yes", false, "skip confirmation")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(initCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type initConfig struct {
|
||||||
|
dbType string
|
||||||
|
dbPath string
|
||||||
|
redisEnable bool
|
||||||
|
redisHost string
|
||||||
|
redisPort int
|
||||||
|
redisPassword string
|
||||||
|
adminUser string
|
||||||
|
adminPass string
|
||||||
|
adminEmail string
|
||||||
|
port int
|
||||||
|
corsOrigin string
|
||||||
|
jwtSecret string
|
||||||
|
bootstrapSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInit(cmd *cobra.Command) error {
|
||||||
|
cfg, err := gatherInitConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示配置摘要
|
||||||
|
fmt.Println("\n=== Configuration Summary ===")
|
||||||
|
fmt.Printf("Database Type: %s\n", cfg.dbType)
|
||||||
|
fmt.Printf("Database Path: %s\n", cfg.dbPath)
|
||||||
|
fmt.Printf("Redis Enabled: %t\n", cfg.redisEnable)
|
||||||
|
if cfg.redisEnable {
|
||||||
|
fmt.Printf("Redis Host: %s:%d\n", cfg.redisHost, cfg.redisPort)
|
||||||
|
}
|
||||||
|
fmt.Printf("Admin User: %s\n", cfg.adminUser)
|
||||||
|
fmt.Printf("Admin Email: %s\n", cfg.adminEmail)
|
||||||
|
fmt.Printf("Server Port: %d\n", cfg.port)
|
||||||
|
fmt.Printf("CORS Origin: %s\n", cfg.corsOrigin)
|
||||||
|
fmt.Println("===========================")
|
||||||
|
|
||||||
|
if !yesFlag {
|
||||||
|
fmt.Print("Proceed with initialization? [y/N]: ")
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(strings.ToLower(input))
|
||||||
|
if input != "y" && input != "yes" {
|
||||||
|
fmt.Println("Initialization cancelled.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 创建数据目录
|
||||||
|
if err := createDataDirectory(cfg); err != nil {
|
||||||
|
return fmt.Errorf("create data directory failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 生成密钥
|
||||||
|
cfg.jwtSecret = generateSecret(32)
|
||||||
|
cfg.bootstrapSecret = generateSecret(16)
|
||||||
|
|
||||||
|
// 3. 创建配置文件
|
||||||
|
if err := createConfigFiles(cfg); err != nil {
|
||||||
|
return fmt.Errorf("create config files failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 执行数据库迁移
|
||||||
|
if err := runMigrations(cfg); err != nil {
|
||||||
|
return fmt.Errorf("run migrations failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 创建管理员账号
|
||||||
|
if err := createAdminUser(cfg); err != nil {
|
||||||
|
return fmt.Errorf("create admin user failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n=== Initialization Complete ===")
|
||||||
|
fmt.Println("Configuration files created:")
|
||||||
|
fmt.Printf(" - %s\n", getConfigPath())
|
||||||
|
fmt.Printf(" - %s\n", getEnvPath())
|
||||||
|
fmt.Println("\nTo start the server:")
|
||||||
|
fmt.Println(" ums serve")
|
||||||
|
fmt.Println("=============================")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherInitConfig() (*initConfig, error) {
|
||||||
|
isInteractive := adminUser == "" || adminPass == "" || adminEmail == ""
|
||||||
|
|
||||||
|
if isInteractive {
|
||||||
|
return gatherInteractiveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非交互式模式:验证必需参数
|
||||||
|
if adminUser == "" {
|
||||||
|
return nil, fmt.Errorf("--admin-user is required")
|
||||||
|
}
|
||||||
|
if adminPass == "" {
|
||||||
|
return nil, fmt.Errorf("--admin-pass is required")
|
||||||
|
}
|
||||||
|
if adminEmail == "" {
|
||||||
|
return nil, fmt.Errorf("--admin-email is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码强度
|
||||||
|
if len(adminPass) < 8 {
|
||||||
|
return nil, fmt.Errorf("password must be at least 8 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &initConfig{
|
||||||
|
dbType: initDBType,
|
||||||
|
dbPath: initDBPath,
|
||||||
|
redisEnable: initRedisEnable,
|
||||||
|
redisHost: initRedisHost,
|
||||||
|
redisPort: initRedisPort,
|
||||||
|
redisPassword: initRedisPassword,
|
||||||
|
adminUser: adminUser,
|
||||||
|
adminPass: adminPass,
|
||||||
|
adminEmail: adminEmail,
|
||||||
|
port: initPort,
|
||||||
|
corsOrigin: initCorsOrigin,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherInteractiveConfig() (*initConfig, error) {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
cfg := &initConfig{
|
||||||
|
dbType: "sqlite",
|
||||||
|
dbPath: "./data/user_management.db",
|
||||||
|
redisEnable: false,
|
||||||
|
redisHost: "localhost",
|
||||||
|
redisPort: 6379,
|
||||||
|
port: 8080,
|
||||||
|
corsOrigin: "http://localhost:3000",
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("=== UMS Initialization ===")
|
||||||
|
|
||||||
|
// 数据库类型
|
||||||
|
fmt.Printf("Database type [%s]: ", cfg.dbType)
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input != "" {
|
||||||
|
cfg.dbType = input
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据库路径
|
||||||
|
fmt.Printf("Database path [%s]: ", cfg.dbPath)
|
||||||
|
input, _ = reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input != "" {
|
||||||
|
cfg.dbPath = input
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis 启用
|
||||||
|
fmt.Printf("Enable Redis? [y/N]: ")
|
||||||
|
input, _ = reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(strings.ToLower(input))
|
||||||
|
cfg.redisEnable = input == "y" || input == "yes"
|
||||||
|
|
||||||
|
if cfg.redisEnable {
|
||||||
|
fmt.Printf("Redis host [%s]: ", cfg.redisHost)
|
||||||
|
input, _ = reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input != "" {
|
||||||
|
cfg.redisHost = input
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Redis port [%d]: ", cfg.redisPort)
|
||||||
|
input, _ = reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input != "" {
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(input, "%d", &port)
|
||||||
|
cfg.redisPort = port
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Redis password (empty for none): ")
|
||||||
|
input, _ = reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
cfg.redisPassword = input
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员信息
|
||||||
|
fmt.Printf("Admin username [admin]: ")
|
||||||
|
input, _ = reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input != "" {
|
||||||
|
cfg.adminUser = input
|
||||||
|
} else {
|
||||||
|
cfg.adminUser = "admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Admin password: ")
|
||||||
|
password, err := readPassword()
|
||||||
|
fmt.Println()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read password failed: %w", err)
|
||||||
|
}
|
||||||
|
if len(password) < 8 {
|
||||||
|
return nil, fmt.Errorf("password must be at least 8 characters")
|
||||||
|
}
|
||||||
|
cfg.adminPass = password
|
||||||
|
|
||||||
|
fmt.Printf("Admin email [%s]: ", cfg.adminEmail)
|
||||||
|
input, _ = reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input != "" {
|
||||||
|
cfg.adminEmail = input
|
||||||
|
} else if cfg.adminEmail == "" {
|
||||||
|
cfg.adminEmail = "admin@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务器端口
|
||||||
|
fmt.Printf("Server port [%d]: ", cfg.port)
|
||||||
|
input, _ = reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input != "" {
|
||||||
|
var port int
|
||||||
|
fmt.Sscanf(input, "%d", &port)
|
||||||
|
cfg.port = port
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS 域名
|
||||||
|
fmt.Printf("CORS origin [%s]: ", cfg.corsOrigin)
|
||||||
|
input, _ = reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input != "" {
|
||||||
|
cfg.corsOrigin = input
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPassword() (string, error) {
|
||||||
|
intrFd := int(os.Stdin.Fd())
|
||||||
|
if term.IsTerminal(intrFd) {
|
||||||
|
password, err := term.ReadPassword(intrFd)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(password), nil
|
||||||
|
}
|
||||||
|
// 非终端模式,使用普通输入
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
return strings.TrimSpace(input), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSecret(length int) string {
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
rand.Read(bytes)
|
||||||
|
return hex.EncodeToString(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDataDirectory(cfg *initConfig) error {
|
||||||
|
var dir string
|
||||||
|
if cfg.dbType == "sqlite" {
|
||||||
|
dir = filepath.Dir(cfg.dbPath)
|
||||||
|
if dir == "." || dir == "" {
|
||||||
|
dir = "./data"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dir = "./data"
|
||||||
|
}
|
||||||
|
|
||||||
|
if dir == "" || dir == "." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.MkdirAll(dir, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfigPath() string {
|
||||||
|
return "./config.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvPath() string {
|
||||||
|
return "./.env"
|
||||||
|
}
|
||||||
|
|
||||||
|
func createConfigFiles(cfg *initConfig) error {
|
||||||
|
// 创建 .env 文件
|
||||||
|
envContent := fmt.Sprintf(`# Auto-generated by ums init
|
||||||
|
# DO NOT COMMIT THIS FILE
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
JWT_SECRET=%s
|
||||||
|
JWT_REFRESH_SECRET=%s
|
||||||
|
|
||||||
|
# Bootstrap secret for admin initialization
|
||||||
|
BOOTSTRAP_SECRET=%s
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_PATH=%s
|
||||||
|
|
||||||
|
# Admin credentials
|
||||||
|
DEFAULT_ADMIN_EMAIL=%s
|
||||||
|
DEFAULT_ADMIN_PASSWORD=%s
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_ENABLED=%t
|
||||||
|
REDIS_HOST=%s
|
||||||
|
REDIS_PORT=%d
|
||||||
|
REDIS_PASSWORD=%s
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ALLOWED_ORIGINS=%s
|
||||||
|
|
||||||
|
# Server
|
||||||
|
SERVER_PORT=%d
|
||||||
|
GIN_MODE=release
|
||||||
|
`,
|
||||||
|
cfg.jwtSecret,
|
||||||
|
cfg.jwtSecret, // refresh secret same as access for simplicity
|
||||||
|
cfg.bootstrapSecret,
|
||||||
|
cfg.dbPath,
|
||||||
|
cfg.adminEmail,
|
||||||
|
cfg.adminPass,
|
||||||
|
cfg.redisEnable,
|
||||||
|
cfg.redisHost,
|
||||||
|
cfg.redisPort,
|
||||||
|
cfg.redisPassword,
|
||||||
|
cfg.corsOrigin,
|
||||||
|
cfg.port,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := os.WriteFile(getEnvPath(), []byte(envContent), 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Created: %s\n", getEnvPath())
|
||||||
|
|
||||||
|
// 创建 config.yaml
|
||||||
|
configYAML := fmt.Sprintf(`server:
|
||||||
|
port: %d
|
||||||
|
mode: release
|
||||||
|
host: 0.0.0.0
|
||||||
|
|
||||||
|
database:
|
||||||
|
type: %s
|
||||||
|
dbname: "%s"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
enabled: %t
|
||||||
|
host: "%s"
|
||||||
|
port: %d
|
||||||
|
password: "%s"
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: "%s"
|
||||||
|
access_token_expire_minutes: 120
|
||||||
|
refresh_token_expire_days: 7
|
||||||
|
|
||||||
|
cors:
|
||||||
|
allowed_origins:
|
||||||
|
- "%s"
|
||||||
|
|
||||||
|
default:
|
||||||
|
admin_email: "%s"
|
||||||
|
admin_password: "%s"
|
||||||
|
`,
|
||||||
|
cfg.port,
|
||||||
|
cfg.dbType,
|
||||||
|
cfg.dbPath,
|
||||||
|
cfg.redisEnable,
|
||||||
|
cfg.redisHost,
|
||||||
|
cfg.redisPort,
|
||||||
|
cfg.redisPassword,
|
||||||
|
cfg.jwtSecret,
|
||||||
|
cfg.corsOrigin,
|
||||||
|
cfg.adminEmail,
|
||||||
|
cfg.adminPass,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := os.WriteFile(getConfigPath(), []byte(configYAML), 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Created: %s\n", getConfigPath())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrations(cfg *initConfig) error {
|
||||||
|
fmt.Println("\nRunning database migrations...")
|
||||||
|
|
||||||
|
// 创建一个临时 config 对象用于迁移
|
||||||
|
tempCfg := &config.Config{
|
||||||
|
Database: config.DatabaseConfig{
|
||||||
|
DBName: cfg.dbPath,
|
||||||
|
},
|
||||||
|
Default: config.DefaultConfig{
|
||||||
|
AdminEmail: cfg.adminEmail,
|
||||||
|
AdminPassword: cfg.adminPass,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := database.NewDB(tempCfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect database failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.AutoMigrate(tempCfg); err != nil {
|
||||||
|
return fmt.Errorf("auto migrate failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Database migrations completed.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAdminUser(cfg *initConfig) error {
|
||||||
|
fmt.Println("\nCreating admin user...")
|
||||||
|
|
||||||
|
// 验证 Redis 连接(如果启用)
|
||||||
|
if cfg.redisEnable {
|
||||||
|
addr := net.JoinHostPort(cfg.redisHost, fmt.Sprintf("%d", cfg.redisPort))
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: Redis connection failed: %v\n", err)
|
||||||
|
fmt.Println("System will run without Redis caching.")
|
||||||
|
} else {
|
||||||
|
conn.Close()
|
||||||
|
fmt.Println("Redis connection verified.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码验证已在 gatherConfig 中完成
|
||||||
|
fmt.Println("Admin user creation skipped (handled by migrations).")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
41
cmd/ums/cmd/root.go
Normal file
41
cmd/ums/cmd/root.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cfgFile string
|
||||||
|
dataDir string
|
||||||
|
)
|
||||||
|
|
||||||
|
// rootCmd is the single instance of the root command
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "ums",
|
||||||
|
Short: "UMS CLI - User Management System",
|
||||||
|
Long: `UMS CLI - Command line interface for User Management System
|
||||||
|
|
||||||
|
Supported commands:
|
||||||
|
ums init Initialize the system (interactive or with flags)
|
||||||
|
ums serve Start the UMS server
|
||||||
|
ums version Print version information`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "./config.yaml", "config file path")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&dataDir, "data-dir", "./data", "data directory")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(
|
||||||
|
newVersionCmd(),
|
||||||
|
initCmd,
|
||||||
|
serveCmd,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
48
cmd/ums/cmd/serve.go
Normal file
48
cmd/ums/cmd/serve.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/user-management-system/internal/config"
|
||||||
|
"github.com/user-management-system/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
servePort string
|
||||||
|
)
|
||||||
|
|
||||||
|
// serveCmd is the serve subcommand
|
||||||
|
var serveCmd = &cobra.Command{
|
||||||
|
Use: "serve",
|
||||||
|
Short: "Start the UMS server",
|
||||||
|
Long: `Start the User Management System HTTP server using config.yaml`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// 设置配置文件路径
|
||||||
|
viper.SetConfigFile(cfgFile)
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load config failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 允许通过 --port 覆盖配置
|
||||||
|
if servePort != "" {
|
||||||
|
var portInt int
|
||||||
|
if _, err := fmt.Sscanf(servePort, "%d", &portInt); err != nil {
|
||||||
|
return fmt.Errorf("invalid port: %w", err)
|
||||||
|
}
|
||||||
|
cfg.Server.Port = portInt
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
return server.Serve(cfg)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
serveCmd.Flags().StringVar(&servePort, "port", "", "server port (overrides config)")
|
||||||
|
rootCmd.AddCommand(serveCmd)
|
||||||
|
}
|
||||||
31
cmd/ums/cmd/version.go
Normal file
31
cmd/ums/cmd/version.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = "1.0.0"
|
||||||
|
Commit = "dev"
|
||||||
|
BuildDate = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newVersionCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Print version information",
|
||||||
|
Long: `Print the UMS version, Go version, and build information`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
fmt.Printf("UMS CLI - User Management System\n")
|
||||||
|
fmt.Printf("Version: %s\n", Version)
|
||||||
|
fmt.Printf("Commit: %s\n", Commit)
|
||||||
|
fmt.Printf("Build Date: %s\n", BuildDate)
|
||||||
|
fmt.Printf("Go Version: %s\n", runtime.Version())
|
||||||
|
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
9
cmd/ums/main.go
Normal file
9
cmd/ums/main.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/user-management-system/cmd/ums/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
||||||
5
go.mod
5
go.mod
@@ -60,6 +60,7 @@ require (
|
|||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/icholy/digest v1.1.0 // indirect
|
github.com/icholy/digest v1.1.0 // indirect
|
||||||
github.com/imroc/req/v3 v3.57.0 // indirect
|
github.com/imroc/req/v3 v3.57.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
@@ -89,7 +90,8 @@ require (
|
|||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/cobra v1.9.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.57 // indirect
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.57 // indirect
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.57 // indirect
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.57 // indirect
|
||||||
@@ -110,6 +112,7 @@ require (
|
|||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
golang.org/x/term v0.41.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
golang.org/x/tools v0.43.0 // indirect
|
golang.org/x/tools v0.43.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
|||||||
11
go.sum
11
go.sum
@@ -59,6 +59,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
@@ -171,6 +172,8 @@ github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
|||||||
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||||
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
|
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
|
||||||
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
|
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
@@ -229,6 +232,7 @@ github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGK
|
|||||||
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
@@ -244,6 +248,7 @@ github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM
|
|||||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
@@ -257,8 +262,12 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
|||||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -423,6 +432,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
|||||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||||
|
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||||
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
|||||||
261
internal/server/server.go
Normal file
261
internal/server/server.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/user-management-system/internal/api/handler"
|
||||||
|
"github.com/user-management-system/internal/api/middleware"
|
||||||
|
"github.com/user-management-system/internal/api/router"
|
||||||
|
"github.com/user-management-system/internal/auth"
|
||||||
|
"github.com/user-management-system/internal/cache"
|
||||||
|
"github.com/user-management-system/internal/config"
|
||||||
|
"github.com/user-management-system/internal/database"
|
||||||
|
"github.com/user-management-system/internal/monitoring"
|
||||||
|
"github.com/user-management-system/internal/repository"
|
||||||
|
"github.com/user-management-system/internal/security"
|
||||||
|
"github.com/user-management-system/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Serve(cfg *config.Config) error {
|
||||||
|
// 设置 Gin 模式
|
||||||
|
gin.SetMode(resolveGinMode(cfg.Server.Mode))
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
db, err := database.NewDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect database failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行数据库迁移
|
||||||
|
if err := db.AutoMigrate(cfg); err != nil {
|
||||||
|
return fmt.Errorf("auto migrate failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// P1-3:Argon2id 启动时自适应校准
|
||||||
|
auth.CalibrateArgon2id(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// 初始化 JWT 管理器
|
||||||
|
jwtManager, err := auth.NewJWTWithOptions(auth.JWTOptions{
|
||||||
|
HS256Secret: cfg.JWT.Secret,
|
||||||
|
AccessTokenExpire: time.Duration(cfg.JWT.AccessTokenExpireMinutes) * time.Minute,
|
||||||
|
RefreshTokenExpire: time.Duration(cfg.JWT.RefreshTokenExpireDays) * 24 * time.Hour,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create jwt manager failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化缓存
|
||||||
|
l1Cache := cache.NewL1Cache()
|
||||||
|
redisAddr := fmt.Sprintf("%s:%d", cfg.Redis.Host, cfg.Redis.Port)
|
||||||
|
redisEnabled := cfg.Redis.Host != "" && cache.ProbeRedis(redisAddr, cfg.Redis.Password, cfg.Redis.DB)
|
||||||
|
if !redisEnabled {
|
||||||
|
log.Printf("cache: running in memory-only mode (Redis unreachable or not configured)")
|
||||||
|
}
|
||||||
|
l2Cache := cache.NewRedisCacheWithConfig(cache.RedisCacheConfig{
|
||||||
|
Enabled: redisEnabled,
|
||||||
|
Addr: redisAddr,
|
||||||
|
Password: cfg.Redis.Password,
|
||||||
|
DB: cfg.Redis.DB,
|
||||||
|
})
|
||||||
|
defer l2Cache.Close()
|
||||||
|
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
||||||
|
|
||||||
|
// 初始化 Repository
|
||||||
|
userRepo := repository.NewUserRepository(db.DB)
|
||||||
|
roleRepo := repository.NewRoleRepository(db.DB)
|
||||||
|
permissionRepo := repository.NewPermissionRepository(db.DB)
|
||||||
|
userRoleRepo := repository.NewUserRoleRepository(db.DB)
|
||||||
|
rolePermissionRepo := repository.NewRolePermissionRepository(db.DB)
|
||||||
|
deviceRepo := repository.NewDeviceRepository(db.DB)
|
||||||
|
loginLogRepo := repository.NewLoginLogRepository(db.DB)
|
||||||
|
operationLogRepo := repository.NewOperationLogRepository(db.DB)
|
||||||
|
customFieldRepo := repository.NewCustomFieldRepository(db.DB)
|
||||||
|
userCustomFieldValueRepo := repository.NewUserCustomFieldValueRepository(db.DB)
|
||||||
|
themeRepo := repository.NewThemeConfigRepository(db.DB)
|
||||||
|
socialRepo, err := repository.NewSocialAccountRepository(db.DB)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("initialize social account repository failed: %w", err)
|
||||||
|
}
|
||||||
|
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB)
|
||||||
|
|
||||||
|
// 初始化 Service
|
||||||
|
deviceService := service.NewDeviceService(deviceRepo, userRepo)
|
||||||
|
authService := service.NewAuthService(
|
||||||
|
userRepo,
|
||||||
|
socialRepo,
|
||||||
|
jwtManager,
|
||||||
|
cacheManager,
|
||||||
|
8, // passwordMinLength
|
||||||
|
5, // maxLoginAttempts
|
||||||
|
15*time.Minute, // loginLockDuration
|
||||||
|
)
|
||||||
|
authService.SetRoleRepositories(userRoleRepo, roleRepo)
|
||||||
|
authService.SetLoginLogRepository(loginLogRepo)
|
||||||
|
authService.SetDeviceService(deviceService)
|
||||||
|
|
||||||
|
// IP 过滤中间件
|
||||||
|
var ipFilterMiddleware *middleware.IPFilterMiddleware
|
||||||
|
ipFilter := security.NewIPFilter()
|
||||||
|
if ipFilter != nil {
|
||||||
|
ipFilterMiddleware = middleware.NewIPFilterMiddleware(ipFilter, middleware.IPFilterConfig{
|
||||||
|
TrustProxy: cfg.CORS.AllowCredentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化异常检测器并注入
|
||||||
|
anomalyDetector := security.NewAnomalyDetector(security.DefaultAnomalyConfig, ipFilter)
|
||||||
|
authService.SetAnomalyDetector(anomalyDetector)
|
||||||
|
log.Println("anomaly detector initialized")
|
||||||
|
|
||||||
|
userService := service.NewUserService(userRepo, userRoleRepo, roleRepo, passwordHistoryRepo)
|
||||||
|
roleService := service.NewRoleService(roleRepo, rolePermissionRepo)
|
||||||
|
permissionService := service.NewPermissionService(permissionRepo)
|
||||||
|
loginLogService := service.NewLoginLogService(loginLogRepo)
|
||||||
|
operationLogService := service.NewOperationLogService(operationLogRepo)
|
||||||
|
captchaService := service.NewCaptchaService(cacheManager)
|
||||||
|
totpService := service.NewTOTPService(userRepo)
|
||||||
|
|
||||||
|
passwordResetConfig := service.DefaultPasswordResetConfig()
|
||||||
|
passwordResetService := service.NewPasswordResetService(userRepo, cacheManager, passwordResetConfig).
|
||||||
|
WithPasswordHistoryRepo(passwordHistoryRepo)
|
||||||
|
|
||||||
|
webhookService := service.NewWebhookService(db.DB, service.WebhookServiceConfig{
|
||||||
|
Enabled: false,
|
||||||
|
})
|
||||||
|
exportService := service.NewExportService(userRepo, roleRepo)
|
||||||
|
statsService := service.NewStatsService(userRepo, loginLogRepo)
|
||||||
|
customFieldService := service.NewCustomFieldService(customFieldRepo, userCustomFieldValueRepo)
|
||||||
|
themeService := service.NewThemeService(themeRepo)
|
||||||
|
|
||||||
|
// 设置 CORS 配置
|
||||||
|
middleware.SetCORSConfig(cfg.CORS)
|
||||||
|
|
||||||
|
// 初始化中间件
|
||||||
|
rateLimitMiddleware := middleware.NewRateLimitMiddleware(cfg.RateLimit)
|
||||||
|
authMiddleware := middleware.NewAuthMiddleware(
|
||||||
|
jwtManager,
|
||||||
|
userRepo,
|
||||||
|
userRoleRepo,
|
||||||
|
l1Cache,
|
||||||
|
)
|
||||||
|
authMiddleware.SetCacheManager(cacheManager)
|
||||||
|
|
||||||
|
opLogMiddleware := middleware.NewOperationLogMiddleware(operationLogRepo)
|
||||||
|
|
||||||
|
// 初始化 Handler
|
||||||
|
authHandler := handler.NewAuthHandler(authService)
|
||||||
|
userHandler := handler.NewUserHandler(userService)
|
||||||
|
roleHandler := handler.NewRoleHandler(roleService)
|
||||||
|
permissionHandler := handler.NewPermissionHandler(permissionService)
|
||||||
|
deviceHandler := handler.NewDeviceHandler(deviceService)
|
||||||
|
logHandler := handler.NewLogHandler(loginLogService, operationLogService)
|
||||||
|
captchaHandler := handler.NewCaptchaHandler(captchaService)
|
||||||
|
totpHandler := handler.NewTOTPHandler(authService, totpService)
|
||||||
|
webhookHandler := handler.NewWebhookHandler(webhookService)
|
||||||
|
exportHandler := handler.NewExportHandler(exportService)
|
||||||
|
statsHandler := handler.NewStatsHandler(statsService)
|
||||||
|
passwordResetHandler := handler.NewPasswordResetHandler(passwordResetService)
|
||||||
|
smsHandler := handler.NewSMSHandler(authService, nil)
|
||||||
|
avatarHandler := handler.NewAvatarHandler(userRepo)
|
||||||
|
customFieldHandler := handler.NewCustomFieldHandler(customFieldService)
|
||||||
|
themeHandler := handler.NewThemeHandler(themeService)
|
||||||
|
|
||||||
|
// 初始化 SSO 管理器
|
||||||
|
ssoManager := auth.NewSSOManager()
|
||||||
|
ssoClientsStore := auth.NewDefaultSSOClientsStore()
|
||||||
|
ssoHandler := handler.NewSSOHandler(ssoManager, ssoClientsStore)
|
||||||
|
|
||||||
|
// 系统设置服务
|
||||||
|
settingsService := service.NewSettingsService()
|
||||||
|
settingsHandler := handler.NewSettingsHandler(settingsService)
|
||||||
|
|
||||||
|
// SSO 会话清理 context(随服务器关闭而取消)
|
||||||
|
ssoCtx, ssoCancel := context.WithCancel(context.Background())
|
||||||
|
defer ssoCancel()
|
||||||
|
ssoManager.StartCleanup(ssoCtx)
|
||||||
|
|
||||||
|
// 初始化监控指标
|
||||||
|
metrics := monitoring.GetGlobalMetrics()
|
||||||
|
sloMetrics := monitoring.GetGlobalSLOMetrics()
|
||||||
|
|
||||||
|
// 启动后台 goroutine 定期采集系统指标
|
||||||
|
metricsCtx, metricsCancel := context.WithCancel(context.Background())
|
||||||
|
defer metricsCancel()
|
||||||
|
go monitoring.StartSystemMetricsCollector(metricsCtx, metrics, sloMetrics, db.DB)
|
||||||
|
|
||||||
|
// 设置路由
|
||||||
|
r := router.NewRouter(
|
||||||
|
authHandler, userHandler, roleHandler, permissionHandler, deviceHandler,
|
||||||
|
logHandler, authMiddleware, rateLimitMiddleware, opLogMiddleware,
|
||||||
|
passwordResetHandler, captchaHandler, totpHandler, webhookHandler,
|
||||||
|
ipFilterMiddleware, exportHandler, statsHandler, smsHandler, customFieldHandler, themeHandler, ssoHandler,
|
||||||
|
settingsHandler, metrics, avatarHandler,
|
||||||
|
)
|
||||||
|
engine := r.Setup()
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
healthCheck := monitoring.NewHealthCheck(db.DB)
|
||||||
|
engine.GET("/health", healthCheck.Handler)
|
||||||
|
engine.GET("/health/live", healthCheck.LivenessHandler)
|
||||||
|
engine.GET("/health/ready", healthCheck.ReadinessHandler)
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: engine,
|
||||||
|
ReadTimeout: 30 * time.Second,
|
||||||
|
WriteTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("server listening on %s", addr)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("listen failed: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 等待中断信号
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
log.Println("shutting down server...")
|
||||||
|
|
||||||
|
// 关闭 Webhook 服务
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer shutdownCancel()
|
||||||
|
if err := webhookService.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Printf("webhook service shutdown: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
return fmt.Errorf("server forced to shutdown: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("server exited")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveGinMode(mode string) string {
|
||||||
|
switch mode {
|
||||||
|
case "debug":
|
||||||
|
return gin.DebugMode
|
||||||
|
case "test":
|
||||||
|
return gin.TestMode
|
||||||
|
default:
|
||||||
|
return gin.ReleaseMode
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user