- 记录失败请求并在用户端/管理端展示;分类下拉改用统一 Select 组件 - 模型过滤改后端 ILIKE 模糊匹配;新增「Key 名称」列(含已删除标记)与按 Key 过滤;时间列移至末列 - 用户可见「已删除 key 失败请求」:OpsErrorLogFilter 加 MatchDeletedKeyOwner,用户侧归属 放宽为 (user_id OR deleted_key_owner_user_id),让 key 原所有者能看到删除 key 后继续请求 导致的认证失败记录(他人仍 NotFound,不泄露存在性) - 迁移 148:ops_error_logs 用户+时间索引 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
615 lines
17 KiB
Go
615 lines
17 KiB
Go
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"
|
||
)
|
||
|
||
// 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,
|
||
}
|
||
}
|
||
|
||
// List handles listing usage records with pagination
|
||
// GET /api/v1/usage
|
||
func (h *UsageHandler) List(c *gin.Context) {
|
||
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
||
if !ok {
|
||
response.Unauthorized(c, "User not authenticated")
|
||
return
|
||
}
|
||
|
||
page, pageSize := response.ParsePagination(c)
|
||
|
||
var apiKeyID int64
|
||
if apiKeyIDStr := c.Query("api_key_id"); apiKeyIDStr != "" {
|
||
id, err := strconv.ParseInt(apiKeyIDStr, 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "Invalid api_key_id")
|
||
return
|
||
}
|
||
|
||
// [Security Fix] Verify API Key ownership to prevent horizontal privilege escalation
|
||
apiKey, err := h.apiKeyService.GetByID(c.Request.Context(), id)
|
||
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 records")
|
||
return
|
||
}
|
||
|
||
apiKeyID = id
|
||
}
|
||
|
||
// Parse additional filters
|
||
model := c.Query("model")
|
||
|
||
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
|
||
}
|
||
value := int16(parsed)
|
||
requestType = &value
|
||
} else if streamStr := c.Query("stream"); streamStr != "" {
|
||
val, err := strconv.ParseBool(streamStr)
|
||
if err != nil {
|
||
response.BadRequest(c, "Invalid stream value, use true or false")
|
||
return
|
||
}
|
||
stream = &val
|
||
}
|
||
|
||
var billingType *int8
|
||
if billingTypeStr := c.Query("billing_type"); billingTypeStr != "" {
|
||
val, err := strconv.ParseInt(billingTypeStr, 10, 8)
|
||
if err != nil {
|
||
response.BadRequest(c, "Invalid billing_type")
|
||
return
|
||
}
|
||
bt := int8(val)
|
||
billingType = &bt
|
||
}
|
||
|
||
// Parse date range
|
||
var startTime, endTime *time.Time
|
||
userTZ := c.Query("timezone") // Get user's timezone from request
|
||
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
|
||
}
|
||
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
|
||
}
|
||
// Use half-open range [start, end), move to next calendar day start (DST-safe).
|
||
t = t.AddDate(0, 0, 1)
|
||
endTime = &t
|
||
}
|
||
|
||
params := pagination.PaginationParams{
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
SortBy: c.DefaultQuery("sort_by", "created_at"),
|
||
SortOrder: c.DefaultQuery("sort_order", "desc"),
|
||
}
|
||
filters := usagestats.UsageLogFilters{
|
||
UserID: subject.UserID, // Always filter by current user for security
|
||
APIKeyID: apiKeyID,
|
||
Model: model,
|
||
RequestType: requestType,
|
||
Stream: stream,
|
||
BillingType: billingType,
|
||
StartTime: startTime,
|
||
EndTime: endTime,
|
||
}
|
||
|
||
records, result, err := h.usageService.ListWithFilters(c.Request.Context(), params, 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
|
||
}
|
||
|
||
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) {
|
||
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
||
if !ok {
|
||
response.Unauthorized(c, "User not authenticated")
|
||
return
|
||
}
|
||
|
||
var apiKeyID int64
|
||
if apiKeyIDStr := c.Query("api_key_id"); apiKeyIDStr != "" {
|
||
id, err := strconv.ParseInt(apiKeyIDStr, 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "Invalid api_key_id")
|
||
return
|
||
}
|
||
|
||
// [Security Fix] Verify API Key ownership to prevent horizontal privilege escalation
|
||
apiKey, err := h.apiKeyService.GetByID(c.Request.Context(), id)
|
||
if err != nil {
|
||
response.NotFound(c, "API key not found")
|
||
return
|
||
}
|
||
if apiKey.UserID != subject.UserID {
|
||
response.Forbidden(c, "Not authorized to access this API key's statistics")
|
||
return
|
||
}
|
||
|
||
apiKeyID = id
|
||
}
|
||
|
||
// 获取时间范围参数
|
||
userTZ := c.Query("timezone") // Get user's timezone from request
|
||
now := timezone.NowInUserLocation(userTZ)
|
||
var startTime, endTime time.Time
|
||
|
||
// 优先使用 start_date 和 end_date 参数
|
||
startDateStr := c.Query("start_date")
|
||
endDateStr := c.Query("end_date")
|
||
|
||
if startDateStr != "" && endDateStr != "" {
|
||
// 使用自定义日期范围
|
||
var err error
|
||
startTime, err = timezone.ParseInUserLocation("2006-01-02", startDateStr, userTZ)
|
||
if err != nil {
|
||
response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD")
|
||
return
|
||
}
|
||
endTime, err = timezone.ParseInUserLocation("2006-01-02", endDateStr, userTZ)
|
||
if err != nil {
|
||
response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD")
|
||
return
|
||
}
|
||
// 与 SQL 条件 created_at < end 对齐,使用次日 00:00 作为上边界(DST-safe)。
|
||
endTime = endTime.AddDate(0, 0, 1)
|
||
} else {
|
||
// 使用 period 参数
|
||
period := c.DefaultQuery("period", "today")
|
||
switch 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, userTZ)
|
||
}
|
||
endTime = now
|
||
}
|
||
|
||
var stats *service.UsageStats
|
||
var err error
|
||
if apiKeyID > 0 {
|
||
stats, err = h.usageService.GetStatsByAPIKey(c.Request.Context(), apiKeyID, startTime, endTime)
|
||
} else {
|
||
stats, err = h.usageService.GetStatsByUser(c.Request.Context(), subject.UserID, startTime, endTime)
|
||
}
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
|
||
response.Success(c, stats)
|
||
}
|
||
|
||
// parseUserTimeRange parses start_date, end_date query parameters for user dashboard
|
||
// Uses user's timezone if provided, otherwise falls back to server timezone
|
||
func parseUserTimeRange(c *gin.Context) (time.Time, time.Time) {
|
||
userTZ := c.Query("timezone") // Get user's timezone from request
|
||
now := timezone.NowInUserLocation(userTZ)
|
||
startDate := c.Query("start_date")
|
||
endDate := c.Query("end_date")
|
||
|
||
var startTime, endTime time.Time
|
||
|
||
if startDate != "" {
|
||
if t, err := timezone.ParseInUserLocation("2006-01-02", startDate, userTZ); err == nil {
|
||
startTime = t
|
||
} else {
|
||
startTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -7), userTZ)
|
||
}
|
||
} else {
|
||
startTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -7), userTZ)
|
||
}
|
||
|
||
if endDate != "" {
|
||
if t, err := timezone.ParseInUserLocation("2006-01-02", endDate, userTZ); err == nil {
|
||
endTime = t.Add(24 * time.Hour) // Include the end date
|
||
} else {
|
||
endTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
|
||
}
|
||
} else {
|
||
endTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
|
||
}
|
||
|
||
return startTime, endTime
|
||
}
|
||
|
||
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) {
|
||
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
||
if !ok {
|
||
response.Unauthorized(c, "User not authenticated")
|
||
return
|
||
}
|
||
|
||
startTime, endTime := parseUserTimeRange(c)
|
||
granularity := c.DefaultQuery("granularity", "day")
|
||
|
||
trend, err := h.usageService.GetUserUsageTrendByUserID(c.Request.Context(), subject.UserID, startTime, endTime, granularity)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{
|
||
"trend": trend,
|
||
"start_date": startTime.Format("2006-01-02"),
|
||
"end_date": 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) {
|
||
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
||
if !ok {
|
||
response.Unauthorized(c, "User not authenticated")
|
||
return
|
||
}
|
||
|
||
startTime, endTime := parseUserTimeRange(c)
|
||
|
||
stats, err := h.usageService.GetUserModelStats(c.Request.Context(), subject.UserID, startTime, endTime)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{
|
||
"models": stats,
|
||
"start_date": startTime.Format("2006-01-02"),
|
||
"end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"),
|
||
})
|
||
}
|
||
|
||
// 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"),
|
||
})
|
||
}
|