From 06eaf748ed97275b4c8871429e15879ae552bd35 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Wed, 14 Oct 2026 18:18:18 +0800 Subject: [PATCH] fix: recover stale IM member credentials --- internal/imclient/client.go | 8 ++++++++ internal/support/service.go | 11 ++++++++++ internal/support/service_test.go | 35 +++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/internal/imclient/client.go b/internal/imclient/client.go index 6ee50a0..cddafdb 100644 --- a/internal/imclient/client.go +++ b/internal/imclient/client.go @@ -381,3 +381,11 @@ func IsConflict(err error) bool { var target *Error return errors.As(err, &target) && target.Status == http.StatusConflict } + +// IsAuthenticationFailed reports whether the IM server rejected user +// credentials. Callers may use this to repair credentials which have drifted +// from the local encrypted copy, while leaving transport failures untouched. +func IsAuthenticationFailed(err error) bool { + var target *Error + return errors.As(err, &target) && target.Status == http.StatusUnauthorized +} diff --git a/internal/support/service.go b/internal/support/service.go index 5f905e3..0d037a6 100644 --- a/internal/support/service.go +++ b/internal/support/service.go @@ -102,6 +102,17 @@ func (s *Service) OpenSession(ctx context.Context, tenantID, memberID, serviceID return nil, fmt.Errorf("decrypt IM credential: %w", err) } imSession, err := s.im.LoginBasic(ctx, identity.IMUsername, password, strings.TrimSpace(in.DeviceID), platform) + if imclient.IsAuthenticationFailed(err) { + identity, err = s.resetAndSaveCredential(ctx, identity) + if err != nil { + return nil, fmt.Errorf("repair IM credential: %w", err) + } + password, err = s.decryptCredential(identity.IMPasswordCipher) + if err != nil { + return nil, fmt.Errorf("decrypt repaired IM credential: %w", err) + } + imSession, err = s.im.LoginBasic(ctx, identity.IMUsername, password, strings.TrimSpace(in.DeviceID), platform) + } if err != nil { return nil, fmt.Errorf("login IM session: %w", err) } diff --git a/internal/support/service_test.go b/internal/support/service_test.go index 48b5163..ed803b7 100644 --- a/internal/support/service_test.go +++ b/internal/support/service_test.go @@ -31,6 +31,9 @@ func (r *repositoryStub) SaveMemberIMIdentity(context.Context, *store.MemberIMId type imStub struct { friendRequests int + loginAttempts int + resetPasswords int + loginError error } func (i *imStub) Ready() bool { return true } @@ -45,9 +48,18 @@ func (i *imStub) FindUserByUsername(context.Context, string) (*imclient.CreatedU return nil, nil } func (i *imStub) LoginBasic(context.Context, string, string, string, string) (*imclient.Session, error) { + i.loginAttempts++ + if i.loginError != nil { + err := i.loginError + i.loginError = nil + return nil, err + } return &imclient.Session{UserID: "usr_member", Token: "token", ExpiresAt: time.Now().Add(time.Hour)}, nil } -func (i *imStub) ResetPassword(context.Context, string, string) error { return nil } +func (i *imStub) ResetPassword(context.Context, string, string) error { + i.resetPasswords++ + return nil +} func (i *imStub) UserProfile(context.Context, string, string) (*imclient.UserProfile, error) { return &imclient.UserProfile{UserID: "usr_agent", Relationship: "none"}, nil } @@ -79,6 +91,27 @@ func TestOpenSessionAutoAcceptsAndReturnsTopic(t *testing.T) { } } +func TestOpenSessionRepairsDriftedIMCredential(t *testing.T) { + repository := &repositoryStub{ + agent: &store.CustomerService{ID: 3, Name: "客服01", IMTenantCode: "allrich", IMUserID: "usr_agent", OnlineStatus: 1}, + identity: &store.MemberIMIdentity{TenantID: 0, MemberID: 9, IMTenantCode: "allrich", IMUserID: "usr_member", IMUsername: "member01"}, + } + im := &imStub{loginError: &imclient.Error{Status: 401, Reason: "authentication failed"}} + service := New(repository, im, config.AuthConfig{FinanceMasterKey: strings.Repeat("k", 32)}) + repository.identity.IMPasswordCipher, _ = service.encryptCredential("stale-password") + + session, err := service.OpenSession(context.Background(), 0, 9, 3, SessionRequest{DeviceID: "web-1", Platform: "web"}) + if err != nil { + t.Fatal(err) + } + if session.Topic != "p2p-topic" { + t.Fatalf("unexpected session: %+v", session) + } + if im.loginAttempts != 2 || im.resetPasswords != 1 { + t.Fatalf("login attempts=%d reset passwords=%d, want 2 and 1", im.loginAttempts, im.resetPasswords) + } +} + func TestMemberIMUsernameMatchesIMConstraint(t *testing.T) { username := memberIMUsername(0, 9223372036854775807, "allrich") if len(username) != 10 {