feat: 增加基础模块,登录、退出登录等接口 #1

Merged
wood911010 merged 1 commits from main into main 2025-07-30 15:16:44 +00:00
28 changed files with 2953 additions and 36 deletions

No files matched your search

+47 -24
View File
@@ -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"
)
// @title License Manager API
// @version 1.0
// @description 软件授权管理平台API文档
// @termsOfService http://swagger.io/terms/
// @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() {
logrus.Info("Starting License Manager Server...")
// 加载配置
if err := config.Load(""); err != nil {
log.Fatalf("Failed to load config: %v", err)
}
r := mux.NewRouter()
// 初始化日志
logger.Init()
log := logger.GetLogger()
// 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")
log.Info("启动 License Manager 服务器...")
// Tools route
r.HandleFunc("/tools/{tool}", getToolHandler).Methods("GET")
cfg := config.GetConfig()
logrus.Info("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
// 设置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)
}
}
// 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) {}
+19 -9
View File
@@ -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"
@@ -10,11 +28,3 @@ database:
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
+20
View File
@@ -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" # 日志格式
+265
View File
@@ -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包
+73
View File
@@ -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切换
+273
View File
@@ -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 <token>
```
**响应格式:**
```json
{
"code": 200,
"message": "登出成功"
}
```
### 3. Token刷新
#### 3.1 刷新机制
- 当Token即将过期时(剩余时间<30分钟),自动刷新
- 刷新Token与访问Token分离(可选实现)
#### 3.2 API接口设计
```http
POST /api/v1/auth/refresh
Authorization: Bearer <token>
```
## 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"
}
```
+350
View File
@@ -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)
}
+326
View File
@@ -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"
}
}
}
+212
View File
@@ -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"
+41
View File
@@ -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
```
+62 -3
View File
@@ -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
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
)
+212
View File
@@ -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=
@@ -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 ""
}
@@ -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,
})
}
+160
View File
@@ -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)
}
+22
View File
@@ -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()
})
}
@@ -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")
}
}
+63
View File
@@ -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
}
+107
View File
@@ -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
}
+27
View File
@@ -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"`
}
+17
View File
@@ -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"`
}
+20
View File
@@ -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"`
}
+70
View File
@@ -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
}
+16
View File
@@ -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{}
}
@@ -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
}
+104
View File
@@ -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
}
+17
View File
@@ -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
}
+112
View File
@@ -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
}