From 92b5656f3f263895d787a6fdf8dd04880e1550f8 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Tue, 20 Oct 2026 20:54:56 +0800 Subject: [PATCH] fix: reprovision IM member identities across tenants --- internal/support/service.go | 14 ++++++++++++-- internal/support/service_test.go | 33 ++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/internal/support/service.go b/internal/support/service.go index fc232e8..56ae2ee 100644 --- a/internal/support/service.go +++ b/internal/support/service.go @@ -175,7 +175,13 @@ func (s *Service) OpenSession(ctx context.Context, tenantID, memberID, serviceID func (s *Service) ensureIdentity(ctx context.Context, tenantID, memberID int64, imTenantCode string) (*store.MemberIMIdentity, error) { identity, err := s.repository.MemberIMIdentity(ctx, tenantID, memberID) if err == nil { - if !strings.EqualFold(identity.IMTenantCode, imTenantCode) || identity.IMUserID == "" || identity.IMUsername == "" { + if !strings.EqualFold(identity.IMTenantCode, imTenantCode) { + // A tenant switch creates a distinct IM user. Keep the old tenant's IM + // account intact, but replace this member's local mapping with a newly + // provisioned account in the active tenant. + return s.createIdentity(ctx, tenantID, memberID, imTenantCode) + } + if identity.IMUserID == "" || identity.IMUsername == "" { return nil, ErrIMUnavailable } if identity.IMPasswordCipher != "" { @@ -186,6 +192,10 @@ func (s *Service) ensureIdentity(ctx context.Context, tenantID, memberID int64, if !errors.Is(err, store.ErrResourceNotFound) { return nil, err } + return s.createIdentity(ctx, tenantID, memberID, imTenantCode) +} + +func (s *Service) createIdentity(ctx context.Context, tenantID, memberID int64, imTenantCode string) (*store.MemberIMIdentity, error) { member, err := s.repository.MemberIMProfile(ctx, tenantID, memberID) if err != nil { return nil, err @@ -220,7 +230,7 @@ func (s *Service) ensureIdentity(ctx context.Context, tenantID, memberID int64, if err != nil { return nil, err } - identity = &store.MemberIMIdentity{TenantID: tenantID, MemberID: memberID, IMTenantCode: imTenantCode, + identity := &store.MemberIMIdentity{TenantID: tenantID, MemberID: memberID, IMTenantCode: imTenantCode, IMUserID: issued.UserID, IMUsername: created.Username, IMPasswordCipher: ciphertext} return s.repository.SaveMemberIMIdentity(ctx, identity) } diff --git a/internal/support/service_test.go b/internal/support/service_test.go index ccd0f6e..e8037ee 100644 --- a/internal/support/service_test.go +++ b/internal/support/service_test.go @@ -14,6 +14,7 @@ import ( type repositoryStub struct { agent *store.CustomerService identity *store.MemberIMIdentity + profile *store.MemberIMProfile } func (r *repositoryStub) CustomerServiceByID(context.Context, int64, int64) (*store.CustomerService, error) { @@ -23,9 +24,14 @@ func (r *repositoryStub) MemberIMIdentity(context.Context, int64, int64) (*store return r.identity, nil } func (r *repositoryStub) MemberIMProfile(context.Context, int64, int64) (*store.MemberIMProfile, error) { - return nil, store.ErrResourceNotFound + if r.profile == nil { + return nil, store.ErrResourceNotFound + } + return r.profile, nil } -func (r *repositoryStub) SaveMemberIMIdentity(context.Context, *store.MemberIMIdentity) (*store.MemberIMIdentity, error) { +func (r *repositoryStub) SaveMemberIMIdentity(_ context.Context, identity *store.MemberIMIdentity) (*store.MemberIMIdentity, error) { + copy := *identity + r.identity = © return r.identity, nil } @@ -41,8 +47,8 @@ func (i *imStub) Config() config.IMConfig { return config.IMConfig{TenantCode: "allrich", HTTPAPIURL: "https://api.118im.com/v1/im", WebSocketURL: "wss://api.118im.com/v0/channels", FileURL: "https://api.118im.com/v0/file", APIKey: "key"} } -func (i *imStub) CreateUser(context.Context, string, string, string) (*imclient.CreatedUser, error) { - return nil, nil +func (i *imStub) CreateUser(_ context.Context, username, nickname, _ string) (*imclient.CreatedUser, error) { + return &imclient.CreatedUser{UserID: imclient.FlexibleID("1001"), Username: username, Nickname: nickname}, nil } func (i *imStub) FindUserByUsername(context.Context, string) (*imclient.CreatedUser, error) { return nil, nil @@ -141,3 +147,22 @@ func TestMemberIMUsernameMatchesIMConstraint(t *testing.T) { } } } + +func TestEnsureIdentityReprovisionsMemberWhenIMTenantChanges(t *testing.T) { + repository := &repositoryStub{ + identity: &store.MemberIMIdentity{TenantID: 0, MemberID: 9, IMTenantCode: "allrich", IMUserID: "usr_old", IMUsername: "olduser"}, + profile: &store.MemberIMProfile{MemberID: 9, Username: "member01"}, + } + im := &imStub{} + service := New(repository, im, config.AuthConfig{FinanceMasterKey: strings.Repeat("k", 32)}) + identity, err := service.ensureIdentity(context.Background(), 0, 9, "default") + if err != nil { + t.Fatal(err) + } + if identity.IMTenantCode != "default" || identity.IMUserID != "usr_member" || identity.IMUsername != memberIMUsername(0, 9, "default") { + t.Fatalf("unexpected migrated identity: %+v", identity) + } + if im.loginAttempts != 1 { + t.Fatalf("provisioning login attempts = %d, want 1", im.loginAttempts) + } +}