mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-21 18:13:29 +00:00
3c3d6a6149
Add /auth/exchange, /auth/me endpoints for the same. Fix duplicate network calls, and agent notification for new conversations.
193 lines
7.3 KiB
Go
193 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"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"
|
|
realip "github.com/ferluci/fast-realip"
|
|
"github.com/valyala/fasthttp"
|
|
"github.com/zerodha/fastglue"
|
|
)
|
|
|
|
const (
|
|
ctxWidgetContactID = "widget_contact_id"
|
|
ctxWidgetIsVisitor = "widget_is_visitor"
|
|
ctxWidgetInbox = "widget_inbox"
|
|
ctxWidgetConfig = "widget_config"
|
|
|
|
hdrWidgetInboxID = "X-Libredesk-Inbox-ID"
|
|
hdrWidgetVisitorToken = "X-Libredesk-Visitor-Token"
|
|
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 {
|
|
return func(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
|
|
inboxUUID := string(r.RequestCtx.Request.Header.Peek(hdrWidgetInboxID))
|
|
if inboxUUID == "" {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
|
|
}
|
|
|
|
inbox, err := app.inbox.GetDBRecord(inboxUUID)
|
|
if err != nil {
|
|
app.lo.Error("error fetching inbox", "inbox_uuid", inboxUUID, "error", err)
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
if !inbox.Enabled {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("status.disabledInbox"), nil, envelope.InputError)
|
|
}
|
|
if inbox.Channel != livechat.ChannelLiveChat {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("validation.notFoundInbox"), nil, envelope.InputError)
|
|
}
|
|
|
|
var config livechat.Config
|
|
if err := json.Unmarshal(inbox.Config, &config); err != nil {
|
|
app.lo.Error("error parsing live chat config", "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
|
|
}
|
|
|
|
if len(config.BlockedIPs) > 0 {
|
|
clientIP := realip.FromRequest(r.RequestCtx)
|
|
if httputil.IsIPBlocked(clientIP, config.BlockedIPs) {
|
|
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.T("widget.ipBlocked"), nil, envelope.PermissionError)
|
|
}
|
|
}
|
|
|
|
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 r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.T("widget.ipBlocked"), nil, envelope.PermissionError)
|
|
}
|
|
}
|
|
|
|
r.RequestCtx.SetUserValue(ctxWidgetInbox, inbox)
|
|
r.RequestCtx.SetUserValue(ctxWidgetConfig, config)
|
|
return next(r)
|
|
}
|
|
}
|
|
|
|
// widgetAuth middleware validates the session token from the Authorization header
|
|
// using Redis lookup. Wraps widgetInboxAuth 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 {
|
|
app := r.Context.(*App)
|
|
inbox, err := getWidgetInbox(r)
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
|
|
}
|
|
|
|
authHeader := string(r.RequestCtx.Request.Header.Peek("Authorization"))
|
|
|
|
// For init endpoint, allow requests without token (visitor creation).
|
|
if authHeader == "" && strings.HasSuffix(string(r.RequestCtx.Path()), "/conversations/init") {
|
|
return next(r)
|
|
}
|
|
|
|
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
|
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
|
}
|
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
|
|
|
session, err := lookupSessionToken(app, token)
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
|
}
|
|
|
|
// Verify session belongs to this inbox.
|
|
if session.InboxID != inbox.ID {
|
|
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
|
}
|
|
|
|
// Verify user exists and is enabled.
|
|
u, err := app.user.Get(session.UserID, "", []string{})
|
|
if err != nil || !u.Enabled {
|
|
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
|
}
|
|
|
|
r.RequestCtx.SetUserValue(ctxWidgetContactID, session.UserID)
|
|
r.RequestCtx.SetUserValue(ctxWidgetIsVisitor, session.IsVisitor)
|
|
|
|
// Merge visitor to contact if visitor token is provided.
|
|
visitorToken := string(r.RequestCtx.Request.Header.Peek(hdrWidgetVisitorToken))
|
|
if visitorToken != "" && session.ExternalUserID != "" && session.UserID > 0 {
|
|
visitorSession, vErr := lookupSessionToken(app, visitorToken)
|
|
if vErr == nil && visitorSession.IsVisitor && visitorSession.UserID > 0 && visitorSession.UserID != session.UserID {
|
|
if err := app.user.MergeVisitorToContact(visitorSession.UserID, session.UserID); err != nil {
|
|
app.lo.Error("error merging visitor to contact", "visitor_id", visitorSession.UserID, "contact_id", session.UserID, "error", err)
|
|
} else {
|
|
app.lo.Info("merged visitor to contact", "visitor_id", visitorSession.UserID, "contact_id", session.UserID)
|
|
deleteSessionToken(app, visitorToken)
|
|
r.RequestCtx.Response.Header.Set(hdrClearVisitorToken, "true")
|
|
}
|
|
}
|
|
}
|
|
|
|
return next(r)
|
|
})
|
|
}
|
|
|
|
// getWidgetContactID extracts contact ID from request context.
|
|
func getWidgetContactID(r *fastglue.Request) (int, error) {
|
|
val := r.RequestCtx.UserValue(ctxWidgetContactID)
|
|
if val == nil {
|
|
return 0, fmt.Errorf("widget middleware not applied: missing contact ID in context")
|
|
}
|
|
contactID, ok := val.(int)
|
|
if !ok {
|
|
return 0, fmt.Errorf("invalid contact ID type in context")
|
|
}
|
|
return contactID, nil
|
|
}
|
|
|
|
// getWidgetIsVisitor extracts the visitor flag from request context.
|
|
func getWidgetIsVisitor(r *fastglue.Request) bool {
|
|
val := r.RequestCtx.UserValue(ctxWidgetIsVisitor)
|
|
if val == nil {
|
|
return true
|
|
}
|
|
v, ok := val.(bool)
|
|
if !ok {
|
|
return true
|
|
}
|
|
return v
|
|
}
|
|
|
|
// getWidgetInbox extracts inbox model from request context.
|
|
func getWidgetInbox(r *fastglue.Request) (imodels.Inbox, error) {
|
|
val := r.RequestCtx.UserValue(ctxWidgetInbox)
|
|
if val == nil {
|
|
return imodels.Inbox{}, fmt.Errorf("widget middleware not applied: missing inbox in context")
|
|
}
|
|
inbox, ok := val.(imodels.Inbox)
|
|
if !ok {
|
|
return imodels.Inbox{}, fmt.Errorf("invalid inbox type in context")
|
|
}
|
|
return inbox, nil
|
|
}
|
|
|
|
// getWidgetConfig extracts parsed livechat config from request context.
|
|
func getWidgetConfig(r *fastglue.Request) (livechat.Config, error) {
|
|
val := r.RequestCtx.UserValue(ctxWidgetConfig)
|
|
if val == nil {
|
|
return livechat.Config{}, fmt.Errorf("widget middleware not applied: missing config in context")
|
|
}
|
|
config, ok := val.(livechat.Config)
|
|
if !ok {
|
|
return livechat.Config{}, fmt.Errorf("invalid config type in context")
|
|
}
|
|
return config, nil
|
|
}
|