diff --git a/plugins/setting-resources/src/components/socialIds/SocialIdPresenter.svelte b/plugins/setting-resources/src/components/socialIds/SocialIdPresenter.svelte index 07da90548e..fed4c36f1d 100644 --- a/plugins/setting-resources/src/components/socialIds/SocialIdPresenter.svelte +++ b/plugins/setting-resources/src/components/socialIds/SocialIdPresenter.svelte @@ -21,7 +21,15 @@ setCurrentAccount, SocialId } from '@hcengineering/core' - import { Button, Icon, Label, showPopup } from '@hcengineering/ui' + import { + Button, + getPlatformColorDef, + Icon, + Label, + PaletteColorIndexes, + showPopup, + themeStore + } from '@hcengineering/ui' import contact, { SocialIdentityProvider, SocialIdentityRef } from '@hcengineering/contact' import { getClient, MessageBox } from '@hcengineering/presentation' import { setPlatformStatus, unknownError } from '@hcengineering/platform' @@ -121,13 +129,15 @@
{#if isLogin} -
+ {@const color = getPlatformColorDef(PaletteColorIndexes.Turquoise, $themeStore.dark)} +
{/if} {#if isPrimary} -
+ {@const color = getPlatformColorDef(PaletteColorIndexes.Ocean, $themeStore.dark)} +
{/if} diff --git a/server/account/src/__tests__/utils.test.ts b/server/account/src/__tests__/utils.test.ts index b8c25d24db..66eb0cc64f 100644 --- a/server/account/src/__tests__/utils.test.ts +++ b/server/account/src/__tests__/utils.test.ts @@ -1956,14 +1956,20 @@ describe('account utils', () => { }) }) - describe('addSocialId', () => { + describe('addSocialIdBase', () => { const mockDb = { person: { - findOne: jest.fn() + findOne: jest.fn(), + update: jest.fn() }, socialId: { findOne: jest.fn(), - insertOne: jest.fn() + find: jest.fn(), + insertOne: jest.fn(), + update: jest.fn() + }, + account: { + findOne: jest.fn() } } as unknown as AccountDB @@ -2033,22 +2039,140 @@ describe('account utils', () => { ) }) - test('should throw error if social id already exists', async () => { - const value = 'test@example.com' - const type = SocialIdType.EMAIL + describe('existing socialId cases', () => { const person = 'test-person-uuid' as PersonUuid - const existingSocialId = { - type, - value, - personUuid: 'other-person-uuid' as PersonUuid - } + const otherPerson = 'other-person-uuid' as PersonUuid + const existingSocialIdId = 'existing-id' as PersonId - ;(mockDb.person.findOne as jest.Mock).mockResolvedValue({}) - ;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(existingSocialId) + beforeEach(() => { + ;(mockDb.person.findOne as jest.Mock).mockResolvedValue({}) + }) - await expect(addSocialIdBase(mockDb, person, type, value, false)).rejects.toThrow( - new PlatformError(new Status(Severity.ERROR, platform.status.SocialIdAlreadyExists, {})) - ) + test('should throw error if socialId exists with different person and is verified', async () => { + const existingSocialId = { + _id: existingSocialIdId, + type: SocialIdType.EMAIL, + value: 'test@example.com', + personUuid: otherPerson, + verifiedOn: Date.now() + } + ;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(existingSocialId) + + await expect(addSocialIdBase(mockDb, person, SocialIdType.EMAIL, 'test@example.com', true)).rejects.toThrow( + new PlatformError(new Status(Severity.ERROR, platform.status.SocialIdAlreadyExists, {})) + ) + }) + + test('should update personUuid if socialId exists with different person and is not verified', async () => { + const existingSocialId = { + _id: existingSocialIdId, + type: SocialIdType.EMAIL, + value: 'test@example.com', + personUuid: otherPerson, + verifiedOn: null + } + ;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(existingSocialId) + ;(mockDb.account.findOne as jest.Mock).mockResolvedValue({ uuid: otherPerson }) + + const result = await addSocialIdBase(mockDb, person, SocialIdType.EMAIL, 'test@example.com', true) + + expect(result).toBe(existingSocialIdId) + expect(mockDb.socialId.update).toHaveBeenCalledWith( + { _id: existingSocialIdId }, + { + personUuid: person, + verifiedOn: expect.any(Number) + } + ) + }) + + test('should merge persons if socialId exists with different person, no account, and confirmed=true', async () => { + const existingSocialId = { + _id: existingSocialIdId, + type: SocialIdType.EMAIL, + value: 'test@example.com', + personUuid: otherPerson, + verifiedOn: null + } + ;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(existingSocialId) + ;(mockDb.account.findOne as jest.Mock).mockResolvedValue(null) + ;(mockDb.socialId.find as jest.Mock).mockResolvedValue([existingSocialId]) + + const result = await addSocialIdBase(mockDb, person, SocialIdType.EMAIL, 'test@example.com', true) + + expect(result).toBe(existingSocialIdId) + expect(mockDb.socialId.update).toHaveBeenCalledWith( + { _id: existingSocialIdId }, + { verifiedOn: expect.any(Number) } + ) + expect(mockDb.socialId.update).toHaveBeenCalledWith( + { _id: existingSocialIdId, personUuid: otherPerson }, + { personUuid: person } + ) + expect(mockDb.person.update).toHaveBeenCalledWith({ uuid: otherPerson }, { migratedTo: person }) + }) + + test('should update verifiedOn if socialId exists for same person and confirmed=true', async () => { + const existingSocialId = { + _id: existingSocialIdId, + type: SocialIdType.EMAIL, + value: 'test@example.com', + personUuid: person, + verifiedOn: null + } + ;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(existingSocialId) + + const result = await addSocialIdBase(mockDb, person, SocialIdType.EMAIL, 'test@example.com', true) + + expect(result).toBe(existingSocialIdId) + expect(mockDb.socialId.update).toHaveBeenCalledWith( + { _id: existingSocialIdId }, + { verifiedOn: expect.any(Number) } + ) + }) + + test('should update displayValue if different from existing', async () => { + const existingSocialId = { + _id: existingSocialIdId, + type: SocialIdType.EMAIL, + value: 'test@example.com', + personUuid: person, + displayValue: 'old display' + } + ;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(existingSocialId) + + const result = await addSocialIdBase( + mockDb, + person, + SocialIdType.EMAIL, + 'test@example.com', + false, + 'new display' + ) + + expect(result).toBe(existingSocialIdId) + expect(mockDb.socialId.update).toHaveBeenCalledWith( + { _id: existingSocialIdId }, + { displayValue: 'new display' } + ) + }) + + test('should not update anything if no changes needed', async () => { + const existingSocialId = { + _id: existingSocialIdId, + type: SocialIdType.EMAIL, + value: 'test@example.com', + personUuid: person, + verifiedOn: Date.now(), + displayValue: 'display' + } + ;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(existingSocialId) + + const result = await addSocialIdBase(mockDb, person, SocialIdType.EMAIL, 'test@example.com', false, 'display') + + expect(result).toBe(existingSocialIdId) + expect(mockDb.socialId.update).not.toHaveBeenCalled() + }) }) test('should normalize value', async () => { diff --git a/server/account/src/utils.ts b/server/account/src/utils.ts index 6c377f9bde..5f0981b793 100644 --- a/server/account/src/utils.ts +++ b/server/account/src/utils.ts @@ -52,6 +52,7 @@ import { type Integration, type LoginInfo, type Meta, + type Operations, type OtpInfo, type RegionInfo, type SocialId, @@ -1513,7 +1514,42 @@ export async function addSocialIdBase ( const socialId = await db.socialId.findOne({ type, value: normalizedValue }) if (socialId != null) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.SocialIdAlreadyExists, {})) + const update: Operations = {} + let needUpdate = false + + if (socialId.personUuid !== personUuid) { + if (socialId.verifiedOn != null) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.SocialIdAlreadyExists, {})) + } else { + // Was not verified. + const accountFromSocialId = await db.account.findOne({ uuid: socialId.personUuid as AccountUuid }) + + if (accountFromSocialId == null && confirmed) { + // If attached to a person w/o account and adding verifiedOn - merge this person into the current account. + await doMergePersons(db, personUuid, socialId.personUuid) + } else { + // Re-wire this social id into the current account + update.personUuid = personUuid + needUpdate = true + } + } + } + + if (confirmed && socialId.verifiedOn == null) { + update.verifiedOn = Date.now() + needUpdate = true + } + + if (displayValue !== socialId.displayValue) { + update.displayValue = displayValue + needUpdate = true + } + + if (needUpdate) { + await db.socialId.update({ _id: socialId._id }, update) + } + + return socialId._id } const newSocialId: Omit = {