api_key_handler.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. // Package handler provides HTTP request handlers for the application.
  2. package handler
  3. import (
  4. "context"
  5. "errors"
  6. "math"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  12. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  13. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  14. "github.com/Wei-Shaw/sub2api/internal/service"
  15. "github.com/gin-gonic/gin"
  16. )
  17. // APIKeyHandler handles API key-related requests
  18. type APIKeyHandler struct {
  19. apiKeyService *service.APIKeyService
  20. }
  21. // NewAPIKeyHandler creates a new APIKeyHandler
  22. func NewAPIKeyHandler(apiKeyService *service.APIKeyService) *APIKeyHandler {
  23. return &APIKeyHandler{
  24. apiKeyService: apiKeyService,
  25. }
  26. }
  27. // CreateAPIKeyRequest represents the create API key request payload
  28. type CreateAPIKeyRequest struct {
  29. Name string `json:"name" binding:"required"`
  30. GroupID *int64 `json:"group_id"` // nullable
  31. CustomKey *string `json:"custom_key"` // 可选的自定义key
  32. IPWhitelist []string `json:"ip_whitelist"` // IP 白名单
  33. IPBlacklist []string `json:"ip_blacklist"` // IP 黑名单
  34. Quota *float64 `json:"quota"` // 配额限制 (USD)
  35. ExpiresInDays *int `json:"expires_in_days"` // 过期天数
  36. // Rate limit fields (0 = unlimited)
  37. RateLimit5h *float64 `json:"rate_limit_5h"`
  38. RateLimit1d *float64 `json:"rate_limit_1d"`
  39. RateLimit7d *float64 `json:"rate_limit_7d"`
  40. }
  41. // UpdateAPIKeyRequest represents the update API key request payload
  42. type UpdateAPIKeyRequest struct {
  43. Name string `json:"name"`
  44. GroupID *int64 `json:"group_id"`
  45. Status string `json:"status" binding:"omitempty,oneof=active inactive"`
  46. IPWhitelist *[]string `json:"ip_whitelist"` // IP 白名单(nil 不修改,空数组清空)
  47. IPBlacklist *[]string `json:"ip_blacklist"` // IP 黑名单(nil 不修改,空数组清空)
  48. Quota *float64 `json:"quota"` // 配额限制 (USD), 0=无限制
  49. ExpiresAt *string `json:"expires_at"` // 过期时间 (ISO 8601)
  50. ResetQuota *bool `json:"reset_quota"` // 重置已用配额
  51. // Rate limit fields (nil = no change, 0 = unlimited)
  52. RateLimit5h *float64 `json:"rate_limit_5h"`
  53. RateLimit1d *float64 `json:"rate_limit_1d"`
  54. RateLimit7d *float64 `json:"rate_limit_7d"`
  55. ResetRateLimitUsage *bool `json:"reset_rate_limit_usage"` // 重置限速用量
  56. }
  57. func validAPIKeyLimit(v float64) bool { return !math.IsNaN(v) && !math.IsInf(v, 0) && v >= 0 }
  58. func validateAPIKeyCreateRequest(req CreateAPIKeyRequest) error {
  59. if req.Quota != nil && !validAPIKeyLimit(*req.Quota) {
  60. return errors.New("invalid quota")
  61. }
  62. if req.RateLimit5h != nil && !validAPIKeyLimit(*req.RateLimit5h) {
  63. return errors.New("invalid rate_limit_5h")
  64. }
  65. if req.RateLimit1d != nil && !validAPIKeyLimit(*req.RateLimit1d) {
  66. return errors.New("invalid rate_limit_1d")
  67. }
  68. if req.RateLimit7d != nil && !validAPIKeyLimit(*req.RateLimit7d) {
  69. return errors.New("invalid rate_limit_7d")
  70. }
  71. if req.ExpiresInDays != nil && *req.ExpiresInDays <= 0 {
  72. return errors.New("invalid expires_in_days")
  73. }
  74. return nil
  75. }
  76. func validateAPIKeyUpdateRequest(req UpdateAPIKeyRequest) error {
  77. if req.Quota != nil && !validAPIKeyLimit(*req.Quota) {
  78. return errors.New("invalid quota")
  79. }
  80. if req.RateLimit5h != nil && !validAPIKeyLimit(*req.RateLimit5h) {
  81. return errors.New("invalid rate_limit_5h")
  82. }
  83. if req.RateLimit1d != nil && !validAPIKeyLimit(*req.RateLimit1d) {
  84. return errors.New("invalid rate_limit_1d")
  85. }
  86. if req.RateLimit7d != nil && !validAPIKeyLimit(*req.RateLimit7d) {
  87. return errors.New("invalid rate_limit_7d")
  88. }
  89. return nil
  90. }
  91. // List handles listing user's API keys with pagination
  92. // GET /api/v1/api-keys
  93. func (h *APIKeyHandler) List(c *gin.Context) {
  94. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  95. if !ok {
  96. response.Unauthorized(c, "User not authenticated")
  97. return
  98. }
  99. page, pageSize := response.ParsePagination(c)
  100. params := pagination.PaginationParams{
  101. Page: page,
  102. PageSize: pageSize,
  103. SortBy: c.DefaultQuery("sort_by", "created_at"),
  104. SortOrder: c.DefaultQuery("sort_order", "desc"),
  105. }
  106. // Parse filter parameters
  107. var filters service.APIKeyListFilters
  108. if search := strings.TrimSpace(c.Query("search")); search != "" {
  109. if len(search) > 100 {
  110. search = search[:100]
  111. }
  112. filters.Search = search
  113. }
  114. filters.Status = c.Query("status")
  115. if groupIDStr := c.Query("group_id"); groupIDStr != "" {
  116. gid, err := strconv.ParseInt(groupIDStr, 10, 64)
  117. if err == nil {
  118. filters.GroupID = &gid
  119. }
  120. }
  121. keys, result, err := h.apiKeyService.List(c.Request.Context(), subject.UserID, params, filters)
  122. if err != nil {
  123. response.ErrorFrom(c, err)
  124. return
  125. }
  126. out := make([]dto.APIKey, 0, len(keys))
  127. for i := range keys {
  128. out = append(out, *dto.APIKeyFromService(&keys[i]))
  129. }
  130. response.Paginated(c, out, result.Total, page, pageSize)
  131. }
  132. // GetByID handles getting a single API key
  133. // GET /api/v1/api-keys/:id
  134. func (h *APIKeyHandler) GetByID(c *gin.Context) {
  135. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  136. if !ok {
  137. response.Unauthorized(c, "User not authenticated")
  138. return
  139. }
  140. keyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  141. if err != nil {
  142. response.BadRequest(c, "Invalid key ID")
  143. return
  144. }
  145. key, err := h.apiKeyService.GetByID(c.Request.Context(), keyID)
  146. if err != nil {
  147. response.ErrorFrom(c, err)
  148. return
  149. }
  150. // 验证所有权
  151. if key.UserID != subject.UserID {
  152. response.NotFound(c, "API key not found")
  153. return
  154. }
  155. response.Success(c, dto.APIKeyFromService(key))
  156. }
  157. // Create handles creating a new API key
  158. // POST /api/v1/api-keys
  159. func (h *APIKeyHandler) Create(c *gin.Context) {
  160. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  161. if !ok {
  162. response.Unauthorized(c, "User not authenticated")
  163. return
  164. }
  165. var req CreateAPIKeyRequest
  166. if err := c.ShouldBindJSON(&req); err != nil {
  167. response.BadRequest(c, "Invalid request: "+err.Error())
  168. return
  169. }
  170. if err := validateAPIKeyCreateRequest(req); err != nil {
  171. response.BadRequest(c, "Invalid request: numeric limits must be finite and non-negative, and expires_in_days must be greater than zero")
  172. return
  173. }
  174. svcReq := service.CreateAPIKeyRequest{
  175. Name: req.Name,
  176. GroupID: req.GroupID,
  177. CustomKey: req.CustomKey,
  178. IPWhitelist: req.IPWhitelist,
  179. IPBlacklist: req.IPBlacklist,
  180. ExpiresInDays: req.ExpiresInDays,
  181. }
  182. if req.Quota != nil {
  183. svcReq.Quota = *req.Quota
  184. }
  185. if req.RateLimit5h != nil {
  186. svcReq.RateLimit5h = *req.RateLimit5h
  187. }
  188. if req.RateLimit1d != nil {
  189. svcReq.RateLimit1d = *req.RateLimit1d
  190. }
  191. if req.RateLimit7d != nil {
  192. svcReq.RateLimit7d = *req.RateLimit7d
  193. }
  194. executeUserIdempotentJSON(c, "user.api_keys.create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
  195. key, err := h.apiKeyService.Create(ctx, subject.UserID, svcReq)
  196. if err != nil {
  197. return nil, err
  198. }
  199. return dto.APIKeyFromService(key), nil
  200. })
  201. }
  202. // Update handles updating an API key
  203. // PUT /api/v1/api-keys/:id
  204. func (h *APIKeyHandler) Update(c *gin.Context) {
  205. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  206. if !ok {
  207. response.Unauthorized(c, "User not authenticated")
  208. return
  209. }
  210. keyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  211. if err != nil {
  212. response.BadRequest(c, "Invalid key ID")
  213. return
  214. }
  215. var req UpdateAPIKeyRequest
  216. if err := c.ShouldBindJSON(&req); err != nil {
  217. response.BadRequest(c, "Invalid request: "+err.Error())
  218. return
  219. }
  220. if err := validateAPIKeyUpdateRequest(req); err != nil {
  221. response.BadRequest(c, "Invalid request: numeric limits must be finite and non-negative")
  222. return
  223. }
  224. svcReq := service.UpdateAPIKeyRequest{
  225. IPWhitelist: req.IPWhitelist,
  226. IPBlacklist: req.IPBlacklist,
  227. Quota: req.Quota,
  228. ResetQuota: req.ResetQuota,
  229. RateLimit5h: req.RateLimit5h,
  230. RateLimit1d: req.RateLimit1d,
  231. RateLimit7d: req.RateLimit7d,
  232. ResetRateLimitUsage: req.ResetRateLimitUsage,
  233. }
  234. if req.Name != "" {
  235. svcReq.Name = &req.Name
  236. }
  237. svcReq.GroupID = req.GroupID
  238. if req.Status != "" {
  239. svcReq.Status = &req.Status
  240. }
  241. // Parse expires_at if provided
  242. if req.ExpiresAt != nil {
  243. if *req.ExpiresAt == "" {
  244. // Empty string means clear expiration
  245. svcReq.ExpiresAt = nil
  246. svcReq.ClearExpiration = true
  247. } else {
  248. t, err := time.Parse(time.RFC3339, *req.ExpiresAt)
  249. if err != nil {
  250. response.BadRequest(c, "Invalid expires_at format: "+err.Error())
  251. return
  252. }
  253. svcReq.ExpiresAt = &t
  254. }
  255. }
  256. key, err := h.apiKeyService.Update(c.Request.Context(), keyID, subject.UserID, svcReq)
  257. if err != nil {
  258. response.ErrorFrom(c, err)
  259. return
  260. }
  261. response.Success(c, dto.APIKeyFromService(key))
  262. }
  263. // Delete handles deleting an API key
  264. // DELETE /api/v1/api-keys/:id
  265. func (h *APIKeyHandler) Delete(c *gin.Context) {
  266. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  267. if !ok {
  268. response.Unauthorized(c, "User not authenticated")
  269. return
  270. }
  271. keyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  272. if err != nil {
  273. response.BadRequest(c, "Invalid key ID")
  274. return
  275. }
  276. err = h.apiKeyService.Delete(c.Request.Context(), keyID, subject.UserID)
  277. if err != nil {
  278. response.ErrorFrom(c, err)
  279. return
  280. }
  281. response.Success(c, gin.H{"message": "API key deleted successfully"})
  282. }
  283. // GetAvailableGroups 获取用户可以绑定的分组列表
  284. // GET /api/v1/groups/available
  285. func (h *APIKeyHandler) GetAvailableGroups(c *gin.Context) {
  286. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  287. if !ok {
  288. response.Unauthorized(c, "User not authenticated")
  289. return
  290. }
  291. groups, err := h.apiKeyService.GetAvailableGroups(c.Request.Context(), subject.UserID)
  292. if err != nil {
  293. response.ErrorFrom(c, err)
  294. return
  295. }
  296. out := make([]dto.Group, 0, len(groups))
  297. for i := range groups {
  298. out = append(out, *dto.GroupFromService(&groups[i]))
  299. }
  300. response.Success(c, out)
  301. }
  302. // GetUserGroupRates 获取当前用户的专属分组倍率配置
  303. // GET /api/v1/groups/rates
  304. func (h *APIKeyHandler) GetUserGroupRates(c *gin.Context) {
  305. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  306. if !ok {
  307. response.Unauthorized(c, "User not authenticated")
  308. return
  309. }
  310. rates, err := h.apiKeyService.GetUserGroupRates(c.Request.Context(), subject.UserID)
  311. if err != nil {
  312. response.ErrorFrom(c, err)
  313. return
  314. }
  315. response.Success(c, rates)
  316. }