Files
gochat/backend/internal/handler/api/v1/platform_account_handler.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
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.
2026-07-07 14:44:12 +08:00

244 lines
7.4 KiB
Go

package v1
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/pagination"
"github.com/gochat/gochat/pkg/response"
)
// PlatformAccountHandler handles Platform API account endpoints (AccessToken auth).
// Reference: Chatwoot Platform::Api::V1::AccountsController — AccessToken authenticated
//
// Uses Permissible system: PlatformApp must have permissible access to operate
// on specific accounts. Create/index auto-permissible.
type PlatformAccountHandler struct {
accountRepo *repository.AccountRepo
permissibleRepo *repository.PermissibleRepo
accountSvc *service.AccountService
}
// NewPlatformAccountHandler creates a new PlatformAccount handler.
func NewPlatformAccountHandler(
accountRepo *repository.AccountRepo,
permissibleRepo *repository.PermissibleRepo,
accountSvc *service.AccountService,
) *PlatformAccountHandler {
return &PlatformAccountHandler{
accountRepo: accountRepo,
permissibleRepo: permissibleRepo,
accountSvc: accountSvc,
}
}
// List returns all accounts the PlatformApp has permissible access to.
// GET /platform/api/v1/accounts
// Reference: Chatwoot Platform::Api::V1::AccountsController#index
func (h *PlatformAccountHandler) List(c *gin.Context) {
platformAppID := getPlatformAppID(c)
page := pagination.Parse(c)
permissibles, err := h.permissibleRepo.FindByPlatformAppID(c.Request.Context(), platformAppID)
if err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
return
}
var accounts []model.Account
var total int64
for _, perm := range permissibles {
if perm.PermissibleType == model.PermissibleTypeAccount {
acct, err := h.accountRepo.FindByID(c.Request.Context(), perm.PermissibleID)
if err != nil {
continue
}
accounts = append(accounts, *acct)
}
}
total = int64(len(accounts))
start := page.Offset
if start > len(accounts) {
start = len(accounts)
}
end := start + page.PerPage
if end > len(accounts) {
end = len(accounts)
}
response.OKWithMeta(c, accounts[start:end], page.Page, page.PerPage, total)
}
// Show retrieves an account by ID.
// GET /platform/api/v1/accounts/:id
// Reference: Chatwoot Platform::Api::V1::AccountsController#show
// Requires: Permissible verification
func (h *PlatformAccountHandler) Show(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account ID")
return
}
platformAppID := getPlatformAppID(c)
// Verify Permissible access
perm, err := h.permissibleRepo.FindByPlatformAppAndResource(c.Request.Context(), platformAppID, model.PermissibleTypeAccount, accountID)
if err != nil || perm == nil {
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, "non permissible resource")
return
}
acct, err := h.accountRepo.FindByID(c.Request.Context(), accountID)
if err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "account not found")
return
}
response.OK(c, acct)
}
// Create creates a new account and auto-creates Permissible record.
// POST /platform/api/v1/accounts
// Reference: Chatwoot Platform::Api::V1::AccountsController#create
// Auto-permissible: PlatformApp automatically gets access to the created account.
func (h *PlatformAccountHandler) Create(c *gin.Context) {
platformAppID := getPlatformAppID(c)
var req struct {
Name string `json:"name" binding:"required"`
Domain string `json:"domain,omitempty"`
Locale string `json:"locale,omitempty"`
Timezone string `json:"timezone,omitempty"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
return
}
acct := &model.Account{
Name: req.Name,
Domain: req.Domain,
Locale: defaultStr(req.Locale, "en"),
Timezone: defaultStr(req.Timezone, "UTC"),
Active: true,
Status: "active",
}
if err := h.accountRepo.Create(c.Request.Context(), acct); err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
return
}
// Auto-create Permissible
perm := &model.Permissible{
PlatformAppID: platformAppID,
PermissibleType: model.PermissibleTypeAccount,
PermissibleID: acct.ID,
}
if err := h.permissibleRepo.Create(c.Request.Context(), perm); err != nil {
// Non-critical — log but don't block account creation
}
response.Created(c, acct)
}
// Update updates an account.
// PATCH /platform/api/v1/accounts/:id
// Reference: Chatwoot Platform::Api::V1::AccountsController#update
// Requires: Permissible verification
func (h *PlatformAccountHandler) Update(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account ID")
return
}
platformAppID := getPlatformAppID(c)
// Verify Permissible access
perm, err := h.permissibleRepo.FindByPlatformAppAndResource(c.Request.Context(), platformAppID, model.PermissibleTypeAccount, accountID)
if err != nil || perm == nil {
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, "non permissible resource")
return
}
acct, err := h.accountRepo.FindByID(c.Request.Context(), accountID)
if err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "account not found")
return
}
var req struct {
Name string `json:"name,omitempty"`
Domain string `json:"domain,omitempty"`
Locale string `json:"locale,omitempty"`
Timezone string `json:"timezone,omitempty"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
return
}
if req.Name != "" {
acct.Name = req.Name
}
if req.Domain != "" {
acct.Domain = req.Domain
}
if req.Locale != "" {
acct.Locale = req.Locale
}
if req.Timezone != "" {
acct.Timezone = req.Timezone
}
if err := h.accountRepo.Update(c.Request.Context(), acct); err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
return
}
response.OK(c, acct)
}
// Destroy deletes an account.
// DELETE /platform/api/v1/accounts/:id
// Reference: Chatwoot Platform::Api::V1::AccountsController#destroy
// Requires: Permissible verification
func (h *PlatformAccountHandler) Destroy(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account ID")
return
}
platformAppID := getPlatformAppID(c)
// Verify Permissible access
perm, err := h.permissibleRepo.FindByPlatformAppAndResource(c.Request.Context(), platformAppID, model.PermissibleTypeAccount, accountID)
if err != nil || perm == nil {
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, "non permissible resource")
return
}
if err := h.accountRepo.Delete(c.Request.Context(), accountID); err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
return
}
response.NoContent(c)
}
// --- Helper ---
func defaultStr(val, fallback string) string {
if val == "" {
return fallback
}
return val
}