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) }