| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- // Package handler provides HTTP request handlers for the application.
- package handler
- import (
- "context"
- "errors"
- "math"
- "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"
- middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- )
- // APIKeyHandler handles API key-related requests
- type APIKeyHandler struct {
- apiKeyService *service.APIKeyService
- }
- // NewAPIKeyHandler creates a new APIKeyHandler
- func NewAPIKeyHandler(apiKeyService *service.APIKeyService) *APIKeyHandler {
- return &APIKeyHandler{
- apiKeyService: apiKeyService,
- }
- }
- // CreateAPIKeyRequest represents the create API key request payload
- type CreateAPIKeyRequest struct {
- Name string `json:"name" binding:"required"`
- GroupID *int64 `json:"group_id"` // nullable
- CustomKey *string `json:"custom_key"` // 可选的自定义key
- IPWhitelist []string `json:"ip_whitelist"` // IP 白名单
- IPBlacklist []string `json:"ip_blacklist"` // IP 黑名单
- Quota *float64 `json:"quota"` // 配额限制 (USD)
- ExpiresInDays *int `json:"expires_in_days"` // 过期天数
- // Rate limit fields (0 = unlimited)
- RateLimit5h *float64 `json:"rate_limit_5h"`
- RateLimit1d *float64 `json:"rate_limit_1d"`
- RateLimit7d *float64 `json:"rate_limit_7d"`
- }
- // UpdateAPIKeyRequest represents the update API key request payload
- type UpdateAPIKeyRequest struct {
- Name string `json:"name"`
- GroupID *int64 `json:"group_id"`
- Status string `json:"status" binding:"omitempty,oneof=active inactive"`
- IPWhitelist *[]string `json:"ip_whitelist"` // IP 白名单(nil 不修改,空数组清空)
- IPBlacklist *[]string `json:"ip_blacklist"` // IP 黑名单(nil 不修改,空数组清空)
- Quota *float64 `json:"quota"` // 配额限制 (USD), 0=无限制
- ExpiresAt *string `json:"expires_at"` // 过期时间 (ISO 8601)
- ResetQuota *bool `json:"reset_quota"` // 重置已用配额
- // Rate limit fields (nil = no change, 0 = unlimited)
- RateLimit5h *float64 `json:"rate_limit_5h"`
- RateLimit1d *float64 `json:"rate_limit_1d"`
- RateLimit7d *float64 `json:"rate_limit_7d"`
- ResetRateLimitUsage *bool `json:"reset_rate_limit_usage"` // 重置限速用量
- }
- func validAPIKeyLimit(v float64) bool { return !math.IsNaN(v) && !math.IsInf(v, 0) && v >= 0 }
- func validateAPIKeyCreateRequest(req CreateAPIKeyRequest) error {
- if req.Quota != nil && !validAPIKeyLimit(*req.Quota) {
- return errors.New("invalid quota")
- }
- if req.RateLimit5h != nil && !validAPIKeyLimit(*req.RateLimit5h) {
- return errors.New("invalid rate_limit_5h")
- }
- if req.RateLimit1d != nil && !validAPIKeyLimit(*req.RateLimit1d) {
- return errors.New("invalid rate_limit_1d")
- }
- if req.RateLimit7d != nil && !validAPIKeyLimit(*req.RateLimit7d) {
- return errors.New("invalid rate_limit_7d")
- }
- if req.ExpiresInDays != nil && *req.ExpiresInDays <= 0 {
- return errors.New("invalid expires_in_days")
- }
- return nil
- }
- func validateAPIKeyUpdateRequest(req UpdateAPIKeyRequest) error {
- if req.Quota != nil && !validAPIKeyLimit(*req.Quota) {
- return errors.New("invalid quota")
- }
- if req.RateLimit5h != nil && !validAPIKeyLimit(*req.RateLimit5h) {
- return errors.New("invalid rate_limit_5h")
- }
- if req.RateLimit1d != nil && !validAPIKeyLimit(*req.RateLimit1d) {
- return errors.New("invalid rate_limit_1d")
- }
- if req.RateLimit7d != nil && !validAPIKeyLimit(*req.RateLimit7d) {
- return errors.New("invalid rate_limit_7d")
- }
- return nil
- }
- // List handles listing user's API keys with pagination
- // GET /api/v1/api-keys
- func (h *APIKeyHandler) List(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- page, pageSize := response.ParsePagination(c)
- params := pagination.PaginationParams{
- Page: page,
- PageSize: pageSize,
- SortBy: c.DefaultQuery("sort_by", "created_at"),
- SortOrder: c.DefaultQuery("sort_order", "desc"),
- }
- // Parse filter parameters
- var filters service.APIKeyListFilters
- if search := strings.TrimSpace(c.Query("search")); search != "" {
- if len(search) > 100 {
- search = search[:100]
- }
- filters.Search = search
- }
- filters.Status = c.Query("status")
- if groupIDStr := c.Query("group_id"); groupIDStr != "" {
- gid, err := strconv.ParseInt(groupIDStr, 10, 64)
- if err == nil {
- filters.GroupID = &gid
- }
- }
- keys, result, err := h.apiKeyService.List(c.Request.Context(), subject.UserID, params, filters)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- out := make([]dto.APIKey, 0, len(keys))
- for i := range keys {
- out = append(out, *dto.APIKeyFromService(&keys[i]))
- }
- response.Paginated(c, out, result.Total, page, pageSize)
- }
- // GetByID handles getting a single API key
- // GET /api/v1/api-keys/:id
- func (h *APIKeyHandler) GetByID(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- keyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid key ID")
- return
- }
- key, err := h.apiKeyService.GetByID(c.Request.Context(), keyID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- // 验证所有权
- if key.UserID != subject.UserID {
- response.NotFound(c, "API key not found")
- return
- }
- response.Success(c, dto.APIKeyFromService(key))
- }
- // Create handles creating a new API key
- // POST /api/v1/api-keys
- func (h *APIKeyHandler) Create(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- var req CreateAPIKeyRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- if err := validateAPIKeyCreateRequest(req); err != nil {
- response.BadRequest(c, "Invalid request: numeric limits must be finite and non-negative, and expires_in_days must be greater than zero")
- return
- }
- svcReq := service.CreateAPIKeyRequest{
- Name: req.Name,
- GroupID: req.GroupID,
- CustomKey: req.CustomKey,
- IPWhitelist: req.IPWhitelist,
- IPBlacklist: req.IPBlacklist,
- ExpiresInDays: req.ExpiresInDays,
- }
- if req.Quota != nil {
- svcReq.Quota = *req.Quota
- }
- if req.RateLimit5h != nil {
- svcReq.RateLimit5h = *req.RateLimit5h
- }
- if req.RateLimit1d != nil {
- svcReq.RateLimit1d = *req.RateLimit1d
- }
- if req.RateLimit7d != nil {
- svcReq.RateLimit7d = *req.RateLimit7d
- }
- executeUserIdempotentJSON(c, "user.api_keys.create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
- key, err := h.apiKeyService.Create(ctx, subject.UserID, svcReq)
- if err != nil {
- return nil, err
- }
- return dto.APIKeyFromService(key), nil
- })
- }
- // Update handles updating an API key
- // PUT /api/v1/api-keys/:id
- func (h *APIKeyHandler) Update(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- keyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid key ID")
- return
- }
- var req UpdateAPIKeyRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- if err := validateAPIKeyUpdateRequest(req); err != nil {
- response.BadRequest(c, "Invalid request: numeric limits must be finite and non-negative")
- return
- }
- svcReq := service.UpdateAPIKeyRequest{
- IPWhitelist: req.IPWhitelist,
- IPBlacklist: req.IPBlacklist,
- Quota: req.Quota,
- ResetQuota: req.ResetQuota,
- RateLimit5h: req.RateLimit5h,
- RateLimit1d: req.RateLimit1d,
- RateLimit7d: req.RateLimit7d,
- ResetRateLimitUsage: req.ResetRateLimitUsage,
- }
- if req.Name != "" {
- svcReq.Name = &req.Name
- }
- svcReq.GroupID = req.GroupID
- if req.Status != "" {
- svcReq.Status = &req.Status
- }
- // Parse expires_at if provided
- if req.ExpiresAt != nil {
- if *req.ExpiresAt == "" {
- // Empty string means clear expiration
- svcReq.ExpiresAt = nil
- svcReq.ClearExpiration = true
- } else {
- t, err := time.Parse(time.RFC3339, *req.ExpiresAt)
- if err != nil {
- response.BadRequest(c, "Invalid expires_at format: "+err.Error())
- return
- }
- svcReq.ExpiresAt = &t
- }
- }
- key, err := h.apiKeyService.Update(c.Request.Context(), keyID, subject.UserID, svcReq)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, dto.APIKeyFromService(key))
- }
- // Delete handles deleting an API key
- // DELETE /api/v1/api-keys/:id
- func (h *APIKeyHandler) Delete(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- keyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid key ID")
- return
- }
- err = h.apiKeyService.Delete(c.Request.Context(), keyID, subject.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{"message": "API key deleted successfully"})
- }
- // GetAvailableGroups 获取用户可以绑定的分组列表
- // GET /api/v1/groups/available
- func (h *APIKeyHandler) GetAvailableGroups(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- groups, err := h.apiKeyService.GetAvailableGroups(c.Request.Context(), subject.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- out := make([]dto.Group, 0, len(groups))
- for i := range groups {
- out = append(out, *dto.GroupFromService(&groups[i]))
- }
- response.Success(c, out)
- }
- // GetUserGroupRates 获取当前用户的专属分组倍率配置
- // GET /api/v1/groups/rates
- func (h *APIKeyHandler) GetUserGroupRates(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- rates, err := h.apiKeyService.GetUserGroupRates(c.Request.Context(), subject.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, rates)
- }
|