Files
gochat/backend/internal/handler/widget/widget_theme_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

172 lines
5.4 KiB
Go

package widget
import (
"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"
)
// --- Widget Theme Handlers (Public-facing, website_token path param) ---
// M11: Extended theme configuration beyond widget_color
// GetThemeConfig returns the custom theme configuration for a widget.
// GET /widget/:website_token/theme_config
// No widget_token required — theme is public config for the widget SDK to render.
func (h *WidgetHandler) GetThemeConfig(c *gin.Context) {
websiteToken := c.Param("website_token")
if websiteToken == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"})
return
}
themeConfig, err := h.widgetService.GetThemeConfig(c.Request.Context(), websiteToken)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if themeConfig == nil {
// No custom theme — return empty so SDK falls back to defaults
c.JSON(http.StatusOK, gin.H{"theme": nil})
return
}
c.JSON(http.StatusOK, gin.H{"theme": themeConfig})
}
// --- Widget Pre-Chat Form Handlers (Public-facing, website_token path param) ---
// M11: Structured pre-chat form before starting conversation
// GetPreChatForm returns the pre-chat form definition for a widget.
// GET /widget/:website_token/pre_chat_form
// No widget_token required — form definition is public config before visitor authenticates.
func (h *WidgetHandler) GetPreChatForm(c *gin.Context) {
websiteToken := c.Param("website_token")
if websiteToken == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"})
return
}
form, err := h.widgetService.GetPreChatForm(c.Request.Context(), websiteToken)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if form == nil {
// No pre-chat form — return empty so SDK skips form step
c.JSON(http.StatusOK, gin.H{"pre_chat_form": nil})
return
}
c.JSON(http.StatusOK, gin.H{"pre_chat_form": form})
}
// SubmitPreChatForm processes a visitor's pre-chat form submission.
// POST /widget/:website_token/pre_chat_form
// This creates/identifies the contact and returns a widget_token for subsequent requests.
// Reference: Chatwoot widget SDK — pre-chat form submission flow
func (h *WidgetHandler) SubmitPreChatForm(c *gin.Context) {
websiteToken := c.Param("website_token")
if websiteToken == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"})
return
}
var submission model.PreChatFormSubmission
if err := c.ShouldBindJSON(&submission); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
return
}
resp, err := h.widgetService.SubmitPreChatForm(c.Request.Context(), websiteToken, submission)
if err != nil {
status := http.StatusBadRequest
if err.Error() == "pre-chat form is not enabled for this inbox" {
status = http.StatusForbidden
}
c.JSON(status, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
// --- Widget File Upload Handlers (website_token path param) ---
// M11: Staged file upload with attachment processing
// StageFileUpload handles a file upload from the widget, staging it for later attachment.
// POST /widget/:website_token/uploads
// Accepts multipart/form-data with a "file" field.
// Returns upload_uuid that can be referenced when sending a message with attachment.
// Reference: Chatwoot widget SDK — file upload before sending message
func (h *WidgetHandler) StageFileUpload(c *gin.Context) {
websiteToken := c.Param("website_token")
if websiteToken == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"})
return
}
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "file field is required", "details": err.Error()})
return
}
fileReader, err := file.Open()
if err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to open uploaded file", "details": err.Error()})
return
}
defer fileReader.Close()
req := service.WidgetUploadRequest{
WebsiteToken: websiteToken,
FileName: file.Filename,
FileSize: file.Size,
FileHeader: file,
}
resp, err := h.widgetService.StageFileUpload(c.Request.Context(), req, fileReader)
if err != nil {
applogger.L().Errorf("Failed to stage file upload for website_token=%s: %v", websiteToken, err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, resp)
}
// GetFileUploadStatus checks the status of a staged file upload by UUID.
// GET /widget/:website_token/uploads/:upload_uuid
func (h *WidgetHandler) GetFileUploadStatus(c *gin.Context) {
websiteToken := c.Param("website_token")
if websiteToken == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"})
return
}
uploadUUID := c.Param("upload_uuid")
if uploadUUID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "upload_uuid is required"})
return
}
resp, err := h.widgetService.GetFileUploadStatus(c.Request.Context(), websiteToken, uploadUUID)
if err != nil {
applogger.L().Errorf("Failed to get upload status for uuid=%s: %v", uploadUUID, err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if resp == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "upload not found"})
return
}
c.JSON(http.StatusOK, resp)
}