mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-22 18:43:33 +00:00
refactor: implement widget authentication middleware with standard HTTP headers
- Add widgetAuth middleware to handle JWT and inbox validation consistently
- Move authentication logic from request body to standard HTTP headers:
* JWT: Authorization: Bearer <token>
* Inbox ID: X-Libredesk-Inbox-ID: <id>
- Refactor all widget handlers to use middleware context instead of duplicate auth code
- Frontend now sends auth headers via HTTP interceptor for all widget requests
This commit is contained in:
+90
-177
@@ -22,11 +22,6 @@ import (
|
||||
"github.com/zerodha/fastglue"
|
||||
)
|
||||
|
||||
type reqCommon struct {
|
||||
JWT string `json:"jwt"`
|
||||
InboxID int `json:"inbox_id"`
|
||||
}
|
||||
|
||||
// Define JWT claims structure
|
||||
type Claims struct {
|
||||
UserID int `json:"user_id,omitempty"`
|
||||
@@ -39,24 +34,11 @@ type Claims struct {
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// Chat widget initialization request
|
||||
type chatInitReq struct {
|
||||
reqCommon
|
||||
VisitorName string `json:"visitor_name,omitempty"`
|
||||
VisitorEmail string `json:"visitor_email,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type conversationResp struct {
|
||||
Conversation cmodels.ChatConversation `json:"conversation"`
|
||||
Messages []cmodels.ChatMessage `json:"messages"`
|
||||
}
|
||||
|
||||
type chatMessageReq struct {
|
||||
Message string `json:"message"`
|
||||
reqCommon
|
||||
}
|
||||
|
||||
type chatSettingsResponse struct {
|
||||
livechat.Config
|
||||
BusinessHours []bhmodels.BusinessHours `json:"business_hours,omitempty"`
|
||||
@@ -175,7 +157,11 @@ func handleGetChatSettings(r *fastglue.Request) error {
|
||||
func handleChatInit(r *fastglue.Request) error {
|
||||
var (
|
||||
app = r.Context.(*App)
|
||||
req = chatInitReq{}
|
||||
req = struct {
|
||||
VisitorName string `json:"visitor_name,omitempty"`
|
||||
VisitorEmail string `json:"visitor_email,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}{}
|
||||
)
|
||||
|
||||
if err := r.Decode(&req, "json"); err != nil {
|
||||
@@ -187,17 +173,20 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.message}"), nil, envelope.InputError)
|
||||
}
|
||||
|
||||
// Make sure the inbox is enabled and of the correct type.
|
||||
inbox, err := app.inbox.GetDBRecord(req.InboxID)
|
||||
// Get authenticated data from context (set by middleware)
|
||||
// Middleware always validates inbox, so we can safely use non-optional getters
|
||||
claims := getWidgetClaimsOptional(r)
|
||||
|
||||
inboxID, err := getWidgetInboxID(r)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching inbox", "inbox_id", req.InboxID, "error", err)
|
||||
return sendErrorEnvelope(r, err)
|
||||
app.lo.Error("error getting inbox ID from middleware context", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
|
||||
}
|
||||
if !inbox.Enabled {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.disabled", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
|
||||
}
|
||||
if inbox.Channel != livechat.ChannelLiveChat {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
|
||||
|
||||
inbox, err := getWidgetInbox(r)
|
||||
if err != nil {
|
||||
app.lo.Error("error getting inbox from middleware context", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -205,6 +194,7 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
conversationUUID string
|
||||
isVisitor bool
|
||||
config livechat.Config
|
||||
newJWT string
|
||||
)
|
||||
|
||||
// Parse inbox config
|
||||
@@ -213,18 +203,16 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.invalid", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
// Handle authenticated user.
|
||||
if req.JWT != "" {
|
||||
var claims Claims
|
||||
var err error
|
||||
|
||||
claims, err = verifyStandardJWT(req.JWT, inbox.Secret.String)
|
||||
// Handle authenticated user vs visitor
|
||||
if claims != nil {
|
||||
// Use authenticated contact ID from middleware
|
||||
contactID, err = getWidgetContactID(r)
|
||||
if err != nil {
|
||||
app.lo.Error("invalid inbox JWT", "jwt", req.JWT, "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.messages.sessionExpired"), nil, envelope.UnauthorizedError)
|
||||
app.lo.Error("error getting contact ID from middleware context", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
// Handle existing contacts they all external user id.
|
||||
// Handle existing contacts with external user id - check if we need to create user
|
||||
if claims.ExternalUserID != "" {
|
||||
// Find or create user based on external_user_id.
|
||||
user, err := app.user.GetByExternalID(claims.ExternalUserID)
|
||||
@@ -262,7 +250,6 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
contactID = user.ID
|
||||
isVisitor = false
|
||||
} else {
|
||||
contactID = claims.UserID
|
||||
isVisitor = claims.IsVisitor
|
||||
}
|
||||
} else {
|
||||
@@ -279,7 +266,7 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
}
|
||||
contactID = visitor.ID
|
||||
secretToUse := []byte(inbox.Secret.String)
|
||||
req.JWT, err = generateUserJWTWithSecret(contactID, isVisitor, time.Now().Add(87600*time.Hour), secretToUse) // 10 years
|
||||
newJWT, err = generateUserJWTWithSecret(contactID, isVisitor, time.Now().Add(87600*time.Hour), secretToUse) // 10 years
|
||||
if err != nil {
|
||||
app.lo.Error("error generating visitor JWT", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorGenerating", "name", "{globals.terms.session}"), nil, envelope.GeneralError)
|
||||
@@ -297,7 +284,7 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
}
|
||||
|
||||
if userConfig.PreventMultipleConversations {
|
||||
conversations, err := app.conversation.GetContactChatConversations(contactID, req.InboxID)
|
||||
conversations, err := app.conversation.GetContactChatConversations(contactID, inboxID)
|
||||
if err != nil {
|
||||
userType := "visitor"
|
||||
if !isVisitor {
|
||||
@@ -316,12 +303,12 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
}
|
||||
}
|
||||
|
||||
app.lo.Info("creating new live chat conversation for user", "user_id", contactID, "inbox_id", req.InboxID, "is_visitor", isVisitor)
|
||||
app.lo.Info("creating new live chat conversation for user", "user_id", contactID, "inbox_id", inboxID, "is_visitor", isVisitor)
|
||||
|
||||
// Create conversation.
|
||||
_, conversationUUID, err = app.conversation.CreateConversation(
|
||||
contactID,
|
||||
req.InboxID,
|
||||
inboxID,
|
||||
"",
|
||||
time.Now(),
|
||||
"",
|
||||
@@ -370,13 +357,20 @@ func handleChatInit(r *fastglue.Request) error {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
return r.SendEnvelope(map[string]any{
|
||||
// For visitors, return the new JWT. For authenticated users, no JWT is needed in response.
|
||||
response := map[string]any{
|
||||
"conversation": resp.Conversation,
|
||||
"messages": resp.Messages,
|
||||
"business_hours_id": resp.BusinessHoursID,
|
||||
"working_hours_utc_offset": resp.WorkingHoursUTCOffset,
|
||||
"jwt": req.JWT,
|
||||
})
|
||||
}
|
||||
|
||||
// Only add JWT for visitor creation
|
||||
if newJWT != "" {
|
||||
response["jwt"] = newJWT
|
||||
}
|
||||
|
||||
return r.SendEnvelope(response)
|
||||
}
|
||||
|
||||
// handleChatUpdateLastSeen updates contact last seen timestamp for a conversation
|
||||
@@ -384,16 +378,17 @@ func handleChatUpdateLastSeen(r *fastglue.Request) error {
|
||||
var (
|
||||
app = r.Context.(*App)
|
||||
conversationUUID = r.RequestCtx.UserValue("uuid").(string)
|
||||
req = reqCommon{}
|
||||
)
|
||||
|
||||
if conversationUUID == "" {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.conversation}"), nil, envelope.InputError)
|
||||
}
|
||||
|
||||
if err := r.Decode(&req, "json"); err != nil {
|
||||
app.lo.Error("error unmarshalling chat update last seen request", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
|
||||
// Get authenticated data from middleware context
|
||||
contactID, err := getWidgetContactID(r)
|
||||
if err != nil {
|
||||
app.lo.Error("error getting contact ID from middleware context", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
conversation, err := app.conversation.GetConversation(0, conversationUUID)
|
||||
@@ -402,26 +397,6 @@ func handleChatUpdateLastSeen(r *fastglue.Request) error {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
inbox, err := app.inbox.GetDBRecord(conversation.InboxID)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching inbox", "inbox_id", conversation.InboxID, "error", err)
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
// Verify JWT.
|
||||
claims, err := verifyStandardJWT(req.JWT, inbox.Secret.String)
|
||||
if err != nil {
|
||||
app.lo.Error("invalid JWT", "jwt", req.JWT, "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
// Resolve user ID from JWT claims (handles both user_id and external_user_id)
|
||||
contactID, err := resolveUserIDFromClaims(app, claims)
|
||||
if err != nil {
|
||||
app.lo.Error("error resolving user ID from JWT claims", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
// Make sure the conversation belongs to the contact.
|
||||
if conversation.ContactID != contactID {
|
||||
app.lo.Error("unauthorized access to conversation", "conversation_uuid", conversationUUID, "contact_id", contactID, "conversation_contact_id", conversation.ContactID)
|
||||
@@ -442,47 +417,26 @@ func handleChatGetConversation(r *fastglue.Request) error {
|
||||
var (
|
||||
app = r.Context.(*App)
|
||||
conversationUUID = r.RequestCtx.UserValue("uuid").(string)
|
||||
chatReq = chatInitReq{}
|
||||
)
|
||||
|
||||
if conversationUUID == "" {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "conversation_id is required", nil, envelope.InputError)
|
||||
}
|
||||
|
||||
// Decode chat request if present
|
||||
if err := r.Decode(&chatReq, "json"); err != nil {
|
||||
app.lo.Error("error unmarshalling chat request", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
|
||||
// Get authenticated data from middleware context
|
||||
contactID, err := getWidgetContactID(r)
|
||||
if err != nil {
|
||||
app.lo.Error("error getting contact ID from middleware context", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
// Fetch conversation first to get inbox info
|
||||
// Fetch conversation
|
||||
conversation, err := app.conversation.GetConversation(0, conversationUUID)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching conversation", "conversation_uuid", conversationUUID, "error", err)
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
// Get inbox to retrieve secret for JWT verification
|
||||
inbox, err := app.inbox.GetDBRecord(conversation.InboxID)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching inbox", "inbox_id", conversation.InboxID, "error", err)
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
// Verify JWT.
|
||||
claims, err := verifyStandardJWT(chatReq.JWT, inbox.Secret.String)
|
||||
if err != nil {
|
||||
app.lo.Error("invalid JWT", "jwt", chatReq.JWT, "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
// Resolve user ID from JWT claims (handles both user_id and external_user_id)
|
||||
contactID, err := resolveUserIDFromClaims(app, claims)
|
||||
if err != nil {
|
||||
app.lo.Error("error resolving user ID from JWT claims", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
// Make sure the conversation belongs to the contact.
|
||||
if conversation.ContactID != contactID {
|
||||
app.lo.Error("unauthorized access to conversation", "conversation_uuid", conversationUUID, "contact_id", contactID, "conversation_contact_id", conversation.ContactID)
|
||||
@@ -502,37 +456,23 @@ func handleChatGetConversation(r *fastglue.Request) error {
|
||||
func handleGetConversations(r *fastglue.Request) error {
|
||||
var (
|
||||
app = r.Context.(*App)
|
||||
req = reqCommon{}
|
||||
)
|
||||
|
||||
if err := r.Decode(&req, "json"); err != nil {
|
||||
app.lo.Error("error unmarshalling chat conversations request", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
|
||||
// Get authenticated data from middleware context
|
||||
contactID, err := getWidgetContactID(r)
|
||||
if err != nil {
|
||||
app.lo.Error("error getting contact ID from middleware context", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
// Get inbox to retrieve secret for JWT verification
|
||||
inbox, err := app.inbox.GetDBRecord(req.InboxID)
|
||||
inboxID, err := getWidgetInboxID(r)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching inbox", "inbox_id", req.InboxID, "error", err)
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
// Verify JWT.
|
||||
claims, err := verifyStandardJWT(req.JWT, inbox.Secret.String)
|
||||
if err != nil {
|
||||
app.lo.Error("invalid JWT", "jwt", req.JWT, "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
// Resolve user ID from JWT claims (handles both user_id and external_user_id)
|
||||
contactID, err := resolveUserIDFromClaims(app, claims)
|
||||
if err != nil {
|
||||
app.lo.Error("error resolving user ID from JWT claims", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
app.lo.Error("error getting inbox ID from middleware context", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
// Fetch conversations for the contact and convert to ChatConversation format.
|
||||
chatConversations, err := app.conversation.GetContactChatConversations(contactID, req.InboxID)
|
||||
chatConversations, err := app.conversation.GetContactChatConversations(contactID, inboxID)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching conversations for contact", "contact_id", contactID, "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.conversation}"), nil, envelope.GeneralError)
|
||||
@@ -546,9 +486,10 @@ func handleChatSendMessage(r *fastglue.Request) error {
|
||||
var (
|
||||
app = r.Context.(*App)
|
||||
conversationUUID = r.RequestCtx.UserValue("uuid").(string)
|
||||
req = chatMessageReq{}
|
||||
senderType = cmodels.SenderTypeContact
|
||||
senderID = 0
|
||||
req = struct {
|
||||
Message string `json:"message"`
|
||||
}{}
|
||||
senderType = cmodels.SenderTypeContact
|
||||
)
|
||||
|
||||
if err := r.Decode(&req, "json"); err != nil {
|
||||
@@ -560,33 +501,26 @@ func handleChatSendMessage(r *fastglue.Request) error {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.message}"), nil, envelope.InputError)
|
||||
}
|
||||
|
||||
// Fetch conversation to ensure it exists and get inbox info.
|
||||
// Get authenticated data from middleware context
|
||||
senderID, err := getWidgetContactID(r)
|
||||
if err != nil {
|
||||
app.lo.Error("error getting contact ID from middleware context", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
inbox, err := getWidgetInbox(r)
|
||||
if err != nil {
|
||||
app.lo.Error("error getting inbox from middleware context", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
// Fetch conversation to ensure it exists
|
||||
conversation, err := app.conversation.GetConversation(0, conversationUUID)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching conversation", "conversation_uuid", conversationUUID, "error", err)
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
// Get inbox to retrieve secret for JWT verification
|
||||
inbox, err := app.inbox.GetDBRecord(conversation.InboxID)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching inbox", "inbox_id", conversation.InboxID, "error", err)
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
claims, err := verifyStandardJWT(req.JWT, inbox.Secret.String)
|
||||
if err != nil {
|
||||
app.lo.Error("invalid JWT", "jwt", req.JWT, "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
// Resolve user ID from JWT claims (handles both user_id and external_user_id)
|
||||
senderID, err = resolveUserIDFromClaims(app, claims)
|
||||
if err != nil {
|
||||
app.lo.Error("error resolving user ID from JWT claims", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
// Fetch sender.
|
||||
sender, err := app.user.Get(senderID, "", "")
|
||||
if err != nil {
|
||||
@@ -655,8 +589,7 @@ func handleChatSendMessage(r *fastglue.Request) error {
|
||||
// handleWidgetMediaUpload handles media uploads for the widget.
|
||||
func handleWidgetMediaUpload(r *fastglue.Request) error {
|
||||
var (
|
||||
app = r.Context.(*App)
|
||||
senderID = 0
|
||||
app = r.Context.(*App)
|
||||
)
|
||||
|
||||
form, err := r.RequestCtx.MultipartForm()
|
||||
@@ -665,12 +598,18 @@ func handleWidgetMediaUpload(r *fastglue.Request) error {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
// Get JWT token from form data
|
||||
jwtValues, jwtOk := form.Value["jwt"]
|
||||
if !jwtOk || len(jwtValues) == 0 || jwtValues[0] == "" {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
// Get authenticated data from middleware context
|
||||
senderID, err := getWidgetContactID(r)
|
||||
if err != nil {
|
||||
app.lo.Error("error getting contact ID from middleware context", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
|
||||
}
|
||||
|
||||
inbox, err := getWidgetInbox(r)
|
||||
if err != nil {
|
||||
app.lo.Error("error getting inbox from middleware context", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
|
||||
}
|
||||
jwtToken := jwtValues[0]
|
||||
|
||||
// Get conversation UUID from form data
|
||||
conversationValues, convOk := form.Value["conversation_uuid"]
|
||||
@@ -686,32 +625,6 @@ func handleWidgetMediaUpload(r *fastglue.Request) error {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
// Get inbox to retrieve secret for JWT verification
|
||||
inbox, err := app.inbox.GetDBRecord(conversation.InboxID)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching inbox", "inbox_id", conversation.InboxID, "error", err)
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
// Make sure the inbox is enabled.
|
||||
if !inbox.Enabled {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.disabled", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
|
||||
}
|
||||
|
||||
// Verify JWT and get user information
|
||||
claims, err := verifyStandardJWT(jwtToken, inbox.Secret.String)
|
||||
if err != nil {
|
||||
app.lo.Error("invalid JWT", "jwt", jwtToken, "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
// Resolve user ID from JWT claims (handles both user_id and external_user_id)
|
||||
senderID, err = resolveUserIDFromClaims(app, claims)
|
||||
if err != nil {
|
||||
app.lo.Error("error resolving user ID from JWT claims", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
if conversation.ContactID != senderID {
|
||||
app.lo.Error("access denied: user attempted to access conversation owned by different contact", "conversation_uuid", conversationUUID, "requesting_contact_id", senderID, "conversation_owner_id", conversation.ContactID)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.Ts("globals.messages.denied", "name", "{globals.terms.permission}"), nil, envelope.PermissionError)
|
||||
@@ -1060,4 +973,4 @@ func generateUserJWTWithSecret(userID int, isVisitor bool, expirationTime time.T
|
||||
return "", err
|
||||
}
|
||||
return tokenString, nil
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -227,12 +227,12 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
|
||||
// Widget APIs.
|
||||
g.GET("/api/v1/widget/chat/settings/launcher", handleGetChatLauncherSettings)
|
||||
g.GET("/api/v1/widget/chat/settings", handleGetChatSettings)
|
||||
g.POST("/api/v1/widget/chat/conversations/init", handleChatInit)
|
||||
g.POST("/api/v1/widget/chat/conversations", handleGetConversations)
|
||||
g.POST("/api/v1/widget/chat/conversations/{uuid}/update-last-seen", handleChatUpdateLastSeen)
|
||||
g.POST("/api/v1/widget/chat/conversations/{uuid}", handleChatGetConversation)
|
||||
g.POST("/api/v1/widget/chat/conversations/{uuid}/message", handleChatSendMessage)
|
||||
g.POST("/api/v1/widget/media/upload", handleWidgetMediaUpload)
|
||||
g.POST("/api/v1/widget/chat/conversations/init", widgetAuth(handleChatInit))
|
||||
g.POST("/api/v1/widget/chat/conversations", widgetAuth(handleGetConversations))
|
||||
g.POST("/api/v1/widget/chat/conversations/{uuid}/update-last-seen", widgetAuth(handleChatUpdateLastSeen))
|
||||
g.POST("/api/v1/widget/chat/conversations/{uuid}", widgetAuth(handleChatGetConversation))
|
||||
g.POST("/api/v1/widget/chat/conversations/{uuid}/message", widgetAuth(handleChatSendMessage))
|
||||
g.POST("/api/v1/widget/media/upload", widgetAuth(handleWidgetMediaUpload))
|
||||
|
||||
// Frontend pages.
|
||||
g.GET("/", notAuthPage(serveIndexPage))
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/abhinavxd/libredesk/internal/envelope"
|
||||
"github.com/abhinavxd/libredesk/internal/inbox/channel/livechat"
|
||||
imodels "github.com/abhinavxd/libredesk/internal/inbox/models"
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/zerodha/fastglue"
|
||||
)
|
||||
|
||||
const (
|
||||
// Context keys for storing authenticated widget data
|
||||
ctxWidgetClaims = "widget_claims"
|
||||
ctxWidgetInboxID = "widget_inbox_id"
|
||||
ctxWidgetContactID = "widget_contact_id"
|
||||
ctxWidgetInbox = "widget_inbox"
|
||||
|
||||
// Header sent in every widget request to identify the inbox
|
||||
hdrWidgetInboxID = "X-Libredesk-Inbox-ID"
|
||||
)
|
||||
|
||||
// widgetAuth middleware authenticates widget requests using JWT and inbox validation.
|
||||
// It always validates the inbox from X-Libredesk-Inbox-ID header, and conditionally validates JWT.
|
||||
// For /conversations/init without JWT, it allows visitor creation while still validating inbox.
|
||||
func widgetAuth(next func(*fastglue.Request) error) func(*fastglue.Request) error {
|
||||
return func(r *fastglue.Request) error {
|
||||
var (
|
||||
app = r.Context.(*App)
|
||||
)
|
||||
|
||||
// Always extract and validate inbox_id from custom header
|
||||
inboxIDHeader := string(r.RequestCtx.Request.Header.Peek(hdrWidgetInboxID))
|
||||
if inboxIDHeader == "" {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
|
||||
}
|
||||
|
||||
inboxID, err := strconv.Atoi(inboxIDHeader)
|
||||
if err != nil || inboxID <= 0 {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
|
||||
}
|
||||
|
||||
// Always fetch and validate inbox
|
||||
inbox, err := app.inbox.GetDBRecord(inboxID)
|
||||
if err != nil {
|
||||
app.lo.Error("error fetching inbox", "inbox_id", inboxID, "error", err)
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
if !inbox.Enabled {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.disabled", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
|
||||
}
|
||||
|
||||
// Check if inbox is the correct type for widget requests
|
||||
if inbox.Channel != livechat.ChannelLiveChat {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
|
||||
}
|
||||
|
||||
// Always store inbox data in context
|
||||
r.RequestCtx.SetUserValue(ctxWidgetInboxID, inboxID)
|
||||
r.RequestCtx.SetUserValue(ctxWidgetInbox, inbox)
|
||||
|
||||
// Extract JWT from Authorization header (Bearer token)
|
||||
authHeader := string(r.RequestCtx.Request.Header.Peek("Authorization"))
|
||||
|
||||
// For init endpoint, allow requests without JWT (visitor creation)
|
||||
if authHeader == "" && strings.Contains(string(r.RequestCtx.Path()), "/conversations/init") {
|
||||
return next(r)
|
||||
}
|
||||
|
||||
// For all other requests, require JWT
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
jwtToken := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// Verify JWT using inbox secret
|
||||
claims, err := verifyStandardJWT(jwtToken, inbox.Secret.String)
|
||||
if err != nil {
|
||||
app.lo.Error("invalid JWT", "jwt", jwtToken, "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
// Resolve user/contact ID from JWT claims
|
||||
contactID, err := resolveUserIDFromClaims(app, claims)
|
||||
if err != nil {
|
||||
app.lo.Error("error resolving user ID from JWT claims", "error", err)
|
||||
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
||||
}
|
||||
|
||||
// Store authenticated data in request context for downstream handlers
|
||||
r.RequestCtx.SetUserValue(ctxWidgetClaims, claims)
|
||||
r.RequestCtx.SetUserValue(ctxWidgetContactID, contactID)
|
||||
|
||||
return next(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to extract authenticated data from request context
|
||||
|
||||
// getWidgetInboxID extracts inbox ID from request context
|
||||
func getWidgetInboxID(r *fastglue.Request) (int, error) {
|
||||
val := r.RequestCtx.UserValue(ctxWidgetInboxID)
|
||||
if val == nil {
|
||||
return 0, fmt.Errorf("widget middleware not applied: missing inbox ID in context")
|
||||
}
|
||||
inboxID, ok := val.(int)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("invalid inbox ID type in context")
|
||||
}
|
||||
return inboxID, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// getWidgetClaimsOptional extracts JWT claims from request context, returns nil if not set
|
||||
func getWidgetClaimsOptional(r *fastglue.Request) *Claims {
|
||||
val := r.RequestCtx.UserValue(ctxWidgetClaims)
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
if claims, ok := val.(Claims); ok {
|
||||
return &claims
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -11,19 +11,25 @@ const http = axios.create({
|
||||
responseType: 'json'
|
||||
})
|
||||
|
||||
// Set content type if not specified and add libredesk_session to POST/PUT requests
|
||||
// Set content type and authentication headers
|
||||
http.interceptors.request.use((request) => {
|
||||
if ((request.method === 'post' || request.method === 'put') && !request.headers['Content-Type']) {
|
||||
request.headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
// Add libredesk_session to POST/PUT request data
|
||||
if (request.method === 'post' || request.method === 'put') {
|
||||
// Add authentication headers for widget API endpoints
|
||||
if (request.url && request.url.includes('/api/v1/widget/')) {
|
||||
const libredeskSession = localStorage.getItem('libredesk_session')
|
||||
request.data = {
|
||||
...request.data,
|
||||
inbox_id: getInboxIDFromQuery(),
|
||||
jwt: libredeskSession
|
||||
const inboxId = getInboxIDFromQuery()
|
||||
|
||||
// Add JWT to Authorization header
|
||||
if (libredeskSession) {
|
||||
request.headers['Authorization'] = `Bearer ${libredeskSession}`
|
||||
}
|
||||
|
||||
// Add inbox ID to custom header
|
||||
if (inboxId) {
|
||||
request.headers['X-Libredesk-Inbox-ID'] = inboxId.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,22 +47,33 @@ const sendChatMessage = (uuid, data) => http.post(`/api/v1/widget/chat/conversat
|
||||
const closeChatConversation = (uuid) => http.post(`/api/v1/widget/chat/conversations/${uuid}/close`)
|
||||
const uploadMedia = (conversationUUID, files) => {
|
||||
const formData = new FormData()
|
||||
|
||||
// Add JWT token
|
||||
const libredeskSession = localStorage.getItem('libredesk_session')
|
||||
formData.append('jwt', libredeskSession)
|
||||
|
||||
// Only add conversation UUID to form data now
|
||||
formData.append('conversation_uuid', conversationUUID)
|
||||
formData.append('inbox_id', getInboxIDFromQuery())
|
||||
|
||||
// Add files
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('files', files[i])
|
||||
}
|
||||
|
||||
// Get authentication data for headers
|
||||
const libredeskSession = localStorage.getItem('libredesk_session')
|
||||
const inboxId = getInboxIDFromQuery()
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
|
||||
// Add authentication headers
|
||||
if (libredeskSession) {
|
||||
headers['Authorization'] = `Bearer ${libredeskSession}`
|
||||
}
|
||||
if (inboxId) {
|
||||
headers['X-Libredesk-Inbox-ID'] = inboxId.toString()
|
||||
}
|
||||
|
||||
return axios.post('/api/v1/widget/media/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
headers,
|
||||
timeout: 30000
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user