feat(platform): align account user membership
This commit is contained in:
@@ -2,6 +2,7 @@ package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -60,7 +61,7 @@ func (h *PlatformAccountUserHandler) Index(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, accountUsers)
|
||||
c.JSON(http.StatusOK, accountUsers)
|
||||
}
|
||||
|
||||
// Create adds a user to an account.
|
||||
@@ -84,39 +85,21 @@ func (h *PlatformAccountUserHandler) Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"user_id" binding:"required"`
|
||||
Role string `json:"role,omitempty"`
|
||||
UserID uint `json:"user_id" form:"user_id" binding:"required"`
|
||||
Role *string `json:"role,omitempty" form:"role"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Verify PlatformApp has permissible access to this user
|
||||
userPerm, err := h.permissibleRepo.FindByPlatformAppAndResource(c.Request.Context(), platformAppID, model.PermissibleTypeUser, req.UserID)
|
||||
if err != nil || userPerm == nil {
|
||||
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, "non permissible resource for user")
|
||||
return
|
||||
}
|
||||
|
||||
role := req.Role
|
||||
if role == "" {
|
||||
role = "agent"
|
||||
}
|
||||
|
||||
if err := h.accountRepo.AddUserToAccount(c.Request.Context(), accountID, req.UserID, role); err != nil {
|
||||
acctUser, err := h.accountRepo.UpsertAccountUser(c.Request.Context(), accountID, req.UserID, req.Role)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Return the created AccountUser
|
||||
acctUser, err := h.accountRepo.FindAccountUserByUserAndAccount(c.Request.Context(), accountID, req.UserID)
|
||||
if err != nil {
|
||||
response.OK(c, gin.H{"account_id": accountID, "user_id": req.UserID, "role": role})
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, acctUser)
|
||||
c.JSON(http.StatusOK, acctUser)
|
||||
}
|
||||
|
||||
// Destroy removes a user from an account.
|
||||
@@ -130,8 +113,8 @@ func (h *PlatformAccountUserHandler) Destroy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := parseUintParam(c, "user_id")
|
||||
if err != nil {
|
||||
userID, err := platformAccountUserID(c)
|
||||
if err != nil || userID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user ID")
|
||||
return
|
||||
}
|
||||
@@ -150,5 +133,19 @@ func (h *PlatformAccountUserHandler) Destroy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.NoContent(c)
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func platformAccountUserID(c *gin.Context) (uint, error) {
|
||||
if raw := c.Param("user_id"); raw != "" {
|
||||
id, err := strconv.ParseUint(raw, 10, 32)
|
||||
return uint(id), err
|
||||
}
|
||||
var req struct {
|
||||
UserID uint `json:"user_id" form:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return req.UserID, nil
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ func setupPlatformTokenTestE2E(t *testing.T) (*gin.Engine, *repository.Permissib
|
||||
// Gin wildcard constraint: nested routes under accounts/:id must reuse :id.
|
||||
platformGroup.GET("/accounts/:id/account_users", platformAccountUser.Index)
|
||||
platformGroup.POST("/accounts/:id/account_users", platformAccountUser.Create)
|
||||
platformGroup.DELETE("/accounts/:id/account_users/destroy", platformAccountUser.Destroy)
|
||||
platformGroup.DELETE("/accounts/:id/account_users/:user_id", platformAccountUser.Destroy)
|
||||
|
||||
return engine, permissibleRepo, userRepo, accountRepo
|
||||
@@ -403,7 +404,22 @@ func TestPlatformAccountUserE2E_Create(t *testing.T) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
engine.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
var accountUser map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &accountUser))
|
||||
assert.Equal(t, "agent", accountUser["role"])
|
||||
assert.NotContains(t, accountUser, "success")
|
||||
|
||||
// Chatwoot find_or_initialize_by updates existing memberships instead of failing duplicates.
|
||||
acctUserBody = fmt.Sprintf(`{"user_id": %s, "role": "administrator"}`, userID)
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", "/platform/api/v1/accounts/"+accountID+"/account_users", bytes.NewBufferString(acctUserBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
engine.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &accountUser))
|
||||
assert.Equal(t, "administrator", accountUser["role"])
|
||||
}
|
||||
|
||||
func TestPlatformAccountUserE2E_Index(t *testing.T) {
|
||||
@@ -435,12 +451,23 @@ func TestPlatformAccountUserE2E_Index(t *testing.T) {
|
||||
req, _ = http.NewRequest("POST", "/platform/api/v1/accounts/"+accountID+"/account_users", bytes.NewBufferString(acctUserBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
engine.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Index account_users
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/platform/api/v1/accounts/"+accountID+"/account_users", nil)
|
||||
engine.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
var accountUsers []map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &accountUsers))
|
||||
require.Len(t, accountUsers, 1)
|
||||
assert.Equal(t, "agent", accountUsers[0]["role"])
|
||||
assert.NotContains(t, accountUsers[0], "success")
|
||||
|
||||
// Chatwoot destroy is a collection route: DELETE /account_users/destroy with user_id param.
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("DELETE", "/platform/api/v1/accounts/"+accountID+"/account_users/destroy?user_id="+userID, nil)
|
||||
engine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
@@ -100,6 +100,46 @@ func (r *AccountRepo) AddUserToAccount(ctx context.Context, accountID, userID ui
|
||||
return r.db.WithContext(ctx).Create(&au).Error
|
||||
}
|
||||
|
||||
// UpsertAccountUser creates or updates an AccountUser membership.
|
||||
// Reference: Chatwoot Platform::Api::V1::AccountUsersController#create uses
|
||||
// account.account_users.find_or_initialize_by(user_id: ...).update!(params).
|
||||
func (r *AccountRepo) UpsertAccountUser(ctx context.Context, accountID, userID uint, role *string) (*model.AccountUser, error) {
|
||||
var au model.AccountUser
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("account_id = ? AND user_id = ?", accountID, userID).
|
||||
First(&au).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
au = model.AccountUser{
|
||||
AccountID: accountID,
|
||||
UserID: userID,
|
||||
Role: "agent",
|
||||
Availability: "offline",
|
||||
AutoOffline: true,
|
||||
}
|
||||
}
|
||||
if role != nil && *role != "" {
|
||||
au.Role = *role
|
||||
}
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
if err := r.db.WithContext(ctx).Create(&au).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &au, nil
|
||||
}
|
||||
|
||||
if role != nil {
|
||||
if err := r.db.WithContext(ctx).Model(&au).Update("role", au.Role).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &au, nil
|
||||
}
|
||||
|
||||
// RemoveUserFromAccount deletes an AccountUser association.
|
||||
func (r *AccountRepo) RemoveUserFromAccount(ctx context.Context, accountID, userID uint) error {
|
||||
return r.db.WithContext(ctx).Exec(
|
||||
@@ -171,4 +211,4 @@ func (r *AccountRepo) FindAccountUserByUserAndAccount(ctx context.Context, accou
|
||||
return nil, err
|
||||
}
|
||||
return &au, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1830,6 +1830,7 @@ func registerPlatformTokenRoutes(g *gin.RouterGroup, h *Handlers) {
|
||||
// Reference: Chatwoot Platform::Api::V1::AccountUsersController (nested under accounts)
|
||||
g.GET("/accounts/:account_id/account_users", h.PlatformAccountUser.Index)
|
||||
g.POST("/accounts/:account_id/account_users", h.PlatformAccountUser.Create)
|
||||
g.DELETE("/accounts/:account_id/account_users/destroy", h.PlatformAccountUser.Destroy)
|
||||
g.DELETE("/accounts/:account_id/account_users/:user_id", h.PlatformAccountUser.Destroy)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user