feat: complete current GoChat updates

This commit is contained in:
2026-09-13 00:13:55 +08:00
parent 6e62f5094f
commit 0dd188c46f
58 changed files with 2662 additions and 1503 deletions
@@ -10,6 +10,7 @@ import (
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/internal/ws"
applogger "github.com/gochat/gochat/pkg/logger"
"github.com/gochat/gochat/pkg/response"
)
@@ -18,8 +19,9 @@ import (
// Reference: Chatwoot app/controllers/api/v1/accounts/agents_controller.rb
// An "agent" in Chatwoot is a User with an AccountUser membership in a specific account.
type AgentHandler struct {
svc *service.AgentService
audit *service.AuditService
svc *service.AgentService
audit *service.AuditService
events *ws.EventPublisher
}
func (h *AgentHandler) WithAuditService(audit *service.AuditService) *AgentHandler {
@@ -27,6 +29,11 @@ func (h *AgentHandler) WithAuditService(audit *service.AuditService) *AgentHandl
return h
}
func (h *AgentHandler) WithEventPublisher(events *ws.EventPublisher) *AgentHandler {
h.events = events
return h
}
// NewAgentHandler creates a new AgentHandler.
func NewAgentHandler(svc *service.AgentService) *AgentHandler {
return &AgentHandler{svc: svc}
@@ -127,6 +134,7 @@ func (h *AgentHandler) Create(c *gin.Context) {
c.Header("Cache-Control", "no-store")
recordAuditMutation(c, h.audit, auditMutation{AccountID: accountID, AuditableType: "User", AuditableID: agent.ID, Action: "create", AuditedChanges: gin.H{"role": agent.Role, "active": agent.Active}})
publishRealtimeEvent(h.events, accountID, "page:reload", nil)
c.JSON(http.StatusOK, serializeAgentDetail(agent, accountID))
}
@@ -171,6 +179,7 @@ func (h *AgentHandler) Update(c *gin.Context) {
}
recordAuditMutation(c, h.audit, auditMutation{AccountID: accountID, AuditableType: "User", AuditableID: uint(id), Action: "update", AuditedChanges: agentUpdateAuditChanges(req)})
publishRealtimeEvent(h.events, accountID, "page:reload", nil)
c.JSON(http.StatusOK, serializeAgentDetail(agent, accountID))
}
@@ -198,6 +207,7 @@ func (h *AgentHandler) Delete(c *gin.Context) {
}
recordAuditMutation(c, h.audit, auditMutation{AccountID: accountID, AuditableType: "User", AuditableID: uint(id), Action: "destroy", AuditedChanges: gin.H{"account_id": accountID}})
publishRealtimeEvent(h.events, accountID, "page:reload", nil)
c.Status(http.StatusOK)
}
@@ -267,6 +277,7 @@ func (h *AgentHandler) BulkCreate(c *gin.Context) {
}
c.Header("Cache-Control", "no-store")
publishRealtimeEvent(h.events, accountID, "page:reload", nil)
c.JSON(http.StatusOK, serializeAgentDetails(agents, accountID))
}
@@ -169,6 +169,7 @@ func (s *AgentHandlerTestSuite) TestListIgnoresPerPageLikeChatwoot() {
func (s *AgentHandlerTestSuite) TestCreateAgent() {
customRoleID := uint(7)
s.Require().NoError(s.db.Create(&model.CustomRole{ID: customRoleID, AccountID: s.account.ID, Name: "Custom", Permissions: "[]"}).Error)
req := service.CreateAgentRequest{
Email: "agent1@test.com",
Name: "Agent One",
@@ -7,6 +7,7 @@ import (
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/internal/ws"
applogger "github.com/gochat/gochat/pkg/logger"
"github.com/gochat/gochat/pkg/response"
)
@@ -16,6 +17,7 @@ import (
type CustomRoleHandler struct {
svc *service.CustomRoleService
auditSvc *service.AuditService
events *ws.EventPublisher
}
// NewCustomRoleHandler creates a new CustomRole handler.
@@ -28,6 +30,11 @@ func (h *CustomRoleHandler) WithAuditService(auditSvc *service.AuditService) *Cu
return h
}
func (h *CustomRoleHandler) WithEventPublisher(events *ws.EventPublisher) *CustomRoleHandler {
h.events = events
return h
}
// List returns all custom roles for an account.
// GET /api/v1/accounts/:account_id/custom_roles
func (h *CustomRoleHandler) List(c *gin.Context) {
@@ -66,14 +73,18 @@ func (h *CustomRoleHandler) Create(c *gin.Context) {
}
var wrapper struct {
CustomRole service.CreateCustomRoleRequest `json:"custom_role"`
CustomRole *service.CreateCustomRoleRequest `json:"custom_role"`
}
if err := c.ShouldBindJSON(&wrapper); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
if wrapper.CustomRole == nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "custom_role envelope is required")
return
}
role, err := h.svc.Create(c.Request.Context(), accountID, wrapper.CustomRole)
role, err := h.svc.Create(c.Request.Context(), accountID, *wrapper.CustomRole)
if err != nil {
applogger.L().Errorf("Create custom role for account %d: %v", accountID, err)
handleServiceError(c, err)
@@ -86,6 +97,7 @@ func (h *CustomRoleHandler) Create(c *gin.Context) {
Action: "create",
AuditedChanges: role,
})
publishRealtimeEvent(h.events, accountID, "page:reload", nil)
c.JSON(http.StatusOK, serializeCustomRole(role))
}
@@ -140,14 +152,18 @@ func (h *CustomRoleHandler) Update(c *gin.Context) {
}
var wrapper struct {
CustomRole service.UpdateCustomRoleRequest `json:"custom_role"`
CustomRole *service.UpdateCustomRoleRequest `json:"custom_role"`
}
if err := c.ShouldBindJSON(&wrapper); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
if wrapper.CustomRole == nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "custom_role envelope is required")
return
}
role, svcErr := h.svc.Update(c.Request.Context(), id, accountID, wrapper.CustomRole)
role, svcErr := h.svc.Update(c.Request.Context(), id, accountID, *wrapper.CustomRole)
if svcErr != nil {
applogger.L().Errorf("Update custom role %d for account %d: %v", id, accountID, svcErr)
handleServiceError(c, svcErr)
@@ -160,6 +176,7 @@ func (h *CustomRoleHandler) Update(c *gin.Context) {
Action: "update",
AuditedChanges: role,
})
publishRealtimeEvent(h.events, accountID, "page:reload", nil)
c.JSON(http.StatusOK, serializeCustomRole(role))
}
@@ -195,6 +212,7 @@ func (h *CustomRoleHandler) Delete(c *gin.Context) {
Action: "destroy",
AuditedChanges: gin.H{"id": id},
})
publishRealtimeEvent(h.events, accountID, "page:reload", nil)
c.Status(http.StatusOK)
}
@@ -92,6 +92,18 @@ func (s *CustomRoleHandlerTestSuite) TestCreate_BadRequest_EmptyBody() {
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
}
func (s *CustomRoleHandlerTestSuite) TestCreate_BadRequest_RequiresEnvelope() {
r := gin.New()
r.POST("/api/v1/accounts/:account_id/custom_roles", withCustomRoleAdminContext(s.account.ID, s.handler.Create))
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/custom_roles", s.account.ID), bytes.NewBufferString(`{"name":"raw-role","permissions":[]}`))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
}
func (s *CustomRoleHandlerTestSuite) TestGet_BadRequest_InvalidID() {
r := gin.New()
r.GET("/api/v1/accounts/:account_id/custom_roles/:id", withCustomRoleAdminContext(s.account.ID, s.handler.Get))
@@ -115,6 +127,20 @@ func (s *CustomRoleHandlerTestSuite) TestUpdate_BadRequest_InvalidID() {
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
}
func (s *CustomRoleHandlerTestSuite) TestUpdate_BadRequest_RequiresEnvelope() {
role := &model.CustomRole{AccountID: s.account.ID, Name: "raw-update", Permissions: `[]`}
s.Require().NoError(s.db.Create(role).Error)
r := gin.New()
r.PATCH("/api/v1/accounts/:account_id/custom_roles/:id", withCustomRoleAdminContext(s.account.ID, s.handler.Update))
w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/accounts/%d/custom_roles/%d", s.account.ID, role.ID), bytes.NewBufferString(`{"name":"raw-update-edited","permissions":[]}`))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
}
func (s *CustomRoleHandlerTestSuite) TestDelete_BadRequest_InvalidID() {
r := gin.New()
r.DELETE("/api/v1/accounts/:account_id/custom_roles/:id", withCustomRoleAdminContext(s.account.ID, s.handler.Delete))