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.
244 lines
7.4 KiB
Go
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
|
|
} |