mirror of
https://github.com/logto-io/logto.git
synced 2025-02-10 21:58:23 -05:00
refactor(core): remove deprecated library functions
This commit is contained in:
parent
6abdd05a40
commit
338b0ed63f
22 changed files with 260 additions and 253 deletions
|
@ -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<typeof createConnectorLibrary>;
|
||||
|
||||
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;
|
||||
|
|
|
@ -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);
|
|
@ -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<typeof createSignInExperienceLibrary>;
|
||||
|
||||
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);
|
||||
|
|
|
@ -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<User>, password: string): Promise<User> => {
|
||||
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<User>, password: string): Promise<User> => {
|
||||
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)<CreateUser, User>(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);
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -64,7 +64,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
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<T extends AnonymousRouter>(
|
|||
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<T extends AnonymousRouter>(
|
|||
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<T extends AnonymousRouter>(
|
|||
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<T extends AnonymousRouter>(
|
|||
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<T extends AnonymousRouter>(
|
|||
// 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<T extends AnonymousRouter>(
|
|||
|
||||
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);
|
||||
|
|
|
@ -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<ContextT> = WithInteractionDetailsContext<
|
|||
signInExperience: SignInExperience;
|
||||
};
|
||||
|
||||
export default function koaInteractionSie<StateT, ContextT, ResponseT>(): MiddlewareType<
|
||||
export default function koaInteractionSie<StateT, ContextT, ResponseT>({
|
||||
getSignInExperienceForApplication,
|
||||
}: SignInExperienceLibrary): MiddlewareType<
|
||||
StateT,
|
||||
WithInteractionSieContext<ContextT>,
|
||||
ResponseT
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
|
|
|
@ -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<AccountIdIdentifier> => {
|
||||
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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ type InteractionResult =
|
|||
|
||||
export default async function verifyIdentifier(
|
||||
ctx: Context,
|
||||
{ provider, libraries }: TenantContext,
|
||||
tenant: TenantContext,
|
||||
interactionRecord: InteractionResult
|
||||
): Promise<RegisterInteractionResult | AccountVerifiedInteractionResult> {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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' });
|
||||
});
|
|
@ -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',
|
||||
})
|
|
@ -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,
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<VerifiedInteractionResult> {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
|
|
|
@ -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<AccountVerifiedInteractionResult> {
|
||||
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);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue