mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-11 01:40:32 +00:00
uberf-9727: allow adding social id to existing person (#8439)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
parent
f26b1d3a12
commit
fe5c74f298
@ -126,6 +126,7 @@ export interface AccountClient {
|
||||
firstName: string,
|
||||
lastName: string
|
||||
) => Promise<{ uuid: PersonUuid, socialId: PersonId }>
|
||||
addSocialIdToPerson: (person: PersonUuid, type: SocialIdType, value: string, confirmed: boolean) => Promise<PersonId>
|
||||
|
||||
setCookie: () => Promise<void>
|
||||
deleteCookie: () => Promise<void>
|
||||
@ -644,6 +645,20 @@ class AccountClientImpl implements AccountClient {
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async addSocialIdToPerson (
|
||||
person: PersonUuid,
|
||||
type: SocialIdType,
|
||||
value: string,
|
||||
confirmed: boolean
|
||||
): Promise<PersonId> {
|
||||
const request = {
|
||||
method: 'addSocialIdToPerson' as const,
|
||||
params: { person, type, value, confirmed }
|
||||
}
|
||||
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async getMailboxOptions (): Promise<MailboxOptions> {
|
||||
const request = {
|
||||
method: 'getMailboxOptions' as const,
|
||||
|
@ -163,7 +163,8 @@ export default plugin(platformId, {
|
||||
WorkspaceLimitReached: '' as StatusCode<{ workspace: string }>,
|
||||
InvalidOtp: '' as StatusCode,
|
||||
InviteNotFound: '' as StatusCode<{ email: string }>,
|
||||
MailboxError: '' as StatusCode<{ reason: string }>
|
||||
MailboxError: '' as StatusCode<{ reason: string }>,
|
||||
SocialIdAlreadyExists: '' as StatusCode
|
||||
},
|
||||
metadata: {
|
||||
locale: '' as Metadata<string>,
|
||||
|
@ -13,14 +13,13 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// src/__tests__/operations.test.ts
|
||||
|
||||
import { AccountRole, MeasureContext, PersonUuid, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { AccountRole, MeasureContext, PersonId, PersonUuid, SocialIdType, WorkspaceUuid } from '@hcengineering/core'
|
||||
import platform, { PlatformError, Status, Severity, getMetadata } from '@hcengineering/platform'
|
||||
import { decodeTokenVerbose } from '@hcengineering/server-token'
|
||||
import * as utils from '../utils'
|
||||
|
||||
import { AccountDB } from '../types'
|
||||
import { createInvite, createInviteLink, sendInvite, resendInvite } from '../operations'
|
||||
import { createInvite, createInviteLink, sendInvite, resendInvite, addSocialIdToPerson } from '../operations'
|
||||
import { accountPlugin } from '../plugin'
|
||||
|
||||
// Mock platform
|
||||
@ -454,4 +453,106 @@ describe('invite operations', () => {
|
||||
expect(global.fetch).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('addSocialIdToPerson', () => {
|
||||
const mockCtx = {
|
||||
error: jest.fn()
|
||||
} as unknown as MeasureContext
|
||||
|
||||
const mockDb = {} as unknown as AccountDB
|
||||
const mockBranding = null
|
||||
const mockToken = 'test-token'
|
||||
|
||||
// Create spy only for this test suite
|
||||
const addSocialIdSpy = jest.spyOn(utils, 'addSocialId')
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
// Restore the original implementation
|
||||
addSocialIdSpy.mockRestore()
|
||||
})
|
||||
|
||||
test('should allow github service to add social id', async () => {
|
||||
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
|
||||
extra: { service: 'github' }
|
||||
})
|
||||
const newSocialId = 'new-social-id' as PersonId
|
||||
addSocialIdSpy.mockResolvedValue(newSocialId)
|
||||
|
||||
const params = {
|
||||
person: 'test-person' as PersonUuid,
|
||||
type: SocialIdType.GITHUB,
|
||||
value: 'test-value',
|
||||
confirmed: true
|
||||
}
|
||||
|
||||
const result = await addSocialIdToPerson(mockCtx, mockDb, mockBranding, mockToken, params)
|
||||
|
||||
expect(result).toBe(newSocialId)
|
||||
expect(addSocialIdSpy).toHaveBeenCalledWith(mockDb, params.person, params.type, params.value, params.confirmed)
|
||||
})
|
||||
|
||||
test('should allow admin to add social id', async () => {
|
||||
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
|
||||
extra: { admin: 'true' }
|
||||
})
|
||||
const newSocialId = 'new-social-id' as PersonId
|
||||
addSocialIdSpy.mockResolvedValue(newSocialId)
|
||||
|
||||
const params = {
|
||||
person: 'test-person' as PersonUuid,
|
||||
type: SocialIdType.GITHUB,
|
||||
value: 'test-value',
|
||||
confirmed: false
|
||||
}
|
||||
|
||||
const result = await addSocialIdToPerson(mockCtx, mockDb, mockBranding, mockToken, params)
|
||||
|
||||
expect(result).toBe(newSocialId)
|
||||
expect(addSocialIdSpy).toHaveBeenCalledWith(mockDb, params.person, params.type, params.value, params.confirmed)
|
||||
})
|
||||
|
||||
test('should throw error for unauthorized service', async () => {
|
||||
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
|
||||
extra: { service: 'other-service' }
|
||||
})
|
||||
|
||||
const params = {
|
||||
person: 'test-person' as PersonUuid,
|
||||
type: SocialIdType.GITHUB,
|
||||
value: 'test-value',
|
||||
confirmed: false
|
||||
}
|
||||
|
||||
await expect(addSocialIdToPerson(mockCtx, mockDb, mockBranding, mockToken, params)).rejects.toThrow(
|
||||
new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
)
|
||||
|
||||
expect(addSocialIdSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should throw error for regular user', async () => {
|
||||
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
|
||||
account: 'test-account',
|
||||
workspace: 'test-workspace',
|
||||
extra: {}
|
||||
})
|
||||
|
||||
const params = {
|
||||
person: 'test-person' as PersonUuid,
|
||||
type: SocialIdType.GITHUB,
|
||||
value: 'test-value',
|
||||
confirmed: false
|
||||
}
|
||||
|
||||
await expect(addSocialIdToPerson(mockCtx, mockDb, mockBranding, mockToken, params)).rejects.toThrow(
|
||||
new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
)
|
||||
|
||||
expect(addSocialIdSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -62,7 +62,8 @@ import {
|
||||
getSocialIdByKey,
|
||||
getWorkspaceInvite,
|
||||
loginOrSignUpWithProvider,
|
||||
sendEmail
|
||||
sendEmail,
|
||||
addSocialId
|
||||
} from '../utils'
|
||||
// eslint-disable-next-line import/no-named-default
|
||||
import platform, { getMetadata, PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
@ -1926,4 +1927,121 @@ describe('account utils', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addSocialId', () => {
|
||||
const mockDb = {
|
||||
person: {
|
||||
findOne: jest.fn()
|
||||
},
|
||||
socialId: {
|
||||
findOne: jest.fn(),
|
||||
insertOne: jest.fn()
|
||||
}
|
||||
} as unknown as AccountDB
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('should add new social id', async () => {
|
||||
const value = 'test@example.com'
|
||||
const type = SocialIdType.EMAIL
|
||||
const person = 'test-person-uuid' as PersonUuid
|
||||
const confirmed = false
|
||||
const normalizedValue = 'test@example.com'
|
||||
const newPersonId = 'new-person-id' as PersonId
|
||||
|
||||
;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(null)
|
||||
;(mockDb.person.findOne as jest.Mock).mockResolvedValue({})
|
||||
;(mockDb.socialId.insertOne as jest.Mock).mockResolvedValue(newPersonId)
|
||||
|
||||
const result = await addSocialId(mockDb, person, type, value, confirmed)
|
||||
|
||||
expect(mockDb.socialId.insertOne).toHaveBeenCalledWith({
|
||||
type,
|
||||
value: normalizedValue,
|
||||
personUuid: person,
|
||||
verifiedOn: undefined
|
||||
})
|
||||
expect(result).toBe(newPersonId)
|
||||
|
||||
expect(mockDb.socialId.findOne).toHaveBeenCalledWith({
|
||||
type,
|
||||
value: normalizedValue
|
||||
})
|
||||
})
|
||||
|
||||
test('should add confirmed social id with verification timestamp', async () => {
|
||||
const value = 'test@example.com'
|
||||
const type = SocialIdType.EMAIL
|
||||
const person = 'test-person-uuid' as PersonUuid
|
||||
const confirmed = true
|
||||
const newPersonId = 'new-person-id' as PersonId
|
||||
|
||||
;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(null)
|
||||
;(mockDb.person.findOne as jest.Mock).mockResolvedValue({})
|
||||
;(mockDb.socialId.insertOne as jest.Mock).mockResolvedValue(newPersonId)
|
||||
|
||||
const result = await addSocialId(mockDb, person, type, value, confirmed)
|
||||
|
||||
expect(mockDb.socialId.insertOne).toHaveBeenCalledWith({
|
||||
type,
|
||||
value,
|
||||
personUuid: person,
|
||||
verifiedOn: expect.any(Number)
|
||||
})
|
||||
expect(result).toBe(newPersonId)
|
||||
})
|
||||
|
||||
test('should throw error if person is not found', async () => {
|
||||
const value = 'test@example.com'
|
||||
const type = SocialIdType.EMAIL
|
||||
const person = 'test-person-uuid' as PersonUuid
|
||||
|
||||
;(mockDb.person.findOne as jest.Mock).mockResolvedValue(null)
|
||||
|
||||
await expect(addSocialId(mockDb, person, type, value, false)).rejects.toThrow(
|
||||
new PlatformError(new Status(Severity.ERROR, platform.status.PersonNotFound, { person }))
|
||||
)
|
||||
})
|
||||
|
||||
test('should throw error if social id already exists', async () => {
|
||||
const value = 'test@example.com'
|
||||
const type = SocialIdType.EMAIL
|
||||
const person = 'test-person-uuid' as PersonUuid
|
||||
const existingSocialId = {
|
||||
type,
|
||||
value,
|
||||
personUuid: 'other-person-uuid' as PersonUuid
|
||||
}
|
||||
|
||||
;(mockDb.person.findOne as jest.Mock).mockResolvedValue({})
|
||||
;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(existingSocialId)
|
||||
|
||||
await expect(addSocialId(mockDb, person, type, value, false)).rejects.toThrow(
|
||||
new PlatformError(new Status(Severity.ERROR, platform.status.SocialIdAlreadyExists, {}))
|
||||
)
|
||||
})
|
||||
|
||||
test('should normalize value', async () => {
|
||||
const value = ' TEST@EXAMPLE.COM '
|
||||
const normalizedValue = 'test@example.com'
|
||||
const type = SocialIdType.EMAIL
|
||||
const person = 'test-person-uuid' as PersonUuid
|
||||
const newPersonId = 'new-person-id' as PersonId
|
||||
|
||||
;(mockDb.person.findOne as jest.Mock).mockResolvedValue({})
|
||||
;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(null)
|
||||
;(mockDb.socialId.insertOne as jest.Mock).mockResolvedValue(newPersonId)
|
||||
|
||||
const result = await addSocialId(mockDb, person, type, value, false)
|
||||
|
||||
expect(mockDb.socialId.insertOne).toHaveBeenCalledWith(expect.objectContaining({ value: normalizedValue }))
|
||||
expect(result).toBe(newPersonId)
|
||||
expect(mockDb.socialId.findOne).toHaveBeenCalledWith({
|
||||
type,
|
||||
value: normalizedValue
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -101,7 +101,8 @@ import {
|
||||
getWorkspaceRole,
|
||||
normalizeValue,
|
||||
isEmail,
|
||||
generatePassword
|
||||
generatePassword,
|
||||
addSocialId
|
||||
} from './utils'
|
||||
|
||||
// Move to config?
|
||||
@ -2107,6 +2108,21 @@ async function deleteMailbox (
|
||||
ctx.info('Mailbox deleted', { mailbox, account })
|
||||
}
|
||||
|
||||
export async function addSocialIdToPerson (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
branding: Branding | null,
|
||||
token: string,
|
||||
params: { person: PersonUuid, type: SocialIdType, value: string, confirmed: boolean }
|
||||
): Promise<PersonId> {
|
||||
const { person, type, value, confirmed } = params
|
||||
const { extra } = decodeTokenVerbose(ctx, token)
|
||||
|
||||
verifyAllowedServices(['github'], extra)
|
||||
|
||||
return await addSocialId(db, person, type, value, confirmed)
|
||||
}
|
||||
|
||||
async function releaseSocialId (
|
||||
db: AccountDB,
|
||||
personUuid: PersonUuid,
|
||||
@ -2169,6 +2185,7 @@ export type AccountMethods =
|
||||
| 'createMailbox'
|
||||
| 'getMailboxes'
|
||||
| 'deleteMailbox'
|
||||
| 'addSocialIdToPerson'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -2228,7 +2245,8 @@ export function getMethods (hasSignUp: boolean = true): Partial<Record<AccountMe
|
||||
listWorkspaces: wrap(listWorkspaces),
|
||||
performWorkspaceOperation: wrap(performWorkspaceOperation),
|
||||
updateWorkspaceRoleBySocialKey: wrap(updateWorkspaceRoleBySocialKey),
|
||||
ensurePerson: wrap(ensurePerson)
|
||||
ensurePerson: wrap(ensurePerson),
|
||||
addSocialIdToPerson: wrap(addSocialIdToPerson)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1274,6 +1274,42 @@ export async function getInviteEmail (
|
||||
}
|
||||
}
|
||||
|
||||
export async function addSocialId (
|
||||
db: AccountDB,
|
||||
personUuid: PersonUuid,
|
||||
type: SocialIdType,
|
||||
value: string,
|
||||
confirmed: boolean
|
||||
): Promise<PersonId> {
|
||||
const normalizedValue = normalizeValue(value ?? '')
|
||||
|
||||
if (!Object.values(SocialIdType).includes(type) || normalizedValue.length === 0) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
|
||||
}
|
||||
|
||||
const person = await db.person.findOne({ uuid: personUuid })
|
||||
if (person == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.PersonNotFound, { person: personUuid }))
|
||||
}
|
||||
|
||||
const socialId = await db.socialId.findOne({ type, value: normalizedValue })
|
||||
if (socialId != null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.SocialIdAlreadyExists, {}))
|
||||
}
|
||||
|
||||
const newSocialId: Omit<SocialId, '_id' | 'key'> = {
|
||||
type,
|
||||
value: normalizedValue,
|
||||
personUuid
|
||||
}
|
||||
|
||||
if (confirmed) {
|
||||
newSocialId.verifiedOn = Date.now()
|
||||
}
|
||||
|
||||
return await db.socialId.insertOne(newSocialId)
|
||||
}
|
||||
|
||||
export async function getWorkspaceRole (
|
||||
db: AccountDB,
|
||||
account: PersonUuid,
|
||||
|
Loading…
Reference in New Issue
Block a user