0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

Merge pull request #2870 from logto-io/gao-log-5122-core-library-factory-sie

refactor(core): migrate sie library to factory mode
This commit is contained in:
Gao Sun 2023-01-10 13:20:35 +08:00 committed by GitHub
commit 048981cf27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 262 additions and 180 deletions

View file

@ -0,0 +1,5 @@
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);

View file

@ -18,34 +18,42 @@ const { mockEsm } = createMockUtils(jest);
const allCustomLanguageTags: LanguageTag[] = [];
const { findAllCustomLanguageTags } = mockEsm('#src/queries/custom-phrase.js', () => ({
const customPhrases = {
findAllCustomLanguageTags: jest.fn(async () => allCustomLanguageTags),
}));
};
const { findAllCustomLanguageTags } = customPhrases;
const { getLogtoConnectors } = mockEsm('#src/connectors.js', () => ({
getLogtoConnectors: jest.fn(),
}));
const { findDefaultSignInExperience, updateDefaultSignInExperience } = mockEsm(
'#src/queries/sign-in-experience.js',
() => ({
findDefaultSignInExperience: jest.fn(),
updateDefaultSignInExperience: jest.fn(
async (data: Partial<CreateSignInExperience>): Promise<SignInExperience> => ({
...mockSignInExperience,
...data,
})
),
})
);
const { validateBranding, validateLanguageInfo, removeUnavailableSocialConnectorTargets } =
await import('./index.js');
const signInExperiences = {
findDefaultSignInExperience: jest.fn(),
updateDefaultSignInExperience: jest.fn(
async (data: Partial<CreateSignInExperience>): Promise<SignInExperience> => ({
...mockSignInExperience,
...data,
})
),
};
const { findDefaultSignInExperience, updateDefaultSignInExperience } = signInExperiences;
const { MockQueries } = await import('#src/test-utils/tenant.js');
const queries = new MockQueries({
customPhrases,
signInExperiences,
});
const { validateBranding, createSignInExperienceLibrary } = await import('./index.js');
const { validateLanguageInfo, removeUnavailableSocialConnectorTargets } =
createSignInExperienceLibrary(queries);
beforeEach(() => {
jest.clearAllMocks();
});
describe('validate branding', () => {
test('should throw when the UI style contains the slogan and slogan is empty', () => {
it('should throw when the UI style contains the slogan and slogan is empty', () => {
expect(() => {
validateBranding({
...mockBranding,
@ -55,7 +63,7 @@ describe('validate branding', () => {
}).toMatchError(new RequestError('sign_in_experiences.empty_slogan'));
});
test('should throw when the logo is empty', () => {
it('should throw when the logo is empty', () => {
expect(() => {
validateBranding({
...mockBranding,
@ -66,7 +74,7 @@ describe('validate branding', () => {
}).toMatchError(new RequestError('sign_in_experiences.empty_logo'));
});
test('should throw when the UI style contains the slogan and slogan is blank', () => {
it('should throw when the UI style contains the slogan and slogan is blank', () => {
expect(() => {
validateBranding({
...mockBranding,
@ -76,7 +84,7 @@ describe('validate branding', () => {
}).toMatchError(new RequestError('sign_in_experiences.empty_slogan'));
});
test('should not throw when the UI style does not contain the slogan and slogan is empty', () => {
it('should not throw when the UI style does not contain the slogan and slogan is empty', () => {
expect(() => {
validateBranding({
...mockBranding,
@ -138,7 +146,7 @@ describe('validate language info', () => {
});
describe('remove unavailable social connector targets', () => {
test('should remove unavailable social connector targets in sign-in experience', async () => {
it('should remove unavailable social connector targets in sign-in experience', async () => {
const mockSocialConnectorTargets = mockSocialConnectors.map(
({ metadata: { target } }) => target
);

View file

@ -13,14 +13,11 @@ import i18next from 'i18next';
import { getLogtoConnectors } from '#src/connectors/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import { findAllCustomLanguageTags } from '#src/queries/custom-phrase.js';
import {
findDefaultSignInExperience,
updateDefaultSignInExperience,
} from '#src/queries/sign-in-experience.js';
import { hasActiveUsers } from '#src/queries/user.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';
@ -32,66 +29,88 @@ export const validateBranding = (branding: Branding) => {
assertThat(branding.logoUrl.trim(), 'sign_in_experiences.empty_logo');
};
export const validateLanguageInfo = async (languageInfo: LanguageInfo) => {
const supportedLanguages = [...builtInLanguages, ...(await findAllCustomLanguageTags())];
export const createSignInExperienceLibrary = (queries: Queries) => {
const {
customPhrases: { findAllCustomLanguageTags },
signInExperiences: { findDefaultSignInExperience, updateDefaultSignInExperience },
users: { hasActiveUsers },
} = queries;
assertThat(
supportedLanguages.includes(languageInfo.fallbackLanguage),
new RequestError({
code: 'sign_in_experiences.unsupported_default_language',
language: languageInfo.fallbackLanguage,
})
);
const validateLanguageInfo = async (languageInfo: LanguageInfo) => {
const supportedLanguages = [...builtInLanguages, ...(await findAllCustomLanguageTags())];
assertThat(
supportedLanguages.includes(languageInfo.fallbackLanguage),
new RequestError({
code: 'sign_in_experiences.unsupported_default_language',
language: languageInfo.fallbackLanguage,
})
);
};
const removeUnavailableSocialConnectorTargets = async () => {
const connectors = await getLogtoConnectors();
const availableSocialConnectorTargets = deduplicate(
connectors
.filter(({ type }) => type === ConnectorType.Social)
.map(({ metadata: { target } }) => target)
);
const { socialSignInConnectorTargets } = await findDefaultSignInExperience();
await updateDefaultSignInExperience({
socialSignInConnectorTargets: socialSignInConnectorTargets.filter((target) =>
availableSocialConnectorTargets.includes(target)
),
});
};
const getSignInExperienceForApplication = async (
applicationId?: string
): Promise<SignInExperience & { notification?: string }> => {
const signInExperience = await findDefaultSignInExperience();
// Hard code AdminConsole sign-in methods settings.
if (applicationId === adminConsoleApplicationId) {
return {
...adminConsoleSignInExperience,
branding: {
...adminConsoleSignInExperience.branding,
slogan: i18next.t('admin_console.welcome.title'),
},
termsOfUseUrl: signInExperience.termsOfUseUrl,
languageInfo: signInExperience.languageInfo,
signInMode: (await hasActiveUsers()) ? SignInMode.SignIn : SignInMode.Register,
socialSignInConnectorTargets: [],
};
}
// Insert Demo App Notification
if (applicationId === demoAppApplicationId) {
const { socialSignInConnectorTargets } = signInExperience;
const notification = i18next.t('demo_app.notification');
return {
...signInExperience,
socialSignInConnectorTargets,
notification,
};
}
return signInExperience;
};
return {
validateLanguageInfo,
removeUnavailableSocialConnectorTargets,
getSignInExperienceForApplication,
};
};
export const removeUnavailableSocialConnectorTargets = async () => {
const connectors = await getLogtoConnectors();
const availableSocialConnectorTargets = deduplicate(
connectors
.filter(({ type }) => type === ConnectorType.Social)
.map(({ metadata: { target } }) => target)
);
const { socialSignInConnectorTargets } = await findDefaultSignInExperience();
await updateDefaultSignInExperience({
socialSignInConnectorTargets: socialSignInConnectorTargets.filter((target) =>
availableSocialConnectorTargets.includes(target)
),
});
};
export const getSignInExperienceForApplication = async (
applicationId?: string
): Promise<SignInExperience & { notification?: string }> => {
const signInExperience = await findDefaultSignInExperience();
// Hard code AdminConsole sign-in methods settings.
if (applicationId === adminConsoleApplicationId) {
return {
...adminConsoleSignInExperience,
branding: {
...adminConsoleSignInExperience.branding,
slogan: i18next.t('admin_console.welcome.title'),
},
termsOfUseUrl: signInExperience.termsOfUseUrl,
languageInfo: signInExperience.languageInfo,
signInMode: (await hasActiveUsers()) ? SignInMode.SignIn : SignInMode.Register,
socialSignInConnectorTargets: [],
};
}
// Insert Demo App Notification
if (applicationId === demoAppApplicationId) {
const { socialSignInConnectorTargets } = signInExperience;
const notification = i18next.t('demo_app.notification');
return {
...signInExperience,
socialSignInConnectorTargets,
notification,
};
}
return signInExperience;
};
/** @deprecated Don't use. This is for transition only and will be removed soon. */
export const {
validateLanguageInfo,
removeUnavailableSocialConnectorTargets,
getSignInExperienceForApplication,
} = createSignInExperienceLibrary(defaultQueries);

View file

@ -10,16 +10,15 @@ import pRetry from 'p-retry';
import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
import envSet from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import Queries from '#src/tenants/Queries.js';
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);
/** @deprecated Don't use. This is for transition only and will be removed soon. */
export const defaultQueries = new Queries(envSet.pool);
export const encryptUserPassword = async (
password: string
): Promise<{

View file

@ -30,7 +30,7 @@ const { findApplicationById } = await mockEsmWithActual('#src/queries/applicatio
),
}));
mockEsm('@logto/core-kit', () => ({
await mockEsmWithActual('@logto/core-kit', () => ({
// eslint-disable-next-line unicorn/consistent-function-scoping
buildIdGenerator: () => () => 'randomId',
generateStandardId: () => 'randomId',

View file

@ -23,7 +23,7 @@ import resourceRoutes from './resource.js';
import roleRoutes from './role.js';
import sessionRoutes from './session/index.js';
import settingRoutes from './setting.js';
import signInExperiencesRoutes from './sign-in-experience.js';
import signInExperiencesRoutes from './sign-in-experience/index.js';
import statusRoutes from './status.js';
import swaggerRoutes from './swagger.js';
import type { AnonymousRouter, AnonymousRouterLegacy, AuthedRouter } from './types.js';

View file

@ -73,14 +73,11 @@ export default function profileRoutes<T extends AnonymousRouter>(
body: object({ username: string().regex(usernameRegEx) }),
}),
async (ctx, next) => {
console.log('?0');
const userId = await checkSessionHealth(ctx, tenant, verificationTimeout);
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
const { username } = ctx.guard.body;
console.log('?1');
await checkIdentifierCollision({ username }, userId);
console.log('?2');
await updateUserById(userId, { username }, 'replace');
ctx.status = 204;

View file

@ -3,25 +3,30 @@ import { BrandingStyle } from '@logto/schemas';
import { pickDefault, createMockUtils } from '@logto/shared/esm';
import { mockBranding, mockSignInExperience } from '#src/__mocks__/index.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js';
const { mockEsm, mockEsmWithActual } = createMockUtils(import.meta.jest);
await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({
updateDefaultSignInExperience: async (
data: Partial<CreateSignInExperience>
): Promise<SignInExperience> => ({
...mockSignInExperience,
...data,
}),
}));
const { mockEsm } = createMockUtils(import.meta.jest);
mockEsm('#src/connectors.js', () => ({
getLogtoConnectors: async () => [],
}));
const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js'));
const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes });
const tenantContext = new MockTenant(undefined, {
signInExperiences: {
updateDefaultSignInExperience: async (
data: Partial<CreateSignInExperience>
): Promise<SignInExperience> => ({
...mockSignInExperience,
...data,
}),
},
});
const signInExperiencesRoutes = await pickDefault(import('./index.js'));
const signInExperienceRequester = createRequester({
authedRoutes: signInExperiencesRoutes,
tenantContext,
});
const expectPatchResponseStatus = async (
signInExperience: Record<string, unknown>,

View file

@ -2,25 +2,29 @@ import type { CreateSignInExperience, SignInExperience } from '@logto/schemas';
import { pickDefault, createMockUtils } from '@logto/shared/esm';
import { mockColor, mockSignInExperience } from '#src/__mocks__/index.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js';
const { mockEsm, mockEsmWithActual } = createMockUtils(import.meta.jest);
await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({
updateDefaultSignInExperience: async (
data: Partial<CreateSignInExperience>
): Promise<SignInExperience> => ({
...mockSignInExperience,
...data,
}),
}));
const { mockEsm } = createMockUtils(import.meta.jest);
mockEsm('#src/connectors.js', () => ({
getLogtoConnectors: async () => [],
}));
const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js'));
const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes });
const signInExperiencesRoutes = await pickDefault(import('./index.js'));
const signInExperienceRequester = createRequester({
authedRoutes: signInExperiencesRoutes,
tenantContext: new MockTenant(undefined, {
signInExperiences: {
updateDefaultSignInExperience: async (
data: Partial<CreateSignInExperience>
): Promise<SignInExperience> => ({
...mockSignInExperience,
...data,
}),
},
}),
});
const expectPatchResponseStatus = async (
signInExperience: Record<string, unknown>,

View file

@ -10,9 +10,10 @@ import {
mockLanguageInfo,
mockSignInExperience,
} from '#src/__mocks__/index.js';
import { MockTenant } from '#src/test-utils/tenant.js';
const { jest } = import.meta;
const { mockEsm, mockEsmWithActual } = createMockUtils(jest);
const { mockEsm } = createMockUtils(jest);
mockEsm('#src/connectors.js', () => ({
getLogtoConnectors: jest.fn(async () => [
@ -24,25 +25,33 @@ mockEsm('#src/connectors.js', () => ({
]),
}));
const { validateLanguageInfo } = await mockEsmWithActual(
'#src/libraries/sign-in-experience.js',
() => ({
validateLanguageInfo: jest.fn(),
})
const validateLanguageInfo = jest.fn();
const tenantContext = new MockTenant(
undefined,
{
signInExperiences: {
updateDefaultSignInExperience: async (
data: Partial<CreateSignInExperience>
): Promise<SignInExperience> => ({
...mockSignInExperience,
...data,
}),
},
},
{
signInExperiences: {
validateLanguageInfo,
},
}
);
await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({
updateDefaultSignInExperience: async (
data: Partial<CreateSignInExperience>
): Promise<SignInExperience> => ({
...mockSignInExperience,
...data,
}),
}));
const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js'));
const signInExperiencesRoutes = await pickDefault(import('./index.js'));
const { createRequester } = await import('#src/utils/test-utils.js');
const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes });
const signInExperienceRequester = createRequester({
authedRoutes: signInExperiencesRoutes,
tenantContext,
});
const expectPatchResponseStatus = async (
signInExperience: Record<string, unknown>,

View file

@ -15,18 +15,11 @@ import {
mockAliyunSmsConnector,
mockTermsOfUseUrl,
} from '#src/__mocks__/index.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta;
const { mockEsm, mockEsmWithActual } = createMockUtils(jest);
const { validateBranding, validateLanguageInfo, validateSignIn, validateSignUp } =
await mockEsmWithActual('#src/libraries/sign-in-experience/index.js', () => ({
validateBranding: jest.fn(),
validateLanguageInfo: jest.fn(),
validateSignIn: jest.fn(),
validateSignUp: jest.fn(),
}));
const { mockEsmWithActual } = createMockUtils(jest);
const logtoConnectors = [
mockFacebookConnector,
@ -40,7 +33,16 @@ await mockEsmWithActual('#src/connectors.js', () => ({
getLogtoConnectors: async () => logtoConnectors,
}));
const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience.js', () => ({
const { validateBranding, validateSignIn, validateSignUp } = await mockEsmWithActual(
'#src/libraries/sign-in-experience/index.js',
() => ({
validateBranding: jest.fn(),
validateSignIn: jest.fn(),
validateSignUp: jest.fn(),
})
);
const signInExperiences = {
findDefaultSignInExperience: jest.fn(async () => mockSignInExperience),
updateDefaultSignInExperience: async (
data: Partial<CreateSignInExperience>
@ -48,14 +50,22 @@ const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience
...mockSignInExperience,
...data,
}),
}));
};
const { findDefaultSignInExperience } = signInExperiences;
mockEsm('#src/queries/custom-phrase.js', () => ({
findAllCustomLanguageTags: async () => [],
}));
const validateLanguageInfo = jest.fn();
const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js'));
const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes });
const tenantContext = new MockTenant(
undefined,
{ signInExperiences, customPhrases: { findAllCustomLanguageTags: async () => [] } },
{ signInExperiences: { validateLanguageInfo } }
);
const signInExperiencesRoutes = await pickDefault(import('./index.js'));
const signInExperienceRequester = createRequester({
authedRoutes: signInExperiencesRoutes,
tenantContext,
});
describe('GET /sign-in-exp', () => {
afterAll(() => {

View file

@ -4,21 +4,19 @@ import { literal, object, string } from 'zod';
import { getLogtoConnectors } from '#src/connectors/index.js';
import {
validateBranding,
validateLanguageInfo,
validateSignUp,
validateSignIn,
} from '#src/libraries/sign-in-experience/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import {
findDefaultSignInExperience,
updateDefaultSignInExperience,
} from '#src/queries/sign-in-experience.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
import type { AuthedRouter, RouterInitArgs } from '../types.js';
export default function signInExperiencesRoutes<T extends AuthedRouter>(
...[router]: RouterInitArgs<T>
...[router, { queries, libraries }]: RouterInitArgs<T>
) {
const { findDefaultSignInExperience, updateDefaultSignInExperience } = queries.signInExperiences;
const { validateLanguageInfo } = libraries.signInExperiences;
/**
* As we only support single signInExperience settings for V1
* always return the default settings in DB for the /sign-in-exp get method

View file

@ -15,9 +15,6 @@ import {
mockWechatConnector,
mockWechatNativeConnector,
} from '#src/__mocks__/index.js';
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta;
const { mockEsm, mockEsmWithActual } = createMockUtils(jest);
@ -28,14 +25,11 @@ await mockEsmWithActual('i18next', () => ({
},
}));
const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience.js', () => ({
const sieQueries = {
updateDefaultSignInExperience: jest.fn(),
findDefaultSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience),
}));
await mockEsmWithActual('#src/queries/user.js', () => ({
hasActiveUsers: jest.fn().mockResolvedValue(true),
}));
};
const { findDefaultSignInExperience } = sieQueries;
mockEsm('#src/connectors.js', () => ({
getLogtoConnectors: jest.fn(async () => [
@ -50,6 +44,9 @@ mockEsm('#src/connectors.js', () => ({
}));
const wellKnownRoutes = await pickDefault(import('#src/routes/well-known.js'));
const { createMockProvider } = await import('#src/test-utils/oidc-provider.js');
const { MockTenant } = await import('#src/test-utils/tenant.js');
const { createRequester } = await import('#src/utils/test-utils.js');
describe('GET /.well-known/sign-in-exp', () => {
afterEach(() => {
@ -59,7 +56,10 @@ describe('GET /.well-known/sign-in-exp', () => {
const provider = createMockProvider();
const sessionRequest = createRequester({
anonymousRoutes: wellKnownRoutes,
tenantContext: new MockTenant(provider),
tenantContext: new MockTenant(provider, {
signInExperiences: sieQueries,
users: { hasActiveUsers: jest.fn().mockResolvedValue(true) },
}),
middlewares: [
async (ctx, next) => {
ctx.addLogContext = jest.fn();

View file

@ -5,13 +5,14 @@ import etag from 'etag';
import { getLogtoConnectors } from '#src/connectors/index.js';
import { getApplicationIdFromInteraction } from '#src/libraries/session.js';
import { getSignInExperienceForApplication } from '#src/libraries/sign-in-experience/index.js';
import type { AnonymousRouter, RouterInitArgs } from './types.js';
export default function wellKnownRoutes<T extends AnonymousRouter>(
...[router, { provider }]: RouterInitArgs<T>
...[router, { provider, libraries }]: RouterInitArgs<T>
) {
const { getSignInExperienceForApplication } = libraries.signInExperiences;
router.get(
'/.well-known/sign-in-exp',
async (ctx, next) => {

View file

@ -1,9 +1,11 @@
import { createSignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js';
import { createUserLibrary } from '#src/libraries/user.js';
import type Queries from './Queries.js';
export default class Libraries {
users = createUserLibrary(this.queries);
signInExperiences = createSignInExperienceLibrary(this.queries);
constructor(public readonly queries: Queries) {}
}

View file

@ -7,13 +7,39 @@ import type TenantContext from '#src/tenants/TenantContext.js';
import type { GrantMock } from './oidc-provider.js';
import { createMockProvider } from './oidc-provider.js';
const { jest } = import.meta;
export const createQueriesWithMockPool = () =>
new Queries(
createMockPool({
query: async (sql, values) => {
return createMockQueryResult([]);
},
})
);
const pool = createMockPool({
query: async (sql, values) => {
return createMockQueryResult([]);
},
});
export class MockQueries extends Queries {
constructor(queriesOverride?: Partial2<Queries>) {
super(
createMockPool({
query: async (sql, values) => {
return createMockQueryResult([]);
},
})
);
if (!queriesOverride) {
return;
}
const overrideKey = <Key extends keyof Queries>(key: Key) => {
this[key] = { ...this[key], ...queriesOverride[key] };
};
// eslint-disable-next-line no-restricted-syntax
for (const key of Object.keys(queriesOverride) as Array<keyof Queries>) {
overrideKey(key);
}
}
}
// eslint-disable-next-line @typescript-eslint/ban-types
export type DeepPartial<T> = T extends object
@ -33,8 +59,7 @@ export class MockTenant implements TenantContext {
queriesOverride?: Partial2<Queries>,
librariesOverride?: Partial2<Libraries>
) {
this.queries = new Queries(pool);
this.setPartial('queries', queriesOverride);
this.queries = new MockQueries(queriesOverride);
this.libraries = new Libraries(this.queries);
this.setPartial('libraries', librariesOverride);
}