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

357 lines
12 KiB
Go

package v1
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"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, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
account, err := h.svc.GetByID(c.Request.Context(), uint(id))
if err != nil {
applogger.L().Errorf("Get account: %v", err)
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrAccountNotFound, "account not found")
return
}
response.OK(c, 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
}
response.Created(c, account)
}
// @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, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
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(), uint(id), req)
if err != nil {
applogger.L().Errorf("Update account: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update account")
return
}
response.OK(c, 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, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
if err := h.svc.Delete(c.Request.Context(), uint(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, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
pg := pagination.Parse(c)
users, total, err := h.svc.ListUsers(c.Request.Context(), uint(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, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
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(), uint(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, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
userID, err := strconv.ParseUint(c.Param("user_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user id")
return
}
if err := h.svc.RemoveUser(c.Request.Context(), uint(accountID), uint(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, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
pg := pagination.Parse(c)
agents, total, err := h.svc.GetAgents(c.Request.Context(), uint(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, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
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(), uint(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
}
response.OK(c, 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, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
userID := getUserID(c)
if err := h.svc.UpdateActiveAt(c.Request.Context(), uint(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
}
response.OK(c, gin.H{"message": "Active timestamp updated"})
}
// CacheKeys returns cache key identifiers for frontend cache invalidation.
// GET /api/v1/accounts/:id/cache_keys
func (h *AccountHandler) CacheKeys(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
userID := getUserID(c)
keys, err := h.svc.CacheKeys(c.Request.Context(), uint(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
}
response.OK(c, keys)
}