package v1 import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gochat/gochat/pkg/pagination" "github.com/gochat/gochat/pkg/response" ) // PlatformAppHandler handles PlatformApp CRUD + Permissible + AccessToken endpoints. // Reference: Chatwoot app/controllers/api/v1/platform/apps_controller.rb // Note: These handlers are registered under /platform/api/v1/ namespace (super admin only) // but also have account-level access under /api/v1/accounts/:account_id/platform_apps type PlatformAppHandler struct { svc *service.PlatformAppService } // NewPlatformAppHandler creates a new PlatformApp handler with service injection. func NewPlatformAppHandler(svc *service.PlatformAppService) *PlatformAppHandler { return &PlatformAppHandler{svc: svc} } // --- PlatformApp CRUD endpoints (existing) --- // List returns all platform apps (account-scoped or platform-wide). // GET /api/v1/accounts/:account_id/platform_apps // GET /platform/api/v1/apps (super admin — lists all) func (h *PlatformAppHandler) List(c *gin.Context) { page := pagination.Parse(c) // Check if this is a platform-level (super-admin) or account-level request accountIDStr := c.Param("account_id") if accountIDStr != "" { // Account-scoped listing accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } apps, total, err := h.svc.ListByAccount(c.Request.Context(), uint(accountID), page.Offset, page.PerPage) if err != nil { applogger.L().Errorf("List platform apps by account %d: %v", accountID, err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list platform apps") return } response.OKWithMeta(c, apps, page.Page, page.PerPage, total) return } // Platform-level (super-admin) listing — all apps apps, total, err := h.svc.ListAll(c.Request.Context(), page.Offset, page.PerPage) if err != nil { applogger.L().Errorf("List all platform apps: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list platform apps") return } response.OKWithMeta(c, apps, page.Page, page.PerPage, total) } // Get returns a single platform app by ID (with relations). // GET /api/v1/accounts/:account_id/platform_apps/:id // GET /platform/api/v1/apps/:id func (h *PlatformAppHandler) Get(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid platform app ID") return } app, err := h.svc.GetByIDWithRelations(c.Request.Context(), id) if err != nil { handleServiceError(c, err) return } response.OK(c, app) } // Create creates a new platform app and auto-generates an AccessToken. // POST /api/v1/accounts/:account_id/platform_apps // POST /platform/api/v1/apps // Returns the app and the plaintext access token (shown only once). func (h *PlatformAppHandler) Create(c *gin.Context) { var req service.CreatePlatformAppRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } // If account-scoped route, override req.AccountID from URL param accountIDStr := c.Param("account_id") if accountIDStr != "" { accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } req.AccountID = uint(accountID) } app, plainToken, err := h.svc.Create(c.Request.Context(), req) if err != nil { handleServiceError(c, err) return } // Return app + plaintext token (this is the only time the plaintext token is shown) response.Created(c, gin.H{ "platform_app": app, "access_token": plainToken, // plaintext — shown only once }) } // Update modifies an existing platform app. // PUT /api/v1/accounts/:account_id/platform_apps/:id // PUT /platform/api/v1/apps/:id func (h *PlatformAppHandler) Update(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid platform app ID") return } var req service.UpdatePlatformAppRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } app, err := h.svc.Update(c.Request.Context(), id, req) if err != nil { handleServiceError(c, err) return } response.OK(c, app) } // Delete soft-deletes a platform app (and its AccessTokens + Permissibles). // DELETE /api/v1/accounts/:account_id/platform_apps/:id // DELETE /platform/api/v1/apps/:id func (h *PlatformAppHandler) Delete(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid platform app ID") return } if err := h.svc.Delete(c.Request.Context(), id); err != nil { handleServiceError(c, err) return } response.NoContent(c) } // --- AccessToken endpoints (Chatwoot AccessTokenable concern) --- // RegenerateAccessToken generates a new AccessToken for a platform app, soft-deleting the old one. // POST /platform/api/v1/apps/:id/regenerate_access_token // POST /api/v1/accounts/:account_id/platform_apps/:id/regenerate_access_token // Reference: Chatwoot PlatformAppsController#regenerate_api_key // Returns the plaintext token (shown only once, never stored). func (h *PlatformAppHandler) RegenerateAccessToken(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid platform app ID") return } plainToken, err := h.svc.RegenerateAccessToken(c.Request.Context(), id) if err != nil { handleServiceError(c, err) return } response.OK(c, gin.H{ "access_token": plainToken, // plaintext — shown only once }) } // ListAccessTokens returns all AccessTokens for a platform app. // GET /platform/api/v1/apps/:id/access_tokens // GET /api/v1/accounts/:account_id/platform_apps/:id/access_tokens func (h *PlatformAppHandler) ListAccessTokens(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid platform app ID") return } tokens, err := h.svc.ListAccessTokens(c.Request.Context(), id) if err != nil { handleServiceError(c, err) return } response.OK(c, tokens) } // --- Permissible endpoints (Chatwoot PlatformAppPermissible) --- // AddPermissible grants a PlatformApp access to a resource. // POST /platform/api/v1/apps/:id/permissibles // POST /api/v1/accounts/:account_id/platform_apps/:id/permissibles // Body: { "permissible_type": "Account|User|AgentBot", "permissible_id": 123 } // Reference: Chatwoot PlatformAppsController#add_permissible func (h *PlatformAppHandler) AddPermissible(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid platform app ID") return } var req addPermissibleRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } permissible, err := h.svc.AddPermissible(c.Request.Context(), id, req.PermissibleType, req.PermissibleID) if err != nil { handleServiceError(c, err) return } response.Created(c, permissible) } // RemovePermissible revokes a PlatformApp's access to a resource. // DELETE /platform/api/v1/apps/:id/permissibles/:permissible_id // DELETE /api/v1/accounts/:account_id/platform_apps/:id/permissibles/:permissible_id // Query params: permissible_type=Account|User|AgentBot // Reference: Chatwoot PlatformAppsController#remove_permissible func (h *PlatformAppHandler) RemovePermissible(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid platform app ID") return } permissibleID, err := parseUintParam(c, "permissible_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid permissible ID") return } // permissible_type comes from query param permissibleType := c.Query("permissible_type") if permissibleType == "" { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "permissible_type query param required (Account, User, or AgentBot)") return } if err := h.svc.RemovePermissible(c.Request.Context(), id, permissibleType, permissibleID); err != nil { handleServiceError(c, err) return } response.NoContent(c) } // ListPermissibles returns all Permissibles for a PlatformApp. // GET /platform/api/v1/apps/:id/permissibles // GET /api/v1/accounts/:account_id/platform_apps/:id/permissibles func (h *PlatformAppHandler) ListPermissibles(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid platform app ID") return } permissibles, err := h.svc.ListPermissibles(c.Request.Context(), id) if err != nil { handleServiceError(c, err) return } response.OK(c, permissibles) } // --- Search endpoint (existing) --- // Search searches platform apps by name. // GET /platform/api/v1/apps/search?q= // GET /api/v1/accounts/:account_id/platform_apps/search?q= func (h *PlatformAppHandler) Search(c *gin.Context) { page := pagination.Parse(c) query := c.Query("q") accountIDStr := c.Param("account_id") if accountIDStr != "" { // Account-scoped search accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } apps, total, err := h.svc.SearchByAccount(c.Request.Context(), uint(accountID), query, page.Offset, page.PerPage) if err != nil { applogger.L().Errorf("Search platform apps by account %d: %v", accountID, err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to search platform apps") return } response.OKWithMeta(c, apps, page.Page, page.PerPage, total) return } // Platform-wide (super-admin) search apps, total, err := h.svc.Search(c.Request.Context(), query, page.Offset, page.PerPage) if err != nil { applogger.L().Errorf("Search platform apps: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to search platform apps") return } response.OKWithMeta(c, apps, page.Page, page.PerPage, total) } // --- DTOs for Permissible requests --- // addPermissibleRequest is the request body for AddPermissible. type addPermissibleRequest struct { PermissibleType string `json:"permissible_type" validate:"required,oneof=Account User AgentBot"` PermissibleID uint `json:"permissible_id" validate:"required"` } // --- Helper: validate permissible_type --- func isValidPermissibleTypeStr(t string) bool { return t == model.PermissibleTypeAccount || t == model.PermissibleTypeUser || t == model.PermissibleTypeAgentBot }