- Add missing IncrementQuotaUsed method to stubApiKeyRepo in api_contract_test.go - Fix gofmt formatting issues in api_key_service.go, dto/types.go, api_key_handler.go
246 lines
6.6 KiB
Go
246 lines
6.6 KiB
Go
// Package handler provides HTTP request handlers for the application.
|
|
package handler
|
|
|
|
import (
|
|
"strconv"
|
|
"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"` // 过期天数
|
|
}
|
|
|
|
// 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 白名单
|
|
IPBlacklist []string `json:"ip_blacklist"` // IP 黑名单
|
|
Quota *float64 `json:"quota"` // 配额限制 (USD), 0=无限制
|
|
ExpiresAt *string `json:"expires_at"` // 过期时间 (ISO 8601)
|
|
ResetQuota *bool `json:"reset_quota"` // 重置已用配额
|
|
}
|
|
|
|
// 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}
|
|
|
|
keys, result, err := h.apiKeyService.List(c.Request.Context(), subject.UserID, params)
|
|
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.Forbidden(c, "Not authorized to access this key")
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
key, err := h.apiKeyService.Create(c.Request.Context(), subject.UserID, svcReq)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.APIKeyFromService(key))
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
svcReq := service.UpdateAPIKeyRequest{
|
|
IPWhitelist: req.IPWhitelist,
|
|
IPBlacklist: req.IPBlacklist,
|
|
Quota: req.Quota,
|
|
ResetQuota: req.ResetQuota,
|
|
}
|
|
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)
|
|
}
|