usage_handler.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. package handler
  2. import (
  3. "net/http"
  4. "strconv"
  5. "strings"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
  12. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  13. "github.com/Wei-Shaw/sub2api/internal/service"
  14. "github.com/gin-gonic/gin"
  15. )
  16. type userUsageFilters struct {
  17. Filters usagestats.UsageLogFilters
  18. StartTime time.Time
  19. EndTime time.Time
  20. }
  21. type userModelStat struct {
  22. Model string `json:"model"`
  23. Requests int64 `json:"requests"`
  24. InputTokens int64 `json:"input_tokens"`
  25. OutputTokens int64 `json:"output_tokens"`
  26. CacheCreationTokens int64 `json:"cache_creation_tokens"`
  27. CacheReadTokens int64 `json:"cache_read_tokens"`
  28. TotalTokens int64 `json:"total_tokens"`
  29. Cost float64 `json:"cost"`
  30. ActualCost float64 `json:"actual_cost"`
  31. }
  32. type userGroupStat struct {
  33. GroupID int64 `json:"group_id"`
  34. GroupName string `json:"group_name"`
  35. Requests int64 `json:"requests"`
  36. TotalTokens int64 `json:"total_tokens"`
  37. Cost float64 `json:"cost"`
  38. ActualCost float64 `json:"actual_cost"`
  39. }
  40. // UsageHandler handles usage-related requests
  41. type UsageHandler struct {
  42. usageService *service.UsageService
  43. apiKeyService *service.APIKeyService
  44. opsService *service.OpsService
  45. settingService *service.SettingService
  46. }
  47. // NewUsageHandler creates a new UsageHandler
  48. func NewUsageHandler(
  49. usageService *service.UsageService,
  50. apiKeyService *service.APIKeyService,
  51. opsService *service.OpsService,
  52. settingService *service.SettingService,
  53. ) *UsageHandler {
  54. return &UsageHandler{
  55. usageService: usageService,
  56. apiKeyService: apiKeyService,
  57. opsService: opsService,
  58. settingService: settingService,
  59. }
  60. }
  61. func (h *UsageHandler) parseUserUsageFilters(c *gin.Context, requireRange bool) (*userUsageFilters, bool) {
  62. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  63. if !ok {
  64. response.Unauthorized(c, "User not authenticated")
  65. return nil, false
  66. }
  67. var apiKeyID int64
  68. if apiKeyIDStr := strings.TrimSpace(c.Query("api_key_id")); apiKeyIDStr != "" {
  69. id, err := strconv.ParseInt(apiKeyIDStr, 10, 64)
  70. if err != nil {
  71. response.BadRequest(c, "Invalid api_key_id")
  72. return nil, false
  73. }
  74. if h.apiKeyService == nil {
  75. response.InternalError(c, "API key service not available")
  76. return nil, false
  77. }
  78. apiKey, err := h.apiKeyService.GetByID(c.Request.Context(), id)
  79. if err != nil {
  80. response.ErrorFrom(c, err)
  81. return nil, false
  82. }
  83. if apiKey.UserID != subject.UserID {
  84. response.Forbidden(c, "Not authorized to access this API key's usage records")
  85. return nil, false
  86. }
  87. apiKeyID = id
  88. }
  89. var groupID int64
  90. if groupIDStr := strings.TrimSpace(c.Query("group_id")); groupIDStr != "" {
  91. id, err := strconv.ParseInt(groupIDStr, 10, 64)
  92. if err != nil {
  93. response.BadRequest(c, "Invalid group_id")
  94. return nil, false
  95. }
  96. groupID = id
  97. }
  98. var requestType *int16
  99. var stream *bool
  100. if requestTypeStr := strings.TrimSpace(c.Query("request_type")); requestTypeStr != "" {
  101. parsed, err := service.ParseUsageRequestType(requestTypeStr)
  102. if err != nil {
  103. response.BadRequest(c, err.Error())
  104. return nil, false
  105. }
  106. value := int16(parsed)
  107. requestType = &value
  108. } else if streamStr := strings.TrimSpace(c.Query("stream")); streamStr != "" {
  109. val, err := strconv.ParseBool(streamStr)
  110. if err != nil {
  111. response.BadRequest(c, "Invalid stream value, use true or false")
  112. return nil, false
  113. }
  114. stream = &val
  115. }
  116. var nativeCompactionV2 *bool
  117. if raw := strings.TrimSpace(c.Query("native_compaction_v2")); raw != "" {
  118. value, err := strconv.ParseBool(raw)
  119. if err != nil {
  120. response.BadRequest(c, "Invalid native_compaction_v2 value, use true or false")
  121. return nil, false
  122. }
  123. nativeCompactionV2 = &value
  124. }
  125. var billingType *int8
  126. if billingTypeStr := strings.TrimSpace(c.Query("billing_type")); billingTypeStr != "" {
  127. val, err := strconv.ParseInt(billingTypeStr, 10, 8)
  128. if err != nil {
  129. response.BadRequest(c, "Invalid billing_type")
  130. return nil, false
  131. }
  132. bt := int8(val)
  133. billingType = &bt
  134. }
  135. billingMode := strings.TrimSpace(c.Query("billing_mode"))
  136. if billingMode != "" && !service.BillingMode(billingMode).IsValidUsageFilter() {
  137. response.BadRequest(c, "Invalid billing_mode")
  138. return nil, false
  139. }
  140. userTZ := c.Query("timezone")
  141. now := timezone.NowInUserLocation(userTZ)
  142. var startTime, endTime time.Time
  143. var startPtr, endPtr *time.Time
  144. startDateStr := strings.TrimSpace(c.Query("start_date"))
  145. endDateStr := strings.TrimSpace(c.Query("end_date"))
  146. if startDateStr != "" {
  147. t, err := timezone.ParseInUserLocation("2006-01-02", startDateStr, userTZ)
  148. if err != nil {
  149. response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD")
  150. return nil, false
  151. }
  152. startTime = t
  153. startPtr = &startTime
  154. }
  155. if endDateStr != "" {
  156. t, err := timezone.ParseInUserLocation("2006-01-02", endDateStr, userTZ)
  157. if err != nil {
  158. response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD")
  159. return nil, false
  160. }
  161. endTime = t.AddDate(0, 0, 1)
  162. endPtr = &endTime
  163. }
  164. if requireRange {
  165. if startPtr == nil {
  166. switch c.DefaultQuery("period", "") {
  167. case "today":
  168. startTime = timezone.StartOfDayInUserLocation(now, userTZ)
  169. case "week":
  170. startTime = now.AddDate(0, 0, -7)
  171. case "month":
  172. startTime = now.AddDate(0, -1, 0)
  173. default:
  174. startTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -7), userTZ)
  175. }
  176. startPtr = &startTime
  177. }
  178. if endPtr == nil {
  179. if strings.TrimSpace(c.Query("period")) != "" {
  180. endTime = now
  181. } else {
  182. endTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
  183. }
  184. endPtr = &endTime
  185. }
  186. }
  187. return &userUsageFilters{
  188. Filters: usagestats.UsageLogFilters{
  189. UserID: subject.UserID,
  190. APIKeyID: apiKeyID,
  191. GroupID: groupID,
  192. Model: strings.TrimSpace(c.Query("model")),
  193. ModelFilterSource: usagestats.ModelSourceRequested,
  194. RequestType: requestType,
  195. Stream: stream,
  196. NativeCompactionV2: nativeCompactionV2,
  197. BillingType: billingType,
  198. BillingMode: billingMode,
  199. StartTime: startPtr,
  200. EndTime: endPtr,
  201. },
  202. StartTime: derefTime(startPtr),
  203. EndTime: derefTime(endPtr),
  204. }, true
  205. }
  206. func derefTime(value *time.Time) time.Time {
  207. if value == nil {
  208. return time.Time{}
  209. }
  210. return *value
  211. }
  212. // List handles listing usage records with pagination
  213. // GET /api/v1/usage
  214. func (h *UsageHandler) List(c *gin.Context) {
  215. page, pageSize := response.ParsePagination(c)
  216. parsed, ok := h.parseUserUsageFilters(c, false)
  217. if !ok {
  218. return
  219. }
  220. params := pagination.PaginationParams{
  221. Page: page,
  222. PageSize: pageSize,
  223. SortBy: c.DefaultQuery("sort_by", "created_at"),
  224. SortOrder: c.DefaultQuery("sort_order", "desc"),
  225. }
  226. records, result, err := h.usageService.ListWithFilters(c.Request.Context(), params, parsed.Filters)
  227. if err != nil {
  228. response.ErrorFrom(c, err)
  229. return
  230. }
  231. out := make([]dto.UsageLog, 0, len(records))
  232. for i := range records {
  233. out = append(out, *dto.UsageLogFromService(&records[i]))
  234. }
  235. response.Paginated(c, out, result.Total, page, pageSize)
  236. }
  237. // ListErrors handles listing the current user's failed requests (redacted).
  238. // GET /api/v1/usage/errors
  239. func (h *UsageHandler) ListErrors(c *gin.Context) {
  240. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  241. if !ok {
  242. response.Unauthorized(c, "User not authenticated")
  243. return
  244. }
  245. // Visibility switch (fail-closed). Defense-in-depth: frontend also hides the tab.
  246. if h.settingService == nil || !h.settingService.IsUserErrorViewAllowed(c.Request.Context()) {
  247. response.Forbidden(c, "Error requests view is disabled")
  248. return
  249. }
  250. if h.opsService == nil {
  251. response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
  252. return
  253. }
  254. page, pageSize := response.ParsePagination(c)
  255. if pageSize > 100 {
  256. pageSize = 100
  257. }
  258. filter := &service.OpsErrorLogFilter{Page: page, PageSize: pageSize}
  259. // Date range (half-open [start, end)), reuse usage-list semantics.
  260. userTZ := c.Query("timezone")
  261. if startDateStr := c.Query("start_date"); startDateStr != "" {
  262. t, err := timezone.ParseInUserLocation("2006-01-02", startDateStr, userTZ)
  263. if err != nil {
  264. response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD")
  265. return
  266. }
  267. filter.StartTime = &t
  268. }
  269. if endDateStr := c.Query("end_date"); endDateStr != "" {
  270. t, err := timezone.ParseInUserLocation("2006-01-02", endDateStr, userTZ)
  271. if err != nil {
  272. response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD")
  273. return
  274. }
  275. t = t.AddDate(0, 0, 1)
  276. filter.EndTime = &t
  277. }
  278. filter.Model = strings.TrimSpace(c.Query("model"))
  279. if k := strings.TrimSpace(c.Query("api_key_id")); k != "" {
  280. n, err := strconv.ParseInt(k, 10, 64)
  281. if err != nil || n < 0 {
  282. response.BadRequest(c, "Invalid api_key_id")
  283. return
  284. }
  285. if n > 0 {
  286. filter.APIKeyID = &n
  287. }
  288. }
  289. if sc := strings.TrimSpace(c.Query("status_code")); sc != "" {
  290. n, err := strconv.Atoi(sc)
  291. if err != nil || n < 0 {
  292. response.BadRequest(c, "Invalid status_code")
  293. return
  294. }
  295. filter.StatusCodes = []int{n}
  296. }
  297. if cat := strings.TrimSpace(c.Query("category")); cat != "" {
  298. phases, types := service.CategoryToFilter(cat)
  299. filter.ErrorPhasesAny = phases
  300. filter.ErrorTypesAny = types
  301. }
  302. // 排序对齐用量明细:列白名单与方向归一在 repo 层,非法值回退 created_at DESC。
  303. filter.SetSort(c.Query("sort_by"), c.Query("sort_order"))
  304. result, err := h.opsService.ListUserErrorRequests(c.Request.Context(), subject.UserID, filter)
  305. if err != nil {
  306. response.ErrorFrom(c, err)
  307. return
  308. }
  309. response.Paginated(c, result.Items, int64(result.Total), result.Page, result.PageSize)
  310. }
  311. // GetErrorDetail handles fetching one of the current user's failed-request details (redacted).
  312. // GET /api/v1/usage/errors/:id
  313. func (h *UsageHandler) GetErrorDetail(c *gin.Context) {
  314. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  315. if !ok {
  316. response.Unauthorized(c, "User not authenticated")
  317. return
  318. }
  319. if h.settingService == nil || !h.settingService.IsUserErrorViewAllowed(c.Request.Context()) {
  320. response.Forbidden(c, "Error requests view is disabled")
  321. return
  322. }
  323. if h.opsService == nil {
  324. response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
  325. return
  326. }
  327. id, err := strconv.ParseInt(strings.TrimSpace(c.Param("id")), 10, 64)
  328. if err != nil || id <= 0 {
  329. response.BadRequest(c, "Invalid id")
  330. return
  331. }
  332. detail, err := h.opsService.GetUserErrorRequestDetail(c.Request.Context(), subject.UserID, id)
  333. if err != nil {
  334. response.ErrorFrom(c, err)
  335. return
  336. }
  337. response.Success(c, detail)
  338. }
  339. // GetByID handles getting a single usage record
  340. // GET /api/v1/usage/:id
  341. func (h *UsageHandler) GetByID(c *gin.Context) {
  342. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  343. if !ok {
  344. response.Unauthorized(c, "User not authenticated")
  345. return
  346. }
  347. usageID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  348. if err != nil {
  349. response.BadRequest(c, "Invalid usage ID")
  350. return
  351. }
  352. record, err := h.usageService.GetByID(c.Request.Context(), usageID)
  353. if err != nil {
  354. response.ErrorFrom(c, err)
  355. return
  356. }
  357. // 验证所有权
  358. if record.UserID != subject.UserID {
  359. response.Forbidden(c, "Not authorized to access this record")
  360. return
  361. }
  362. response.Success(c, dto.UsageLogFromService(record))
  363. }
  364. // Stats handles getting usage statistics
  365. // GET /api/v1/usage/stats
  366. func (h *UsageHandler) Stats(c *gin.Context) {
  367. parsed, ok := h.parseUserUsageFilters(c, true)
  368. if !ok {
  369. return
  370. }
  371. stats, err := h.usageService.GetStatsWithFilters(c.Request.Context(), parsed.Filters)
  372. if err != nil {
  373. response.ErrorFrom(c, err)
  374. return
  375. }
  376. stats.TotalAccountCost = nil
  377. stats.UpstreamEndpoints = nil
  378. stats.EndpointPaths = nil
  379. response.Success(c, stats)
  380. }
  381. const (
  382. defaultAPIKeyDailyUsageDays = 30
  383. maxAPIKeyDailyUsageDays = 90
  384. )
  385. func parseAPIKeyDailyUsageDays(raw string) (int, bool) {
  386. if strings.TrimSpace(raw) == "" {
  387. return defaultAPIKeyDailyUsageDays, true
  388. }
  389. days, err := strconv.Atoi(raw)
  390. if err != nil || days <= 0 || days > maxAPIKeyDailyUsageDays {
  391. return 0, false
  392. }
  393. return days, true
  394. }
  395. func apiKeyDailyUsageRange(days int, userTZ string) (time.Time, time.Time) {
  396. now := timezone.NowInUserLocation(userTZ)
  397. startTime := timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -(days-1)), userTZ)
  398. endTime := timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
  399. return startTime, endTime
  400. }
  401. // DashboardStats handles getting user dashboard statistics
  402. // GET /api/v1/usage/dashboard/stats
  403. func (h *UsageHandler) DashboardStats(c *gin.Context) {
  404. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  405. if !ok {
  406. response.Unauthorized(c, "User not authenticated")
  407. return
  408. }
  409. stats, err := h.usageService.GetUserDashboardStats(c.Request.Context(), subject.UserID)
  410. if err != nil {
  411. response.ErrorFrom(c, err)
  412. return
  413. }
  414. response.Success(c, stats)
  415. }
  416. // DashboardTrend handles getting user usage trend data
  417. // GET /api/v1/usage/dashboard/trend
  418. func (h *UsageHandler) DashboardTrend(c *gin.Context) {
  419. parsed, ok := h.parseUserUsageFilters(c, true)
  420. if !ok {
  421. return
  422. }
  423. granularity := c.DefaultQuery("granularity", "day")
  424. trend, err := h.usageService.GetUsageTrendWithFilters(c.Request.Context(), parsed.StartTime, parsed.EndTime, granularity, parsed.Filters)
  425. if err != nil {
  426. response.ErrorFrom(c, err)
  427. return
  428. }
  429. response.Success(c, gin.H{
  430. "trend": trend,
  431. "start_date": parsed.StartTime.Format("2006-01-02"),
  432. "end_date": parsed.EndTime.Add(-24 * time.Hour).Format("2006-01-02"),
  433. "granularity": granularity,
  434. })
  435. }
  436. // DashboardModels handles getting user model usage statistics
  437. // GET /api/v1/usage/dashboard/models
  438. func (h *UsageHandler) DashboardModels(c *gin.Context) {
  439. parsed, ok := h.parseUserUsageFilters(c, true)
  440. if !ok {
  441. return
  442. }
  443. modelSource := strings.TrimSpace(c.Query("model_source"))
  444. if modelSource != "" && modelSource != usagestats.ModelSourceRequested {
  445. response.BadRequest(c, "Invalid model_source, user usage only supports requested")
  446. return
  447. }
  448. stats, err := h.usageService.GetModelStatsWithFiltersBySource(c.Request.Context(), parsed.StartTime, parsed.EndTime, parsed.Filters, usagestats.ModelSourceRequested)
  449. if err != nil {
  450. response.ErrorFrom(c, err)
  451. return
  452. }
  453. response.Success(c, gin.H{
  454. "models": userModelStatsFromUsageStats(stats),
  455. "start_date": parsed.StartTime.Format("2006-01-02"),
  456. "end_date": parsed.EndTime.Add(-24 * time.Hour).Format("2006-01-02"),
  457. })
  458. }
  459. // DashboardSnapshotV2 returns usage-page chart data scoped to the current user.
  460. // GET /api/v1/usage/dashboard/snapshot-v2
  461. func (h *UsageHandler) DashboardSnapshotV2(c *gin.Context) {
  462. parsed, ok := h.parseUserUsageFilters(c, true)
  463. if !ok {
  464. return
  465. }
  466. granularity := strings.TrimSpace(c.DefaultQuery("granularity", "day"))
  467. if granularity != "hour" {
  468. granularity = "day"
  469. }
  470. includeTrend, ok := parseBoolQueryWithDefault(c, "include_trend", true)
  471. if !ok {
  472. return
  473. }
  474. includeModels, ok := parseBoolQueryWithDefault(c, "include_model_stats", true)
  475. if !ok {
  476. return
  477. }
  478. includeGroups, ok := parseBoolQueryWithDefault(c, "include_group_stats", false)
  479. if !ok {
  480. return
  481. }
  482. resp := gin.H{
  483. "generated_at": time.Now().UTC().Format(time.RFC3339),
  484. "start_date": parsed.StartTime.Format("2006-01-02"),
  485. "end_date": parsed.EndTime.Add(-24 * time.Hour).Format("2006-01-02"),
  486. "granularity": granularity,
  487. }
  488. if includeTrend {
  489. trend, err := h.usageService.GetUsageTrendWithFilters(c.Request.Context(), parsed.StartTime, parsed.EndTime, granularity, parsed.Filters)
  490. if err != nil {
  491. response.ErrorFrom(c, err)
  492. return
  493. }
  494. resp["trend"] = trend
  495. }
  496. if includeModels {
  497. models, err := h.usageService.GetModelStatsWithFiltersBySource(c.Request.Context(), parsed.StartTime, parsed.EndTime, parsed.Filters, usagestats.ModelSourceRequested)
  498. if err != nil {
  499. response.ErrorFrom(c, err)
  500. return
  501. }
  502. resp["models"] = userModelStatsFromUsageStats(models)
  503. }
  504. if includeGroups {
  505. groups, err := h.usageService.GetGroupStatsWithFilters(c.Request.Context(), parsed.StartTime, parsed.EndTime, parsed.Filters)
  506. if err != nil {
  507. response.ErrorFrom(c, err)
  508. return
  509. }
  510. resp["groups"] = userGroupStatsFromUsageStats(groups)
  511. }
  512. response.Success(c, resp)
  513. }
  514. func userModelStatsFromUsageStats(stats []usagestats.ModelStat) []userModelStat {
  515. out := make([]userModelStat, 0, len(stats))
  516. for _, stat := range stats {
  517. out = append(out, userModelStat{
  518. Model: stat.Model,
  519. Requests: stat.Requests,
  520. InputTokens: stat.InputTokens,
  521. OutputTokens: stat.OutputTokens,
  522. CacheCreationTokens: stat.CacheCreationTokens,
  523. CacheReadTokens: stat.CacheReadTokens,
  524. TotalTokens: stat.TotalTokens,
  525. Cost: stat.Cost,
  526. ActualCost: stat.ActualCost,
  527. })
  528. }
  529. return out
  530. }
  531. func userGroupStatsFromUsageStats(stats []usagestats.GroupStat) []userGroupStat {
  532. out := make([]userGroupStat, 0, len(stats))
  533. for _, stat := range stats {
  534. out = append(out, userGroupStat{
  535. GroupID: stat.GroupID,
  536. GroupName: stat.GroupName,
  537. Requests: stat.Requests,
  538. TotalTokens: stat.TotalTokens,
  539. Cost: stat.Cost,
  540. ActualCost: stat.ActualCost,
  541. })
  542. }
  543. return out
  544. }
  545. func parseBoolQueryWithDefault(c *gin.Context, key string, fallback bool) (bool, bool) {
  546. raw := c.Query(key)
  547. if strings.TrimSpace(raw) == "" {
  548. return fallback, true
  549. }
  550. parsed, err := strconv.ParseBool(raw)
  551. if err != nil {
  552. response.BadRequest(c, "Invalid "+key+" value, use true or false")
  553. return false, false
  554. }
  555. return parsed, true
  556. }
  557. // BatchAPIKeysUsageRequest represents the request for batch API keys usage
  558. type BatchAPIKeysUsageRequest struct {
  559. APIKeyIDs []int64 `json:"api_key_ids" binding:"required"`
  560. }
  561. // DashboardAPIKeysUsage handles getting usage stats for user's own API keys
  562. // POST /api/v1/usage/dashboard/api-keys-usage
  563. func (h *UsageHandler) DashboardAPIKeysUsage(c *gin.Context) {
  564. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  565. if !ok {
  566. response.Unauthorized(c, "User not authenticated")
  567. return
  568. }
  569. var req BatchAPIKeysUsageRequest
  570. if err := c.ShouldBindJSON(&req); err != nil {
  571. response.BadRequest(c, "Invalid request: "+err.Error())
  572. return
  573. }
  574. if len(req.APIKeyIDs) == 0 {
  575. response.Success(c, gin.H{"stats": map[string]any{}})
  576. return
  577. }
  578. // Limit the number of API key IDs to prevent SQL parameter overflow
  579. if len(req.APIKeyIDs) > 100 {
  580. response.BadRequest(c, "Too many API key IDs (maximum 100 allowed)")
  581. return
  582. }
  583. validAPIKeyIDs, err := h.apiKeyService.VerifyOwnership(c.Request.Context(), subject.UserID, req.APIKeyIDs)
  584. if err != nil {
  585. response.ErrorFrom(c, err)
  586. return
  587. }
  588. if len(validAPIKeyIDs) == 0 {
  589. response.Success(c, gin.H{"stats": map[string]any{}})
  590. return
  591. }
  592. stats, err := h.usageService.GetBatchAPIKeyUsageStats(c.Request.Context(), validAPIKeyIDs, time.Time{}, time.Time{})
  593. if err != nil {
  594. response.ErrorFrom(c, err)
  595. return
  596. }
  597. response.Success(c, gin.H{"stats": stats})
  598. }
  599. // GetMyAPIKeyDailyUsage handles getting daily usage details for the current user's API key.
  600. // GET /api/v1/user/api-keys/:id/usage/daily?days=30
  601. func (h *UsageHandler) GetMyAPIKeyDailyUsage(c *gin.Context) {
  602. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  603. if !ok {
  604. response.Unauthorized(c, "User not authenticated")
  605. return
  606. }
  607. apiKeyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  608. if err != nil {
  609. response.BadRequest(c, "Invalid API key ID")
  610. return
  611. }
  612. days, ok := parseAPIKeyDailyUsageDays(c.DefaultQuery("days", ""))
  613. if !ok {
  614. response.BadRequest(c, "Invalid days, allowed range is 1-90")
  615. return
  616. }
  617. if h.apiKeyService == nil {
  618. response.InternalError(c, "API key service is not configured")
  619. return
  620. }
  621. apiKey, err := h.apiKeyService.GetByID(c.Request.Context(), apiKeyID)
  622. if err != nil {
  623. response.ErrorFrom(c, err)
  624. return
  625. }
  626. if apiKey.UserID != subject.UserID {
  627. response.Forbidden(c, "Not authorized to access this API key's usage")
  628. return
  629. }
  630. userTZ := c.Query("timezone")
  631. startTime, endTime := apiKeyDailyUsageRange(days, userTZ)
  632. items, err := h.usageService.GetAPIKeyDailyUsage(c.Request.Context(), subject.UserID, apiKeyID, startTime, endTime)
  633. if err != nil {
  634. response.ErrorFrom(c, err)
  635. return
  636. }
  637. response.Success(c, gin.H{
  638. "items": items,
  639. "days": days,
  640. "start_date": startTime.Format("2006-01-02"),
  641. "end_date": endTime.AddDate(0, 0, -1).Format("2006-01-02"),
  642. })
  643. }