| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724 |
- package handler
- import (
- "net/http"
- "strconv"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/handler/dto"
- "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
- "github.com/Wei-Shaw/sub2api/internal/pkg/response"
- "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
- "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
- middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- )
- type userUsageFilters struct {
- Filters usagestats.UsageLogFilters
- StartTime time.Time
- EndTime time.Time
- }
- type userModelStat struct {
- Model string `json:"model"`
- Requests int64 `json:"requests"`
- InputTokens int64 `json:"input_tokens"`
- OutputTokens int64 `json:"output_tokens"`
- CacheCreationTokens int64 `json:"cache_creation_tokens"`
- CacheReadTokens int64 `json:"cache_read_tokens"`
- TotalTokens int64 `json:"total_tokens"`
- Cost float64 `json:"cost"`
- ActualCost float64 `json:"actual_cost"`
- }
- type userGroupStat struct {
- GroupID int64 `json:"group_id"`
- GroupName string `json:"group_name"`
- Requests int64 `json:"requests"`
- TotalTokens int64 `json:"total_tokens"`
- Cost float64 `json:"cost"`
- ActualCost float64 `json:"actual_cost"`
- }
- // UsageHandler handles usage-related requests
- type UsageHandler struct {
- usageService *service.UsageService
- apiKeyService *service.APIKeyService
- opsService *service.OpsService
- settingService *service.SettingService
- }
- // NewUsageHandler creates a new UsageHandler
- func NewUsageHandler(
- usageService *service.UsageService,
- apiKeyService *service.APIKeyService,
- opsService *service.OpsService,
- settingService *service.SettingService,
- ) *UsageHandler {
- return &UsageHandler{
- usageService: usageService,
- apiKeyService: apiKeyService,
- opsService: opsService,
- settingService: settingService,
- }
- }
- func (h *UsageHandler) parseUserUsageFilters(c *gin.Context, requireRange bool) (*userUsageFilters, bool) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return nil, false
- }
- var apiKeyID int64
- if apiKeyIDStr := strings.TrimSpace(c.Query("api_key_id")); apiKeyIDStr != "" {
- id, err := strconv.ParseInt(apiKeyIDStr, 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid api_key_id")
- return nil, false
- }
- if h.apiKeyService == nil {
- response.InternalError(c, "API key service not available")
- return nil, false
- }
- apiKey, err := h.apiKeyService.GetByID(c.Request.Context(), id)
- if err != nil {
- response.ErrorFrom(c, err)
- return nil, false
- }
- if apiKey.UserID != subject.UserID {
- response.Forbidden(c, "Not authorized to access this API key's usage records")
- return nil, false
- }
- apiKeyID = id
- }
- var groupID int64
- if groupIDStr := strings.TrimSpace(c.Query("group_id")); groupIDStr != "" {
- id, err := strconv.ParseInt(groupIDStr, 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid group_id")
- return nil, false
- }
- groupID = id
- }
- var requestType *int16
- var stream *bool
- if requestTypeStr := strings.TrimSpace(c.Query("request_type")); requestTypeStr != "" {
- parsed, err := service.ParseUsageRequestType(requestTypeStr)
- if err != nil {
- response.BadRequest(c, err.Error())
- return nil, false
- }
- value := int16(parsed)
- requestType = &value
- } else if streamStr := strings.TrimSpace(c.Query("stream")); streamStr != "" {
- val, err := strconv.ParseBool(streamStr)
- if err != nil {
- response.BadRequest(c, "Invalid stream value, use true or false")
- return nil, false
- }
- stream = &val
- }
- var nativeCompactionV2 *bool
- if raw := strings.TrimSpace(c.Query("native_compaction_v2")); raw != "" {
- value, err := strconv.ParseBool(raw)
- if err != nil {
- response.BadRequest(c, "Invalid native_compaction_v2 value, use true or false")
- return nil, false
- }
- nativeCompactionV2 = &value
- }
- var billingType *int8
- if billingTypeStr := strings.TrimSpace(c.Query("billing_type")); billingTypeStr != "" {
- val, err := strconv.ParseInt(billingTypeStr, 10, 8)
- if err != nil {
- response.BadRequest(c, "Invalid billing_type")
- return nil, false
- }
- bt := int8(val)
- billingType = &bt
- }
- billingMode := strings.TrimSpace(c.Query("billing_mode"))
- if billingMode != "" && !service.BillingMode(billingMode).IsValidUsageFilter() {
- response.BadRequest(c, "Invalid billing_mode")
- return nil, false
- }
- userTZ := c.Query("timezone")
- now := timezone.NowInUserLocation(userTZ)
- var startTime, endTime time.Time
- var startPtr, endPtr *time.Time
- startDateStr := strings.TrimSpace(c.Query("start_date"))
- endDateStr := strings.TrimSpace(c.Query("end_date"))
- if startDateStr != "" {
- t, err := timezone.ParseInUserLocation("2006-01-02", startDateStr, userTZ)
- if err != nil {
- response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD")
- return nil, false
- }
- startTime = t
- startPtr = &startTime
- }
- if endDateStr != "" {
- t, err := timezone.ParseInUserLocation("2006-01-02", endDateStr, userTZ)
- if err != nil {
- response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD")
- return nil, false
- }
- endTime = t.AddDate(0, 0, 1)
- endPtr = &endTime
- }
- if requireRange {
- if startPtr == nil {
- switch c.DefaultQuery("period", "") {
- case "today":
- startTime = timezone.StartOfDayInUserLocation(now, userTZ)
- case "week":
- startTime = now.AddDate(0, 0, -7)
- case "month":
- startTime = now.AddDate(0, -1, 0)
- default:
- startTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -7), userTZ)
- }
- startPtr = &startTime
- }
- if endPtr == nil {
- if strings.TrimSpace(c.Query("period")) != "" {
- endTime = now
- } else {
- endTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
- }
- endPtr = &endTime
- }
- }
- return &userUsageFilters{
- Filters: usagestats.UsageLogFilters{
- UserID: subject.UserID,
- APIKeyID: apiKeyID,
- GroupID: groupID,
- Model: strings.TrimSpace(c.Query("model")),
- ModelFilterSource: usagestats.ModelSourceRequested,
- RequestType: requestType,
- Stream: stream,
- NativeCompactionV2: nativeCompactionV2,
- BillingType: billingType,
- BillingMode: billingMode,
- StartTime: startPtr,
- EndTime: endPtr,
- },
- StartTime: derefTime(startPtr),
- EndTime: derefTime(endPtr),
- }, true
- }
- func derefTime(value *time.Time) time.Time {
- if value == nil {
- return time.Time{}
- }
- return *value
- }
- // List handles listing usage records with pagination
- // GET /api/v1/usage
- func (h *UsageHandler) List(c *gin.Context) {
- page, pageSize := response.ParsePagination(c)
- parsed, ok := h.parseUserUsageFilters(c, false)
- if !ok {
- return
- }
- params := pagination.PaginationParams{
- Page: page,
- PageSize: pageSize,
- SortBy: c.DefaultQuery("sort_by", "created_at"),
- SortOrder: c.DefaultQuery("sort_order", "desc"),
- }
- records, result, err := h.usageService.ListWithFilters(c.Request.Context(), params, parsed.Filters)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- out := make([]dto.UsageLog, 0, len(records))
- for i := range records {
- out = append(out, *dto.UsageLogFromService(&records[i]))
- }
- response.Paginated(c, out, result.Total, page, pageSize)
- }
- // ListErrors handles listing the current user's failed requests (redacted).
- // GET /api/v1/usage/errors
- func (h *UsageHandler) ListErrors(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- // Visibility switch (fail-closed). Defense-in-depth: frontend also hides the tab.
- if h.settingService == nil || !h.settingService.IsUserErrorViewAllowed(c.Request.Context()) {
- response.Forbidden(c, "Error requests view is disabled")
- return
- }
- if h.opsService == nil {
- response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
- return
- }
- page, pageSize := response.ParsePagination(c)
- if pageSize > 100 {
- pageSize = 100
- }
- filter := &service.OpsErrorLogFilter{Page: page, PageSize: pageSize}
- // Date range (half-open [start, end)), reuse usage-list semantics.
- userTZ := c.Query("timezone")
- if startDateStr := c.Query("start_date"); startDateStr != "" {
- t, err := timezone.ParseInUserLocation("2006-01-02", startDateStr, userTZ)
- if err != nil {
- response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD")
- return
- }
- filter.StartTime = &t
- }
- if endDateStr := c.Query("end_date"); endDateStr != "" {
- t, err := timezone.ParseInUserLocation("2006-01-02", endDateStr, userTZ)
- if err != nil {
- response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD")
- return
- }
- t = t.AddDate(0, 0, 1)
- filter.EndTime = &t
- }
- filter.Model = strings.TrimSpace(c.Query("model"))
- if k := strings.TrimSpace(c.Query("api_key_id")); k != "" {
- n, err := strconv.ParseInt(k, 10, 64)
- if err != nil || n < 0 {
- response.BadRequest(c, "Invalid api_key_id")
- return
- }
- if n > 0 {
- filter.APIKeyID = &n
- }
- }
- if sc := strings.TrimSpace(c.Query("status_code")); sc != "" {
- n, err := strconv.Atoi(sc)
- if err != nil || n < 0 {
- response.BadRequest(c, "Invalid status_code")
- return
- }
- filter.StatusCodes = []int{n}
- }
- if cat := strings.TrimSpace(c.Query("category")); cat != "" {
- phases, types := service.CategoryToFilter(cat)
- filter.ErrorPhasesAny = phases
- filter.ErrorTypesAny = types
- }
- // 排序对齐用量明细:列白名单与方向归一在 repo 层,非法值回退 created_at DESC。
- filter.SetSort(c.Query("sort_by"), c.Query("sort_order"))
- result, err := h.opsService.ListUserErrorRequests(c.Request.Context(), subject.UserID, filter)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Paginated(c, result.Items, int64(result.Total), result.Page, result.PageSize)
- }
- // GetErrorDetail handles fetching one of the current user's failed-request details (redacted).
- // GET /api/v1/usage/errors/:id
- func (h *UsageHandler) GetErrorDetail(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- if h.settingService == nil || !h.settingService.IsUserErrorViewAllowed(c.Request.Context()) {
- response.Forbidden(c, "Error requests view is disabled")
- return
- }
- if h.opsService == nil {
- response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
- return
- }
- id, err := strconv.ParseInt(strings.TrimSpace(c.Param("id")), 10, 64)
- if err != nil || id <= 0 {
- response.BadRequest(c, "Invalid id")
- return
- }
- detail, err := h.opsService.GetUserErrorRequestDetail(c.Request.Context(), subject.UserID, id)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, detail)
- }
- // GetByID handles getting a single usage record
- // GET /api/v1/usage/:id
- func (h *UsageHandler) GetByID(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- usageID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid usage ID")
- return
- }
- record, err := h.usageService.GetByID(c.Request.Context(), usageID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- // 验证所有权
- if record.UserID != subject.UserID {
- response.Forbidden(c, "Not authorized to access this record")
- return
- }
- response.Success(c, dto.UsageLogFromService(record))
- }
- // Stats handles getting usage statistics
- // GET /api/v1/usage/stats
- func (h *UsageHandler) Stats(c *gin.Context) {
- parsed, ok := h.parseUserUsageFilters(c, true)
- if !ok {
- return
- }
- stats, err := h.usageService.GetStatsWithFilters(c.Request.Context(), parsed.Filters)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- stats.TotalAccountCost = nil
- stats.UpstreamEndpoints = nil
- stats.EndpointPaths = nil
- response.Success(c, stats)
- }
- const (
- defaultAPIKeyDailyUsageDays = 30
- maxAPIKeyDailyUsageDays = 90
- )
- func parseAPIKeyDailyUsageDays(raw string) (int, bool) {
- if strings.TrimSpace(raw) == "" {
- return defaultAPIKeyDailyUsageDays, true
- }
- days, err := strconv.Atoi(raw)
- if err != nil || days <= 0 || days > maxAPIKeyDailyUsageDays {
- return 0, false
- }
- return days, true
- }
- func apiKeyDailyUsageRange(days int, userTZ string) (time.Time, time.Time) {
- now := timezone.NowInUserLocation(userTZ)
- startTime := timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -(days-1)), userTZ)
- endTime := timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
- return startTime, endTime
- }
- // DashboardStats handles getting user dashboard statistics
- // GET /api/v1/usage/dashboard/stats
- func (h *UsageHandler) DashboardStats(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- stats, err := h.usageService.GetUserDashboardStats(c.Request.Context(), subject.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, stats)
- }
- // DashboardTrend handles getting user usage trend data
- // GET /api/v1/usage/dashboard/trend
- func (h *UsageHandler) DashboardTrend(c *gin.Context) {
- parsed, ok := h.parseUserUsageFilters(c, true)
- if !ok {
- return
- }
- granularity := c.DefaultQuery("granularity", "day")
- trend, err := h.usageService.GetUsageTrendWithFilters(c.Request.Context(), parsed.StartTime, parsed.EndTime, granularity, parsed.Filters)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{
- "trend": trend,
- "start_date": parsed.StartTime.Format("2006-01-02"),
- "end_date": parsed.EndTime.Add(-24 * time.Hour).Format("2006-01-02"),
- "granularity": granularity,
- })
- }
- // DashboardModels handles getting user model usage statistics
- // GET /api/v1/usage/dashboard/models
- func (h *UsageHandler) DashboardModels(c *gin.Context) {
- parsed, ok := h.parseUserUsageFilters(c, true)
- if !ok {
- return
- }
- modelSource := strings.TrimSpace(c.Query("model_source"))
- if modelSource != "" && modelSource != usagestats.ModelSourceRequested {
- response.BadRequest(c, "Invalid model_source, user usage only supports requested")
- return
- }
- stats, err := h.usageService.GetModelStatsWithFiltersBySource(c.Request.Context(), parsed.StartTime, parsed.EndTime, parsed.Filters, usagestats.ModelSourceRequested)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{
- "models": userModelStatsFromUsageStats(stats),
- "start_date": parsed.StartTime.Format("2006-01-02"),
- "end_date": parsed.EndTime.Add(-24 * time.Hour).Format("2006-01-02"),
- })
- }
- // DashboardSnapshotV2 returns usage-page chart data scoped to the current user.
- // GET /api/v1/usage/dashboard/snapshot-v2
- func (h *UsageHandler) DashboardSnapshotV2(c *gin.Context) {
- parsed, ok := h.parseUserUsageFilters(c, true)
- if !ok {
- return
- }
- granularity := strings.TrimSpace(c.DefaultQuery("granularity", "day"))
- if granularity != "hour" {
- granularity = "day"
- }
- includeTrend, ok := parseBoolQueryWithDefault(c, "include_trend", true)
- if !ok {
- return
- }
- includeModels, ok := parseBoolQueryWithDefault(c, "include_model_stats", true)
- if !ok {
- return
- }
- includeGroups, ok := parseBoolQueryWithDefault(c, "include_group_stats", false)
- if !ok {
- return
- }
- resp := gin.H{
- "generated_at": time.Now().UTC().Format(time.RFC3339),
- "start_date": parsed.StartTime.Format("2006-01-02"),
- "end_date": parsed.EndTime.Add(-24 * time.Hour).Format("2006-01-02"),
- "granularity": granularity,
- }
- if includeTrend {
- trend, err := h.usageService.GetUsageTrendWithFilters(c.Request.Context(), parsed.StartTime, parsed.EndTime, granularity, parsed.Filters)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- resp["trend"] = trend
- }
- if includeModels {
- models, err := h.usageService.GetModelStatsWithFiltersBySource(c.Request.Context(), parsed.StartTime, parsed.EndTime, parsed.Filters, usagestats.ModelSourceRequested)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- resp["models"] = userModelStatsFromUsageStats(models)
- }
- if includeGroups {
- groups, err := h.usageService.GetGroupStatsWithFilters(c.Request.Context(), parsed.StartTime, parsed.EndTime, parsed.Filters)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- resp["groups"] = userGroupStatsFromUsageStats(groups)
- }
- response.Success(c, resp)
- }
- func userModelStatsFromUsageStats(stats []usagestats.ModelStat) []userModelStat {
- out := make([]userModelStat, 0, len(stats))
- for _, stat := range stats {
- out = append(out, userModelStat{
- Model: stat.Model,
- Requests: stat.Requests,
- InputTokens: stat.InputTokens,
- OutputTokens: stat.OutputTokens,
- CacheCreationTokens: stat.CacheCreationTokens,
- CacheReadTokens: stat.CacheReadTokens,
- TotalTokens: stat.TotalTokens,
- Cost: stat.Cost,
- ActualCost: stat.ActualCost,
- })
- }
- return out
- }
- func userGroupStatsFromUsageStats(stats []usagestats.GroupStat) []userGroupStat {
- out := make([]userGroupStat, 0, len(stats))
- for _, stat := range stats {
- out = append(out, userGroupStat{
- GroupID: stat.GroupID,
- GroupName: stat.GroupName,
- Requests: stat.Requests,
- TotalTokens: stat.TotalTokens,
- Cost: stat.Cost,
- ActualCost: stat.ActualCost,
- })
- }
- return out
- }
- func parseBoolQueryWithDefault(c *gin.Context, key string, fallback bool) (bool, bool) {
- raw := c.Query(key)
- if strings.TrimSpace(raw) == "" {
- return fallback, true
- }
- parsed, err := strconv.ParseBool(raw)
- if err != nil {
- response.BadRequest(c, "Invalid "+key+" value, use true or false")
- return false, false
- }
- return parsed, true
- }
- // BatchAPIKeysUsageRequest represents the request for batch API keys usage
- type BatchAPIKeysUsageRequest struct {
- APIKeyIDs []int64 `json:"api_key_ids" binding:"required"`
- }
- // DashboardAPIKeysUsage handles getting usage stats for user's own API keys
- // POST /api/v1/usage/dashboard/api-keys-usage
- func (h *UsageHandler) DashboardAPIKeysUsage(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- var req BatchAPIKeysUsageRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- if len(req.APIKeyIDs) == 0 {
- response.Success(c, gin.H{"stats": map[string]any{}})
- return
- }
- // Limit the number of API key IDs to prevent SQL parameter overflow
- if len(req.APIKeyIDs) > 100 {
- response.BadRequest(c, "Too many API key IDs (maximum 100 allowed)")
- return
- }
- validAPIKeyIDs, err := h.apiKeyService.VerifyOwnership(c.Request.Context(), subject.UserID, req.APIKeyIDs)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- if len(validAPIKeyIDs) == 0 {
- response.Success(c, gin.H{"stats": map[string]any{}})
- return
- }
- stats, err := h.usageService.GetBatchAPIKeyUsageStats(c.Request.Context(), validAPIKeyIDs, time.Time{}, time.Time{})
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{"stats": stats})
- }
- // GetMyAPIKeyDailyUsage handles getting daily usage details for the current user's API key.
- // GET /api/v1/user/api-keys/:id/usage/daily?days=30
- func (h *UsageHandler) GetMyAPIKeyDailyUsage(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- apiKeyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid API key ID")
- return
- }
- days, ok := parseAPIKeyDailyUsageDays(c.DefaultQuery("days", ""))
- if !ok {
- response.BadRequest(c, "Invalid days, allowed range is 1-90")
- return
- }
- if h.apiKeyService == nil {
- response.InternalError(c, "API key service is not configured")
- return
- }
- apiKey, err := h.apiKeyService.GetByID(c.Request.Context(), apiKeyID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- if apiKey.UserID != subject.UserID {
- response.Forbidden(c, "Not authorized to access this API key's usage")
- return
- }
- userTZ := c.Query("timezone")
- startTime, endTime := apiKeyDailyUsageRange(days, userTZ)
- items, err := h.usageService.GetAPIKeyDailyUsage(c.Request.Context(), subject.UserID, apiKeyID, startTime, endTime)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{
- "items": items,
- "days": days,
- "start_date": startTime.Format("2006-01-02"),
- "end_date": endTime.AddDate(0, 0, -1).Format("2006-01-02"),
- })
- }
|