H-337: restore Web widget reply visibility (#64)

* H-337: restore widget reply delivery

* H-337: harden widget conversation ownership

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-20 22:22:27 +08:00
committed by GitHub
co-authored by rogee
parent abb1bed424
commit b31b1b9562
22 changed files with 847 additions and 83 deletions
+22 -18
View File
@@ -60,8 +60,8 @@ func NewWSAuthenticator(jwtService *auth.JWTService, contactInboxRepo *repositor
// 1. Agent/User auth (primary): JWT token from 'token' query param or Authorization header.
// Validates via jwtService.ValidateAccessToken, populates WSClaims from auth.Claims.
//
// 2. Contact auth (secondary): pubsub_token + user_id query params.
// Looks up ContactInbox by pubsub_token, verifies the contact belongs to the account,
// 2. Contact auth (secondary): pubsub_token query param.
// Looks up ContactInbox by pubsub_token and resolves the contact and account,
// populates WSClaims with contact identity.
//
// Returns WSClaims on success, or an error suitable for HTTP 401 rejection.
@@ -98,18 +98,16 @@ func (a *WSAuthenticator) Authenticate(c *gin.Context) (*WSClaims, error) {
// --- Path 2: Contact authentication via pubsub_token ---
pubsubToken := c.Query("pubsub_token")
if pubsubToken == "" {
return nil, errors.New("authentication required: provide 'token' (JWT) or 'pubsub_token' + 'user_id' params")
return nil, errors.New("authentication required: provide 'token' (JWT) or 'pubsub_token'")
}
// Contact auth requires user_id param (Chatwoot RoomChannel: contact_id from params)
userIDStr := c.Query("user_id")
if userIDStr == "" {
return nil, errors.New("contact auth requires 'user_id' parameter alongside 'pubsub_token'")
}
contactID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid user_id parameter: %w", err)
var providedContactID *uint
if userIDStr := c.Query("user_id"); userIDStr != "" {
contactID, parseErr := strconv.ParseUint(userIDStr, 10, 32)
if parseErr != nil {
return nil, fmt.Errorf("invalid user_id parameter: %w", parseErr)
}
value := uint(contactID)
providedContactID = &value
}
// Lookup ContactInbox by pubsub_token
@@ -119,14 +117,17 @@ func (a *WSAuthenticator) Authenticate(c *gin.Context) (*WSClaims, error) {
return nil, fmt.Errorf("invalid pubsub_token: %w", err)
}
// Verify the contact ID matches
if contactInbox.ContactID != uint(contactID) {
logger.L().Debugf("ws auth: contact mismatch (expected=%d, found=%d)", uint(contactID), contactInbox.ContactID)
return nil, errors.New("pubsub_token does not belong to the specified contact")
// Legacy clients may still send user_id. Treat it as an additional
// fail-closed check, while Chatwoot widgets authenticate by token alone.
if providedContactID != nil {
if contactInbox.ContactID != *providedContactID {
logger.L().Debugf("ws auth: contact mismatch (expected=%d, found=%d)", *providedContactID, contactInbox.ContactID)
return nil, errors.New("pubsub_token does not belong to the specified contact")
}
}
wsClaims := &WSClaims{
UserID: uint(contactID), // for contacts, UserID maps to contact_id (Chatwoot convention)
UserID: contactInbox.ContactID, // for contacts, UserID maps to contact_id (Chatwoot convention)
AccountID: contactInbox.Contact.AccountID,
Role: "contact",
Provider: "pubsub_token",
@@ -208,6 +209,9 @@ func (a *WSAuthenticator) AuthenticateAndServeWS(c *gin.Context) {
// findContactInboxByPubsubToken looks up a ContactInbox by its PubsubToken field.
// Preloads the associated Contact to resolve the AccountID for authorization.
func (a *WSAuthenticator) findContactInboxByPubsubToken(ctx context.Context, pubsubToken string) (*model.ContactInbox, error) {
if a.contactInboxRepo == nil {
return nil, errors.New("contact inbox repository unavailable")
}
return a.contactInboxRepo.FindByPubsubToken(ctx, pubsubToken)
}
+1 -1
View File
@@ -36,7 +36,7 @@ func TestWSAuthenticator_Authenticate_PubsubTokenNoUserID_Cov4(t *testing.T) {
a := NewWSAuthenticator(nil, nil)
_, err := a.Authenticate(c)
assert.Error(t, err)
assert.Contains(t, err.Error(), "user_id")
assert.Contains(t, err.Error(), "invalid pubsub_token")
}
func TestWSAuthenticator_Authenticate_PubsubTokenInvalidUserID_Cov4(t *testing.T) {
+1 -1
View File
@@ -109,7 +109,7 @@ func TestAuthenticate_PubsubTokenWithoutUserID_Cov5(t *testing.T) {
claims, err := authenticator.Authenticate(c)
require.Error(t, err)
assert.Nil(t, claims)
assert.Contains(t, err.Error(), "requires 'user_id' parameter")
assert.Contains(t, err.Error(), "invalid pubsub_token")
}
func TestAuthenticate_PubsubToken_InvalidUserID_Cov5(t *testing.T) {
+1 -1
View File
@@ -142,7 +142,7 @@ func TestAuthenticate_PubsubNoUserID_Cov7(t *testing.T) {
claims, err := a.Authenticate(c)
require.Error(t, err)
assert.Nil(t, claims)
assert.Contains(t, err.Error(), "user_id")
assert.Contains(t, err.Error(), "invalid pubsub_token")
}
func TestAuthenticate_PubsubInvalidUserID_Cov7(t *testing.T) {