Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
300 lines
8.8 KiB
Go
300 lines
8.8 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) {
|
|
platformAppID := getPlatformAppID(c)
|
|
page := pagination.Parse(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)
|
|
}
|