Files
gochat/backend/internal/handler/api/v1/platform_user_handler.go
T
Rogeeandrogee 8d5d019bb5 HH-548: allow Super Admin sessions to load platform lists (#125)
* fix(HH-548): authorize super admin platform lists

* fix(HH-548): limit dual auth to platform lists

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-23 20:21:05 +08:00

309 lines
9.2 KiB
Go

package v1
import (
"net/http"
"net/url"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/pagination"
"github.com/gochat/gochat/pkg/response"
)
// PlatformUserHandler handles Platform API user endpoints (AccessToken auth).
// Reference: Chatwoot Platform::Api::V1::UsersController — AccessToken authenticated
//
// Distinct from SuperAdmin-platform routes: uses api_access_token header
// authentication + Permissible system for resource-level access control.
type PlatformUserHandler struct {
svc *service.PlatformUserService
}
// NewPlatformUserHandler creates a new PlatformUser handler with service injection.
func NewPlatformUserHandler(svc *service.PlatformUserService) *PlatformUserHandler {
return &PlatformUserHandler{svc: svc}
}
// Show retrieves a user by ID.
// GET /platform/api/v1/users/:id
// Reference: Chatwoot Platform::Api::V1::UsersController#show
// Requires: Permissible verification (PlatformApp must have access to this user)
func (h *PlatformUserHandler) Show(c *gin.Context) {
userID, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user ID")
return
}
platformAppID := getPlatformAppID(c)
user, err := h.svc.GetUserResponse(c.Request.Context(), platformAppID, userID)
if err != nil {
handlePlatformError(c, err)
return
}
c.JSON(http.StatusOK, serializePlatformUser(user))
}
// Create creates a new user and auto-creates Permissible record.
// POST /platform/api/v1/users
// Reference: Chatwoot Platform::Api::V1::UsersController#create
// Auto-permissible: PlatformApp automatically gets access to the created user.
func (h *PlatformUserHandler) Create(c *gin.Context) {
platformAppID := getPlatformAppID(c)
var req service.PlatformUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
return
}
if req.Email == "" {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "email is required")
return
}
user, err := h.svc.CreateUser(c.Request.Context(), platformAppID, req)
if err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
return
}
c.JSON(http.StatusOK, serializePlatformUser(user))
}
// Login generates an SSO login link for a user.
// GET /platform/api/v1/users/:id/login
// Reference: Chatwoot Platform::Api::V1::UsersController#login
// Returns: { url: sso_redirect_url }
func (h *PlatformUserHandler) Login(c *gin.Context) {
userID, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user ID")
return
}
platformAppID := getPlatformAppID(c)
user, err := h.svc.GetUserResponse(c.Request.Context(), platformAppID, userID)
if err != nil {
handlePlatformError(c, err)
return
}
query := url.Values{}
query.Set("email", user.User.Email)
query.Set("sso_auth_token", user.AccessToken)
c.JSON(http.StatusOK, gin.H{
"url": "/app/login?" + query.Encode(),
})
}
// Token returns the SSO authentication token for a user.
// POST /platform/api/v1/users/:id/token
// Reference: Chatwoot Platform::Api::V1::UsersController#token
// Returns: user resource with sso_auth_token field
func (h *PlatformUserHandler) Token(c *gin.Context) {
userID, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user ID")
return
}
platformAppID := getPlatformAppID(c)
// Verify Permissible access first
if err := h.svc.ValidatePermissible(c.Request.Context(), platformAppID, userID); err != nil {
handlePlatformError(c, err)
return
}
user, err := h.svc.TokenResponse(c.Request.Context(), platformAppID, userID)
if err != nil {
handlePlatformError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"access_token": user.AccessToken,
"expiry": nil,
"user": gin.H{
"id": user.User.ID,
"name": user.User.Name,
"display_name": user.User.DisplayName,
"email": user.User.Email,
"pubsub_token": user.User.PubsubToken,
},
})
}
// Update updates a user.
// PATCH /platform/api/v1/users/:id
// Reference: Chatwoot Platform::Api::V1::UsersController#update
// Requires: Permissible verification
func (h *PlatformUserHandler) Update(c *gin.Context) {
userID, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user ID")
return
}
platformAppID := getPlatformAppID(c)
var req service.PlatformUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
return
}
user, err := h.svc.UpdateUser(c.Request.Context(), platformAppID, userID, req)
if err != nil {
handlePlatformError(c, err)
return
}
c.JSON(http.StatusOK, serializePlatformUser(user))
}
// Destroy deletes a user.
// DELETE /platform/api/v1/users/:id
// Reference: Chatwoot Platform::Api::V1::UsersController#destroy
// Requires: Permissible verification
func (h *PlatformUserHandler) Destroy(c *gin.Context) {
userID, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user ID")
return
}
platformAppID := getPlatformAppID(c)
if err := h.svc.DeleteUser(c.Request.Context(), platformAppID, userID); err != nil {
handlePlatformError(c, err)
return
}
c.Status(http.StatusOK)
}
func serializePlatformUser(payload *service.PlatformUserResponse) gin.H {
if payload == nil {
return gin.H{}
}
user := payload.User
out := gin.H{
"access_token": payload.AccessToken,
"account_id": activeAccountID(payload.AccountUsers),
"available_name": nonEmpty(user.DisplayName, user.Name),
"avatar_url": user.AvatarURL,
"confirmed": user.ConfirmedAt != nil,
"display_name": user.DisplayName,
"message_signature": user.MessageSignature,
"email": user.Email,
"id": user.ID,
"name": user.Name,
"provider": nonEmpty(user.Provider, "email"),
"pubsub_token": user.PubsubToken,
"role": activeAccountRole(payload.AccountUsers),
"ui_settings": jsonObject(user.UISettings),
"uid": user.UID,
"accounts": serializePlatformUserAccounts(payload.AccountUsers),
}
if attrs := jsonObject(user.CustomAttributes); len(attrs) > 0 {
out["custom_attributes"] = attrs
}
return out
}
func serializePlatformUserAccounts(accountUsers []model.AccountUser) []gin.H {
accounts := make([]gin.H, 0, len(accountUsers))
for _, au := range accountUsers {
accounts = append(accounts, gin.H{
"id": au.AccountID,
"name": au.Account.Name,
"active_at": au.ActiveAt,
"role": au.Role,
})
}
return accounts
}
func activeAccountID(accountUsers []model.AccountUser) any {
if len(accountUsers) == 0 {
return nil
}
return accountUsers[0].AccountID
}
func activeAccountRole(accountUsers []model.AccountUser) any {
if len(accountUsers) == 0 {
return nil
}
return accountUsers[0].Role
}
// --- Helper functions for Platform API handlers ---
// getPlatformAppID extracts platform_app_id from the Gin context.
// Set by PlatformAppAuth middleware during AccessToken authentication.
func getPlatformAppID(c *gin.Context) uint {
if id, exists := c.Get("platform_app_id"); exists {
switch v := id.(type) {
case uint:
return v
case float64:
return uint(v)
case int:
return uint(v)
}
}
return 0
}
// handlePlatformError maps service errors to HTTP responses.
func handlePlatformError(c *gin.Context, err error) {
if err.Error() == "non permissible resource" {
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, "non permissible resource")
return
}
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
}
// PlatformUsersListHandler returns all users the PlatformApp has permissible access to.
// GET /platform/api/v1/users
// Reference: Chatwoot Platform::Api::V1::UsersController#index (lists permissibles)
func (h *PlatformUserHandler) List(c *gin.Context) {
page := pagination.Parse(c)
if c.GetBool("is_super_admin") {
users, total, err := h.svc.ListUsers(c.Request.Context(), page.Offset, page.PerPage)
if err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
return
}
response.OKWithMeta(c, users, page.Page, page.PerPage, total)
return
}
platformAppID := getPlatformAppID(c)
users, err := h.svc.ListPermissibleUsers(c.Request.Context(), platformAppID)
if err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
return
}
// Manual pagination since we filter by permissible first
total := int64(len(users))
start := page.Offset
if start > len(users) {
start = len(users)
}
end := start + page.PerPage
if end > len(users) {
end = len(users)
}
response.OKWithMeta(c, users[start:end], page.Page, page.PerPage, total)
}