subscription_handler.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. package admin
  2. import (
  3. "context"
  4. "strconv"
  5. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  8. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/gin-gonic/gin"
  11. )
  12. // toResponsePagination converts pagination.PaginationResult to response.PaginationResult
  13. func toResponsePagination(p *pagination.PaginationResult) *response.PaginationResult {
  14. if p == nil {
  15. return nil
  16. }
  17. return &response.PaginationResult{
  18. Total: p.Total,
  19. Page: p.Page,
  20. PageSize: p.PageSize,
  21. Pages: p.Pages,
  22. }
  23. }
  24. // SubscriptionHandler handles admin subscription management
  25. type SubscriptionHandler struct {
  26. subscriptionService *service.SubscriptionService
  27. }
  28. // NewSubscriptionHandler creates a new admin subscription handler
  29. func NewSubscriptionHandler(subscriptionService *service.SubscriptionService) *SubscriptionHandler {
  30. return &SubscriptionHandler{
  31. subscriptionService: subscriptionService,
  32. }
  33. }
  34. // AssignSubscriptionRequest represents assign subscription request
  35. type AssignSubscriptionRequest struct {
  36. UserID int64 `json:"user_id" binding:"required"`
  37. GroupID int64 `json:"group_id" binding:"required"`
  38. ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // max 100 years
  39. Notes string `json:"notes"`
  40. }
  41. // BulkAssignSubscriptionRequest represents bulk assign subscription request
  42. type BulkAssignSubscriptionRequest struct {
  43. UserIDs []int64 `json:"user_ids" binding:"required,min=1"`
  44. GroupID int64 `json:"group_id" binding:"required"`
  45. ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // max 100 years
  46. Notes string `json:"notes"`
  47. }
  48. // AdjustSubscriptionRequest represents adjust subscription request (extend or shorten)
  49. type AdjustSubscriptionRequest struct {
  50. Days int `json:"days" binding:"required,min=-36500,max=36500"` // negative to shorten, positive to extend
  51. }
  52. // List handles listing all subscriptions with pagination and filters
  53. // GET /api/v1/admin/subscriptions
  54. func (h *SubscriptionHandler) List(c *gin.Context) {
  55. page, pageSize := response.ParsePagination(c)
  56. // Parse optional filters
  57. var userID, groupID *int64
  58. if userIDStr := c.Query("user_id"); userIDStr != "" {
  59. if id, err := strconv.ParseInt(userIDStr, 10, 64); err == nil {
  60. userID = &id
  61. }
  62. }
  63. if groupIDStr := c.Query("group_id"); groupIDStr != "" {
  64. if id, err := strconv.ParseInt(groupIDStr, 10, 64); err == nil {
  65. groupID = &id
  66. }
  67. }
  68. status := c.Query("status")
  69. platform := c.Query("platform")
  70. // Parse sorting parameters
  71. sortBy := c.DefaultQuery("sort_by", "created_at")
  72. sortOrder := c.DefaultQuery("sort_order", "desc")
  73. subscriptions, pagination, err := h.subscriptionService.List(c.Request.Context(), page, pageSize, userID, groupID, status, platform, sortBy, sortOrder)
  74. if err != nil {
  75. response.ErrorFrom(c, err)
  76. return
  77. }
  78. out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
  79. for i := range subscriptions {
  80. out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
  81. }
  82. response.PaginatedWithResult(c, out, toResponsePagination(pagination))
  83. }
  84. // GetByID handles getting a subscription by ID
  85. // GET /api/v1/admin/subscriptions/:id
  86. func (h *SubscriptionHandler) GetByID(c *gin.Context) {
  87. subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  88. if err != nil {
  89. response.BadRequest(c, "Invalid subscription ID")
  90. return
  91. }
  92. subscription, err := h.subscriptionService.GetByID(c.Request.Context(), subscriptionID)
  93. if err != nil {
  94. response.ErrorFrom(c, err)
  95. return
  96. }
  97. response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
  98. }
  99. // GetProgress handles getting subscription usage progress
  100. // GET /api/v1/admin/subscriptions/:id/progress
  101. func (h *SubscriptionHandler) GetProgress(c *gin.Context) {
  102. subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  103. if err != nil {
  104. response.BadRequest(c, "Invalid subscription ID")
  105. return
  106. }
  107. progress, err := h.subscriptionService.GetSubscriptionProgress(c.Request.Context(), subscriptionID)
  108. if err != nil {
  109. response.NotFound(c, "Subscription not found")
  110. return
  111. }
  112. response.Success(c, progress)
  113. }
  114. // Assign handles assigning a subscription to a user
  115. // POST /api/v1/admin/subscriptions/assign
  116. func (h *SubscriptionHandler) Assign(c *gin.Context) {
  117. var req AssignSubscriptionRequest
  118. if err := c.ShouldBindJSON(&req); err != nil {
  119. response.BadRequest(c, "Invalid request: "+err.Error())
  120. return
  121. }
  122. // Get admin user ID from context
  123. adminID := getAdminIDFromContext(c)
  124. subscription, err := h.subscriptionService.AssignSubscription(c.Request.Context(), &service.AssignSubscriptionInput{
  125. UserID: req.UserID,
  126. GroupID: req.GroupID,
  127. ValidityDays: req.ValidityDays,
  128. AssignedBy: adminID,
  129. Notes: req.Notes,
  130. })
  131. if err != nil {
  132. response.ErrorFrom(c, err)
  133. return
  134. }
  135. response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
  136. }
  137. // BulkAssign handles bulk assigning subscriptions to multiple users
  138. // POST /api/v1/admin/subscriptions/bulk-assign
  139. func (h *SubscriptionHandler) BulkAssign(c *gin.Context) {
  140. var req BulkAssignSubscriptionRequest
  141. if err := c.ShouldBindJSON(&req); err != nil {
  142. response.BadRequest(c, "Invalid request: "+err.Error())
  143. return
  144. }
  145. // Get admin user ID from context
  146. adminID := getAdminIDFromContext(c)
  147. result, err := h.subscriptionService.BulkAssignSubscription(c.Request.Context(), &service.BulkAssignSubscriptionInput{
  148. UserIDs: req.UserIDs,
  149. GroupID: req.GroupID,
  150. ValidityDays: req.ValidityDays,
  151. AssignedBy: adminID,
  152. Notes: req.Notes,
  153. })
  154. if err != nil {
  155. response.ErrorFrom(c, err)
  156. return
  157. }
  158. response.Success(c, dto.BulkAssignResultFromService(result))
  159. }
  160. // Extend handles adjusting a subscription (extend or shorten)
  161. // POST /api/v1/admin/subscriptions/:id/extend
  162. func (h *SubscriptionHandler) Extend(c *gin.Context) {
  163. subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  164. if err != nil {
  165. response.BadRequest(c, "Invalid subscription ID")
  166. return
  167. }
  168. var req AdjustSubscriptionRequest
  169. if err := c.ShouldBindJSON(&req); err != nil {
  170. response.BadRequest(c, "Invalid request: "+err.Error())
  171. return
  172. }
  173. idempotencyPayload := struct {
  174. SubscriptionID int64 `json:"subscription_id"`
  175. Body AdjustSubscriptionRequest `json:"body"`
  176. }{
  177. SubscriptionID: subscriptionID,
  178. Body: req,
  179. }
  180. executeAdminIdempotentJSON(c, "admin.subscriptions.extend", idempotencyPayload, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
  181. subscription, execErr := h.subscriptionService.ExtendSubscription(ctx, subscriptionID, req.Days)
  182. if execErr != nil {
  183. return nil, execErr
  184. }
  185. return dto.UserSubscriptionFromServiceAdmin(subscription), nil
  186. })
  187. }
  188. // ResetSubscriptionQuotaRequest represents the reset quota request
  189. type ResetSubscriptionQuotaRequest struct {
  190. Daily bool `json:"daily"`
  191. Weekly bool `json:"weekly"`
  192. Monthly bool `json:"monthly"`
  193. }
  194. // ResetQuota resets daily, weekly, and/or monthly usage for a subscription.
  195. // POST /api/v1/admin/subscriptions/:id/reset-quota
  196. func (h *SubscriptionHandler) ResetQuota(c *gin.Context) {
  197. subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  198. if err != nil {
  199. response.BadRequest(c, "Invalid subscription ID")
  200. return
  201. }
  202. var req ResetSubscriptionQuotaRequest
  203. if err := c.ShouldBindJSON(&req); err != nil {
  204. response.BadRequest(c, "Invalid request: "+err.Error())
  205. return
  206. }
  207. if !req.Daily && !req.Weekly && !req.Monthly {
  208. response.BadRequest(c, "At least one of 'daily', 'weekly', or 'monthly' must be true")
  209. return
  210. }
  211. sub, err := h.subscriptionService.AdminResetQuota(c.Request.Context(), subscriptionID, req.Daily, req.Weekly, req.Monthly)
  212. if err != nil {
  213. response.ErrorFrom(c, err)
  214. return
  215. }
  216. response.Success(c, dto.UserSubscriptionFromServiceAdmin(sub))
  217. }
  218. // Revoke handles revoking a subscription.
  219. // POST /api/v1/admin/subscriptions/:id/revoke
  220. // DELETE /api/v1/admin/subscriptions/:id is kept for backward compatibility.
  221. func (h *SubscriptionHandler) Revoke(c *gin.Context) {
  222. subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  223. if err != nil {
  224. response.BadRequest(c, "Invalid subscription ID")
  225. return
  226. }
  227. err = h.subscriptionService.RevokeSubscription(c.Request.Context(), subscriptionID)
  228. if err != nil {
  229. response.ErrorFrom(c, err)
  230. return
  231. }
  232. response.Success(c, gin.H{"message": "Subscription revoked successfully"})
  233. }
  234. // Restore handles restoring a revoked subscription.
  235. // POST /api/v1/admin/subscriptions/:id/restore
  236. func (h *SubscriptionHandler) Restore(c *gin.Context) {
  237. subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  238. if err != nil {
  239. response.BadRequest(c, "Invalid subscription ID")
  240. return
  241. }
  242. subscription, err := h.subscriptionService.RestoreSubscription(c.Request.Context(), subscriptionID)
  243. if err != nil {
  244. response.ErrorFrom(c, err)
  245. return
  246. }
  247. response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
  248. }
  249. // ListByGroup handles listing subscriptions for a specific group
  250. // GET /api/v1/admin/groups/:id/subscriptions
  251. func (h *SubscriptionHandler) ListByGroup(c *gin.Context) {
  252. groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  253. if err != nil {
  254. response.BadRequest(c, "Invalid group ID")
  255. return
  256. }
  257. page, pageSize := response.ParsePagination(c)
  258. subscriptions, pagination, err := h.subscriptionService.ListGroupSubscriptions(c.Request.Context(), groupID, page, pageSize)
  259. if err != nil {
  260. response.ErrorFrom(c, err)
  261. return
  262. }
  263. out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
  264. for i := range subscriptions {
  265. out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
  266. }
  267. response.PaginatedWithResult(c, out, toResponsePagination(pagination))
  268. }
  269. // ListByUser handles listing subscriptions for a specific user
  270. // GET /api/v1/admin/users/:id/subscriptions
  271. func (h *SubscriptionHandler) ListByUser(c *gin.Context) {
  272. userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  273. if err != nil {
  274. response.BadRequest(c, "Invalid user ID")
  275. return
  276. }
  277. subscriptions, err := h.subscriptionService.ListUserSubscriptions(c.Request.Context(), userID)
  278. if err != nil {
  279. response.ErrorFrom(c, err)
  280. return
  281. }
  282. out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
  283. for i := range subscriptions {
  284. out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
  285. }
  286. response.Success(c, out)
  287. }
  288. // Helper function to get admin ID from context
  289. func getAdminIDFromContext(c *gin.Context) int64 {
  290. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  291. if !ok {
  292. return 0
  293. }
  294. return subject.UserID
  295. }