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.
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user