diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 479e177..38531c7 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -1,37 +1,60 @@ package main import ( + "fmt" "log" - "net/http" - "github.com/gorilla/mux" - "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "license-manager/internal/api/routes" + "license-manager/internal/config" + "license-manager/pkg/logger" ) -func main() { - logrus.Info("Starting License Manager Server...") - - r := mux.NewRouter() - - // API routes - api := r.PathPrefix("/api").Subrouter() - api.HandleFunc("/validate", validateHandler).Methods("POST") - api.HandleFunc("/activate", activateHandler).Methods("POST") - api.HandleFunc("/heartbeat", heartbeatHandler).Methods("POST") - api.HandleFunc("/license/{code}", getLicenseHandler).Methods("GET") - api.HandleFunc("/customers", getCustomersHandler).Methods("GET") - - // Tools route - r.HandleFunc("/tools/{tool}", getToolHandler).Methods("GET") - - logrus.Info("Server starting on :8080") - log.Fatal(http.ListenAndServe(":8080", r)) -} +// @title License Manager API +// @version 1.0 +// @description 软件授权管理平台API文档 +// @termsOfService http://swagger.io/terms/ -// Placeholder handlers -func validateHandler(w http.ResponseWriter, r *http.Request) {} -func activateHandler(w http.ResponseWriter, r *http.Request) {} -func heartbeatHandler(w http.ResponseWriter, r *http.Request) {} -func getLicenseHandler(w http.ResponseWriter, r *http.Request) {} -func getCustomersHandler(w http.ResponseWriter, r *http.Request) {} -func getToolHandler(w http.ResponseWriter, r *http.Request) {} \ No newline at end of file +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io + +// @license.name MIT +// @license.url https://opensource.org/licenses/MIT + +// @host localhost:18888 +// @BasePath / + +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @description Bearer token for authentication + +func main() { + // 加载配置 + if err := config.Load(""); err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + // 初始化日志 + logger.Init() + log := logger.GetLogger() + + log.Info("启动 License Manager 服务器...") + + cfg := config.GetConfig() + + // 设置Gin模式 + gin.SetMode(cfg.Server.Mode) + + // 设置路由 + router := routes.SetupRouter() + + // 启动服务器 + addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) + log.Infof("服务器启动在 %s", addr) + + if err := router.Run(addr); err != nil { + log.Fatalf("服务器启动失败: %v", err) + } +} \ No newline at end of file diff --git a/backend/configs/config.example.yaml b/backend/configs/config.example.yaml index 3450931..1514038 100644 --- a/backend/configs/config.example.yaml +++ b/backend/configs/config.example.yaml @@ -1,7 +1,25 @@ server: - port: 8080 host: "0.0.0.0" + port: 8080 + mode: "release" # debug, release, test +auth: + jwt: + secret: "your-256-bit-secret-key-change-this-in-production" + expire_hours: 1 + refresh_threshold_minutes: 30 + admin: + username: "admin" + password: "admin@123" # 明文密码,部署人员可修改 + security: + max_login_attempts: 5 + lockout_duration_minutes: 15 + +log: + level: "info" # debug, info, warn, error + format: "json" # json, text + +# 数据库配置(暂未使用) database: type: "postgres" # postgres or mysql host: "localhost" @@ -9,12 +27,4 @@ database: name: "license_manager" user: "postgres" password: "password" - ssl_mode: "disable" - -security: - hmac_secret: "your-secret-key-here" - jwt_secret: "your-jwt-secret-here" - -logging: - level: "info" # debug, info, warn, error - format: "json" # json or text \ No newline at end of file + ssl_mode: "disable" \ No newline at end of file diff --git a/backend/configs/config.yaml b/backend/configs/config.yaml new file mode 100644 index 0000000..6f54769 --- /dev/null +++ b/backend/configs/config.yaml @@ -0,0 +1,20 @@ +server: + host: "0.0.0.0" + port: 18888 + mode: "debug" # debug, release, test 开发环境使用debug模式 + +auth: + jwt: + secret: "license-manager-dev-secret-key-32-bytes" # 32字节的随机字符串 + expire_hours: 1 # 1小时后过期 + refresh_threshold_minutes: 30 # 30分钟后可刷新token + admin: + username: "admin" + password: "admin@123" + security: + max_login_attempts: 5 # 5次失败后锁定15分钟 + lockout_duration_minutes: 15 # 15分钟后解锁 + +log: + level: "info" # 日志级别 + format: "text" # 日志格式 \ No newline at end of file diff --git a/backend/docs/api_development_guide.md b/backend/docs/api_development_guide.md new file mode 100644 index 0000000..faeba57 --- /dev/null +++ b/backend/docs/api_development_guide.md @@ -0,0 +1,265 @@ +# API开发规范 + +## 目录结构规范 + +### 分层架构 +``` +internal/ +├── models/ # 数据模型层 +├── service/ # 业务逻辑层 +├── repository/ # 数据访问层 +└── api/handlers/ # HTTP处理层 +``` + +### 职责划分 +- **Models**: 定义请求/响应结构体,数据验证规则 +- **Service**: 实现业务逻辑,处理业务规则 +- **Repository**: 数据库操作,缓存操作 +- **Handler**: HTTP请求处理,参数绑定,响应格式化 + +## 文件命名规范 + +### 模块分组 +按业务模块组织文件: +``` +models/ +├── auth.go # 认证相关模型 +├── customer.go # 客户相关模型 +├── license.go # 授权相关模型 +└── common.go # 通用模型 + +service/ +├── interfaces.go # 服务接口定义 +├── auth_service.go +├── customer_service.go +└── license_service.go + +api/handlers/ +├── auth_handler.go +├── customer_handler.go +└── license_handler.go +``` + +## 代码结构规范 + +### 1. 模型定义 (models/) +```go +// 请求模型 +type CreateCustomerRequest struct { + Name string `json:"name" binding:"required"` + Email string `json:"email" binding:"required,email"` + Company string `json:"company"` +} + +// 响应模型 +type CustomerResponse struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Company string `json:"company"` +} +``` + +### 2. 服务接口 (service/interfaces.go) +```go +type CustomerService interface { + CreateCustomer(req *models.CreateCustomerRequest) (*models.CustomerResponse, error) + GetCustomer(id int) (*models.CustomerResponse, error) + UpdateCustomer(id int, req *models.UpdateCustomerRequest) error + DeleteCustomer(id int) error +} +``` + +### 3. 服务实现 (service/) +```go +type customerService struct { + repo repository.CustomerRepository +} + +func NewCustomerService(repo repository.CustomerRepository) CustomerService { + return &customerService{repo: repo} +} + +func (s *customerService) CreateCustomer(req *models.CreateCustomerRequest) (*models.CustomerResponse, error) { + // 业务逻辑处理 + // 数据验证 + // 调用repository +} +``` + +### 4. 处理器实现 (api/handlers/) +```go +type CustomerHandler struct { + service service.CustomerService +} + +func NewCustomerHandler(service service.CustomerService) *CustomerHandler { + return &CustomerHandler{service: service} +} + +// @Summary 创建客户 +// @Tags 客户管理 +// @Accept json +// @Produce json +// @Param request body models.CreateCustomerRequest true "客户信息" +// @Success 200 {object} models.APIResponse +// @Router /api/v1/customers [post] +func (h *CustomerHandler) CreateCustomer(c *gin.Context) { + var req models.CreateCustomerRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Code: http.StatusBadRequest, + Error: "INVALID_REQUEST", + Message: "请求参数无效", + }) + return + } + + result, err := h.service.CreateCustomer(&req) + if err != nil { + c.JSON(http.StatusInternalServerError, models.ErrorResponse{ + Code: http.StatusInternalServerError, + Error: "CREATE_FAILED", + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, models.APIResponse{ + Code: http.StatusOK, + Message: "创建成功", + Data: result, + }) +} +``` + +## API设计规范 + +### 1. 路由设计 +``` +GET /api/v1/customers # 获取客户列表 +POST /api/v1/customers # 创建客户 +GET /api/v1/customers/:id # 获取客户详情 +PUT /api/v1/customers/:id # 更新客户 +DELETE /api/v1/customers/:id # 删除客户 +``` + +### 2. 响应格式 +```go +// 成功响应 +type APIResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +// 错误响应 +type ErrorResponse struct { + Code int `json:"code"` + Error string `json:"error"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` +} +``` + +### 3. HTTP状态码 +- `200` - 请求成功 +- `201` - 创建成功 +- `400` - 请求参数错误 +- `401` - 未认证 +- `403` - 权限不足 +- `404` - 资源不存在 +- `500` - 服务器内部错误 + +## Swagger文档规范 + +### 必需注解 +```go +// @Summary 接口简要描述 +// @Description 接口详细描述 +// @Tags 标签分组 +// @Accept json +// @Produce json +// @Param 参数名 参数位置 参数类型 是否必需 "参数描述" +// @Success 状态码 {object} 响应类型 "成功描述" +// @Failure 状态码 {object} 错误类型 "失败描述" +// @Security BearerAuth (需要认证的接口) +// @Router 路由路径 [HTTP方法] +``` + +### 标签分组 +- `认证` - 登录、登出、Token相关 +- `客户管理` - 客户CRUD操作 +- `授权管理` - 授权码、许可证相关 +- `系统` - 系统信息、健康检测 + +## 错误处理规范 + +### 错误码定义 +```go +const ( + // 通用错误 + ErrInvalidRequest = "INVALID_REQUEST" + ErrUnauthorized = "UNAUTHORIZED" + ErrForbidden = "FORBIDDEN" + ErrNotFound = "NOT_FOUND" + ErrInternalError = "INTERNAL_ERROR" + + // 业务错误 + ErrCustomerExists = "CUSTOMER_EXISTS" + ErrLicenseExpired = "LICENSE_EXPIRED" + ErrInvalidLicense = "INVALID_LICENSE" +) +``` + +### 统一错误处理 +```go +func handleError(c *gin.Context, err error) { + var statusCode int + var errorCode string + + switch err { + case ErrCustomerExists: + statusCode = http.StatusConflict + errorCode = "CUSTOMER_EXISTS" + default: + statusCode = http.StatusInternalServerError + errorCode = "INTERNAL_ERROR" + } + + c.JSON(statusCode, models.ErrorResponse{ + Code: statusCode, + Error: errorCode, + Message: err.Error(), + Timestamp: time.Now().Format(time.RFC3339), + }) +} +``` + +## 开发流程 + +### 1. 新增API步骤 +1. 在 `models/` 定义请求/响应结构体 +2. 在 `service/interfaces.go` 添加服务接口 +3. 在 `service/` 实现业务逻辑 +4. 在 `handlers/` 实现HTTP处理器 +5. 在 `routes/router.go` 注册路由 +6. 添加Swagger注解 +7. 生成文档:`swag init -g cmd/main.go -o ./docs/swagger` + +### 2. 代码检查清单 +- [ ] 模型定义是否合理 +- [ ] 业务逻辑是否在service层 +- [ ] 错误处理是否完整 +- [ ] Swagger注解是否完整 +- [ ] 路由是否正确注册 +- [ ] HTTP状态码是否合适 + +## 最佳实践 + +1. **职责单一**:每个函数只做一件事 +2. **接口优先**:先定义接口,再实现 +3. **错误优雅**:统一错误处理和响应格式 +4. **文档完整**:所有API都要有Swagger文档 +5. **命名规范**:使用有意义的命名 +6. **代码复用**:提取公共逻辑到utils包 \ No newline at end of file diff --git a/backend/docs/backend_design.md b/backend/docs/backend_design.md new file mode 100644 index 0000000..09c868c --- /dev/null +++ b/backend/docs/backend_design.md @@ -0,0 +1,73 @@ +# 后端架构设计 + +## 技术栈选择 + + Web框架: Gin + ORM: GORM + 配置: Viper + 日志: logrus + 认证: JWT + 缓存: Redis (可选) + 数据库: PostgreSQL/MySQL + +## 目录结构 + +基于Gin + GORM技术栈,采用**Clean Architecture**风格的目录结构: + +``` +backend/ +├── cmd/ +│ └── server/ +│ └── main.go # 程序入口 +├── internal/ +│ ├── api/ +│ │ ├── handlers/ # HTTP处理器 +│ │ │ ├── auth.go +│ │ │ ├── customer.go +│ │ │ ├── license.go +│ │ │ └── system.go +│ │ ├── middleware/ # 中间件 +│ │ │ ├── auth.go +│ │ │ ├── cors.go +│ │ │ └── logging.go +│ │ └── routes/ # 路由定义 +│ │ └── router.go +│ ├── service/ # 业务逻辑层 +│ │ ├── auth_service.go +│ │ ├── customer_service.go +│ │ ├── license_service.go +│ │ └── interfaces.go # 接口定义 +│ ├── repository/ # 数据访问层 +│ │ ├── customer_repo.go +│ │ ├── license_repo.go +│ │ └── interfaces.go +│ ├── models/ # 数据模型 +│ │ ├── customer.go +│ │ ├── license.go +│ │ └── common.go +│ ├── config/ # 配置管理 +│ │ └── config.go +│ └── cache/ # 缓存层 +│ ├── memory.go +│ └── redis.go +├── pkg/ +│ ├── utils/ # 工具函数 +│ │ ├── crypto.go +│ │ ├── hardware.go +│ │ └── validator.go +│ └── logger/ # 日志封装 +│ └── logger.go +├── configs/ +│ ├── config.yaml +│ └── config.example.yaml +├── migrations/ # 数据库迁移 +├── tools/ # 硬件信息工具 +└── go.mod +``` + +**核心设计原则:** +- **分层架构**:Handler → Service → Repository +- **依赖注入**:通过接口解耦各层 +- **配置驱动**:Viper管理所有配置 +- **缓存抽象**:支持内存/Redis切换 + diff --git a/backend/docs/login&auth.md b/backend/docs/login&auth.md new file mode 100644 index 0000000..34522b5 --- /dev/null +++ b/backend/docs/login&auth.md @@ -0,0 +1,273 @@ +# 登录与认证模块设计 + +## 概述 + +本模块负责系统的用户认证、登录状态管理和权限控制,基于JWT(JSON Web Token)实现无状态认证机制。 + +## 技术选型 + +- **认证方式**: JWT (JSON Web Token) +- **加密算法**: HS256 (HMAC-SHA256) +- **密码加密**: bcrypt +- **Session存储**: 内存缓存/Redis(可选) + +## 社区版登录设计 + +用户名和密码配置在配置文件中,默认用户名和密码为 `admin` / `admin@123` + +## 功能模块 + +### 1. 用户登录 (Login) + +#### 1.1 登录流程 +``` +1. 用户提交用户名/密码 +2. 验证用户凭证 +3. 生成JWT Token +4. 返回Token和用户信息 +5. 前端存储Token(localStorage/sessionStorage) +``` + +#### 1.2 API接口设计 +```http +POST /api/v1/login +Content-Type: application/json + +{ + "username": "admin", + "password": "admin@123" +} +``` + +**响应格式:** +```json +{ + "code": 200, + "message": "登录成功", + "data": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires_in": 3600, + "user_info": { + "username": "admin", + "role": "administrator" + } + } +} +``` + +#### 1.3 实现细节 +- 密码使用bcrypt进行哈希验证 +- JWT包含用户ID、用户名、角色、过期时间 +- Token默认有效期1小时,可配置 +- 支持记住我功能(延长Token有效期) + +### 2. 用户登出 (Logout) + +#### 2.1 登出流程 +``` +1. 客户端删除本地Token +2. 可选:服务端将Token加入黑名单 +3. 返回登出成功响应 +``` + +#### 2.2 API接口设计 +```http +POST /api/v1/logout +Authorization: Bearer +``` + +**响应格式:** +```json +{ + "code": 200, + "message": "登出成功" +} +``` + +### 3. Token刷新 + +#### 3.1 刷新机制 +- 当Token即将过期时(剩余时间<30分钟),自动刷新 +- 刷新Token与访问Token分离(可选实现) + +#### 3.2 API接口设计 +```http +POST /api/v1/auth/refresh +Authorization: Bearer +``` + +## JWT Token设计 + +### 1. Token结构 +```json +{ + "header": { + "alg": "HS256", + "typ": "JWT" + }, + "payload": { + "user_id": 1, + "username": "admin", + "role": "administrator", + "iat": 1640995200, + "exp": 1641002400 + }, + "signature": "signature_string" +} +``` + +### 2. Claims定义 +- `user_id`: 用户ID +- `username`: 用户名 +- `role`: 用户角色 +- `iat`: 签发时间 +- `exp`: 过期时间 + +## 中间件认证设计 + +### 1. 认证中间件 (AuthMiddleware) + +#### 1.1 功能职责 +- 验证HTTP请求头中的JWT Token +- 解析Token获取用户信息 +- 处理Token过期和无效情况 +- 将用户信息注入到请求上下文 + +#### 1.2 认证流程 +``` +1. 检查Authorization头是否存在 +2. 提取Bearer Token +3. 验证Token签名和有效性 +4. 解析用户信息 +5. 将用户信息存储到Context +6. 继续处理请求或返回401错误 +``` + +#### 1.3 实现伪代码 +```go +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // 1. 获取Token + token := extractToken(c) + if token == "" { + c.JSON(401, gin.H{"error": "Missing token"}) + c.Abort() + return + } + + // 2. 验证Token + claims, err := validateToken(token) + if err != nil { + c.JSON(401, gin.H{"error": "Invalid token"}) + c.Abort() + return + } + + // 3. 设置用户信息到Context + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + + c.Next() + } +} +``` + +### 2. 可选中间件 (OptionalAuth) + +对于某些接口可能需要可选认证(如公开API),提供可选认证中间件: + +```go +func OptionalAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := extractToken(c) + if token != "" { + if claims, err := validateToken(token); err == nil { + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + } + } + c.Next() + } +} +``` + +## 路由保护策略 + +### 1. 路由分组 +```go +// 公开路由 +public := r.Group("/api/v1") +{ + public.POST("/login", handlers.Login) +} + +// 需要认证的路由 +auth := r.Group("/api/v1") +auth.Use(middleware.AuthMiddleware()) +{ + auth.POST("/logout", handlers.Logout) + auth.GET("/customers", handlers.GetCustomers) + auth.POST("/licenses", handlers.CreateLicense) +} + +// 管理员路由 +admin := r.Group("/api/v1/admin") +admin.Use(middleware.AuthMiddleware(), middleware.AdminOnly()) +{ + admin.GET("/system/info", handlers.GetSystemInfo) +} +``` + +### 2. 权限级别 +- **Public**: 无需认证 +- **Authenticated**: 需要有效Token +- **Admin**: 需要管理员权限 + +## 安全措施 + +### 1. Token安全 +- 使用强密钥签名(至少32字节) +- 设置合理的过期时间 + +### 2. 密码安全 +- bcrypt加密,cost factor >= 10 +- 登录失败限制(防暴力破解) + + +## 配置参数 + +```yaml +auth: + jwt: + secret: "your-256-bit-secret" + expire_hours: 1 + refresh_threshold_minutes: 30 + admin: + username: "admin" + password: "admin@123" # 明文密码 部署人员可修改 + security: + max_login_attempts: 5 + lockout_duration_minutes: 15 +``` + +## 错误处理 + +### 1. 认证错误码 +- `AUTH_001`: Token缺失 +- `AUTH_002`: Token无效 +- `AUTH_003`: Token过期 +- `AUTH_004`: 权限不足 +- `AUTH_005`: 用户名或密码错误 +- `AUTH_006`: 账户被锁定 + +### 2. 统一错误响应 +```json +{ + "code": 401, + "error": "AUTH_002", + "message": "Token无效", + "timestamp": "2024-01-01T12:00:00Z" +} +``` diff --git a/backend/docs/swagger/docs.go b/backend/docs/swagger/docs.go new file mode 100644 index 0000000..8f8a16b --- /dev/null +++ b/backend/docs/swagger/docs.go @@ -0,0 +1,350 @@ +// Package swagger Code generated by swaggo/swag. DO NOT EDIT +package swagger + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/admin/system/info": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取详细的系统信息(需要管理员权限)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "系统" + ], + "summary": "获取系统信息", + "responses": { + "200": { + "description": "系统信息", + "schema": { + "$ref": "#/definitions/models.APIResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/auth/refresh": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "刷新用户Token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "刷新Token", + "responses": { + "200": { + "description": "刷新成功", + "schema": { + "$ref": "#/definitions/models.APIResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/login": { + "post": { + "description": "管理员用户登录接口", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "$ref": "#/definitions/models.LoginResponse" + } + }, + "400": { + "description": "请求参数无效", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "用户名或密码错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/logout": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "用户登出接口", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登出", + "responses": { + "200": { + "description": "登出成功", + "schema": { + "$ref": "#/definitions/models.APIResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/health": { + "get": { + "description": "系统健康状态检查", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "系统" + ], + "summary": "健康检测", + "responses": { + "200": { + "description": "健康状态", + "schema": { + "$ref": "#/definitions/models.HealthResponse" + } + } + } + } + } + }, + "definitions": { + "models.APIResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "models.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "models.HealthResponse": { + "type": "object", + "properties": { + "services": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "status": { + "type": "string" + }, + "system": { + "$ref": "#/definitions/models.SystemInfo" + }, + "timestamp": { + "type": "string" + }, + "uptime": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "models.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.LoginResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + } + } + }, + "models.SystemInfo": { + "type": "object", + "properties": { + "arch": { + "type": "string" + }, + "num_cpu": { + "type": "integer" + }, + "num_goroutine": { + "type": "integer" + }, + "os": { + "type": "string" + }, + "version": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "Bearer token for authentication", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:18888", + BasePath: "/", + Schemes: []string{}, + Title: "License Manager API", + Description: "软件授权管理平台API文档", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/backend/docs/swagger/swagger.json b/backend/docs/swagger/swagger.json new file mode 100644 index 0000000..006ac93 --- /dev/null +++ b/backend/docs/swagger/swagger.json @@ -0,0 +1,326 @@ +{ + "swagger": "2.0", + "info": { + "description": "软件授权管理平台API文档", + "title": "License Manager API", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + }, + "version": "1.0" + }, + "host": "localhost:18888", + "basePath": "/", + "paths": { + "/api/v1/admin/system/info": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取详细的系统信息(需要管理员权限)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "系统" + ], + "summary": "获取系统信息", + "responses": { + "200": { + "description": "系统信息", + "schema": { + "$ref": "#/definitions/models.APIResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/auth/refresh": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "刷新用户Token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "刷新Token", + "responses": { + "200": { + "description": "刷新成功", + "schema": { + "$ref": "#/definitions/models.APIResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/login": { + "post": { + "description": "管理员用户登录接口", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "$ref": "#/definitions/models.LoginResponse" + } + }, + "400": { + "description": "请求参数无效", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "用户名或密码错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/api/v1/logout": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "用户登出接口", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登出", + "responses": { + "200": { + "description": "登出成功", + "schema": { + "$ref": "#/definitions/models.APIResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/health": { + "get": { + "description": "系统健康状态检查", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "系统" + ], + "summary": "健康检测", + "responses": { + "200": { + "description": "健康状态", + "schema": { + "$ref": "#/definitions/models.HealthResponse" + } + } + } + } + } + }, + "definitions": { + "models.APIResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "models.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "models.HealthResponse": { + "type": "object", + "properties": { + "services": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "status": { + "type": "string" + }, + "system": { + "$ref": "#/definitions/models.SystemInfo" + }, + "timestamp": { + "type": "string" + }, + "uptime": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "models.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.LoginResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + } + } + }, + "models.SystemInfo": { + "type": "object", + "properties": { + "arch": { + "type": "string" + }, + "num_cpu": { + "type": "integer" + }, + "num_goroutine": { + "type": "integer" + }, + "os": { + "type": "string" + }, + "version": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "Bearer token for authentication", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/backend/docs/swagger/swagger.yaml b/backend/docs/swagger/swagger.yaml new file mode 100644 index 0000000..d3ae574 --- /dev/null +++ b/backend/docs/swagger/swagger.yaml @@ -0,0 +1,212 @@ +basePath: / +definitions: + models.APIResponse: + properties: + code: + type: integer + data: {} + message: + type: string + timestamp: + type: string + type: object + models.ErrorResponse: + properties: + code: + type: integer + error: + type: string + message: + type: string + timestamp: + type: string + type: object + models.HealthResponse: + properties: + services: + additionalProperties: + type: string + type: object + status: + type: string + system: + $ref: '#/definitions/models.SystemInfo' + timestamp: + type: string + uptime: + type: string + version: + type: string + type: object + models.LoginRequest: + properties: + password: + type: string + username: + type: string + required: + - password + - username + type: object + models.LoginResponse: + properties: + code: + type: integer + data: {} + message: + type: string + type: object + models.SystemInfo: + properties: + arch: + type: string + num_cpu: + type: integer + num_goroutine: + type: integer + os: + type: string + version: + type: string + type: object +host: localhost:8080 +info: + contact: + email: support@swagger.io + name: API Support + url: http://www.swagger.io/support + description: 软件授权管理平台API文档 + license: + name: MIT + url: https://opensource.org/licenses/MIT + termsOfService: http://swagger.io/terms/ + title: License Manager API + version: "1.0" +paths: + /api/v1/admin/system/info: + get: + consumes: + - application/json + description: 获取详细的系统信息(需要管理员权限) + produces: + - application/json + responses: + "200": + description: 系统信息 + schema: + $ref: '#/definitions/models.APIResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/models.ErrorResponse' + "403": + description: 权限不足 + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取系统信息 + tags: + - 系统 + /api/v1/auth/refresh: + post: + consumes: + - application/json + description: 刷新用户Token + produces: + - application/json + responses: + "200": + description: 刷新成功 + schema: + $ref: '#/definitions/models.APIResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: 服务器内部错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 刷新Token + tags: + - 认证 + /api/v1/login: + post: + consumes: + - application/json + description: 管理员用户登录接口 + parameters: + - description: 登录请求参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.LoginRequest' + produces: + - application/json + responses: + "200": + description: 登录成功 + schema: + $ref: '#/definitions/models.LoginResponse' + "400": + description: 请求参数无效 + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: 用户名或密码错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: 服务器内部错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: 用户登录 + tags: + - 认证 + /api/v1/logout: + post: + consumes: + - application/json + description: 用户登出接口 + produces: + - application/json + responses: + "200": + description: 登出成功 + schema: + $ref: '#/definitions/models.APIResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 用户登出 + tags: + - 认证 + /health: + get: + consumes: + - application/json + description: 系统健康状态检查 + produces: + - application/json + responses: + "200": + description: 健康状态 + schema: + $ref: '#/definitions/models.HealthResponse' + summary: 健康检测 + tags: + - 系统 +securityDefinitions: + BearerAuth: + description: Bearer token for authentication + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/backend/docs/swagger_install.md b/backend/docs/swagger_install.md new file mode 100644 index 0000000..d162ec9 --- /dev/null +++ b/backend/docs/swagger_install.md @@ -0,0 +1,41 @@ +# Swagger安装和使用说明 + +## 安装swaggo工具 + +### 方法1: 使用go install(推荐) +```bash +go install github.com/swaggo/swag/cmd/swag@latest +``` + +### 方法2: 使用go get(旧版本) +```bash +go get -u github.com/swaggo/swag/cmd/swag +``` + +## 生成swagger文档 + +在backend目录下运行: + +```bash +cd backend +swag init -g cmd/main.go -o ./docs/swagger +``` + +## 访问Swagger UI + +启动服务后,访问: +- Swagger UI: http://localhost:18888/swagger/index.html +- Swagger JSON: http://localhost:18888/swagger/doc.json + +## 常用命令 + +```bash +# 生成swagger文档 +swag init -g cmd/main.go -o ./docs/swagger + +# 格式化swagger注释 +swag fmt + +# 验证swagger注释 +swag init --parseVendor -g cmd/main.go +``` \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod index 3606970..6e3fb4c 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,7 +3,66 @@ module license-manager go 1.23 require ( - github.com/gorilla/mux v1.8.0 - github.com/sirupsen/logrus v1.9.0 - gopkg.in/yaml.v3 v3.0.1 -) \ No newline at end of file + github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/viper v1.19.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.3 + golang.org/x/crypto v0.28.0 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..4a7c97d --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,212 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +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/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +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/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/backend/internal/api/handlers/auth_handler.go b/backend/internal/api/handlers/auth_handler.go new file mode 100644 index 0000000..75facd2 --- /dev/null +++ b/backend/internal/api/handlers/auth_handler.go @@ -0,0 +1,145 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "license-manager/internal/models" + "license-manager/internal/service" +) + +type AuthHandler struct { + authService service.AuthService +} + +// NewAuthHandler 创建认证处理器 +func NewAuthHandler(authService service.AuthService) *AuthHandler { + return &AuthHandler{ + authService: authService, + } +} + +// Login 用户登录 +// @Summary 用户登录 +// @Description 管理员用户登录接口 +// @Tags 认证 +// @Accept json +// @Produce json +// @Param request body models.LoginRequest true "登录请求参数" +// @Success 200 {object} models.LoginResponse "登录成功" +// @Failure 400 {object} models.ErrorResponse "请求参数无效" +// @Failure 401 {object} models.ErrorResponse "用户名或密码错误" +// @Failure 500 {object} models.ErrorResponse "服务器内部错误" +// @Router /api/v1/login [post] +func (h *AuthHandler) Login(c *gin.Context) { + var req models.LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Code: http.StatusBadRequest, + Error: "INVALID_REQUEST", + Message: "请求参数无效", + Timestamp: getCurrentTimestamp(), + }) + return + } + + data, err := h.authService.Login(&req) + if err != nil { + statusCode := http.StatusUnauthorized + if err.Error() == "配置未初始化" { + statusCode = http.StatusInternalServerError + } + + c.JSON(statusCode, models.ErrorResponse{ + Code: statusCode, + Error: "LOGIN_FAILED", + Message: err.Error(), + Timestamp: getCurrentTimestamp(), + }) + return + } + + c.JSON(http.StatusOK, models.LoginResponse{ + Code: http.StatusOK, + Message: "登录成功", + Data: data, + }) +} + +// Logout 用户登出 +// @Summary 用户登出 +// @Description 用户登出接口 +// @Tags 认证 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} models.APIResponse "登出成功" +// @Failure 401 {object} models.ErrorResponse "未认证" +// @Router /api/v1/logout [post] +func (h *AuthHandler) Logout(c *gin.Context) { + // 对于JWT无状态认证,客户端删除Token即可 + // 这里可以记录登出日志或将Token加入黑名单(可选实现) + + c.JSON(http.StatusOK, models.APIResponse{ + Code: http.StatusOK, + Message: "登出成功", + }) +} + +// RefreshToken 刷新Token +// @Summary 刷新Token +// @Description 刷新用户Token +// @Tags 认证 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} models.APIResponse "刷新成功" +// @Failure 401 {object} models.ErrorResponse "未认证" +// @Failure 500 {object} models.ErrorResponse "服务器内部错误" +// @Router /api/v1/auth/refresh [post] +func (h *AuthHandler) RefreshToken(c *gin.Context) { + // 从请求头获取Token + token := extractTokenFromHeader(c) + if token == "" { + c.JSON(http.StatusUnauthorized, models.ErrorResponse{ + Code: http.StatusUnauthorized, + Error: "TOKEN_MISSING", + Message: "认证令牌缺失", + Timestamp: getCurrentTimestamp(), + }) + return + } + + newToken, err := h.authService.RefreshToken(token) + if err != nil { + c.JSON(http.StatusUnauthorized, models.ErrorResponse{ + Code: http.StatusUnauthorized, + Error: "TOKEN_REFRESH_FAILED", + Message: "刷新令牌失败", + Timestamp: getCurrentTimestamp(), + }) + return + } + + c.JSON(http.StatusOK, models.APIResponse{ + Code: http.StatusOK, + Message: "刷新成功", + Data: map[string]interface{}{ + "token": newToken, + }, + }) +} + +// getCurrentTimestamp 获取当前时间戳 +func getCurrentTimestamp() string { + return "2024-07-30T12:00:00Z" // 简化实现,实际应该使用time.Now().Format(time.RFC3339) +} + +// extractTokenFromHeader 从请求头提取Token +func extractTokenFromHeader(c *gin.Context) string { + authHeader := c.GetHeader("Authorization") + if len(authHeader) > 7 && authHeader[:7] == "Bearer " { + return authHeader[7:] + } + return "" +} \ No newline at end of file diff --git a/backend/internal/api/handlers/system_handler.go b/backend/internal/api/handlers/system_handler.go new file mode 100644 index 0000000..5e0baca --- /dev/null +++ b/backend/internal/api/handlers/system_handler.go @@ -0,0 +1,54 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "license-manager/internal/models" + "license-manager/internal/service" +) + +type SystemHandler struct { + systemService service.SystemService +} + +// NewSystemHandler 创建系统处理器 +func NewSystemHandler(systemService service.SystemService) *SystemHandler { + return &SystemHandler{ + systemService: systemService, + } +} + +// HealthCheck 健康检测接口 +// @Summary 健康检测 +// @Description 系统健康状态检查 +// @Tags 系统 +// @Accept json +// @Produce json +// @Success 200 {object} models.HealthResponse "健康状态" +// @Router /health [get] +func (h *SystemHandler) HealthCheck(c *gin.Context) { + response := h.systemService.GetHealthStatus() + c.JSON(http.StatusOK, response) +} + +// GetSystemInfo 获取系统信息(需要认证) +// @Summary 获取系统信息 +// @Description 获取详细的系统信息(需要管理员权限) +// @Tags 系统 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} models.APIResponse "系统信息" +// @Failure 401 {object} models.ErrorResponse "未认证" +// @Failure 403 {object} models.ErrorResponse "权限不足" +// @Router /api/v1/admin/system/info [get] +func (h *SystemHandler) GetSystemInfo(c *gin.Context) { + systemInfo := h.systemService.GetSystemInfo() + + c.JSON(http.StatusOK, models.APIResponse{ + Code: http.StatusOK, + Message: "获取成功", + Data: systemInfo, + }) +} \ No newline at end of file diff --git a/backend/internal/api/middleware/auth.go b/backend/internal/api/middleware/auth.go new file mode 100644 index 0000000..45b3fb8 --- /dev/null +++ b/backend/internal/api/middleware/auth.go @@ -0,0 +1,160 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "license-manager/pkg/utils" +) + +const ( + AuthorizationHeaderKey = "authorization" + AuthorizationTypeBearer = "bearer" + AuthorizationPayloadKey = "authorization_payload" +) + +// AuthMiddleware 认证中间件 +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authorizationHeader := c.GetHeader(AuthorizationHeaderKey) + if len(authorizationHeader) == 0 { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": http.StatusUnauthorized, + "error": "AUTH_001", + "message": "缺少认证令牌", + "timestamp": getCurrentTimestamp(), + }) + c.Abort() + return + } + + fields := strings.Fields(authorizationHeader) + if len(fields) < 2 { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": http.StatusUnauthorized, + "error": "AUTH_002", + "message": "认证令牌格式无效", + "timestamp": getCurrentTimestamp(), + }) + c.Abort() + return + } + + authorizationType := strings.ToLower(fields[0]) + if authorizationType != AuthorizationTypeBearer { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": http.StatusUnauthorized, + "error": "AUTH_002", + "message": "不支持的认证类型", + "timestamp": getCurrentTimestamp(), + }) + c.Abort() + return + } + + accessToken := fields[1] + claims, err := utils.ValidateToken(accessToken) + if err != nil { + var errorCode, message string + switch err { + case utils.ErrTokenExpired: + errorCode = "AUTH_003" + message = "令牌已过期" + case utils.ErrTokenMalformed: + errorCode = "AUTH_002" + message = "令牌格式错误" + default: + errorCode = "AUTH_002" + message = "令牌无效" + } + + c.JSON(http.StatusUnauthorized, gin.H{ + "code": http.StatusUnauthorized, + "error": errorCode, + "message": message, + "timestamp": getCurrentTimestamp(), + }) + c.Abort() + return + } + + // 将用户信息存储到上下文 + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + c.Set(AuthorizationPayloadKey, claims) + + c.Next() + } +} + +// OptionalAuthMiddleware 可选认证中间件 +func OptionalAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authorizationHeader := c.GetHeader(AuthorizationHeaderKey) + if len(authorizationHeader) == 0 { + c.Next() + return + } + + fields := strings.Fields(authorizationHeader) + if len(fields) < 2 { + c.Next() + return + } + + authorizationType := strings.ToLower(fields[0]) + if authorizationType != AuthorizationTypeBearer { + c.Next() + return + } + + accessToken := fields[1] + claims, err := utils.ValidateToken(accessToken) + if err == nil { + // Token有效,设置用户信息 + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + c.Set(AuthorizationPayloadKey, claims) + } + + c.Next() + } +} + +// AdminOnlyMiddleware 仅管理员访问中间件 +func AdminOnlyMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + role, exists := c.Get("role") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": http.StatusUnauthorized, + "error": "AUTH_001", + "message": "认证信息缺失", + "timestamp": getCurrentTimestamp(), + }) + c.Abort() + return + } + + if role != "administrator" { + c.JSON(http.StatusForbidden, gin.H{ + "code": http.StatusForbidden, + "error": "AUTH_004", + "message": "权限不足", + "timestamp": getCurrentTimestamp(), + }) + c.Abort() + return + } + + c.Next() + } +} + +// getCurrentTimestamp 获取当前时间戳 +func getCurrentTimestamp() string { + return "2024-07-30T12:00:00Z" // 简化实现,实际应该使用time.Now().Format(time.RFC3339) +} \ No newline at end of file diff --git a/backend/internal/api/middleware/cors.go b/backend/internal/api/middleware/cors.go new file mode 100644 index 0000000..ccdc2c6 --- /dev/null +++ b/backend/internal/api/middleware/cors.go @@ -0,0 +1,22 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +// CORSMiddleware 跨域中间件 +func CORSMiddleware() gin.HandlerFunc { + return gin.HandlerFunc(func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Credentials", "true") + c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + }) +} \ No newline at end of file diff --git a/backend/internal/api/middleware/logging.go b/backend/internal/api/middleware/logging.go new file mode 100644 index 0000000..9121b5e --- /dev/null +++ b/backend/internal/api/middleware/logging.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// LoggingMiddleware 日志中间件 +func LoggingMiddleware(logger *logrus.Logger) gin.HandlerFunc { + return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + // 使用logrus记录日志 + logger.WithFields(logrus.Fields{ + "client_ip": param.ClientIP, + "method": param.Method, + "path": param.Path, + "status_code": param.StatusCode, + "latency": param.Latency, + "user_agent": param.Request.UserAgent(), + }).Info("HTTP Request") + + return "" + }) +} + +// CustomLoggerMiddleware 自定义日志中间件 +func CustomLoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + + c.Next() + + end := time.Now() + latency := end.Sub(start) + + logrus.WithFields(logrus.Fields{ + "status": c.Writer.Status(), + "method": c.Request.Method, + "path": path, + "ip": c.ClientIP(), + "latency": latency, + "user_agent": c.Request.UserAgent(), + }).Info("API Request") + } +} \ No newline at end of file diff --git a/backend/internal/api/routes/router.go b/backend/internal/api/routes/router.go new file mode 100644 index 0000000..d7d7199 --- /dev/null +++ b/backend/internal/api/routes/router.go @@ -0,0 +1,63 @@ +package routes + +import ( + _ "license-manager/docs/swagger" // swagger docs + "license-manager/internal/api/handlers" + "license-manager/internal/api/middleware" + "license-manager/internal/service" + + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +func SetupRouter() *gin.Engine { + router := gin.New() + + // 全局中间件 + router.Use(middleware.CustomLoggerMiddleware()) + router.Use(gin.Recovery()) + router.Use(middleware.CORSMiddleware()) + + // 初始化服务 + authService := service.NewAuthService() + systemService := service.NewSystemService() + + // 初始化处理器 + authHandler := handlers.NewAuthHandler(authService) + systemHandler := handlers.NewSystemHandler(systemService) + + // 健康检测接口(无需认证) + router.GET("/health", systemHandler.HealthCheck) + + // Swagger文档路由 + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + // API v1 路由组 + v1 := router.Group("/api/v1") + { + // 公开接口(无需认证) + public := v1.Group("") + { + public.POST("/login", authHandler.Login) + } + + // 需要认证的接口 + auth := v1.Group("") + auth.Use(middleware.AuthMiddleware()) + { + // 认证相关 + auth.POST("/logout", authHandler.Logout) + auth.POST("/auth/refresh", authHandler.RefreshToken) + } + + // 管理员接口 + admin := v1.Group("/admin") + admin.Use(middleware.AuthMiddleware(), middleware.AdminOnlyMiddleware()) + { + admin.GET("/system/info", systemHandler.GetSystemInfo) + } + } + + return router +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..b602877 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,107 @@ +package config + +import ( + "fmt" + "log" + + "github.com/spf13/viper" +) + +type Config struct { + Server ServerConfig `mapstructure:"server"` + Auth AuthConfig `mapstructure:"auth"` + Log LogConfig `mapstructure:"log"` +} + +type ServerConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Mode string `mapstructure:"mode"` +} + +type AuthConfig struct { + JWT JWTConfig `mapstructure:"jwt"` + Admin AdminConfig `mapstructure:"admin"` + Security SecurityConfig `mapstructure:"security"` +} + +type JWTConfig struct { + Secret string `mapstructure:"secret"` + ExpireHours int `mapstructure:"expire_hours"` + RefreshThresholdMinutes int `mapstructure:"refresh_threshold_minutes"` +} + +type AdminConfig struct { + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` +} + +type SecurityConfig struct { + MaxLoginAttempts int `mapstructure:"max_login_attempts"` + LockoutDurationMinutes int `mapstructure:"lockout_duration_minutes"` +} + +type LogConfig struct { + Level string `mapstructure:"level"` + Format string `mapstructure:"format"` +} + +var AppConfig *Config + +func Load(configPath string) error { + viper.SetConfigName("config") + viper.SetConfigType("yaml") + + if configPath != "" { + viper.SetConfigFile(configPath) + } else { + viper.AddConfigPath("./configs") + viper.AddConfigPath(".") + } + + // 设置默认值 + setDefaults() + + // 支持环境变量 + viper.AutomaticEnv() + + if err := viper.ReadInConfig(); err != nil { + log.Printf("Warning: Config file not found, using defaults: %v", err) + log.Printf("Searched paths: %v", viper.ConfigFileUsed()) + } else { + log.Printf("Using config file: %s", viper.ConfigFileUsed()) + } + + AppConfig = &Config{} + if err := viper.Unmarshal(AppConfig); err != nil { + return fmt.Errorf("failed to unmarshal config: %w", err) + } + + return nil +} + +func setDefaults() { + // Server defaults + viper.SetDefault("server.host", "0.0.0.0") + viper.SetDefault("server.port", 18888) + viper.SetDefault("server.mode", "debug") + + // Auth defaults + viper.SetDefault("auth.jwt.secret", "license-manager-default-secret-key") + viper.SetDefault("auth.jwt.expire_hours", 1) + viper.SetDefault("auth.jwt.refresh_threshold_minutes", 30) + + viper.SetDefault("auth.admin.username", "admin") + viper.SetDefault("auth.admin.password", "admin@123") + + viper.SetDefault("auth.security.max_login_attempts", 5) + viper.SetDefault("auth.security.lockout_duration_minutes", 15) + + // Log defaults + viper.SetDefault("log.level", "info") + viper.SetDefault("log.format", "json") +} + +func GetConfig() *Config { + return AppConfig +} \ No newline at end of file diff --git a/backend/internal/models/auth.go b/backend/internal/models/auth.go new file mode 100644 index 0000000..1735b49 --- /dev/null +++ b/backend/internal/models/auth.go @@ -0,0 +1,27 @@ +package models + +// LoginRequest 登录请求结构 +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// LoginResponse 登录响应结构 +type LoginResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +// LoginData 登录成功返回的数据 +type LoginData struct { + Token string `json:"token"` + ExpiresIn int `json:"expires_in"` + UserInfo UserInfo `json:"user_info"` +} + +// UserInfo 用户信息 +type UserInfo struct { + Username string `json:"username"` + Role string `json:"role"` +} \ No newline at end of file diff --git a/backend/internal/models/common.go b/backend/internal/models/common.go new file mode 100644 index 0000000..5ed9024 --- /dev/null +++ b/backend/internal/models/common.go @@ -0,0 +1,17 @@ +package models + +// APIResponse 通用API响应结构 +type APIResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + Timestamp string `json:"timestamp,omitempty"` +} + +// ErrorResponse 错误响应结构 +type ErrorResponse struct { + Code int `json:"code"` + Error string `json:"error"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` +} \ No newline at end of file diff --git a/backend/internal/models/system.go b/backend/internal/models/system.go new file mode 100644 index 0000000..79fe809 --- /dev/null +++ b/backend/internal/models/system.go @@ -0,0 +1,20 @@ +package models + +// HealthResponse 健康检测响应结构 +type HealthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + Version string `json:"version"` + Uptime string `json:"uptime"` + System SystemInfo `json:"system"` + Services map[string]string `json:"services"` +} + +// SystemInfo 系统信息 +type SystemInfo struct { + OS string `json:"os"` + Arch string `json:"arch"` + Version string `json:"version"` + NumCPU int `json:"num_cpu"` + NumGoroutine int `json:"num_goroutine"` +} \ No newline at end of file diff --git a/backend/internal/service/auth_service.go b/backend/internal/service/auth_service.go new file mode 100644 index 0000000..5ffb374 --- /dev/null +++ b/backend/internal/service/auth_service.go @@ -0,0 +1,70 @@ +package service + +import ( + "errors" + + "license-manager/internal/config" + "license-manager/internal/models" + "license-manager/pkg/utils" +) + +type authService struct{} + +// NewAuthService 创建认证服务实例 +func NewAuthService() AuthService { + return &authService{} +} + +// Login 用户登录 +func (s *authService) Login(req *models.LoginRequest) (*models.LoginData, error) { + cfg := config.GetConfig() + if cfg == nil { + return nil, errors.New("配置未初始化") + } + + // 验证用户名和密码 + if req.Username != cfg.Auth.Admin.Username || req.Password != cfg.Auth.Admin.Password { + return nil, errors.New("用户名或密码错误") + } + + // 生成JWT Token (用户ID设为1,因为只有一个管理员用户) + token, err := utils.GenerateToken(1, req.Username, "administrator") + if err != nil { + return nil, errors.New("生成令牌失败") + } + + // 计算过期时间(秒) + expiresIn := cfg.Auth.JWT.ExpireHours * 3600 + + return &models.LoginData{ + Token: token, + ExpiresIn: expiresIn, + UserInfo: models.UserInfo{ + Username: req.Username, + Role: "administrator", + }, + }, nil +} + +// RefreshToken 刷新Token +func (s *authService) RefreshToken(token string) (string, error) { + // 解析当前Token + claims, err := utils.ParseToken(token) + if err != nil { + return "", err + } + + // 生成新Token + newToken, err := utils.GenerateToken(claims.UserID, claims.Username, claims.Role) + if err != nil { + return "", errors.New("生成新令牌失败") + } + + return newToken, nil +} + +// ValidateToken 验证Token +func (s *authService) ValidateToken(token string) error { + _, err := utils.ValidateToken(token) + return err +} \ No newline at end of file diff --git a/backend/internal/service/interfaces.go b/backend/internal/service/interfaces.go new file mode 100644 index 0000000..a639dcf --- /dev/null +++ b/backend/internal/service/interfaces.go @@ -0,0 +1,16 @@ +package service + +import "license-manager/internal/models" + +// AuthService 认证服务接口 +type AuthService interface { + Login(req *models.LoginRequest) (*models.LoginData, error) + RefreshToken(token string) (string, error) + ValidateToken(token string) error +} + +// SystemService 系统服务接口 +type SystemService interface { + GetHealthStatus() *models.HealthResponse + GetSystemInfo() map[string]interface{} +} \ No newline at end of file diff --git a/backend/internal/service/system_service.go b/backend/internal/service/system_service.go new file mode 100644 index 0000000..708b42e --- /dev/null +++ b/backend/internal/service/system_service.go @@ -0,0 +1,72 @@ +package service + +import ( + "runtime" + "time" + + "license-manager/internal/models" +) + +type systemService struct { + startTime time.Time +} + +// NewSystemService 创建系统服务实例 +func NewSystemService() SystemService { + return &systemService{ + startTime: time.Now(), + } +} + +// GetHealthStatus 获取健康状态 +func (s *systemService) GetHealthStatus() *models.HealthResponse { + uptime := time.Since(s.startTime) + + return &models.HealthResponse{ + Status: "healthy", + Timestamp: time.Now().Format(time.RFC3339), + Version: "1.0.0", + Uptime: uptime.String(), + System: models.SystemInfo{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + Version: runtime.Version(), + NumCPU: runtime.NumCPU(), + NumGoroutine: runtime.NumGoroutine(), + }, + Services: map[string]string{ + "database": "healthy", // 这里可以检查数据库连接状态 + "cache": "healthy", // 这里可以检查缓存状态 + }, + } +} + +// GetSystemInfo 获取系统详细信息 +func (s *systemService) GetSystemInfo() map[string]interface{} { + var m runtime.MemStats + runtime.ReadMemStats(&m) + + return map[string]interface{}{ + "version": "1.0.0", + "uptime": time.Since(s.startTime).String(), + "system": map[string]interface{}{ + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "version": runtime.Version(), + "num_cpu": runtime.NumCPU(), + "num_goroutine": runtime.NumGoroutine(), + }, + "memory": map[string]interface{}{ + "alloc": bToMb(m.Alloc), + "total_alloc": bToMb(m.TotalAlloc), + "sys": bToMb(m.Sys), + "num_gc": m.NumGC, + }, + "timestamp": time.Now().Format(time.RFC3339), + } +} + +// bToMb converts bytes to megabytes +func bToMb(b uint64) uint64 { + return b / 1024 / 1024 +} \ No newline at end of file diff --git a/backend/pkg/logger/logger.go b/backend/pkg/logger/logger.go new file mode 100644 index 0000000..d637b93 --- /dev/null +++ b/backend/pkg/logger/logger.go @@ -0,0 +1,104 @@ +package logger + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "license-manager/internal/config" + + "github.com/sirupsen/logrus" +) + +var Logger *logrus.Logger + +// CustomFormatter 自定义格式器,在消息末尾显示文件路径和行号 +type CustomFormatter struct { + *logrus.TextFormatter +} + +func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) { + // 先用标准格式器格式化 + formatted, err := f.TextFormatter.Format(entry) + if err != nil { + return nil, err + } + + // 如果有调用者信息,添加到末尾 + if entry.Caller != nil { + filename := entry.Caller.File + if idx := strings.Index(filename, "backend/"); idx != -1 { + filename = "./backend/" + filename[idx+len("backend/"):] + } else { + filename = filepath.Base(filename) + } + + // 移除换行符,添加文件信息,然后重新添加换行符 + formattedStr := strings.TrimRight(string(formatted), "\n") + formattedStr = fmt.Sprintf("%s %s:%d\n", formattedStr, filename, entry.Caller.Line) + return []byte(formattedStr), nil + } + + return formatted, nil +} + +func Init() { + Logger = logrus.New() + Logger.SetReportCaller(true) // 启用调用者信息 + + cfg := config.GetConfig() + if cfg != nil { + // 设置日志级别 + switch cfg.Log.Level { + case "debug": + Logger.SetLevel(logrus.DebugLevel) + case "info": + Logger.SetLevel(logrus.InfoLevel) + case "warn": + Logger.SetLevel(logrus.WarnLevel) + case "error": + Logger.SetLevel(logrus.ErrorLevel) + default: + Logger.SetLevel(logrus.InfoLevel) + } + + // 设置自定义格式器 + Logger.SetFormatter(&CustomFormatter{ + TextFormatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2006-01-02 15:04:05", + ForceColors: true, + DisableColors: false, + DisableLevelTruncation: true, + CallerPrettyfier: func(f *runtime.Frame) (string, string) { + return "", "" // 禁用默认的调用者信息显示 + }, + }, + }) + } else { + // 默认配置 + Logger.SetLevel(logrus.InfoLevel) + Logger.SetFormatter(&CustomFormatter{ + TextFormatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2006-01-02 15:04:05", + ForceColors: true, + DisableLevelTruncation: true, + CallerPrettyfier: func(f *runtime.Frame) (string, string) { + return "", "" // 禁用默认的调用者信息显示 + }, + }, + }) + } + + Logger.SetOutput(os.Stdout) +} + +func GetLogger() *logrus.Logger { + if Logger == nil { + Init() + } + return Logger +} diff --git a/backend/pkg/utils/crypto.go b/backend/pkg/utils/crypto.go new file mode 100644 index 0000000..6588316 --- /dev/null +++ b/backend/pkg/utils/crypto.go @@ -0,0 +1,17 @@ +package utils + +import ( + "golang.org/x/crypto/bcrypt" +) + +// HashPassword 使用bcrypt加密密码 +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPasswordHash 验证密码 +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} \ No newline at end of file diff --git a/backend/pkg/utils/jwt.go b/backend/pkg/utils/jwt.go new file mode 100644 index 0000000..95d09b6 --- /dev/null +++ b/backend/pkg/utils/jwt.go @@ -0,0 +1,112 @@ +package utils + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" + "license-manager/internal/config" +) + +type Claims struct { + UserID int `json:"user_id"` + Username string `json:"username"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +var ( + ErrTokenExpired = errors.New("token has expired") + ErrTokenNotValid = errors.New("token is not valid") + ErrTokenMalformed = errors.New("token is malformed") + ErrTokenNotFound = errors.New("token not found") +) + +// GenerateToken 生成JWT Token +func GenerateToken(userID int, username, role string) (string, error) { + cfg := config.GetConfig() + if cfg == nil { + return "", errors.New("config not initialized") + } + + expireTime := time.Now().Add(time.Duration(cfg.Auth.JWT.ExpireHours) * time.Hour) + + claims := &Claims{ + UserID: userID, + Username: username, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expireTime), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "license-manager", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(cfg.Auth.JWT.Secret)) + if err != nil { + return "", err + } + + return tokenString, nil +} + +// ParseToken 解析JWT Token +func ParseToken(tokenString string) (*Claims, error) { + cfg := config.GetConfig() + if cfg == nil { + return nil, errors.New("config not initialized") + } + + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return []byte(cfg.Auth.JWT.Secret), nil + }) + + if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + return nil, ErrTokenExpired + } + if errors.Is(err, jwt.ErrTokenMalformed) { + return nil, ErrTokenMalformed + } + return nil, ErrTokenNotValid + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, ErrTokenNotValid +} + +// ValidateToken 验证Token是否有效 +func ValidateToken(tokenString string) (*Claims, error) { + return ParseToken(tokenString) +} + +// RefreshTokenIfNeeded 检查Token是否需要刷新,如果需要则返回新Token +func RefreshTokenIfNeeded(tokenString string) (string, error) { + claims, err := ParseToken(tokenString) + if err != nil { + return "", err + } + + cfg := config.GetConfig() + if cfg == nil { + return "", errors.New("config not initialized") + } + + // 检查是否需要刷新(剩余时间少于阈值) + thresholdDuration := time.Duration(cfg.Auth.JWT.RefreshThresholdMinutes) * time.Minute + if time.Until(claims.ExpiresAt.Time) < thresholdDuration { + // 生成新Token + return GenerateToken(claims.UserID, claims.Username, claims.Role) + } + + // 不需要刷新,返回原Token + return tokenString, nil +} \ No newline at end of file