535 lines
17 KiB
Go
535 lines
17 KiB
Go
package v1
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"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"
|
|
)
|
|
|
|
// AccountHandler handles account-related API endpoints.
|
|
// Reference: Chatwoot app/controllers/api/v1/accounts_controller.rb
|
|
type AccountHandler struct {
|
|
svc *service.AccountService
|
|
}
|
|
|
|
// NewAccountHandler creates a new AccountHandler.
|
|
func NewAccountHandler(svc *service.AccountService) *AccountHandler {
|
|
return &AccountHandler{svc: svc}
|
|
}
|
|
|
|
// @Summary List accounts accessible by the current user
|
|
// @Description Returns all accounts the authenticated user has access to, with pagination support
|
|
// @Tags Accounts
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param page_size query int false "Items per page" default(25)
|
|
// @Success 200 {object} []model.Account
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts [get]
|
|
func (h *AccountHandler) List(c *gin.Context) {
|
|
userID := getUserID(c)
|
|
if userID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated")
|
|
return
|
|
}
|
|
|
|
pg := pagination.Parse(c)
|
|
accounts, total, err := h.svc.ListByUser(c.Request.Context(), userID, pg.Offset, pg.PerPage)
|
|
if err != nil {
|
|
applogger.L().Errorf("List accounts: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list accounts")
|
|
return
|
|
}
|
|
|
|
response.OKWithMeta(c, accounts, pg.Page, pg.PerPage, total)
|
|
}
|
|
|
|
// @Summary Get a single account by ID
|
|
// @Description Retrieves detailed information about a specific account
|
|
// @Tags Accounts
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path uint true "Account ID"
|
|
// @Success 200 {object} model.Account
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 404 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{id} [get]
|
|
func (h *AccountHandler) Get(c *gin.Context) {
|
|
id := parseAccountIDParam(c)
|
|
if id == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
account, err := h.svc.GetByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
applogger.L().Errorf("Get account: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrAccountNotFound, "account not found")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, serializeAccount(account))
|
|
}
|
|
|
|
// @Summary Create a new account
|
|
// @Description Creates a new account and assigns the creator as administrator
|
|
// @Tags Accounts
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body service.CreateAccountRequest true "Account creation payload"
|
|
// @Success 201 {object} model.Account
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts [post]
|
|
func (h *AccountHandler) Create(c *gin.Context) {
|
|
userID := getUserID(c)
|
|
if userID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated")
|
|
return
|
|
}
|
|
|
|
var req service.CreateAccountRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
account, err := h.svc.Create(c.Request.Context(), userID, req)
|
|
if err != nil {
|
|
applogger.L().Errorf("Create account: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create account")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": gin.H{"id": userID, "account_id": account.ID}})
|
|
}
|
|
|
|
// @Summary Update an existing account
|
|
// @Description Modifies account details such as name and settings
|
|
// @Tags Accounts
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path uint true "Account ID"
|
|
// @Param body body service.UpdateAccountRequest true "Account update payload"
|
|
// @Success 200 {object} model.Account
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{id} [put]
|
|
func (h *AccountHandler) Update(c *gin.Context) {
|
|
id := parseAccountIDParam(c)
|
|
if id == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
var req service.UpdateAccountRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
account, err := h.svc.Update(c.Request.Context(), id, req)
|
|
if err != nil {
|
|
applogger.L().Errorf("Update account: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update account")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, serializeAccount(account))
|
|
}
|
|
|
|
// UpdateOnboarding updates account details from the dashboard onboarding flow.
|
|
// PATCH /api/v1/accounts/:account_id/onboarding
|
|
// Reference: Chatwoot Api::V1::Accounts::OnboardingsController#update.
|
|
func (h *AccountHandler) UpdateOnboarding(c *gin.Context) {
|
|
id := parseAccountIDParam(c)
|
|
if id == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
var req service.UpdateAccountOnboardingRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
account, err := h.svc.UpdateOnboarding(c.Request.Context(), id, req)
|
|
if err != nil {
|
|
applogger.L().Errorf("Update onboarding account: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update account onboarding")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, serializeAccount(account))
|
|
}
|
|
|
|
// @Summary Delete an account
|
|
// @Description Soft-deletes an account by ID
|
|
// @Tags Accounts
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path uint true "Account ID"
|
|
// @Success 204 "No Content"
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{id} [delete]
|
|
func (h *AccountHandler) Delete(c *gin.Context) {
|
|
id := parseAccountIDParam(c)
|
|
if id == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
if err := h.svc.Delete(c.Request.Context(), id); err != nil {
|
|
applogger.L().Errorf("Delete account: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to delete account")
|
|
return
|
|
}
|
|
|
|
response.NoContent(c)
|
|
}
|
|
|
|
// ListUsers returns all users belonging to an account.
|
|
// GET /api/v1/accounts/:id/users
|
|
// Reference: Chatwoot Accounts::AccountUsersController#index
|
|
func (h *AccountHandler) ListUsers(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
pg := pagination.Parse(c)
|
|
users, total, err := h.svc.ListUsers(c.Request.Context(), accountID, pg.Offset, pg.PerPage)
|
|
if err != nil {
|
|
applogger.L().Errorf("List account users: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list account users")
|
|
return
|
|
}
|
|
|
|
response.OKWithMeta(c, users, pg.Page, pg.PerPage, total)
|
|
}
|
|
|
|
// AddUser adds a user to an account with a specified role.
|
|
// POST /api/v1/accounts/:id/users
|
|
// Reference: Chatwoot Accounts::AccountUsersController#create
|
|
func (h *AccountHandler) AddUser(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
var req service.AddUserRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.svc.AddUser(c.Request.Context(), accountID, req); err != nil {
|
|
applogger.L().Errorf("Add user to account: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to add user to account")
|
|
return
|
|
}
|
|
|
|
response.NoContent(c)
|
|
}
|
|
|
|
// RemoveUser removes a user from an account.
|
|
// DELETE /api/v1/accounts/:id/users/:user_id
|
|
// Reference: Chatwoot Accounts::AccountUsersController#destroy
|
|
func (h *AccountHandler) RemoveUser(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
userID, err := parseUintParam(c, "user_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user id")
|
|
return
|
|
}
|
|
|
|
if err := h.svc.RemoveUser(c.Request.Context(), accountID, userID); err != nil {
|
|
applogger.L().Errorf("Remove user from account: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to remove user from account")
|
|
return
|
|
}
|
|
|
|
response.NoContent(c)
|
|
}
|
|
|
|
// GetAll lists all accounts (platform admin level, no user-scoping).
|
|
// GET /api/v1/accounts/all
|
|
// Reference: Chatwoot platform admin listing all accounts
|
|
func (h *AccountHandler) GetAll(c *gin.Context) {
|
|
pg := pagination.Parse(c)
|
|
accounts, total, err := h.svc.GetAll(c.Request.Context(), pg.Offset, pg.PerPage)
|
|
if err != nil {
|
|
applogger.L().Errorf("GetAll accounts: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list all accounts")
|
|
return
|
|
}
|
|
|
|
response.OKWithMeta(c, accounts, pg.Page, pg.PerPage, total)
|
|
}
|
|
|
|
// GetAgents retrieves all agents (AccountUser records) for an account.
|
|
// GET /api/v1/accounts/:id/agents
|
|
// Reference: Chatwoot app/controllers/api/v1/accounts/agents_controller.rb#index
|
|
func (h *AccountHandler) GetAgents(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
pg := pagination.Parse(c)
|
|
agents, total, err := h.svc.GetAgents(c.Request.Context(), accountID, pg.Offset, pg.PerPage)
|
|
if err != nil {
|
|
applogger.L().Errorf("GetAgents for account %d: %v", accountID, err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list agents")
|
|
return
|
|
}
|
|
|
|
response.OKWithMeta(c, agents, pg.Page, pg.PerPage, total)
|
|
}
|
|
|
|
// UpdateSettings updates account-level settings.
|
|
// PUT /api/v1/accounts/:id/settings
|
|
// Reference: Chatwoot AccountsController#update (settings subset)
|
|
func (h *AccountHandler) UpdateSettings(c *gin.Context) {
|
|
id := parseAccountIDParam(c)
|
|
if id == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
var req service.UpdateAccountSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
account, err := h.svc.UpdateSettings(c.Request.Context(), id, req)
|
|
if err != nil {
|
|
applogger.L().Errorf("Update account settings: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update account settings")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, serializeAccount(account))
|
|
}
|
|
|
|
// --- Account extension handlers (G8) ---
|
|
// Reference: Chatwoot accounts_controller.rb#update_active_at, #cache_keys
|
|
|
|
// UpdateActiveAt updates the active_at timestamp for the current user in an account.
|
|
// POST /api/v1/accounts/:id/update_active_at
|
|
func (h *AccountHandler) UpdateActiveAt(c *gin.Context) {
|
|
id := parseAccountIDParam(c)
|
|
if id == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
userID := getUserID(c)
|
|
if err := h.svc.UpdateActiveAt(c.Request.Context(), id, userID); err != nil {
|
|
applogger.L().Errorf("UpdateActiveAt for account %d, user %d: %v", id, userID, err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update active_at")
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// CacheKeys returns cache key identifiers for frontend cache invalidation.
|
|
// GET /api/v1/accounts/:id/cache_keys
|
|
func (h *AccountHandler) CacheKeys(c *gin.Context) {
|
|
id := parseAccountIDParam(c)
|
|
if id == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
|
|
return
|
|
}
|
|
|
|
userID := getUserID(c)
|
|
keys, err := h.svc.CacheKeys(c.Request.Context(), id, userID)
|
|
if err != nil {
|
|
applogger.L().Errorf("CacheKeys for account %d, user %d: %v", id, userID, err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to get cache keys")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"cache_keys": keys})
|
|
}
|
|
|
|
func serializeAccount(account *model.Account) map[string]any {
|
|
if account == nil {
|
|
return map[string]any{}
|
|
}
|
|
customAttributes := serializeAccountCustomAttributes(account)
|
|
return map[string]any{
|
|
"settings": serializeAccountSettings(account),
|
|
"created_at": account.CreatedAt,
|
|
"domain": account.Domain,
|
|
"features": parseAccountFeatures(account.FeatureFlags),
|
|
"id": account.ID,
|
|
"locale": nonEmpty(account.Locale, "en"),
|
|
"name": account.Name,
|
|
"support_email": nil,
|
|
"status": nonEmpty(account.Status, "active"),
|
|
"cache_keys": map[string]string{"label": "0000000000", "inbox": "0000000000", "team": "0000000000"},
|
|
"custom_attributes": customAttributes,
|
|
}
|
|
}
|
|
|
|
func serializeAccountCustomAttributes(account *model.Account) map[string]any {
|
|
attrs := account.CustomAttributesMap()
|
|
out := map[string]any{
|
|
"plan_name": attrs["plan_name"],
|
|
"subscribed_quantity": attrs["subscribed_quantity"],
|
|
"subscription_status": attrs["subscription_status"],
|
|
"subscription_ends_on": attrs["subscription_ends_on"],
|
|
}
|
|
|
|
copyPresentAttribute(out, attrs, "website")
|
|
copyPresentAttribute(out, attrs, "industry")
|
|
copyPresentAttribute(out, attrs, "company_size")
|
|
copyPresentAttribute(out, attrs, "timezone")
|
|
copyPresentAttribute(out, attrs, "logo")
|
|
copyPresentAttribute(out, attrs, "referral_source")
|
|
copyPresentAttribute(out, attrs, "brand_info")
|
|
if account.OnboardingStep != "" {
|
|
out["onboarding_step"] = account.OnboardingStep
|
|
} else if isPresent(attrs["onboarding_step"]) {
|
|
out["onboarding_step"] = attrs["onboarding_step"]
|
|
}
|
|
copyPresentAttribute(out, attrs, "marked_for_deletion_at")
|
|
copyPresentAttribute(out, attrs, "marked_for_deletion_reason")
|
|
if _, ok := out["timezone"]; !ok && account.Timezone != "" {
|
|
out["timezone"] = account.Timezone
|
|
}
|
|
return out
|
|
}
|
|
|
|
func copyPresentAttribute(out map[string]any, attrs map[string]any, key string) {
|
|
if value, ok := attrs[key]; ok && isPresent(value) {
|
|
out[key] = value
|
|
}
|
|
}
|
|
|
|
func isPresent(value any) bool {
|
|
if value == nil {
|
|
return false
|
|
}
|
|
if s, ok := value.(string); ok {
|
|
return s != ""
|
|
}
|
|
return true
|
|
}
|
|
|
|
func serializeAccountSettings(account *model.Account) map[string]any {
|
|
settings := map[string]any{
|
|
"auto_resolve_after": account.AutoResolveDuration,
|
|
"auto_resolve_duration": account.AutoResolveDuration,
|
|
"auto_resolve_message": "",
|
|
"auto_resolve_ignore_waiting": false,
|
|
"audio_transcriptions": account.AudioTranscriptions,
|
|
"auto_resolve_label": "",
|
|
"reporting_timezone": account.ReportingTimezone,
|
|
}
|
|
return settings
|
|
}
|
|
|
|
var chatwootDefaultEnabledAccountFeatures = []string{
|
|
"inbound_emails",
|
|
"channel_email",
|
|
"channel_facebook",
|
|
"help_center",
|
|
"agent_bots",
|
|
"macros",
|
|
"agent_management",
|
|
"team_management",
|
|
"inbox_management",
|
|
"labels",
|
|
"custom_attributes",
|
|
"automations",
|
|
"canned_responses",
|
|
"integrations",
|
|
"voice_recorder",
|
|
"channel_website",
|
|
"campaigns",
|
|
"reports",
|
|
"crm",
|
|
"auto_resolve_conversations",
|
|
"chatwoot_v4",
|
|
"contact_chatwoot_support_team",
|
|
"channel_instagram",
|
|
"channel_tiktok",
|
|
"assignment_v2",
|
|
"captain_tasks",
|
|
}
|
|
|
|
func parseAccountFeatures(raw string) map[string]bool {
|
|
features := defaultEnabledAccountFeatures()
|
|
if raw == "" {
|
|
return onlyEnabledAccountFeatures(features)
|
|
}
|
|
|
|
objectFlags := map[string]bool{}
|
|
if err := json.Unmarshal([]byte(raw), &objectFlags); err == nil {
|
|
for key, enabled := range objectFlags {
|
|
features[key] = enabled
|
|
}
|
|
return onlyEnabledAccountFeatures(features)
|
|
}
|
|
|
|
arrayFlags := []string{}
|
|
if err := json.Unmarshal([]byte(raw), &arrayFlags); err == nil {
|
|
for _, key := range arrayFlags {
|
|
features[key] = true
|
|
}
|
|
return onlyEnabledAccountFeatures(features)
|
|
}
|
|
|
|
return onlyEnabledAccountFeatures(features)
|
|
}
|
|
|
|
func defaultEnabledAccountFeatures() map[string]bool {
|
|
features := make(map[string]bool, len(chatwootDefaultEnabledAccountFeatures))
|
|
for _, key := range chatwootDefaultEnabledAccountFeatures {
|
|
features[key] = true
|
|
}
|
|
return features
|
|
}
|
|
|
|
func onlyEnabledAccountFeatures(features map[string]bool) map[string]bool {
|
|
enabled := map[string]bool{}
|
|
for key, value := range features {
|
|
if value {
|
|
enabled[key] = true
|
|
}
|
|
}
|
|
return enabled
|
|
}
|