user_cleanup_handler.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. package admin
  2. import (
  3. "errors"
  4. "strconv"
  5. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  6. "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/gin-gonic/gin"
  9. )
  10. type UserCleanupHandler struct{ service *service.UserCleanupService }
  11. func NewUserCleanupHandler(svc *service.UserCleanupService) *UserCleanupHandler {
  12. return &UserCleanupHandler{service: svc}
  13. }
  14. // Defense in depth: authentication middleware protects routes, and these handlers
  15. // reject ordinary users even when accidentally mounted outside the admin group.
  16. func userCleanupActorID(c *gin.Context) (int64, bool) {
  17. subject, ok := middleware.GetAuthSubjectFromContext(c)
  18. if !ok || subject.UserID <= 0 {
  19. response.Unauthorized(c, "Authentication required")
  20. return 0, false
  21. }
  22. role, ok := middleware.GetUserRoleFromContext(c)
  23. if !ok || (role != "admin" && role != "super_admin") {
  24. response.Forbidden(c, "Administrator access required")
  25. return 0, false
  26. }
  27. return subject.UserID, true
  28. }
  29. func (h *UserCleanupHandler) Summary(c *gin.Context) {
  30. id, ok := userCleanupActorID(c)
  31. if !ok {
  32. return
  33. }
  34. out, err := h.service.Summary(c.Request.Context(), id)
  35. if err != nil {
  36. response.ErrorFrom(c, err)
  37. return
  38. }
  39. response.Success(c, out)
  40. }
  41. func (h *UserCleanupHandler) Preview(c *gin.Context) {
  42. id, ok := userCleanupActorID(c)
  43. if !ok {
  44. return
  45. }
  46. out, err := h.service.Preview(c.Request.Context(), id)
  47. if err != nil {
  48. response.ErrorFrom(c, err)
  49. return
  50. }
  51. response.Success(c, out)
  52. }
  53. func (h *UserCleanupHandler) Execute(c *gin.Context) {
  54. id, ok := userCleanupActorID(c)
  55. if !ok {
  56. return
  57. }
  58. var req struct {
  59. PreviewID string `json:"preview_id" binding:"required,uuid"`
  60. Confirm bool `json:"confirm"`
  61. ExpectedCount int `json:"expected_count" binding:"required,min=1,max=500"`
  62. }
  63. if err := c.ShouldBindJSON(&req); err != nil {
  64. response.BadRequest(c, "Invalid cleanup confirmation")
  65. return
  66. }
  67. if !req.Confirm {
  68. response.ErrorFrom(c, service.ErrUserCleanupConfirmation)
  69. return
  70. }
  71. out, err := h.service.Execute(c.Request.Context(), id, req.PreviewID, req.Confirm, req.ExpectedCount, userCleanupAudit(c))
  72. if err != nil {
  73. if errors.Is(err, service.ErrUserCleanupBusy) {
  74. c.Header("Retry-After", "3")
  75. }
  76. response.ErrorFrom(c, err)
  77. return
  78. }
  79. if out.Replayed {
  80. c.Header("X-Idempotency-Replayed", "true")
  81. }
  82. middleware.SetAuditAction(c, "admin.users.cleanup.request")
  83. middleware.SetAuditExtra(c, map[string]any{"requested_count": req.ExpectedCount, "matched_count": out.DeletedCount, "result": "completed"})
  84. response.Success(c, out)
  85. }
  86. func (h *UserCleanupHandler) GetGuard(c *gin.Context) {
  87. actorID, ok := userCleanupActorID(c)
  88. if !ok {
  89. return
  90. }
  91. id, ok := cleanupTargetID(c)
  92. if !ok {
  93. return
  94. }
  95. out, err := h.service.GetGuard(c.Request.Context(), actorID, id)
  96. if err != nil {
  97. response.ErrorFrom(c, err)
  98. return
  99. }
  100. response.Success(c, out)
  101. }
  102. func (h *UserCleanupHandler) UpdateGuard(c *gin.Context) {
  103. actorID, ok := userCleanupActorID(c)
  104. if !ok {
  105. return
  106. }
  107. id, ok := cleanupTargetID(c)
  108. if !ok {
  109. return
  110. }
  111. var req struct {
  112. IsProtected *bool `json:"is_protected" binding:"required"`
  113. IsSystem *bool `json:"is_system"`
  114. }
  115. if err := c.ShouldBindJSON(&req); err != nil {
  116. response.BadRequest(c, "is_protected is required")
  117. return
  118. }
  119. out, err := h.service.UpdateGuard(c.Request.Context(), actorID, id, *req.IsProtected, req.IsSystem, userCleanupAudit(c))
  120. if err != nil {
  121. response.ErrorFrom(c, err)
  122. return
  123. }
  124. response.Success(c, out)
  125. }
  126. func cleanupTargetID(c *gin.Context) (int64, bool) {
  127. id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  128. if err != nil || id <= 0 {
  129. response.BadRequest(c, "Invalid user ID")
  130. return 0, false
  131. }
  132. return id, true
  133. }
  134. func userCleanupAudit(c *gin.Context) service.UserCleanupAuditContext {
  135. return service.UserCleanupAuditContext{AuthMethod: c.GetString("auth_method"), ClientIP: c.ClientIP(), RequestID: c.GetHeader("X-Request-ID"), Method: c.Request.Method, Path: c.Request.URL.Path}
  136. }