242 lines
7.9 KiB
Go
242 lines
7.9 KiB
Go
package middleware
|
||
|
||
import (
|
||
"bytes"
|
||
"io"
|
||
"net/http"
|
||
|
||
"github.com/QuantumNous/new-api/model"
|
||
"github.com/QuantumNous/new-api/service"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// operationLogConfig 记录操作日志的路径配置。
|
||
// key: HTTP method + path pattern, value: {action, targetType}
|
||
type operationLogConfig struct {
|
||
Action string
|
||
TargetType string
|
||
}
|
||
|
||
// auditRoutes 需要记录操作日志的路由配置。
|
||
// 仅记录增删改操作,不记录查询操作。
|
||
var auditRoutes = map[string]operationLogConfig{
|
||
// 渠道管理
|
||
"POST /api/channel/": {Action: model.OperationActionCreate, TargetType: "channel"},
|
||
"PUT /api/channel/": {Action: model.OperationActionUpdate, TargetType: "channel"},
|
||
"DELETE /api/channel/:id": {Action: model.OperationActionDelete, TargetType: "channel"},
|
||
"POST /api/channel/batch": {Action: model.OperationActionDelete, TargetType: "channel"},
|
||
"POST /api/channel/copy/:id": {Action: model.OperationActionCreate, TargetType: "channel"},
|
||
// 令牌管理
|
||
"POST /api/token/": {Action: model.OperationActionCreate, TargetType: "token"},
|
||
"PUT /api/token/": {Action: model.OperationActionUpdate, TargetType: "token"},
|
||
"DELETE /api/token/:id": {Action: model.OperationActionDelete, TargetType: "token"},
|
||
"POST /api/token/batch": {Action: model.OperationActionDelete, TargetType: "token"},
|
||
// 用户管理
|
||
"POST /api/user/": {Action: model.OperationActionCreate, TargetType: "user"},
|
||
"PUT /api/user/": {Action: model.OperationActionUpdate, TargetType: "user"},
|
||
"DELETE /api/user/:id": {Action: model.OperationActionDelete, TargetType: "user"},
|
||
"POST /api/user/manage": {Action: model.OperationActionUpdate, TargetType: "user"},
|
||
// 兑换码管理
|
||
"POST /api/redemption/": {Action: model.OperationActionCreate, TargetType: "redemption"},
|
||
"PUT /api/redemption/": {Action: model.OperationActionUpdate, TargetType: "redemption"},
|
||
"DELETE /api/redemption/:id": {Action: model.OperationActionDelete, TargetType: "redemption"},
|
||
"DELETE /api/redemption/invalid": {Action: model.OperationActionDelete, TargetType: "redemption"},
|
||
// 模型管理
|
||
"POST /api/models/": {Action: model.OperationActionCreate, TargetType: "model"},
|
||
"PUT /api/models/": {Action: model.OperationActionUpdate, TargetType: "model"},
|
||
"DELETE /api/models/:id": {Action: model.OperationActionDelete, TargetType: "model"},
|
||
// 供应商管理
|
||
"POST /api/user/supplier/application": {Action: model.OperationActionCreate, TargetType: "supplier_application"},
|
||
"PUT /api/user/supplier/application/self": {Action: model.OperationActionUpdate, TargetType: "supplier_application"},
|
||
"PUT /api/user/supplier/application/:id/capability": {Action: model.OperationActionUpdate, TargetType: "supplier_capability"},
|
||
// 系统设置
|
||
"PUT /api/option/": {Action: model.OperationActionUpdate, TargetType: "setting"},
|
||
}
|
||
|
||
// OperationLogMiddleware 自动记录匹配路由的增删改操作。
|
||
// 对于路径参数路由(如 /api/channel/:id),尝试用实际路径匹配。
|
||
func OperationLogMiddleware() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
method := c.Request.Method
|
||
// 仅拦截写操作
|
||
if method != http.MethodPost && method != http.MethodPut && method != http.MethodPatch && method != http.MethodDelete {
|
||
c.Next()
|
||
return
|
||
}
|
||
|
||
path := c.Request.URL.Path
|
||
key := method + " " + path
|
||
|
||
config, matched := auditRoutes[key]
|
||
if !matched {
|
||
// 尝试匹配带 :id 的路由模式
|
||
// 通过遍历已知模式,将 :id 部分替换为实际值进行匹配
|
||
for pattern, cfg := range auditRoutes {
|
||
if matchRoutePattern(pattern, method, path) {
|
||
config = cfg
|
||
matched = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if !matched {
|
||
c.Next()
|
||
return
|
||
}
|
||
|
||
// 读取请求体用于审计
|
||
var requestBody string
|
||
if c.Request.Body != nil && (method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch) {
|
||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||
if err == nil {
|
||
requestBody = string(bodyBytes)
|
||
// 将请求体放回,供后续处理器读取
|
||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||
}
|
||
}
|
||
|
||
// 先执行后续处理器
|
||
c.Next()
|
||
|
||
// 如果 controller 已经手动记录了操作日志,则跳过中间件记录,避免重复
|
||
if already, _ := c.Get("_operation_logged"); already != nil {
|
||
if logged, ok := already.(bool); ok && logged {
|
||
return
|
||
}
|
||
}
|
||
|
||
// 只有成功时才记录(2xx 状态码)
|
||
status := c.Writer.Status()
|
||
if status < 200 || status >= 300 {
|
||
return
|
||
}
|
||
|
||
userId := c.GetInt("id")
|
||
if userId == 0 {
|
||
return
|
||
}
|
||
|
||
// 从上下文中获取目标 ID 和名称(由各 controller 设置)
|
||
targetId, _ := c.Get("operation_target_id")
|
||
targetName, _ := c.Get("operation_target_name")
|
||
content, _ := c.Get("operation_content")
|
||
|
||
targetIdInt := 0
|
||
if v, ok := targetId.(int); ok {
|
||
targetIdInt = v
|
||
}
|
||
|
||
targetNameStr := ""
|
||
if v, ok := targetName.(string); ok {
|
||
targetNameStr = v
|
||
}
|
||
|
||
contentStr := ""
|
||
if v, ok := content.(string); ok {
|
||
contentStr = v
|
||
} else {
|
||
// 自动生成默认描述
|
||
contentStr = commonActionLabel(config.Action, config.TargetType, targetNameStr)
|
||
}
|
||
|
||
sanitizedBody := model.SanitizeRequestBody(requestBody)
|
||
model.RecordOperationLog(c, userId, config.Action, config.TargetType, targetIdInt, targetNameStr, contentStr, sanitizedBody)
|
||
}
|
||
}
|
||
|
||
// matchRoutePattern 检查实际请求路径是否匹配带 :param 的路由模式。
|
||
func matchRoutePattern(pattern string, method string, path string) bool {
|
||
// 简单实现:检查方法前缀和路径段数量是否一致
|
||
if len(pattern) < len(method)+2 || pattern[:len(method)+1] != method+" " {
|
||
return false
|
||
}
|
||
patternPath := pattern[len(method)+1:]
|
||
|
||
// 分段比较
|
||
patternSegments := splitPath(patternPath)
|
||
pathSegments := splitPath(path)
|
||
if len(patternSegments) != len(pathSegments) {
|
||
return false
|
||
}
|
||
|
||
for i := range patternSegments {
|
||
if len(patternSegments[i]) > 0 && patternSegments[i][0] == ':' {
|
||
// 动态段,跳过
|
||
continue
|
||
}
|
||
if patternSegments[i] != pathSegments[i] {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func splitPath(path string) []string {
|
||
var segments []string
|
||
start := 0
|
||
for i := 0; i <= len(path); i++ {
|
||
if i == len(path) || path[i] == '/' {
|
||
if i > start {
|
||
segments = append(segments, path[start:i])
|
||
}
|
||
start = i + 1
|
||
}
|
||
}
|
||
return segments
|
||
}
|
||
|
||
// commonActionLabel 生成默认的操作描述。
|
||
func commonActionLabel(action string, targetType string, targetName string) string {
|
||
actionLabel := ""
|
||
switch action {
|
||
case model.OperationActionCreate:
|
||
actionLabel = "创建"
|
||
case model.OperationActionUpdate:
|
||
actionLabel = "更新"
|
||
case model.OperationActionDelete:
|
||
actionLabel = "删除"
|
||
default:
|
||
actionLabel = action
|
||
}
|
||
|
||
typeLabel := targetType
|
||
switch targetType {
|
||
case "channel":
|
||
typeLabel = "渠道"
|
||
case "token":
|
||
typeLabel = "令牌"
|
||
case "user":
|
||
typeLabel = "用户"
|
||
case "redemption":
|
||
typeLabel = "兑换码"
|
||
case "model":
|
||
typeLabel = "模型"
|
||
case "setting":
|
||
typeLabel = "系统设置"
|
||
case "supplier_application":
|
||
typeLabel = "供应商申请"
|
||
case "supplier_capability":
|
||
typeLabel = "供应商能力"
|
||
}
|
||
|
||
if targetName != "" {
|
||
return actionLabel + typeLabel + ": " + targetName
|
||
}
|
||
return actionLabel + typeLabel
|
||
}
|
||
|
||
// SetOperationTarget 在 controller 中设置操作目标信息,供中间件读取。
|
||
func SetOperationTarget(c *gin.Context, targetId int, targetName string, content string) {
|
||
c.Set("operation_target_id", targetId)
|
||
c.Set("operation_target_name", targetName)
|
||
c.Set("operation_content", content)
|
||
}
|
||
|
||
// RecordOperationDirectly 在 controller 中直接记录操作日志(不走中间件)。
|
||
// 适用于需要自定义逻辑的场景。
|
||
func RecordOperationDirectly(c *gin.Context, action string, targetType string, targetId int, targetName string, content string) {
|
||
service.RecordOperation(c, action, targetType, targetId, targetName, content, "")
|
||
}
|