| |
| package handler |
|
|
| import ( |
| "context" |
| "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" |
| ) |
|
|
| |
| type APIKeyHandler struct { |
| apiKeyService *service.APIKeyService |
| } |
|
|
| |
| func NewAPIKeyHandler(apiKeyService *service.APIKeyService) *APIKeyHandler { |
| return &APIKeyHandler{ |
| apiKeyService: apiKeyService, |
| } |
| } |
|
|
| |
| type CreateAPIKeyRequest struct { |
| Name string `json:"name" binding:"required"` |
| GroupID *int64 `json:"group_id"` |
| CustomKey *string `json:"custom_key"` |
| IPWhitelist []string `json:"ip_whitelist"` |
| IPBlacklist []string `json:"ip_blacklist"` |
| Quota *float64 `json:"quota"` |
| ExpiresInDays *int `json:"expires_in_days"` |
|
|
| |
| RateLimit5h *float64 `json:"rate_limit_5h"` |
| RateLimit1d *float64 `json:"rate_limit_1d"` |
| RateLimit7d *float64 `json:"rate_limit_7d"` |
| } |
|
|
| |
| 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"` |
| IPBlacklist []string `json:"ip_blacklist"` |
| Quota *float64 `json:"quota"` |
| ExpiresAt *string `json:"expires_at"` |
| ResetQuota *bool `json:"reset_quota"` |
|
|
| |
| 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 (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} |
|
|
| |
| 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) |
| } |
|
|
| |
| |
| 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.Forbidden(c, "Not authorized to access this key") |
| return |
| } |
|
|
| response.Success(c, dto.APIKeyFromService(key)) |
| } |
|
|
| |
| |
| 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 |
| } |
|
|
| 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 |
| }) |
| } |
|
|
| |
| |
| 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 |
| } |
|
|
| 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 |
| } |
| |
| if req.ExpiresAt != nil { |
| if *req.ExpiresAt == "" { |
| |
| 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)) |
| } |
|
|
| |
| |
| 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"}) |
| } |
|
|
| |
| |
| 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) |
| } |
|
|
| |
| |
| 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) |
| } |
|
|