| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- package admin
- import (
- "context"
- "strconv"
- "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"
- )
- // toResponsePagination converts pagination.PaginationResult to response.PaginationResult
- func toResponsePagination(p *pagination.PaginationResult) *response.PaginationResult {
- if p == nil {
- return nil
- }
- return &response.PaginationResult{
- Total: p.Total,
- Page: p.Page,
- PageSize: p.PageSize,
- Pages: p.Pages,
- }
- }
- // SubscriptionHandler handles admin subscription management
- type SubscriptionHandler struct {
- subscriptionService *service.SubscriptionService
- }
- // NewSubscriptionHandler creates a new admin subscription handler
- func NewSubscriptionHandler(subscriptionService *service.SubscriptionService) *SubscriptionHandler {
- return &SubscriptionHandler{
- subscriptionService: subscriptionService,
- }
- }
- // AssignSubscriptionRequest represents assign subscription request
- type AssignSubscriptionRequest struct {
- UserID int64 `json:"user_id" binding:"required"`
- GroupID int64 `json:"group_id" binding:"required"`
- ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // max 100 years
- Notes string `json:"notes"`
- }
- // BulkAssignSubscriptionRequest represents bulk assign subscription request
- type BulkAssignSubscriptionRequest struct {
- UserIDs []int64 `json:"user_ids" binding:"required,min=1"`
- GroupID int64 `json:"group_id" binding:"required"`
- ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // max 100 years
- Notes string `json:"notes"`
- }
- // AdjustSubscriptionRequest represents adjust subscription request (extend or shorten)
- type AdjustSubscriptionRequest struct {
- Days int `json:"days" binding:"required,min=-36500,max=36500"` // negative to shorten, positive to extend
- }
- // List handles listing all subscriptions with pagination and filters
- // GET /api/v1/admin/subscriptions
- func (h *SubscriptionHandler) List(c *gin.Context) {
- page, pageSize := response.ParsePagination(c)
- // Parse optional filters
- var userID, groupID *int64
- if userIDStr := c.Query("user_id"); userIDStr != "" {
- if id, err := strconv.ParseInt(userIDStr, 10, 64); err == nil {
- userID = &id
- }
- }
- if groupIDStr := c.Query("group_id"); groupIDStr != "" {
- if id, err := strconv.ParseInt(groupIDStr, 10, 64); err == nil {
- groupID = &id
- }
- }
- status := c.Query("status")
- platform := c.Query("platform")
- // Parse sorting parameters
- sortBy := c.DefaultQuery("sort_by", "created_at")
- sortOrder := c.DefaultQuery("sort_order", "desc")
- subscriptions, pagination, err := h.subscriptionService.List(c.Request.Context(), page, pageSize, userID, groupID, status, platform, sortBy, sortOrder)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
- for i := range subscriptions {
- out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
- }
- response.PaginatedWithResult(c, out, toResponsePagination(pagination))
- }
- // GetByID handles getting a subscription by ID
- // GET /api/v1/admin/subscriptions/:id
- func (h *SubscriptionHandler) GetByID(c *gin.Context) {
- subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid subscription ID")
- return
- }
- subscription, err := h.subscriptionService.GetByID(c.Request.Context(), subscriptionID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
- }
- // GetProgress handles getting subscription usage progress
- // GET /api/v1/admin/subscriptions/:id/progress
- func (h *SubscriptionHandler) GetProgress(c *gin.Context) {
- subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid subscription ID")
- return
- }
- progress, err := h.subscriptionService.GetSubscriptionProgress(c.Request.Context(), subscriptionID)
- if err != nil {
- response.NotFound(c, "Subscription not found")
- return
- }
- response.Success(c, progress)
- }
- // Assign handles assigning a subscription to a user
- // POST /api/v1/admin/subscriptions/assign
- func (h *SubscriptionHandler) Assign(c *gin.Context) {
- var req AssignSubscriptionRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- // Get admin user ID from context
- adminID := getAdminIDFromContext(c)
- subscription, err := h.subscriptionService.AssignSubscription(c.Request.Context(), &service.AssignSubscriptionInput{
- UserID: req.UserID,
- GroupID: req.GroupID,
- ValidityDays: req.ValidityDays,
- AssignedBy: adminID,
- Notes: req.Notes,
- })
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
- }
- // BulkAssign handles bulk assigning subscriptions to multiple users
- // POST /api/v1/admin/subscriptions/bulk-assign
- func (h *SubscriptionHandler) BulkAssign(c *gin.Context) {
- var req BulkAssignSubscriptionRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- // Get admin user ID from context
- adminID := getAdminIDFromContext(c)
- result, err := h.subscriptionService.BulkAssignSubscription(c.Request.Context(), &service.BulkAssignSubscriptionInput{
- UserIDs: req.UserIDs,
- GroupID: req.GroupID,
- ValidityDays: req.ValidityDays,
- AssignedBy: adminID,
- Notes: req.Notes,
- })
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, dto.BulkAssignResultFromService(result))
- }
- // Extend handles adjusting a subscription (extend or shorten)
- // POST /api/v1/admin/subscriptions/:id/extend
- func (h *SubscriptionHandler) Extend(c *gin.Context) {
- subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid subscription ID")
- return
- }
- var req AdjustSubscriptionRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- idempotencyPayload := struct {
- SubscriptionID int64 `json:"subscription_id"`
- Body AdjustSubscriptionRequest `json:"body"`
- }{
- SubscriptionID: subscriptionID,
- Body: req,
- }
- executeAdminIdempotentJSON(c, "admin.subscriptions.extend", idempotencyPayload, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
- subscription, execErr := h.subscriptionService.ExtendSubscription(ctx, subscriptionID, req.Days)
- if execErr != nil {
- return nil, execErr
- }
- return dto.UserSubscriptionFromServiceAdmin(subscription), nil
- })
- }
- // ResetSubscriptionQuotaRequest represents the reset quota request
- type ResetSubscriptionQuotaRequest struct {
- Daily bool `json:"daily"`
- Weekly bool `json:"weekly"`
- Monthly bool `json:"monthly"`
- }
- // ResetQuota resets daily, weekly, and/or monthly usage for a subscription.
- // POST /api/v1/admin/subscriptions/:id/reset-quota
- func (h *SubscriptionHandler) ResetQuota(c *gin.Context) {
- subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid subscription ID")
- return
- }
- var req ResetSubscriptionQuotaRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- if !req.Daily && !req.Weekly && !req.Monthly {
- response.BadRequest(c, "At least one of 'daily', 'weekly', or 'monthly' must be true")
- return
- }
- sub, err := h.subscriptionService.AdminResetQuota(c.Request.Context(), subscriptionID, req.Daily, req.Weekly, req.Monthly)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, dto.UserSubscriptionFromServiceAdmin(sub))
- }
- // Revoke handles revoking a subscription.
- // POST /api/v1/admin/subscriptions/:id/revoke
- // DELETE /api/v1/admin/subscriptions/:id is kept for backward compatibility.
- func (h *SubscriptionHandler) Revoke(c *gin.Context) {
- subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid subscription ID")
- return
- }
- err = h.subscriptionService.RevokeSubscription(c.Request.Context(), subscriptionID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{"message": "Subscription revoked successfully"})
- }
- // Restore handles restoring a revoked subscription.
- // POST /api/v1/admin/subscriptions/:id/restore
- func (h *SubscriptionHandler) Restore(c *gin.Context) {
- subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid subscription ID")
- return
- }
- subscription, err := h.subscriptionService.RestoreSubscription(c.Request.Context(), subscriptionID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
- }
- // ListByGroup handles listing subscriptions for a specific group
- // GET /api/v1/admin/groups/:id/subscriptions
- func (h *SubscriptionHandler) ListByGroup(c *gin.Context) {
- groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid group ID")
- return
- }
- page, pageSize := response.ParsePagination(c)
- subscriptions, pagination, err := h.subscriptionService.ListGroupSubscriptions(c.Request.Context(), groupID, page, pageSize)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
- for i := range subscriptions {
- out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
- }
- response.PaginatedWithResult(c, out, toResponsePagination(pagination))
- }
- // ListByUser handles listing subscriptions for a specific user
- // GET /api/v1/admin/users/:id/subscriptions
- func (h *SubscriptionHandler) ListByUser(c *gin.Context) {
- userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid user ID")
- return
- }
- subscriptions, err := h.subscriptionService.ListUserSubscriptions(c.Request.Context(), userID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
- for i := range subscriptions {
- out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
- }
- response.Success(c, out)
- }
- // Helper function to get admin ID from context
- func getAdminIDFromContext(c *gin.Context) int64 {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- return 0
- }
- return subject.UserID
- }
|