diff --git a/packages/core/src/libraries/connector.ts b/packages/core/src/libraries/connector.ts index e775cfd74..4195442c7 100644 --- a/packages/core/src/libraries/connector.ts +++ b/packages/core/src/libraries/connector.ts @@ -9,8 +9,6 @@ import { loadConnectorFactories } from '#src/utils/connectors/factories.js'; import { validateConnectorModule, parseMetadata } from '#src/utils/connectors/index.js'; import type { LogtoConnector } from '#src/utils/connectors/types.js'; -import { defaultQueries } from './shared.js'; - export type ConnectorLibrary = ReturnType; export const createConnectorLibrary = (queries: Queries) => { @@ -94,10 +92,3 @@ export const createConnectorLibrary = (queries: Queries) => { return { getConnectorConfig, getLogtoConnectors, getLogtoConnectorById }; }; - -/** @deprecated Don't use. This is for transition only and will be removed soon. */ -export const defaultConnectorLibrary = createConnectorLibrary(defaultQueries); - -/** @deprecated Don't use. This is for transition only and will be removed soon. */ -export const { getConnectorConfig, getLogtoConnectors, getLogtoConnectorById } = - defaultConnectorLibrary; diff --git a/packages/core/src/libraries/shared.ts b/packages/core/src/libraries/shared.ts deleted file mode 100644 index c37f8ec7f..000000000 --- a/packages/core/src/libraries/shared.ts +++ /dev/null @@ -1,5 +0,0 @@ -import envSet from '#src/env-set/index.js'; -import Queries from '#src/tenants/Queries.js'; - -/** @deprecated Don't use. This is for transition only and will be removed soon. */ -export const defaultQueries = new Queries(envSet.pool); diff --git a/packages/core/src/libraries/sign-in-experience/index.ts b/packages/core/src/libraries/sign-in-experience/index.ts index 52804ef18..3d1009670 100644 --- a/packages/core/src/libraries/sign-in-experience/index.ts +++ b/packages/core/src/libraries/sign-in-experience/index.ts @@ -13,12 +13,9 @@ import i18next from 'i18next'; import RequestError from '#src/errors/RequestError/index.js'; import type { ConnectorLibrary } from '#src/libraries/connector.js'; -import { defaultConnectorLibrary } from '#src/libraries/connector.js'; import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; -import { defaultQueries } from '../shared.js'; - export * from './sign-up.js'; export * from './sign-in.js'; @@ -30,6 +27,8 @@ export const validateBranding = (branding: Branding) => { assertThat(branding.logoUrl.trim(), 'sign_in_experiences.empty_logo'); }; +export type SignInExperienceLibrary = ReturnType; + export const createSignInExperienceLibrary = ( queries: Queries, connectorLibrary: ConnectorLibrary @@ -111,10 +110,3 @@ export const createSignInExperienceLibrary = ( getSignInExperienceForApplication, }; }; - -/** @deprecated Don't use. This is for transition only and will be removed soon. */ -export const { - validateLanguageInfo, - removeUnavailableSocialConnectorTargets, - getSignInExperienceForApplication, -} = createSignInExperienceLibrary(defaultQueries, defaultConnectorLibrary); diff --git a/packages/core/src/libraries/user.ts b/packages/core/src/libraries/user.ts index 7c7624b75..a50129b1f 100644 --- a/packages/core/src/libraries/user.ts +++ b/packages/core/src/libraries/user.ts @@ -14,8 +14,6 @@ import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; import { encryptPassword } from '#src/utils/password.js'; -import { defaultQueries } from './shared.js'; - const userId = buildIdGenerator(12); const roleId = buildIdGenerator(21); @@ -35,6 +33,19 @@ export const encryptUserPassword = async ( return { passwordEncrypted, passwordEncryptionMethod }; }; +export const verifyUserPassword = async (user: Nullable, password: string): Promise => { + assertThat(user, 'session.invalid_credentials'); + const { passwordEncrypted, passwordEncryptionMethod } = user; + + assertThat(passwordEncrypted && passwordEncryptionMethod, 'session.invalid_credentials'); + + const result = await argon2Verify({ password, hash: passwordEncrypted }); + + assertThat(result, 'session.invalid_credentials'); + + return user; +}; + export const createUserLibrary = (queries: Queries) => { const { pool, @@ -57,19 +68,6 @@ export const createUserLibrary = (queries: Queries) => { { retries, factor: 0 } // No need for exponential backoff ); - const verifyUserPassword = async (user: Nullable, password: string): Promise => { - assertThat(user, 'session.invalid_credentials'); - const { passwordEncrypted, passwordEncryptionMethod } = user; - - assertThat(passwordEncrypted && passwordEncryptionMethod, 'session.invalid_credentials'); - - const result = await argon2Verify({ password, hash: passwordEncrypted }); - - assertThat(result, 'session.invalid_credentials'); - - return user; - }; - const insertUserQuery = buildInsertIntoWithPool(pool)(Users, { returning: true, }); @@ -144,12 +142,7 @@ export const createUserLibrary = (queries: Queries) => { return { generateUserId, - verifyUserPassword, insertUser, checkIdentifierCollision, }; }; - -/** @deprecated Don't use. This is for transition only and will be removed soon. */ -export const { generateUserId, verifyUserPassword, insertUser, checkIdentifierCollision } = - createUserLibrary(defaultQueries); diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts index abc795ee6..7dd7d7641 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts @@ -16,11 +16,9 @@ import type { const { jest } = import.meta; const { mockEsm } = createMockUtils(jest); -const { getLogtoConnectorById } = mockEsm('#src/libraries/connector.js', () => ({ - getLogtoConnectorById: jest - .fn() - .mockResolvedValue({ metadata: { target: 'logto' }, dbEntry: { syncProfile: true } }), -})); +const getLogtoConnectorById = jest + .fn() + .mockResolvedValue({ metadata: { target: 'logto' }, dbEntry: { syncProfile: true } }); const { assignInteractionResults } = mockEsm('#src/libraries/session.js', () => ({ assignInteractionResults: jest.fn(), @@ -51,7 +49,11 @@ const now = Date.now(); jest.useFakeTimers().setSystemTime(now); describe('submit action', () => { - const tenant = new MockTenant(undefined, { users: userQueries }, { users: userLibraries }); + const tenant = new MockTenant( + undefined, + { users: userQueries }, + { users: userLibraries, connectors: { getLogtoConnectorById } } + ); const ctx = { ...createContextWithRouteParameters(), ...createMockLogContext(), diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 5e41ea490..fd1d73733 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -2,7 +2,7 @@ import type { User, Profile } from '@logto/schemas'; import { InteractionEvent, UserRole, adminConsoleApplicationId } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; -import { getLogtoConnectorById } from '#src/libraries/connector.js'; +import type { ConnectorLibrary } from '#src/libraries/connector.js'; import { assignInteractionResults } from '#src/libraries/session.js'; import { encryptUserPassword } from '#src/libraries/user.js'; import type { LogEntry } from '#src/middleware/koa-audit-log.js'; @@ -21,15 +21,18 @@ import { clearInteractionStorage, categorizeIdentifiers } from '../utils/interac const filterSocialIdentifiers = (identifiers: Identifier[]): SocialIdentifier[] => identifiers.filter((identifier): identifier is SocialIdentifier => identifier.key === 'social'); -const getNewSocialProfile = async ({ - user, - connectorId, - identifiers, -}: { - user?: User; - connectorId: string; - identifiers: SocialIdentifier[]; -}) => { +const getNewSocialProfile = async ( + { getLogtoConnectorById }: ConnectorLibrary, + { + user, + connectorId, + identifiers, + }: { + user?: User; + connectorId: string; + identifiers: SocialIdentifier[]; + } +) => { // TODO: @simeng refactor me. This step should be verified by the previous profile verification cycle Already. // Should pickup the verified social user info result automatically const socialIdentifier = identifiers.find((identifier) => identifier.connectorId === connectorId); @@ -60,7 +63,10 @@ const getNewSocialProfile = async ({ }; }; -const getSyncedSocialUserProfile = async (socialIdentifier: SocialIdentifier) => { +const getSyncedSocialUserProfile = async ( + { getLogtoConnectorById }: ConnectorLibrary, + socialIdentifier: SocialIdentifier +) => { const { userInfo: { name, avatar }, connectorId, @@ -79,6 +85,7 @@ const getSyncedSocialUserProfile = async (socialIdentifier: SocialIdentifier) => }; const parseNewUserProfile = async ( + connectorLibrary: ConnectorLibrary, profile: Profile, profileIdentifiers: Identifier[], user?: User @@ -89,7 +96,7 @@ const parseNewUserProfile = async ( conditional(password && (await encryptUserPassword(password))), conditional( connectorId && - (await getNewSocialProfile({ + (await getNewSocialProfile(connectorLibrary, { connectorId, identifiers: filterSocialIdentifiers(profileIdentifiers), user, @@ -107,18 +114,20 @@ const parseNewUserProfile = async ( }; const parseUserProfile = async ( + connectorLibrary: ConnectorLibrary, { profile, identifiers }: VerifiedSignInInteractionResult | VerifiedRegisterInteractionResult, user?: User ) => { const { authIdentifiers, profileIdentifiers } = categorizeIdentifiers(identifiers ?? [], profile); - const newUserProfile = profile && (await parseNewUserProfile(profile, profileIdentifiers, user)); + const newUserProfile = + profile && (await parseNewUserProfile(connectorLibrary, profile, profileIdentifiers, user)); // Sync the last social profile const socialIdentifier = filterSocialIdentifiers(authIdentifiers).slice(-1)[0]; const syncedSocialUserProfile = - socialIdentifier && (await getSyncedSocialUserProfile(socialIdentifier)); + socialIdentifier && (await getSyncedSocialUserProfile(connectorLibrary, socialIdentifier)); return { ...syncedSocialUserProfile, @@ -137,12 +146,13 @@ export default async function submitInteraction( const { users: { generateUserId, insertUser }, + connectors, } = libraries; const { event, profile } = interaction; if (event === InteractionEvent.Register) { const id = await generateUserId(); - const upsertProfile = await parseUserProfile(interaction); + const upsertProfile = await parseUserProfile(connectors, interaction); const { client_id } = ctx.interactionDetails.params; @@ -168,7 +178,7 @@ export default async function submitInteraction( if (event === InteractionEvent.SignIn) { const user = await findUserById(accountId); - const upsertProfile = await parseUserProfile(interaction, user); + const upsertProfile = await parseUserProfile(connectors, interaction, user); await updateUserById(accountId, upsertProfile); diff --git a/packages/core/src/routes/interaction/index.test.ts b/packages/core/src/routes/interaction/index.test.ts index a41ef36db..3cb611359 100644 --- a/packages/core/src/routes/interaction/index.test.ts +++ b/packages/core/src/routes/interaction/index.test.ts @@ -1,5 +1,5 @@ import { ConnectorType } from '@logto/connector-kit'; -import { InteractionEvent, demoAppApplicationId } from '@logto/schemas'; +import { demoAppApplicationId, InteractionEvent } from '@logto/schemas'; import { createMockUtils } from '@logto/shared/esm'; import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; @@ -7,7 +7,7 @@ import RequestError from '#src/errors/RequestError/index.js'; import type koaAuditLog from '#src/middleware/koa-audit-log.js'; import { createMockLogContext } from '#src/test-utils/koa-audit-log.js'; import { createMockProvider } from '#src/test-utils/oidc-provider.js'; -import { createMockTenantWithInteraction, MockTenant } from '#src/test-utils/tenant.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; import type { LogtoConnector } from '#src/utils/connectors/types.js'; import { createRequester } from '#src/utils/test-utils.js'; @@ -35,10 +35,6 @@ const getLogtoConnectorByIdHelper = jest.fn(async (connectorId: string) => { }; }); -await mockEsmWithActual('#src/libraries/sign-in-experience/index.js', () => ({ - getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience), -})); - const { assignInteractionResults } = await mockEsmWithActual('#src/libraries/session.js', () => ({ assignInteractionResults: jest.fn(), })); @@ -95,37 +91,40 @@ await mockEsmWithActual( }) ); +const baseProviderMock = { + params: {}, + jti: 'jti', + client_id: demoAppApplicationId, +}; + +const tenantContext = new MockTenant( + createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)), + undefined, + { + connectors: { + getLogtoConnectorById: async (connectorId: string) => { + const connector = await getLogtoConnectorByIdHelper(connectorId); + + if (connector.type !== ConnectorType.Social) { + throw new RequestError({ + code: 'entity.not_found', + status: 404, + }); + } + + // @ts-expect-error + return connector as LogtoConnector; + }, + }, + signInExperiences: { + getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience), + }, + } +); + const { default: interactionRoutes } = await import('./index.js'); describe('interaction routes', () => { - const baseProviderMock = { - params: {}, - jti: 'jti', - client_id: demoAppApplicationId, - }; - - const tenantContext = new MockTenant( - createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)), - undefined, - { - connectors: { - getLogtoConnectorById: async (connectorId: string) => { - const connector = await getLogtoConnectorByIdHelper(connectorId); - - if (connector.type !== ConnectorType.Social) { - throw new RequestError({ - code: 'entity.not_found', - status: 404, - }); - } - - // @ts-expect-error - return connector as LogtoConnector; - }, - }, - } - ); - const sessionRequest = createRequester({ anonymousRoutes: interactionRoutes, tenantContext, @@ -266,7 +265,7 @@ describe('interaction routes', () => { const path = `${interactionPrefix}/profile`; const sessionRequest = createRequester({ anonymousRoutes: interactionRoutes, - tenantContext: createMockTenantWithInteraction(jest.fn().mockResolvedValue(baseProviderMock)), + tenantContext, }); it('PUT /interaction/profile', async () => { diff --git a/packages/core/src/routes/interaction/index.ts b/packages/core/src/routes/interaction/index.ts index 9864a24ff..db1ac4500 100644 --- a/packages/core/src/routes/interaction/index.ts +++ b/packages/core/src/routes/interaction/index.ts @@ -64,7 +64,7 @@ export default function interactionRoutes( profile: profileGuard.optional(), }), }), - koaInteractionSie(), + koaInteractionSie(libraries.signInExperiences), async (ctx, next) => { const { event, identifier, profile } = ctx.guard.body; const { signInExperience, createLog } = ctx; @@ -114,7 +114,7 @@ export default function interactionRoutes( router.put( `${interactionPrefix}/event`, koaGuard({ body: z.object({ event: eventGuard }) }), - koaInteractionSie(), + koaInteractionSie(libraries.signInExperiences), async (ctx, next) => { const { event } = ctx.guard.body; const { signInExperience, interactionDetails, createLog } = ctx; @@ -152,7 +152,7 @@ export default function interactionRoutes( koaGuard({ body: identifierPayloadGuard, }), - koaInteractionSie(), + koaInteractionSie(libraries.signInExperiences), async (ctx, next) => { const identifierPayload = ctx.guard.body; const { signInExperience, interactionDetails, createLog } = ctx; @@ -189,7 +189,7 @@ export default function interactionRoutes( koaGuard({ body: profileGuard, }), - koaInteractionSie(), + koaInteractionSie(libraries.signInExperiences), async (ctx, next) => { const profilePayload = ctx.guard.body; const { signInExperience, interactionDetails, createLog } = ctx; @@ -226,7 +226,7 @@ export default function interactionRoutes( koaGuard({ body: profileGuard, }), - koaInteractionSie(), + koaInteractionSie(libraries.signInExperiences), async (ctx, next) => { const profilePayload = ctx.guard.body; const { signInExperience, interactionDetails, createLog } = ctx; @@ -279,7 +279,7 @@ export default function interactionRoutes( // Submit Interaction router.post( `${interactionPrefix}/submit`, - koaInteractionSie(), + koaInteractionSie(libraries.signInExperiences), koaInteractionHooks(tenant), async (ctx, next) => { const { interactionDetails, createLog } = ctx; @@ -291,7 +291,7 @@ export default function interactionRoutes( const accountVerifiedInteraction = await verifyIdentifier(ctx, tenant, interactionStorage); - const verifiedInteraction = await verifyProfile(accountVerifiedInteraction); + const verifiedInteraction = await verifyProfile(tenant, accountVerifiedInteraction); if (event !== InteractionEvent.ForgotPassword) { await validateMandatoryUserProfile(ctx, verifiedInteraction); diff --git a/packages/core/src/routes/interaction/middleware/koa-interaction-sie.ts b/packages/core/src/routes/interaction/middleware/koa-interaction-sie.ts index 8d88e7a83..23c3ad9a3 100644 --- a/packages/core/src/routes/interaction/middleware/koa-interaction-sie.ts +++ b/packages/core/src/routes/interaction/middleware/koa-interaction-sie.ts @@ -2,7 +2,7 @@ import type { SignInExperience } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import type { MiddlewareType } from 'koa'; -import { getSignInExperienceForApplication } from '#src/libraries/sign-in-experience/index.js'; +import type { SignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js'; import type { WithInteractionDetailsContext } from './koa-interaction-details.js'; @@ -10,7 +10,9 @@ export type WithInteractionSieContext = WithInteractionDetailsContext< signInExperience: SignInExperience; }; -export default function koaInteractionSie(): MiddlewareType< +export default function koaInteractionSie({ + getSignInExperienceForApplication, +}: SignInExperienceLibrary): MiddlewareType< StateT, WithInteractionSieContext, ResponseT diff --git a/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts b/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts index 9b5781aaa..a2c8b4009 100644 --- a/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts +++ b/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts @@ -1,7 +1,8 @@ -import { pickDefault, createMockUtils } from '@logto/shared/esm'; +import { pickDefault } from '@logto/shared/esm'; + +import { MockTenant } from '#src/test-utils/tenant.js'; const { jest } = import.meta; -const { mockEsm } = createMockUtils(jest); const queries = { findUserByEmail: jest.fn(), @@ -10,32 +11,39 @@ const queries = { findUserByIdentity: jest.fn(), }; -mockEsm('#src/queries/user.js', () => queries); +const getLogtoConnectorById = jest.fn().mockResolvedValue({ metadata: { target: 'logto' } }); -const { getLogtoConnectorById } = mockEsm('#src/libraries/connector.js', () => ({ - getLogtoConnectorById: jest.fn().mockResolvedValue({ metadata: { target: 'logto' } }), -})); +const tenantContext = new MockTenant( + undefined, + { + users: queries, + }, + { connectors: { getLogtoConnectorById } } +); const findUserByIdentifier = await pickDefault(import('./find-user-by-identifier.js')); describe('findUserByIdentifier', () => { it('username', async () => { - await findUserByIdentifier({ username: 'foo' }); + await findUserByIdentifier(tenantContext, { username: 'foo' }); expect(queries.findUserByUsername).toBeCalledWith('foo'); }); it('email', async () => { - await findUserByIdentifier({ email: 'foo@logto.io' }); + await findUserByIdentifier(tenantContext, { email: 'foo@logto.io' }); expect(queries.findUserByEmail).toBeCalledWith('foo@logto.io'); }); it('phone', async () => { - await findUserByIdentifier({ phone: '123456' }); + await findUserByIdentifier(tenantContext, { phone: '123456' }); expect(queries.findUserByPhone).toBeCalledWith('123456'); }); it('social', async () => { - await findUserByIdentifier({ connectorId: 'connector', userInfo: { id: 'foo' } }); + await findUserByIdentifier(tenantContext, { + connectorId: 'connector', + userInfo: { id: 'foo' }, + }); expect(getLogtoConnectorById).toBeCalledWith('connector'); expect(queries.findUserByIdentity).toBeCalledWith('logto', 'foo'); }); diff --git a/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts b/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts index 28ca7b69c..d4ca483ac 100644 --- a/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts +++ b/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts @@ -1,14 +1,15 @@ -import { getLogtoConnectorById } from '#src/libraries/connector.js'; -import { - findUserByEmail, - findUserByUsername, - findUserByPhone, - findUserByIdentity, -} from '#src/queries/user.js'; +import type TenantContext from '#src/tenants/TenantContext.js'; import type { UserIdentity } from '../types/index.js'; -export default async function findUserByIdentifier(identity: UserIdentity) { +export default async function findUserByIdentifier( + { queries, libraries }: TenantContext, + identity: UserIdentity +) { + const { findUserByEmail, findUserByUsername, findUserByPhone, findUserByIdentity } = + queries.users; + const { getLogtoConnectorById } = libraries.connectors; + if ('username' in identity) { return findUserByUsername(identity.username); } diff --git a/packages/core/src/routes/interaction/verifications/identifier-payload-verification.test.ts b/packages/core/src/routes/interaction/verifications/identifier-payload-verification.test.ts index 4d55a0e89..6179ea0e8 100644 --- a/packages/core/src/routes/interaction/verifications/identifier-payload-verification.test.ts +++ b/packages/core/src/routes/interaction/verifications/identifier-payload-verification.test.ts @@ -28,13 +28,16 @@ const { verifySocialIdentity } = mockEsm('../utils/social-verification.js', () = verifySocialIdentity: jest.fn().mockResolvedValue({ id: 'foo' }), })); +const { verifyUserPassword } = await mockEsmWithActual('#src/libraries/user.js', () => ({ + verifyUserPassword: jest.fn(), +})); + const identifierPayloadVerification = await pickDefault( import('./identifier-payload-verification.js') ); const logContext = createMockLogContext(); -const verifyUserPassword = jest.fn(); -const tenant = new MockTenant(undefined, undefined, { users: { verifyUserPassword } }); +const tenant = new MockTenant(); describe('identifier verification', () => { const baseCtx = { ...createContextWithRouteParameters(), ...logContext }; @@ -55,7 +58,7 @@ describe('identifier verification', () => { await expect( identifierPayloadVerification(baseCtx, tenant, identifier, interactionStorage) ).rejects.toThrow(); - expect(findUserByIdentifier).toBeCalledWith({ username: 'username' }); + expect(findUserByIdentifier).toBeCalledWith(tenant, { username: 'username' }); expect(verifyUserPassword).toBeCalledWith(null, 'password'); }); @@ -72,7 +75,7 @@ describe('identifier verification', () => { identifierPayloadVerification(baseCtx, tenant, identifier, interactionStorage) ).rejects.toMatchError(new RequestError({ code: 'user.suspended', status: 401 })); - expect(findUserByIdentifier).toBeCalledWith({ username: 'username' }); + expect(findUserByIdentifier).toBeCalledWith(tenant, { username: 'username' }); expect(verifyUserPassword).toBeCalledWith({ id: 'foo' }, 'password'); }); @@ -91,7 +94,7 @@ describe('identifier verification', () => { identifier, interactionStorage ); - expect(findUserByIdentifier).toBeCalledWith({ email: 'email' }); + expect(findUserByIdentifier).toBeCalledWith(tenant, { email: 'email' }); expect(verifyUserPassword).toBeCalledWith({ id: 'foo' }, 'password'); expect(result).toEqual({ key: 'accountId', value: 'foo' }); }); @@ -111,7 +114,7 @@ describe('identifier verification', () => { identifier, interactionStorage ); - expect(findUserByIdentifier).toBeCalledWith({ phone: 'phone' }); + expect(findUserByIdentifier).toBeCalledWith(tenant, { phone: 'phone' }); expect(verifyUserPassword).toBeCalledWith({ id: 'foo' }, 'password'); expect(result).toEqual({ key: 'accountId', value: 'foo' }); }); diff --git a/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts b/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts index 7e96342af..a7a7a1ef0 100644 --- a/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts +++ b/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts @@ -6,6 +6,7 @@ import type { } from '@logto/schemas'; import RequestError from '#src/errors/RequestError/index.js'; +import { verifyUserPassword } from '#src/libraries/user.js'; import type { WithLogContext } from '#src/middleware/koa-audit-log.js'; import type TenantContext from '#src/tenants/TenantContext.js'; import assertThat from '#src/utils/assert-that.js'; @@ -33,15 +34,15 @@ const verifyPasswordIdentifier = async ( event: InteractionEvent, identifier: PasswordIdentifierPayload, ctx: WithLogContext, - { libraries }: TenantContext + tenant: TenantContext ): Promise => { const { password, ...identity } = identifier; const log = ctx.createLog(`Interaction.${event}.Identifier.Password.Submit`); log.append({ ...identity }); - const user = await findUserByIdentifier(identity); - const verifiedUser = await libraries.users.verifyUserPassword(user, password); + const user = await findUserByIdentifier(tenant, identity); + const verifiedUser = await verifyUserPassword(user, password); const { isSuspended, id } = verifiedUser; diff --git a/packages/core/src/routes/interaction/verifications/identifier-verification.test.ts b/packages/core/src/routes/interaction/verifications/identifier-verification.test.ts index 9a4447411..fac9925b3 100644 --- a/packages/core/src/routes/interaction/verifications/identifier-verification.test.ts +++ b/packages/core/src/routes/interaction/verifications/identifier-verification.test.ts @@ -56,7 +56,7 @@ describe('verifyIdentifier', () => { const result = await verifyIdentifier(ctx, tenant, interactionRecord); expect(result).toBe(verifiedRecord); - expect(verifyUserAccount).toBeCalledWith(interactionRecord, tenant.libraries.socials); + expect(verifyUserAccount).toBeCalledWith(tenant, interactionRecord); expect(storeInteractionResult).toBeCalledWith(verifiedRecord, ctx, tenant.provider); }); }); diff --git a/packages/core/src/routes/interaction/verifications/identifier-verification.ts b/packages/core/src/routes/interaction/verifications/identifier-verification.ts index 980d312eb..0d8bc3889 100644 --- a/packages/core/src/routes/interaction/verifications/identifier-verification.ts +++ b/packages/core/src/routes/interaction/verifications/identifier-verification.ts @@ -19,7 +19,7 @@ type InteractionResult = export default async function verifyIdentifier( ctx: Context, - { provider, libraries }: TenantContext, + tenant: TenantContext, interactionRecord: InteractionResult ): Promise { if (interactionRecord.event === InteractionEvent.Register) { @@ -27,11 +27,8 @@ export default async function verifyIdentifier( } // Verify the user account and assign the verified result to the interaction record - const accountVerifiedInteractionResult = await verifyUserAccount( - interactionRecord, - libraries.socials - ); - await storeInteractionResult(accountVerifiedInteractionResult, ctx, provider); + const accountVerifiedInteractionResult = await verifyUserAccount(tenant, interactionRecord); + await storeInteractionResult(accountVerifiedInteractionResult, ctx, tenant.provider); return accountVerifiedInteractionResult; } diff --git a/packages/core/src/routes/interaction/verifications/profile-verification-forgot-password.test.ts b/packages/core/src/routes/interaction/verifications/profile-verification.forgot-password.test.ts similarity index 75% rename from packages/core/src/routes/interaction/verifications/profile-verification-forgot-password.test.ts rename to packages/core/src/routes/interaction/verifications/profile-verification.forgot-password.test.ts index 784bc0d10..702debd27 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification-forgot-password.test.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.forgot-password.test.ts @@ -2,15 +2,16 @@ import { InteractionEvent } from '@logto/schemas'; import { createMockUtils, pickDefault } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; import type { Identifier } from '../types/index.js'; const { jest } = import.meta; -const { mockEsm, mockEsmWithActual } = createMockUtils(jest); +const { mockEsm } = createMockUtils(jest); -const { findUserById } = await mockEsmWithActual('#src/queries/user.js', () => ({ - findUserById: jest.fn().mockResolvedValue({ id: 'foo', passwordEncrypted: 'passwordHash' }), -})); +const findUserById = jest.fn().mockResolvedValue({ id: 'foo', passwordEncrypted: 'passwordHash' }); + +const tenantContext = new MockTenant(undefined, { users: { findUserById } }); const { argon2Verify } = mockEsm('hash-wasm', () => ({ argon2Verify: jest.fn(), @@ -26,7 +27,7 @@ describe('forgot password interaction profile verification', () => { }; it('missing profile', async () => { - await expect(verifyProfile(baseInteraction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, baseInteraction)).rejects.toMatchError( new RequestError({ code: 'user.new_password_required_in_profile', status: 422, @@ -43,7 +44,7 @@ describe('forgot password interaction profile verification', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'user.same_password', status: 422, @@ -61,7 +62,7 @@ describe('forgot password interaction profile verification', () => { }, }; - await expect(verifyProfile(interaction)).resolves.not.toThrow(); + await expect(verifyProfile(tenantContext, interaction)).resolves.not.toThrow(); expect(findUserById).toBeCalledWith(interaction.accountId); expect(argon2Verify).toBeCalledWith({ password: 'password', hash: 'passwordHash' }); }); diff --git a/packages/core/src/routes/interaction/verifications/profile-verification-profile-exist.test.ts b/packages/core/src/routes/interaction/verifications/profile-verification.profile-exist.test.ts similarity index 81% rename from packages/core/src/routes/interaction/verifications/profile-verification-profile-exist.test.ts rename to packages/core/src/routes/interaction/verifications/profile-verification.profile-exist.test.ts index 9b5e52eb6..fc82f55e2 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification-profile-exist.test.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.profile-exist.test.ts @@ -2,23 +2,26 @@ import { InteractionEvent } from '@logto/schemas'; import { createMockUtils, pickDefault } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; import type { Identifier, IdentifierVerifiedInteractionResult } from '../types/index.js'; const { jest } = import.meta; -const { mockEsm, mockEsmWithActual } = createMockUtils(jest); +const { mockEsm } = createMockUtils(jest); -const { findUserById } = await mockEsmWithActual('#src/queries/user.js', () => ({ +const userQueries = { findUserById: jest.fn().mockResolvedValue({ id: 'foo' }), hasUserWithEmail: jest.fn().mockResolvedValue(false), hasUserWithPhone: jest.fn().mockResolvedValue(false), hasUserWithIdentity: jest.fn().mockResolvedValue(false), -})); +}; +const { findUserById } = userQueries; mockEsm('../utils/index.js', () => ({ isUserPasswordSet: jest.fn().mockResolvedValueOnce(true), })); +const tenantContext = new MockTenant(undefined, { users: userQueries }); const verifyProfile = await pickDefault(import('./profile-verification.js')); describe('Should throw when providing existing identifiers in profile', () => { @@ -48,7 +51,7 @@ describe('Should throw when providing existing identifiers in profile', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'user.username_exists_in_profile', }) @@ -65,7 +68,7 @@ describe('Should throw when providing existing identifiers in profile', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'user.email_exists_in_profile', }) @@ -82,7 +85,7 @@ describe('Should throw when providing existing identifiers in profile', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'user.phone_exists_in_profile', }) @@ -99,7 +102,7 @@ describe('Should throw when providing existing identifiers in profile', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'user.password_exists_in_profile', }) diff --git a/packages/core/src/routes/interaction/verifications/profile-verification-profile-registered.test.ts b/packages/core/src/routes/interaction/verifications/profile-verification.profile-registered.test.ts similarity index 68% rename from packages/core/src/routes/interaction/verifications/profile-verification-profile-registered.test.ts rename to packages/core/src/routes/interaction/verifications/profile-verification.profile-registered.test.ts index 6ad7cbd35..a13d88611 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification-profile-registered.test.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.profile-registered.test.ts @@ -1,28 +1,31 @@ import { InteractionEvent } from '@logto/schemas'; -import { createMockUtils, pickDefault } from '@logto/shared/esm'; +import { pickDefault } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; import type { Identifier, IdentifierVerifiedInteractionResult } from '../types/index.js'; const { jest } = import.meta; -const { mockEsm, mockEsmWithActual } = createMockUtils(jest); -const { hasUser, hasUserWithEmail, hasUserWithPhone, hasUserWithIdentity } = - await mockEsmWithActual('#src/queries/user.js', () => ({ - hasUser: jest.fn().mockResolvedValue(false), - findUserById: jest.fn().mockResolvedValue({ id: 'foo' }), - hasUserWithEmail: jest.fn().mockResolvedValue(false), - hasUserWithPhone: jest.fn().mockResolvedValue(false), - hasUserWithIdentity: jest.fn().mockResolvedValue(false), - })); +const userQueries = { + hasUser: jest.fn().mockResolvedValue(false), + findUserById: jest.fn().mockResolvedValue({ id: 'foo' }), + hasUserWithEmail: jest.fn().mockResolvedValue(false), + hasUserWithPhone: jest.fn().mockResolvedValue(false), + hasUserWithIdentity: jest.fn().mockResolvedValue(false), +}; +const { hasUser, hasUserWithEmail, hasUserWithPhone, hasUserWithIdentity } = userQueries; -mockEsm('#src/libraries/connector.js', () => ({ - getLogtoConnectorById: jest.fn().mockResolvedValue({ - metadata: { target: 'logto' }, - }), -})); +const getLogtoConnectorById = jest.fn().mockResolvedValue({ + metadata: { target: 'logto' }, +}); +const tenantContext = new MockTenant( + undefined, + { users: userQueries }, + { connectors: { getLogtoConnectorById } } +); const verifyProfile = await pickDefault(import('./profile-verification.js')); const identifiers: Identifier[] = [ @@ -49,7 +52,7 @@ describe('profile registered validation', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'user.username_already_in_use', status: 422, @@ -66,7 +69,7 @@ describe('profile registered validation', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'user.email_already_in_use', status: 422, @@ -83,7 +86,7 @@ describe('profile registered validation', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'user.phone_already_in_use', status: 422, @@ -101,7 +104,7 @@ describe('profile registered validation', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'user.identity_already_in_use', status: 422, diff --git a/packages/core/src/routes/interaction/verifications/profile-verification-protected-identifier.test.ts b/packages/core/src/routes/interaction/verifications/profile-verification.protected-identifier.test.ts similarity index 74% rename from packages/core/src/routes/interaction/verifications/profile-verification-protected-identifier.test.ts rename to packages/core/src/routes/interaction/verifications/profile-verification.protected-identifier.test.ts index 742739617..420dc87b6 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification-protected-identifier.test.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.protected-identifier.test.ts @@ -1,26 +1,31 @@ import { InteractionEvent } from '@logto/schemas'; -import { createMockUtils, pickDefault } from '@logto/shared/esm'; +import { pickDefault } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; import type { Identifier } from '../types/index.js'; const { jest } = import.meta; -const { mockEsm, mockEsmWithActual } = createMockUtils(jest); - -await mockEsmWithActual('#src/queries/user.js', () => ({ - findUserById: jest.fn().mockResolvedValue({ id: 'foo' }), - hasUserWithEmail: jest.fn().mockResolvedValue(false), - hasUserWithPhone: jest.fn().mockResolvedValue(false), - hasUserWithIdentity: jest.fn().mockResolvedValue(false), -})); - -mockEsm('#src/libraries/connector.js', () => ({ - getLogtoConnectorById: jest.fn().mockResolvedValue({ - metadata: { target: 'logto' }, - }), -})); +const tenantContext = new MockTenant( + undefined, + { + users: { + findUserById: jest.fn().mockResolvedValue({ id: 'foo' }), + hasUserWithEmail: jest.fn().mockResolvedValue(false), + hasUserWithPhone: jest.fn().mockResolvedValue(false), + hasUserWithIdentity: jest.fn().mockResolvedValue(false), + }, + }, + { + connectors: { + getLogtoConnectorById: jest.fn().mockResolvedValue({ + metadata: { target: 'logto' }, + }), + }, + } +); const verifyProfile = await pickDefault(import('./profile-verification.js')); describe('profile protected identifier verification', () => { @@ -43,7 +48,7 @@ describe('profile protected identifier verification', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'session.verification_session_not_found', status: 404 }) ); }); @@ -58,7 +63,7 @@ describe('profile protected identifier verification', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'session.verification_session_not_found', status: 404 }) ); }); @@ -73,7 +78,7 @@ describe('profile protected identifier verification', () => { }, }; - await expect(verifyProfile(interaction)).resolves.not.toThrow(); + await expect(verifyProfile(tenantContext, interaction)).resolves.not.toThrow(); }); it('phone without a verified identifier should throw', async () => { @@ -84,7 +89,7 @@ describe('profile protected identifier verification', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'session.verification_session_not_found', status: 404 }) ); }); @@ -99,7 +104,7 @@ describe('profile protected identifier verification', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'session.verification_session_not_found', status: 404 }) ); }); @@ -114,7 +119,7 @@ describe('profile protected identifier verification', () => { }, }; - await expect(verifyProfile(interaction)).resolves.not.toThrow(); + await expect(verifyProfile(tenantContext, interaction)).resolves.not.toThrow(); }); it('connectorId without a verified identifier should throw', async () => { @@ -127,7 +132,7 @@ describe('profile protected identifier verification', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'session.connector_session_not_found', status: 404 }) ); }); @@ -144,7 +149,7 @@ describe('profile protected identifier verification', () => { }, }; - await expect(verifyProfile(interaction)).rejects.toMatchError( + await expect(verifyProfile(tenantContext, interaction)).rejects.toMatchError( new RequestError({ code: 'session.connector_session_not_found', status: 404 }) ); }); @@ -163,7 +168,7 @@ describe('profile protected identifier verification', () => { }, }; - await expect(verifyProfile(interaction)).resolves.not.toThrow(); + await expect(verifyProfile(tenantContext, interaction)).resolves.not.toThrow(); }); }); }); diff --git a/packages/core/src/routes/interaction/verifications/profile-verification.ts b/packages/core/src/routes/interaction/verifications/profile-verification.ts index 3cc05ca76..fef88d309 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.ts @@ -3,14 +3,7 @@ import { InteractionEvent } from '@logto/schemas'; import { argon2Verify } from 'hash-wasm'; import RequestError from '#src/errors/RequestError/index.js'; -import { getLogtoConnectorById } from '#src/libraries/connector.js'; -import { - findUserById, - hasUser, - hasUserWithEmail, - hasUserWithPhone, - hasUserWithIdentity, -} from '#src/queries/user.js'; +import type TenantContext from '#src/tenants/TenantContext.js'; import assertThat from '#src/utils/assert-that.js'; import { forgotPasswordProfileGuard } from '../types/guard.js'; @@ -65,9 +58,12 @@ const verifyProfileIdentifiers = ( }; const verifyProfileNotRegisteredByOtherUserAccount = async ( + { queries, libraries }: TenantContext, { username, email, phone, connectorId }: Profile, identifiers: Identifier[] = [] ) => { + const { hasUser, hasUserWithEmail, hasUserWithPhone, hasUserWithIdentity } = queries.users; + if (username) { assertThat( !(await hasUser(username)), @@ -101,7 +97,7 @@ const verifyProfileNotRegisteredByOtherUserAccount = async ( if (connectorId) { const { metadata: { target }, - } = await getLogtoConnectorById(connectorId); + } = await libraries.connectors.getLogtoConnectorById(connectorId); const socialIdentifier = identifiers.find( (identifier): identifier is SocialIdentifier => identifier.key === 'social' @@ -164,13 +160,15 @@ const verifyProfileNotExistInCurrentUserAccount = async ( }; export default async function verifyProfile( + tenant: TenantContext, interaction: IdentifierVerifiedInteractionResult ): Promise { + const { findUserById } = tenant.queries.users; const { event, identifiers, accountId, profile = {} } = interaction; if (event === InteractionEvent.Register) { verifyProfileIdentifiers(profile, identifiers); - await verifyProfileNotRegisteredByOtherUserAccount(profile, identifiers); + await verifyProfileNotRegisteredByOtherUserAccount(tenant, profile, identifiers); return interaction; } @@ -180,7 +178,7 @@ export default async function verifyProfile( // Find existing account const user = await findUserById(accountId); await verifyProfileNotExistInCurrentUserAccount(profile, user); - await verifyProfileNotRegisteredByOtherUserAccount(profile, identifiers); + await verifyProfileNotRegisteredByOtherUserAccount(tenant, profile, identifiers); return interaction; } diff --git a/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts b/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts index 516b467e5..79d0ee225 100644 --- a/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts +++ b/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts @@ -2,19 +2,20 @@ import { InteractionEvent } from '@logto/schemas'; import { createMockUtils, pickDefault } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; -import type { SocialLibrary } from '#src/libraries/social.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; import type { SignInInteractionResult } from '../types/index.js'; const { jest } = import.meta; -const { mockEsm, mockEsmDefault } = createMockUtils(jest); +const { mockEsmDefault } = createMockUtils(jest); const findUserByIdentifier = mockEsmDefault('../utils/find-user-by-identifier.js', () => jest.fn()); -// @ts-expect-error -const socialLibrary: SocialLibrary = { - findSocialRelatedUser: jest.fn().mockResolvedValue(null), -}; +const tenant = new MockTenant( + undefined, + {}, + { socials: { findSocialRelatedUser: jest.fn().mockResolvedValue(null) } } +); const verifyUserAccount = await pickDefault(import('./user-identity-verification.js')); @@ -30,7 +31,7 @@ describe('verifyUserAccount', () => { event: InteractionEvent.SignIn, }; - await expect(verifyUserAccount(interaction, socialLibrary)).rejects.toMatchError( + await expect(verifyUserAccount(tenant, interaction)).rejects.toMatchError( new RequestError({ code: 'session.identifier_not_found', status: 404 }) ); }); @@ -41,7 +42,7 @@ describe('verifyUserAccount', () => { identifiers: [{ key: 'accountId', value: 'foo' }], }; - const result = await verifyUserAccount(interaction, socialLibrary); + const result = await verifyUserAccount(tenant, interaction); expect(result).toEqual({ ...interaction, accountId: 'foo' }); }); @@ -54,8 +55,8 @@ describe('verifyUserAccount', () => { identifiers: [{ key: 'emailVerified', value: 'email' }], }; - const result = await verifyUserAccount(interaction, socialLibrary); - expect(findUserByIdentifierMock).toBeCalledWith({ email: 'email' }); + const result = await verifyUserAccount(tenant, interaction); + expect(findUserByIdentifierMock).toBeCalledWith(tenant, { email: 'email' }); expect(result).toEqual({ ...interaction, accountId: 'foo' }); }); @@ -68,8 +69,8 @@ describe('verifyUserAccount', () => { identifiers: [{ key: 'phoneVerified', value: '123456' }], }; - const result = await verifyUserAccount(interaction, socialLibrary); - expect(findUserByIdentifierMock).toBeCalledWith({ phone: '123456' }); + const result = await verifyUserAccount(tenant, interaction); + expect(findUserByIdentifierMock).toBeCalledWith(tenant, { phone: '123456' }); expect(result).toEqual({ ...interaction, accountId: 'foo' }); }); @@ -82,8 +83,8 @@ describe('verifyUserAccount', () => { identifiers: [{ key: 'social', connectorId: 'connectorId', userInfo: { id: 'foo' } }], }; - const result = await verifyUserAccount(interaction, socialLibrary); - expect(findUserByIdentifierMock).toBeCalledWith({ + const result = await verifyUserAccount(tenant, interaction); + expect(findUserByIdentifierMock).toBeCalledWith(tenant, { connectorId: 'connectorId', userInfo: { id: 'foo' }, }); @@ -99,7 +100,7 @@ describe('verifyUserAccount', () => { identifiers: [{ key: 'social', connectorId: 'connectorId', userInfo: { id: 'foo' } }], }; - await expect(verifyUserAccount(interaction, socialLibrary)).rejects.toMatchError( + await expect(verifyUserAccount(tenant, interaction)).rejects.toMatchError( new RequestError( { code: 'user.identity_not_exist', @@ -109,7 +110,7 @@ describe('verifyUserAccount', () => { ) ); - expect(findUserByIdentifierMock).toBeCalledWith({ + expect(findUserByIdentifierMock).toBeCalledWith(tenant, { connectorId: 'connectorId', userInfo: { id: 'foo' }, }); @@ -126,8 +127,8 @@ describe('verifyUserAccount', () => { ], }; - const result = await verifyUserAccount(interaction, socialLibrary); - expect(findUserByIdentifierMock).toBeCalledWith({ email: 'email' }); + const result = await verifyUserAccount(tenant, interaction); + expect(findUserByIdentifierMock).toBeCalledWith(tenant, { email: 'email' }); expect(result).toEqual({ ...interaction, accountId: 'foo' }); }); @@ -143,11 +144,11 @@ describe('verifyUserAccount', () => { ], }; - await expect(verifyUserAccount(interaction, socialLibrary)).rejects.toMatchError( + await expect(verifyUserAccount(tenant, interaction)).rejects.toMatchError( new RequestError({ code: 'user.user_not_exist', status: 404 }, { identifier: 'email' }) ); - expect(findUserByIdentifierMock).toBeCalledWith({ email: 'email' }); + expect(findUserByIdentifierMock).toBeCalledWith(tenant, { email: 'email' }); }); it('verify phoneVerified and emailVerified identifier with email user suspend', async () => { @@ -163,12 +164,12 @@ describe('verifyUserAccount', () => { ], }; - await expect(verifyUserAccount(interaction, socialLibrary)).rejects.toMatchError( + await expect(verifyUserAccount(tenant, interaction)).rejects.toMatchError( new RequestError({ code: 'user.suspended', status: 401 }) ); - expect(findUserByIdentifierMock).toHaveBeenNthCalledWith(1, { email: 'email' }); - expect(findUserByIdentifierMock).toHaveBeenNthCalledWith(2, { phone: '123456' }); + expect(findUserByIdentifierMock).toHaveBeenNthCalledWith(1, tenant, { email: 'email' }); + expect(findUserByIdentifierMock).toHaveBeenNthCalledWith(2, tenant, { phone: '123456' }); }); it('verify phoneVerified and emailVerified identifier returns inconsistent id', async () => { @@ -184,11 +185,11 @@ describe('verifyUserAccount', () => { ], }; - await expect(verifyUserAccount(interaction, socialLibrary)).rejects.toMatchError( + await expect(verifyUserAccount(tenant, interaction)).rejects.toMatchError( new RequestError('session.verification_failed') ); - expect(findUserByIdentifierMock).toHaveBeenNthCalledWith(1, { email: 'email' }); - expect(findUserByIdentifierMock).toHaveBeenNthCalledWith(2, { phone: '123456' }); + expect(findUserByIdentifierMock).toHaveBeenNthCalledWith(1, tenant, { email: 'email' }); + expect(findUserByIdentifierMock).toHaveBeenNthCalledWith(2, tenant, { phone: '123456' }); }); it('verify emailVerified identifier returns inconsistent id with existing accountId', async () => { @@ -200,10 +201,10 @@ describe('verifyUserAccount', () => { identifiers: [{ key: 'emailVerified', value: 'email' }], }; - await expect(verifyUserAccount(interaction, socialLibrary)).rejects.toMatchError( + await expect(verifyUserAccount(tenant, interaction)).rejects.toMatchError( new RequestError('session.verification_failed') ); - expect(findUserByIdentifierMock).toBeCalledWith({ email: 'email' }); + expect(findUserByIdentifierMock).toBeCalledWith(tenant, { email: 'email' }); }); it('profile use identifier should remain', async () => { @@ -222,8 +223,8 @@ describe('verifyUserAccount', () => { }, }; - const result = await verifyUserAccount(interaction, socialLibrary); - expect(findUserByIdentifierMock).toBeCalledWith({ email: 'email' }); + const result = await verifyUserAccount(tenant, interaction); + expect(findUserByIdentifierMock).toBeCalledWith(tenant, { email: 'email' }); expect(result).toEqual({ ...interaction, accountId: 'foo' }); }); diff --git a/packages/core/src/routes/interaction/verifications/user-identity-verification.ts b/packages/core/src/routes/interaction/verifications/user-identity-verification.ts index 5f1e6dcfc..26e3596c5 100644 --- a/packages/core/src/routes/interaction/verifications/user-identity-verification.ts +++ b/packages/core/src/routes/interaction/verifications/user-identity-verification.ts @@ -1,7 +1,7 @@ import { deduplicate } from '@silverhand/essentials'; import RequestError from '#src/errors/RequestError/index.js'; -import type { SocialLibrary } from '#src/libraries/social.js'; +import type TenantContext from '#src/tenants/TenantContext.js'; import assertThat from '#src/utils/assert-that.js'; import { maskUserInfo } from '#src/utils/format.js'; @@ -18,9 +18,11 @@ import findUserByIdentifier from '../utils/find-user-by-identifier.js'; import { categorizeIdentifiers } from '../utils/interaction.js'; const identifyUserByVerifiedEmailOrPhone = async ( + tenant: TenantContext, identifier: VerifiedEmailIdentifier | VerifiedPhoneIdentifier ) => { const user = await findUserByIdentifier( + tenant, identifier.key === 'emailVerified' ? { email: identifier.value } : { phone: identifier.value } ); @@ -37,15 +39,15 @@ const identifyUserByVerifiedEmailOrPhone = async ( }; const identifyUserBySocialIdentifier = async ( - identifier: SocialIdentifier, - socialLibrary: SocialLibrary + tenant: TenantContext, + identifier: SocialIdentifier ) => { const { connectorId, userInfo } = identifier; - const user = await findUserByIdentifier({ connectorId, userInfo }); + const user = await findUserByIdentifier(tenant, { connectorId, userInfo }); if (!user) { - const relatedInfo = await socialLibrary.findSocialRelatedUser(userInfo); + const relatedInfo = await tenant.libraries.socials.findSocialRelatedUser(userInfo); throw new RequestError( { @@ -63,21 +65,21 @@ const identifyUserBySocialIdentifier = async ( return id; }; -const identifyUser = async (identifier: Identifier, socialLibrary: SocialLibrary) => { +const identifyUser = async (tenant: TenantContext, identifier: Identifier) => { if (identifier.key === 'social') { - return identifyUserBySocialIdentifier(identifier, socialLibrary); + return identifyUserBySocialIdentifier(tenant, identifier); } if (identifier.key === 'accountId') { return identifier.value; } - return identifyUserByVerifiedEmailOrPhone(identifier); + return identifyUserByVerifiedEmailOrPhone(tenant, identifier); }; export default async function verifyUserAccount( - interaction: SignInInteractionResult | ForgotPasswordInteractionResult, - socialLibrary: SocialLibrary + tenant: TenantContext, + interaction: SignInInteractionResult | ForgotPasswordInteractionResult ): Promise { const { identifiers = [], accountId, profile } = interaction; @@ -95,7 +97,7 @@ export default async function verifyUserAccount( // Verify authIdentifiers const accountIds = await Promise.all( - authIdentifiers.map(async (identifier) => identifyUser(identifier, socialLibrary)) + authIdentifiers.map(async (identifier) => identifyUser(tenant, identifier)) ); const deduplicateAccountIds = deduplicate(accountIds);