tokenFactory/middleware/operation_log.go

242 lines
7.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, "")
}