Files
gochat/internal/handler/api/v1/dashboard_app_handler.go
T

396 lines
13 KiB
Go

package v1
import (
"encoding/json"
"errors"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
applogger "github.com/gochat/gochat/pkg/logger"
"github.com/gochat/gochat/pkg/response"
)
// DashboardAppHandler handles DashboardApp CRUD endpoints.
// Reference: Chatwoot app/controllers/api/v1/accounts/dashboard_apps_controller.rb
// Chatwoot routes: resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy]
// Chatwoot request: params.require(:dashboard_app).permit(:title, content: [:url, :type])
// Chatwoot response: Jbuilder partial — {id, title, content, created_at}
// Chatwoot destroy: head :no_content (204)
// GoChat extensions: search, widgets CRUD (not in Chatwoot)
type DashboardAppHandler struct {
svc *service.DashboardAppService
}
type dashboardAppPayload struct {
ID uint `json:"id"`
Title string `json:"title"`
Content json.RawMessage `json:"content"`
CreatedAt time.Time `json:"created_at"`
}
func NewDashboardAppHandler(svc *service.DashboardAppService) *DashboardAppHandler {
return &DashboardAppHandler{svc: svc}
}
// Create creates a new dashboard app.
// POST /api/v1/accounts/:account_id/dashboard_apps
// Chatwoot: Current.account.dashboard_apps.create!(permitted_payload.merge(user_id: Current.user.id))
func (h *DashboardAppHandler) Create(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
userID := getUserID(c)
userIDPtr := &userID
req, err := bindDashboardAppCreate(c)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
if req.Title == "" {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "title is required")
return
}
app, err := h.svc.Create(c.Request.Context(), accountID, userIDPtr, req)
if err != nil {
applogger.L().Errorf("Create dashboard app: %v", err)
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, err.Error())
return
}
c.JSON(http.StatusOK, serializeDashboardApp(app))
}
// Get retrieves a dashboard app by ID.
// GET /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id
func (h *DashboardAppHandler) Get(c *gin.Context) {
id, err := parseUintParam(c, "dashboard_app_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id")
return
}
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
app, err := h.svc.GetByAccountAndID(c.Request.Context(), accountID, id)
if err != nil {
applogger.L().Errorf("Get dashboard app: %v", err)
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "dashboard app not found")
return
}
c.JSON(http.StatusOK, serializeDashboardApp(app))
}
// Update modifies an existing dashboard app.
// PUT /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id
// Chatwoot: @dashboard_app.update!(permitted_payload)
func (h *DashboardAppHandler) Update(c *gin.Context) {
id, err := parseUintParam(c, "dashboard_app_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id")
return
}
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
req, err := bindDashboardAppUpdate(c)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
app, err := h.svc.UpdateByAccountAndID(c.Request.Context(), accountID, id, req)
if err != nil {
applogger.L().Errorf("Update dashboard app: %v", err)
handleDashboardAppMutationError(c, err)
return
}
c.JSON(http.StatusOK, serializeDashboardApp(app))
}
// Delete removes a dashboard app.
// DELETE /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id
// Chatwoot: @dashboard_app.destroy!; head :no_content (204)
func (h *DashboardAppHandler) Delete(c *gin.Context) {
id, err := parseUintParam(c, "dashboard_app_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id")
return
}
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
if err := h.svc.DeleteByAccountAndID(c.Request.Context(), accountID, id); err != nil {
applogger.L().Errorf("Delete dashboard app: %v", err)
if errors.Is(err, gorm.ErrRecordNotFound) {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "dashboard app not found")
return
}
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to delete dashboard app")
return
}
response.NoContent(c)
}
// Patch partially updates a dashboard app (GoChat extension — Chatwoot has no PATCH).
// PATCH /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id
func (h *DashboardAppHandler) Patch(c *gin.Context) {
id, err := parseUintParam(c, "dashboard_app_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id")
return
}
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
req, err := bindDashboardAppUpdate(c)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
app, err := h.svc.UpdateByAccountAndID(c.Request.Context(), accountID, id, req)
if err != nil {
applogger.L().Errorf("Patch dashboard app: %v", err)
handleDashboardAppMutationError(c, err)
return
}
c.JSON(http.StatusOK, serializeDashboardApp(app))
}
// List returns all dashboard apps for an account.
// GET /api/v1/accounts/:account_id/dashboard_apps
// Chatwoot: json.array! @dashboard_apps — pure JSON array, no meta
func (h *DashboardAppHandler) List(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
apps, err := h.svc.ListByAccount(c.Request.Context(), accountID)
if err != nil {
applogger.L().Errorf("List dashboard apps: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list dashboard apps")
return
}
payload := make([]dashboardAppPayload, 0, len(apps))
for i := range apps {
payload = append(payload, serializeDashboardApp(&apps[i]))
}
c.JSON(http.StatusOK, payload)
}
func bindDashboardAppCreate(c *gin.Context) (*service.CreateDashboardAppRequest, error) {
var raw map[string]json.RawMessage
if err := c.ShouldBindJSON(&raw); err != nil {
return nil, err
}
if nested, ok := raw["dashboard_app"]; ok {
var req service.CreateDashboardAppRequest
if err := json.Unmarshal(nested, &req); err != nil {
return nil, err
}
return &req, nil
}
body, _ := json.Marshal(raw)
var req service.CreateDashboardAppRequest
if err := json.Unmarshal(body, &req); err != nil {
return nil, err
}
return &req, nil
}
func bindDashboardAppUpdate(c *gin.Context) (*service.UpdateDashboardAppRequest, error) {
var raw map[string]json.RawMessage
if err := c.ShouldBindJSON(&raw); err != nil {
return nil, err
}
if nested, ok := raw["dashboard_app"]; ok {
var req service.UpdateDashboardAppRequest
if err := json.Unmarshal(nested, &req); err != nil {
return nil, err
}
return &req, nil
}
body, _ := json.Marshal(raw)
var req service.UpdateDashboardAppRequest
if err := json.Unmarshal(body, &req); err != nil {
return nil, err
}
return &req, nil
}
func serializeDashboardApp(app *model.DashboardApp) dashboardAppPayload {
content := app.Content
if len(content) == 0 {
content = json.RawMessage(`[]`)
}
return dashboardAppPayload{ID: app.ID, Title: app.Title, Content: content, CreatedAt: app.CreatedAt}
}
func handleDashboardAppMutationError(c *gin.Context, err error) {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "dashboard app not found")
return
}
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, err.Error())
}
// ========== GoChat Extension Endpoints (not in Chatwoot) ==========
// Search searches dashboard apps by title keyword within an account.
// GET /api/v1/accounts/:account_id/dashboard_apps/search?q=...
func (h *DashboardAppHandler) Search(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
query := c.Query("q")
if query == "" {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "search query 'q' is required")
return
}
apps, err := h.svc.Search(c.Request.Context(), accountID, query)
if err != nil {
applogger.L().Errorf("Search dashboard apps: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to search dashboard apps")
return
}
c.JSON(http.StatusOK, apps)
}
// GetWidgets returns all widgets in a dashboard app's content.
// GET /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets
func (h *DashboardAppHandler) GetWidgets(c *gin.Context) {
id, err := parseUintParam(c, "dashboard_app_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id")
return
}
widgets, err := h.svc.GetWidgets(c.Request.Context(), id)
if err != nil {
applogger.L().Errorf("Get widgets: %v", err)
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "dashboard app not found")
return
}
response.OK(c, widgets)
}
// AddWidget adds a widget to a dashboard app's content array.
// POST /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets
func (h *DashboardAppHandler) AddWidget(c *gin.Context) {
id, err := parseUintParam(c, "dashboard_app_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id")
return
}
var req service.AddWidgetRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
widgets, err := h.svc.AddWidget(c.Request.Context(), id, &req)
if err != nil {
applogger.L().Errorf("Add widget: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to add widget")
return
}
response.OK(c, widgets)
}
// RemoveWidget removes a widget from a dashboard app's content array by index.
// DELETE /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets/:widget_index
func (h *DashboardAppHandler) RemoveWidget(c *gin.Context) {
id, err := parseUintParam(c, "dashboard_app_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id")
return
}
widgetIndex, err := parseUintParam(c, "widget_index")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid widget_index")
return
}
widgets, err := h.svc.RemoveWidget(c.Request.Context(), id, int(widgetIndex))
if err != nil {
applogger.L().Errorf("Remove widget: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to remove widget")
return
}
response.OK(c, widgets)
}
// UpdateWidget updates a widget in a dashboard app's content array by index.
// PUT /api/v1/accounts/:account_id/dashboard_apps/:dashboard_app_id/widgets/:widget_index
// GoChat extension — Chatwoot has no widget sub-resource CRUD.
func (h *DashboardAppHandler) UpdateWidget(c *gin.Context) {
id, err := parseUintParam(c, "dashboard_app_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid dashboard_app_id")
return
}
widgetIndex, err := parseUintParam(c, "widget_index")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid widget_index")
return
}
var req service.UpdateWidgetRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
widgets, err := h.svc.UpdateWidget(c.Request.Context(), id, int(widgetIndex), &req)
if err != nil {
applogger.L().Errorf("Update widget: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update widget")
return
}
response.OK(c, widgets)
}