package v1 import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gochat/gochat/pkg/response" ) const auditLogsPerPage = 25 // AuditHandler handles audit log listing and retrieval. // Reference: Chatwoot enterprise audit logs controller + P2B M11 spec type AuditHandler struct { svc *service.AuditService } // NewAuditHandler creates a new AuditLog handler. func NewAuditHandler(svc *service.AuditService) *AuditHandler { return &AuditHandler{svc: svc} } // List retrieves paginated audit log entries for the current account. // GET /api/v1/accounts/:account_id/audit_logs // Query filters: action, auditable_type func (h *AuditHandler) List(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } if !isAuditAdmin(c) { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "administrator role required") return } page := parseAuditPage(c.Query("page")) action := c.Query("action") auditableType := c.Query("auditable_type") audits, total, svcErr := h.svc.ListByAccount(c.Request.Context(), accountID, action, auditableType, page, auditLogsPerPage) if svcErr != nil { applogger.L().Errorf("AuditHandler.List account=%d: %v", accountID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, gin.H{ "per_page": auditLogsPerPage, "total_entries": total, "current_page": page, "audit_logs": serializeAuditLogs(audits), }) } // Get retrieves a single audit log entry by ID. // GET /api/v1/accounts/:account_id/audit_logs/:id func (h *AuditHandler) Get(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } if !isAuditAdmin(c) { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "administrator role required") return } id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid audit log id") return } audit, svcErr := h.svc.GetByIDForAccount(c.Request.Context(), accountID, uint(id)) if svcErr != nil { applogger.L().Errorf("AuditHandler.Get id=%d: %v", id, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeAuditLog(*audit)) } // RegisterAuditRoutes registers audit log routes on a gin.RouterGroup. func RegisterAuditRoutes(rg *gin.RouterGroup, h *AuditHandler) { audits := rg.Group("/audit_logs") { audits.GET("/", h.List) audits.GET("/:id", h.Get) } } func isAuditAdmin(c *gin.Context) bool { role := getRole(c) return role == "administrator" || role == "super_admin" } func parseAuditPage(raw string) int { page, err := strconv.Atoi(raw) if err != nil || page < 1 { return 1 } return page } func serializeAuditLogs(audits []model.Audit) []gin.H { items := make([]gin.H, 0, len(audits)) for _, audit := range audits { items = append(items, serializeAuditLog(audit)) } return items } func serializeAuditLog(audit model.Audit) gin.H { return gin.H{ "id": audit.ID, "auditable_id": audit.AuditableID, "auditable_type": audit.AuditableType, "auditable": nil, "associated_id": audit.AssociatedID, "associated_type": audit.AssociatedType, "user_id": audit.UserID, "user_type": audit.UserType, "username": audit.Username, "action": audit.Action, "audited_changes": audit.AuditedChanges, "version": audit.Version, "comment": audit.Comment, "request_uuid": audit.RequestUUID, "created_at": audit.CreatedAt.Unix(), "remote_address": audit.RemoteAddress, } }