Files
superidou 5759c1862e AI Gateway Go 0.10.0 源码快照 + 旗舰版需求规划报告
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。
含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 11:45:54 +08:00

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