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.
324 lines
11 KiB
Go
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
|
|
} |