5759c1862e
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。 含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。 Co-Authored-By: Claude <noreply@anthropic.com>
145 lines
4.9 KiB
Go
145 lines
4.9 KiB
Go
package audit
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"aigateway.local/core/internal/identity"
|
|
"aigateway.local/core/internal/platform/apiresponse"
|
|
)
|
|
|
|
type AdminHTTPHandler struct {
|
|
query *QueryService
|
|
identity *identity.Service
|
|
mux *http.ServeMux
|
|
}
|
|
|
|
func NewAdminHTTPHandler(query *QueryService, identityService *identity.Service) *AdminHTTPHandler {
|
|
h := &AdminHTTPHandler{query: query, identity: identityService, mux: http.NewServeMux()}
|
|
h.mux.HandleFunc("GET /api/v1/admin/audit-events", h.listEvents)
|
|
h.mux.HandleFunc("GET /api/v1/admin/usage/daily", h.listDailyUsage)
|
|
return h
|
|
}
|
|
|
|
func (h *AdminHTTPHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|
h.mux.ServeHTTP(writer, request)
|
|
}
|
|
|
|
func (h *AdminHTTPHandler) listEvents(writer http.ResponseWriter, request *http.Request) {
|
|
if !h.requirePermission(writer, request, identity.PermissionAuditRead) {
|
|
return
|
|
}
|
|
now := time.Now().UTC()
|
|
from, err := queryTime(request, "from", now.Add(-24*time.Hour))
|
|
if err != nil {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "from 时间无效")
|
|
return
|
|
}
|
|
to, err := queryTime(request, "to", now.Add(time.Second))
|
|
if err != nil || !to.After(from) || to.Sub(from) > 366*24*time.Hour {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "审计查询时间范围无效或超过 366 天")
|
|
return
|
|
}
|
|
limit := 50
|
|
if value := request.URL.Query().Get("limit"); value != "" {
|
|
limit, err = strconv.Atoi(value)
|
|
if err != nil || limit < 1 || limit > 200 {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "limit 必须在 1 到 200 之间")
|
|
return
|
|
}
|
|
}
|
|
var before *time.Time
|
|
if value := request.URL.Query().Get("before"); value != "" {
|
|
parsed, parseErr := time.Parse(time.RFC3339Nano, value)
|
|
if parseErr != nil {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "before 游标无效")
|
|
return
|
|
}
|
|
before = &parsed
|
|
}
|
|
var status *int
|
|
if value := request.URL.Query().Get("status"); value != "" {
|
|
parsed, parseErr := strconv.Atoi(value)
|
|
if parseErr != nil || parsed < 100 || parsed > 599 {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "status 无效")
|
|
return
|
|
}
|
|
status = &parsed
|
|
}
|
|
items, err := h.query.ListEvents(request.Context(), EventFilter{
|
|
From: from, To: to, Before: before, APIKeyID: strings.TrimSpace(request.URL.Query().Get("api_key_id")),
|
|
Provider: strings.TrimSpace(request.URL.Query().Get("provider")), Model: strings.TrimSpace(request.URL.Query().Get("model")), StatusCode: status, Limit: limit,
|
|
})
|
|
if err != nil {
|
|
apiresponse.Error(writer, http.StatusServiceUnavailable, "审计查询服务暂不可用")
|
|
return
|
|
}
|
|
next := ""
|
|
if len(items) == limit {
|
|
next = items[len(items)-1].RecordedAt.Format(time.RFC3339Nano)
|
|
}
|
|
apiresponse.OK(writer, map[string]any{"items": items, "next_before": next})
|
|
}
|
|
|
|
func (h *AdminHTTPHandler) listDailyUsage(writer http.ResponseWriter, request *http.Request) {
|
|
if !h.requirePermission(writer, request, identity.PermissionUsageRead) {
|
|
return
|
|
}
|
|
now := time.Now().UTC()
|
|
from, err := queryDate(request, "from", now.AddDate(0, 0, -29))
|
|
if err != nil {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "from 日期无效")
|
|
return
|
|
}
|
|
to, err := queryDate(request, "to", now)
|
|
if err != nil || to.Before(from) || to.Sub(from) > 366*24*time.Hour {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "usage 查询日期范围无效或超过 366 天")
|
|
return
|
|
}
|
|
items, err := h.query.ListDailyUsage(request.Context(), UsageFilter{
|
|
From: from, To: to, APIKeyID: strings.TrimSpace(request.URL.Query().Get("api_key_id")),
|
|
Provider: strings.TrimSpace(request.URL.Query().Get("provider")), Model: strings.TrimSpace(request.URL.Query().Get("model")),
|
|
})
|
|
if err != nil {
|
|
apiresponse.Error(writer, http.StatusServiceUnavailable, "usage 查询服务暂不可用")
|
|
return
|
|
}
|
|
apiresponse.OK(writer, items)
|
|
}
|
|
|
|
func (h *AdminHTTPHandler) requirePermission(writer http.ResponseWriter, request *http.Request, permission string) bool {
|
|
account, err := h.identity.Authenticate(request.Context(), identity.KindAdmin, request.Header.Get("Authorization"))
|
|
if err != nil {
|
|
status := http.StatusUnauthorized
|
|
if !errors.Is(err, identity.ErrInvalidSession) && !errors.Is(err, identity.ErrNotFound) {
|
|
status = http.StatusServiceUnavailable
|
|
}
|
|
apiresponse.Error(writer, status, "登录状态无效或身份服务暂不可用")
|
|
return false
|
|
}
|
|
if !identity.HasPermission(account, permission) {
|
|
apiresponse.Error(writer, http.StatusForbidden, "缺少审计或 usage 查看权限")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func queryTime(request *http.Request, name string, fallback time.Time) (time.Time, error) {
|
|
value := request.URL.Query().Get(name)
|
|
if value == "" {
|
|
return fallback, nil
|
|
}
|
|
return time.Parse(time.RFC3339, value)
|
|
}
|
|
|
|
func queryDate(request *http.Request, name string, fallback time.Time) (time.Time, error) {
|
|
value := request.URL.Query().Get(name)
|
|
if value == "" {
|
|
value = fallback.Format("2006-01-02")
|
|
}
|
|
return time.Parse("2006-01-02", value)
|
|
}
|