mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-10 22:25:39 +00:00
DRY up widget inbox validation, use validateWidgetInbox middleware
This commit is contained in:
+7
-81
@@ -19,7 +19,6 @@ import (
|
||||
bhmodels "github.com/abhinavxd/libredesk/internal/business_hours/models"
|
||||
cmodels "github.com/abhinavxd/libredesk/internal/conversation/models"
|
||||
"github.com/abhinavxd/libredesk/internal/envelope"
|
||||
"github.com/abhinavxd/libredesk/internal/httputil"
|
||||
"github.com/abhinavxd/libredesk/internal/inbox/channel/livechat"
|
||||
imodels "github.com/abhinavxd/libredesk/internal/inbox/models"
|
||||
"github.com/abhinavxd/libredesk/internal/stringutil"
|
||||
@@ -97,9 +96,9 @@ type conversationResponseWithBusinessHours struct {
|
||||
// handleGetChatLauncherSettings returns the live chat launcher settings for the widget.
|
||||
func handleGetChatLauncherSettings(r *fastglue.Request) error {
|
||||
r.RequestCtx.Response.Header.Set("Access-Control-Allow-Origin", "*")
|
||||
_, config, err := validateLiveChatInbox(r)
|
||||
config, err := getWidgetConfig(r)
|
||||
if err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, err.Error(), nil))
|
||||
}
|
||||
|
||||
return r.SendEnvelope(map[string]any{
|
||||
@@ -112,9 +111,9 @@ func handleGetChatLauncherSettings(r *fastglue.Request) error {
|
||||
func handleGetChatSettings(r *fastglue.Request) error {
|
||||
app := r.Context.(*App)
|
||||
|
||||
_, config, err := validateLiveChatInbox(r)
|
||||
config, err := getWidgetConfig(r)
|
||||
if err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, err.Error(), nil))
|
||||
}
|
||||
|
||||
response := chatSettingsResponse{
|
||||
@@ -711,14 +710,6 @@ func getContactConversation(r *fastglue.Request, conversationUUID string) (int,
|
||||
return contactID, conversation, nil
|
||||
}
|
||||
|
||||
func parseLiveChatConfig(inbox imodels.Inbox) (livechat.Config, error) {
|
||||
var config livechat.Config
|
||||
if err := json.Unmarshal(inbox.Config, &config); err != nil {
|
||||
return livechat.Config{}, fmt.Errorf("parsing live chat config: %w", err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func userTypeLabel(isVisitor bool) string {
|
||||
if isVisitor {
|
||||
return "visitor"
|
||||
@@ -726,67 +717,6 @@ func userTypeLabel(isVisitor bool) string {
|
||||
return "user"
|
||||
}
|
||||
|
||||
// validateLiveChatInbox validates inbox_id from query params and returns the inbox and parsed config.
|
||||
// Used by public widget endpoints that don't require JWT authentication.
|
||||
func validateLiveChatInbox(r *fastglue.Request) (imodels.Inbox, livechat.Config, error) {
|
||||
app := r.Context.(*App)
|
||||
inboxUUID := string(r.RequestCtx.QueryArgs().Peek("inbox_id"))
|
||||
|
||||
if inboxUUID == "" {
|
||||
return imodels.Inbox{}, livechat.Config{}, envelope.NewError(envelope.InputError,
|
||||
app.i18n.T("globals.messages.somethingWentWrong"), nil)
|
||||
}
|
||||
|
||||
inbox, err := app.inbox.GetDBRecord(inboxUUID)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching inbox", "inbox_uuid", inboxUUID, "error", err)
|
||||
return imodels.Inbox{}, livechat.Config{}, envelope.NewError(envelope.GeneralError,
|
||||
app.i18n.T("globals.messages.somethingWentWrong"), nil)
|
||||
}
|
||||
|
||||
if inbox.Channel != livechat.ChannelLiveChat {
|
||||
return imodels.Inbox{}, livechat.Config{}, envelope.NewError(envelope.InputError,
|
||||
app.i18n.T("validation.notFoundInbox"), nil)
|
||||
}
|
||||
|
||||
if !inbox.Enabled {
|
||||
return imodels.Inbox{}, livechat.Config{}, envelope.NewError(envelope.InputError,
|
||||
app.i18n.T("status.disabledInbox"), nil)
|
||||
}
|
||||
|
||||
config, err := parseLiveChatConfig(inbox)
|
||||
if err != nil {
|
||||
app.lo.Error("error parsing live chat config", "error", err)
|
||||
return imodels.Inbox{}, livechat.Config{}, envelope.NewError(envelope.GeneralError,
|
||||
app.i18n.T("validation.invalidInbox"), nil)
|
||||
}
|
||||
|
||||
// Check if the client's IP is blocked.
|
||||
if len(config.BlockedIPs) > 0 {
|
||||
clientIP := realip.FromRequest(r.RequestCtx)
|
||||
if httputil.IsIPBlocked(clientIP, config.BlockedIPs) {
|
||||
app.lo.Info("client IP blocked for live chat inbox", "client_id", clientIP, "inbox_uuid", inboxUUID)
|
||||
return imodels.Inbox{}, livechat.Config{}, envelope.NewError(envelope.PermissionError,
|
||||
app.i18n.T("widget.ipBlocked"), nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate referer against trusted domains.
|
||||
if len(config.TrustedDomains) > 0 {
|
||||
referer := string(r.RequestCtx.Request.Header.Peek("Referer"))
|
||||
if referer != "" && !httputil.IsOriginTrusted(referer, config.TrustedDomains) {
|
||||
app.lo.Warn("widget request from untrusted referer blocked",
|
||||
"referer", referer,
|
||||
"inbox_uuid", inboxUUID,
|
||||
"trusted_domains", config.TrustedDomains)
|
||||
return imodels.Inbox{}, livechat.Config{}, envelope.NewError(envelope.PermissionError,
|
||||
app.i18n.T("widget.ipBlocked"), nil)
|
||||
}
|
||||
}
|
||||
|
||||
return inbox, config, nil
|
||||
}
|
||||
|
||||
// saveContactAttrsAndCollectConvoAttrs validates and saves contact custom attributes from JWT and form data.
|
||||
// Returns conversation custom attributes from the pre-chat form for the caller to apply after conversation creation.
|
||||
func saveContactAttrsAndCollectConvoAttrs(app *App, contactID int, claims *Claims, formData map[string]any, config livechat.Config) map[string]any {
|
||||
@@ -928,8 +858,8 @@ func buildConversationResponseWithBusinessHours(app *App, conversation cmodels.C
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// resolveUserIDFromClaims resolves the actual user ID from JWT claims,
|
||||
// handling both regular user_id and external_user_id cases
|
||||
// resolveUserFromClaims resolves the actual user from JWT claims,
|
||||
// handling both regular user_id and external_user_id cases.
|
||||
func resolveUserFromClaims(app *App, claims Claims) (umodels.User, error) {
|
||||
var (
|
||||
user umodels.User
|
||||
@@ -955,22 +885,18 @@ func resolveUserFromClaims(app *App, claims Claims) (umodels.User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// verifyJWT verifies and validates a JWT token with proper signature verification
|
||||
// verifyJWT verifies and validates a JWT token with proper signature verification.
|
||||
func verifyJWT(tokenString string, secretKey []byte) (*Claims, error) {
|
||||
// Parse and verify the token
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
// Verify the signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return secretKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract claims if token is valid
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
+4
-9
@@ -272,9 +272,9 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
|
||||
g.GET("/widget/ws", rateLimit(handleWidgetWS, "widget"))
|
||||
|
||||
// Widget APIs.
|
||||
g.GET("/api/v1/widget/chat/settings/launcher", rateLimit(handleGetChatLauncherSettings, "widget"))
|
||||
g.GET("/api/v1/widget/chat/settings", rateLimit(handleGetChatSettings, "widget"))
|
||||
g.POST("/api/v1/widget/chat/auth/exchange", rateLimit(widgetInboxAuth(handleAuthExchange), "widget"))
|
||||
g.GET("/api/v1/widget/chat/settings/launcher", rateLimit(validateWidgetInbox(handleGetChatLauncherSettings), "widget"))
|
||||
g.GET("/api/v1/widget/chat/settings", rateLimit(validateWidgetInbox(handleGetChatSettings), "widget"))
|
||||
g.POST("/api/v1/widget/chat/auth/exchange", rateLimit(validateWidgetInbox(handleAuthExchange), "widget"))
|
||||
g.GET("/api/v1/widget/chat/auth/me", rateLimit(widgetAuth(handleWidgetAuthMe), "widget"))
|
||||
g.POST("/api/v1/widget/chat/conversations/init", rateLimit(widgetAuth(handleChatInit), "widget"))
|
||||
g.GET("/api/v1/widget/chat/conversations", rateLimit(widgetAuth(handleGetConversations), "widget"))
|
||||
@@ -285,7 +285,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
|
||||
|
||||
// Frontend pages.
|
||||
g.GET("/", notAuthPage(serveIndexPage))
|
||||
g.GET("/widget", serveWidgetIndexPage)
|
||||
g.GET("/widget", validateWidgetInbox(serveWidgetIndexPage))
|
||||
g.GET("/inboxes/{all:*}", authPage(serveIndexPage))
|
||||
g.GET("/teams/{all:*}", authPage(serveIndexPage))
|
||||
g.GET("/views/{all:*}", authPage(serveIndexPage))
|
||||
@@ -342,11 +342,6 @@ func serveIndexPage(r *fastglue.Request) error {
|
||||
func serveWidgetIndexPage(r *fastglue.Request) error {
|
||||
app := r.Context.(*App)
|
||||
|
||||
// Validate inbox and referer/IP.
|
||||
if _, _, err := validateLiveChatInbox(r); err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
// Prevent caching of the index page.
|
||||
r.RequestCtx.Response.Header.Add("Cache-Control", "no-store, no-cache, must-revalidate, post-check=0, pre-check=0")
|
||||
r.RequestCtx.Response.Header.Add("Pragma", "no-cache")
|
||||
|
||||
@@ -25,13 +25,16 @@ const (
|
||||
hdrClearVisitorToken = "X-Libredesk-Clear-Visitor"
|
||||
)
|
||||
|
||||
// widgetInboxAuth middleware validates the inbox from the request header, checks IP/domain
|
||||
// restrictions, and sets inbox + config in context. Does not require session token.
|
||||
func widgetInboxAuth(next func(*fastglue.Request) error) func(*fastglue.Request) error {
|
||||
// validateWidgetInbox middleware validates the inbox from the request header or query param,
|
||||
// checks IP/domain restrictions, and sets inbox + config in context.
|
||||
func validateWidgetInbox(next func(*fastglue.Request) error) func(*fastglue.Request) error {
|
||||
return func(r *fastglue.Request) error {
|
||||
app := r.Context.(*App)
|
||||
|
||||
inboxUUID := string(r.RequestCtx.Request.Header.Peek(hdrWidgetInboxID))
|
||||
if inboxUUID == "" {
|
||||
inboxUUID = string(r.RequestCtx.QueryArgs().Peek("inbox_id"))
|
||||
}
|
||||
if inboxUUID == "" {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
|
||||
}
|
||||
@@ -79,10 +82,10 @@ func widgetInboxAuth(next func(*fastglue.Request) error) func(*fastglue.Request)
|
||||
}
|
||||
|
||||
// widgetAuth middleware validates the session token from the Authorization header
|
||||
// using Redis lookup. Wraps widgetInboxAuth for inbox validation.
|
||||
// using Redis lookup. Wraps validateWidgetInbox for inbox validation.
|
||||
// For /conversations/init without a token, allows visitor creation.
|
||||
func widgetAuth(next func(*fastglue.Request) error) func(*fastglue.Request) error {
|
||||
return widgetInboxAuth(func(r *fastglue.Request) error {
|
||||
return validateWidgetInbox(func(r *fastglue.Request) error {
|
||||
app := r.Context.(*App)
|
||||
inbox, err := getWidgetInbox(r)
|
||||
if err != nil {
|
||||
|
||||
+1
-12
@@ -138,15 +138,4 @@ batch_check_interval = "5m"
|
||||
|
||||
[sla]
|
||||
# How often to evaluate SLA compliance for conversations
|
||||
evaluation_interval = "5m"
|
||||
|
||||
[rate_limit]
|
||||
[rate_limit.widget]
|
||||
enabled = true
|
||||
requests_per_minute = 100
|
||||
[rate_limit.auth]
|
||||
enabled = true
|
||||
requests_per_minute = 20
|
||||
[rate_limit.public]
|
||||
enabled = true
|
||||
requests_per_minute = 60
|
||||
evaluation_interval = "5m"
|
||||
Reference in New Issue
Block a user