audit_log_handler.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. package admin
  2. import (
  3. "net/http"
  4. "strconv"
  5. "strings"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  8. "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/gin-gonic/gin"
  11. )
  12. // AuditLogHandler 操作审计日志管理接口。
  13. // 审计日志仅管理员可见;不提供单条删除,仅支持带 TOTP 验证的全量清空。
  14. type AuditLogHandler struct {
  15. auditService *service.AuditLogService
  16. totpService *service.TotpService
  17. }
  18. // NewAuditLogHandler 创建审计日志处理器。
  19. func NewAuditLogHandler(auditService *service.AuditLogService, totpService *service.TotpService) *AuditLogHandler {
  20. return &AuditLogHandler{
  21. auditService: auditService,
  22. totpService: totpService,
  23. }
  24. }
  25. // List 分页查询审计日志。
  26. // GET /api/v1/admin/audit-logs
  27. func (h *AuditLogHandler) List(c *gin.Context) {
  28. page, pageSize := response.ParsePagination(c)
  29. if pageSize > 200 {
  30. pageSize = 200
  31. }
  32. filter := &service.AuditLogFilter{
  33. Page: page,
  34. PageSize: pageSize,
  35. ActorEmail: strings.TrimSpace(c.Query("actor_email")),
  36. AuthMethod: strings.TrimSpace(c.Query("auth_method")),
  37. Action: strings.TrimSpace(c.Query("action")),
  38. Method: strings.TrimSpace(c.Query("method")),
  39. ClientIP: strings.TrimSpace(c.Query("client_ip")),
  40. Query: strings.TrimSpace(c.Query("q")),
  41. }
  42. if v := strings.TrimSpace(c.Query("actor_user_id")); v != "" {
  43. id, err := strconv.ParseInt(v, 10, 64)
  44. if err != nil || id <= 0 {
  45. response.BadRequest(c, "Invalid actor_user_id")
  46. return
  47. }
  48. filter.ActorUserID = &id
  49. }
  50. if v := strings.TrimSpace(c.Query("start_time")); v != "" {
  51. t, err := time.Parse(time.RFC3339, v)
  52. if err != nil {
  53. response.BadRequest(c, "Invalid start_time, expect RFC3339")
  54. return
  55. }
  56. filter.StartTime = &t
  57. }
  58. if v := strings.TrimSpace(c.Query("end_time")); v != "" {
  59. t, err := time.Parse(time.RFC3339, v)
  60. if err != nil {
  61. response.BadRequest(c, "Invalid end_time, expect RFC3339")
  62. return
  63. }
  64. filter.EndTime = &t
  65. }
  66. if v := strings.TrimSpace(c.Query("success")); v != "" {
  67. success := v == "true"
  68. filter.Success = &success
  69. }
  70. result, err := h.auditService.List(c.Request.Context(), filter)
  71. if err != nil {
  72. response.ErrorFrom(c, err)
  73. return
  74. }
  75. response.Paginated(c, result.Logs, int64(result.Total), result.Page, result.PageSize)
  76. }
  77. // Get 查询单条审计日志详情(含脱敏后的请求体)。
  78. // GET /api/v1/admin/audit-logs/:id
  79. func (h *AuditLogHandler) Get(c *gin.Context) {
  80. id, err := strconv.ParseInt(strings.TrimSpace(c.Param("id")), 10, 64)
  81. if err != nil || id <= 0 {
  82. response.BadRequest(c, "Invalid audit log id")
  83. return
  84. }
  85. item, err := h.auditService.GetByID(c.Request.Context(), id)
  86. if err != nil {
  87. response.ErrorFrom(c, err)
  88. return
  89. }
  90. response.Success(c, item)
  91. }
  92. type auditLogClearRequest struct {
  93. TotpCode string `json:"totp_code" binding:"required"`
  94. }
  95. // Clear 全量清空审计日志。
  96. // POST /api/v1/admin/audit-logs/clear
  97. //
  98. // 安全要求(与需求对齐):
  99. // 1. 每次清空都必须现场验证 TOTP 码(不复用 step-up sudo 窗口)
  100. // 2. 未启用 TOTP 的管理员不允许清空
  101. // 3. admin API key(机器凭证)不允许清空
  102. // 4. 清空完成后同步写入一条留痕记录(操作者、IP、UA、删除行数)
  103. func (h *AuditLogHandler) Clear(c *gin.Context) {
  104. if c.GetString("auth_method") == service.AuditAuthMethodAdminAPIKey {
  105. response.ErrorWithDetails(c, http.StatusForbidden,
  106. "Admin API key cannot clear audit logs; a two-factor verified admin session is required",
  107. "STEP_UP_ADMIN_API_KEY_FORBIDDEN", nil)
  108. return
  109. }
  110. subject, ok := middleware.GetAuthSubjectFromContext(c)
  111. if !ok || subject.UserID <= 0 {
  112. response.Unauthorized(c, "Unauthorized")
  113. return
  114. }
  115. var req auditLogClearRequest
  116. if err := c.ShouldBindJSON(&req); err != nil {
  117. response.ErrorWithDetails(c, http.StatusBadRequest,
  118. "TOTP code is required to clear audit logs", "TOTP_CODE_REQUIRED", nil)
  119. return
  120. }
  121. // 校验 TOTP:未启用(ErrTotpNotSetup)、码错误、尝试过多均在此拦截。
  122. if err := h.totpService.VerifyCode(c.Request.Context(), subject.UserID, strings.TrimSpace(req.TotpCode)); err != nil {
  123. response.ErrorFrom(c, err)
  124. return
  125. }
  126. uid := subject.UserID
  127. role, _ := middleware.GetUserRoleFromContext(c)
  128. trace := &service.AuditLog{
  129. ActorUserID: &uid,
  130. ActorEmail: c.GetString(middleware.ContextKeyAuthEmail),
  131. ActorRole: role,
  132. AuthMethod: c.GetString("auth_method"),
  133. CredentialMasked: middleware.MaskedRequestCredential(c),
  134. Method: http.MethodPost,
  135. Path: c.FullPath(),
  136. ClientIP: middleware.SecurityClientIP(c),
  137. UserAgent: c.Request.UserAgent(),
  138. StatusCode: http.StatusOK,
  139. }
  140. if requestID := c.Writer.Header().Get("X-Request-ID"); requestID != "" {
  141. trace.RequestID = requestID
  142. }
  143. deleted, err := h.auditService.ClearAll(c.Request.Context(), trace)
  144. if err != nil {
  145. response.ErrorFrom(c, err)
  146. return
  147. }
  148. // 留痕记录已同步落库,跳过异步审计中间件的重复记录。
  149. middleware.SkipAudit(c)
  150. response.Success(c, gin.H{"deleted": deleted})
  151. }