Files
gochat/backend/internal/handler/widget/widget_theme_handler.go
T
Rogeeandrogee f719529d66 fix(security): harden auth and secret handling (HH-444) (#101)
* fix(security): harden auth and credential handling (HH-444)

* fix(security): address HH-444 review blockers

* fix(security): close remaining HH-444 review blockers

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-22 15:45:06 +08:00

180 lines
5.7 KiB
Go

package widget
import (
"net/http"
"strings"
"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) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, (16<<20)+(1<<20))
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 {
status := http.StatusBadRequest
if strings.Contains(strings.ToLower(err.Error()), "request body too large") {
status = http.StatusRequestEntityTooLarge
}
c.JSON(status, 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{
WidgetToken: widgetTokenFromRequest(c),
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 widget file upload: %v", 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, widgetTokenFromRequest(c))
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)
}