Files
gochat/internal/handler/api/v1/platform_handler.go
T
2026-06-04 15:44:48 +08:00

324 lines
11 KiB
Go

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=<query>
// GET /api/v1/accounts/:account_id/platform_apps/search?q=<query>
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
}