From 3b28b1d38db3d58bec3a86f44702ba9681e07f6a Mon Sep 17 00:00:00 2001 From: simeng-li Date: Tue, 15 Nov 2022 15:27:57 +0800 Subject: [PATCH 001/166] fix(console): fix forgotpassword not shown on preview (#2443) --- .../src/pages/SignInExperience/components/Preview.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/console/src/pages/SignInExperience/components/Preview.tsx b/packages/console/src/pages/SignInExperience/components/Preview.tsx index dc5c82303..34cea8208 100644 --- a/packages/console/src/pages/SignInExperience/components/Preview.tsx +++ b/packages/console/src/pages/SignInExperience/components/Preview.tsx @@ -106,15 +106,15 @@ const Preview = ({ signInExperience, className }: Props) => { signInExperience: { ...signInExperience, socialConnectors, + forgotPassword: { + email: hasEmailConnector, + sms: hasSmsConnector, + }, }, language, mode, platform: platform === 'desktopWeb' ? 'web' : 'mobile', isNative: platform === 'mobile', - forgotPassword: { - email: hasEmailConnector, - sms: hasSmsConnector, - }, }; }, [allConnectors, language, mode, platform, signInExperience]); From 793046868ebff3b729ab37298a3bd8d596bfb811 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Fri, 11 Nov 2022 10:50:20 +0800 Subject: [PATCH 002/166] feat(core): add change username session api --- packages/core/src/routes/admin-user.ts | 4 +- packages/core/src/routes/session/index.ts | 6 +- .../core/src/routes/session/profile.test.ts | 93 +++++++++++++++++++ packages/core/src/routes/session/profile.ts | 55 +++++++++++ packages/core/src/routes/session/utils.ts | 24 ++++- 5 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/routes/session/profile.test.ts create mode 100644 packages/core/src/routes/session/profile.ts diff --git a/packages/core/src/routes/admin-user.ts b/packages/core/src/routes/admin-user.ts index 3d72b96c9..cf9708a6d 100644 --- a/packages/core/src/routes/admin-user.ts +++ b/packages/core/src/routes/admin-user.ts @@ -23,7 +23,7 @@ import { } from '@/queries/user'; import assertThat from '@/utils/assert-that'; -import { checkExistingSignUpIdentifiers } from './session/utils'; +import { checkSignUpIdentifierCollision } from './session/utils'; import type { AuthedRouter } from './types'; export default function adminUserRoutes(router: T) { @@ -187,7 +187,7 @@ export default function adminUserRoutes(router: T) { } = ctx.guard; await findUserById(userId); - await checkExistingSignUpIdentifiers(body, userId); + await checkSignUpIdentifierCollision(body, userId); // Temp solution to validate the existence of input roleNames if (body.roleNames?.length) { diff --git a/packages/core/src/routes/session/index.ts b/packages/core/src/routes/session/index.ts index bdae57efa..4b7f8f4fb 100644 --- a/packages/core/src/routes/session/index.ts +++ b/packages/core/src/routes/session/index.ts @@ -18,6 +18,7 @@ import forgotPasswordRoutes from './forgot-password'; import koaGuardSessionAction from './middleware/koa-guard-session-action'; import passwordRoutes from './password'; import passwordlessRoutes from './passwordless'; +import profileRoutes from './profile'; import socialRoutes from './social'; import { getRoutePrefix } from './utils'; @@ -51,8 +52,7 @@ export default function sessionRoutes(router: T, prov const { accountId } = session; - // Temp solution before migrating to RBAC. Block non-admin user from consent to admin console - + // Temp solution before migrating to RBAC. Block non-admin user from consenting to admin console if (String(client_id) === adminConsoleApplicationId) { const { roleNames } = await findUserById(accountId); @@ -105,6 +105,6 @@ export default function sessionRoutes(router: T, prov passwordlessRoutes(router, provider); socialRoutes(router, provider); continueRoutes(router, provider); - forgotPasswordRoutes(router, provider); + profileRoutes(router, provider); } diff --git a/packages/core/src/routes/session/profile.test.ts b/packages/core/src/routes/session/profile.test.ts new file mode 100644 index 000000000..01296baeb --- /dev/null +++ b/packages/core/src/routes/session/profile.test.ts @@ -0,0 +1,93 @@ +import type { CreateUser, User } from '@logto/schemas'; +import { SignUpIdentifier } from '@logto/schemas'; +import { getUnixTime } from 'date-fns'; +import { Provider } from 'oidc-provider'; + +import { mockUser, mockUserResponse } from '@/__mocks__'; +import { createRequester } from '@/utils/test-utils'; + +import profileRoutes, { profileRoute } from './profile'; + +const mockFindUserById = jest.fn(async (): Promise => mockUser); +const mockHasUser = jest.fn(async () => false); +const mockHasUserWithEmail = jest.fn(async () => false); +const mockHasUserWithPhone = jest.fn(async () => false); +const mockUpdateUserById = jest.fn( + async (_, data: Partial): Promise => ({ + ...mockUser, + ...data, + }) +); + +jest.mock('oidc-provider', () => ({ + Provider: jest.fn(() => ({ + Session: { + get: jest.fn(async () => ({ accountId: 'id', loginTs: getUnixTime(new Date()) - 60 })), + }, + })), +})); + +jest.mock('@/queries/user', () => ({ + ...jest.requireActual('@/queries/user'), + findUserById: async () => mockFindUserById(), + hasUser: async () => mockHasUser(), + hasUserWithEmail: async () => mockHasUserWithEmail(), + hasUserWithPhone: async () => mockHasUserWithPhone(), + updateUserById: async (id: string, data: Partial) => mockUpdateUserById(id, data), +})); + +const mockFindDefaultSignInExperience = jest.fn(async () => ({ + signUp: { + identifier: SignUpIdentifier.None, + password: false, + verify: false, + }, +})); + +jest.mock('@/queries/sign-in-experience', () => ({ + findDefaultSignInExperience: jest.fn(async () => mockFindDefaultSignInExperience()), +})); + +describe('session -> profileRoutes', () => { + const sessionRequest = createRequester({ + anonymousRoutes: profileRoutes, + provider: new Provider(''), + middlewares: [ + async (ctx, next) => { + ctx.addLogContext = jest.fn(); + ctx.log = jest.fn(); + + return next(); + }, + ], + }); + + test('GET /session/profile should return current user data', async () => { + const response = await sessionRequest.get(profileRoute); + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual(mockUserResponse); + }); + + describe('PATCH /session/profile/username', () => { + it('should update username with the new value', async () => { + const newUsername = 'charles'; + + const response = await sessionRequest + .patch(`${profileRoute}/username`) + .send({ username: newUsername }); + + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual({ ...mockUserResponse, username: newUsername }); + }); + + it('should throw when username is already in use', async () => { + mockHasUser.mockImplementationOnce(async () => true); + + const response = await sessionRequest + .patch(`${profileRoute}/username`) + .send({ username: 'test' }); + + expect(response.statusCode).toEqual(422); + }); + }); +}); diff --git a/packages/core/src/routes/session/profile.ts b/packages/core/src/routes/session/profile.ts new file mode 100644 index 000000000..3467a2c77 --- /dev/null +++ b/packages/core/src/routes/session/profile.ts @@ -0,0 +1,55 @@ +import { usernameRegEx } from '@logto/core-kit'; +import { userInfoSelectFields } from '@logto/schemas'; +import pick from 'lodash.pick'; +import type { Provider } from 'oidc-provider'; +import { object, string } from 'zod'; + +import RequestError from '@/errors/RequestError'; +import { checkSessionHealth } from '@/lib/session'; +import koaGuard from '@/middleware/koa-guard'; +import { findUserById, updateUserById } from '@/queries/user'; +import assertThat from '@/utils/assert-that'; + +import type { AnonymousRouter } from '../types'; +import { verificationTimeout } from './consts'; +import { checkSignUpIdentifierCollision } from './utils'; + +export const profileRoute = '/session/profile'; + +export default function profileRoutes(router: T, provider: Provider) { + router.get(profileRoute, async (ctx, next) => { + const { accountId } = await provider.Session.get(ctx); + + if (!accountId) { + throw new RequestError('auth.unauthorized'); + } + + const user = await findUserById(accountId); + + ctx.body = pick(user, ...userInfoSelectFields); + + return next(); + }); + + router.patch( + `${profileRoute}/username`, + koaGuard({ + body: object({ username: string().regex(usernameRegEx) }), + }), + async (ctx, next) => { + const userId = await checkSessionHealth(ctx, provider, verificationTimeout); + + assertThat(userId, new RequestError('auth.unauthorized')); + + const { username } = ctx.guard.body; + + await checkSignUpIdentifierCollision({ username }, userId); + + const user = await updateUserById(userId, { username }, 'replace'); + + ctx.body = pick(user, ...userInfoSelectFields); + + return next(); + } + ); +} diff --git a/packages/core/src/routes/session/utils.ts b/packages/core/src/routes/session/utils.ts index 629f60d40..8749171d8 100644 --- a/packages/core/src/routes/session/utils.ts +++ b/packages/core/src/routes/session/utils.ts @@ -196,9 +196,31 @@ export const checkRequiredProfile = async ( throw new RequestError({ code: 'user.require_email_or_sms', status: 422 }); } }; + +export const checkMissingRequiredSignUpIdentifiers = async (identifiers: { + primaryEmail?: Nullable; + primaryPhone?: Nullable; +}) => { + // We do not check username as we decided to prohibit the removal of username from user profile. + const { primaryEmail, primaryPhone } = identifiers; + + const { signUp } = await getSignInExperienceForApplication(); + + if (signUp.identifier === SignUpIdentifier.Email && !primaryEmail) { + throw new RequestError({ code: 'user.require_email', status: 422 }); + } + + if (signUp.identifier === SignUpIdentifier.Sms && !primaryPhone) { + throw new RequestError({ code: 'user.require_sms', status: 422 }); + } + + if (signUp.identifier === SignUpIdentifier.EmailOrSms && !primaryEmail && !primaryPhone) { + throw new RequestError({ code: 'user.require_email_or_sms', status: 422 }); + } +}; /* eslint-enable complexity */ -export const checkExistingSignUpIdentifiers = async ( +export const checkSignUpIdentifierCollision = async ( identifiers: { username?: Nullable; primaryEmail?: Nullable; From 1a7bb4c4e729738fd9d9d0b7d6f460c797d85c43 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Fri, 11 Nov 2022 14:29:37 +0800 Subject: [PATCH 003/166] feat(core): add change password session api --- .../routes/session/forgot-password.test.ts | 2 +- .../src/routes/session/forgot-password.ts | 5 +- .../core/src/routes/session/profile.test.ts | 53 ++++++++++++++++++- packages/core/src/routes/session/profile.ts | 32 ++++++++++- 4 files changed, 86 insertions(+), 6 deletions(-) diff --git a/packages/core/src/routes/session/forgot-password.test.ts b/packages/core/src/routes/session/forgot-password.test.ts index e517b6068..6dc66d73d 100644 --- a/packages/core/src/routes/session/forgot-password.test.ts +++ b/packages/core/src/routes/session/forgot-password.test.ts @@ -197,7 +197,7 @@ describe('session -> forgotPasswordRoutes', () => { const response = await sessionRequest .post(`${forgotPasswordRoute}/reset`) .send({ password: mockPasswordEncrypted }); - expect(response).toHaveProperty('status', 400); + expect(response).toHaveProperty('status', 422); expect(updateUserById).toBeCalledTimes(0); }); it('should redirect when there was no old password', async () => { diff --git a/packages/core/src/routes/session/forgot-password.ts b/packages/core/src/routes/session/forgot-password.ts index 2e979d101..c724f02f3 100644 --- a/packages/core/src/routes/session/forgot-password.ts +++ b/packages/core/src/routes/session/forgot-password.ts @@ -46,9 +46,8 @@ export default function forgotPasswordRoutes( const { passwordEncrypted: oldPasswordEncrypted } = await findUserById(userId); assertThat( - !oldPasswordEncrypted || - (oldPasswordEncrypted && !(await argon2Verify({ password, hash: oldPasswordEncrypted }))), - new RequestError({ code: 'user.same_password', status: 400 }) + !oldPasswordEncrypted || !(await argon2Verify({ password, hash: oldPasswordEncrypted })), + new RequestError({ code: 'user.same_password', status: 422 }) ); const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password); diff --git a/packages/core/src/routes/session/profile.test.ts b/packages/core/src/routes/session/profile.test.ts index 01296baeb..0e4ab678d 100644 --- a/packages/core/src/routes/session/profile.test.ts +++ b/packages/core/src/routes/session/profile.test.ts @@ -3,7 +3,7 @@ import { SignUpIdentifier } from '@logto/schemas'; import { getUnixTime } from 'date-fns'; import { Provider } from 'oidc-provider'; -import { mockUser, mockUserResponse } from '@/__mocks__'; +import { mockPasswordEncrypted, mockUser, mockUserResponse } from '@/__mocks__'; import { createRequester } from '@/utils/test-utils'; import profileRoutes, { profileRoute } from './profile'; @@ -18,6 +18,15 @@ const mockUpdateUserById = jest.fn( ...data, }) ); +const encryptUserPassword = jest.fn(async (password: string) => ({ + passwordEncrypted: password + '_user1', + passwordEncryptionMethod: 'Argon2i', +})); +const mockArgon2Verify = jest.fn(async (password: string) => password === mockPasswordEncrypted); + +const interactionDetails: jest.MockedFunction<() => Promise> = jest.fn(async () => ({ + result: { login: { accountId: 'id', ts: getUnixTime(new Date()) - 60 } }, +})); jest.mock('oidc-provider', () => ({ Provider: jest.fn(() => ({ @@ -27,6 +36,11 @@ jest.mock('oidc-provider', () => ({ })), })); +jest.mock('@/lib/user', () => ({ + ...jest.requireActual('@/lib/user'), + encryptUserPassword: async (password: string) => encryptUserPassword(password), +})); + jest.mock('@/queries/user', () => ({ ...jest.requireActual('@/queries/user'), findUserById: async () => mockFindUserById(), @@ -48,6 +62,10 @@ jest.mock('@/queries/sign-in-experience', () => ({ findDefaultSignInExperience: jest.fn(async () => mockFindDefaultSignInExperience()), })); +jest.mock('hash-wasm', () => ({ + argon2Verify: async (password: string) => mockArgon2Verify(password), +})); + describe('session -> profileRoutes', () => { const sessionRequest = createRequester({ anonymousRoutes: profileRoutes, @@ -90,4 +108,37 @@ describe('session -> profileRoutes', () => { expect(response.statusCode).toEqual(422); }); }); + + describe('POST /session/profile/password', () => { + it('should update password with the new value', async () => { + const response = await sessionRequest + .post(`${profileRoute}/password`) + .send({ password: mockPasswordEncrypted }); + + expect(mockUpdateUserById).toBeCalledWith( + 'id', + expect.objectContaining({ + passwordEncrypted: 'a1b2c3_user1', + passwordEncryptionMethod: 'Argon2i', + }) + ); + expect(response.statusCode).toEqual(204); + }); + + it('should throw if new password is identical to old password', async () => { + jest.clearAllMocks(); + encryptUserPassword.mockImplementationOnce(async (password: string) => ({ + passwordEncrypted: password, + passwordEncryptionMethod: 'Argon2i', + })); + mockArgon2Verify.mockResolvedValueOnce(true); + + const response = await sessionRequest + .post(`${profileRoute}/password`) + .send({ password: 'password' }); + + expect(response.statusCode).toEqual(422); + expect(mockUpdateUserById).not.toBeCalled(); + }); + }); }); diff --git a/packages/core/src/routes/session/profile.ts b/packages/core/src/routes/session/profile.ts index 3467a2c77..50f3aef30 100644 --- a/packages/core/src/routes/session/profile.ts +++ b/packages/core/src/routes/session/profile.ts @@ -1,11 +1,13 @@ -import { usernameRegEx } from '@logto/core-kit'; +import { passwordRegEx, usernameRegEx } from '@logto/core-kit'; import { userInfoSelectFields } from '@logto/schemas'; +import { argon2Verify } from 'hash-wasm'; import pick from 'lodash.pick'; import type { Provider } from 'oidc-provider'; import { object, string } from 'zod'; import RequestError from '@/errors/RequestError'; import { checkSessionHealth } from '@/lib/session'; +import { encryptUserPassword } from '@/lib/user'; import koaGuard from '@/middleware/koa-guard'; import { findUserById, updateUserById } from '@/queries/user'; import assertThat from '@/utils/assert-that'; @@ -52,4 +54,32 @@ export default function profileRoutes(router: T, prov return next(); } ); + + router.post( + `${profileRoute}/password`, + koaGuard({ + body: object({ password: string().regex(passwordRegEx) }), + }), + async (ctx, next) => { + const userId = await checkSessionHealth(ctx, provider, verificationTimeout); + + assertThat(userId, new RequestError('auth.unauthorized')); + + const { password } = ctx.guard.body; + const { passwordEncrypted: oldPasswordEncrypted } = await findUserById(userId); + + assertThat( + !oldPasswordEncrypted || !(await argon2Verify({ password, hash: oldPasswordEncrypted })), + new RequestError({ code: 'user.same_password', status: 422 }) + ); + + const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password); + + await updateUserById(userId, { passwordEncrypted, passwordEncryptionMethod }); + + ctx.status = 204; + + return next(); + } + ); } From 64436a2a91639534c83d0fb050bd2a2105db87e2 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Wed, 16 Nov 2022 13:46:04 +0800 Subject: [PATCH 004/166] fix(ui): fix phone input android layout bug (#2421) --- packages/ui/src/components/Input/phoneInput.module.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ui/src/components/Input/phoneInput.module.scss b/packages/ui/src/components/Input/phoneInput.module.scss index c522e0a21..68ac5b054 100644 --- a/packages/ui/src/components/Input/phoneInput.module.scss +++ b/packages/ui/src/components/Input/phoneInput.module.scss @@ -40,6 +40,11 @@ width: 16px; height: 16px; } + + + input { + // hot fix unknown android bug of input width + width: 0; + } } } From 6ac6992000cecd6d68114f87c3245dab81c5ab11 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 16 Nov 2022 13:32:16 +0800 Subject: [PATCH 005/166] feat: email session apis --- .../core/src/routes/session/profile.test.ts | 131 ++++++++++++++++-- packages/core/src/routes/session/profile.ts | 41 +++++- packages/core/src/routes/session/utils.ts | 2 +- 3 files changed, 163 insertions(+), 11 deletions(-) diff --git a/packages/core/src/routes/session/profile.test.ts b/packages/core/src/routes/session/profile.test.ts index 0e4ab678d..e1be05fa3 100644 --- a/packages/core/src/routes/session/profile.test.ts +++ b/packages/core/src/routes/session/profile.test.ts @@ -23,15 +23,15 @@ const encryptUserPassword = jest.fn(async (password: string) => ({ passwordEncryptionMethod: 'Argon2i', })); const mockArgon2Verify = jest.fn(async (password: string) => password === mockPasswordEncrypted); - -const interactionDetails: jest.MockedFunction<() => Promise> = jest.fn(async () => ({ - result: { login: { accountId: 'id', ts: getUnixTime(new Date()) - 60 } }, +const mockGetSession = jest.fn(async () => ({ + accountId: 'id', + loginTs: getUnixTime(new Date()) - 60, })); jest.mock('oidc-provider', () => ({ Provider: jest.fn(() => ({ Session: { - get: jest.fn(async () => ({ accountId: 'id', loginTs: getUnixTime(new Date()) - 60 })), + get: async () => mockGetSession(), }, })), })); @@ -67,6 +67,10 @@ jest.mock('hash-wasm', () => ({ })); describe('session -> profileRoutes', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const sessionRequest = createRequester({ anonymousRoutes: profileRoutes, provider: new Provider(''), @@ -87,6 +91,20 @@ describe('session -> profileRoutes', () => { }); describe('PATCH /session/profile/username', () => { + it('should throw if last authentication time is over 10 mins ago', async () => { + mockGetSession.mockImplementationOnce(async () => ({ + accountId: 'id', + loginTs: getUnixTime(new Date()) - 601, + })); + + const response = await sessionRequest + .patch(`${profileRoute}/username`) + .send({ username: 'test' }); + + expect(response.statusCode).toEqual(422); + expect(mockUpdateUserById).not.toBeCalled(); + }); + it('should update username with the new value', async () => { const newUsername = 'charles'; @@ -109,10 +127,24 @@ describe('session -> profileRoutes', () => { }); }); - describe('POST /session/profile/password', () => { + describe('PATCH /session/profile/password', () => { + it('should throw if last authentication time is over 10 mins ago', async () => { + mockGetSession.mockImplementationOnce(async () => ({ + accountId: 'id', + loginTs: getUnixTime(new Date()) - 601, + })); + + const response = await sessionRequest + .patch(`${profileRoute}/password`) + .send({ password: mockPasswordEncrypted }); + + expect(response.statusCode).toEqual(422); + expect(mockUpdateUserById).not.toBeCalled(); + }); + it('should update password with the new value', async () => { const response = await sessionRequest - .post(`${profileRoute}/password`) + .patch(`${profileRoute}/password`) .send({ password: mockPasswordEncrypted }); expect(mockUpdateUserById).toBeCalledWith( @@ -126,7 +158,6 @@ describe('session -> profileRoutes', () => { }); it('should throw if new password is identical to old password', async () => { - jest.clearAllMocks(); encryptUserPassword.mockImplementationOnce(async (password: string) => ({ passwordEncrypted: password, passwordEncryptionMethod: 'Argon2i', @@ -134,11 +165,95 @@ describe('session -> profileRoutes', () => { mockArgon2Verify.mockResolvedValueOnce(true); const response = await sessionRequest - .post(`${profileRoute}/password`) + .patch(`${profileRoute}/password`) .send({ password: 'password' }); expect(response.statusCode).toEqual(422); expect(mockUpdateUserById).not.toBeCalled(); }); }); + + describe('email related APIs', () => { + it('should throw if last authentication time is over 10 mins ago on linking email', async () => { + mockGetSession.mockImplementationOnce(async () => ({ + accountId: 'id', + loginTs: getUnixTime(new Date()) - 601, + })); + + const updateResponse = await sessionRequest + .patch(`${profileRoute}/email`) + .send({ primaryEmail: 'test@logto.io' }); + + expect(updateResponse.statusCode).toEqual(422); + expect(mockUpdateUserById).not.toBeCalled(); + }); + + it('should link email address to the user profile', async () => { + const mockEmailAddress = 'bar@logto.io'; + const response = await sessionRequest + .patch(`${profileRoute}/email`) + .send({ primaryEmail: mockEmailAddress }); + + expect(mockUpdateUserById).toBeCalledWith( + 'id', + expect.objectContaining({ + primaryEmail: mockEmailAddress, + }) + ); + expect(response.statusCode).toEqual(204); + }); + + it('should throw when email address already exists', async () => { + mockHasUserWithEmail.mockImplementationOnce(async () => true); + + const response = await sessionRequest + .patch(`${profileRoute}/email`) + .send({ primaryEmail: mockUser.primaryEmail }); + + expect(response.statusCode).toEqual(422); + expect(mockUpdateUserById).not.toBeCalled(); + }); + + it('should throw when email address is invalid', async () => { + mockHasUserWithEmail.mockImplementationOnce(async () => true); + + const response = await sessionRequest + .patch(`${profileRoute}/email`) + .send({ primaryEmail: 'test' }); + + expect(response.statusCode).toEqual(400); + expect(mockUpdateUserById).not.toBeCalled(); + }); + + it('should throw if last authentication time is over 10 mins ago on unlinking email', async () => { + mockGetSession.mockImplementationOnce(async () => ({ + accountId: 'id', + loginTs: getUnixTime(new Date()) - 601, + })); + + const deleteResponse = await sessionRequest.delete(`${profileRoute}/email`); + + expect(deleteResponse.statusCode).toEqual(422); + expect(mockUpdateUserById).not.toBeCalled(); + }); + + it('should unlink email address from user', async () => { + const response = await sessionRequest.delete(`${profileRoute}/email`); + expect(response.statusCode).toEqual(204); + expect(mockUpdateUserById).toBeCalledWith( + 'id', + expect.objectContaining({ + primaryEmail: null, + }) + ); + }); + + it('should throw when no email address found in user', async () => { + mockFindUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryEmail: null })); + const response = await sessionRequest.delete(`${profileRoute}/email`); + + expect(response.statusCode).toEqual(422); + expect(mockUpdateUserById).not.toBeCalled(); + }); + }); }); diff --git a/packages/core/src/routes/session/profile.ts b/packages/core/src/routes/session/profile.ts index 50f3aef30..e287507d0 100644 --- a/packages/core/src/routes/session/profile.ts +++ b/packages/core/src/routes/session/profile.ts @@ -1,4 +1,4 @@ -import { passwordRegEx, usernameRegEx } from '@logto/core-kit'; +import { emailRegEx, passwordRegEx, usernameRegEx } from '@logto/core-kit'; import { userInfoSelectFields } from '@logto/schemas'; import { argon2Verify } from 'hash-wasm'; import pick from 'lodash.pick'; @@ -55,7 +55,7 @@ export default function profileRoutes(router: T, prov } ); - router.post( + router.patch( `${profileRoute}/password`, koaGuard({ body: object({ password: string().regex(passwordRegEx) }), @@ -82,4 +82,41 @@ export default function profileRoutes(router: T, prov return next(); } ); + + router.patch( + `${profileRoute}/email`, + koaGuard({ + body: object({ primaryEmail: string().regex(emailRegEx) }), + }), + async (ctx, next) => { + const userId = await checkSessionHealth(ctx, provider, verificationTimeout); + + assertThat(userId, new RequestError('auth.unauthorized')); + + const { primaryEmail } = ctx.guard.body; + + await checkSignUpIdentifierCollision({ primaryEmail }); + await updateUserById(userId, { primaryEmail }); + + ctx.status = 204; + + return next(); + } + ); + + router.delete(`${profileRoute}/email`, async (ctx, next) => { + const userId = await checkSessionHealth(ctx, provider, verificationTimeout); + + assertThat(userId, new RequestError('auth.unauthorized')); + + const { primaryEmail } = await findUserById(userId); + + assertThat(primaryEmail, new RequestError({ code: 'user.email_not_exists', status: 422 })); + + await updateUserById(userId, { primaryEmail: null }); + + ctx.status = 204; + + return next(); + }); } diff --git a/packages/core/src/routes/session/utils.ts b/packages/core/src/routes/session/utils.ts index 8749171d8..861b0e40b 100644 --- a/packages/core/src/routes/session/utils.ts +++ b/packages/core/src/routes/session/utils.ts @@ -226,7 +226,7 @@ export const checkSignUpIdentifierCollision = async ( primaryEmail?: Nullable; primaryPhone?: Nullable; }, - excludeUserId: string + excludeUserId?: string ) => { const { username, primaryEmail, primaryPhone } = identifiers; From 1073ce436351dd192a287d677e65511d592cc15d Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 16 Nov 2022 14:35:38 +0800 Subject: [PATCH 006/166] feat: phone related profile session APIs --- .../core/src/routes/session/profile.test.ts | 94 ++++++++++++++++++- packages/core/src/routes/session/profile.ts | 39 +++++++- 2 files changed, 127 insertions(+), 6 deletions(-) diff --git a/packages/core/src/routes/session/profile.test.ts b/packages/core/src/routes/session/profile.test.ts index e1be05fa3..0fbf2827a 100644 --- a/packages/core/src/routes/session/profile.test.ts +++ b/packages/core/src/routes/session/profile.test.ts @@ -180,11 +180,11 @@ describe('session -> profileRoutes', () => { loginTs: getUnixTime(new Date()) - 601, })); - const updateResponse = await sessionRequest + const response = await sessionRequest .patch(`${profileRoute}/email`) .send({ primaryEmail: 'test@logto.io' }); - expect(updateResponse.statusCode).toEqual(422); + expect(response.statusCode).toEqual(422); expect(mockUpdateUserById).not.toBeCalled(); }); @@ -231,9 +231,9 @@ describe('session -> profileRoutes', () => { loginTs: getUnixTime(new Date()) - 601, })); - const deleteResponse = await sessionRequest.delete(`${profileRoute}/email`); + const response = await sessionRequest.delete(`${profileRoute}/email`); - expect(deleteResponse.statusCode).toEqual(422); + expect(response.statusCode).toEqual(422); expect(mockUpdateUserById).not.toBeCalled(); }); @@ -248,7 +248,7 @@ describe('session -> profileRoutes', () => { ); }); - it('should throw when no email address found in user', async () => { + it('should throw when no email address found in user on unlinking email', async () => { mockFindUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryEmail: null })); const response = await sessionRequest.delete(`${profileRoute}/email`); @@ -256,4 +256,88 @@ describe('session -> profileRoutes', () => { expect(mockUpdateUserById).not.toBeCalled(); }); }); + + describe('phone related APIs', () => { + it('should throw if last authentication time is over 10 mins ago on linking phone number', async () => { + mockGetSession.mockImplementationOnce(async () => ({ + accountId: 'id', + loginTs: getUnixTime(new Date()) - 601, + })); + + const updateResponse = await sessionRequest + .patch(`${profileRoute}/phone`) + .send({ primaryPhone: '6533333333' }); + + expect(updateResponse.statusCode).toEqual(422); + expect(mockUpdateUserById).not.toBeCalled(); + }); + + it('should link phone number to the user profile', async () => { + const mockPhoneNumber = '6533333333'; + const response = await sessionRequest + .patch(`${profileRoute}/phone`) + .send({ primaryPhone: mockPhoneNumber }); + + expect(mockUpdateUserById).toBeCalledWith( + 'id', + expect.objectContaining({ + primaryPhone: mockPhoneNumber, + }) + ); + expect(response.statusCode).toEqual(204); + }); + + it('should throw when phone number already exists on linking phone number', async () => { + mockHasUserWithPhone.mockImplementationOnce(async () => true); + + const response = await sessionRequest + .patch(`${profileRoute}/phone`) + .send({ primaryPhone: mockUser.primaryPhone }); + + expect(response.statusCode).toEqual(422); + expect(mockUpdateUserById).not.toBeCalled(); + }); + + it('should throw when phone number is invalid', async () => { + mockHasUserWithPhone.mockImplementationOnce(async () => true); + + const response = await sessionRequest + .patch(`${profileRoute}/phone`) + .send({ primaryPhone: 'invalid' }); + + expect(response.statusCode).toEqual(400); + expect(mockUpdateUserById).not.toBeCalled(); + }); + + it('should throw if last authentication time is over 10 mins ago on unlinking phone number', async () => { + mockGetSession.mockImplementationOnce(async () => ({ + accountId: 'id', + loginTs: getUnixTime(new Date()) - 601, + })); + + const response = await sessionRequest.delete(`${profileRoute}/phone`); + + expect(response.statusCode).toEqual(422); + expect(mockUpdateUserById).not.toBeCalled(); + }); + + it('should unlink phone number from user', async () => { + const response = await sessionRequest.delete(`${profileRoute}/phone`); + expect(response.statusCode).toEqual(204); + expect(mockUpdateUserById).toBeCalledWith( + 'id', + expect.objectContaining({ + primaryPhone: null, + }) + ); + }); + + it('should throw when no phone number found in user on unlinking phone number', async () => { + mockFindUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryPhone: null })); + const response = await sessionRequest.delete(`${profileRoute}/phone`); + + expect(response.statusCode).toEqual(422); + expect(mockUpdateUserById).not.toBeCalled(); + }); + }); }); diff --git a/packages/core/src/routes/session/profile.ts b/packages/core/src/routes/session/profile.ts index e287507d0..8f032e37f 100644 --- a/packages/core/src/routes/session/profile.ts +++ b/packages/core/src/routes/session/profile.ts @@ -1,4 +1,4 @@ -import { emailRegEx, passwordRegEx, usernameRegEx } from '@logto/core-kit'; +import { emailRegEx, passwordRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit'; import { userInfoSelectFields } from '@logto/schemas'; import { argon2Verify } from 'hash-wasm'; import pick from 'lodash.pick'; @@ -119,4 +119,41 @@ export default function profileRoutes(router: T, prov return next(); }); + + router.patch( + `${profileRoute}/phone`, + koaGuard({ + body: object({ primaryPhone: string().regex(phoneRegEx) }), + }), + async (ctx, next) => { + const userId = await checkSessionHealth(ctx, provider, verificationTimeout); + + assertThat(userId, new RequestError('auth.unauthorized')); + + const { primaryPhone } = ctx.guard.body; + + await checkSignUpIdentifierCollision({ primaryPhone }); + await updateUserById(userId, { primaryPhone }); + + ctx.status = 204; + + return next(); + } + ); + + router.delete(`${profileRoute}/phone`, async (ctx, next) => { + const userId = await checkSessionHealth(ctx, provider, verificationTimeout); + + assertThat(userId, new RequestError('auth.unauthorized')); + + const { primaryPhone } = await findUserById(userId); + + assertThat(primaryPhone, new RequestError({ code: 'user.phone_not_exists', status: 422 })); + + await updateUserById(userId, { primaryPhone: null }); + + ctx.status = 204; + + return next(); + }); } From 1161c8322fdf13aff99b0aa79a28d9aa613305a1 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Wed, 16 Nov 2022 15:01:20 +0800 Subject: [PATCH 007/166] chore: update README.md (#2449) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d8bfff9a..04e21ca9f 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ npm init @logto ## Language support ```ts -const languages = ['English', 'Français', 'Português', '简体中文', 'Türkçe', '한국어']; +const languages = ['Deutsch', 'English', 'Français', 'Português', '简体中文', 'Türkçe', '한국어']; ``` ## Bug report, feature request, feedback From 92cb1c5bd8aa92339246789c3e189eb7944de02d Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Wed, 16 Nov 2022 15:35:53 +0800 Subject: [PATCH 008/166] ci: fetch all tags for release workflow (#2447) --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb44ca684..401e7b952 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -89,6 +89,7 @@ jobs: with: # Set Git operations with the bot PAT since we have tag protection rule token: ${{ secrets.BOT_PAT }} + fetch-depth: 0 - name: Setup Node and pnpm uses: silverhand-io/actions-node-pnpm-run-steps@v2 From 887befb76187e30483c303ffd6e11f7083cb881b Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 16 Nov 2022 18:20:47 +0800 Subject: [PATCH 009/166] refactor(console): connector setup warning (#2445) --- .../src/hooks/use-enabled-connector-types.ts | 25 +++++++++++++++++++ .../components/ConnectorSetupWarning.tsx | 12 +++------ 2 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 packages/console/src/hooks/use-enabled-connector-types.ts diff --git a/packages/console/src/hooks/use-enabled-connector-types.ts b/packages/console/src/hooks/use-enabled-connector-types.ts new file mode 100644 index 000000000..ea7efc38a --- /dev/null +++ b/packages/console/src/hooks/use-enabled-connector-types.ts @@ -0,0 +1,25 @@ +import type { ConnectorResponse, ConnectorType } from '@logto/schemas'; +import { useCallback, useMemo } from 'react'; +import useSWR from 'swr'; + +import type { RequestError } from './use-api'; + +const useEnabledConnectorTypes = () => { + const { data: connectors } = useSWR('/api/connectors'); + + const enabledConnectorTypes = useMemo( + () => connectors?.filter(({ enabled }) => enabled).map(({ type }) => type) ?? [], + [connectors] + ); + + const isConnectorTypeEnabled = useCallback( + (connectorType: ConnectorType) => enabledConnectorTypes.includes(connectorType), + [enabledConnectorTypes] + ); + + return { + isConnectorTypeEnabled, + }; +}; + +export default useEnabledConnectorTypes; diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/ConnectorSetupWarning.tsx b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/ConnectorSetupWarning.tsx index fa109e7dc..3351da7b1 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/ConnectorSetupWarning.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/components/ConnectorSetupWarning.tsx @@ -1,25 +1,19 @@ -import type { ConnectorResponse } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; import { useTranslation } from 'react-i18next'; -import useSWR from 'swr'; import Alert from '@/components/Alert'; -import type { RequestError } from '@/hooks/use-api'; +import useEnabledConnectorTypes from '@/hooks/use-enabled-connector-types'; type Props = { requiredConnectors: ConnectorType[]; }; const ConnectorSetupWarning = ({ requiredConnectors }: Props) => { - const { data: connectors } = useSWR('/api/connectors'); + const { isConnectorTypeEnabled } = useEnabledConnectorTypes(); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - if (!connectors) { - return null; - } - const missingConnectors = requiredConnectors.filter( - (connectorType) => !connectors.some(({ type, enabled }) => type === connectorType && enabled) + (connectorType) => !isConnectorTypeEnabled(connectorType) ); if (missingConnectors.length === 0) { From 0af95d790bad7ec4584f52a399b2ab6297b11be2 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 16 Nov 2022 18:31:42 +0800 Subject: [PATCH 010/166] refactor(console): validate sign-up identifier (#2446) --- .../tabs/SignUpAndSignInTab/SignUpForm.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/SignUpForm.tsx b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/SignUpForm.tsx index ed7f29143..968ffd21f 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/SignUpForm.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignInTab/SignUpForm.tsx @@ -6,6 +6,7 @@ import { snakeCase } from 'snake-case'; import Checkbox from '@/components/Checkbox'; import FormField from '@/components/FormField'; import Select from '@/components/Select'; +import useEnabledConnectorTypes from '@/hooks/use-enabled-connector-types'; import type { SignInExperienceForm } from '../../types'; import ConnectorSetupWarning from './components/ConnectorSetupWarning'; @@ -18,7 +19,13 @@ import * as styles from './index.module.scss'; const SignUpForm = () => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const { control, setValue, watch } = useFormContext(); + const { + control, + setValue, + watch, + formState: { errors }, + } = useFormContext(); + const { isConnectorTypeEnabled } = useEnabledConnectorTypes(); const signUpIdentifier = watch('signUp.identifier'); @@ -56,9 +63,21 @@ const SignUpForm = () => { { + if (!value) { + return false; + } + + return signUpIdentifierToRequiredConnectorMapping[value].every((connectorType) => + isConnectorTypeEnabled(connectorType) + ); + }, + }} render={({ field: { value, onChange } }) => ( + )} + /> + ); }; diff --git a/packages/console/src/pages/Connectors/components/Guide/index.tsx b/packages/console/src/pages/Connectors/components/Guide/index.tsx index 879bb5dc8..bb652a9b3 100644 --- a/packages/console/src/pages/Connectors/components/Guide/index.tsx +++ b/packages/console/src/pages/Connectors/components/Guide/index.tsx @@ -19,6 +19,7 @@ import SenderTester from '@/pages/ConnectorDetails/components/SenderTester'; import { safeParseJson } from '@/utilities/json'; import type { ConnectorFormType } from '../../types'; +import { SyncProfileMode } from '../../types'; import ConnectorForm from '../ConnectorForm'; import * as styles from './index.module.scss'; @@ -36,7 +37,12 @@ const Guide = ({ connector, onClose }: Props) => { const connectorName = conditional(isLanguageTag(language) && name[language]) ?? name.en; const isSocialConnector = connectorType !== ConnectorType.Sms && connectorType !== ConnectorType.Email; - const methods = useForm({ reValidateMode: 'onBlur' }); + const methods = useForm({ + reValidateMode: 'onBlur', + defaultValues: { + syncProfile: SyncProfileMode.OnlyAtRegister, + }, + }); const { formState: { isSubmitting }, handleSubmit, @@ -48,7 +54,7 @@ const Guide = ({ connector, onClose }: Props) => { return; } - const { config, name, ...otherData } = data; + const { config, name, syncProfile, ...otherData } = data; const result = safeParseJson(config); if (!result.success) { @@ -64,6 +70,7 @@ const Guide = ({ connector, onClose }: Props) => { json: { config: result.data, connectorId, + syncProfile: syncProfile === SyncProfileMode.EachSignIn, metadata: conditional( isStandard && { ...otherData, diff --git a/packages/console/src/pages/Connectors/types.ts b/packages/console/src/pages/Connectors/types.ts index 7fb60ef92..d25683d8e 100644 --- a/packages/console/src/pages/Connectors/types.ts +++ b/packages/console/src/pages/Connectors/types.ts @@ -4,4 +4,10 @@ export type ConnectorFormType = { logo: string; logoDark: string; target: string; + syncProfile: SyncProfileMode; }; + +export enum SyncProfileMode { + OnlyAtRegister = 'OnlyAtRegister', + EachSignIn = 'EachSignIn', +} diff --git a/packages/phrases/src/locales/de/translation/admin-console/connectors.ts b/packages/phrases/src/locales/de/translation/admin-console/connectors.ts index 33af5d7f9..617d9341a 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/connectors.ts @@ -45,6 +45,9 @@ const connectors = { target: 'Connector identity target', // UNTRANSLATED target_tip: 'A unique identifier for the connector.', // UNTRANSLATED config: 'Enter your JSON here', // UNTRANSLATED + sync_profile: 'Sync profile information form the social provider', // UNTRANSLATED + sync_profile_only_at_register: 'Only sync at register', // UNTRANSLATED + sync_profile_each_sign_in: 'Always sync at each sign-in', // UNTRANSLATED }, platform: { universal: 'Universal', diff --git a/packages/phrases/src/locales/en/translation/admin-console/connectors.ts b/packages/phrases/src/locales/en/translation/admin-console/connectors.ts index 1aebb7034..8b4b24443 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/connectors.ts @@ -45,6 +45,9 @@ const connectors = { target: 'Connector identity target', target_tip: 'A unique identifier for the connector.', config: 'Enter your JSON here', + sync_profile: 'Sync profile information form the social provider', + sync_profile_only_at_register: 'Only sync at register', + sync_profile_each_sign_in: 'Always sync at each sign-in', }, platform: { universal: 'Universal', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts b/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts index dfc02a9eb..927e1e56e 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts @@ -46,6 +46,9 @@ const connectors = { target: 'Connector identity target', // UNTRANSLATED target_tip: 'A unique identifier for the connector.', // UNTRANSLATED config: 'Enter your JSON here', // UNTRANSLATED + sync_profile: 'Sync profile information form the social provider', // UNTRANSLATED + sync_profile_only_at_register: 'Only sync at register', // UNTRANSLATED + sync_profile_each_sign_in: 'Always sync at each sign-in', // UNTRANSLATED }, platform: { universal: 'Universel', diff --git a/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts b/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts index 096d18946..67fc23f80 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts @@ -45,6 +45,9 @@ const connectors = { target: 'Connector identity target', // UNTRANSLATED target_tip: 'A unique identifier for the connector.', // UNTRANSLATED config: 'Enter your JSON here', // UNTRANSLATED + sync_profile: 'Sync profile information form the social provider', // UNTRANSLATED + sync_profile_only_at_register: 'Only sync at register', // UNTRANSLATED + sync_profile_each_sign_in: 'Always sync at each sign-in', // UNTRANSLATED }, platform: { universal: 'Universal', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts index 8f336f598..546b14bfd 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts @@ -45,6 +45,9 @@ const connectors = { target: 'Connector identity target', // UNTRANSLATED target_tip: 'A unique identifier for the connector.', // UNTRANSLATED config: 'Enter your JSON here', // UNTRANSLATED + sync_profile: 'Sync profile information form the social provider', // UNTRANSLATED + sync_profile_only_at_register: 'Only sync at register', // UNTRANSLATED + sync_profile_each_sign_in: 'Always sync at each sign-in', // UNTRANSLATED }, platform: { universal: 'Universal', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts index 32af270a5..5e2d22d08 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts @@ -46,6 +46,9 @@ const connectors = { target: 'Connector identity target', // UNTRANSLATED target_tip: 'A unique identifier for the connector.', // UNTRANSLATED config: 'Enter your JSON here', // UNTRANSLATED + sync_profile: 'Sync profile information form the social provider', // UNTRANSLATED + sync_profile_only_at_register: 'Only sync at register', // UNTRANSLATED + sync_profile_each_sign_in: 'Always sync at each sign-in', // UNTRANSLATED }, platform: { universal: 'Evrensel', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts index 70380b49e..c6c714552 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts @@ -43,6 +43,9 @@ const connectors = { target: '连接器 target', target_tip: '连接器标识符', config: '请在此输入你的 JSON 配置', + sync_profile: '从社交服务商同步用户数据', + sync_profile_only_at_register: '仅在注册时同步', + sync_profile_each_sign_in: '每次登录都同步', }, platform: { universal: '通用', From 2d15ead429f999e8c6c0b40dd4ad73cde39d1563 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Mon, 5 Dec 2022 16:37:06 +0800 Subject: [PATCH 115/166] style(console): `CopyToClipboard` component (#2584) --- .../components/CopyToClipboard/index.module.scss | 14 +++++++++----- .../src/components/CopyToClipboard/index.tsx | 15 +++++++++------ .../console/src/components/IconButton/index.tsx | 5 +++-- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/console/src/components/CopyToClipboard/index.module.scss b/packages/console/src/components/CopyToClipboard/index.module.scss index ae68d6b67..cb7cd89ea 100644 --- a/packages/console/src/components/CopyToClipboard/index.module.scss +++ b/packages/console/src/components/CopyToClipboard/index.module.scss @@ -9,7 +9,7 @@ cursor: default; &.contained { - padding: _.unit(1) _.unit(1) _.unit(1) _.unit(3); + padding: _.unit(1) _.unit(2); background: var(--color-layer-2); } @@ -31,12 +31,16 @@ text-overflow: ellipsis; } - .copyIcon { + .copyIconButton { margin-left: _.unit(3); + height: 20px; + width: 20px; - svg { - width: 16px; - height: 16px; + .copyIcon { + svg { + width: 16px; + height: 16px; + } } } } diff --git a/packages/console/src/components/CopyToClipboard/index.tsx b/packages/console/src/components/CopyToClipboard/index.tsx index 186b09d37..8696b5703 100644 --- a/packages/console/src/components/CopyToClipboard/index.tsx +++ b/packages/console/src/components/CopyToClipboard/index.tsx @@ -28,7 +28,7 @@ const CopyToClipboard = ({ hasVisibilityToggle, variant = 'contained', }: Props) => { - const copyIconReference = useRef(null); + const copyIconReference = useRef(null); const [copyState, setCopyState] = useState('copy'); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.general' }); const [showHiddenContent, setShowHiddenContent] = useState(false); @@ -78,11 +78,14 @@ const CopyToClipboard = ({ )} -
- - - -
+ + + , 'size' | 'type'> & { size?: 'small' | 'medium' | 'large'; tooltip?: ReactNode; + iconClassName?: string; }; const IconButton = ( - { size = 'medium', children, className, tooltip, ...rest }: Props, + { size = 'medium', children, className, tooltip, iconClassName, ...rest }: Props, reference: ForwardedRef ) => { const tipRef = useRef(null); @@ -23,7 +24,7 @@ const IconButton = ( className={classNames(styles.button, styles[size], className)} {...rest} > -
+
{children}
{tooltip && ( From cbb61eff2353735e7ac61f433b33426da06bf822 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Mon, 5 Dec 2022 16:37:19 +0800 Subject: [PATCH 116/166] style(console): set the `TabNavItem` text color to neutral-30 (#2585) --- packages/console/src/components/TabNav/TabNavItem.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/src/components/TabNav/TabNavItem.module.scss b/packages/console/src/components/TabNav/TabNavItem.module.scss index d41a13443..89b59946d 100644 --- a/packages/console/src/components/TabNav/TabNavItem.module.scss +++ b/packages/console/src/components/TabNav/TabNavItem.module.scss @@ -16,7 +16,7 @@ a { display: inline-block; - color: var(--color-text-secondary); + color: var(--color-neutral-30); text-decoration: none; cursor: pointer; } From 9616b9383c1b0d4fee4d953877e7ae860eae7a1e Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Mon, 5 Dec 2022 16:37:34 +0800 Subject: [PATCH 117/166] style(console): center align create buttons on list page (#2586) --- packages/console/src/scss/resources.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/console/src/scss/resources.module.scss b/packages/console/src/scss/resources.module.scss index 58e094587..57d36a0d1 100644 --- a/packages/console/src/scss/resources.module.scss +++ b/packages/console/src/scss/resources.module.scss @@ -11,6 +11,7 @@ .headline { display: flex; justify-content: space-between; + align-items: center; } .table { From 732421582a83f462295804c3b4a845e72e197d2c Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Tue, 6 Dec 2022 11:56:56 +0800 Subject: [PATCH 118/166] chore: upgrade toolkit --- packages/console/package.json | 4 +- packages/core/package.json | 6 +-- packages/demo-app/package.json | 4 +- packages/phrases-ui/package.json | 4 +- packages/phrases/package.json | 4 +- packages/schemas/package.json | 6 +-- packages/ui/package.json | 4 +- pnpm-lock.yaml | 87 ++++++++++++++++---------------- 8 files changed, 59 insertions(+), 60 deletions(-) diff --git a/packages/console/package.json b/packages/console/package.json index 9e5ae815c..9ab567fdf 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -19,8 +19,8 @@ }, "devDependencies": { "@fontsource/roboto-mono": "^4.5.7", - "@logto/core-kit": "1.0.0-beta.26", - "@logto/language-kit": "1.0.0-beta.26", + "@logto/core-kit": "1.0.0-beta.28", + "@logto/language-kit": "1.0.0-beta.28", "@logto/phrases": "workspace:^", "@logto/phrases-ui": "workspace:^", "@logto/react": "1.0.0-beta.13", diff --git a/packages/core/package.json b/packages/core/package.json index 009b5ecba..834c7a317 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -24,9 +24,9 @@ }, "dependencies": { "@logto/cli": "workspace:^", - "@logto/connector-kit": "1.0.0-beta.27", - "@logto/core-kit": "1.0.0-beta.26", - "@logto/language-kit": "1.0.0-beta.26", + "@logto/connector-kit": "1.0.0-beta.28", + "@logto/core-kit": "1.0.0-beta.28", + "@logto/language-kit": "1.0.0-beta.28", "@logto/phrases": "workspace:^", "@logto/phrases-ui": "workspace:^", "@logto/schemas": "workspace:^", diff --git a/packages/demo-app/package.json b/packages/demo-app/package.json index 5b52c2f7c..2c536b5d6 100644 --- a/packages/demo-app/package.json +++ b/packages/demo-app/package.json @@ -16,8 +16,8 @@ "stylelint": "stylelint \"src/**/*.scss\"" }, "devDependencies": { - "@logto/core-kit": "1.0.0-beta.26", - "@logto/language-kit": "1.0.0-beta.26", + "@logto/core-kit": "1.0.0-beta.28", + "@logto/language-kit": "1.0.0-beta.28", "@logto/phrases": "workspace:^", "@logto/react": "1.0.0-beta.13", "@logto/schemas": "workspace:^", diff --git a/packages/phrases-ui/package.json b/packages/phrases-ui/package.json index 44aaae484..27d0084ef 100644 --- a/packages/phrases-ui/package.json +++ b/packages/phrases-ui/package.json @@ -32,8 +32,8 @@ "url": "https://github.com/logto-io/logto/issues" }, "dependencies": { - "@logto/core-kit": "1.0.0-beta.26", - "@logto/language-kit": "1.0.0-beta.26", + "@logto/core-kit": "1.0.0-beta.28", + "@logto/language-kit": "1.0.0-beta.28", "@silverhand/essentials": "^1.3.0", "zod": "^3.19.1" }, diff --git a/packages/phrases/package.json b/packages/phrases/package.json index 926b08d1b..5819bf24d 100644 --- a/packages/phrases/package.json +++ b/packages/phrases/package.json @@ -32,8 +32,8 @@ "url": "https://github.com/logto-io/logto/issues" }, "dependencies": { - "@logto/core-kit": "1.0.0-beta.26", - "@logto/language-kit": "1.0.0-beta.26", + "@logto/core-kit": "1.0.0-beta.28", + "@logto/language-kit": "1.0.0-beta.28", "@silverhand/essentials": "^1.3.0", "zod": "^3.19.1" }, diff --git a/packages/schemas/package.json b/packages/schemas/package.json index cdea73346..0fa3b4524 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -68,9 +68,9 @@ }, "prettier": "@silverhand/eslint-config/.prettierrc", "dependencies": { - "@logto/connector-kit": "1.0.0-beta.27", - "@logto/core-kit": "1.0.0-beta.26", - "@logto/language-kit": "1.0.0-beta.26", + "@logto/connector-kit": "1.0.0-beta.28", + "@logto/core-kit": "1.0.0-beta.28", + "@logto/language-kit": "1.0.0-beta.28", "@logto/phrases": "workspace:^", "@logto/phrases-ui": "workspace:^", "zod": "^3.19.1" diff --git a/packages/ui/package.json b/packages/ui/package.json index 00b46f0ef..114f55159 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -17,8 +17,8 @@ "test": "jest" }, "devDependencies": { - "@logto/core-kit": "1.0.0-beta.26", - "@logto/language-kit": "1.0.0-beta.26", + "@logto/core-kit": "1.0.0-beta.28", + "@logto/language-kit": "1.0.0-beta.28", "@logto/phrases": "workspace:^", "@logto/phrases-ui": "workspace:^", "@logto/schemas": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0cb27402..ad23a5179 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,8 +105,8 @@ importers: packages/console: specifiers: '@fontsource/roboto-mono': ^4.5.7 - '@logto/core-kit': 1.0.0-beta.26 - '@logto/language-kit': 1.0.0-beta.26 + '@logto/core-kit': 1.0.0-beta.28 + '@logto/language-kit': 1.0.0-beta.28 '@logto/phrases': workspace:^ '@logto/phrases-ui': workspace:^ '@logto/react': 1.0.0-beta.13 @@ -177,8 +177,8 @@ importers: zod: ^3.19.1 devDependencies: '@fontsource/roboto-mono': 4.5.7 - '@logto/core-kit': 1.0.0-beta.26_zod@3.19.1 - '@logto/language-kit': 1.0.0-beta.26_zod@3.19.1 + '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 + '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 '@logto/phrases': link:../phrases '@logto/phrases-ui': link:../phrases-ui '@logto/react': 1.0.0-beta.13_react@18.2.0 @@ -251,9 +251,9 @@ importers: packages/core: specifiers: '@logto/cli': workspace:^ - '@logto/connector-kit': 1.0.0-beta.27 - '@logto/core-kit': 1.0.0-beta.26 - '@logto/language-kit': 1.0.0-beta.26 + '@logto/connector-kit': 1.0.0-beta.28 + '@logto/core-kit': 1.0.0-beta.28 + '@logto/language-kit': 1.0.0-beta.28 '@logto/phrases': workspace:^ '@logto/phrases-ui': workspace:^ '@logto/schemas': workspace:^ @@ -327,9 +327,9 @@ importers: zod: ^3.19.1 dependencies: '@logto/cli': link:../cli - '@logto/connector-kit': 1.0.0-beta.27_zod@3.19.1 - '@logto/core-kit': 1.0.0-beta.26_zod@3.19.1 - '@logto/language-kit': 1.0.0-beta.26_zod@3.19.1 + '@logto/connector-kit': 1.0.0-beta.28_zod@3.19.1 + '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 + '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 '@logto/phrases': link:../phrases '@logto/phrases-ui': link:../phrases-ui '@logto/schemas': link:../schemas @@ -411,8 +411,8 @@ importers: packages/demo-app: specifiers: - '@logto/core-kit': 1.0.0-beta.26 - '@logto/language-kit': 1.0.0-beta.26 + '@logto/core-kit': 1.0.0-beta.28 + '@logto/language-kit': 1.0.0-beta.28 '@logto/phrases': workspace:^ '@logto/react': 1.0.0-beta.13 '@logto/schemas': workspace:^ @@ -439,8 +439,8 @@ importers: typescript: ^4.7.4 zod: ^3.19.1 devDependencies: - '@logto/core-kit': 1.0.0-beta.26_zod@3.19.1 - '@logto/language-kit': 1.0.0-beta.26_zod@3.19.1 + '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 + '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 '@logto/phrases': link:../phrases '@logto/react': 1.0.0-beta.13_react@18.2.0 '@logto/schemas': link:../schemas @@ -523,8 +523,8 @@ importers: packages/phrases: specifiers: - '@logto/core-kit': 1.0.0-beta.26 - '@logto/language-kit': 1.0.0-beta.26 + '@logto/core-kit': 1.0.0-beta.28 + '@logto/language-kit': 1.0.0-beta.28 '@silverhand/eslint-config': 1.3.0 '@silverhand/essentials': ^1.3.0 '@silverhand/ts-config': 1.2.1 @@ -534,8 +534,8 @@ importers: typescript: ^4.7.4 zod: ^3.19.1 dependencies: - '@logto/core-kit': 1.0.0-beta.26_zod@3.19.1 - '@logto/language-kit': 1.0.0-beta.26_zod@3.19.1 + '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 + '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 '@silverhand/essentials': 1.3.0 zod: 3.19.1 devDependencies: @@ -548,8 +548,8 @@ importers: packages/phrases-ui: specifiers: - '@logto/core-kit': 1.0.0-beta.26 - '@logto/language-kit': 1.0.0-beta.26 + '@logto/core-kit': 1.0.0-beta.28 + '@logto/language-kit': 1.0.0-beta.28 '@silverhand/eslint-config': 1.3.0 '@silverhand/essentials': ^1.3.0 '@silverhand/ts-config': 1.2.1 @@ -559,8 +559,8 @@ importers: typescript: ^4.7.4 zod: ^3.19.1 dependencies: - '@logto/core-kit': 1.0.0-beta.26_zod@3.19.1 - '@logto/language-kit': 1.0.0-beta.26_zod@3.19.1 + '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 + '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 '@silverhand/essentials': 1.3.0 zod: 3.19.1 devDependencies: @@ -573,9 +573,9 @@ importers: packages/schemas: specifiers: - '@logto/connector-kit': 1.0.0-beta.27 - '@logto/core-kit': 1.0.0-beta.26 - '@logto/language-kit': 1.0.0-beta.26 + '@logto/connector-kit': 1.0.0-beta.28 + '@logto/core-kit': 1.0.0-beta.28 + '@logto/language-kit': 1.0.0-beta.28 '@logto/phrases': workspace:^ '@logto/phrases-ui': workspace:^ '@silverhand/eslint-config': 1.3.0 @@ -598,9 +598,9 @@ importers: typescript: ^4.7.4 zod: ^3.19.1 dependencies: - '@logto/connector-kit': 1.0.0-beta.27_zod@3.19.1 - '@logto/core-kit': 1.0.0-beta.26_zod@3.19.1 - '@logto/language-kit': 1.0.0-beta.26_zod@3.19.1 + '@logto/connector-kit': 1.0.0-beta.28_zod@3.19.1 + '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 + '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 '@logto/phrases': link:../phrases '@logto/phrases-ui': link:../phrases-ui zod: 3.19.1 @@ -661,8 +661,8 @@ importers: packages/ui: specifiers: - '@logto/core-kit': 1.0.0-beta.26 - '@logto/language-kit': 1.0.0-beta.26 + '@logto/core-kit': 1.0.0-beta.28 + '@logto/language-kit': 1.0.0-beta.28 '@logto/phrases': workspace:^ '@logto/phrases-ui': workspace:^ '@logto/schemas': workspace:^ @@ -715,8 +715,8 @@ importers: use-debounced-loader: ^0.1.1 zod: ^3.19.1 devDependencies: - '@logto/core-kit': 1.0.0-beta.26_zod@3.19.1 - '@logto/language-kit': 1.0.0-beta.26_zod@3.19.1 + '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 + '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 '@logto/phrases': link:../phrases '@logto/phrases-ui': link:../phrases-ui '@logto/schemas': link:../schemas @@ -2448,14 +2448,14 @@ packages: lodash.once: 4.1.1 dev: true - /@logto/connector-kit/1.0.0-beta.27_zod@3.19.1: - resolution: {integrity: sha512-wN+m1cQWUZXzci36yUJXNbCTPcj1IZ485+m26rMt/kJ0xosUq9Bt7OtCwL3FNNxkPs+5fF7EbHTWSttW4R90Jw==} + /@logto/connector-kit/1.0.0-beta.28_zod@3.19.1: + resolution: {integrity: sha512-f+Hnn84nC6cnu6jwKLDAYo2+rNypHhWhs1NWYQKWVPUtIoLBKircLKGlvVjzEQnXZM6Cnd47X1Zk15IVFhraWA==} engines: {node: ^16.13.0 || ^18.12.0} peerDependencies: zod: ^3.19.1 dependencies: - '@logto/core-kit': 1.0.0-beta.26_zod@3.19.1 - '@logto/language-kit': 1.0.0-beta.26_zod@3.19.1 + '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 + '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 '@silverhand/essentials': 1.3.0 zod: 3.19.1 dev: false @@ -2464,21 +2464,21 @@ packages: resolution: {integrity: sha512-seYvL/aGYRfO4d0FYfKIW/Cu9PnFMRpRM5/oRXwXbcbv+LY1a3TcAX0itrVXeBygIrxiAmWd9DL7CGIWzb48Qg==} engines: {node: ^16.0.0} dependencies: - '@logto/language-kit': 1.0.0-beta.26_zod@3.19.1 + '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 color: 4.2.3 nanoid: 3.3.1 zod: 3.19.1 dev: true - /@logto/core-kit/1.0.0-beta.26_zod@3.19.1: - resolution: {integrity: sha512-WsgJ6sIPNO0yH+5V94Vjomi6pP0txky53MkIk7tGV3xGUQSJTBFjEHciTwCl2tHUB6VsPeerjs22jawakFyfFg==} + /@logto/core-kit/1.0.0-beta.28_zod@3.19.1: + resolution: {integrity: sha512-tbT34SupNvk7mriMhBq32KknCEe+qm7LrZBc2E7JDD3FBJmMxyHQCJgdPBlnR+0sXB0uG32NmYwLqG4p9slAPw==} engines: {node: ^16.13.0 || ^18.12.0} peerDependencies: zod: ^3.19.1 dependencies: - '@logto/language-kit': 1.0.0-beta.26_zod@3.19.1 + '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 color: 4.2.3 - nanoid: 3.3.4 + nanoid: 3.3.1 zod: 3.19.1 /@logto/js/1.0.0-beta.13: @@ -2491,8 +2491,8 @@ packages: lodash.get: 4.4.2 dev: true - /@logto/language-kit/1.0.0-beta.26_zod@3.19.1: - resolution: {integrity: sha512-z8YufwLQoVfx3NcNiz2ORlmOIRFy8K+OY0e8bAhnT+N3x8MSPPlcFmK2EidEpl3DqL27QLJVHQaOsEnDHCad8g==} + /@logto/language-kit/1.0.0-beta.28_zod@3.19.1: + resolution: {integrity: sha512-levNkJ0uiTufnn0r8dLZLdPKgRFiV5mSQ0H04AhpP9dHlm7+1PMRspoi8NjcQqUgemhmcwpP8oCtn4rgW8vpJA==} engines: {node: ^16.13.0 || ^18.12.0} peerDependencies: zod: ^3.19.1 @@ -11350,7 +11350,6 @@ packages: resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: true /nanoid/3.3.4: resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} From c8005174f333d82782b6916156feef3e84894c57 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 6 Dec 2022 12:35:02 +0800 Subject: [PATCH 119/166] style(console): skeletons should not overlap the table header (#2592) --- packages/console/src/scss/table.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/console/src/scss/table.module.scss b/packages/console/src/scss/table.module.scss index 16f1737df..10582f0ef 100644 --- a/packages/console/src/scss/table.module.scss +++ b/packages/console/src/scss/table.module.scss @@ -27,6 +27,7 @@ tr.clickable { background: var(--color-layer-1); position: sticky; top: 0; + z-index: 1; } tbody { From e88dc8e7670799a3ff3328cde7a45dd0d1eaf672 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 6 Dec 2022 12:51:57 +0800 Subject: [PATCH 120/166] style(console): fix the minus icon size (#2593) --- packages/console/src/assets/images/minus.svg | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/console/src/assets/images/minus.svg b/packages/console/src/assets/images/minus.svg index 6176fc2b3..8e8ac8c11 100644 --- a/packages/console/src/assets/images/minus.svg +++ b/packages/console/src/assets/images/minus.svg @@ -1,5 +1,3 @@ - - + + From 0bec9f0ff517b43961ef8a4ce53943740800a55c Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 6 Dec 2022 12:56:02 +0800 Subject: [PATCH 121/166] refactor(console): extract toggle tip logic into a `ToggleTipButton` component (#2588) --- .../ToggleTipButton/index.module.scss | 19 +++++ .../src/components/ToggleTipButton/index.tsx | 59 +++++++++++++++ .../ConnectorStatusField/index.module.scss | 9 +-- .../components/ConnectorStatusField/index.tsx | 74 +++++++------------ 4 files changed, 107 insertions(+), 54 deletions(-) create mode 100644 packages/console/src/components/ToggleTipButton/index.module.scss create mode 100644 packages/console/src/components/ToggleTipButton/index.tsx diff --git a/packages/console/src/components/ToggleTipButton/index.module.scss b/packages/console/src/components/ToggleTipButton/index.module.scss new file mode 100644 index 000000000..1b432a460 --- /dev/null +++ b/packages/console/src/components/ToggleTipButton/index.module.scss @@ -0,0 +1,19 @@ +@use '@/scss/underscore' as _; + +.toggleTipButton { + border-radius: 4px; + padding: _.unit(1); + + .icon { + > svg { + display: block; + cursor: pointer; + width: 16px; + height: 16px; + } + } + + &:hover { + background: var(--color-hover); + } +} diff --git a/packages/console/src/components/ToggleTipButton/index.tsx b/packages/console/src/components/ToggleTipButton/index.tsx new file mode 100644 index 000000000..63cef14ad --- /dev/null +++ b/packages/console/src/components/ToggleTipButton/index.tsx @@ -0,0 +1,59 @@ +import classNames from 'classnames'; +import type { ReactElement } from 'react'; +import { useRef, useState } from 'react'; + +import Tip from '@/assets/images/tip.svg'; +import type { HorizontalAlignment } from '@/hooks/use-position'; +import { onKeyDownHandler } from '@/utilities/a11y'; + +import type { TipBubblePosition } from '../TipBubble'; +import ToggleTip from '../ToggleTip'; +import * as styles from './index.module.scss'; + +type Props = { + render: (closeTipHandler: () => void) => ReactElement; + className?: string; + tipPosition?: TipBubblePosition; + tipHorizontalAlignment?: HorizontalAlignment; +}; + +const ToggleTipButton = ({ render, className, tipPosition, tipHorizontalAlignment }: Props) => { + const anchorRef = useRef(null); + const [isTipOpen, setIsTipOpen] = useState(false); + + const closeTipHandler = () => { + setIsTipOpen(false); + }; + + return ( +
+
{ + setIsTipOpen(true); + }} + onKeyDown={onKeyDownHandler(() => { + setIsTipOpen(true); + })} + > + +
+ { + setIsTipOpen(false); + }} + > + {render(closeTipHandler)} + +
+ ); +}; + +export default ToggleTipButton; diff --git a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss index e70a6b512..65a58acbe 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss +++ b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss @@ -3,14 +3,9 @@ .field { display: flex; align-items: center; -} -.tipIcon { - margin-left: _.unit(1); - - > svg { - display: block; - cursor: pointer; + .tipButton { + margin-left: _.unit(1); } } diff --git a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx index c626fb465..7196b8299 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx +++ b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx @@ -1,62 +1,42 @@ -import { useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import Tip from '@/assets/images/tip.svg'; -import ToggleTip from '@/components/ToggleTip'; -import { onKeyDownHandler } from '@/utilities/a11y'; +import ToggleTipButton from '@/components/ToggleTipButton'; import * as styles from './index.module.scss'; const ConnectorStatusField = () => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const [isTipOpen, setIsTipOpen] = useState(false); - const anchorRef = useRef(null); return (
{t('connectors.connector_status')} -
- { - setIsTipOpen(true); - }} - onKeyDown={onKeyDownHandler(() => { - setIsTipOpen(true); - })} - /> -
- { - setIsTipOpen(false); - }} - > -
{t('connectors.connector_status')}
-
- { - setIsTipOpen(false); - }} - /> - ), - }} - > - {t('connectors.not_in_use_tip.content', { - link: t('connectors.not_in_use_tip.go_to_sie'), - })} - -
-
+ ( + <> +
{t('connectors.connector_status')}
+
+ + ), + }} + > + {t('connectors.not_in_use_tip.content', { + link: t('connectors.not_in_use_tip.go_to_sie'), + })} + +
+ + )} + />
); }; From b997d6f420ef1e618bf6bbda6b80e5e4bab8ddd3 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 6 Dec 2022 13:08:35 +0800 Subject: [PATCH 122/166] refactor(console): replace info tooltips with toggle tips (#2590) --- .../components/FormField/index.module.scss | 5 +--- .../src/components/FormField/index.tsx | 24 ++++--------------- .../components/MultiTextInputField/index.tsx | 6 ++--- .../components/CreateForm/index.tsx | 2 +- .../components/AdvancedSettings.tsx | 2 +- .../components/Settings.tsx | 6 ++--- .../Dashboard/components/Block.module.scss | 9 +------ .../src/pages/Dashboard/components/Block.tsx | 20 ++++++---------- .../console/src/pages/Dashboard/index.tsx | 12 +++++----- .../tabs/Others/TermsForm.tsx | 2 +- .../UserDetails/components/UserSettings.tsx | 2 +- 11 files changed, 30 insertions(+), 60 deletions(-) diff --git a/packages/console/src/components/FormField/index.module.scss b/packages/console/src/components/FormField/index.module.scss index 3fd003454..58fe6cd32 100644 --- a/packages/console/src/components/FormField/index.module.scss +++ b/packages/console/src/components/FormField/index.module.scss @@ -16,11 +16,8 @@ color: var(--color-text); } - .icon { + .toggleTipButton { margin-left: _.unit(1); - width: 16px; - height: 16px; - color: var(--color-text-secondary); } .required { diff --git a/packages/console/src/components/FormField/index.tsx b/packages/console/src/components/FormField/index.tsx index 620fb2ed2..759e38ec3 100644 --- a/packages/console/src/components/FormField/index.tsx +++ b/packages/console/src/components/FormField/index.tsx @@ -1,14 +1,11 @@ import type { AdminConsoleKey } from '@logto/phrases'; import classNames from 'classnames'; import type { ReactElement, ReactNode } from 'react'; -import { useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import Tip from '@/assets/images/tip.svg'; - import type DangerousRaw from '../DangerousRaw'; import Spacer from '../Spacer'; -import Tooltip from '../Tooltip'; +import ToggleTipButton from '../ToggleTipButton'; import * as styles from './index.module.scss'; export type Props = { @@ -17,29 +14,18 @@ export type Props = { isRequired?: boolean; className?: string; headlineClassName?: string; - tooltip?: AdminConsoleKey; + tip?: AdminConsoleKey; }; -const FormField = ({ - title, - children, - isRequired, - className, - tooltip, - headlineClassName, -}: Props) => { +const FormField = ({ title, children, isRequired, className, tip, headlineClassName }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const tipRef = useRef(null); return (
{typeof title === 'string' ? t(title) : title}
- {tooltip && ( -
- - -
+ {tip && ( +
{t(tip)}
} /> )} {isRequired &&
{t('general.required')}
} diff --git a/packages/console/src/components/MultiTextInputField/index.tsx b/packages/console/src/components/MultiTextInputField/index.tsx index 4e87156d6..3cdb2165f 100644 --- a/packages/console/src/components/MultiTextInputField/index.tsx +++ b/packages/console/src/components/MultiTextInputField/index.tsx @@ -8,14 +8,14 @@ import MultiTextInput from '../MultiTextInput'; import * as styles from './index.module.scss'; type Props = MultiTextInputProps & - Pick & { + Pick & { formFieldClassName?: FormFieldProps['className']; }; const MultiTextInputField = ({ title, isRequired, - tooltip, + tip, formFieldClassName, value, ...rest @@ -23,7 +23,7 @@ const MultiTextInputField = ({ 1 && styles.headlineWithMultiInputs)} > diff --git a/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx b/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx index b48344a8b..f3c3610d2 100644 --- a/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx +++ b/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx @@ -64,7 +64,7 @@ const CreateForm = ({ onClose }: Props) => { { > { { render={({ field: { onChange, value }, fieldState: { error } }) => ( { render={({ field: { onChange, value }, fieldState: { error } }) => ( svg { - display: block; - } } } diff --git a/packages/console/src/pages/Dashboard/components/Block.tsx b/packages/console/src/pages/Dashboard/components/Block.tsx index 969728db3..84c911e25 100644 --- a/packages/console/src/pages/Dashboard/components/Block.tsx +++ b/packages/console/src/pages/Dashboard/components/Block.tsx @@ -1,14 +1,12 @@ import type { AdminConsoleKey } from '@logto/phrases'; import { conditionalString } from '@silverhand/essentials'; import classNames from 'classnames'; -import { useRef } from 'react'; import { useTranslation } from 'react-i18next'; import ArrowDown from '@/assets/images/arrow-down.svg'; import ArrowUp from '@/assets/images/arrow-up.svg'; -import Tip from '@/assets/images/tip.svg'; import Card from '@/components/Card'; -import Tooltip from '@/components/Tooltip'; +import ToggleTipButton from '@/components/ToggleTipButton'; import { formatNumberWithComma } from '@/utilities/number'; import * as styles from './Block.module.scss'; @@ -17,32 +15,28 @@ type Props = { count: number; delta?: number; title: AdminConsoleKey; - tooltip?: AdminConsoleKey; + tip?: AdminConsoleKey; variant?: 'bordered' | 'default' | 'plain'; }; -const Block = ({ variant = 'default', count, delta, title, tooltip }: Props) => { +const Block = ({ variant = 'default', count, delta, title, tip }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const tipRef = useRef(null); - const deltaLable = delta !== undefined && `${conditionalString(delta >= 0 && '+')}${delta}`; + const deltaLabel = delta !== undefined && `${conditionalString(delta >= 0 && '+')}${delta}`; return (
{t(title)} - {tooltip && ( -
- - -
+ {tip && ( +
{t(tip)}
} /> )}
{formatNumberWithComma(count)}
{delta !== undefined && (
- ({deltaLable}) + ({deltaLabel}) {delta > 0 && } {delta < 0 && }
diff --git a/packages/console/src/pages/Dashboard/index.tsx b/packages/console/src/pages/Dashboard/index.tsx index 4d4c8500b..4c0519a7e 100644 --- a/packages/console/src/pages/Dashboard/index.tsx +++ b/packages/console/src/pages/Dashboard/index.tsx @@ -69,18 +69,18 @@ const Dashboard = () => {
@@ -88,7 +88,7 @@ const Dashboard = () => { {
{ From bbf54216fea05a6c2608801f9c54c70bf7fd8660 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Tue, 6 Dec 2022 16:42:41 +0800 Subject: [PATCH 123/166] feat(console): sort connectors (#2587) --- .../Connectors/components/CreateForm/constants.ts | 12 ++++++++++++ .../pages/Connectors/components/CreateForm/index.tsx | 10 +++++++++- .../pages/Connectors/components/CreateForm/utils.ts | 12 ++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 packages/console/src/pages/Connectors/components/CreateForm/constants.ts create mode 100644 packages/console/src/pages/Connectors/components/CreateForm/utils.ts diff --git a/packages/console/src/pages/Connectors/components/CreateForm/constants.ts b/packages/console/src/pages/Connectors/components/CreateForm/constants.ts new file mode 100644 index 000000000..6932c6f96 --- /dev/null +++ b/packages/console/src/pages/Connectors/components/CreateForm/constants.ts @@ -0,0 +1,12 @@ +export const featuredConnectorTargets = [ + 'google', + 'apple', + 'facebook', + 'github', + 'discord', + 'wechat', + 'alipay', + 'kakao', + 'naver', + 'azuread', +]; diff --git a/packages/console/src/pages/Connectors/components/CreateForm/index.tsx b/packages/console/src/pages/Connectors/components/CreateForm/index.tsx index 111b63f52..fa127654b 100644 --- a/packages/console/src/pages/Connectors/components/CreateForm/index.tsx +++ b/packages/console/src/pages/Connectors/components/CreateForm/index.tsx @@ -16,6 +16,7 @@ import { getConnectorGroups } from '../../utils'; import Guide from '../Guide'; import PlatformSelector from './PlatformSelector'; import * as styles from './index.module.scss'; +import { getConnectorOrder } from './utils'; type Props = { isOpen: boolean; @@ -56,7 +57,14 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => { existingConnectors.some(({ connectorId }) => connector.id === connectorId), })), })) - .filter(({ connectors }) => !connectors.every(({ added }) => added)); + .filter(({ connectors }) => !connectors.every(({ added }) => added)) + .slice() + .sort((connectorA, connectorB) => { + const orderA = getConnectorOrder(connectorA.target, connectorA.isStandard); + const orderB = getConnectorOrder(connectorB.target, connectorB.isStandard); + + return orderA - orderB; + }); }, [factories, type, existingConnectors]); const activeGroup = useMemo( diff --git a/packages/console/src/pages/Connectors/components/CreateForm/utils.ts b/packages/console/src/pages/Connectors/components/CreateForm/utils.ts new file mode 100644 index 000000000..4c44e3002 --- /dev/null +++ b/packages/console/src/pages/Connectors/components/CreateForm/utils.ts @@ -0,0 +1,12 @@ +import { featuredConnectorTargets } from './constants'; + +export const getConnectorOrder = (target: string, isStandard?: boolean): number => { + const order = featuredConnectorTargets.indexOf(target); + + if (order === -1) { + // Standard connectors come last. + return isStandard ? featuredConnectorTargets.length + 1 : featuredConnectorTargets.length; + } + + return order; +}; From 121a240b1f62a87e68f392504a9d3e40a000f430 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Tue, 6 Dec 2022 17:00:42 +0800 Subject: [PATCH 124/166] fix(console): error state for connector form (#2594) --- .../Connectors/components/ConnectorForm/index.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx b/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx index c6614a1db..00dd36a34 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx +++ b/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx @@ -20,7 +20,11 @@ type Props = { const ConnectorForm = ({ connector }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { configTemplate, isStandard } = connector; - const { control, register } = useFormContext(); + const { + control, + register, + formState: { errors }, + } = useFormContext(); const [darkVisible, setDarkVisible] = useState(false); const toggleDarkVisible = () => { @@ -45,6 +49,7 @@ const ConnectorForm = ({ connector }: Props) => {
{t('connectors.guide.name_tip')}
@@ -76,7 +81,10 @@ const ConnectorForm = ({ connector }: Props) => { onClick={toggleDarkVisible} /> - +
{t('connectors.guide.target_tip')}
From 5d30810eb8c7b82c09b71b08a10c0bdcfa1b8f73 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Tue, 6 Dec 2022 17:00:51 +0800 Subject: [PATCH 125/166] feat(console): add trailing icon to dark visible toggle button (#2595) --- packages/console/src/assets/images/caret-down.svg | 3 +++ packages/console/src/assets/images/caret-up.svg | 3 +++ packages/console/src/components/Button/index.module.scss | 6 ++++++ packages/console/src/components/Button/index.tsx | 3 +++ .../src/pages/Connectors/components/ConnectorForm/index.tsx | 3 +++ 5 files changed, 18 insertions(+) create mode 100644 packages/console/src/assets/images/caret-down.svg create mode 100644 packages/console/src/assets/images/caret-up.svg diff --git a/packages/console/src/assets/images/caret-down.svg b/packages/console/src/assets/images/caret-down.svg new file mode 100644 index 000000000..a485a9313 --- /dev/null +++ b/packages/console/src/assets/images/caret-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/console/src/assets/images/caret-up.svg b/packages/console/src/assets/images/caret-up.svg new file mode 100644 index 000000000..9f31eabf2 --- /dev/null +++ b/packages/console/src/assets/images/caret-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/console/src/components/Button/index.module.scss b/packages/console/src/components/Button/index.module.scss index 61820ff09..d0a2c533e 100644 --- a/packages/console/src/components/Button/index.module.scss +++ b/packages/console/src/components/Button/index.module.scss @@ -49,6 +49,12 @@ } } + .trailingIcon { + display: block; + width: 16px; + height: 16px; + } + &.small { height: 30px; padding: 0 _.unit(3); diff --git a/packages/console/src/components/Button/index.tsx b/packages/console/src/components/Button/index.tsx index 3f52dc82e..64a15fc85 100644 --- a/packages/console/src/components/Button/index.tsx +++ b/packages/console/src/components/Button/index.tsx @@ -17,6 +17,7 @@ type BaseProps = Omit, 'type' | 'size' | 'title'> & size?: 'small' | 'medium' | 'large'; isLoading?: boolean; loadingDelay?: number; + trailingIcon?: ReactNode; }; type TitleButtonProps = BaseProps & { @@ -41,6 +42,7 @@ const Button = ({ isLoading = false, loadingDelay = 500, onClick, + trailingIcon, ...rest }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); @@ -84,6 +86,7 @@ const Button = ({ {showSpinner && } {icon && {icon}} {title && (typeof title === 'string' ? {t(title)} : title)} + {trailingIcon && {trailingIcon}} ); }; diff --git a/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx b/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx index 00dd36a34..3f4774a5a 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx +++ b/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx @@ -3,6 +3,8 @@ import { useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import CaretDown from '@/assets/images/caret-down.svg'; +import CaretUp from '@/assets/images/caret-up.svg'; import Button from '@/components/Button'; import CodeEditor from '@/components/CodeEditor'; import FormField from '@/components/FormField'; @@ -78,6 +80,7 @@ const ConnectorForm = ({ connector }: Props) => { ? 'connectors.guide.logo_dark_collapse' : 'connectors.guide.logo_dark_show' } + trailingIcon={darkVisible ? : } onClick={toggleDarkVisible} /> From 57234cee660dd732e6f9ef594787c9ed60185ee8 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 6 Dec 2022 17:04:36 +0800 Subject: [PATCH 126/166] fix: drop empty string, undefined and null fields of connector configurable metadata (#2576) --- packages/core/src/routes/connector.test.ts | 2 + packages/core/src/routes/connector.ts | 14 ++++++- .../core/src/routes/connector.update.test.ts | 40 ++++++++++++++++++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/core/src/routes/connector.test.ts b/packages/core/src/routes/connector.test.ts index 1b96b0bac..6ef17d060 100644 --- a/packages/core/src/routes/connector.test.ts +++ b/packages/core/src/routes/connector.test.ts @@ -244,6 +244,7 @@ describe('connector route', () => { const response = await connectorRequest.post('/connectors').send({ connectorId: 'id0', config: { cliend_id: 'client_id', client_secret: 'client_secret' }, + metadata: { target: 'target', name: { en: '' }, logo: '', logoDark: null }, }); expect(response).toHaveProperty('statusCode', 200); expect(response.body).toMatchObject( @@ -253,6 +254,7 @@ describe('connector route', () => { cliend_id: 'client_id', client_secret: 'client_secret', }, + metadata: { target: 'target' }, }) ); expect(deleteConnectorByIds).toHaveBeenCalledWith(['id']); diff --git a/packages/core/src/routes/connector.ts b/packages/core/src/routes/connector.ts index 995919e74..673448a3a 100644 --- a/packages/core/src/routes/connector.ts +++ b/packages/core/src/routes/connector.ts @@ -3,6 +3,8 @@ import { emailRegEx, phoneRegEx } from '@logto/core-kit'; import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas'; import { arbitraryObjectGuard, Connectors, ConnectorType } from '@logto/schemas'; import { buildIdGenerator } from '@logto/shared'; +import { conditional } from '@silverhand/essentials'; +import cleanDeep from 'clean-deep'; import { object, string } from 'zod'; import { @@ -134,9 +136,12 @@ export default function connectorRoutes(router: T) { ); const insertConnectorId = generateConnectorId(); + const { metadata, ...rest } = body; + ctx.body = await insertConnector({ id: insertConnectorId, - ...body, + ...conditional(metadata && { metadata: cleanDeep(metadata) }), + ...rest, }); /** @@ -193,7 +198,12 @@ export default function connectorRoutes(router: T) { validateConfig(config); } - await updateConnector({ set: body, where: { id }, jsonbMode: 'replace' }); + const { metadata: databaseMetadata, ...rest } = body; + await updateConnector({ + set: databaseMetadata ? { metadata: cleanDeep(databaseMetadata), ...rest } : rest, + where: { id }, + jsonbMode: 'replace', + }); const connector = await getLogtoConnectorById(id); ctx.body = transpileLogtoConnector(connector); diff --git a/packages/core/src/routes/connector.update.test.ts b/packages/core/src/routes/connector.update.test.ts index ee0b82f71..e20d8a4fb 100644 --- a/packages/core/src/routes/connector.update.test.ts +++ b/packages/core/src/routes/connector.update.test.ts @@ -105,7 +105,6 @@ describe('connector PATCH routes', () => { target: 'target', name: { en: 'connector_name', fr: 'connector_name' }, logo: 'new_logo.png', - logoDark: null, }, }); const response = await connectorRequest.patch('/connectors/id').send({ @@ -126,7 +125,6 @@ describe('connector PATCH routes', () => { target: 'target', name: { en: 'connector_name', fr: 'connector_name' }, logo: 'new_logo.png', - logoDark: null, }, }, jsonbMode: 'replace', @@ -135,6 +133,44 @@ describe('connector PATCH routes', () => { expect(response).toHaveProperty('statusCode', 200); }); + it('successfully clear connector config metadata', async () => { + getLogtoConnectorsPlaceholder.mockResolvedValueOnce([ + { + dbEntry: mockConnector, + metadata: { ...mockMetadata, isStandard: true }, + type: ConnectorType.Social, + ...mockLogtoConnector, + }, + ]); + mockedUpdateConnector.mockResolvedValueOnce({ + ...mockConnector, + metadata: { + target: '', + name: { en: '' }, + logo: '', + logoDark: '', + }, + }); + const response = await connectorRequest.patch('/connectors/id').send({ + metadata: { + target: '', + name: { en: '' }, + logo: '', + logoDark: '', + }, + }); + expect(updateConnector).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: 'id' }, + set: { + metadata: {}, + }, + jsonbMode: 'replace', + }) + ); + expect(response).toHaveProperty('statusCode', 200); + }); + it('throws when set syncProfile to `true` and with non-social connector', async () => { getLogtoConnectorsPlaceholder.mockResolvedValueOnce([ { From dd2a33f921782674c2bbc86b446f51aa039057f6 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 6 Dec 2022 17:04:57 +0800 Subject: [PATCH 127/166] chore: add IT for switching passwordless connector (#2591) --- .github/workflows/integration-test.yml | 2 +- .../src/__mocks__/connectors-mock.ts | 39 +++++++++++++++++++ .../tests/api/admin-user.test.ts | 3 ++ .../tests/api/connector.test.ts | 27 +++++++++++-- .../tests/api/social-session.test.ts | 8 ++++ 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 787e41aab..578674a77 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -84,7 +84,7 @@ jobs: - name: Add mock connectors working-directory: tests run: | - npm run cli connector add @logto/connector-mock-sms @logto/connector-mock-email @logto/connector-mock-social -- -p ../logto + npm run cli connector add @logto/connector-mock-sms @logto/connector-mock-email @logto/connector-mock-standard-email @logto/connector-mock-social -- -p ../logto - name: Run Logto working-directory: logto/ diff --git a/packages/integration-tests/src/__mocks__/connectors-mock.ts b/packages/integration-tests/src/__mocks__/connectors-mock.ts index 049e65ea5..a27714899 100644 --- a/packages/integration-tests/src/__mocks__/connectors-mock.ts +++ b/packages/integration-tests/src/__mocks__/connectors-mock.ts @@ -140,6 +140,45 @@ export const mockEmailConnectorConfig = { ], }; +export const mockStandardEmailConnectorId = 'mock-standard-email-service'; +export const mockStandardEmailConnectorConfig = { + apiKey: 'api-key-value', + fromEmail: 'noreply@logto.test.io', + fromName: 'from-name-value', + templates: [ + { + usageType: 'SignIn', + type: 'text/plain', + subject: 'Logto SignIn Template', + content: 'This is for sign-in purposes only. Your passcode is {{code}}.', + }, + { + usageType: 'Register', + type: 'text/plain', + subject: 'Logto Register Template', + content: 'This is for registering purposes only. Your passcode is {{code}}.', + }, + { + usageType: 'ForgotPassword', + type: 'text/plain', + subject: 'Logto Forgot Password Template', + content: 'This is for forgot-password purposes only. Your passcode is {{code}}.', + }, + { + usageType: 'Continue', + type: 'text/plain', + subject: 'Logto Continue Template', + content: 'This is for completing user profile purposes only. Your passcode is {{code}}.', + }, + { + usageType: 'Test', + type: 'text/plain', + subject: 'Logto Test Template', + content: 'This is for testing purposes only. Your passcode is {{code}}.', + }, + ], +}; + export const mockSocialConnectorId = 'mock-social-connector'; export const mockSocialConnectorTarget = 'mock-social'; export const mockSocialConnectorConfig = { diff --git a/packages/integration-tests/tests/api/admin-user.test.ts b/packages/integration-tests/tests/api/admin-user.test.ts index fbfd06b04..266ee2005 100644 --- a/packages/integration-tests/tests/api/admin-user.test.ts +++ b/packages/integration-tests/tests/api/admin-user.test.ts @@ -14,6 +14,7 @@ import { deleteUserIdentity, postConnector, updateConnectorConfig, + deleteConnectorById, } from '@/api'; import { createUserByAdmin, bindSocialToNewCreatedUser } from '@/helpers'; @@ -81,5 +82,7 @@ describe('admin console user management', () => { const updatedUser = await getUser(createdUserId); expect(updatedUser.identities).not.toHaveProperty(mockSocialConnectorTarget); + + await deleteConnectorById(id); }); }); diff --git a/packages/integration-tests/tests/api/connector.test.ts b/packages/integration-tests/tests/api/connector.test.ts index 9f777f8bc..ec5d65aca 100644 --- a/packages/integration-tests/tests/api/connector.test.ts +++ b/packages/integration-tests/tests/api/connector.test.ts @@ -7,6 +7,8 @@ import { mockSmsConnectorId, mockSocialConnectorConfig, mockSocialConnectorId, + mockStandardEmailConnectorConfig, + mockStandardEmailConnectorId, } from '@/__mocks__/connectors-mock'; import { deleteConnectorById, @@ -69,15 +71,29 @@ test('connector set-up flow', async () => { /* * Change to another SMS/Email connector */ - // FIXME @Darcy [LOG-4750,4751]: complete this IT after add another mock sms/email connector (or other current existing connector could be affected) + const { id } = await postConnector(mockStandardEmailConnectorId); + await updateConnectorConfig(id, mockStandardEmailConnectorConfig); + connectorIdMap.set(mockStandardEmailConnectorId, id); + const currentConnectors = await listConnectors(); + expect( + currentConnectors.some((connector) => connector.connectorId === mockEmailConnectorId) + ).toBeFalsy(); + expect( + currentConnectors.some((connector) => connector.connectorId === mockStandardEmailConnectorId) + ).toBeTruthy(); + expect( + currentConnectors.find((connector) => connector.connectorId === mockStandardEmailConnectorId) + ?.config + ).toEqual(mockStandardEmailConnectorConfig); + connectorIdMap.delete(mockEmailConnectorId); /* * Delete (i.e. disable) a connector */ await expect( - deleteConnectorById(connectorIdMap.get(mockEmailConnectorId)) + deleteConnectorById(connectorIdMap.get(mockStandardEmailConnectorId)) ).resolves.not.toThrow(); - connectorIdMap.delete(mockEmailConnectorId); + connectorIdMap.delete(mockStandardEmailConnectorId); /** * List connectors after manually setting up connectors. @@ -130,4 +146,9 @@ test('send SMS/email test message', async () => { ).resolves.not.toThrow(); await expect(sendSmsTestMessage(mockSmsConnectorId, phone, {})).rejects.toThrow(HTTPError); await expect(sendEmailTestMessage(mockEmailConnectorId, email, {})).rejects.toThrow(HTTPError); + + for (const [_connectorId, id] of connectorIdMap.entries()) { + // eslint-disable-next-line no-await-in-loop + await deleteConnectorById(id); + } }); diff --git a/packages/integration-tests/tests/api/social-session.test.ts b/packages/integration-tests/tests/api/social-session.test.ts index cacdde01c..4c5bf484b 100644 --- a/packages/integration-tests/tests/api/social-session.test.ts +++ b/packages/integration-tests/tests/api/social-session.test.ts @@ -15,6 +15,7 @@ import { getUser, postConnector, updateConnectorConfig, + deleteConnectorById, } from '@/api'; import MockClient from '@/client'; import { signUpIdentifiers } from '@/constants'; @@ -112,6 +113,13 @@ describe('social bind account', () => { await createUserByAdmin(username, password); }); + afterAll(async () => { + for (const [_connectorId, id] of connectorIdMap.entries()) { + // eslint-disable-next-line no-await-in-loop + await deleteConnectorById(id); + } + }); + it('bind new social account', async () => { const client = new MockClient(); From 6d76a5e169d521e31eb0599e4dc4c48892e8c1b0 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 6 Dec 2022 17:12:58 +0800 Subject: [PATCH 128/166] Revert "style(console): skeletons should not overlap the table header" (#2597) --- packages/console/src/scss/table.module.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/console/src/scss/table.module.scss b/packages/console/src/scss/table.module.scss index 10582f0ef..16f1737df 100644 --- a/packages/console/src/scss/table.module.scss +++ b/packages/console/src/scss/table.module.scss @@ -27,7 +27,6 @@ tr.clickable { background: var(--color-layer-1); position: sticky; top: 0; - z-index: 1; } tbody { From a64816422ed75bd260179689b01394b1f213f4b3 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Tue, 6 Dec 2022 18:03:29 +0800 Subject: [PATCH 129/166] feat(core,console): prevent target change after created (#2589) --- .../components/ConnectorForm/index.tsx | 4 +++- .../Connectors/components/Guide/index.tsx | 2 +- packages/core/src/routes/connector.ts | 2 ++ .../core/src/routes/connector.update.test.ts | 20 ++++++++++++++++--- packages/phrases/src/locales/de/errors.ts | 1 + packages/phrases/src/locales/en/errors.ts | 1 + packages/phrases/src/locales/fr/errors.ts | 1 + packages/phrases/src/locales/ko/errors.ts | 1 + packages/phrases/src/locales/pt-pt/errors.ts | 1 + packages/phrases/src/locales/tr-tr/errors.ts | 1 + packages/phrases/src/locales/zh-cn/errors.ts | 1 + 11 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx b/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx index 3f4774a5a..16f8d9790 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx +++ b/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx @@ -17,9 +17,10 @@ import * as styles from './index.module.scss'; type Props = { connector: ConnectorFactoryResponse; + isAllowEditTarget?: boolean; }; -const ConnectorForm = ({ connector }: Props) => { +const ConnectorForm = ({ connector, isAllowEditTarget }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { configTemplate, isStandard } = connector; const { @@ -86,6 +87,7 @@ const ConnectorForm = ({ connector }: Props) => {
{t('connectors.guide.target_tip')}
diff --git a/packages/console/src/pages/Connectors/components/Guide/index.tsx b/packages/console/src/pages/Connectors/components/Guide/index.tsx index bb652a9b3..ef7f791a0 100644 --- a/packages/console/src/pages/Connectors/components/Guide/index.tsx +++ b/packages/console/src/pages/Connectors/components/Guide/index.tsx @@ -109,7 +109,7 @@ const Guide = ({ connector, onClose }: Props) => {
{t('connectors.guide.connector_setting')}
- + {!isSocialConnector && ( (router: T) { if (config) { validateConfig(config); } + // Once created, target can not be modified. + assertThat(body.metadata?.target === undefined, 'connector.can_not_modify_target'); const { metadata: databaseMetadata, ...rest } = body; await updateConnector({ diff --git a/packages/core/src/routes/connector.update.test.ts b/packages/core/src/routes/connector.update.test.ts index e20d8a4fb..06eb2599e 100644 --- a/packages/core/src/routes/connector.update.test.ts +++ b/packages/core/src/routes/connector.update.test.ts @@ -90,6 +90,23 @@ describe('connector PATCH routes', () => { expect(response).toHaveProperty('statusCode', 500); }); + it('throws when trying to update target', async () => { + getLogtoConnectorsPlaceholder.mockResolvedValue([ + { + dbEntry: mockConnector, + metadata: { ...mockMetadata, isStandard: true }, + type: ConnectorType.Social, + ...mockLogtoConnector, + }, + ]); + const response = await connectorRequest.patch('/connectors/id').send({ + metadata: { + target: 'target', + }, + }); + expect(response).toHaveProperty('statusCode', 400); + }); + it('successfully updates connector configs', async () => { getLogtoConnectorsPlaceholder.mockResolvedValue([ { @@ -110,7 +127,6 @@ describe('connector PATCH routes', () => { const response = await connectorRequest.patch('/connectors/id').send({ config: { cliend_id: 'client_id', client_secret: 'client_secret' }, metadata: { - target: 'target', name: { en: 'connector_name', fr: 'connector_name' }, logo: 'new_logo.png', logoDark: null, @@ -122,7 +138,6 @@ describe('connector PATCH routes', () => { set: { config: { cliend_id: 'client_id', client_secret: 'client_secret' }, metadata: { - target: 'target', name: { en: 'connector_name', fr: 'connector_name' }, logo: 'new_logo.png', }, @@ -153,7 +168,6 @@ describe('connector PATCH routes', () => { }); const response = await connectorRequest.patch('/connectors/id').send({ metadata: { - target: '', name: { en: '' }, logo: '', logoDark: '', diff --git a/packages/phrases/src/locales/de/errors.ts b/packages/phrases/src/locales/de/errors.ts index bcd6bd116..256d739fa 100644 --- a/packages/phrases/src/locales/de/errors.ts +++ b/packages/phrases/src/locales/de/errors.ts @@ -105,6 +105,7 @@ const errors = { multiple_instances_not_supported: 'Can not create multiple instance with picked standard connector.', invalid_type_for_syncing_profile: 'You can only sync user profile with social connectors.', + can_not_modify_target: 'The connector target can not be modified.', }, passcode: { phone_email_empty: 'Telefonnummer oder E-Mail darf nicht leer sein.', diff --git a/packages/phrases/src/locales/en/errors.ts b/packages/phrases/src/locales/en/errors.ts index 0dd04b266..8b46f65a6 100644 --- a/packages/phrases/src/locales/en/errors.ts +++ b/packages/phrases/src/locales/en/errors.ts @@ -104,6 +104,7 @@ const errors = { multiple_instances_not_supported: 'Can not create multiple instance with picked standard connector.', invalid_type_for_syncing_profile: 'You can only sync user profile with social connectors.', + can_not_modify_target: 'The connector target can not be modified.', }, passcode: { phone_email_empty: 'Both phone and email are empty.', diff --git a/packages/phrases/src/locales/fr/errors.ts b/packages/phrases/src/locales/fr/errors.ts index ab6bd6eeb..2849fddfa 100644 --- a/packages/phrases/src/locales/fr/errors.ts +++ b/packages/phrases/src/locales/fr/errors.ts @@ -111,6 +111,7 @@ const errors = { multiple_instances_not_supported: 'Can not create multiple instance with picked standard connector.', // UNTRANSLATED invalid_type_for_syncing_profile: 'You can only sync user profile with social connectors.', // UNTRANSLATED + can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED }, passcode: { phone_email_empty: "Le téléphone et l'email sont vides.", diff --git a/packages/phrases/src/locales/ko/errors.ts b/packages/phrases/src/locales/ko/errors.ts index 67b4c21f8..e85eb243e 100644 --- a/packages/phrases/src/locales/ko/errors.ts +++ b/packages/phrases/src/locales/ko/errors.ts @@ -103,6 +103,7 @@ const errors = { multiple_instances_not_supported: 'Can not create multiple instance with picked standard connector.', // UNTRANSLATED invalid_type_for_syncing_profile: 'You can only sync user profile with social connectors.', // UNTRANSLATED + can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED }, passcode: { phone_email_empty: '휴대전화번호 그리고 이메일이 비어있어요.', diff --git a/packages/phrases/src/locales/pt-pt/errors.ts b/packages/phrases/src/locales/pt-pt/errors.ts index b1ea0d7bd..26feaaa5d 100644 --- a/packages/phrases/src/locales/pt-pt/errors.ts +++ b/packages/phrases/src/locales/pt-pt/errors.ts @@ -106,6 +106,7 @@ const errors = { multiple_instances_not_supported: 'Can not create multiple instance with picked standard connector.', // UNTRANSLATED invalid_type_for_syncing_profile: 'You can only sync user profile with social connectors.', // UNTRANSLATED + can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED }, passcode: { phone_email_empty: 'O campos telefone e email estão vazios.', diff --git a/packages/phrases/src/locales/tr-tr/errors.ts b/packages/phrases/src/locales/tr-tr/errors.ts index 0912454b3..b2aa56886 100644 --- a/packages/phrases/src/locales/tr-tr/errors.ts +++ b/packages/phrases/src/locales/tr-tr/errors.ts @@ -105,6 +105,7 @@ const errors = { multiple_instances_not_supported: 'Can not create multiple instance with picked standard connector.', // UNTRANSLATED invalid_type_for_syncing_profile: 'You can only sync user profile with social connectors.', // UNTRANSLATED + can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED }, passcode: { phone_email_empty: 'Hem telefon hem de e-posta adresi yok.', diff --git a/packages/phrases/src/locales/zh-cn/errors.ts b/packages/phrases/src/locales/zh-cn/errors.ts index 5d0ad7a7c..dc2518549 100644 --- a/packages/phrases/src/locales/zh-cn/errors.ts +++ b/packages/phrases/src/locales/zh-cn/errors.ts @@ -100,6 +100,7 @@ const errors = { not_found_with_connector_id: '找不到所给 connector id 对应的连接器', multiple_instances_not_supported: '你选择的连接器不支持创建多实例。', invalid_type_for_syncing_profile: '只有社交连接器可以开启用户档案同步。', + can_not_modify_target: '不可修改连接器 target。', }, passcode: { phone_email_empty: '手机号与邮箱地址均为空', From a00782de298ba8724d8353b44a5466c815d4176d Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 7 Dec 2022 11:28:32 +0800 Subject: [PATCH 130/166] refactor(console): align text link style (#2599) --- .../src/components/Alert/index.module.scss | 7 ---- .../console/src/components/Alert/index.tsx | 11 +++-- .../src/components/FormCard/index.module.scss | 4 +- .../console/src/components/FormCard/index.tsx | 5 ++- .../src/components/LinkButton/index.tsx | 28 ------------- .../src/components/Markdown/index.module.scss | 10 ----- .../index.module.scss | 23 ++++++----- .../console/src/components/TextLink/index.tsx | 41 +++++++++++++++++++ .../src/mdx-components/Step/index.module.scss | 10 ----- .../ApiResourceDetails/index.module.scss | 1 + .../src/pages/ApiResourceDetails/index.tsx | 11 ++--- .../ApplicationDetails/index.module.scss | 1 + .../src/pages/ApplicationDetails/index.tsx | 11 ++--- .../Applications/components/Guide/index.tsx | 5 ++- .../pages/AuditLogDetails/index.module.scss | 1 + .../src/pages/AuditLogDetails/index.tsx | 17 ++++---- .../pages/ConnectorDetails/index.module.scss | 1 + .../src/pages/ConnectorDetails/index.tsx | 9 ++-- .../ConnectorStatusField/index.module.scss | 7 +++- .../components/ConnectorStatusField/index.tsx | 5 ++- .../index.module.scss | 5 --- .../SignInExperienceSetupNotice/index.tsx | 4 +- .../components/ConnectorSetupWarning.tsx | 27 ++++++++---- .../SocialConnectorEditBox/index.module.scss | 5 +-- .../SocialConnectorEditBox/index.tsx | 8 ++-- .../src/pages/UserDetails/index.module.scss | 1 + .../console/src/pages/UserDetails/index.tsx | 11 ++--- .../translation/admin-console/sign-in-exp.ts | 7 ++-- .../translation/admin-console/sign-in-exp.ts | 7 ++-- .../translation/admin-console/sign-in-exp.ts | 9 ++-- .../translation/admin-console/sign-in-exp.ts | 7 ++-- .../translation/admin-console/sign-in-exp.ts | 7 ++-- .../translation/admin-console/sign-in-exp.ts | 7 ++-- .../translation/admin-console/sign-in-exp.ts | 10 +++-- 34 files changed, 160 insertions(+), 163 deletions(-) delete mode 100644 packages/console/src/components/LinkButton/index.tsx rename packages/console/src/components/{LinkButton => TextLink}/index.module.scss (58%) create mode 100644 packages/console/src/components/TextLink/index.tsx diff --git a/packages/console/src/components/Alert/index.module.scss b/packages/console/src/components/Alert/index.module.scss index 5c5e0bffd..3c33382d1 100644 --- a/packages/console/src/components/Alert/index.module.scss +++ b/packages/console/src/components/Alert/index.module.scss @@ -24,13 +24,6 @@ margin: 0 _.unit(3); } - .action { - a { - color: var(--color-text); - text-decoration: underline; - } - } - &.info { background: var(--color-surface-variant); diff --git a/packages/console/src/components/Alert/index.tsx b/packages/console/src/components/Alert/index.tsx index 7b2a4c465..f693e3ace 100644 --- a/packages/console/src/components/Alert/index.tsx +++ b/packages/console/src/components/Alert/index.tsx @@ -1,11 +1,12 @@ import type { AdminConsoleKey } from '@logto/phrases'; import classNames from 'classnames'; import type { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; import Info from '@/assets/images/info.svg'; -import LinkButton from '@/components/LinkButton'; import Button from '../Button'; +import TextLink from '../TextLink'; import * as styles from './index.module.scss'; type Props = { @@ -27,17 +28,15 @@ const Alert = ({ variant = 'plain', className, }: Props) => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + return (
{children}
- {action && href && ( -
- -
- )} + {action && href && {t(action)}} {action && onClick && (
); }; diff --git a/packages/console/src/components/TipBubble/index.module.scss b/packages/console/src/components/Tip/TipBubble/index.module.scss similarity index 100% rename from packages/console/src/components/TipBubble/index.module.scss rename to packages/console/src/components/Tip/TipBubble/index.module.scss diff --git a/packages/console/src/components/TipBubble/index.tsx b/packages/console/src/components/Tip/TipBubble/index.tsx similarity index 100% rename from packages/console/src/components/TipBubble/index.tsx rename to packages/console/src/components/Tip/TipBubble/index.tsx diff --git a/packages/console/src/components/TipBubble/utils.ts b/packages/console/src/components/Tip/TipBubble/utils.ts similarity index 100% rename from packages/console/src/components/TipBubble/utils.ts rename to packages/console/src/components/Tip/TipBubble/utils.ts diff --git a/packages/console/src/components/ToggleTip/index.module.scss b/packages/console/src/components/Tip/ToggleTip/index.module.scss similarity index 100% rename from packages/console/src/components/ToggleTip/index.module.scss rename to packages/console/src/components/Tip/ToggleTip/index.module.scss diff --git a/packages/console/src/components/Tip/ToggleTip/index.tsx b/packages/console/src/components/Tip/ToggleTip/index.tsx new file mode 100644 index 000000000..ca442b952 --- /dev/null +++ b/packages/console/src/components/Tip/ToggleTip/index.tsx @@ -0,0 +1,104 @@ +import type { ReactNode } from 'react'; +import { useCallback, useState, useRef } from 'react'; +import ReactModal from 'react-modal'; + +import type { HorizontalAlignment } from '@/hooks/use-position'; +import usePosition from '@/hooks/use-position'; +import { onKeyDownHandler } from '@/utilities/a11y'; + +import type { TipBubblePosition } from '../TipBubble'; +import TipBubble from '../TipBubble'; +import { + getVerticalAlignment, + getHorizontalAlignment, + getVerticalOffset, + getHorizontalOffset, +} from '../TipBubble/utils'; +import * as styles from './index.module.scss'; + +type Props = { + children: ReactNode; + className?: string; + anchorClassName?: string; + position?: TipBubblePosition; + horizontalAlign?: HorizontalAlignment; + content?: ((closeTip: () => void) => ReactNode) | ReactNode; +}; + +const ToggleTip = ({ + children, + className, + anchorClassName, + position = 'top', + horizontalAlign = 'center', + content, +}: Props) => { + const overlayRef = useRef(null); + const anchorRef = useRef(null); + + const [isOpen, setIsOpen] = useState(false); + + const onClose = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + + const { + position: layoutPosition, + positionState, + mutate, + } = usePosition({ + verticalAlign: getVerticalAlignment(position), + horizontalAlign: getHorizontalAlignment(position, horizontalAlign), + offset: { + vertical: getVerticalOffset(position), + horizontal: getHorizontalOffset(position, horizontalAlign), + }, + anchorRef, + overlayRef, + }); + + return ( + <> +
{ + setIsOpen(true); + }} + onKeyDown={onKeyDownHandler(() => { + setIsOpen(true); + })} + > + {children} +
+ + + {typeof content === 'function' ? content(onClose) : content} + + + + ); +}; + +export default ToggleTip; diff --git a/packages/console/src/components/Tooltip/index.module.scss b/packages/console/src/components/Tip/Tooltip/index.module.scss similarity index 100% rename from packages/console/src/components/Tooltip/index.module.scss rename to packages/console/src/components/Tip/Tooltip/index.module.scss diff --git a/packages/console/src/components/Tooltip/index.tsx b/packages/console/src/components/Tip/Tooltip/index.tsx similarity index 74% rename from packages/console/src/components/Tooltip/index.tsx rename to packages/console/src/components/Tip/Tooltip/index.tsx index 3dcc7f7cb..c9aa037dc 100644 --- a/packages/console/src/components/Tooltip/index.tsx +++ b/packages/console/src/components/Tip/Tooltip/index.tsx @@ -1,4 +1,4 @@ -import type { ReactNode, RefObject } from 'react'; +import type { ReactNode } from 'react'; import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; @@ -16,23 +16,26 @@ import { import * as styles from './index.module.scss'; type Props = { - content: ReactNode | Record; - anchorRef: RefObject; className?: string; isKeepOpen?: boolean; position?: TipBubblePosition; horizontalAlign?: HorizontalAlignment; + anchorClassName?: string; + children?: ReactNode; + content?: ReactNode; }; const Tooltip = ({ - content, - anchorRef, className, isKeepOpen = false, position = 'top', - horizontalAlign = 'start', + horizontalAlign = 'center', + anchorClassName, + children, + content, }: Props) => { const [tooltipDom, setTooltipDom] = useState(); + const anchorRef = useRef(null); const tooltipRef = useRef(null); const { @@ -119,25 +122,30 @@ const Tooltip = ({ useLayoutEffect(() => { mutate(); - }, [content, mutate]); + }, [mutate, content]); - if (!tooltipDom) { - return null; - } - - return createPortal( -
- -
{content}
-
-
, - tooltipDom + return ( + <> +
+ {children} +
+ {tooltipDom && + content && + createPortal( +
+ +
{content}
+
+
, + tooltipDom + )} + ); }; diff --git a/packages/console/src/components/Tip/index.ts b/packages/console/src/components/Tip/index.ts new file mode 100644 index 000000000..34c0f359d --- /dev/null +++ b/packages/console/src/components/Tip/index.ts @@ -0,0 +1,2 @@ +export { default as Tooltip } from './Tooltip'; +export { default as ToggleTip } from './ToggleTip'; diff --git a/packages/console/src/components/ToggleTip/index.tsx b/packages/console/src/components/ToggleTip/index.tsx deleted file mode 100644 index 1eba09dee..000000000 --- a/packages/console/src/components/ToggleTip/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import type { HTMLProps, ReactNode, RefObject } from 'react'; -import { useRef } from 'react'; -import ReactModal from 'react-modal'; - -import type { HorizontalAlignment } from '@/hooks/use-position'; -import usePosition from '@/hooks/use-position'; - -import type { TipBubblePosition } from '../TipBubble'; -import TipBubble from '../TipBubble'; -import { - getVerticalAlignment, - getHorizontalAlignment, - getVerticalOffset, - getHorizontalOffset, -} from '../TipBubble/utils'; -import * as styles from './index.module.scss'; - -type Props = HTMLProps & { - children: ReactNode; - isOpen: boolean; - onClose: () => void; - anchorRef: RefObject; - position?: TipBubblePosition; - horizontalAlign?: HorizontalAlignment; -}; - -const ToggleTip = ({ - children, - isOpen, - onClose, - anchorRef, - position = 'top', - horizontalAlign = 'start', -}: Props) => { - const overlayRef = useRef(null); - - const { - position: layoutPosition, - positionState, - mutate, - } = usePosition({ - verticalAlign: getVerticalAlignment(position), - horizontalAlign: getHorizontalAlignment(position, horizontalAlign), - offset: { - vertical: getVerticalOffset(position), - horizontal: getHorizontalOffset(position, horizontalAlign), - }, - anchorRef, - overlayRef, - }); - - return ( - - - {children} - - - ); -}; - -export default ToggleTip; diff --git a/packages/console/src/components/ToggleTipButton/index.module.scss b/packages/console/src/components/ToggleTipButton/index.module.scss deleted file mode 100644 index 1b432a460..000000000 --- a/packages/console/src/components/ToggleTipButton/index.module.scss +++ /dev/null @@ -1,19 +0,0 @@ -@use '@/scss/underscore' as _; - -.toggleTipButton { - border-radius: 4px; - padding: _.unit(1); - - .icon { - > svg { - display: block; - cursor: pointer; - width: 16px; - height: 16px; - } - } - - &:hover { - background: var(--color-hover); - } -} diff --git a/packages/console/src/components/ToggleTipButton/index.tsx b/packages/console/src/components/ToggleTipButton/index.tsx deleted file mode 100644 index 63cef14ad..000000000 --- a/packages/console/src/components/ToggleTipButton/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import classNames from 'classnames'; -import type { ReactElement } from 'react'; -import { useRef, useState } from 'react'; - -import Tip from '@/assets/images/tip.svg'; -import type { HorizontalAlignment } from '@/hooks/use-position'; -import { onKeyDownHandler } from '@/utilities/a11y'; - -import type { TipBubblePosition } from '../TipBubble'; -import ToggleTip from '../ToggleTip'; -import * as styles from './index.module.scss'; - -type Props = { - render: (closeTipHandler: () => void) => ReactElement; - className?: string; - tipPosition?: TipBubblePosition; - tipHorizontalAlignment?: HorizontalAlignment; -}; - -const ToggleTipButton = ({ render, className, tipPosition, tipHorizontalAlignment }: Props) => { - const anchorRef = useRef(null); - const [isTipOpen, setIsTipOpen] = useState(false); - - const closeTipHandler = () => { - setIsTipOpen(false); - }; - - return ( -
-
{ - setIsTipOpen(true); - }} - onKeyDown={onKeyDownHandler(() => { - setIsTipOpen(true); - })} - > - -
- { - setIsTipOpen(false); - }} - > - {render(closeTipHandler)} - -
- ); -}; - -export default ToggleTipButton; diff --git a/packages/console/src/pages/Applications/components/GuideHeader/index.module.scss b/packages/console/src/pages/Applications/components/GuideHeader/index.module.scss index b44ace2b3..449b08294 100644 --- a/packages/console/src/pages/Applications/components/GuideHeader/index.module.scss +++ b/packages/console/src/pages/Applications/components/GuideHeader/index.module.scss @@ -17,9 +17,11 @@ color: var(--color-text-secondary); } - .githubIcon { + .githubToolTipAnchor { margin-right: _.unit(4); + } + .githubIcon { div { display: flex; } diff --git a/packages/console/src/pages/Applications/components/GuideHeader/index.tsx b/packages/console/src/pages/Applications/components/GuideHeader/index.tsx index c9513f3b3..3ec1a9b32 100644 --- a/packages/console/src/pages/Applications/components/GuideHeader/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideHeader/index.tsx @@ -1,4 +1,3 @@ -import { useRef } from 'react'; import { useTranslation } from 'react-i18next'; import Close from '@/assets/images/close.svg'; @@ -8,7 +7,7 @@ import CardTitle from '@/components/CardTitle'; import DangerousRaw from '@/components/DangerousRaw'; import IconButton from '@/components/IconButton'; import Spacer from '@/components/Spacer'; -import Tooltip from '@/components/Tooltip'; +import Tooltip from '@/components/Tip/Tooltip'; import { SupportedSdk } from '@/types/applications'; import * as styles from './index.module.scss'; @@ -47,7 +46,6 @@ const getSampleProjectUrl = (sdk: SupportedSdk) => { const GuideHeader = ({ appName, selectedSdk, isCompact = false, onClose }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const tipRef = useRef(null); const onClickGetSample = () => { const sampleUrl = getSampleProjectUrl(selectedSdk); @@ -64,12 +62,15 @@ const GuideHeader = ({ appName, selectedSdk, isCompact = false, onClose }: Props subtitle="applications.guide.header_description" /> - -
+ + -
- -
+ + diff --git a/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx b/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx index 05fe5b335..7c94b5ace 100644 --- a/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx +++ b/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx @@ -1,7 +1,8 @@ import { phoneRegEx, emailRegEx } from '@logto/core-kit'; import { ConnectorType } from '@logto/schemas'; +import { conditional } from '@silverhand/essentials'; import classNames from 'classnames'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; @@ -9,7 +10,7 @@ import { useTranslation } from 'react-i18next'; import Button from '@/components/Button'; import FormField from '@/components/FormField'; import TextInput from '@/components/TextInput'; -import Tooltip from '@/components/Tooltip'; +import { Tooltip } from '@/components/Tip'; import useApi from '@/hooks/use-api'; import { safeParseJson } from '@/utilities/json'; @@ -27,7 +28,6 @@ type FormData = { }; const SenderTester = ({ connectorId, connectorType, config, className }: Props) => { - const buttonPosReference = useRef(null); const [showTooltip, setShowTooltip] = useState(false); const { handleSubmit, @@ -100,23 +100,19 @@ const SenderTester = ({ connectorId, connectorType, config, className }: Props) })} /> -
+
- {showTooltip && ( - - )} +
{inputError?.message ?? t('connector_details.test_sender_description')} diff --git a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss index cdf4727e3..f50e92fbb 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss +++ b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss @@ -5,7 +5,7 @@ align-items: center; .tipButton { - margin-left: _.unit(1); + margin-left: _.unit(0.5); } } diff --git a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx index b48102e2f..9096f950b 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx +++ b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx @@ -1,7 +1,9 @@ import { Trans, useTranslation } from 'react-i18next'; +import Tip from '@/assets/images/tip.svg'; +import IconButton from '@/components/IconButton'; import TextLink from '@/components/TextLink'; -import ToggleTipButton from '@/components/ToggleTipButton'; +import { ToggleTip } from '@/components/Tip'; import * as styles from './index.module.scss'; @@ -11,10 +13,9 @@ const ConnectorStatusField = () => { return (
{t('connectors.connector_status')} - ( + ( <>
{t('connectors.connector_status')}
@@ -37,7 +38,11 @@ const ConnectorStatusField = () => {
)} - /> + > + + + +
); }; diff --git a/packages/console/src/pages/Dashboard/components/Block.module.scss b/packages/console/src/pages/Dashboard/components/Block.module.scss index da98085a8..478c6846e 100644 --- a/packages/console/src/pages/Dashboard/components/Block.module.scss +++ b/packages/console/src/pages/Dashboard/components/Block.module.scss @@ -35,7 +35,7 @@ align-items: center; .toggleTipButton { - margin-left: _.unit(1); + margin-left: _.unit(0.5); } } diff --git a/packages/console/src/pages/Dashboard/components/Block.tsx b/packages/console/src/pages/Dashboard/components/Block.tsx index 84c911e25..78eb5e332 100644 --- a/packages/console/src/pages/Dashboard/components/Block.tsx +++ b/packages/console/src/pages/Dashboard/components/Block.tsx @@ -5,8 +5,10 @@ import { useTranslation } from 'react-i18next'; import ArrowDown from '@/assets/images/arrow-down.svg'; import ArrowUp from '@/assets/images/arrow-up.svg'; +import Tip from '@/assets/images/tip.svg'; import Card from '@/components/Card'; -import ToggleTipButton from '@/components/ToggleTipButton'; +import IconButton from '@/components/IconButton'; +import { ToggleTip } from '@/components/Tip'; import { formatNumberWithComma } from '@/utilities/number'; import * as styles from './Block.module.scss'; @@ -29,7 +31,11 @@ const Block = ({ variant = 'default', count, delta, title, tip }: Props) => {
{t(title)} {tip && ( -
{t(tip)}
} /> + {t(tip)}
}> + + + + )}
diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx index b1661efbe..2ef6f18ec 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx @@ -16,6 +16,7 @@ import Delete from '@/assets/images/delete.svg'; import Button from '@/components/Button'; import ConfirmModal from '@/components/ConfirmModal'; import IconButton from '@/components/IconButton'; +import { Tooltip } from '@/components/Tip'; import useApi, { RequestError } from '@/hooks/use-api'; import useUiLanguages from '@/hooks/use-ui-languages'; import { @@ -162,14 +163,15 @@ const LanguageDetails = () => { )}
{!isBuiltIn && ( - { - setIsDeletionAlertOpen(true); - }} - > - - + + { + setIsDeletionAlertOpen(true); + }} + > + + + )}
{ {t('sign_in_exp.others.manage_language.custom_values')} - { - for (const [key, value] of Object.entries( - flattenTranslation(emptyUiTranslation) - )) { - setValue(key, value, { shouldDirty: true }); - } - }} + - - + { + for (const [key, value] of Object.entries( + flattenTranslation(emptyUiTranslation) + )) { + setValue(key, value, { shouldDirty: true }); + } + }} + > + + + diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SignInMethodEditBox/SignInMethodItem.tsx b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SignInMethodEditBox/SignInMethodItem.tsx index bca249fdb..bceb6b4fd 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SignInMethodEditBox/SignInMethodItem.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SignInMethodEditBox/SignInMethodItem.tsx @@ -10,6 +10,7 @@ import Minus from '@/assets/images/minus.svg'; import SwitchArrowIcon from '@/assets/images/switch-arrow.svg'; import Checkbox from '@/components/Checkbox'; import IconButton from '@/components/IconButton'; +import { Tooltip } from '@/components/Tip'; import type { SignInMethod } from '@/pages/SignInExperience/types'; import ConnectorSetupWarning from '../ConnectorSetupWarning'; @@ -73,13 +74,14 @@ const SignInMethodItem = ({ /> {identifier !== SignInIdentifier.Username && ( <> - - - + + + +
- - - + + + +
{errorMessage &&
{errorMessage}
} From 07fae9a6fcc99315e600ba4b45d82adfd5801e36 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 9 Dec 2022 11:57:19 +0800 Subject: [PATCH 141/166] refactor(console): make the `LanguageEditor` responsive (#2620) --- .../LanguageDetails.module.scss | 116 ++++++++++-------- .../LanguageEditor/LanguageDetails.tsx | 1 + .../LanguageEditor/LanguageNav.module.scss | 4 +- .../LanguageEditor/index.module.scss | 50 +++++++- .../ManageLanguage/LanguageEditor/index.tsx | 30 +++-- 5 files changed, 130 insertions(+), 71 deletions(-) diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.module.scss b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.module.scss index 07e9beb63..acddd1c8f 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.module.scss +++ b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.module.scss @@ -1,9 +1,11 @@ @use '@/scss/underscore' as _; .languageDetails { - flex-grow: 1; + display: flex; + flex-direction: column; .title { + flex-shrink: 0; padding: _.unit(6) _.unit(5); font: var(--font-title-large); color: var(--color-text); @@ -31,68 +33,78 @@ } } - .content { - border-top: 1px solid var(--color-divider); - height: 481px; + .form { + flex: 1; + display: flex; + flex-direction: column; overflow-y: auto; - > table { - border: none; + .content { + flex: 1; + border-top: 1px solid var(--color-divider); + overflow-y: auto; - > thead { - position: sticky; - top: 0; - // Note: cells with `position: relative` style will overlap this sticky header, add a z-index to fix it. - z-index: 1; + .customValuesColumn { + display: flex; + align-items: center; + } - tr > th { - padding: _.unit(1) _.unit(5); - font: var(--font-label-large); - color: var(--color-text); - background-color: var(--color-layer-1); + .clearButton { + margin-left: _.unit(1); + } + + .clearIcon { + width: 16px; + height: 16px; + } + + > table { + border: none; + + > thead { + position: sticky; + top: 0; + // Note: cells with `position: relative` style will overlap this sticky header, add a z-index to fix it. + z-index: 1; + + tr > th { + padding: _.unit(1) _.unit(5); + font: var(--font-label-large); + color: var(--color-text); + background-color: var(--color-layer-1); + } + } + + > tbody > tr > td { + padding: _.unit(2) _.unit(5); + border: none; + word-wrap: break-word; } } - > tbody > tr > td { - padding: _.unit(2) _.unit(5); - border: none; + .sectionTitle { + @include _.subhead-cap; + background-color: var(--color-layer-light); + } + + .sectionDataKey { + padding: _.unit(4) _.unit(5); + font: var(--font-body-medium); + color: var(--color-text); + } + + .sectionBuiltInText { + padding: _.unit(2) 0; } } - .customValuesColumn { + .footer { + flex-shrink: 0; + border-top: 1px solid var(--color-divider); + height: 85px; display: flex; - align-items: center; + flex-direction: row-reverse; + padding: _.unit(5); } - - .clearButton { - margin-left: _.unit(1); - } - - .clearIcon { - width: 16px; - height: 16px; - } - - .sectionTitle { - @include _.subhead-cap; - background-color: var(--color-layer-light); - } - - .sectionDataKey { - padding: _.unit(4) _.unit(5); - font: var(--font-body-medium); - color: var(--color-text); - } - - .sectionBuiltInText { - padding: _.unit(2) 0; - } - } - - .footer { - border-top: 1px solid var(--color-divider); - display: flex; - flex-direction: row-reverse; - padding: _.unit(5); } } diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx index 2ef6f18ec..a8ce8d382 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx @@ -175,6 +175,7 @@ const LanguageDetails = () => { )}
{ // Note: Avoid propagating the 'submit' event to the outer sign-in-experience form. event.stopPropagation(); diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageNav.module.scss b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageNav.module.scss index b0a11ec54..42f104fcd 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageNav.module.scss +++ b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageNav.module.scss @@ -6,10 +6,12 @@ flex-shrink: 0; background-color: var(--color-layer-light); border-right: 1px solid var(--color-divider); + display: flex; + flex-direction: column; .languageItemList { + flex: 1; margin-top: _.unit(3); - height: 569px; overflow-y: auto; } } diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.module.scss b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.module.scss index c334f6519..9977a1077 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.module.scss +++ b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.module.scss @@ -1,7 +1,45 @@ -.container { - display: flex; - flex-direction: row; - border: 1px solid var(--color-divider); - border-radius: 8px; - overflow: hidden; +@use '@/scss/underscore' as _; +@use '@/scss/dimensions' as dim; + + +.modalOverlay { + position: fixed; + inset: 0; + background: var(--color-overlay); + + .modalContent { + position: fixed; + inset: 0; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; + } + + .editor { + margin: 0 auto; + max-width: dim.$modal-layout-width-xlarge; + max-height: 85vh; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: var(--shadow-3); + + .header { + display: flex; + align-items: flex-start; + justify-content: space-between; + flex-shrink: 0; + margin-bottom: _.unit(6); + } + + .content { + flex: 1; + display: flex; + flex-direction: row; + border: 1px solid var(--color-divider); + border-radius: 8px; + overflow: hidden; + } + } } diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.tsx b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.tsx index 2d2cc367a..a97263df4 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.tsx @@ -2,14 +2,16 @@ import { useContext, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import Modal from 'react-modal'; +import Close from '@/assets/images/close.svg'; +import Card from '@/components/Card'; +import CardTitle from '@/components/CardTitle'; import ConfirmModal from '@/components/ConfirmModal'; -import ModalLayout from '@/components/ModalLayout'; +import IconButton from '@/components/IconButton'; import useUiLanguages from '@/hooks/use-ui-languages'; -import * as modalStyles from '@/scss/modal.module.scss'; import LanguageDetails from './LanguageDetails'; import LanguageNav from './LanguageNav'; -import * as style from './index.module.scss'; +import * as styles from './index.module.scss'; import useLanguageEditorContext, { LanguageEditorContext } from './use-language-editor-context'; type Props = { @@ -69,18 +71,22 @@ const LanguageEditorModal = ({ isOpen, onClose }: Props) => { }; return ( - - -
+ + +
+ + + + +
+
- +
Date: Fri, 9 Dec 2022 12:01:44 +0800 Subject: [PATCH 142/166] refactor(console): add link to toggle tips (#2613) --- .../src/components/FormField/index.tsx | 5 +- .../Tip/TipBubble/index.module.scss | 8 +++ .../src/components/Tip/ToggleTip/index.tsx | 2 +- .../components/CreateForm/index.tsx | 19 ++++++- .../components/AdvancedSettings.tsx | 19 ++++++- .../components/Settings.tsx | 56 +++++++++++++++++-- .../ConnectorStatusField/index.module.scss | 8 --- .../components/ConnectorStatusField/index.tsx | 1 - .../src/pages/Dashboard/components/Block.tsx | 5 +- .../console/src/pages/Dashboard/index.tsx | 12 ++-- .../tabs/Others/TermsForm.tsx | 2 +- .../UserDetails/components/UserSettings.tsx | 2 +- .../admin-console/api-resources.ts | 2 +- .../admin-console/application-details.ts | 8 ++- .../admin-console/api-resources.ts | 2 +- .../admin-console/application-details.ts | 8 ++- .../admin-console/api-resources.ts | 2 +- .../admin-console/application-details.ts | 8 ++- .../admin-console/api-resources.ts | 2 +- .../admin-console/application-details.ts | 8 ++- .../admin-console/api-resources.ts | 2 +- .../admin-console/application-details.ts | 8 ++- .../admin-console/api-resources.ts | 2 +- .../admin-console/application-details.ts | 8 ++- .../admin-console/api-resources.ts | 2 +- .../admin-console/application-details.ts | 8 ++- .../admin-console/api-resources.ts | 2 +- .../admin-console/application-details.ts | 9 ++- 28 files changed, 157 insertions(+), 63 deletions(-) diff --git a/packages/console/src/components/FormField/index.tsx b/packages/console/src/components/FormField/index.tsx index 770008682..f672d7ad4 100644 --- a/packages/console/src/components/FormField/index.tsx +++ b/packages/console/src/components/FormField/index.tsx @@ -9,6 +9,7 @@ import type DangerousRaw from '../DangerousRaw'; import IconButton from '../IconButton'; import Spacer from '../Spacer'; import { ToggleTip } from '../Tip'; +import type { Props as ToggleTipProps } from '../Tip/ToggleTip'; import * as styles from './index.module.scss'; export type Props = { @@ -17,7 +18,7 @@ export type Props = { isRequired?: boolean; className?: string; headlineClassName?: string; - tip?: AdminConsoleKey; + tip?: ToggleTipProps['content']; }; const FormField = ({ title, children, isRequired, className, tip, headlineClassName }: Props) => { @@ -28,7 +29,7 @@ const FormField = ({ title, children, isRequired, className, tip, headlineClassN
{typeof title === 'string' ? t(title) : title}
{tip && ( - {t(tip)}
}> + diff --git a/packages/console/src/components/Tip/TipBubble/index.module.scss b/packages/console/src/components/Tip/TipBubble/index.module.scss index e312cf60d..5f8ab299b 100644 --- a/packages/console/src/components/Tip/TipBubble/index.module.scss +++ b/packages/console/src/components/Tip/TipBubble/index.module.scss @@ -10,6 +10,14 @@ font: var(--font-body-medium); max-width: 300px; + a { + color: #cabeff; + + &:active { + color: #cabeff; + } + } + &::after { content: ''; display: block; diff --git a/packages/console/src/components/Tip/ToggleTip/index.tsx b/packages/console/src/components/Tip/ToggleTip/index.tsx index ca442b952..da46eab14 100644 --- a/packages/console/src/components/Tip/ToggleTip/index.tsx +++ b/packages/console/src/components/Tip/ToggleTip/index.tsx @@ -16,7 +16,7 @@ import { } from '../TipBubble/utils'; import * as styles from './index.module.scss'; -type Props = { +export type Props = { children: ReactNode; className?: string; anchorClassName?: string; diff --git a/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx b/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx index f3c3610d2..6d87757c5 100644 --- a/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx +++ b/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx @@ -1,11 +1,12 @@ import type { Resource } from '@logto/schemas'; import { useForm } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import Button from '@/components/Button'; import FormField from '@/components/FormField'; import ModalLayout from '@/components/ModalLayout'; import TextInput from '@/components/TextInput'; +import TextLink from '@/components/TextLink'; import useApi from '@/hooks/use-api'; type FormData = { @@ -64,7 +65,21 @@ const CreateForm = ({ onClose }: Props) => { ( + + ), + }} + > + {t('api_resources.api_identifier_tip')} + + )} > { > ( + + ), + }} + > + {t('application_details.authorization_endpoint_tip')} + + )} > { placeholder={t('application_details.description_placeholder')} /> - + ( + + ), + }} + > + {t('application_details.application_id_tip')} + + )} + > {[ApplicationType.Traditional, ApplicationType.MachineToMachine].includes( @@ -85,7 +103,21 @@ const Settings = ({ data }: Props) => { ( + + ), + }} + > + {t('application_details.redirect_uri_tip')} + + )} value={value} error={convertRhfErrorMessage(error?.message)} placeholder={ @@ -109,7 +141,7 @@ const Settings = ({ data }: Props) => { render={({ field: { onChange, value }, fieldState: { error } }) => ( { render={({ field: { onChange, value }, fieldState: { error } }) => ( ( + + ), + }} + > + {t('application_details.cors_allowed_origins_tip')} + + )} value={value} error={convertRhfErrorMessage(error?.message)} placeholder={t('application_details.cors_allowed_origins_placeholder')} diff --git a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss index f50e92fbb..beacde6fc 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss +++ b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.module.scss @@ -15,12 +15,4 @@ .content { font: var(--font-body-medium); - - .link { - color: #cabeff; - - &:active { - color: #cabeff; - } - } } diff --git a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx index 9096f950b..fca04e788 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx +++ b/packages/console/src/pages/Connectors/components/ConnectorStatusField/index.tsx @@ -25,7 +25,6 @@ const ConnectorStatusField = () => { ), diff --git a/packages/console/src/pages/Dashboard/components/Block.tsx b/packages/console/src/pages/Dashboard/components/Block.tsx index 78eb5e332..84550cc6e 100644 --- a/packages/console/src/pages/Dashboard/components/Block.tsx +++ b/packages/console/src/pages/Dashboard/components/Block.tsx @@ -9,6 +9,7 @@ import Tip from '@/assets/images/tip.svg'; import Card from '@/components/Card'; import IconButton from '@/components/IconButton'; import { ToggleTip } from '@/components/Tip'; +import type { Props as ToggleTipProps } from '@/components/Tip/ToggleTip'; import { formatNumberWithComma } from '@/utilities/number'; import * as styles from './Block.module.scss'; @@ -17,7 +18,7 @@ type Props = { count: number; delta?: number; title: AdminConsoleKey; - tip?: AdminConsoleKey; + tip?: ToggleTipProps['content']; variant?: 'bordered' | 'default' | 'plain'; }; @@ -31,7 +32,7 @@ const Block = ({ variant = 'default', count, delta, title, tip }: Props) => {
{t(title)} {tip && ( - {t(tip)}
}> + diff --git a/packages/console/src/pages/Dashboard/index.tsx b/packages/console/src/pages/Dashboard/index.tsx index 4c0519a7e..7d60fbace 100644 --- a/packages/console/src/pages/Dashboard/index.tsx +++ b/packages/console/src/pages/Dashboard/index.tsx @@ -69,18 +69,18 @@ const Dashboard = () => {
@@ -88,7 +88,7 @@ const Dashboard = () => { {
{ diff --git a/packages/phrases/src/locales/de/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/de/translation/admin-console/api-resources.ts index b15b5094d..f0eab1642 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/api-resources.ts @@ -6,7 +6,7 @@ const api_resources = { api_name_placeholder: 'Gib einen API Namen ein', api_identifier: 'API Identifikator', api_identifier_tip: - 'Der eindeutige Identifikator der API Ressource muss eine absolute URI ohne Fragmentbezeichner (#) sein. Entspricht dem Ressourcen Parameter in OAuth 2.0.', + 'Der eindeutige Identifikator der API Ressource muss eine absolute URI ohne Fragmentbezeichner (#) sein. Entspricht dem Ressourcen Parameter in OAuth 2.0.', api_resource_created: 'Die API Ressource {{name}} wurde erfolgreich angelegt', api_identifier_placeholder: 'https://dein-api-identifikator/', }; diff --git a/packages/phrases/src/locales/de/translation/admin-console/application-details.ts b/packages/phrases/src/locales/de/translation/admin-console/application-details.ts index 0883c9be9..5a906234a 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/application-details.ts @@ -13,15 +13,17 @@ const application_details = { description_placeholder: 'Gib eine Beschreibung ein', authorization_endpoint: 'Autorisierungs-Endpoint', authorization_endpoint_tip: - 'Der Endpoint, der für die Authentifizierung und Autorisierung via OpenID Connect verwendet wird.', + 'Der Endpoint, der für die Authentifizierung und Autorisierung via OpenID Connect verwendet wird.', application_id: 'App ID', + application_id_tip: + 'The unique application identifier normally generated by Logto. It also stands for “client_id” in OpenID Connect.', // UNTRANSLATED application_secret: 'App Geheimnis', redirect_uri: 'Umleitungs-URI', redirect_uris: 'Umleitungs-URIs', redirect_uri_placeholder: 'https://deine.website.de/app', redirect_uri_placeholder_native: 'io.logto://callback', redirect_uri_tip: - 'URI zu der der Benutzer nach der Anmeldung (egal ob erfolgreich oder nicht) weitergeleitet wird. See OpenID Connect AuthRequest for more info.', + 'URI zu der der Benutzer nach der Anmeldung (egal ob erfolgreich oder nicht) weitergeleitet wird. See OpenID Connect AuthRequest for more info.', post_sign_out_redirect_uri: 'Post Sign-out Umleitungs-URI', post_sign_out_redirect_uris: 'Post Sign-out Umleitungs-URIs', post_sign_out_redirect_uri_placeholder: 'https://deine.website.de/home', @@ -30,7 +32,7 @@ const application_details = { cors_allowed_origins: 'CORS allowed origins', cors_allowed_origins_placeholder: 'https://your.website.de', cors_allowed_origins_tip: - 'Es sind standardmäßig alle Umleitungs-URI Origins erlaubt. Normalerweise ist dieses Feld nicht erforderlich.', + 'Es sind standardmäßig alle Umleitungs-URI Origins erlaubt. Normalerweise ist dieses Feld nicht erforderlich. See the MDN doc for detailed info.', // UNTRANSLATED add_another: 'Weitere hinzufügen', id_token_expiration: 'ID Token Ablaufzeit', refresh_token_expiration: 'Refresh Token Ablaufzeit', diff --git a/packages/phrases/src/locales/en/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/en/translation/admin-console/api-resources.ts index 06850a981..7b9aa8130 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/api-resources.ts @@ -6,7 +6,7 @@ const api_resources = { api_name_placeholder: 'Enter your API name', api_identifier: 'API identifier', api_identifier_tip: - 'The unique identifier to the API resource. It must be an absolute URI and has no fragment (#) component. Equals to the resource parameter in OAuth 2.0.', + 'The unique identifier to the API resource. It must be an absolute URI and has no fragment (#) component. Equals to the resource parameter in OAuth 2.0.', api_resource_created: 'The API resource {{name}} has been successfully created', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts index bcd554bbf..71d51326c 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts @@ -13,15 +13,17 @@ const application_details = { description_placeholder: 'Enter your application description', authorization_endpoint: 'Authorization endpoint', authorization_endpoint_tip: - "The endpoint to perform authentication and authorization. It's used for OpenID Connect Authentication.", + "The endpoint to perform authentication and authorization. It's used for OpenID Connect Authentication.", application_id: 'App ID', + application_id_tip: + 'The unique application identifier normally generated by Logto. It also stands for “client_id” in OpenID Connect.', application_secret: 'App Secret', redirect_uri: 'Redirect URI', redirect_uris: 'Redirect URIs', redirect_uri_placeholder: 'https://your.website.com/app', redirect_uri_placeholder_native: 'io.logto://callback', redirect_uri_tip: - 'The URI redirects after a user sign-in (whether successful or not). See OpenID Connect AuthRequest for more info.', + 'The URI redirects after a user sign-in (whether successful or not). See OpenID Connect AuthRequest for more info.', post_sign_out_redirect_uri: 'Post Sign-out Redirect URI', post_sign_out_redirect_uris: 'Post Sign-out Redirect URIs', post_sign_out_redirect_uri_placeholder: 'https://your.website.com/home', @@ -30,7 +32,7 @@ const application_details = { cors_allowed_origins: 'CORS allowed origins', cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: - 'By default, all the origins of Redirect URIs will be allowed. Usually no action is required for this field.', + 'By default, all the origins of Redirect URIs will be allowed. Usually no action is required for this field. See the MDN doc for detailed info.', add_another: 'Add Another', id_token_expiration: 'ID Token expiration', refresh_token_expiration: 'Refresh Token expiration', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/fr/translation/admin-console/api-resources.ts index a73a7e114..6eaf4731b 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/api-resources.ts @@ -6,7 +6,7 @@ const api_resources = { api_name_placeholder: "Entrez votre nom d'API", api_identifier: 'Identifiant API', api_identifier_tip: - "L'identifiant unique de la ressource API. Il doit s'agir d'un URI absolu et ne doit pas comporter de fragment (#). Équivaut au paramètre de ressource dans OAuth 2.0.", + "L'identifiant unique de la ressource API. Il doit s'agir d'un URI absolu et ne doit pas comporter de fragment (#). Équivaut au paramètre de ressource dans OAuth 2.0.", api_resource_created: 'La ressource API {{name}} a été créée avec succès.', api_identifier_placeholder: 'https://votre-identifiant-api/', }; diff --git a/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts b/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts index fe75fd1c7..c927d50ad 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts @@ -13,15 +13,17 @@ const application_details = { description_placeholder: 'Entrez la description de votre application', authorization_endpoint: 'Authorization endpoint', authorization_endpoint_tip: - "Le point de terminaison pour effectuer l'authentification et l'autorisation. Il est utilisé pour l'authentification OpenID Connect.", + "Le point de terminaison pour effectuer l'authentification et l'autorisation. Il est utilisé pour l'authentification OpenID Connect.", application_id: 'App ID', + application_id_tip: + 'The unique application identifier normally generated by Logto. It also stands for “client_id” in OpenID Connect.', // UNTRANSLATED application_secret: 'App Secret', redirect_uri: 'Redirect URI', redirect_uris: 'Redirect URIs', redirect_uri_placeholder: 'https://votre.site.com/app', redirect_uri_placeholder_native: 'io.logto://callback', redirect_uri_tip: - "L'URI de redirection après la connexion d'un utilisateur (qu'elle soit réussie ou non). Voir OpenID Connect AuthRequest pour plus d'informations.", + "L'URI de redirection après la connexion d'un utilisateur (qu'elle soit réussie ou non). Voir OpenID Connect AuthRequest pour plus d'informations.", post_sign_out_redirect_uri: 'URI de redirection post-signature', post_sign_out_redirect_uris: 'URI de redirection après la signature', post_sign_out_redirect_uri_placeholder: 'https://votre.site.com/home', @@ -30,7 +32,7 @@ const application_details = { cors_allowed_origins: 'Origines CORS autorisées', cors_allowed_origins_placeholder: 'https://votre.site.com', cors_allowed_origins_tip: - "Par défaut, toutes les origines des URI de redirection seront autorisées. En général, aucune action n'est requise pour ce champ.", + "Par défaut, toutes les origines des URI de redirection seront autorisées. En général, aucune action n'est requise pour ce champ. See the MDN doc for detailed info.", // UNTRANSLATED add_another: 'Ajouter un autre', id_token_expiration: "Expiration du jeton d'identification", refresh_token_expiration: "Rafraîchir l'expiration du jeton", diff --git a/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts index c94596fda..8ed1d3de0 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts @@ -6,7 +6,7 @@ const api_resources = { api_name_placeholder: 'API 이름 입력', api_identifier: 'API 식별자', api_identifier_tip: - 'API 리소스에 대한 유일한 식별자예요. 반드시, 절대적인 URI 이여야 하며, 프래그먼트 (#) 요소가 없어야해요. OAuth 2.0의 리소스 파라미터와 동일해요.', + 'The unique identifier to the API resource. It must be an absolute URI and has no fragment (#) component. Equals to the resource parameter in OAuth 2.0.', // UNTRANSLATED api_resource_created: '{{name}} API 리소스가 성공적으로 생성되었어요.', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts b/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts index 74de94d91..f611c5229 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts @@ -13,15 +13,17 @@ const application_details = { description_placeholder: '어플리케이션 설명을 적어주세요.', authorization_endpoint: '인증 End-Point', authorization_endpoint_tip: - '인증 및 권한 부여를 진행할 End-Point예요. OpenID Connect 인증에서 사용되던 값 이에요.', + "The endpoint to perform authentication and authorization. It's used for OpenID Connect Authentication.", // UNTRANSLATED application_id: 'App ID', + application_id_tip: + 'The unique application identifier normally generated by Logto. It also stands for “client_id” in OpenID Connect.', // UNTRANSLATED application_secret: 'App Secret', redirect_uri: 'Redirect URI', redirect_uris: 'Redirect URIs', redirect_uri_placeholder: 'https://your.website.com/app', redirect_uri_placeholder_native: 'io.logto://callback', redirect_uri_tip: - '사용자 로그인 이후, 리다이렉트 될 URI 경로예요. 더욱 자세한 정보는 OpenID Connect AuthRequest를 참고해주세요.', + '사용자 로그인 이후, 리다이렉트 될 URI 경로예요. 더욱 자세한 정보는 OpenID Connect AuthRequest를 참고해주세요.', post_sign_out_redirect_uri: '로그아웃 이후 Redirect URI', post_sign_out_redirect_uris: '로그아웃 이후 Redirect URIs', post_sign_out_redirect_uri_placeholder: 'https://your.website.com/home', @@ -30,7 +32,7 @@ const application_details = { cors_allowed_origins: 'CORS Allow Origins', cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: - '기본으로 모든 리다이렉트의 오리진들은 허용되요. 대체적으로 이 값을 건들 필요는 없어요.', + '기본으로 모든 리다이렉트의 오리진들은 허용되요. 대체적으로 이 값을 건들 필요는 없어요. See the MDN doc for detailed info.', // UNTRANSLATED add_another: '새로 추가', id_token_expiration: 'ID 토큰 만료', refresh_token_expiration: 'Refresh 토큰 만료', diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/api-resources.ts index 1b70d4040..ffd84a22f 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/api-resources.ts @@ -6,7 +6,7 @@ const api_resources = { api_name_placeholder: 'Digite o nome da sua API', api_identifier: 'Identificador de API', api_identifier_tip: - 'O identificador exclusivo para o recurso da API. Deve ser um URI absoluto e não tem nenhum componente de fragmento (#). Igual ao parâmetro de recurso em OAuth 2.0.', + 'O identificador exclusivo para o recurso da API. Deve ser um URI absoluto e não tem nenhum componente de fragmento (#). Igual ao parâmetro de recurso em OAuth 2.0.', api_resource_created: 'O recurso API {{name}} foi criado com sucesso', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts index 292fe785d..daab4da54 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts @@ -13,15 +13,17 @@ const application_details = { description_placeholder: 'Digite a descrição do seu aplicativo', authorization_endpoint: 'Endpoint de autorização', authorization_endpoint_tip: - 'O endpoint para executar autenticação e autorização. É usado para autenticação OpenID Connect.', + 'O endpoint para executar autenticação e autorização. É usado para autenticação OpenID Connect.', application_id: 'ID do aplicativo', + application_id_tip: + 'The unique application identifier normally generated by Logto. It also stands for “client_id” in OpenID Connect.', // UNTRANSLATED application_secret: 'Secret do aplicativo', redirect_uri: 'URI de redirecionamento', redirect_uris: 'URIs de redirecionamento', redirect_uri_placeholder: 'https://your.website.com/app', redirect_uri_placeholder_native: 'io.logto://callback', redirect_uri_tip: - 'O URI é redirecionado após o login do usuário (seja bem-sucedido ou não). Consulte OpenID Connect AuthRequest para obter mais informações.', + 'O URI é redirecionado após o login do usuário (seja bem-sucedido ou não). Consulte OpenID Connect AuthRequest para obter mais informações.', post_sign_out_redirect_uri: 'URI de redirecionamento Post Sign-out', post_sign_out_redirect_uris: 'URIs de redirecionamento Post Sign-out', post_sign_out_redirect_uri_placeholder: 'https://your.website.com/home', @@ -30,7 +32,7 @@ const application_details = { cors_allowed_origins: 'Origens permitidas pelo CORS', cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: - 'Por padrão, todas as origens de URIs de redirecionamento serão permitidas. Normalmente, nenhuma ação é necessária para este campo.', + 'Por padrão, todas as origens de URIs de redirecionamento serão permitidas. Normalmente, nenhuma ação é necessária para este campo. See the MDN doc for detailed info.', // UNTRANSLATED add_another: 'Adicionar outro', id_token_expiration: 'Expiração do token de ID', refresh_token_expiration: 'Expiração Refresh Token', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/api-resources.ts index fc37bc6b2..656c0760b 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/api-resources.ts @@ -6,7 +6,7 @@ const api_resources = { api_name_placeholder: 'Introduza o nome da sua API', api_identifier: 'identificador da API', api_identifier_tip: - 'O identificador exclusivo para o recurso API. Deve ser um URI absoluto e não tem componente de fragmento (#). Igual ao resource parameter no OAuth 2.0.', + 'O identificador exclusivo para o recurso API. Deve ser um URI absoluto e não tem componente de fragmento (#). Igual ao resource parameter no OAuth 2.0.', api_resource_created: 'O recurso API {{name}} foi criado com sucesso', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts index d4e43dc5b..29f16112b 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts @@ -13,15 +13,17 @@ const application_details = { description_placeholder: 'Insira a descrição da sua aplicação', authorization_endpoint: 'Endpoint de autorização', authorization_endpoint_tip: - 'O endpoint para realizar autenticação e autorização. É usado para autenticação OpenID Connect.', + 'O endpoint para realizar autenticação e autorização. É usado para autenticação OpenID Connect.', application_id: 'ID da aplicação', + application_id_tip: + 'The unique application identifier normally generated by Logto. It also stands for “client_id” in OpenID Connect.', // UNTRANSLATED application_secret: 'Segredo da aplicação', redirect_uri: 'URI de redirecionamento', redirect_uris: 'URIs de redirecionamento', redirect_uri_placeholder: 'https://your.website.com/app', redirect_uri_placeholder_native: 'io.logto://callback', redirect_uri_tip: - 'O URI redireciona após o login de um utilizador (com êxito ou não). Consulte OpenID Connect AuthRequest para obter mais informações.', + 'O URI redireciona após o login de um utilizador (com êxito ou não). Consulte OpenID Connect AuthRequest para obter mais informações.', post_sign_out_redirect_uri: 'URI de redirecionamento pós-logout', post_sign_out_redirect_uris: 'URIs de redirecionamento pós-logout', post_sign_out_redirect_uri_placeholder: 'https://your.website.com/home', @@ -30,7 +32,7 @@ const application_details = { cors_allowed_origins: 'origens permitidas CORS', cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: - 'Por padrão, todas as origens de redirecionamento serão permitidas. Recomenda-se restringir isto.', + 'Por padrão, todas as origens de redirecionamento serão permitidas. Recomenda-se restringir isto. See the MDN doc for detailed info.', // UNTRANSLATED add_another: 'Adicionar outro', id_token_expiration: 'Expiração do token de ID', refresh_token_expiration: 'Expiração do token de atualização', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/api-resources.ts index 26e7a32bc..51063d05e 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/api-resources.ts @@ -6,7 +6,7 @@ const api_resources = { api_name_placeholder: 'API adını giriniz', api_identifier: 'API belirteci', api_identifier_tip: - 'Api kaynağına özgün belirteç. Mutlak URI olmalı ve parça bileşeni (#) içermemeli. OAuth 2.0deki kaynak parametresine eşittir.', + 'Api kaynağına özgün belirteç. Mutlak URI olmalı ve parça bileşeni (#) içermemeli. OAuth 2.0deki kaynak parametresine eşittir.', api_resource_created: '{{name}} API kaynağı başarıyla oluşturuldu', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts index a5ad537d6..5b2d977fd 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts @@ -13,15 +13,17 @@ const application_details = { description_placeholder: 'Uygulama açıklamasını giriniz', authorization_endpoint: 'Yetkilendirme bitiş noktası', authorization_endpoint_tip: - 'Kimlik doğrulama ve yetkilendirme gerçekleştirmek için bitiş noktası. OpenID Connect Authentication için kullanılır.', + 'Kimlik doğrulama ve yetkilendirme gerçekleştirmek için bitiş noktası. OpenID Connect Authentication için kullanılır.', application_id: 'Uygulama IDsi', + application_id_tip: + 'The unique application identifier normally generated by Logto. It also stands for “client_id” in OpenID Connect.', // UNTRANSLATED application_secret: 'Uygulama Sırrı', redirect_uri: 'Yönlendirme URIı', redirect_uris: 'Yönlendirme URIları', redirect_uri_placeholder: 'https://your.website.com/app', redirect_uri_placeholder_native: 'io.logto://callback', redirect_uri_tip: - 'URI kullanıcı oturum açma işlemiden sonra yönlendirir (Başarılı olsa da olmasa da). Detaylı bilgi için OpenID Connect AuthRequesta bakınız.', + 'URI kullanıcı oturum açma işlemiden sonra yönlendirir (Başarılı olsa da olmasa da). Detaylı bilgi için OpenID Connect AuthRequesta bakınız.', post_sign_out_redirect_uri: 'Oturumdan Çıkış sonrası yönlendirme URIı', post_sign_out_redirect_uris: 'Oturumdan Çıkış sonrası yönlendirme URIları', post_sign_out_redirect_uri_placeholder: 'https://your.website.com/home', @@ -30,7 +32,7 @@ const application_details = { cors_allowed_origins: 'CORS izinli originler', cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: - 'Varsayılan olarak, Yönlendirme URIlerinin tüm originlerine izin verilir. Genellikle bu alan için herhangi bir işlem gerekmez.', + 'Varsayılan olarak, Yönlendirme URIlerinin tüm originlerine izin verilir. Genellikle bu alan için herhangi bir işlem gerekmez. See the MDN doc for detailed info.', // UNTRANSLATED add_another: 'Bir tane daha ekle', id_token_expiration: 'ID Token sona erme süresi', refresh_token_expiration: 'Refresh Token sona erme süresi', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/api-resources.ts index dc90e49da..a4f331cb8 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/api-resources.ts @@ -7,7 +7,7 @@ const api_resources = { api_identifier: 'API Identifier', api_identifier_placeholder: 'https://your-api-identifier/', api_identifier_tip: - '对于 API 资源的唯一标识符。它必须是一个绝对 URI 并没有 fragment (#) 组件。等价于 OAuth 2.0 中的 resource parameter。', + '对于 API 资源的唯一标识符。它必须是一个绝对 URI 并没有 fragment (#) 组件。等价于 OAuth 2.0 中的 resource parameter。', api_resource_created: ' API 资源 {{name}} 已成功创建!', }; diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts index a355ef3d1..cf83fd6a9 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts @@ -12,15 +12,18 @@ const application_details = { description: '描述', description_placeholder: '请输入应用描述', authorization_endpoint: 'Authorization Endpoint', - authorization_endpoint_tip: '进行鉴权与授权的端点 endpoint。用于 OpenID Connect 中的鉴权流程。', + authorization_endpoint_tip: + '进行鉴权与授权的端点 endpoint。用于 OpenID Connect 中的 鉴权 流程。', application_id: 'App ID', + application_id_tip: + '应用的唯一标识,通常由 Logto 生成。等价于 OpenID Connect 中的 client_id。', application_secret: 'App Secret', redirect_uri: 'Redirect URI', redirect_uris: 'Redirect URIs', redirect_uri_placeholder: 'https://your.website.com/app', redirect_uri_placeholder_native: 'io.logto://callback', redirect_uri_tip: - '在用户登录完成(不论成功与否)后重定向的目标 URI。参见 OpenID Connect AuthRequest 以了解更多。', + '在用户登录完成(不论成功与否)后重定向的目标 URI。参见 OpenID Connect AuthRequest 以了解更多。', post_sign_out_redirect_uri: 'Post Sign-out Redirect URI', post_sign_out_redirect_uris: 'Post sign out redirect URIs', post_sign_out_redirect_uri_placeholder: 'https://your.website.com/home', @@ -29,7 +32,7 @@ const application_details = { cors_allowed_origins: 'CORS Allowed Origins', cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: - '所有 Redirect URI 的 origin 将默认被允许。通常不需要对此字段进行操作。', + '所有 Redirect URI 的 origin 将默认被允许。通常不需要对此字段进行操作。参见 MDN 文档以了解更多', add_another: '新增', id_token_expiration: 'ID Token 过期时间', refresh_token_expiration: 'Refresh Token 过期时间', From 698610a12fba4a1f7fcc952c70b90de2ce338124 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Fri, 9 Dec 2022 12:12:32 +0800 Subject: [PATCH 143/166] refactor(core): refactor the user account verification (#2607) --- packages/core/src/routes/admin-user.test.ts | 4 +- packages/core/src/routes/admin-user.ts | 9 +- packages/core/src/routes/init.ts | 15 +- .../interaction/actions/submit-interaction.ts | 2 + .../core/src/routes/interaction/index.test.ts | 168 +++++++++++++++++- packages/core/src/routes/interaction/index.ts | 10 +- .../src/routes/interaction/utils/index.ts | 24 +-- .../interaction/utils/interaction.test.ts | 61 +++---- .../routes/interaction/utils/interaction.ts | 71 ++++++-- .../identifier-payload-verification.ts | 10 +- .../verifications/profile-verification.ts | 13 +- .../user-identity-verification.test.ts | 2 +- .../user-identity-verification.ts | 42 ++--- 13 files changed, 322 insertions(+), 109 deletions(-) diff --git a/packages/core/src/routes/admin-user.test.ts b/packages/core/src/routes/admin-user.test.ts index 601ecaef8..b0711b92e 100644 --- a/packages/core/src/routes/admin-user.test.ts +++ b/packages/core/src/routes/admin-user.test.ts @@ -129,11 +129,11 @@ describe('adminUserRoutes', () => { const username = 'MJAtLogto'; const password = 'PASSWORD'; const name = 'Michael'; - const primaryEmail = 'foo@logto.io'; + const { primaryEmail, primaryPhone } = mockUser; const response = await userRequest .post('/users') - .send({ primaryEmail, username, password, name }); + .send({ primaryEmail, primaryPhone, username, password, name }); expect(response.status).toEqual(200); expect(response.body).toEqual({ ...mockUserResponse, diff --git a/packages/core/src/routes/admin-user.ts b/packages/core/src/routes/admin-user.ts index 660cabcc3..3d967d66b 100644 --- a/packages/core/src/routes/admin-user.ts +++ b/packages/core/src/routes/admin-user.ts @@ -25,6 +25,7 @@ import { hasUser, updateUserById, hasUserWithEmail, + hasUserWithPhone, } from '#src/queries/user.js'; import assertThat from '#src/utils/assert-that.js'; @@ -127,6 +128,7 @@ export default function adminUserRoutes(router: T) { '/users', koaGuard({ body: object({ + primaryPhone: string().regex(phoneRegEx).optional(), primaryEmail: string().regex(emailRegEx).optional(), username: string().regex(usernameRegEx).optional(), password: string().regex(passwordRegEx), @@ -134,7 +136,7 @@ export default function adminUserRoutes(router: T) { }), }), async (ctx, next) => { - const { primaryEmail, username, password, name } = ctx.guard.body; + const { primaryEmail, primaryPhone, username, password, name } = ctx.guard.body; assertThat( !username || !(await hasUser(username)), @@ -150,6 +152,10 @@ export default function adminUserRoutes(router: T) { status: 422, }) ); + assertThat( + !primaryPhone || !(await hasUserWithPhone(primaryPhone)), + new RequestError({ code: 'user.phone_already_in_use' }) + ); const id = await generateUserId(); @@ -158,6 +164,7 @@ export default function adminUserRoutes(router: T) { const user = await insertUser({ id, primaryEmail, + primaryPhone, username, passwordEncrypted, passwordEncryptionMethod, diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index b0d093845..474a586a8 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -12,6 +12,7 @@ import authnRoutes from './authn.js'; import connectorRoutes from './connector.js'; import customPhraseRoutes from './custom-phrase.js'; import dashboardRoutes from './dashboard.js'; +import interactionRoutes from './interaction/index.js'; import logRoutes from './log.js'; import phraseRoutes from './phrase.js'; import profileRoutes from './profile.js'; @@ -30,6 +31,10 @@ const createRouters = (provider: Provider) => { sessionRouter.use(koaLogSession(provider)); sessionRoutes(sessionRouter, provider); + const interactionRouter: AnonymousRouter = new Router(); + interactionRouter.use(koaLogSession(provider)); + interactionRoutes(interactionRouter, provider); + const managementRouter: AuthedRouter = new Router(); managementRouter.use(koaAuth(UserRole.Admin)); applicationRoutes(managementRouter); @@ -52,9 +57,15 @@ const createRouters = (provider: Provider) => { statusRoutes(anonymousRouter); authnRoutes(anonymousRouter); // The swagger.json should contain all API routers. - swaggerRoutes(anonymousRouter, [sessionRouter, profileRouter, managementRouter, anonymousRouter]); + swaggerRoutes(anonymousRouter, [ + sessionRouter, + interactionRouter, + profileRouter, + managementRouter, + anonymousRouter, + ]); - return [sessionRouter, profileRouter, managementRouter, anonymousRouter]; + return [sessionRouter, interactionRouter, profileRouter, managementRouter, anonymousRouter]; }; export default function initRouter(app: Koa, provider: Provider) { diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 41b75e480..47ae8fdb6 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -16,6 +16,7 @@ import type { VerifiedSignInInteractionResult, VerifiedRegisterInteractionResult, } from '../types/index.js'; +import { clearInteractionStorage } from '../utils/interaction.js'; const getSocialUpdateProfile = async ({ user, @@ -124,5 +125,6 @@ export default async function submitInteraction( profile.password ); await updateUserById(accountId, { passwordEncrypted, passwordEncryptionMethod }); + await clearInteractionStorage(ctx, provider); ctx.status = 204; } diff --git a/packages/core/src/routes/interaction/index.test.ts b/packages/core/src/routes/interaction/index.test.ts index 4b7a57f4e..af08a4710 100644 --- a/packages/core/src/routes/interaction/index.test.ts +++ b/packages/core/src/routes/interaction/index.test.ts @@ -2,17 +2,23 @@ import { ConnectorType } from '@logto/connector-kit'; import { Event } from '@logto/schemas'; import { Provider } from 'oidc-provider'; +import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; import RequestError from '#src/errors/RequestError/index.js'; import { createRequester } from '#src/utils/test-utils.js'; -import interactionRoutes, { verificationPrefix } from './index.js'; +import submitInteraction from './actions/submit-interaction.js'; +import interactionRoutes, { verificationPrefix, interactionPrefix } from './index.js'; +import type { InteractionContext } from './types/index.js'; +import { getInteractionStorage } from './utils/interaction.js'; import { sendPasscodeToIdentifier } from './utils/passcode-validation.js'; +import { + verifyIdentifier, + verifyProfile, + validateMandatoryUserProfile, +} from './verifications/index.js'; // FIXME @Darcy: no more `enabled` for `connectors` table const getLogtoConnectorByIdHelper = jest.fn(async (connectorId: string) => { - const database = { - enabled: connectorId === 'social_enabled', - }; const metadata = { id: connectorId === 'social_enabled' @@ -23,7 +29,7 @@ const getLogtoConnectorByIdHelper = jest.fn(async (connectorId: string) => { }; return { - dbEntry: database, + dbEntry: {}, metadata, type: connectorId.startsWith('social') ? ConnectorType.Social : ConnectorType.Sms, getAuthorizationUri: jest.fn(async () => ''), @@ -53,12 +59,45 @@ jest.mock('oidc-provider', () => ({ Provider: jest.fn(() => ({ interactionDetails: jest.fn().mockResolvedValue({ jti: 'jti', + result: {}, + params: { + client_id: 'demo_app', + }, }), })), })); +jest.mock('#src/lib/sign-in-experience/index.js', () => ({ + getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience), +})); + +jest.mock('./verifications/index.js', () => ({ + verifyIdentifier: jest.fn(), + verifyProfile: jest.fn(), + validateMandatoryUserProfile: jest.fn(), +})); + +jest.mock('./actions/submit-interaction.js', () => + jest.fn((_interaction, ctx: InteractionContext) => { + ctx.body = { redirectUri: 'logto.io' }; + }) +); + +jest.mock('./utils/interaction.js', () => ({ + getInteractionStorage: jest.fn(), +})); + const log = jest.fn(); +const koaInteractionBodyGuardSpy = jest.spyOn( + jest.requireActual('./middleware/koa-interaction-body-guard.js'), + 'default' +); +const koaSessionSignInExperienceGuardSpy = jest.spyOn( + jest.requireActual('./middleware/koa-session-sign-in-experience-guard.js'), + 'default' +); + describe('session -> interactionRoutes', () => { const sessionRequest = createRequester({ anonymousRoutes: interactionRoutes, @@ -73,6 +112,125 @@ describe('session -> interactionRoutes', () => { ], }); + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('PUT /interaction', () => { + const path = interactionPrefix; + + it('sign-in event should call methods properly', async () => { + const body = { + event: Event.SignIn, + identifier: { + username: 'username', + password: 'password', + }, + }; + const response = await sessionRequest.put(path).send(body); + expect(koaInteractionBodyGuardSpy).toBeCalled(); + expect(koaSessionSignInExperienceGuardSpy).toBeCalled(); + expect(verifyIdentifier).toBeCalled(); + expect(verifyProfile).toBeCalled(); + expect(validateMandatoryUserProfile).toBeCalled(); + expect(submitInteraction).toBeCalled(); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ redirectUri: 'logto.io' }); + }); + + it('forgot password event should not call UserProfile validation', async () => { + const body = { + event: Event.ForgotPassword, + identifier: { + email: 'email@logto.io', + passcode: 'passcode', + }, + profile: { + password: 'password', + }, + }; + + const response = await sessionRequest.put(path).send(body); + + expect(verifyIdentifier).toBeCalled(); + expect(verifyProfile).toBeCalled(); + expect(validateMandatoryUserProfile).not.toBeCalled(); + expect(submitInteraction).toBeCalled(); + expect(response.status).toEqual(200); + }); + }); + + describe('PATCH /interaction', () => { + const path = interactionPrefix; + const getInteractionStorageMock = getInteractionStorage as jest.Mock; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('sign-in event with register event interaction session in record should call methods properly', async () => { + getInteractionStorageMock.mockResolvedValueOnce({ event: Event.Register }); + + const body = { + event: Event.SignIn, + }; + + const response = await sessionRequest.patch(path).send(body); + expect(verifyIdentifier).toBeCalled(); + expect(verifyProfile).toBeCalled(); + expect(validateMandatoryUserProfile).toBeCalled(); + expect(submitInteraction).toBeCalled(); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ redirectUri: 'logto.io' }); + }); + + it('sign-in event with forgot password event interaction session in record should reject', async () => { + getInteractionStorageMock.mockResolvedValueOnce({ event: Event.ForgotPassword }); + + const body = { + event: Event.SignIn, + }; + + const response = await sessionRequest.patch(path).send(body); + expect(verifyIdentifier).not.toBeCalled(); + expect(verifyProfile).not.toBeCalled(); + expect(validateMandatoryUserProfile).not.toBeCalled(); + expect(submitInteraction).not.toBeCalled(); + expect(response.status).toEqual(404); + }); + + it('Forgot event with forgot password event interaction session in record should call methods properly', async () => { + getInteractionStorageMock.mockResolvedValueOnce({ event: Event.ForgotPassword }); + + const body = { + event: Event.ForgotPassword, + }; + + const response = await sessionRequest.patch(path).send(body); + expect(verifyIdentifier).toBeCalled(); + expect(verifyProfile).toBeCalled(); + expect(validateMandatoryUserProfile).not.toBeCalled(); + expect(submitInteraction).toBeCalled(); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ redirectUri: 'logto.io' }); + }); + + it('Forgot event with sign-in event interaction session in record should call methods properly', async () => { + getInteractionStorageMock.mockResolvedValueOnce({ event: Event.SignIn }); + + const body = { + event: Event.ForgotPassword, + }; + + const response = await sessionRequest.patch(path).send(body); + expect(verifyIdentifier).not.toBeCalled(); + expect(verifyProfile).not.toBeCalled(); + expect(validateMandatoryUserProfile).not.toBeCalled(); + expect(submitInteraction).not.toBeCalled(); + expect(response.status).toEqual(404); + }); + }); + describe('POST /verification/passcode', () => { const path = `${verificationPrefix}/passcode`; it('should call send passcode properly', async () => { diff --git a/packages/core/src/routes/interaction/index.ts b/packages/core/src/routes/interaction/index.ts index f02c788c9..4bfbd051d 100644 --- a/packages/core/src/routes/interaction/index.ts +++ b/packages/core/src/routes/interaction/index.ts @@ -21,7 +21,7 @@ import { validateMandatoryUserProfile, } from './verifications/index.js'; -export const identifierPrefix = '/identifier'; +export const interactionPrefix = '/interaction'; export const verificationPrefix = '/verification'; export default function interactionRoutes( @@ -29,7 +29,7 @@ export default function interactionRoutes( provider: Provider ) { router.put( - identifierPrefix, + interactionPrefix, koaInteractionBodyGuard(), koaSessionSignInExperienceGuard(provider), async (ctx, next) => { @@ -53,7 +53,7 @@ export default function interactionRoutes( ); router.patch( - identifierPrefix, + interactionPrefix, koaInteractionBodyGuard(), koaSessionSignInExperienceGuard(provider), async (ctx, next) => { @@ -65,7 +65,7 @@ export default function interactionRoutes( event === Event.ForgotPassword ? interactionStorage.event === Event.ForgotPassword : interactionStorage.event !== Event.ForgotPassword, - new RequestError({ code: 'session.verification_session_not_found' }) + new RequestError({ code: 'session.verification_session_not_found', status: 404 }) ); const identifierVerifiedInteraction = await verifyIdentifier( @@ -86,7 +86,7 @@ export default function interactionRoutes( } ); - router.delete(identifierPrefix, async (ctx, next) => { + router.delete(interactionPrefix, async (ctx, next) => { await provider.interactionDetails(ctx.req, ctx.res); const error: LogtoErrorCode = 'oidc.aborted'; await assignInteractionResults(ctx, provider, { error }); diff --git a/packages/core/src/routes/interaction/utils/index.ts b/packages/core/src/routes/interaction/utils/index.ts index 5e4a285a8..efaa83e29 100644 --- a/packages/core/src/routes/interaction/utils/index.ts +++ b/packages/core/src/routes/interaction/utils/index.ts @@ -1,10 +1,6 @@ -import type { Profile, SocialConnectorPayload, User, IdentifierPayload } from '@logto/schemas'; +import type { SocialConnectorPayload, User, IdentifierPayload } from '@logto/schemas'; -import type { - PasscodeIdentifierPayload, - PasswordIdentifierPayload, - Identifier, -} from '../types/index.js'; +import type { PasscodeIdentifierPayload, PasswordIdentifierPayload } from '../types/index.js'; export const isPasswordIdentifier = ( identifier: IdentifierPayload @@ -19,22 +15,6 @@ export const isSocialIdentifier = ( ): identifier is SocialConnectorPayload => 'connectorId' in identifier && 'connectorData' in identifier; -export const isProfileIdentifier = (identifier: Identifier, profile?: Profile) => { - if (identifier.key === 'accountId') { - return false; - } - - if (identifier.key === 'emailVerified') { - return profile?.email === identifier.value; - } - - if (identifier.key === 'phoneVerified') { - return profile?.phone === identifier.value; - } - - return profile?.connectorId === identifier.connectorId; -}; - // Social identities can take place the role of password export const isUserPasswordSet = ({ passwordEncrypted, diff --git a/packages/core/src/routes/interaction/utils/interaction.test.ts b/packages/core/src/routes/interaction/utils/interaction.test.ts index 274f97d97..6d0a8858b 100644 --- a/packages/core/src/routes/interaction/utils/interaction.test.ts +++ b/packages/core/src/routes/interaction/utils/interaction.test.ts @@ -6,40 +6,37 @@ describe('interaction utils', () => { const emailIdentifier: Identifier = { key: 'emailVerified', value: 'foo@logto.io' }; const phoneIdentifier: Identifier = { key: 'phoneVerified', value: '12346' }; - it('mergeIdentifiers', () => { - expect(mergeIdentifiers({})).toEqual(undefined); - expect(mergeIdentifiers({ oldIdentifiers: [usernameIdentifier] })).toEqual([ - usernameIdentifier, - ]); - expect(mergeIdentifiers({ newIdentifiers: [usernameIdentifier] })).toEqual([ - usernameIdentifier, - ]); - expect( - mergeIdentifiers({ - oldIdentifiers: [usernameIdentifier], - newIdentifiers: [usernameIdentifier], - }) - ).toEqual([usernameIdentifier]); + describe('mergeIdentifiers', () => { + it('new identifiers only ', () => { + expect(mergeIdentifiers([usernameIdentifier])).toEqual([usernameIdentifier]); + }); - expect( - mergeIdentifiers({ - oldIdentifiers: [emailIdentifier], - newIdentifiers: [usernameIdentifier], - }) - ).toEqual([emailIdentifier, usernameIdentifier]); + it('same identifiers should replace', () => { + expect(mergeIdentifiers([usernameIdentifier], [{ key: 'accountId', value: 'foo2' }])).toEqual( + [usernameIdentifier] + ); + }); - expect( - mergeIdentifiers({ - oldIdentifiers: [emailIdentifier, phoneIdentifier], - newIdentifiers: [phoneIdentifier, usernameIdentifier], - }) - ).toEqual([emailIdentifier, phoneIdentifier, usernameIdentifier]); + it('different identifiers should merge', () => { + expect(mergeIdentifiers([emailIdentifier], [usernameIdentifier])).toEqual([ + usernameIdentifier, + emailIdentifier, + ]); - expect( - mergeIdentifiers({ - oldIdentifiers: [emailIdentifier, phoneIdentifier], - newIdentifiers: [usernameIdentifier], - }) - ).toEqual([emailIdentifier, phoneIdentifier, usernameIdentifier]); + expect(mergeIdentifiers([usernameIdentifier], [emailIdentifier, phoneIdentifier])).toEqual([ + emailIdentifier, + phoneIdentifier, + usernameIdentifier, + ]); + }); + + it('mixed identifiers should replace and merge', () => { + expect( + mergeIdentifiers( + [phoneIdentifier, usernameIdentifier], + [emailIdentifier, { key: 'phoneVerified', value: '465789' }] + ) + ).toEqual([emailIdentifier, phoneIdentifier, usernameIdentifier]); + }); }); }); diff --git a/packages/core/src/routes/interaction/utils/interaction.ts b/packages/core/src/routes/interaction/utils/interaction.ts index 49a169113..5237e6e41 100644 --- a/packages/core/src/routes/interaction/utils/interaction.ts +++ b/packages/core/src/routes/interaction/utils/interaction.ts @@ -1,4 +1,4 @@ -import type { Event } from '@logto/schemas'; +import type { Event, Profile } from '@logto/schemas'; import type { Context } from 'koa'; import type { Provider } from 'oidc-provider'; @@ -12,21 +12,29 @@ import type { AccountVerifiedInteractionResult, } from '../types/index.js'; -// Unique identifier type is required -export const mergeIdentifiers = (pairs: { - newIdentifiers?: Identifier[]; - oldIdentifiers?: Identifier[]; -}) => { - const { newIdentifiers, oldIdentifiers } = pairs; - - if (!newIdentifiers) { - return oldIdentifiers; +const isProfileIdentifier = (identifier: Identifier, profile?: Profile) => { + if (identifier.key === 'accountId') { + return false; } + if (identifier.key === 'emailVerified') { + return profile?.email === identifier.value; + } + + if (identifier.key === 'phoneVerified') { + return profile?.phone === identifier.value; + } + + return profile?.connectorId === identifier.connectorId; +}; + +// Unique identifier type is required +export const mergeIdentifiers = (newIdentifiers: Identifier[], oldIdentifiers?: Identifier[]) => { if (!oldIdentifiers) { return newIdentifiers; } + // Filter out identifiers with the same key in the oldIdentifiers and replaced with new ones const leftOvers = oldIdentifiers.filter((oldIdentifier) => { return !newIdentifiers.some((newIdentifier) => newIdentifier.key === oldIdentifier.key); }); @@ -34,6 +42,40 @@ export const mergeIdentifiers = (pairs: { return [...leftOvers, ...newIdentifiers]; }; +/** + * Categorize the identifiers based on their different use cases + * @typedef {Object} result + * @property {Identifier[]} userAccountIdentifiers - identifiers to verify a specific user account e.g. for sign-in and reset-password + * @property {Identifier[]} profileIdentifiers - identifiers to verify a new anonymous profile e.g. new email, new phone or new social identity + * + * @param {Identifier[]} identifiers + * @param {Profile} profile + * @returns + */ +export const categorizeIdentifiers = ( + identifiers: Identifier[], + profile?: Profile +): { + userAccountIdentifiers: Identifier[]; + profileIdentifiers: Identifier[]; +} => { + const userAccountIdentifiers = new Set(); + const profileIdentifiers = new Set(); + + for (const identifier of identifiers) { + if (isProfileIdentifier(identifier, profile)) { + profileIdentifiers.add(identifier); + continue; + } + userAccountIdentifiers.add(identifier); + } + + return { + userAccountIdentifiers: [...userAccountIdentifiers], + profileIdentifiers: [...profileIdentifiers], + }; +}; + export const isAccountVerifiedInteractionResult = ( interaction: AnonymousInteractionResult ): interaction is AccountVerifiedInteractionResult => Boolean(interaction.accountId); @@ -68,3 +110,12 @@ export const getInteractionStorage = async (ctx: Context, provider: Provider) => return parseResult.data; }; + +export const clearInteractionStorage = async (ctx: Context, provider: Provider) => { + const { result } = await provider.interactionDetails(ctx.req, ctx.res); + + if (result) { + const { event, profile, identifier, ...rest } = result; + await provider.interactionResult(ctx.req, ctx.res, { ...rest }); + } +}; 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 3424c4232..325f6aec7 100644 --- a/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts +++ b/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts @@ -66,6 +66,7 @@ const verifySocialIdentityInInteractionRecord = async ( { connectorId, identityType }: SocialIdentityPayload, interactionRecord?: AnonymousInteractionResult ): Promise => { + // Sign-In with social verified email or phone requires a social identifier in the interaction result const socialIdentifierRecord = interactionRecord?.identifiers?.find( (entity): entity is SocialIdentifier => entity.key === 'social' && entity.connectorId === connectorId @@ -88,6 +89,7 @@ const verifyIdentifierPayload = async ( ): Promise => { const { identifier, event } = ctx.interactionPayload; + // No Identifier in payload if (!identifier) { return; } @@ -104,6 +106,7 @@ const verifyIdentifierPayload = async ( return verifySocialIdentifier(identifier, ctx); } + // Sign-In with social verified email or phone return verifySocialIdentityInInteractionRecord(identifier, interactionRecord); }; @@ -119,10 +122,9 @@ export default async function identifierPayloadVerification( const interaction: PayloadVerifiedInteractionResult = { ...interactionRecord, event, - identifiers: mergeIdentifiers({ - oldIdentifiers: interactionRecord?.identifiers, - newIdentifiers: identifier && [identifier], - }), + identifiers: identifier + ? mergeIdentifiers([identifier], interactionRecord?.identifiers) + : interactionRecord?.identifiers, }; await storeInteractionResult(interaction, ctx, provider); diff --git a/packages/core/src/routes/interaction/verifications/profile-verification.ts b/packages/core/src/routes/interaction/verifications/profile-verification.ts index dba032815..a986a6945 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.ts @@ -70,7 +70,7 @@ const verifyProfileIdentifiers = ( } }; -const verifyProfileNotRegistered = async ( +const verifyProfileNotRegisteredByOtherUserAccount = async ( { username, email, phone, connectorId }: Profile, identifiers: Identifier[] = [] ) => { @@ -128,7 +128,10 @@ const verifyProfileNotRegistered = async ( } }; -const verifyProfileNotExist = async ({ username, email, phone, password }: Profile, user: User) => { +const verifyProfileNotExistInCurrentUserAccount = async ( + { username, email, phone, password }: Profile, + user: User +) => { if (username) { assertThat( !user.username, @@ -183,7 +186,7 @@ export default async function verifyProfile( assertThat(isValidRegisterProfile(profile), new RequestError({ code: 'guard.invalid_input' })); verifyProfileIdentifiers(profile, identifiers); - await verifyProfileNotRegistered(profile, identifiers); + await verifyProfileNotRegisteredByOtherUserAccount(profile, identifiers); const interactionWithProfile: VerifiedRegisterInteractionResult = { ...interaction, profile }; await storeInteractionResult(interactionWithProfile, ctx, provider); @@ -195,8 +198,8 @@ export default async function verifyProfile( verifyProfileIdentifiers(profile, identifiers); // Find existing account const user = await findUserById(accountId); - await verifyProfileNotExist(profile, user); - await verifyProfileNotRegistered(profile, identifiers); + await verifyProfileNotExistInCurrentUserAccount(profile, user); + await verifyProfileNotRegisteredByOtherUserAccount(profile, identifiers); const interactionWithProfile: VerifiedSignInInteractionResult = { ...interaction, profile }; await storeInteractionResult(interactionWithProfile, ctx, provider); 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 1678d90ea..8ac0bc348 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 @@ -58,7 +58,7 @@ describe('userAccountVerification', () => { }; await expect(userAccountVerification(interaction, ctx, provider)).rejects.toMatchError( - new RequestError({ code: 'session.unauthorized', status: 401 }) + new RequestError({ code: 'session.verification_session_not_found', status: 404 }) ); expect(storeInteractionResult).not.toBeCalled(); }); 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 d13ee0aad..1c8a319b0 100644 --- a/packages/core/src/routes/interaction/verifications/user-identity-verification.ts +++ b/packages/core/src/routes/interaction/verifications/user-identity-verification.ts @@ -16,10 +16,10 @@ import type { InteractionContext, } from '../types/index.js'; import findUserByIdentifier from '../utils/find-user-by-identifier.js'; -import { isProfileIdentifier } from '../utils/index.js'; import { storeInteractionResult, isAccountVerifiedInteractionResult, + categorizeIdentifiers, } from '../utils/interaction.js'; const identifyUserByVerifiedEmailOrPhone = async ( @@ -82,46 +82,48 @@ export default async function userAccountVerification( ctx: InteractionContext, provider: Provider ): Promise { - const { identifiers = [], accountId } = interaction; - // Need to merge the profile in payload - const profile = { ...interaction.profile, ...ctx.interactionPayload.profile }; + const { identifiers = [], accountId, profile } = interaction; - // Filter all non-profile identifiers - const userIdentifiers = identifiers.filter( - (identifier) => !isProfileIdentifier(identifier, profile) + const { userAccountIdentifiers, profileIdentifiers } = categorizeIdentifiers( + identifiers, + // Need to merge the profile in payload + { ...profile, ...ctx.interactionPayload.profile } ); - if (isAccountVerifiedInteractionResult(interaction) && userIdentifiers.length === 0) { + // Return the interaction directly if it is accountVerified and has no unverified userAccountIdentifiers + // e.g. profile fulfillment request with account already verified in the interaction result + if (isAccountVerifiedInteractionResult(interaction) && userAccountIdentifiers.length === 0) { return interaction; } + // _userAccountIdentifiers is required to identify a user account assertThat( - userIdentifiers.length > 0, + userAccountIdentifiers.length > 0, new RequestError({ - code: 'session.unauthorized', - status: 401, + code: 'session.verification_session_not_found', + status: 404, }) ); - // Verify All non-profile identifiers + // Verify userAccountIdentifiers const accountIds = await Promise.all( - userIdentifiers.map(async (identifier) => identifyUser(identifier)) + userAccountIdentifiers.map(async (identifier) => identifyUser(identifier)) ); - const deduplicateAccountIds = deduplicate(accountIds); - // Inconsistent identities + // Inconsistent account identifiers check + assertThat(deduplicateAccountIds.length === 1, new RequestError('session.verification_failed')); + + // Valid accountId verification. Should also equal to the accountId in record if exist. Else throw assertThat( - deduplicateAccountIds.length === 1 && - deduplicateAccountIds[0] && - (!accountId || accountId === deduplicateAccountIds[0]), + deduplicateAccountIds[0] && (!accountId || accountId === deduplicateAccountIds[0]), new RequestError('session.verification_failed') ); - // Assign verification result and filter out account verified identifiers + // Assign the verification result and store the profile identifiers left const verifiedInteraction: AccountVerifiedInteractionResult = { ...interaction, - identifiers: identifiers.filter((identifier) => isProfileIdentifier(identifier, profile)), + identifiers: profileIdentifiers, accountId: deduplicateAccountIds[0], }; From ce1aaeeae3dbfda781f32413df76e9bd9bb84f4f Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Fri, 9 Dec 2022 12:13:22 +0800 Subject: [PATCH 144/166] fix: should block add of social connector when original target and platform exists (#2610) --- packages/core/src/routes/connector.test.ts | 62 +++++++++++++++++++++- packages/core/src/routes/connector.ts | 13 +++-- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/packages/core/src/routes/connector.test.ts b/packages/core/src/routes/connector.test.ts index a817d4be1..2b42e5681 100644 --- a/packages/core/src/routes/connector.test.ts +++ b/packages/core/src/routes/connector.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import type { EmailConnector, SmsConnector } from '@logto/connector-kit'; import { ConnectorPlatform, MessageTypes } from '@logto/connector-kit'; import { ConnectorType } from '@logto/schemas'; @@ -159,6 +160,14 @@ describe('connector route', () => { }, ]); mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); + getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([ + { + dbEntry: { ...mockConnector, connectorId: 'id0' }, + metadata: { ...mockMetadata, id: 'id0' }, + type: ConnectorType.Sms, + ...mockLogtoConnector, + }, + ]); const response = await connectorRequest.post('/connectors').send({ connectorId: 'connectorId', config: { cliend_id: 'client_id', client_secret: 'client_secret' }, @@ -194,13 +203,27 @@ describe('connector route', () => { loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([ { ...mockConnectorFactory, - metadata: { ...mockConnectorFactory.metadata, id: 'id0', isStandard: true }, + metadata: { + ...mockMetadata, + id: 'id0', + isStandard: true, + platform: ConnectorPlatform.Universal, + }, }, ]); mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 1 }); + getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([ + { + dbEntry: { ...mockConnector, connectorId: 'id0' }, + metadata: { ...mockMetadata, id: 'id0', platform: ConnectorPlatform.Universal }, + type: ConnectorType.Social, + ...mockLogtoConnector, + }, + ]); const response = await connectorRequest.post('/connectors').send({ connectorId: 'id0', config: { cliend_id: 'client_id', client_secret: 'client_secret' }, + metadata: { target: 'new_target' }, }); expect(response.body).toMatchObject( expect.objectContaining({ @@ -209,6 +232,7 @@ describe('connector route', () => { cliend_id: 'client_id', client_secret: 'client_secret', }, + metadata: { target: 'new_target' }, }) ); expect(response).toHaveProperty('statusCode', 200); @@ -264,7 +288,7 @@ describe('connector route', () => { expect(deleteConnectorByIds).toHaveBeenCalledWith(['id']); }); - it('throws when add more than 1 social connector instance with same target and platform', async () => { + it('throws when add more than 1 social connector instance with same target and platform (add from standard connector)', async () => { loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([ { ...mockConnectorFactory, @@ -296,6 +320,39 @@ describe('connector route', () => { }); expect(response).toHaveProperty('statusCode', 422); }); + + it('throws when add more than 1 social connector instance with same target and platform (add social connector)', async () => { + loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([ + { + ...mockConnectorFactory, + metadata: { + ...mockConnectorFactory.metadata, + id: 'id0', + platform: ConnectorPlatform.Universal, + target: 'target', + isStandard: true, + }, + }, + ]); + mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); + getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([ + { + dbEntry: { ...mockConnector, connectorId: 'id0', metadata: { target: 'target' } }, + metadata: { + ...mockMetadata, + id: 'id0', + target: 'target', + platform: ConnectorPlatform.Universal, + }, + type: ConnectorType.Social, + ...mockLogtoConnector, + }, + ]); + const response = await connectorRequest.post('/connectors').send({ + connectorId: 'id0', + }); + expect(response).toHaveProperty('statusCode', 422); + }); }); describe('POST /connectors/:id/test', () => { @@ -426,3 +483,4 @@ describe('connector route', () => { }); }); }); +/* eslint-enable max-lines */ diff --git a/packages/core/src/routes/connector.ts b/packages/core/src/routes/connector.ts index 3f3b6f6f8..b30fb5e68 100644 --- a/packages/core/src/routes/connector.ts +++ b/packages/core/src/routes/connector.ts @@ -138,13 +138,16 @@ export default function connectorRoutes(router: T) { }) ); - if (body.metadata?.target && connectorFactory.type === ConnectorType.Social) { + if (connectorFactory.type === ConnectorType.Social) { const connectors = await getLogtoConnectors(); + const connectorTarget = body.metadata?.target ?? connectorFactory.metadata.target; assertThat( - !connectors.some( - ({ metadata: { target, platform } }) => - target === body.metadata?.target && platform === connectorFactory.metadata.platform - ), + !connectors + .filter(({ type }) => type === ConnectorType.Social) + .some( + ({ metadata: { target, platform } }) => + target === connectorTarget && platform === connectorFactory.metadata.platform + ), new RequestError({ code: 'connector.multiple_target_with_same_platform', status: 422 }) ); } From 6a03e286c1bdff3fae126b817289a3fd3b0fb0a0 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 9 Dec 2022 12:19:25 +0800 Subject: [PATCH 145/166] refactor(console): support small-size `CopyToClipBoard` (#2617) --- .../CopyToClipboard/index.module.scss | 33 ++++++++++++++----- .../src/components/CopyToClipboard/index.tsx | 20 ++++++----- .../src/pages/ApiResourceDetails/index.tsx | 2 +- .../src/pages/ApplicationDetails/index.tsx | 2 +- .../console/src/pages/UserDetails/index.tsx | 2 +- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/packages/console/src/components/CopyToClipboard/index.module.scss b/packages/console/src/components/CopyToClipboard/index.module.scss index 8a5daa2c2..8ec1bfd29 100644 --- a/packages/console/src/components/CopyToClipboard/index.module.scss +++ b/packages/console/src/components/CopyToClipboard/index.module.scss @@ -31,18 +31,35 @@ text-overflow: ellipsis; } + .copyToolTipAnchor { - margin-left: _.unit(3); + margin-left: _.unit(2); } + } - .copyIconButton { - height: 20px; - width: 20px; + &.default { + .row { + .copyToolTipAnchor { + margin-left: _.unit(3); + } + } + } - .copyIcon { - svg { - width: 16px; - height: 16px; + &.small { + .row { + .copyToolTipAnchor { + margin-left: _.unit(1); + } + + .iconButton { + height: 20px; + width: 20px; + + .icon { + svg { + width: 12px; + height: 12px; + } } } } diff --git a/packages/console/src/components/CopyToClipboard/index.tsx b/packages/console/src/components/CopyToClipboard/index.tsx index e72083e2a..4e840be80 100644 --- a/packages/console/src/components/CopyToClipboard/index.tsx +++ b/packages/console/src/components/CopyToClipboard/index.tsx @@ -18,6 +18,7 @@ type Props = { className?: string; variant?: 'text' | 'contained' | 'border' | 'icon'; hasVisibilityToggle?: boolean; + size?: 'default' | 'small'; }; type CopyState = TFuncKey<'translation', 'admin_console.general'>; @@ -27,6 +28,7 @@ const CopyToClipboard = ({ className, hasVisibilityToggle, variant = 'contained', + size = 'default', }: Props) => { const copyIconReference = useRef(null); const [copyState, setCopyState] = useState('copy'); @@ -59,7 +61,7 @@ const CopyToClipboard = ({ return (
{ @@ -72,11 +74,13 @@ const CopyToClipboard = ({
{variant !== 'icon' &&
{displayValue}
} {hasVisibilityToggle && ( -
- - {showHiddenContent ? : } - -
+ + {showHiddenContent ? : } + )} diff --git a/packages/console/src/pages/ApiResourceDetails/index.tsx b/packages/console/src/pages/ApiResourceDetails/index.tsx index 2f40825a2..8e07b4d93 100644 --- a/packages/console/src/pages/ApiResourceDetails/index.tsx +++ b/packages/console/src/pages/ApiResourceDetails/index.tsx @@ -117,7 +117,7 @@ const ApiResourceDetails = () => {
{data.name}
- +
{!isLogtoManagementApiResource && ( diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx index 54e7c71a3..b6264809e 100644 --- a/packages/console/src/pages/ApplicationDetails/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/index.tsx @@ -140,7 +140,7 @@ const ApplicationDetails = () => {
{t(`${applicationTypeI18nKey[data.type]}.title`)}
App ID
- +
diff --git a/packages/console/src/pages/UserDetails/index.tsx b/packages/console/src/pages/UserDetails/index.tsx index 79b1b6457..fe6e6797f 100644 --- a/packages/console/src/pages/UserDetails/index.tsx +++ b/packages/console/src/pages/UserDetails/index.tsx @@ -110,7 +110,7 @@ const UserDetails = () => { )}
User ID
- +
From 25e90fb4085dc8f708a5fe9fbeadd5fdca1fd00c Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 9 Dec 2022 19:46:54 +0800 Subject: [PATCH 146/166] style(console): fix dropdown title text ellipsis (#2628) --- packages/console/src/components/Select/index.module.scss | 7 ++++--- packages/console/src/components/Select/index.tsx | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/console/src/components/Select/index.module.scss b/packages/console/src/components/Select/index.module.scss index 708568ce1..9a3d5f24a 100644 --- a/packages/console/src/components/Select/index.module.scss +++ b/packages/console/src/components/Select/index.module.scss @@ -15,9 +15,10 @@ font: var(--font-body-medium); cursor: pointer; position: relative; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + + .title { + @include _.text-ellipsis; + } &.open { border-color: var(--color-primary); diff --git a/packages/console/src/components/Select/index.tsx b/packages/console/src/components/Select/index.tsx index a5f3bb5a4..d81a9ec9a 100644 --- a/packages/console/src/components/Select/index.tsx +++ b/packages/console/src/components/Select/index.tsx @@ -80,7 +80,7 @@ const Select = ({ } }} > - {current?.title ?? placeholder} +
{current?.title ?? placeholder}
{isClearable && ( Date: Fri, 9 Dec 2022 19:47:46 +0800 Subject: [PATCH 147/166] style(console): add radio groups margin-top on the sie branding page (#2629) --- .../src/pages/SignInExperience/tabs/Branding/BrandingForm.tsx | 2 +- .../console/src/pages/SignInExperience/tabs/index.module.scss | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/console/src/pages/SignInExperience/tabs/Branding/BrandingForm.tsx b/packages/console/src/pages/SignInExperience/tabs/Branding/BrandingForm.tsx index 67bb7bbc2..8f9eb4f16 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Branding/BrandingForm.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Branding/BrandingForm.tsx @@ -33,7 +33,7 @@ const BrandingForm = () => { control={control} defaultValue={BrandingStyle.Logo_Slogan} render={({ field: { onChange, value, name } }) => ( - + Date: Fri, 9 Dec 2022 19:57:28 +0800 Subject: [PATCH 148/166] refactor(console): support close modal on `esc` pressed (#2627) --- .../components/Sidebar/components/Contact/index.tsx | 2 ++ .../console/src/components/ConfirmModal/index.tsx | 2 ++ packages/console/src/pages/ApiResources/index.tsx | 4 ++++ .../Applications/components/CreateForm/index.tsx | 7 ++++++- packages/console/src/pages/Applications/index.tsx | 9 +++++++-- .../pages/Connectors/components/CreateForm/index.tsx | 11 ++++++++++- .../src/pages/Settings/components/ChangePassword.tsx | 4 ++++ .../components/Welcome/GuideModal.tsx | 7 ++++++- .../ManageLanguage/LanguageEditor/index.tsx | 8 +++++++- .../pages/UserDetails/components/CreateSuccess.tsx | 8 +++++++- packages/console/src/pages/UserDetails/index.tsx | 4 ++++ packages/console/src/pages/Users/index.tsx | 4 ++++ packages/ui/src/components/ConfirmModal/AcModal.tsx | 2 ++ .../ui/src/components/ConfirmModal/MobileModal.tsx | 2 ++ 14 files changed, 67 insertions(+), 7 deletions(-) diff --git a/packages/console/src/components/AppContent/components/Sidebar/components/Contact/index.tsx b/packages/console/src/components/AppContent/components/Sidebar/components/Contact/index.tsx index 5904145c0..16a8b0328 100644 --- a/packages/console/src/components/AppContent/components/Sidebar/components/Contact/index.tsx +++ b/packages/console/src/components/AppContent/components/Sidebar/components/Contact/index.tsx @@ -19,9 +19,11 @@ const Contact = ({ isOpen, onCancel }: Props) => { return (
diff --git a/packages/console/src/components/ConfirmModal/index.tsx b/packages/console/src/components/ConfirmModal/index.tsx index 495053b80..dd85c6e93 100644 --- a/packages/console/src/components/ConfirmModal/index.tsx +++ b/packages/console/src/components/ConfirmModal/index.tsx @@ -39,9 +39,11 @@ const ConfirmModal = ({ }: ConfirmModalProps) => { return ( { }} /> { + setIsCreateFormOpen(false); + }} > { diff --git a/packages/console/src/pages/Applications/components/CreateForm/index.tsx b/packages/console/src/pages/Applications/components/CreateForm/index.tsx index ccf8c2722..ba5eee512 100644 --- a/packages/console/src/pages/Applications/components/CreateForm/index.tsx +++ b/packages/console/src/pages/Applications/components/CreateForm/index.tsx @@ -118,7 +118,12 @@ const CreateForm = ({ onClose }: Props) => { {createdApp && ( - + )} diff --git a/packages/console/src/pages/Applications/index.tsx b/packages/console/src/pages/Applications/index.tsx index 4442e7a9c..52f65dc06 100644 --- a/packages/console/src/pages/Applications/index.tsx +++ b/packages/console/src/pages/Applications/index.tsx @@ -54,18 +54,23 @@ const Applications = () => { }} /> { + navigate('/applications'); + }} > { - navigate('/applications'); - if (createdApp) { toast.success(t('applications.application_created', { name: createdApp.name })); navigate(`/applications/${createdApp.id}`); + + return; } + navigate('/applications'); }} /> diff --git a/packages/console/src/pages/Connectors/components/CreateForm/index.tsx b/packages/console/src/pages/Connectors/components/CreateForm/index.tsx index fa127654b..8cc3d233f 100644 --- a/packages/console/src/pages/Connectors/components/CreateForm/index.tsx +++ b/packages/console/src/pages/Connectors/components/CreateForm/index.tsx @@ -124,9 +124,13 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => { return ( { + onClose?.(); + }} > { /> )} {activeFactory && ( - + )} diff --git a/packages/console/src/pages/Settings/components/ChangePassword.tsx b/packages/console/src/pages/Settings/components/ChangePassword.tsx index f3903eecf..97e8372d2 100644 --- a/packages/console/src/pages/Settings/components/ChangePassword.tsx +++ b/packages/console/src/pages/Settings/components/ChangePassword.tsx @@ -60,9 +60,13 @@ const ChangePassword = () => {
{ + setIsOpen(false); + }} > { }; return ( - +
diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.tsx b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.tsx index a97263df4..15e24a1dc 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/index.tsx @@ -71,7 +71,13 @@ const LanguageEditorModal = ({ isOpen, onClose }: Props) => { }; return ( - +
+ { { + setIsResetPasswordFormOpen(false); + }} > { }} /> { + setIsCreateFormOpen(false); + }} > { diff --git a/packages/ui/src/components/ConfirmModal/AcModal.tsx b/packages/ui/src/components/ConfirmModal/AcModal.tsx index 0ed4b1645..a019afe90 100644 --- a/packages/ui/src/components/ConfirmModal/AcModal.tsx +++ b/packages/ui/src/components/ConfirmModal/AcModal.tsx @@ -27,6 +27,7 @@ const AcModal = ({ return ( { contentRef.current?.focus(); }} + onRequestClose={onClose} >
{ return (
{children}
From 25f0a2e1589e589f9b3ee47b85329c984748cf1d Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 12 Dec 2022 13:43:23 +0800 Subject: [PATCH 149/166] test: use native ESM (#2621) --- .github/CODEOWNERS | 1 + .github/workflows/main.yml | 8 +- packages/cli/jest.config.js | 10 + packages/cli/jest.config.ts | 17 - packages/cli/jest.setup.ts | 19 - packages/cli/package.json | 11 +- .../database/alteration/index.test.ts | 62 ++- .../src/commands/database/alteration/index.ts | 55 +-- .../src/commands/database/alteration/utils.ts | 55 +++ .../commands/database/alteration/version.ts | 1 + packages/cli/src/queries/logto-config.test.ts | 1 + packages/cli/src/utilities.ts | 3 +- packages/cli/tsconfig.test.json | 3 +- packages/console/package.json | 2 +- packages/core/jest.config.js | 14 + packages/core/jest.config.ts | 18 - packages/core/jest.setup.js | 39 ++ packages/core/jest.setup.ts | 34 -- packages/core/package.json | 14 +- packages/core/src/__mocks__/connector.ts | 2 + packages/core/src/app/init.test.ts | 49 +- .../src/connectors/utilities/index.test.ts | 12 +- .../core/src/database/insert-into.test.ts | 1 + .../core/src/database/update-where.test.ts | 1 + packages/core/src/database/update-where.ts | 2 +- .../core/src/i18n/detect-language.test.ts | 3 +- packages/core/src/lib/passcode.test.ts | 81 ++-- packages/core/src/lib/phrase.test.ts | 9 +- .../src/lib/sign-in-experience/index.test.ts | 59 ++- .../lib/sign-in-experience/sign-up.test.ts | 9 +- packages/core/src/lib/user.test.ts | 24 +- packages/core/src/middleware/koa-auth.test.ts | 26 +- .../koa-connector-error-handler.test.ts | 2 + .../src/middleware/koa-error-handler.test.ts | 4 +- .../core/src/middleware/koa-guard.test.ts | 6 +- .../core/src/middleware/koa-i18next.test.ts | 11 +- .../src/middleware/koa-log-session.test.ts | 18 +- packages/core/src/middleware/koa-log.test.ts | 24 +- .../middleware/koa-oidc-error-handler.test.ts | 2 + .../src/middleware/koa-pagination.test.ts | 5 +- .../src/middleware/koa-root-proxy.test.ts | 2 + .../koa-slonik-error-handler.test.ts | 2 + .../core/src/middleware/koa-spa-proxy.test.ts | 13 +- .../middleware/koa-spa-session-guard.test.ts | 34 +- .../src/middleware/koa-welcome-proxy.test.ts | 13 +- packages/core/src/oidc/adapter.test.ts | 32 +- packages/core/src/queries/application.test.ts | 1 + packages/core/src/queries/connector.test.ts | 1 + .../src/queries/oidc-model-instance.test.ts | 23 +- packages/core/src/queries/passcode.test.ts | 1 + packages/core/src/queries/resource.test.ts | 1 + packages/core/src/queries/roles.test.ts | 1 + packages/core/src/queries/setting.test.ts | 1 + .../src/queries/sign-in-experience.test.ts | 1 + packages/core/src/queries/user.test.ts | 1 + packages/core/src/routes/admin-user.test.ts | 88 ++-- packages/core/src/routes/application.test.ts | 17 +- packages/core/src/routes/authn.test.ts | 40 +- packages/core/src/routes/connector.test.ts | 130 +++-- .../core/src/routes/connector.update.test.ts | 51 +- .../core/src/routes/custom-phrase.test.ts | 85 ++-- packages/core/src/routes/dashboard.test.ts | 40 +- .../actions/submit-interaction.test.ts | 30 +- .../core/src/routes/interaction/index.test.ts | 110 +++-- .../koa-interaction-body-guard.test.ts | 13 +- ...oa-session-sign-inexperience-guard.test.ts | 35 +- .../utils/find-user-by-identifier.test.ts | 28 +- .../utils/passcode-validation.test.ts | 19 +- .../utils/social-verification.test.ts | 10 +- .../identifier-payload-verification.test.ts | 82 ++-- .../mandatory-user-profile-validation.test.ts | 26 +- ...ofile-verification-forgot-password.test.ts | 27 +- ...profile-verification-profile-exist.test.ts | 30 +- ...le-verification-profile-registered.test.ts | 48 +- ...-verification-protected-identifier.test.ts | 21 +- .../user-identity-verification.test.ts | 26 +- packages/core/src/routes/log.test.ts | 26 +- .../routes/phrase.content-language.test.ts | 47 +- packages/core/src/routes/phrase.test.ts | 89 ++-- packages/core/src/routes/profile.test.ts | 198 ++++---- packages/core/src/routes/profile.ts | 3 - packages/core/src/routes/resource.test.ts | 37 +- packages/core/src/routes/role.test.ts | 6 +- .../core/src/routes/session/utils.test.ts | 2 +- packages/core/src/routes/setting.test.ts | 19 +- .../sign-in-experience.branding.guard.test.ts | 22 +- .../sign-in-experience.color.guard.test.ts | 22 +- .../routes/sign-in-experience.guard.test.ts | 32 +- .../src/routes/sign-in-experience.test.ts | 62 +-- packages/core/src/routes/swagger.test.ts | 16 +- packages/core/src/routes/well-known.test.ts | 65 ++- .../src/test-utils/jest-koa-mocks/LICENSE | 9 + .../src/test-utils/jest-koa-mocks/README.md | 3 + .../jest-koa-mocks/create-mock-context.ts | 119 +++++ .../jest-koa-mocks/create-mock-cookies.ts | 37 ++ packages/core/src/test-utils/oidc-provider.ts | 24 + .../oidc-provider-event-listener.test.ts | 20 +- packages/core/src/utils/test-utils.ts | 4 +- packages/shared/package.json | 8 + packages/shared/src/esm/index.ts | 2 + packages/shared/src/esm/mock-esm.ts | 72 +++ .../shared/src/{utils => esm}/module-proxy.ts | 1 + .../shared/src/include.d/import-meta.d.ts | 10 + packages/shared/src/utils/index.ts | 1 - pnpm-lock.yaml | 455 +++++++++--------- 105 files changed, 1691 insertions(+), 1520 deletions(-) create mode 100644 packages/cli/jest.config.js delete mode 100644 packages/cli/jest.config.ts delete mode 100644 packages/cli/jest.setup.ts create mode 100644 packages/cli/src/commands/database/alteration/utils.ts create mode 100644 packages/core/jest.config.js delete mode 100644 packages/core/jest.config.ts create mode 100644 packages/core/jest.setup.js delete mode 100644 packages/core/jest.setup.ts create mode 100644 packages/core/src/test-utils/jest-koa-mocks/LICENSE create mode 100644 packages/core/src/test-utils/jest-koa-mocks/README.md create mode 100644 packages/core/src/test-utils/jest-koa-mocks/create-mock-context.ts create mode 100644 packages/core/src/test-utils/jest-koa-mocks/create-mock-cookies.ts create mode 100644 packages/core/src/test-utils/oidc-provider.ts create mode 100644 packages/shared/src/esm/index.ts create mode 100644 packages/shared/src/esm/mock-esm.ts rename packages/shared/src/{utils => esm}/module-proxy.ts (91%) create mode 100644 packages/shared/src/include.d/import-meta.d.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7fceb6eef..d9132021c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,3 @@ /packages/schemas/tables @simeng-li @wangsijie +/packages/core/src/routes/session @simeng-li @wangsijie /.changeset @gao-sun diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index af5765f0d..ce7f5aeb4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,20 +20,22 @@ jobs: - name: Setup Node and pnpm uses: silverhand-io/actions-node-pnpm-run-steps@v2 + with: + node-version: 18 - name: Build run: pnpm ci:build main-lint: - # avoid out of memory issue since macOS has bigger memory - # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources - runs-on: ubuntu-latest-4-cores + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node and pnpm uses: silverhand-io/actions-node-pnpm-run-steps@v2 + with: + node-version: 18 - name: Prepack run: pnpm prepack diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js new file mode 100644 index 000000000..652de5472 --- /dev/null +++ b/packages/cli/jest.config.js @@ -0,0 +1,10 @@ +const config = { + coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'], + coverageReporters: ['text-summary', 'lcov'], + roots: ['./lib'], + moduleNameMapper: { + '^(chalk|inquirer)$': '/../shared/lib/esm/module-proxy.js', + }, +}; + +export default config; diff --git a/packages/cli/jest.config.ts b/packages/cli/jest.config.ts deleted file mode 100644 index b6fcd166f..000000000 --- a/packages/cli/jest.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Config } from '@silverhand/jest-config'; -import { merge } from '@silverhand/jest-config'; - -const config: Config.InitialOptions = { - ...merge({ - setupFilesAfterEnv: ['./jest.setup.ts'], - roots: ['./src'], - moduleNameMapper: { - '^(\\.{1,2}/.*)\\.js$': '$1', - '^(chalk|inquirer|ora)$': '/../shared/src/utils/module-proxy.ts', - }, - }), - // Will update common config soon - transformIgnorePatterns: ['node_modules/(?!(.*(nanoid|jose|ky|@logto))/)'], -}; - -export default config; diff --git a/packages/cli/jest.setup.ts b/packages/cli/jest.setup.ts deleted file mode 100644 index 7ed7c3aaa..000000000 --- a/packages/cli/jest.setup.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Mocking `import.meta.url` and `got` here since they inevitably needs native ESM, but jest is sticking with CJS. - * Will figure out a way to run tests in native ESM mode. - */ - -jest.mock('./src/commands/database/alteration/meta-url.js', () => ({ - metaUrl: 'file:///', -})); - -jest.mock('./src/meta-url.js', () => ({ - metaUrl: 'file:///', -})); - -jest.mock('got', () => ({ - got: {}, -})); - -// Make lint-staged happy -export {}; diff --git a/packages/cli/package.json b/packages/cli/package.json index afb40c820..6ddca2849 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -25,13 +25,14 @@ "precommit": "lint-staged", "prepare:package-json": "node -p \"'export const packageJson = ' + JSON.stringify(require('./package.json'), undefined, 2) + ';'\" > src/package-json.ts", "build": "rimraf lib && pnpm prepare:package-json && tsc -p tsconfig.build.json", + "build:test": "rm -rf build/ && tsc -p tsconfig.test.json --sourcemap", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental", "start": "node .", "start:dev": "ts-node --files src/index.ts", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", - "test": "jest", - "test:ci": "jest", + "test": "pnpm build:test && NODE_OPTIONS=--experimental-vm-modules jest", + "test:ci": "pnpm run test", "prepack": "pnpm build" }, "engines": { @@ -52,7 +53,7 @@ "hpagent": "^1.2.0", "inquirer": "^8.2.2", "nanoid": "^3.3.4", - "ora": "^5.0.0", + "ora": "^6.1.2", "p-retry": "^4.6.1", "roarr": "^7.11.0", "semver": "^7.3.8", @@ -65,13 +66,13 @@ }, "devDependencies": { "@silverhand/eslint-config": "1.3.0", - "@silverhand/jest-config": "1.2.2", "@silverhand/ts-config": "1.2.1", "@types/fs-extra": "^9.0.13", "@types/inquirer": "^8.2.1", "@types/jest": "^29.1.2", "@types/node": "^16.0.0", "@types/semver": "^7.3.12", + "@types/sinon": "^10.0.13", "@types/tar": "^6.1.2", "@types/yargs": "^17.0.13", "eslint": "^8.21.0", @@ -79,7 +80,7 @@ "lint-staged": "^13.0.0", "prettier": "^2.7.1", "rimraf": "^3.0.2", - "ts-node": "^10.9.1", + "sinon": "^15.0.0", "typescript": "^4.7.4" }, "eslintConfig": { diff --git a/packages/cli/src/commands/database/alteration/index.test.ts b/packages/cli/src/commands/database/alteration/index.test.ts index 9d546a63a..6033c4c23 100644 --- a/packages/cli/src/commands/database/alteration/index.test.ts +++ b/packages/cli/src/commands/database/alteration/index.test.ts @@ -1,42 +1,45 @@ +import { mockEsmWithActual } from '@logto/shared/esm'; +import Sinon from 'sinon'; import { createMockPool } from 'slonik'; -import * as queries from '../../../queries/logto-config.js'; -import type { QueryType } from '../../../test-utilities.js'; -import * as functions from './index.js'; import { chooseAlterationsByVersion } from './version.js'; -const mockQuery: jest.MockedFunction = jest.fn(); +const { jest } = import.meta; const pool = createMockPool({ - query: async (sql, values) => { - return mockQuery(sql, values); - }, + query: jest.fn(), }); +const files = Object.freeze([ + { filename: '1.0.0-1663923770-a.js', path: '/alterations-js/1.0.0-1663923770-a.js' }, + { filename: '1.0.0-1663923771-b.js', path: '/alterations-js/1.0.0-1663923771-b.js' }, + { filename: '1.0.0-1663923772-c.js', path: '/alterations-js/1.0.0-1663923772-c.js' }, +]); + +await mockEsmWithActual('./utils.js', () => ({ + getAlterationFiles: async () => files, +})); + +const { getCurrentDatabaseAlterationTimestamp } = await mockEsmWithActual( + '../../../queries/logto-config.js', + () => ({ + getCurrentDatabaseAlterationTimestamp: jest.fn(), + }) +); + +const { getUndeployedAlterations } = await import('./index.js'); + describe('getUndeployedAlterations()', () => { - const files = Object.freeze([ - { filename: '1.0.0-1663923770-a.js', path: '/alterations-js/1.0.0-1663923770-a.js' }, - { filename: '1.0.0-1663923771-b.js', path: '/alterations-js/1.0.0-1663923771-b.js' }, - { filename: '1.0.0-1663923772-c.js', path: '/alterations-js/1.0.0-1663923772-c.js' }, - ]); - - beforeEach(() => { - // `getAlterationFiles()` will ensure the order - jest.spyOn(functions, 'getAlterationFiles').mockResolvedValueOnce([...files]); - }); - it('returns all files if database timestamp is 0', async () => { - jest.spyOn(queries, 'getCurrentDatabaseAlterationTimestamp').mockResolvedValueOnce(0); + getCurrentDatabaseAlterationTimestamp.mockResolvedValue(0); - await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual(files); + await expect(getUndeployedAlterations(pool)).resolves.toEqual(files); }); it('returns files whose timestamp is greater then database timestamp', async () => { - jest - .spyOn(queries, 'getCurrentDatabaseAlterationTimestamp') - .mockResolvedValueOnce(1_663_923_770); + getCurrentDatabaseAlterationTimestamp.mockResolvedValue(1_663_923_770); - await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual([files[1], files[2]]); + await expect(getUndeployedAlterations(pool)).resolves.toEqual([files[1], files[2]]); }); }); @@ -58,12 +61,19 @@ describe('chooseAlterationsByVersion()', () => { 'next1-1663923781-c.js', ].map((filename) => ({ filename, path: '/alterations/' + filename })) ); + const stub = Sinon.stub(global, 'process').value({ stdin: { isTTY: false } }); + + afterAll(() => { + stub.restore(); + }); it('chooses nothing when input version is invalid', async () => { await expect(chooseAlterationsByVersion(files, 'next1')).rejects.toThrow( - 'Invalid Version: next1' + new TypeError('Invalid Version: next1') + ); + await expect(chooseAlterationsByVersion([], 'ok')).rejects.toThrow( + new TypeError('Invalid Version: ok') ); - await expect(chooseAlterationsByVersion([], 'ok')).rejects.toThrow('Invalid Version: ok'); }); it('chooses correct alteration files', async () => { diff --git a/packages/cli/src/commands/database/alteration/index.ts b/packages/cli/src/commands/database/alteration/index.ts index 61dd31262..17a93a3b1 100644 --- a/packages/cli/src/commands/database/alteration/index.ts +++ b/packages/cli/src/commands/database/alteration/index.ts @@ -1,11 +1,6 @@ -import { fileURLToPath } from 'node:url'; -import path from 'path'; - import type { AlterationScript } from '@logto/schemas/lib/types/alteration.js'; -import { findPackage } from '@logto/shared'; import { conditionalString } from '@silverhand/essentials'; import chalk from 'chalk'; -import fsExtra from 'fs-extra'; import type { DatabasePool } from 'slonik'; import type { CommandModule } from 'yargs'; @@ -14,25 +9,11 @@ import { getCurrentDatabaseAlterationTimestamp, updateDatabaseTimestamp, } from '../../../queries/logto-config.js'; -import { getPathInModule, log } from '../../../utilities.js'; -import { metaUrl } from './meta-url.js'; +import { log } from '../../../utilities.js'; import type { AlterationFile } from './type.js'; +import { getAlterationFiles, getTimestampFromFilename } from './utils.js'; import { chooseAlterationsByVersion } from './version.js'; -const currentDirname = path.dirname(fileURLToPath(metaUrl)); -const { copy, existsSync, remove, readdir } = fsExtra; -const alterationFilenameRegex = /-(\d+)-?.*\.js$/; - -const getTimestampFromFilename = (filename: string) => { - const match = alterationFilenameRegex.exec(filename); - - if (!match?.[1]) { - throw new Error(`Can not get timestamp: ${filename}`); - } - - return Number(match[1]); -}; - const importAlterationScript = async (filePath: string): Promise => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const module = await import(filePath); @@ -41,38 +22,6 @@ const importAlterationScript = async (filePath: string): Promise => { - const alterationDirectory = getPathInModule('@logto/schemas', 'alterations-js'); - - /** - * We copy all alteration scripts to the CLI package root directory, - * since they need a proper context that includes required dependencies (such as slonik) in `node_modules/`. - * While the original `@logto/schemas` may remove them in production. - */ - const packageDirectory = await findPackage(currentDirname); - - const localAlterationDirectory = path.resolve( - packageDirectory ?? currentDirname, - 'alteration-scripts' - ); - - if (!existsSync(alterationDirectory)) { - return []; - } - - // We need to copy alteration files to execute in the CLI context to make `slonik` available - await remove(localAlterationDirectory); - await copy(alterationDirectory, localAlterationDirectory); - - const directory = await readdir(localAlterationDirectory); - const files = directory.filter((file) => alterationFilenameRegex.test(file)); - - return files - .slice() - .sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2)) - .map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename })); -}; - export const getLatestAlterationTimestamp = async () => { const files = await getAlterationFiles(); const lastFile = files[files.length - 1]; diff --git a/packages/cli/src/commands/database/alteration/utils.ts b/packages/cli/src/commands/database/alteration/utils.ts new file mode 100644 index 000000000..6037b4fc2 --- /dev/null +++ b/packages/cli/src/commands/database/alteration/utils.ts @@ -0,0 +1,55 @@ +import { fileURLToPath } from 'node:url'; +import path from 'path'; + +import { findPackage } from '@logto/shared'; +import fsExtra from 'fs-extra'; + +import { getPathInModule } from '../../../utilities.js'; +import { metaUrl } from './meta-url.js'; +import type { AlterationFile } from './type.js'; + +const currentDirname = path.dirname(fileURLToPath(metaUrl)); +const { copy, existsSync, remove, readdir } = fsExtra; +const alterationFilenameRegex = /-(\d+)-?.*\.js$/; + +export const getTimestampFromFilename = (filename: string) => { + const match = alterationFilenameRegex.exec(filename); + + if (!match?.[1]) { + throw new Error(`Can not get timestamp: ${filename}`); + } + + return Number(match[1]); +}; + +export const getAlterationFiles = async (): Promise => { + const alterationDirectory = getPathInModule('@logto/schemas', 'alterations-js'); + + /** + * We copy all alteration scripts to the CLI package root directory, + * since they need a proper context that includes required dependencies (such as slonik) in `node_modules/`. + * While the original `@logto/schemas` may remove them in production. + */ + const packageDirectory = await findPackage(currentDirname); + + const localAlterationDirectory = path.resolve( + packageDirectory ?? currentDirname, + 'alteration-scripts' + ); + + if (!existsSync(alterationDirectory)) { + return []; + } + + // We need to copy alteration files to execute in the CLI context to make `slonik` available + await remove(localAlterationDirectory); + await copy(alterationDirectory, localAlterationDirectory); + + const directory = await readdir(localAlterationDirectory); + const files = directory.filter((file) => alterationFilenameRegex.test(file)); + + return files + .slice() + .sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2)) + .map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename })); +}; diff --git a/packages/cli/src/commands/database/alteration/version.ts b/packages/cli/src/commands/database/alteration/version.ts index 2b7f30747..5e0c01fa9 100644 --- a/packages/cli/src/commands/database/alteration/version.ts +++ b/packages/cli/src/commands/database/alteration/version.ts @@ -42,6 +42,7 @@ export const chooseAlterationsByVersion = async ( .filter((version, index, self) => index === self.findIndex((another) => eq(version, another))) .slice() .sort((i, j) => compare(j, i)); + const initialSemVersion = conditional( initialVersion && initialVersion !== latestTag && new SemVer(initialVersion) ); diff --git a/packages/cli/src/queries/logto-config.test.ts b/packages/cli/src/queries/logto-config.test.ts index 2762a56d5..c19c8d283 100644 --- a/packages/cli/src/queries/logto-config.test.ts +++ b/packages/cli/src/queries/logto-config.test.ts @@ -6,6 +6,7 @@ import type { QueryType } from '../test-utilities.js'; import { expectSqlAssert } from '../test-utilities.js'; import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './logto-config.js'; +const { jest } = import.meta; const mockQuery: jest.MockedFunction = jest.fn(); const pool = createMockPool({ diff --git a/packages/cli/src/utilities.ts b/packages/cli/src/utilities.ts index 96483a899..50d35f400 100644 --- a/packages/cli/src/utilities.ts +++ b/packages/cli/src/utilities.ts @@ -11,6 +11,7 @@ import type { Progress } from 'got'; import { got } from 'got'; import { HttpsProxyAgent } from 'hpagent'; import inquirer from 'inquirer'; +import type { Options } from 'ora'; import ora from 'ora'; import { z } from 'zod'; @@ -92,7 +93,7 @@ export const getPathInModule = (moduleName: string, relativePath = '/') => export const oraPromise = async ( promise: PromiseLike, - options?: ora.Options, + options?: Options, exitOnError = false ) => { const spinner = ora(options).start(); diff --git a/packages/cli/tsconfig.test.json b/packages/cli/tsconfig.test.json index aa0bf1ab7..55de18c33 100644 --- a/packages/cli/tsconfig.test.json +++ b/packages/cli/tsconfig.test.json @@ -3,5 +3,6 @@ "compilerOptions": { "isolatedModules": false, "allowJs": true - } + }, + "include": ["src"] } diff --git a/packages/console/package.json b/packages/console/package.json index ed5834780..636d76828 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -62,7 +62,7 @@ "lint-staged": "^13.0.0", "lodash.get": "^4.4.2", "lodash.kebabcase": "^4.1.1", - "nanoid": "^3.1.23", + "nanoid": "^3.3.4", "parcel": "2.8.0", "postcss": "^8.4.6", "postcss-modules": "^4.3.0", diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js new file mode 100644 index 000000000..fff6b08fe --- /dev/null +++ b/packages/core/jest.config.js @@ -0,0 +1,14 @@ +/** @type {import('jest').Config} */ +const config = { + coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'], + coverageReporters: ['text-summary', 'lcov'], + testPathIgnorePatterns: ['/node_modules/', '/build/routes/session/'], // `routes/session` is freezed + setupFilesAfterEnv: ['jest-matcher-specific-error', './jest.setup.js'], + roots: ['./build'], + moduleNameMapper: { + '^#src/(.*)\\.js(x)?$': '/build/$1', + '^(chalk|inquirer)$': '/../shared/lib/esm/module-proxy.js', + }, +}; + +export default config; diff --git a/packages/core/jest.config.ts b/packages/core/jest.config.ts deleted file mode 100644 index 4888767bc..000000000 --- a/packages/core/jest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Config } from '@silverhand/jest-config'; -import { merge } from '@silverhand/jest-config'; - -const config: Config.InitialOptions = { - ...merge({ - testPathIgnorePatterns: ['/core/connectors/'], - setupFilesAfterEnv: ['jest-matcher-specific-error', './jest.setup.ts'], - moduleNameMapper: { - '^#src/(.*)\\.js(x)?$': '/src/$1', - '^(\\.{1,2}/.*)\\.js$': '$1', - '^(chalk|inquirer|ora)$': '/../shared/src/utils/module-proxy.ts', - }, - }), - // Will update common config soon - transformIgnorePatterns: ['node_modules/(?!(.*(nanoid|jose|ky|@logto|got))/)'], -}; - -export default config; diff --git a/packages/core/jest.setup.js b/packages/core/jest.setup.js new file mode 100644 index 000000000..deb0c995e --- /dev/null +++ b/packages/core/jest.setup.js @@ -0,0 +1,39 @@ +/** + * Setup environment variables for unit test + */ + +import { mockEsm } from '@logto/shared/esm'; +import { createMockQueryResult, createMockPool } from 'slonik'; + +const { jest } = import.meta; + +mockEsm('#src/env-set/index.js', () => ({ + MountedApps: { + Api: 'api', + Oidc: 'oidc', + Console: 'console', + DemoApp: 'demo-app', + Welcome: 'welcome', + }, + default: { + get values() { + return { + endpoint: 'https://logto.test', + adminConsoleUrl: 'https://logto.test/console', + }; + }, + get oidc() { + return { + issuer: 'https://logto.test/oidc', + }; + }, + get pool() { + return createMockPool({ query: async () => createMockQueryResult([]) }); + }, + load: jest.fn(), + }, +})); + +// Logger is not considered in all test cases +// eslint-disable-next-line unicorn/consistent-function-scoping +mockEsm('koa-logger', () => ({ default: () => (_, next) => next() })); diff --git a/packages/core/jest.setup.ts b/packages/core/jest.setup.ts deleted file mode 100644 index 2e91af71d..000000000 --- a/packages/core/jest.setup.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Setup environment variables for unit test - */ - -import envSet from '#src/env-set/index.js'; - -jest.mock('#src/lib/logto-config.js'); -jest.mock('#src/env-set/check-alteration-state.js'); - -// eslint-disable-next-line unicorn/prefer-top-level-await -(async () => { - await envSet.load(); -})(); - -/** - * Mocking `import.meta.url` and `got` here since they inevitably needs native ESM, but jest is sticking with CJS. - * Will figure out a way to run tests in native ESM mode. - */ - -jest.mock('./src/connectors/meta-url.js', () => ({ - metaUrl: 'file:///', -})); - -jest.mock('../cli/lib/meta-url.js', () => ({ - metaUrl: 'file:///', -})); - -jest.mock('../cli/lib/commands/database/alteration/meta-url.js', () => ({ - metaUrl: 'file:///', -})); - -jest.mock('got', () => ({ - got: {}, -})); diff --git a/packages/core/package.json b/packages/core/package.json index 6628584f9..23ab9455a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -14,12 +14,14 @@ "precommit": "lint-staged", "copyfiles": "copyfiles -u 1 src/**/*.md build", "build": "rm -rf build/ && tsc -p tsconfig.build.json && pnpm run copyfiles", + "build:test": "rm -rf build/ && tsc -p tsconfig.test.json --sourcemap", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", "dev": "rm -rf build/ && pnpm run copyfiles && nodemon", "start": "NODE_ENV=production node build/index.js", - "test": "jest", - "test:ci": "jest --coverage --silent", + "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", + "test": "pnpm build:test && pnpm test:only build/", + "test:ci": "pnpm run test --coverage --silent", "test:report": "codecov -F core" }, "dependencies": { @@ -56,7 +58,7 @@ "koa-router": "^12.0.0", "koa-send": "^5.0.1", "lodash.pick": "^4.4.0", - "nanoid": "^3.1.23", + "nanoid": "^3.3.4", "oidc-provider": "^7.13.0", "p-retry": "^4.6.1", "query-string": "^7.0.1", @@ -69,9 +71,7 @@ "zod": "^3.19.1" }, "devDependencies": { - "@shopify/jest-koa-mocks": "^5.0.1", "@silverhand/eslint-config": "1.3.0", - "@silverhand/jest-config": "1.2.2", "@silverhand/ts-config": "1.2.1", "@types/debug": "^4.1.7", "@types/etag": "^1.8.1", @@ -87,6 +87,7 @@ "@types/lodash.pick": "^4.4.6", "@types/node": "^16.0.0", "@types/oidc-provider": "^7.12.0", + "@types/sinon": "^10.0.13", "@types/supertest": "^2.0.11", "copyfiles": "^2.4.1", "eslint": "^8.21.0", @@ -94,10 +95,11 @@ "jest": "^29.1.2", "jest-matcher-specific-error": "^1.0.0", "lint-staged": "^13.0.0", - "nock": "^13.2.2", + "node-mocks-http": "^1.12.1", "nodemon": "^2.0.19", "openapi-types": "^12.0.0", "prettier": "^2.7.1", + "sinon": "^15.0.0", "supertest": "^6.2.2", "typescript": "^4.9.3" }, diff --git a/packages/core/src/__mocks__/connector.ts b/packages/core/src/__mocks__/connector.ts index a233f8eb3..db3db5267 100644 --- a/packages/core/src/__mocks__/connector.ts +++ b/packages/core/src/__mocks__/connector.ts @@ -33,6 +33,8 @@ export { mockMetadata3, } from './connector-base-data.js'; +const { jest } = import.meta; + export const mockConnector: Connector = { id: 'id', config: {}, diff --git a/packages/core/src/app/init.test.ts b/packages/core/src/app/init.test.ts index eea72195f..0ea5765f2 100644 --- a/packages/core/src/app/init.test.ts +++ b/packages/core/src/app/init.test.ts @@ -1,46 +1,39 @@ +import { mockEsmDefault, pickDefault } from '@logto/shared/esm'; import Koa from 'koa'; -import * as koaErrorHandler from '#src/middleware/koa-error-handler.js'; -import * as koaI18next from '#src/middleware/koa-i18next.js'; -import * as koaLog from '#src/middleware/koa-log.js'; -import * as koaOIDCErrorHandler from '#src/middleware/koa-oidc-error-handler.js'; -import * as koaSlonikErrorHandler from '#src/middleware/koa-slonik-error-handler.js'; -import * as koaSpaProxy from '#src/middleware/koa-spa-proxy.js'; -import * as initOidc from '#src/oidc/init.js'; -import * as initRouter from '#src/routes/init.js'; +import { emptyMiddleware } from '#src/utils/test-utils.js'; -import initI18n from '../i18n/init.js'; -import initApp from './init.js'; +const { jest } = import.meta; + +const middlewareList = [ + 'error-handler', + 'i18next', + 'log', + 'oidc-error-handler', + 'slonik-error-handler', + 'spa-proxy', +].map((name) => { + const mock = jest.fn(() => emptyMiddleware); + mockEsmDefault(`#src/middleware/koa-${name}.js`, () => mock); + + return mock; +}); + +const initI18n = await pickDefault(import('../i18n/init.js')); +const initApp = await pickDefault(import('./init.js')); describe('App Init', () => { const listenMock = jest.spyOn(Koa.prototype, 'listen').mockImplementation(jest.fn()); - const middlewareList = [ - koaErrorHandler, - koaI18next, - koaLog, - koaOIDCErrorHandler, - koaSlonikErrorHandler, - koaSpaProxy, - ]; - const initMethods = [initRouter, initOidc]; - - const middlewareSpys = middlewareList.map((module) => jest.spyOn(module, 'default')); - const initMethodSpys = initMethods.map((module) => jest.spyOn(module, 'default')); - it('app init properly with 404 not found route', async () => { const app = new Koa(); await initI18n(); await initApp(app); - for (const middleware of middlewareSpys) { + for (const middleware of middlewareList) { expect(middleware).toBeCalled(); } - for (const inits of initMethodSpys) { - expect(inits).toBeCalled(); - } - expect(listenMock).toBeCalled(); }); }); diff --git a/packages/core/src/connectors/utilities/index.test.ts b/packages/core/src/connectors/utilities/index.test.ts index aec5cd4a7..25b409f23 100644 --- a/packages/core/src/connectors/utilities/index.test.ts +++ b/packages/core/src/connectors/utilities/index.test.ts @@ -1,8 +1,9 @@ import type { Connector } from '@logto/schemas'; +import { mockEsmWithActual } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; -import { getConnectorConfig } from './index.js'; +const { jest } = import.meta; const connectors: Connector[] = [ { @@ -15,13 +16,12 @@ const connectors: Connector[] = [ }, ]; -const findAllConnectors = jest.fn(async () => connectors); - -jest.mock('#src/queries/connector.js', () => ({ - ...jest.requireActual('#src/queries/connector.js'), - findAllConnectors: async () => findAllConnectors(), +await mockEsmWithActual('#src/queries/connector.js', () => ({ + findAllConnectors: jest.fn(async () => connectors), })); +const { getConnectorConfig } = await import('./index.js'); + it('getConnectorConfig() should return right config', async () => { const config = await getConnectorConfig('id'); expect(config).toMatchObject({ foo: 'bar' }); diff --git a/packages/core/src/database/insert-into.test.ts b/packages/core/src/database/insert-into.test.ts index 9d1b5c045..7585eb90b 100644 --- a/packages/core/src/database/insert-into.test.ts +++ b/packages/core/src/database/insert-into.test.ts @@ -9,6 +9,7 @@ import { createTestPool } from '#src/utils/test-utils.js'; import { buildInsertInto } from './insert-into.js'; +const { jest } = import.meta; const poolSpy = jest.spyOn(envSet, 'pool', 'get'); const buildExpectedInsertIntoSql = (keys: string[]) => [ diff --git a/packages/core/src/database/update-where.test.ts b/packages/core/src/database/update-where.test.ts index 353f9f1bd..0595bf0c1 100644 --- a/packages/core/src/database/update-where.test.ts +++ b/packages/core/src/database/update-where.test.ts @@ -8,6 +8,7 @@ import { createTestPool } from '#src/utils/test-utils.js'; import { buildUpdateWhere } from './update-where.js'; +const { jest } = import.meta; const poolSpy = jest.spyOn(envSet, 'pool', 'get'); describe('buildUpdateWhere()', () => { diff --git a/packages/core/src/database/update-where.ts b/packages/core/src/database/update-where.ts index 23f7cd28b..99d3e74b4 100644 --- a/packages/core/src/database/update-where.ts +++ b/packages/core/src/database/update-where.ts @@ -1,6 +1,6 @@ import type { SchemaLike, GeneratedSchema } from '@logto/schemas'; -import type { UpdateWhereData } from '@logto/shared'; import { convertToIdentifiers, convertToPrimitiveOrSql, conditionalSql } from '@logto/shared'; +import type { UpdateWhereData } from '@logto/shared'; import type { Truthy } from '@silverhand/essentials'; import { notFalsy } from '@silverhand/essentials'; import { sql } from 'slonik'; diff --git a/packages/core/src/i18n/detect-language.test.ts b/packages/core/src/i18n/detect-language.test.ts index a0170e7ce..ab4e25814 100644 --- a/packages/core/src/i18n/detect-language.test.ts +++ b/packages/core/src/i18n/detect-language.test.ts @@ -1,6 +1,7 @@ -import { createMockContext } from '@shopify/jest-koa-mocks'; import type { ParameterizedContext } from 'koa'; +import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js'; + import detectLanguage from './detect-language.js'; describe('detectLanguage', () => { diff --git a/packages/core/src/lib/passcode.test.ts b/packages/core/src/lib/passcode.test.ts index 0162c1aba..d4f531848 100644 --- a/packages/core/src/lib/passcode.test.ts +++ b/packages/core/src/lib/passcode.test.ts @@ -1,56 +1,47 @@ import { ConnectorType } from '@logto/connector-kit'; import type { Passcode } from '@logto/schemas'; import { PasscodeType } from '@logto/schemas'; +import { mockEsm } from '@logto/shared/esm'; import { any } from 'zod'; import { mockConnector, mockMetadata } from '#src/__mocks__/index.js'; import { defaultConnectorMethods } from '#src/connectors/consts.js'; -import { getLogtoConnectors } from '#src/connectors/index.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { - consumePasscode, - deletePasscodesByIds, + +const { jest } = import.meta; + +const { findUnconsumedPasscodeByJtiAndType, findUnconsumedPasscodesByJtiAndType, + deletePasscodesByIds, increasePasscodeTryCount, insertPasscode, -} from '#src/queries/passcode.js'; + consumePasscode, +} = mockEsm('#src/queries/passcode.js', () => ({ + findUnconsumedPasscodesByJtiAndType: jest.fn(), + findUnconsumedPasscodeByJtiAndType: jest.fn(), + deletePasscodesByIds: jest.fn(), + insertPasscode: jest.fn(), + consumePasscode: jest.fn(), + increasePasscodeTryCount: jest.fn(), +})); -import { +const { getLogtoConnectors } = mockEsm('#src/connectors/index.js', () => ({ + getLogtoConnectors: jest.fn(), +})); + +const { createPasscode, passcodeExpiration, passcodeMaxTryCount, passcodeLength, sendPasscode, verifyPasscode, -} from './passcode.js'; - -jest.mock('#src/queries/passcode.js'); -jest.mock('#src/connectors.js'); - -const mockedFindUnconsumedPasscodesByJtiAndType = - findUnconsumedPasscodesByJtiAndType as jest.MockedFunction< - typeof findUnconsumedPasscodesByJtiAndType - >; -const mockedFindUnconsumedPasscodeByJtiAndType = - findUnconsumedPasscodeByJtiAndType as jest.MockedFunction< - typeof findUnconsumedPasscodeByJtiAndType - >; -const mockedDeletePasscodesByIds = deletePasscodesByIds as jest.MockedFunction< - typeof deletePasscodesByIds ->; -const mockedInsertPasscode = insertPasscode as jest.MockedFunction; -const mockedGetLogtoConnectors = getLogtoConnectors as jest.MockedFunction< - typeof getLogtoConnectors ->; -const mockedConsumePasscode = consumePasscode as jest.MockedFunction; -const mockedIncreasePasscodeTryCount = increasePasscodeTryCount as jest.MockedFunction< - typeof increasePasscodeTryCount ->; +} = await import('./passcode.js'); beforeAll(() => { - mockedFindUnconsumedPasscodesByJtiAndType.mockResolvedValue([]); - mockedInsertPasscode.mockImplementation(async (data): Promise => { + findUnconsumedPasscodesByJtiAndType.mockResolvedValue([]); + insertPasscode.mockImplementation(async (data): Promise => { return { phone: null, email: null, @@ -88,7 +79,7 @@ describe('createPasscode', () => { it('should disable existing passcode', async () => { const email = 'jony@example.com'; const jti = 'jti'; - mockedFindUnconsumedPasscodesByJtiAndType.mockResolvedValue([ + findUnconsumedPasscodesByJtiAndType.mockResolvedValue([ { id: 'id', interactionJti: jti, @@ -104,7 +95,7 @@ describe('createPasscode', () => { await createPasscode(jti, PasscodeType.SignIn, { email, }); - expect(mockedDeletePasscodesByIds).toHaveBeenCalledWith(['id']); + expect(deletePasscodesByIds).toHaveBeenCalledWith(['id']); }); }); @@ -127,7 +118,7 @@ describe('sendPasscode', () => { }); it('should throw error when email or sms connector can not be found', async () => { - mockedGetLogtoConnectors.mockResolvedValueOnce([ + getLogtoConnectors.mockResolvedValueOnce([ { ...defaultConnectorMethods, dbEntry: { @@ -164,7 +155,7 @@ describe('sendPasscode', () => { it('should call sendPasscode with params matching', async () => { const sendMessage = jest.fn(); - mockedGetLogtoConnectors.mockResolvedValueOnce([ + getLogtoConnectors.mockResolvedValueOnce([ { ...defaultConnectorMethods, configGuard: any(), @@ -230,20 +221,20 @@ describe('verifyPasscode', () => { }; it('should mark as consumed on successful verification', async () => { - mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode); + findUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode); await verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, { phone: 'phone' }); - expect(mockedConsumePasscode).toHaveBeenCalledWith(passcode.id); + expect(consumePasscode).toHaveBeenCalledWith(passcode.id); }); it('should fail when passcode not found', async () => { - mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(null); + findUnconsumedPasscodeByJtiAndType.mockResolvedValue(null); await expect( verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, { phone: 'phone' }) ).rejects.toThrow(new RequestError('passcode.not_found')); }); it('should fail when phone mismatch', async () => { - mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode); + findUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode); await expect( verifyPasscode(passcode.interactionJti, passcode.type, passcode.code, { phone: 'invalid_phone', @@ -252,7 +243,7 @@ describe('verifyPasscode', () => { }); it('should fail when email mismatch', async () => { - mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue({ + findUnconsumedPasscodeByJtiAndType.mockResolvedValue({ ...passcode, phone: null, email: 'email', @@ -265,7 +256,7 @@ describe('verifyPasscode', () => { }); it('should fail when expired', async () => { - mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue({ + findUnconsumedPasscodeByJtiAndType.mockResolvedValue({ ...passcode, createdAt: Date.now() - passcodeExpiration - 100, }); @@ -275,7 +266,7 @@ describe('verifyPasscode', () => { }); it('should fail when exceed max count', async () => { - mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue({ + findUnconsumedPasscodeByJtiAndType.mockResolvedValue({ ...passcode, tryCount: passcodeMaxTryCount, }); @@ -285,10 +276,10 @@ describe('verifyPasscode', () => { }); it('should fail when invalid code, and should increase try_count', async () => { - mockedFindUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode); + findUnconsumedPasscodeByJtiAndType.mockResolvedValue(passcode); await expect( verifyPasscode(passcode.interactionJti, passcode.type, 'invalid', { phone: 'phone' }) ).rejects.toThrow(new RequestError('passcode.code_mismatch')); - expect(mockedIncreasePasscodeTryCount).toHaveBeenCalledWith(passcode.id); + expect(increasePasscodeTryCount).toHaveBeenCalledWith(passcode.id); }); }); diff --git a/packages/core/src/lib/phrase.test.ts b/packages/core/src/lib/phrase.test.ts index 9d14d21bf..607bb7169 100644 --- a/packages/core/src/lib/phrase.test.ts +++ b/packages/core/src/lib/phrase.test.ts @@ -1,5 +1,6 @@ import resource from '@logto/phrases-ui'; import type { CustomPhrase } from '@logto/schemas'; +import { mockEsm } from '@logto/shared/esm'; import deepmerge from 'deepmerge'; import { @@ -12,8 +13,8 @@ import { zhHkTag, } from '#src/__mocks__/custom-phrase.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { getPhrase } from '#src/lib/phrase.js'; +const { jest } = import.meta; const englishBuiltInPhrase = resource[enTag]; const customOnlyLanguage = zhHkTag; @@ -39,10 +40,12 @@ const findCustomPhraseByLanguageTag = jest.fn(async (languageTag: string) => { return mockCustomPhrase; }); -jest.mock('#src/queries/custom-phrase.js', () => ({ - findCustomPhraseByLanguageTag: async (key: string) => findCustomPhraseByLanguageTag(key), +mockEsm('#src/queries/custom-phrase.js', () => ({ + findCustomPhraseByLanguageTag, })); +const { getPhrase } = await import('#src/lib/phrase.js'); + afterEach(() => { jest.clearAllMocks(); }); diff --git a/packages/core/src/lib/sign-in-experience/index.test.ts b/packages/core/src/lib/sign-in-experience/index.test.ts index 710877d31..b41930a41 100644 --- a/packages/core/src/lib/sign-in-experience/index.test.ts +++ b/packages/core/src/lib/sign-in-experience/index.test.ts @@ -2,6 +2,7 @@ import type { LanguageTag } from '@logto/language-kit'; import { builtInLanguages } from '@logto/phrases-ui'; import type { CreateSignInExperience, SignInExperience } from '@logto/schemas'; import { BrandingStyle } from '@logto/schemas'; +import { mockEsm } from '@logto/shared/esm'; import { socialTarget01, @@ -10,42 +11,36 @@ import { mockSignInExperience, mockSocialConnectors, } from '#src/__mocks__/index.js'; -import type { LogtoConnector } from '#src/connectors/types.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { + +const { jest } = import.meta; +const allCustomLanguageTags: LanguageTag[] = []; + +const { findAllCustomLanguageTags } = mockEsm('#src/queries/custom-phrase.js', () => ({ + findAllCustomLanguageTags: jest.fn(async () => allCustomLanguageTags), +})); +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): Promise => ({ + ...mockSignInExperience, + ...data, + }) + ), + }) +); + +const { validateBranding, validateTermsOfUse, validateLanguageInfo, removeUnavailableSocialConnectorTargets, -} from '#src/lib/sign-in-experience/index.js'; -import { updateDefaultSignInExperience } from '#src/queries/sign-in-experience.js'; - -const allCustomLanguageTags: LanguageTag[] = []; -const findAllCustomLanguageTags = jest.fn(async () => allCustomLanguageTags); -const getLogtoConnectorsPlaceHolder = jest.fn() as jest.MockedFunction< - () => Promise ->; -const findDefaultSignInExperience = jest.fn() as jest.MockedFunction< - () => Promise ->; - -jest.mock('#src/queries/custom-phrase.js', () => ({ - findAllCustomLanguageTags: async () => findAllCustomLanguageTags(), -})); - -jest.mock('#src/connectors.js', () => ({ - getLogtoConnectors: async () => getLogtoConnectorsPlaceHolder(), -})); - -jest.mock('#src/queries/sign-in-experience.js', () => ({ - findDefaultSignInExperience: async () => findDefaultSignInExperience(), - updateDefaultSignInExperience: jest.fn( - async (data: Partial): Promise => ({ - ...mockSignInExperience, - ...data, - }) - ), -})); +} = await import('./index.js'); beforeEach(() => { jest.clearAllMocks(); @@ -163,7 +158,7 @@ describe('remove unavailable social connector targets', () => { ...mockSignInExperience, socialSignInConnectorTargets: mockSocialConnectorTargets, }); - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce(mockSocialConnectors); + getLogtoConnectors.mockResolvedValueOnce(mockSocialConnectors); expect(mockSocialConnectorTargets).toEqual([socialTarget01, socialTarget02]); await removeUnavailableSocialConnectorTargets(); expect(updateDefaultSignInExperience).toBeCalledWith({ diff --git a/packages/core/src/lib/sign-in-experience/sign-up.test.ts b/packages/core/src/lib/sign-in-experience/sign-up.test.ts index 20b60caa1..8516237f1 100644 --- a/packages/core/src/lib/sign-in-experience/sign-up.test.ts +++ b/packages/core/src/lib/sign-in-experience/sign-up.test.ts @@ -1,17 +1,18 @@ import { ConnectorType, SignInIdentifier } from '@logto/schemas'; +import { mockEsmWithActual } from '@logto/shared/esm'; import { mockAliyunDmConnector, mockAliyunSmsConnector, mockSignUp } from '#src/__mocks__/index.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { validateSignUp } from './sign-up.js'; - +const { jest } = import.meta; const enabledConnectors = [mockAliyunDmConnector, mockAliyunSmsConnector]; -jest.mock('#src/lib/session.js', () => ({ - ...jest.requireActual('#src/lib/session.js'), +await mockEsmWithActual('#src/lib/session.js', () => ({ getApplicationIdFromInteraction: jest.fn(), })); +const { validateSignUp } = await import('./sign-up.js'); + describe('validate sign-up', () => { describe('There must be at least one connector for the specific identifier.', () => { test('should throw when there is no email connector and identifier is email', async () => { diff --git a/packages/core/src/lib/user.test.ts b/packages/core/src/lib/user.test.ts index 91020dff6..54d75926b 100644 --- a/packages/core/src/lib/user.test.ts +++ b/packages/core/src/lib/user.test.ts @@ -1,20 +1,22 @@ import { UsersPasswordEncryptionMethod } from '@logto/schemas'; +import { mockEsmWithActual } from '@logto/shared/esm'; -import { hasUserWithId, updateUserById } from '#src/queries/user.js'; +const { jest } = import.meta; -import { encryptUserPassword, generateUserId } from './user.js'; +const { updateUserById, hasUserWithId } = await mockEsmWithActual('#src/queries/user.js', () => ({ + updateUserById: jest.fn(), + hasUserWithId: jest.fn(), +})); -jest.mock('#src/queries/user.js'); +const { encryptUserPassword, generateUserId } = await import('./user.js'); describe('generateUserId()', () => { afterEach(() => { - (hasUserWithId as jest.MockedFunction).mockClear(); + hasUserWithId.mockClear(); }); it('generates user ID with correct length when no conflict found', async () => { - const mockedHasUserWithId = ( - hasUserWithId as jest.MockedFunction - ).mockImplementationOnce(async () => false); + const mockedHasUserWithId = hasUserWithId.mockImplementationOnce(async () => false); await expect(generateUserId()).resolves.toHaveLength(12); expect(mockedHasUserWithId).toBeCalledTimes(1); @@ -23,9 +25,7 @@ describe('generateUserId()', () => { it('generates user ID with correct length when retry limit is not reached', async () => { // eslint-disable-next-line @silverhand/fp/no-let let tried = 0; - const mockedHasUserWithId = ( - hasUserWithId as jest.MockedFunction - ).mockImplementation(async () => { + const mockedHasUserWithId = hasUserWithId.mockImplementation(async () => { if (tried) { return false; } @@ -41,9 +41,7 @@ describe('generateUserId()', () => { }); it('rejects with correct error message when retry limit is reached', async () => { - const mockedHasUserWithId = ( - hasUserWithId as jest.MockedFunction - ).mockImplementation(async () => true); + const mockedHasUserWithId = hasUserWithId.mockImplementation(async () => true); await expect(generateUserId(10)).rejects.toThrow( 'Cannot generate user ID in reasonable retries' diff --git a/packages/core/src/middleware/koa-auth.test.ts b/packages/core/src/middleware/koa-auth.test.ts index 20510477c..a064bec7a 100644 --- a/packages/core/src/middleware/koa-auth.test.ts +++ b/packages/core/src/middleware/koa-auth.test.ts @@ -1,5 +1,5 @@ import { UserRole } from '@logto/schemas'; -import { jwtVerify } from 'jose'; +import { mockEsm, pickDefault } from '@logto/shared/esm'; import type { Context } from 'koa'; import type { IRouterParamContext } from 'koa-router'; @@ -8,12 +8,15 @@ import RequestError from '#src/errors/RequestError/index.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import type { WithAuthContext } from './koa-auth.js'; -import koaAuth from './koa-auth.js'; -jest.mock('jose', () => ({ - jwtVerify: jest.fn(() => ({ payload: { sub: 'fooUser', role_names: ['admin'] } })), +const { jest } = import.meta; + +const { jwtVerify } = mockEsm('jose', () => ({ + jwtVerify: jest.fn().mockReturnValue({ payload: { sub: 'fooUser', role_names: ['admin'] } }), })); +const koaAuth = await pickDefault(import('./koa-auth.js')); + describe('koaAuth middleware', () => { const baseCtx = createContextWithRouteParameters(); @@ -136,8 +139,7 @@ describe('koaAuth middleware', () => { }); it('expect to throw if jwt sub is missing', async () => { - const mockJwtVerify = jwtVerify as jest.Mock; - mockJwtVerify.mockImplementationOnce(() => ({ payload: {} })); + jwtVerify.mockImplementationOnce(() => ({ payload: {} })); ctx.request = { ...ctx.request, @@ -150,8 +152,7 @@ describe('koaAuth middleware', () => { }); it('expect to have `client` type per jwt verify result', async () => { - const mockJwtVerify = jwtVerify as jest.Mock; - mockJwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'bar', client_id: 'bar' } })); + jwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'bar', client_id: 'bar' } })); ctx.request = { ...ctx.request, @@ -165,8 +166,7 @@ describe('koaAuth middleware', () => { }); it('expect to throw if jwt role_names is missing', async () => { - const mockJwtVerify = jwtVerify as jest.Mock; - mockJwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'fooUser' } })); + jwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'fooUser' } })); ctx.request = { ...ctx.request, @@ -179,8 +179,7 @@ describe('koaAuth middleware', () => { }); it('expect to throw if jwt role_names does not include admin', async () => { - const mockJwtVerify = jwtVerify as jest.Mock; - mockJwtVerify.mockImplementationOnce(() => ({ + jwtVerify.mockImplementationOnce(() => ({ payload: { sub: 'fooUser', role_names: ['foo'] }, })); @@ -195,8 +194,7 @@ describe('koaAuth middleware', () => { }); it('expect to throw unauthorized error if unknown error occurs', async () => { - const mockJwtVerify = jwtVerify as jest.Mock; - mockJwtVerify.mockImplementationOnce(() => { + jwtVerify.mockImplementationOnce(() => { throw new Error('unknown error'); }); ctx.request = { diff --git a/packages/core/src/middleware/koa-connector-error-handler.test.ts b/packages/core/src/middleware/koa-connector-error-handler.test.ts index 7feae32a6..aa425465d 100644 --- a/packages/core/src/middleware/koa-connector-error-handler.test.ts +++ b/packages/core/src/middleware/koa-connector-error-handler.test.ts @@ -5,6 +5,8 @@ import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import koaConnectorErrorHandler from './koa-connector-error-handler.js'; +const { jest } = import.meta; + describe('koaConnectorErrorHandler middleware', () => { const next = jest.fn(); const ctx = createContextWithRouteParameters(); diff --git a/packages/core/src/middleware/koa-error-handler.test.ts b/packages/core/src/middleware/koa-error-handler.test.ts index d7646ab94..450ec4d26 100644 --- a/packages/core/src/middleware/koa-error-handler.test.ts +++ b/packages/core/src/middleware/koa-error-handler.test.ts @@ -1,10 +1,12 @@ -import { createMockContext } from '@shopify/jest-koa-mocks'; import createHttpError from 'http-errors'; import RequestError from '#src/errors/RequestError/index.js'; +import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js'; import koaErrorHandler from './koa-error-handler.js'; +const { jest } = import.meta; + describe('koaErrorHandler middleware', () => { const mockBody = { data: 'foo' }; diff --git a/packages/core/src/middleware/koa-guard.test.ts b/packages/core/src/middleware/koa-guard.test.ts index de0316f4d..8342be47c 100644 --- a/packages/core/src/middleware/koa-guard.test.ts +++ b/packages/core/src/middleware/koa-guard.test.ts @@ -1,10 +1,12 @@ +import { mockEsmDefault } from '@logto/shared/esm'; import { z } from 'zod'; import { emptyMiddleware, createContextWithRouteParameters } from '#src/utils/test-utils.js'; -import koaGuard, { isGuardMiddleware } from './koa-guard.js'; +const { jest } = import.meta; -jest.mock('koa-body', () => emptyMiddleware); +mockEsmDefault('koa-body', () => emptyMiddleware); +const { default: koaGuard, isGuardMiddleware } = await import('./koa-guard.js'); describe('koaGuardMiddleware', () => { describe('isGuardMiddleware', () => { diff --git a/packages/core/src/middleware/koa-i18next.test.ts b/packages/core/src/middleware/koa-i18next.test.ts index a7f79f5ef..1f6f4adf0 100644 --- a/packages/core/src/middleware/koa-i18next.test.ts +++ b/packages/core/src/middleware/koa-i18next.test.ts @@ -1,13 +1,14 @@ +import { mockEsmDefault, pickDefault } from '@logto/shared/esm'; import i18next from 'i18next'; -import initI18n from '#src/i18n/init.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; -import koaI18next from './koa-i18next.js'; +const { jest } = import.meta; +const mockLanguage = () => ['zh-cn']; +mockEsmDefault('#src/i18n/detect-language.js', () => mockLanguage); -// Can not access outter scope function in jest mock -// eslint-disable-next-line unicorn/consistent-function-scoping -jest.mock('#src/i18n/detect-language.js', () => () => ['zh-cn']); +const initI18n = await pickDefault(import('#src/i18n/init.js')); +const koaI18next = await pickDefault(import('./koa-i18next.js')); const changLanguageSpy = jest.spyOn(i18next, 'changeLanguage'); describe('koaI18next', () => { diff --git a/packages/core/src/middleware/koa-log-session.test.ts b/packages/core/src/middleware/koa-log-session.test.ts index dea68604a..2cfb4467c 100644 --- a/packages/core/src/middleware/koa-log-session.test.ts +++ b/packages/core/src/middleware/koa-log-session.test.ts @@ -4,13 +4,10 @@ import koaLogSession from '#src/middleware/koa-log-session.js'; import type { WithLogContext } from '#src/middleware/koa-log.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; -const interactionDetails: jest.MockedFunction<() => Promise> = jest.fn(async () => ({})); +const { jest } = import.meta; -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails, - })), -})); +const provider = new Provider('https://logto.test'); +const interactionDetails = jest.spyOn(provider, 'interactionDetails'); describe('koaLogSession', () => { const sessionId = 'sessionId'; @@ -19,6 +16,7 @@ describe('koaLogSession', () => { const log = jest.fn(); const next = jest.fn(); + // @ts-expect-error for testing interactionDetails.mockResolvedValue({ jti: sessionId, params: { @@ -37,7 +35,7 @@ describe('koaLogSession', () => { log, }; - await expect(koaLogSession(new Provider(''))(ctx, next)).resolves.not.toThrow(); + await expect(koaLogSession(provider)(ctx, next)).resolves.not.toThrow(); expect(interactionDetails).toHaveBeenCalled(); }); @@ -48,7 +46,7 @@ describe('koaLogSession', () => { log, }; - await expect(koaLogSession(new Provider(''))(ctx, next)).resolves.not.toThrow(); + await expect(koaLogSession(provider)(ctx, next)).resolves.not.toThrow(); expect(addLogContext).toHaveBeenCalledWith({ sessionId, applicationId }); }); @@ -59,7 +57,7 @@ describe('koaLogSession', () => { log, }; - await expect(koaLogSession(new Provider(''))(ctx, next)).resolves.not.toThrow(); + await expect(koaLogSession(provider)(ctx, next)).resolves.not.toThrow(); expect(next).toHaveBeenCalled(); }); @@ -74,6 +72,6 @@ describe('koaLogSession', () => { throw new Error('message'); }); - await expect(koaLogSession(new Provider(''))(ctx, next)).resolves.not.toThrow(); + await expect(koaLogSession(provider)(ctx, next)).resolves.not.toThrow(); }); }); diff --git a/packages/core/src/middleware/koa-log.test.ts b/packages/core/src/middleware/koa-log.test.ts index af535c7cf..cedf4346f 100644 --- a/packages/core/src/middleware/koa-log.test.ts +++ b/packages/core/src/middleware/koa-log.test.ts @@ -1,29 +1,31 @@ import type { LogPayload } from '@logto/schemas'; import { LogResult } from '@logto/schemas'; +import { mockEsm, pickDefault } from '@logto/shared/esm'; import i18next from 'i18next'; import RequestError from '#src/errors/RequestError/index.js'; -import { insertLog } from '#src/queries/log.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import type { WithLogContext } from './koa-log.js'; -import koaLog from './koa-log.js'; + +const { jest } = import.meta; const nanoIdMock = 'mockId'; const addLogContext = jest.fn(); const log = jest.fn(); -jest.mock('#src/queries/log.js', () => ({ - insertLog: jest.fn(async () => 0), +const { insertLog } = mockEsm('#src/queries/log.js', () => ({ + insertLog: jest.fn(), })); -jest.mock('nanoid', () => ({ - nanoid: jest.fn(() => nanoIdMock), +mockEsm('nanoid', () => ({ + nanoid: () => nanoIdMock, })); +const koaLog = await pickDefault(import('./koa-log.js')); + describe('koaLog middleware', () => { - const insertLogMock = insertLog as jest.Mock; const type = 'SignInUsernamePassword'; const mockPayload: LogPayload = { userId: 'foo', @@ -54,7 +56,7 @@ describe('koaLog middleware', () => { }; await koaLog()(ctx, next); - expect(insertLogMock).toBeCalledWith({ + expect(insertLog).toBeCalledWith({ id: nanoIdMock, type, payload: { @@ -79,7 +81,7 @@ describe('koaLog middleware', () => { // eslint-disable-next-line unicorn/consistent-function-scoping, @typescript-eslint/no-empty-function const next = async () => {}; await koaLog()(ctx, next); - expect(insertLogMock).not.toBeCalled(); + expect(insertLog).not.toBeCalled(); }); describe('should insert an error log with the error message when next() throws an error', () => { @@ -101,7 +103,7 @@ describe('koaLog middleware', () => { }; await expect(koaLog()(ctx, next)).rejects.toMatchError(error); - expect(insertLogMock).toBeCalledWith({ + expect(insertLog).toBeCalledWith({ id: nanoIdMock, type, payload: { @@ -135,7 +137,7 @@ describe('koaLog middleware', () => { }; await expect(koaLog()(ctx, next)).rejects.toMatchError(error); - expect(insertLogMock).toBeCalledWith({ + expect(insertLog).toBeCalledWith({ id: nanoIdMock, type, payload: { diff --git a/packages/core/src/middleware/koa-oidc-error-handler.test.ts b/packages/core/src/middleware/koa-oidc-error-handler.test.ts index 36778fb4d..0dcd79460 100644 --- a/packages/core/src/middleware/koa-oidc-error-handler.test.ts +++ b/packages/core/src/middleware/koa-oidc-error-handler.test.ts @@ -5,6 +5,8 @@ import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import koaOIDCErrorHandler from './koa-oidc-error-handler.js'; +const { jest } = import.meta; + describe('koaOIDCErrorHandler middleware', () => { const next = jest.fn(); const ctx = createContextWithRouteParameters(); diff --git a/packages/core/src/middleware/koa-pagination.test.ts b/packages/core/src/middleware/koa-pagination.test.ts index 2f74fb0ad..3f7da06ad 100644 --- a/packages/core/src/middleware/koa-pagination.test.ts +++ b/packages/core/src/middleware/koa-pagination.test.ts @@ -1,9 +1,12 @@ -import { createMockContext } from '@shopify/jest-koa-mocks'; import type { Context } from 'koa'; +import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js'; + import type { WithPaginationContext } from './koa-pagination.js'; import koaPagination from './koa-pagination.js'; +const { jest } = import.meta; + const next = jest.fn(); const setHeader = jest.fn(); const links = new Set(); diff --git a/packages/core/src/middleware/koa-root-proxy.test.ts b/packages/core/src/middleware/koa-root-proxy.test.ts index 381f4bd0d..7105161e0 100644 --- a/packages/core/src/middleware/koa-root-proxy.test.ts +++ b/packages/core/src/middleware/koa-root-proxy.test.ts @@ -2,6 +2,8 @@ import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import koaRootProxy from './koa-root-proxy.js'; +const { jest } = import.meta; + describe('koaRootProxy', () => { const next = jest.fn(); diff --git a/packages/core/src/middleware/koa-slonik-error-handler.test.ts b/packages/core/src/middleware/koa-slonik-error-handler.test.ts index 5e46ee1b6..6c189a4a2 100644 --- a/packages/core/src/middleware/koa-slonik-error-handler.test.ts +++ b/packages/core/src/middleware/koa-slonik-error-handler.test.ts @@ -7,6 +7,8 @@ import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import koaSlonikErrorHandler from './koa-slonik-error-handler.js'; +const { jest } = import.meta; + describe('koaSlonikErrorHandler middleware', () => { const next = jest.fn(); const ctx = createContextWithRouteParameters(); diff --git a/packages/core/src/middleware/koa-spa-proxy.test.ts b/packages/core/src/middleware/koa-spa-proxy.test.ts index b8b6a7ec4..e84cc7845 100644 --- a/packages/core/src/middleware/koa-spa-proxy.test.ts +++ b/packages/core/src/middleware/koa-spa-proxy.test.ts @@ -1,18 +1,21 @@ +import { mockEsmDefault, pickDefault } from '@logto/shared/esm'; + import envSet, { MountedApps } from '#src/env-set/index.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; -import koaSpaProxy from './koa-spa-proxy.js'; +const { jest } = import.meta; const mockProxyMiddleware = jest.fn(); const mockStaticMiddleware = jest.fn(); -jest.mock('fs/promises', () => ({ - ...jest.requireActual('fs/promises'), +mockEsmDefault('fs/promises', () => ({ readdir: jest.fn().mockResolvedValue(['sign-in']), })); -jest.mock('koa-proxies', () => jest.fn(() => mockProxyMiddleware)); -jest.mock('#src/middleware/koa-serve-static.js', () => jest.fn(() => mockStaticMiddleware)); +mockEsmDefault('koa-proxies', () => jest.fn(() => mockProxyMiddleware)); +mockEsmDefault('#src/middleware/koa-serve-static.js', () => jest.fn(() => mockStaticMiddleware)); + +const koaSpaProxy = await pickDefault(import('./koa-spa-proxy.js')); describe('koaSpaProxy middleware', () => { const envBackup = process.env; diff --git a/packages/core/src/middleware/koa-spa-session-guard.test.ts b/packages/core/src/middleware/koa-spa-session-guard.test.ts index 1a30e1496..f80c22475 100644 --- a/packages/core/src/middleware/koa-spa-session-guard.test.ts +++ b/packages/core/src/middleware/koa-spa-session-guard.test.ts @@ -1,23 +1,25 @@ +import { mockEsmWithActual } from '@logto/shared/esm'; import { Provider } from 'oidc-provider'; import { MountedApps } from '#src/env-set/index.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; -import koaSpaSessionGuard, { sessionNotFoundPath, guardedPath } from './koa-spa-session-guard.js'; +const { jest } = import.meta; -jest.mock('fs/promises', () => ({ - ...jest.requireActual('fs/promises'), +await mockEsmWithActual('fs/promises', () => ({ readdir: jest.fn().mockResolvedValue(['index.js']), })); -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails: jest.fn(), - })), -})); +const { + default: koaSpaSessionGuard, + sessionNotFoundPath, + guardedPath, +} = await import('./koa-spa-session-guard.js'); describe('koaSpaSessionGuard', () => { const envBackup = process.env; + const provider = new Provider('https://logto.test'); + const interactionDetails = jest.spyOn(provider, 'interactionDetails'); beforeEach(() => { process.env = { ...envBackup }; @@ -33,7 +35,6 @@ describe('koaSpaSessionGuard', () => { for (const app of Object.values(MountedApps)) { // eslint-disable-next-line @typescript-eslint/no-loop-func it(`${app} path should not redirect`, async () => { - const provider = new Provider(''); const ctx = createContextWithRouteParameters({ url: `/${app}/foo`, }); @@ -45,9 +46,7 @@ describe('koaSpaSessionGuard', () => { } it(`should not redirect for path ${sessionNotFoundPath}`, async () => { - const provider = new Provider(''); - - (provider.interactionDetails as jest.Mock).mockRejectedValue(new Error('session not found')); + interactionDetails.mockRejectedValue(new Error('session not found')); const ctx = createContextWithRouteParameters({ url: `${sessionNotFoundPath}`, }); @@ -56,9 +55,7 @@ describe('koaSpaSessionGuard', () => { }); it(`should not redirect for path /callback`, async () => { - const provider = new Provider(''); - - (provider.interactionDetails as jest.Mock).mockRejectedValue(new Error('session not found')); + interactionDetails.mockRejectedValue(new Error('session not found')); const ctx = createContextWithRouteParameters({ url: '/callback/github', }); @@ -67,7 +64,8 @@ describe('koaSpaSessionGuard', () => { }); it('should not redirect if session found', async () => { - const provider = new Provider(''); + // @ts-expect-error for testing + interactionDetails.mockResolvedValue({}); const ctx = createContextWithRouteParameters({ url: `/sign-in`, }); @@ -78,9 +76,7 @@ describe('koaSpaSessionGuard', () => { for (const path of guardedPath) { // eslint-disable-next-line @typescript-eslint/no-loop-func it(`should redirect if session not found for ${path}`, async () => { - const provider = new Provider(''); - - (provider.interactionDetails as jest.Mock).mockRejectedValue(new Error('session not found')); + interactionDetails.mockRejectedValue(new Error('session not found')); const ctx = createContextWithRouteParameters({ url: `${path}/foo`, }); diff --git a/packages/core/src/middleware/koa-welcome-proxy.test.ts b/packages/core/src/middleware/koa-welcome-proxy.test.ts index ac79f9b2b..5750416aa 100644 --- a/packages/core/src/middleware/koa-welcome-proxy.test.ts +++ b/packages/core/src/middleware/koa-welcome-proxy.test.ts @@ -1,13 +1,16 @@ +import { mockEsm, pickDefault } from '@logto/shared/esm'; + import envSet, { MountedApps } from '#src/env-set/index.js'; -import { hasActiveUsers } from '#src/queries/user.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; -import koaWelcomeProxy from './koa-welcome-proxy.js'; +const { jest } = import.meta; -jest.mock('#src/queries/user.js', () => ({ +const { hasActiveUsers } = mockEsm('#src/queries/user.js', () => ({ hasActiveUsers: jest.fn(), })); +const koaWelcomeProxy = await pickDefault(import('./koa-welcome-proxy.js')); + describe('koaWelcomeProxy', () => { const next = jest.fn(); @@ -18,7 +21,7 @@ describe('koaWelcomeProxy', () => { it('should redirect to admin console if has AdminUsers', async () => { const { endpoint } = envSet.values; - (hasActiveUsers as jest.Mock).mockResolvedValue(true); + hasActiveUsers.mockResolvedValue(true); const ctx = createContextWithRouteParameters({ url: `/${MountedApps.Welcome}`, }); @@ -31,7 +34,7 @@ describe('koaWelcomeProxy', () => { it('should redirect to welcome page if has no Users', async () => { const { endpoint } = envSet.values; - (hasActiveUsers as jest.Mock).mockResolvedValue(false); + hasActiveUsers.mockResolvedValue(false); const ctx = createContextWithRouteParameters({ url: `/${MountedApps.Welcome}`, }); diff --git a/packages/core/src/oidc/adapter.test.ts b/packages/core/src/oidc/adapter.test.ts index f2dc9526e..7d9f24c81 100644 --- a/packages/core/src/oidc/adapter.test.ts +++ b/packages/core/src/oidc/adapter.test.ts @@ -1,24 +1,18 @@ import type { Application } from '@logto/schemas'; +import { mockEsm } from '@logto/shared/esm'; import snakecaseKeys from 'snakecase-keys'; import { mockApplication } from '#src/__mocks__/index.js'; -import { - consumeInstanceById, - destroyInstanceById, - findPayloadById, - findPayloadByPayloadField, - revokeInstanceByGrantId, - upsertInstance, -} from '#src/queries/oidc-model-instance.js'; -import postgresAdapter from './adapter.js'; import { getConstantClientMetadata } from './utils.js'; -jest.mock('#src/queries/application.js', () => ({ +const { jest } = import.meta; + +mockEsm('#src/queries/application.js', () => ({ findApplicationById: jest.fn(async (): Promise => mockApplication), })); -jest.mock('#src/queries/oidc-model-instance.js', () => ({ +mockEsm('#src/queries/oidc-model-instance.js', () => ({ upsertInstance: jest.fn(), findPayloadById: jest.fn(), findPayloadByPayloadField: jest.fn(), @@ -27,15 +21,25 @@ jest.mock('#src/queries/oidc-model-instance.js', () => ({ revokeInstanceByGrantId: jest.fn(), })); -const now = Date.now(); - -jest.mock( +mockEsm( 'date-fns', jest.fn(() => ({ addSeconds: jest.fn((_: Date, seconds: number) => new Date(now + seconds * 1000)), })) ); +const { default: postgresAdapter } = await import('./adapter.js'); +const { + consumeInstanceById, + destroyInstanceById, + findPayloadById, + findPayloadByPayloadField, + revokeInstanceByGrantId, + upsertInstance, +} = await import('#src/queries/oidc-model-instance.js'); + +const now = Date.now(); + describe('postgres Adapter', () => { it('Client Modal', async () => { const rejectError = new Error('Not implemented'); diff --git a/packages/core/src/queries/application.test.ts b/packages/core/src/queries/application.test.ts index 794388d38..87fa00b5e 100644 --- a/packages/core/src/queries/application.test.ts +++ b/packages/core/src/queries/application.test.ts @@ -18,6 +18,7 @@ import { deleteApplicationById, } from './application.js'; +const { jest } = import.meta; const mockQuery: jest.MockedFunction = jest.fn(); jest.spyOn(envSet, 'pool', 'get').mockReturnValue( diff --git a/packages/core/src/queries/connector.test.ts b/packages/core/src/queries/connector.test.ts index 3aa1ca6b6..05b3169b9 100644 --- a/packages/core/src/queries/connector.test.ts +++ b/packages/core/src/queries/connector.test.ts @@ -18,6 +18,7 @@ import { updateConnector, } from './connector.js'; +const { jest } = import.meta; const mockQuery: jest.MockedFunction = jest.fn(); jest.spyOn(envSet, 'pool', 'get').mockReturnValue( diff --git a/packages/core/src/queries/oidc-model-instance.test.ts b/packages/core/src/queries/oidc-model-instance.test.ts index cbdc4e1ea..3d6366a45 100644 --- a/packages/core/src/queries/oidc-model-instance.test.ts +++ b/packages/core/src/queries/oidc-model-instance.test.ts @@ -1,21 +1,14 @@ import type { CreateOidcModelInstance } from '@logto/schemas'; import { OidcModelInstances } from '@logto/schemas'; import { convertToIdentifiers } from '@logto/shared'; +import { mockEsmWithActual } from '@logto/shared/esm'; import { createMockPool, createMockQueryResult, sql } from 'slonik'; import envSet from '#src/env-set/index.js'; import type { QueryType } from '#src/utils/test-utils.js'; import { expectSqlAssert } from '#src/utils/test-utils.js'; -import { - upsertInstance, - findPayloadById, - findPayloadByPayloadField, - consumeInstanceById, - destroyInstanceById, - revokeInstanceByGrantId, -} from './oidc-model-instance.js'; - +const { jest } = import.meta; const mockQuery: jest.MockedFunction = jest.fn(); jest.spyOn(envSet, 'pool', 'get').mockReturnValue( @@ -26,11 +19,19 @@ jest.spyOn(envSet, 'pool', 'get').mockReturnValue( }) ); -jest.mock('@logto/shared', () => ({ - ...jest.requireActual('@logto/shared'), +await mockEsmWithActual('@logto/shared', () => ({ convertToTimestamp: () => 100, })); +const { + upsertInstance, + findPayloadById, + findPayloadByPayloadField, + consumeInstanceById, + destroyInstanceById, + revokeInstanceByGrantId, +} = await import('./oidc-model-instance.js'); + describe('oidc-model-instance query', () => { const { table, fields } = convertToIdentifiers(OidcModelInstances); const expiresAt = Date.now(); diff --git a/packages/core/src/queries/passcode.test.ts b/packages/core/src/queries/passcode.test.ts index 1b26c5a7d..283e1aee1 100644 --- a/packages/core/src/queries/passcode.test.ts +++ b/packages/core/src/queries/passcode.test.ts @@ -17,6 +17,7 @@ import { deletePasscodesByIds, } from './passcode.js'; +const { jest } = import.meta; const mockQuery: jest.MockedFunction = jest.fn(); jest.spyOn(envSet, 'pool', 'get').mockReturnValue( diff --git a/packages/core/src/queries/resource.test.ts b/packages/core/src/queries/resource.test.ts index 25a74e8b0..20d3cd0f8 100644 --- a/packages/core/src/queries/resource.test.ts +++ b/packages/core/src/queries/resource.test.ts @@ -18,6 +18,7 @@ import { deleteResourceById, } from './resource.js'; +const { jest } = import.meta; const mockQuery: jest.MockedFunction = jest.fn(); jest.spyOn(envSet, 'pool', 'get').mockReturnValue( diff --git a/packages/core/src/queries/roles.test.ts b/packages/core/src/queries/roles.test.ts index e24e414b0..b024b3bb9 100644 --- a/packages/core/src/queries/roles.test.ts +++ b/packages/core/src/queries/roles.test.ts @@ -9,6 +9,7 @@ import { expectSqlAssert } from '#src/utils/test-utils.js'; import { findAllRoles, findRolesByRoleNames } from './roles.js'; +const { jest } = import.meta; const mockQuery: jest.MockedFunction = jest.fn(); jest.spyOn(envSet, 'pool', 'get').mockReturnValue( diff --git a/packages/core/src/queries/setting.test.ts b/packages/core/src/queries/setting.test.ts index 7eb89ff52..ef4a2e800 100644 --- a/packages/core/src/queries/setting.test.ts +++ b/packages/core/src/queries/setting.test.ts @@ -9,6 +9,7 @@ import { expectSqlAssert } from '#src/utils/test-utils.js'; import { defaultSettingId, getSetting, updateSetting } from './setting.js'; +const { jest } = import.meta; const mockQuery: jest.MockedFunction = jest.fn(); jest.spyOn(envSet, 'pool', 'get').mockReturnValue( diff --git a/packages/core/src/queries/sign-in-experience.test.ts b/packages/core/src/queries/sign-in-experience.test.ts index 8c4f81f8e..f2977c95a 100644 --- a/packages/core/src/queries/sign-in-experience.test.ts +++ b/packages/core/src/queries/sign-in-experience.test.ts @@ -10,6 +10,7 @@ import { updateDefaultSignInExperience, } from './sign-in-experience.js'; +const { jest } = import.meta; const mockQuery: jest.MockedFunction = jest.fn(); jest.spyOn(envSet, 'pool', 'get').mockReturnValue( diff --git a/packages/core/src/queries/user.test.ts b/packages/core/src/queries/user.test.ts index b488619b9..523c514ca 100644 --- a/packages/core/src/queries/user.test.ts +++ b/packages/core/src/queries/user.test.ts @@ -26,6 +26,7 @@ import { deleteUserIdentity, } from './user.js'; +const { jest } = import.meta; const mockQuery: jest.MockedFunction = jest.fn(); jest.spyOn(envSet, 'pool', 'get').mockReturnValue( diff --git a/packages/core/src/routes/admin-user.test.ts b/packages/core/src/routes/admin-user.test.ts index b0711b92e..050cc3ab8 100644 --- a/packages/core/src/routes/admin-user.test.ts +++ b/packages/core/src/routes/admin-user.test.ts @@ -1,5 +1,6 @@ import type { CreateUser, Role, User } from '@logto/schemas'; import { userInfoSelectFields } from '@logto/schemas'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import pick from 'lodash.pick'; import { @@ -8,19 +9,9 @@ import { mockUserListResponse, mockUserResponse, } from '#src/__mocks__/index.js'; -import { encryptUserPassword } from '#src/lib/user.js'; -import { findRolesByRoleNames } from '#src/queries/roles.js'; -import { - hasUser, - findUserById, - updateUserById, - deleteUserIdentity, - deleteUserById, -} from '#src/queries/user.js'; import { createRequester } from '#src/utils/test-utils.js'; -import adminUserRoutes from './admin-user.js'; - +const { jest } = import.meta; const filterUsersWithSearch = (users: User[], search: string) => users.filter((user) => [user.username, user.primaryEmail, user.primaryPhone, user.name].some((value) => @@ -28,45 +19,44 @@ const filterUsersWithSearch = (users: User[], search: string) => ) ); -const mockFindDefaultSignInExperience = jest.fn(async () => ({ - signUp: { - identifiers: [], - password: false, - verify: false, - }, -})); - -jest.mock('#src/queries/sign-in-experience.js', () => ({ - findDefaultSignInExperience: jest.fn(async () => mockFindDefaultSignInExperience()), +mockEsm('#src/queries/sign-in-experience.js', () => ({ + findDefaultSignInExperience: jest.fn(async () => ({ + signUp: { + identifiers: [], + password: false, + verify: false, + }, + })), })); const mockHasUser = jest.fn(async () => false); const mockHasUserWithEmail = jest.fn(async () => false); const mockHasUserWithPhone = jest.fn(async () => false); -jest.mock('#src/queries/user.js', () => ({ - countUsers: jest.fn(async (search) => ({ - count: search ? filterUsersWithSearch(mockUserList, search).length : mockUserList.length, - })), - findUsers: jest.fn( - async (limit, offset, search): Promise => - search ? filterUsersWithSearch(mockUserList, search) : mockUserList - ), - findUserById: jest.fn(async (): Promise => mockUser), - hasUser: jest.fn(async () => mockHasUser()), - hasUserWithEmail: jest.fn(async () => mockHasUserWithEmail()), - hasUserWithPhone: jest.fn(async () => mockHasUserWithPhone()), - updateUserById: jest.fn( - async (_, data: Partial): Promise => ({ - ...mockUser, - ...data, - }) - ), - deleteUserById: jest.fn(), - deleteUserIdentity: jest.fn(), -})); -jest.mock('#src/lib/user.js', () => ({ - ...jest.requireActual('#src/lib/user.js'), +const { hasUser, findUserById, updateUserById, deleteUserIdentity, deleteUserById } = + await mockEsmWithActual('#src/queries/user.js', () => ({ + countUsers: jest.fn(async (search) => ({ + count: search ? filterUsersWithSearch(mockUserList, search).length : mockUserList.length, + })), + findUsers: jest.fn( + async (limit, offset, search): Promise => + search ? filterUsersWithSearch(mockUserList, search) : mockUserList + ), + findUserById: jest.fn(async (): Promise => mockUser), + hasUser: jest.fn(async () => mockHasUser()), + hasUserWithEmail: jest.fn(async () => mockHasUserWithEmail()), + hasUserWithPhone: jest.fn(async () => mockHasUserWithPhone()), + updateUserById: jest.fn( + async (_, data: Partial): Promise => ({ + ...mockUser, + ...data, + }) + ), + deleteUserById: jest.fn(), + deleteUserIdentity: jest.fn(), + })); + +const { encryptUserPassword } = await mockEsmWithActual('#src/lib/user.js', () => ({ generateUserId: jest.fn(() => 'fooId'), encryptUserPassword: jest.fn(() => ({ passwordEncrypted: 'password', @@ -80,18 +70,18 @@ jest.mock('#src/lib/user.js', () => ({ ), })); -jest.mock('#src/queries/roles.js', () => ({ +const { findRolesByRoleNames } = mockEsm('#src/queries/roles.js', () => ({ findRolesByRoleNames: jest.fn( async (): Promise => [{ id: 'role_id', name: 'admin', description: 'none' }] ), })); -const revokeInstanceByUserId = jest.fn(); -jest.mock('#src/queries/oidc-model-instance.js', () => ({ - revokeInstanceByUserId: async (modelName: string, userId: string) => - revokeInstanceByUserId(modelName, userId), +const { revokeInstanceByUserId } = mockEsm('#src/queries/oidc-model-instance.js', () => ({ + revokeInstanceByUserId: jest.fn(), })); +const adminUserRoutes = await pickDefault(import('./admin-user.js')); + describe('adminUserRoutes', () => { const userRequest = createRequester({ authedRoutes: adminUserRoutes }); diff --git a/packages/core/src/routes/application.test.ts b/packages/core/src/routes/application.test.ts index 863e4e25f..5106240df 100644 --- a/packages/core/src/routes/application.test.ts +++ b/packages/core/src/routes/application.test.ts @@ -1,13 +1,12 @@ import type { Application, CreateApplication } from '@logto/schemas'; import { ApplicationType } from '@logto/schemas'; +import { mockEsm, pickDefault } from '@logto/shared/esm'; import { mockApplication } from '#src/__mocks__/index.js'; -import { findApplicationById } from '#src/queries/application.js'; -import { createRequester } from '#src/utils/test-utils.js'; -import applicationRoutes from './application.js'; +const { jest } = import.meta; -jest.mock('#src/queries/application.js', () => ({ +const { findApplicationById } = mockEsm('#src/queries/application.js', () => ({ findTotalNumberOfApplications: jest.fn(async () => ({ count: 10 })), findAllApplications: jest.fn(async () => [mockApplication]), findApplicationById: jest.fn(async () => mockApplication), @@ -30,23 +29,21 @@ jest.mock('#src/queries/application.js', () => ({ ), })); -jest.mock('@logto/shared', () => ({ +mockEsm('@logto/shared', () => ({ // eslint-disable-next-line unicorn/consistent-function-scoping buildIdGenerator: jest.fn(() => () => 'randomId'), buildApplicationSecret: jest.fn(() => 'randomId'), })); +const { createRequester } = await import('#src/utils/test-utils.js'); +const applicationRoutes = await pickDefault(import('./application.js')); + const customClientMetadata = { corsAllowedOrigins: ['http://localhost:5000', 'http://localhost:5001', 'https://silverhand.com'], idTokenTtl: 999_999, refreshTokenTtl: 100_000_000, }; -const customOidcClientMetadata = { - redirectUris: [], - postLogoutRedirectUris: [], -}; - describe('application route', () => { const applicationRequest = createRequester({ authedRoutes: applicationRoutes }); diff --git a/packages/core/src/routes/authn.test.ts b/packages/core/src/routes/authn.test.ts index 2993dd95a..7f54d02c3 100644 --- a/packages/core/src/routes/authn.test.ts +++ b/packages/core/src/routes/authn.test.ts @@ -1,11 +1,22 @@ +import { mockEsmWithActual, pickDefault } from '@logto/shared/esm'; + import RequestError from '#src/errors/RequestError/index.js'; -import * as functions from '#src/middleware/koa-auth.js'; import { createRequester } from '#src/utils/test-utils.js'; -import authnRoutes from './authn.js'; +const { jest } = import.meta; + +const { verifyBearerTokenFromRequest } = await mockEsmWithActual( + '#src/middleware/koa-auth.js', + () => ({ + verifyBearerTokenFromRequest: jest.fn(), + }) +); + +const request = createRequester({ + anonymousRoutes: await pickDefault(import('#src/routes/authn.js')), +}); describe('authn route for Hasura', () => { - const request = createRequester({ anonymousRoutes: authnRoutes }); const mockUserId = 'foo'; const mockExpectedRole = 'some_role'; const mockUnauthorizedRole = 'V'; @@ -17,7 +28,7 @@ describe('authn route for Hasura', () => { describe('with successful verification', () => { beforeEach(() => { - jest.spyOn(functions, 'verifyBearerTokenFromRequest').mockResolvedValue({ + verifyBearerTokenFromRequest.mockResolvedValue({ clientId: 'ok', sub: mockUserId, roleNames: [mockExpectedRole], @@ -59,15 +70,13 @@ describe('authn route for Hasura', () => { describe('with failed verification', () => { beforeEach(() => { - jest - .spyOn(functions, 'verifyBearerTokenFromRequest') - .mockImplementation(async (_, resource) => { - if (resource) { - throw new RequestError({ code: 'auth.jwt_sub_missing', status: 401 }); - } + verifyBearerTokenFromRequest.mockImplementation(async (_, resource) => { + if (resource) { + throw new RequestError({ code: 'auth.jwt_sub_missing', status: 401 }); + } - return { clientId: 'not ok', sub: mockUserId }; - }); + return { clientId: 'not ok', sub: mockUserId }; + }); }); it('throws 401 if no unauthorized role presents', async () => { @@ -91,9 +100,10 @@ describe('authn route for Hasura', () => { }); it('falls back to unauthorized role if JWT is invalid', async () => { - jest - .spyOn(functions, 'verifyBearerTokenFromRequest') - .mockRejectedValue(new RequestError({ code: 'auth.jwt_sub_missing', status: 401 })); + verifyBearerTokenFromRequest.mockRejectedValue( + new RequestError({ code: 'auth.jwt_sub_missing', status: 401 }) + ); + const response = await request .get('/authn/hasura') .query({ resource: 'https://api.logto.io', unauthorizedRole: mockUnauthorizedRole }); diff --git a/packages/core/src/routes/connector.test.ts b/packages/core/src/routes/connector.test.ts index 2b42e5681..f0fd3caa6 100644 --- a/packages/core/src/routes/connector.test.ts +++ b/packages/core/src/routes/connector.test.ts @@ -2,6 +2,7 @@ import type { EmailConnector, SmsConnector } from '@logto/connector-kit'; import { ConnectorPlatform, MessageTypes } from '@logto/connector-kit'; import { ConnectorType } from '@logto/schemas'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { any } from 'zod'; import { @@ -16,36 +17,30 @@ import { mockLogtoConnector, } from '#src/__mocks__/index.js'; import { defaultConnectorMethods } from '#src/connectors/consts.js'; -import type { ConnectorFactory, LogtoConnector } from '#src/connectors/types.js'; +import type { LogtoConnector } from '#src/connectors/types.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { removeUnavailableSocialConnectorTargets } from '#src/lib/sign-in-experience/index.js'; -import { +import assertThat from '#src/utils/assert-that.js'; +import { createRequester } from '#src/utils/test-utils.js'; + +const { jest } = import.meta; + +mockEsm('#src/lib/connector.js', () => ({ + checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(), +})); + +const { removeUnavailableSocialConnectorTargets } = mockEsm( + '#src/lib/sign-in-experience/index.js', + () => ({ + removeUnavailableSocialConnectorTargets: jest.fn(), + }) +); + +const { findConnectorById, countConnectorByConnectorId, deleteConnectorById, deleteConnectorByIds, -} from '#src/queries/connector.js'; -import assertThat from '#src/utils/assert-that.js'; -import { createRequester } from '#src/utils/test-utils.js'; - -import connectorRoutes from './connector.js'; - -const loadConnectorFactoriesPlaceHolder = jest.fn() as jest.MockedFunction< - () => Promise ->; -const getLogtoConnectorsPlaceHolder = jest.fn() as jest.MockedFunction< - () => Promise ->; - -jest.mock('#src/lib/connector.js', () => ({ - checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(), -})); - -jest.mock('#src/lib/sign-in-experience/index.js', () => ({ - removeUnavailableSocialConnectorTargets: jest.fn(), -})); - -jest.mock('#src/queries/connector.js', () => ({ +} = await mockEsmWithActual('#src/queries/connector.js', () => ({ findConnectorById: jest.fn(), countConnectorByConnectorId: jest.fn(), deleteConnectorById: jest.fn(), @@ -53,11 +48,13 @@ jest.mock('#src/queries/connector.js', () => ({ insertConnector: jest.fn(async (body: unknown) => body), })); -jest.mock('#src/connectors/index.js', () => ({ - loadConnectorFactories: async () => loadConnectorFactoriesPlaceHolder(), - getLogtoConnectors: async () => getLogtoConnectorsPlaceHolder(), +// eslint-disable-next-line @typescript-eslint/ban-types +const getLogtoConnectors = jest.fn, []>(); +const { loadConnectorFactories } = mockEsm('#src/connectors/index.js', () => ({ + loadConnectorFactories: jest.fn(), + getLogtoConnectors, getLogtoConnectorById: async (connectorId: string) => { - const connectors = await getLogtoConnectorsPlaceHolder(); + const connectors = await getLogtoConnectors(); const connector = connectors.find(({ dbEntry }) => dbEntry.id === connectorId); assertThat( connector, @@ -71,6 +68,7 @@ jest.mock('#src/connectors/index.js', () => ({ return connector; }, })); +const connectorRoutes = await pickDefault(import('./connector.js')); describe('connector route', () => { const connectorRequest = createRequester({ authedRoutes: connectorRoutes }); @@ -81,13 +79,13 @@ describe('connector route', () => { }); it('throws if more than one email connector exists', async () => { - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce(mockLogtoConnectorList); + getLogtoConnectors.mockResolvedValueOnce(mockLogtoConnectorList); const response = await connectorRequest.get('/connectors').send({}); expect(response).toHaveProperty('statusCode', 400); }); it('throws if more than one SMS connector exists', async () => { - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce( + getLogtoConnectors.mockResolvedValueOnce( mockLogtoConnectorList.filter((connector) => connector.type !== ConnectorType.Email) ); const response = await connectorRequest.get('/connectors').send({}); @@ -95,7 +93,7 @@ describe('connector route', () => { }); it('shows all connectors', async () => { - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce( + getLogtoConnectors.mockResolvedValueOnce( mockLogtoConnectorList.filter((connector) => connector.type === ConnectorType.Social) ); const response = await connectorRequest.get('/connectors').send({}); @@ -105,7 +103,7 @@ describe('connector route', () => { describe('GET /connector-factories', () => { it('show all connector factories', async () => { - (loadConnectorFactoriesPlaceHolder as jest.Mock).mockResolvedValueOnce([ + loadConnectorFactories.mockResolvedValueOnce([ { ...mockConnectorFactory, metadata: mockMetadata0, type: ConnectorType.Sms }, { ...mockConnectorFactory, metadata: mockMetadata1, type: ConnectorType.Social }, { ...mockConnectorFactory, metadata: mockMetadata2, type: ConnectorType.Email }, @@ -128,39 +126,38 @@ describe('connector route', () => { }); it('throws when connector can not be found by given connectorId (locally)', async () => { - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce(mockLogtoConnectorList.slice(2)); + getLogtoConnectors.mockResolvedValueOnce(mockLogtoConnectorList.slice(2)); const response = await connectorRequest.get('/connectors/findConnector').send({}); expect(response).toHaveProperty('statusCode', 404); }); it('throws when connector can not be found by given connectorId (remotely)', async () => { - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([]); + getLogtoConnectors.mockResolvedValueOnce([]); const response = await connectorRequest.get('/connectors/id0').send({}); expect(response).toHaveProperty('statusCode', 404); }); it('shows found connector information', async () => { - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce(mockLogtoConnectorList); + getLogtoConnectors.mockResolvedValueOnce(mockLogtoConnectorList); const response = await connectorRequest.get('/connectors/id0').send({}); expect(response).toHaveProperty('statusCode', 200); }); }); describe('POST /connectors', () => { - const mockedCountConnectorByConnectorId = countConnectorByConnectorId as jest.Mock; afterEach(() => { jest.clearAllMocks(); }); it('should post a new connector record', async () => { - loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([ + loadConnectorFactories.mockResolvedValueOnce([ { ...mockConnectorFactory, metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' }, }, ]); - mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([ + countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); + getLogtoConnectors.mockResolvedValueOnce([ { dbEntry: { ...mockConnector, connectorId: 'id0' }, metadata: { ...mockMetadata, id: 'id0' }, @@ -185,13 +182,13 @@ describe('connector route', () => { }); it('throws when connector factory not found', async () => { - loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([ + loadConnectorFactories.mockResolvedValueOnce([ { ...mockConnectorFactory, metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' }, }, ]); - mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); + countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); const response = await connectorRequest.post('/connectors').send({ connectorId: 'id0', config: { cliend_id: 'client_id', client_secret: 'client_secret' }, @@ -200,7 +197,7 @@ describe('connector route', () => { }); it('should post a new record when add more than 1 instance with connector factory', async () => { - loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([ + loadConnectorFactories.mockResolvedValueOnce([ { ...mockConnectorFactory, metadata: { @@ -211,8 +208,8 @@ describe('connector route', () => { }, }, ]); - mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 1 }); - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([ + countConnectorByConnectorId.mockResolvedValueOnce({ count: 1 }); + getLogtoConnectors.mockResolvedValueOnce([ { dbEntry: { ...mockConnector, connectorId: 'id0' }, metadata: { ...mockMetadata, id: 'id0', platform: ConnectorPlatform.Universal }, @@ -239,13 +236,13 @@ describe('connector route', () => { }); it('throws when add more than 1 instance with non-connector factory', async () => { - loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([ + loadConnectorFactories.mockResolvedValueOnce([ { ...mockConnectorFactory, metadata: { ...mockConnectorFactory.metadata, id: 'id0' }, }, ]); - mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 1 }); + countConnectorByConnectorId.mockResolvedValueOnce({ count: 1 }); const response = await connectorRequest.post('/connectors').send({ connectorId: 'id0', config: { cliend_id: 'client_id', client_secret: 'client_secret' }, @@ -254,14 +251,14 @@ describe('connector route', () => { }); it('should add a new record and delete old records with same connector type when add passwordless connectors', async () => { - loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([ + loadConnectorFactories.mockResolvedValueOnce([ { ...mockConnectorFactory, type: ConnectorType.Sms, metadata: { ...mockConnectorFactory.metadata, id: 'id0', isStandard: true }, }, ]); - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([ + getLogtoConnectors.mockResolvedValueOnce([ { dbEntry: { ...mockConnector, connectorId: 'id0' }, metadata: { ...mockMetadata, id: 'id0' }, @@ -289,7 +286,7 @@ describe('connector route', () => { }); it('throws when add more than 1 social connector instance with same target and platform (add from standard connector)', async () => { - loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([ + loadConnectorFactories.mockResolvedValueOnce([ { ...mockConnectorFactory, metadata: { @@ -300,8 +297,8 @@ describe('connector route', () => { }, }, ]); - mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([ + countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); + getLogtoConnectors.mockResolvedValueOnce([ { dbEntry: { ...mockConnector, connectorId: 'id0', metadata: { target: 'target' } }, metadata: { @@ -322,7 +319,7 @@ describe('connector route', () => { }); it('throws when add more than 1 social connector instance with same target and platform (add social connector)', async () => { - loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([ + loadConnectorFactories.mockResolvedValueOnce([ { ...mockConnectorFactory, metadata: { @@ -334,8 +331,8 @@ describe('connector route', () => { }, }, ]); - mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([ + countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); + getLogtoConnectors.mockResolvedValueOnce([ { dbEntry: { ...mockConnector, connectorId: 'id0', metadata: { target: 'target' } }, metadata: { @@ -373,7 +370,7 @@ describe('connector route', () => { ...defaultConnectorMethods, sendMessage, }; - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([mockedSmsConnector]); + getLogtoConnectors.mockResolvedValueOnce([mockedSmsConnector]); const response = await connectorRequest .post('/connectors/id/test') .send({ phone: '12345678901', config: { test: 123 } }); @@ -401,7 +398,7 @@ describe('connector route', () => { ...defaultConnectorMethods, sendMessage, }; - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([mockedEmailConnector]); + getLogtoConnectors.mockResolvedValueOnce([mockedEmailConnector]); const response = await connectorRequest .post('/connectors/id/test') .send({ email: 'test@email.com', config: { test: 123 } }); @@ -425,7 +422,7 @@ describe('connector route', () => { }); it('should throw when sms connector is not found', async () => { - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([]); + getLogtoConnectors.mockResolvedValueOnce([]); const response = await connectorRequest .post('/connectors/id/test') .send({ phone: '12345678901' }); @@ -433,7 +430,7 @@ describe('connector route', () => { }); it('should throw when email connector is not found', async () => { - getLogtoConnectorsPlaceHolder.mockResolvedValueOnce([]); + getLogtoConnectors.mockResolvedValueOnce([]); const response = await connectorRequest .post('/connectors/id/test') .send({ email: 'test@email.com' }); @@ -445,21 +442,22 @@ describe('connector route', () => { beforeEach(() => { jest.resetAllMocks(); }); + afterEach(() => { jest.clearAllMocks(); }); it('delete connector instance and remove unavailable social connector targets', async () => { - (findConnectorById as jest.Mock).mockResolvedValueOnce(mockConnector); - loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([mockConnectorFactory]); + findConnectorById.mockResolvedValueOnce(mockConnector); + loadConnectorFactories.mockResolvedValueOnce([mockConnectorFactory]); await connectorRequest.delete('/connectors/id').send({}); expect(deleteConnectorById).toHaveBeenCalledTimes(1); expect(removeUnavailableSocialConnectorTargets).toHaveBeenCalledTimes(1); }); it('delete connector instance (connector factory is not social type)', async () => { - (findConnectorById as jest.Mock).mockResolvedValueOnce(mockConnector); - loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([ + findConnectorById.mockResolvedValueOnce(mockConnector); + loadConnectorFactories.mockResolvedValueOnce([ { ...mockConnectorFactory, type: ConnectorType.Sms }, ]); await connectorRequest.delete('/connectors/id').send({}); @@ -468,8 +466,8 @@ describe('connector route', () => { }); it('delete connector instance (connector factory is not found)', async () => { - (findConnectorById as jest.Mock).mockResolvedValueOnce(mockConnector); - loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([]); + findConnectorById.mockResolvedValueOnce(mockConnector); + loadConnectorFactories.mockResolvedValueOnce([]); await connectorRequest.delete('/connectors/id').send({}); expect(deleteConnectorById).toHaveBeenCalledTimes(1); expect(removeUnavailableSocialConnectorTargets).toHaveBeenCalledTimes(0); @@ -477,7 +475,7 @@ describe('connector route', () => { it('throws when connector not exists with `id`', async () => { // eslint-disable-next-line unicorn/no-useless-undefined - (findConnectorById as jest.Mock).mockResolvedValueOnce(undefined); + findConnectorById.mockResolvedValueOnce(undefined); const response = await connectorRequest.delete('/connectors/id').send({}); expect(response).toHaveProperty('statusCode', 500); }); diff --git a/packages/core/src/routes/connector.update.test.ts b/packages/core/src/routes/connector.update.test.ts index 06eb2599e..b295476e5 100644 --- a/packages/core/src/routes/connector.update.test.ts +++ b/packages/core/src/routes/connector.update.test.ts @@ -1,5 +1,6 @@ import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit'; import { ConnectorType } from '@logto/schemas'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { mockMetadata, @@ -9,17 +10,14 @@ import { } from '#src/__mocks__/index.js'; import type { LogtoConnector } from '#src/connectors/types.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { updateConnector } from '#src/queries/connector.js'; import assertThat from '#src/utils/assert-that.js'; import { createRequester } from '#src/utils/test-utils.js'; -import connectorRoutes from './connector.js'; +const { jest } = import.meta; -const getLogtoConnectorsPlaceholder = jest.fn() as jest.MockedFunction< - () => Promise ->; -const getLogtoConnectorByIdPlaceholder = jest.fn(async (connectorId: string) => { - const connectors = await getLogtoConnectorsPlaceholder(); +const getLogtoConnectors = jest.fn() as jest.MockedFunction<() => Promise>; +const getLogtoConnectorById = jest.fn(async (connectorId: string) => { + const connectors = await getLogtoConnectors(); const connector = connectors.find(({ dbEntry }) => dbEntry.id === connectorId); assertThat( @@ -36,22 +34,25 @@ const getLogtoConnectorByIdPlaceholder = jest.fn(async (connectorId: string) => sendMessage: sendMessagePlaceHolder, }; }) as jest.MockedFunction<(connectorId: string) => Promise>; -const mockedUpdateConnector = updateConnector as jest.Mock; + const sendMessagePlaceHolder = jest.fn(); -jest.mock('#src/queries/connector.js', () => ({ +const { updateConnector } = await mockEsmWithActual('#src/queries/connector.js', () => ({ updateConnector: jest.fn(), })); -jest.mock('#src/connectors.js', () => ({ - getLogtoConnectors: async () => getLogtoConnectorsPlaceholder(), - getLogtoConnectorById: async (connectorId: string) => - getLogtoConnectorByIdPlaceholder(connectorId), + +await mockEsmWithActual('#src/connectors.js', () => ({ + getLogtoConnectors, + getLogtoConnectorById, })); -jest.mock('#src/lib/sign-in-experience.js', () => ({ + +mockEsm('#src/lib/sign-in-experience.js', () => ({ // eslint-disable-next-line @typescript-eslint/no-empty-function removeUnavailableSocialConnectorTargets: async () => {}, })); +const connectorRoutes = await pickDefault(import('./connector.js')); + describe('connector PATCH routes', () => { const connectorRequest = createRequester({ authedRoutes: connectorRoutes }); @@ -61,19 +62,19 @@ describe('connector PATCH routes', () => { }); it('throws when connector can not be found by given connectorId (locally)', async () => { - getLogtoConnectorsPlaceholder.mockResolvedValueOnce(mockLogtoConnectorList.slice(0, 1)); + getLogtoConnectors.mockResolvedValueOnce(mockLogtoConnectorList.slice(0, 1)); const response = await connectorRequest.patch('/connectors/findConnector').send({}); expect(response).toHaveProperty('statusCode', 404); }); it('throws when connector can not be found by given connectorId (remotely)', async () => { - getLogtoConnectorsPlaceholder.mockResolvedValueOnce([]); + getLogtoConnectors.mockResolvedValueOnce([]); const response = await connectorRequest.patch('/connectors/id0').send({}); expect(response).toHaveProperty('statusCode', 404); }); it('config validation fails', async () => { - getLogtoConnectorsPlaceholder.mockResolvedValueOnce([ + getLogtoConnectors.mockResolvedValueOnce([ { dbEntry: mockConnector, metadata: mockMetadata, @@ -91,7 +92,7 @@ describe('connector PATCH routes', () => { }); it('throws when trying to update target', async () => { - getLogtoConnectorsPlaceholder.mockResolvedValue([ + getLogtoConnectors.mockResolvedValue([ { dbEntry: mockConnector, metadata: { ...mockMetadata, isStandard: true }, @@ -108,7 +109,7 @@ describe('connector PATCH routes', () => { }); it('successfully updates connector configs', async () => { - getLogtoConnectorsPlaceholder.mockResolvedValue([ + getLogtoConnectors.mockResolvedValue([ { dbEntry: mockConnector, metadata: { ...mockMetadata, isStandard: true }, @@ -116,7 +117,7 @@ describe('connector PATCH routes', () => { ...mockLogtoConnector, }, ]); - mockedUpdateConnector.mockResolvedValueOnce({ + updateConnector.mockResolvedValueOnce({ ...mockConnector, metadata: { target: 'target', @@ -149,7 +150,7 @@ describe('connector PATCH routes', () => { }); it('successfully clear connector config metadata', async () => { - getLogtoConnectorsPlaceholder.mockResolvedValueOnce([ + getLogtoConnectors.mockResolvedValueOnce([ { dbEntry: mockConnector, metadata: { ...mockMetadata, isStandard: true }, @@ -157,7 +158,7 @@ describe('connector PATCH routes', () => { ...mockLogtoConnector, }, ]); - mockedUpdateConnector.mockResolvedValueOnce({ + updateConnector.mockResolvedValueOnce({ ...mockConnector, metadata: { target: '', @@ -186,7 +187,7 @@ describe('connector PATCH routes', () => { }); it('throws when set syncProfile to `true` and with non-social connector', async () => { - getLogtoConnectorsPlaceholder.mockResolvedValueOnce([ + getLogtoConnectors.mockResolvedValueOnce([ { dbEntry: mockConnector, metadata: mockMetadata, @@ -200,7 +201,7 @@ describe('connector PATCH routes', () => { }); it('successfully set syncProfile to `true` and with social connector', async () => { - getLogtoConnectorsPlaceholder.mockResolvedValue([ + getLogtoConnectors.mockResolvedValue([ { dbEntry: { ...mockConnector, syncProfile: false }, metadata: mockMetadata, @@ -220,7 +221,7 @@ describe('connector PATCH routes', () => { }); it('successfully set syncProfile to `false`', async () => { - getLogtoConnectorsPlaceholder.mockResolvedValue([ + getLogtoConnectors.mockResolvedValue([ { dbEntry: { ...mockConnector, syncProfile: false }, metadata: mockMetadata, diff --git a/packages/core/src/routes/custom-phrase.test.ts b/packages/core/src/routes/custom-phrase.test.ts index 72da14e2f..be0c35550 100644 --- a/packages/core/src/routes/custom-phrase.test.ts +++ b/packages/core/src/routes/custom-phrase.test.ts @@ -1,73 +1,66 @@ import en from '@logto/phrases-ui/lib/locales/en.js'; -import type { CustomPhrase, SignInExperience, Translation } from '@logto/schemas'; +import type { CustomPhrase, SignInExperience } from '@logto/schemas'; +import { mockEsm, pickDefault } from '@logto/shared/esm'; import { mockZhCnCustomPhrase, trTrTag, zhCnTag } from '#src/__mocks__/custom-phrase.js'; import { mockSignInExperience } from '#src/__mocks__/index.js'; import RequestError from '#src/errors/RequestError/index.js'; -import customPhraseRoutes from '#src/routes/custom-phrase.js'; import { createRequester } from '#src/utils/test-utils.js'; +const { jest } = import.meta; + const mockLanguageTag = zhCnTag; const mockPhrase = mockZhCnCustomPhrase; const mockCustomPhrases: Record = { [mockLanguageTag]: mockPhrase, }; -const deleteCustomPhraseByLanguageTag = jest.fn(async (languageTag: string) => { - if (!mockCustomPhrases[languageTag]) { - throw new RequestError({ code: 'entity.not_found', status: 404 }); - } -}); +const { + deleteCustomPhraseByLanguageTag, + findAllCustomPhrases, + findCustomPhraseByLanguageTag, + upsertCustomPhrase, +} = mockEsm('#src/queries/custom-phrase.js', () => ({ + deleteCustomPhraseByLanguageTag: jest.fn(async (languageTag: string) => { + if (!mockCustomPhrases[languageTag]) { + throw new RequestError({ code: 'entity.not_found', status: 404 }); + } + }), + findAllCustomPhrases: jest.fn(async (): Promise => []), + findCustomPhraseByLanguageTag: jest.fn(async (languageTag: string) => { + const mockCustomPhrase = mockCustomPhrases[languageTag]; -const findCustomPhraseByLanguageTag = jest.fn(async (languageTag: string) => { - const mockCustomPhrase = mockCustomPhrases[languageTag]; + if (!mockCustomPhrase) { + throw new RequestError({ code: 'entity.not_found', status: 404 }); + } - if (!mockCustomPhrase) { - throw new RequestError({ code: 'entity.not_found', status: 404 }); - } - - return mockCustomPhrase; -}); - -const findAllCustomPhrases = jest.fn(async (): Promise => []); - -const upsertCustomPhrase = jest.fn(async (customPhrase: CustomPhrase) => mockPhrase); - -jest.mock('#src/queries/custom-phrase.js', () => ({ - deleteCustomPhraseByLanguageTag: async (tag: string) => deleteCustomPhraseByLanguageTag(tag), - findAllCustomPhrases: async () => findAllCustomPhrases(), - findCustomPhraseByLanguageTag: async (tag: string) => findCustomPhraseByLanguageTag(tag), - upsertCustomPhrase: async (customPhrase: CustomPhrase) => upsertCustomPhrase(customPhrase), + return mockCustomPhrase; + }), + upsertCustomPhrase: jest.fn(async () => mockPhrase), })); -const isStrictlyPartial = jest.fn( - (fullTranslation: Translation, partialTranslation: Partial) => true -); - -jest.mock('#src/utils/translation.js', () => ({ - isStrictlyPartial: (fullTranslation: Translation, partialTranslation: Translation) => - isStrictlyPartial(fullTranslation, partialTranslation), +const { isStrictlyPartial } = mockEsm('#src/utils/translation.js', () => ({ + isStrictlyPartial: jest.fn(() => true), })); const mockFallbackLanguage = trTrTag; -const findDefaultSignInExperience = jest.fn( - async (): Promise => ({ - ...mockSignInExperience, - languageInfo: { - autoDetect: true, - fallbackLanguage: mockFallbackLanguage, - }, - }) -); - -jest.mock('#src/queries/sign-in-experience.js', () => ({ - findDefaultSignInExperience: async () => findDefaultSignInExperience(), +mockEsm('#src/queries/sign-in-experience.js', () => ({ + findDefaultSignInExperience: jest.fn( + async (): Promise => ({ + ...mockSignInExperience, + languageInfo: { + autoDetect: true, + fallbackLanguage: mockFallbackLanguage, + }, + }) + ), })); -describe('customPhraseRoutes', () => { - const customPhraseRequest = createRequester({ authedRoutes: customPhraseRoutes }); +const customPhraseRoutes = await pickDefault(import('./custom-phrase.js')); +const customPhraseRequest = createRequester({ authedRoutes: customPhraseRoutes }); +describe('customPhraseRoutes', () => { afterEach(() => { jest.clearAllMocks(); }); diff --git a/packages/core/src/routes/dashboard.test.ts b/packages/core/src/routes/dashboard.test.ts index 386847384..e8766c7de 100644 --- a/packages/core/src/routes/dashboard.test.ts +++ b/packages/core/src/routes/dashboard.test.ts @@ -1,25 +1,20 @@ // The FP version works better for `format()` /* eslint-disable import/no-duplicates */ +import { mockEsm, pickDefault } from '@logto/shared/esm'; import { endOfDay, subDays } from 'date-fns'; import { format } from 'date-fns/fp'; + +import { createRequester } from '#src/utils/test-utils.js'; /* eslint-enable import/no-duplicates */ -import dashboardRoutes from '#src/routes/dashboard.js'; -import { createRequester } from '#src/utils/test-utils.js'; +const { jest } = import.meta; const totalUserCount = 1000; -const countUsers = jest.fn(async () => ({ count: totalUserCount })); -const getDailyNewUserCountsByTimeInterval = jest.fn( - async (startTimeExclusive: number, endTimeInclusive: number) => mockDailyNewUserCounts -); const formatToQueryDate = format('yyyy-MM-dd'); -jest.mock('#src/queries/user.js', () => ({ - countUsers: async () => countUsers(), - getDailyNewUserCountsByTimeInterval: async ( - startTimeExclusive: number, - endTimeInclusive: number - ) => getDailyNewUserCountsByTimeInterval(startTimeExclusive, endTimeInclusive), +const { countUsers, getDailyNewUserCountsByTimeInterval } = mockEsm('#src/queries/user.js', () => ({ + countUsers: jest.fn(async () => ({ count: totalUserCount })), + getDailyNewUserCountsByTimeInterval: jest.fn(async () => mockDailyNewUserCounts), })); const mockDailyNewUserCounts = [ @@ -44,21 +39,14 @@ const mockDailyActiveUserCounts = [ const mockActiveUserCount = 1000; -const getDailyActiveUserCountsByTimeInterval = jest.fn( - async (startTimeExclusive: number, endTimeInclusive: number) => mockDailyActiveUserCounts +const { getDailyActiveUserCountsByTimeInterval, countActiveUsersByTimeInterval } = mockEsm( + '#src/queries/log.js', + () => ({ + getDailyActiveUserCountsByTimeInterval: jest.fn().mockResolvedValue(mockDailyActiveUserCounts), + countActiveUsersByTimeInterval: jest.fn().mockResolvedValue({ count: mockActiveUserCount }), + }) ); -const countActiveUsersByTimeInterval = jest.fn( - async (startTimeExclusive: number, endTimeInclusive: number) => ({ count: mockActiveUserCount }) -); - -jest.mock('#src/queries/log.js', () => ({ - getDailyActiveUserCountsByTimeInterval: async ( - startTimeExclusive: number, - endTimeInclusive: number - ) => getDailyActiveUserCountsByTimeInterval(startTimeExclusive, endTimeInclusive), - countActiveUsersByTimeInterval: async (startTimeExclusive: number, endTimeInclusive: number) => - countActiveUsersByTimeInterval(startTimeExclusive, endTimeInclusive), -})); +const dashboardRoutes = await pickDefault(import('./dashboard.js')); describe('dashboardRoutes', () => { const logRequest = createRequester({ authedRoutes: dashboardRoutes }); 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 168e4196c..878e7c815 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts @@ -1,10 +1,7 @@ import { Event } from '@logto/schemas'; -import { Provider } from 'oidc-provider'; +import { mockEsm, pickDefault } from '@logto/shared/esm'; -import { getLogtoConnectorById } from '#src/connectors/index.js'; -import { assignInteractionResults } from '#src/lib/session.js'; -import { encryptUserPassword, generateUserId, insertUser } from '#src/lib/user.js'; -import { updateUserById } from '#src/queries/user.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import type { @@ -14,19 +11,20 @@ import type { VerifiedSignInInteractionResult, VerifiedForgotPasswordInteractionResult, } from '../types/index.js'; -import submitInteraction from './submit-interaction.js'; -jest.mock('#src/connectors/index.js', () => ({ +const { jest } = import.meta; + +const { getLogtoConnectorById } = mockEsm('#src/connectors/index.js', () => ({ getLogtoConnectorById: jest .fn() .mockResolvedValue({ metadata: { target: 'logto' }, dbEntry: { syncProfile: true } }), })); -jest.mock('#src/lib/session.js', () => ({ +const { assignInteractionResults } = mockEsm('#src/lib/session.js', () => ({ assignInteractionResults: jest.fn(), })); -jest.mock('#src/lib/user.js', () => ({ +const { encryptUserPassword, generateUserId, insertUser } = mockEsm('#src/lib/user.js', () => ({ encryptUserPassword: jest.fn().mockResolvedValue({ passwordEncrypted: 'passwordEncrypted', passwordEncryptionMethod: 'plain', @@ -35,25 +33,21 @@ jest.mock('#src/lib/user.js', () => ({ insertUser: jest.fn(), })); -jest.mock('#src/queries/user.js', () => ({ +mockEsm('#src/queries/user.js', () => ({ findUserById: jest .fn() .mockResolvedValue({ identities: { google: { userId: 'googleId', details: {} } } }), updateUserById: jest.fn(), })); -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })), - })), -})); - +const { updateUserById } = await import('#src/queries/user.js'); +const submitInteraction = await pickDefault(import('./submit-interaction.js')); const now = Date.now(); jest.useFakeTimers().setSystemTime(now); describe('submit action', () => { - const provider = new Provider(''); + const provider = createMockProvider(); const log = jest.fn(); const ctx: InteractionContext = { ...createContextWithRouteParameters(), @@ -116,7 +110,7 @@ describe('submit action', () => { }); it('sign-in', async () => { - (getLogtoConnectorById as jest.Mock).mockResolvedValueOnce({ + getLogtoConnectorById.mockResolvedValueOnce({ metadata: { target: 'logto' }, dbEntry: { syncProfile: false }, }); diff --git a/packages/core/src/routes/interaction/index.test.ts b/packages/core/src/routes/interaction/index.test.ts index af08a4710..7751beda7 100644 --- a/packages/core/src/routes/interaction/index.test.ts +++ b/packages/core/src/routes/interaction/index.test.ts @@ -1,21 +1,16 @@ import { ConnectorType } from '@logto/connector-kit'; import { Event } from '@logto/schemas'; -import { Provider } from 'oidc-provider'; +import { demoAppApplicationId } from '@logto/schemas/lib/seeds/application.js'; +import { mockEsm, mockEsmDefault, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; import RequestError from '#src/errors/RequestError/index.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createRequester } from '#src/utils/test-utils.js'; -import submitInteraction from './actions/submit-interaction.js'; -import interactionRoutes, { verificationPrefix, interactionPrefix } from './index.js'; import type { InteractionContext } from './types/index.js'; -import { getInteractionStorage } from './utils/interaction.js'; -import { sendPasscodeToIdentifier } from './utils/passcode-validation.js'; -import { - verifyIdentifier, - verifyProfile, - validateMandatoryUserProfile, -} from './verifications/index.js'; + +const { jest } = import.meta; // FIXME @Darcy: no more `enabled` for `connectors` table const getLogtoConnectorByIdHelper = jest.fn(async (connectorId: string) => { @@ -36,7 +31,7 @@ const getLogtoConnectorByIdHelper = jest.fn(async (connectorId: string) => { }; }); -jest.mock('#src/connectors.js', () => ({ +await mockEsmWithActual('#src/connectors/index.js', () => ({ getLogtoConnectorById: jest.fn(async (connectorId: string) => { const connector = await getLogtoConnectorByIdHelper(connectorId); @@ -51,57 +46,67 @@ jest.mock('#src/connectors.js', () => ({ }), })); -jest.mock('./utils/passcode-validation.js', () => ({ - sendPasscodeToIdentifier: jest.fn(), -})); - -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails: jest.fn().mockResolvedValue({ - jti: 'jti', - result: {}, - params: { - client_id: 'demo_app', - }, - }), - })), -})); - -jest.mock('#src/lib/sign-in-experience/index.js', () => ({ - getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience), -})); - -jest.mock('./verifications/index.js', () => ({ - verifyIdentifier: jest.fn(), - verifyProfile: jest.fn(), - validateMandatoryUserProfile: jest.fn(), -})); - -jest.mock('./actions/submit-interaction.js', () => - jest.fn((_interaction, ctx: InteractionContext) => { - ctx.body = { redirectUri: 'logto.io' }; +const { sendPasscodeToIdentifier } = await mockEsmWithActual( + './utils/passcode-validation.js', + () => ({ + sendPasscodeToIdentifier: jest.fn(), }) ); -jest.mock('./utils/interaction.js', () => ({ +mockEsm('#src/lib/sign-in-experience/index.js', () => ({ + getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience), +})); + +const { verifyIdentifier, verifyProfile, validateMandatoryUserProfile } = mockEsm( + './verifications/index.js', + () => ({ + verifyIdentifier: jest.fn(), + verifyProfile: jest.fn(), + validateMandatoryUserProfile: jest.fn(), + }) +); + +const { default: submitInteraction } = mockEsm('./actions/submit-interaction.js', () => ({ + default: jest.fn((_interaction, ctx: InteractionContext) => { + ctx.body = { redirectUri: 'logto.io' }; + }), +})); + +const { getInteractionStorage } = mockEsm('./utils/interaction.js', () => ({ getInteractionStorage: jest.fn(), })); const log = jest.fn(); -const koaInteractionBodyGuardSpy = jest.spyOn( - jest.requireActual('./middleware/koa-interaction-body-guard.js'), - 'default' +const koaInteractionBodyGuard = await pickDefault( + import('./middleware/koa-interaction-body-guard.js') ); -const koaSessionSignInExperienceGuardSpy = jest.spyOn( - jest.requireActual('./middleware/koa-session-sign-in-experience-guard.js'), - 'default' +const koaSessionSignInExperienceGuard = await pickDefault( + import('./middleware/koa-session-sign-in-experience-guard.js') ); +const koaInteractionBodyGuardSpy = mockEsmDefault( + './middleware/koa-interaction-body-guard.js', + () => jest.fn(koaInteractionBodyGuard) +); + +const koaSessionSignInExperienceGuardSpy = mockEsmDefault( + './middleware/koa-session-sign-in-experience-guard.js', + () => jest.fn(koaSessionSignInExperienceGuard) +); + +const { + default: interactionRoutes, + verificationPrefix, + interactionPrefix, +} = await import('./index.js'); + describe('session -> interactionRoutes', () => { const sessionRequest = createRequester({ anonymousRoutes: interactionRoutes, - provider: new Provider(''), + provider: createMockProvider( + jest.fn().mockResolvedValue({ params: {}, jti: 'jti', client_id: demoAppApplicationId }) + ), middlewares: [ async (ctx, next) => { ctx.addLogContext = jest.fn(); @@ -162,14 +167,13 @@ describe('session -> interactionRoutes', () => { describe('PATCH /interaction', () => { const path = interactionPrefix; - const getInteractionStorageMock = getInteractionStorage as jest.Mock; afterEach(() => { jest.clearAllMocks(); }); it('sign-in event with register event interaction session in record should call methods properly', async () => { - getInteractionStorageMock.mockResolvedValueOnce({ event: Event.Register }); + getInteractionStorage.mockResolvedValueOnce({ event: Event.Register }); const body = { event: Event.SignIn, @@ -185,7 +189,7 @@ describe('session -> interactionRoutes', () => { }); it('sign-in event with forgot password event interaction session in record should reject', async () => { - getInteractionStorageMock.mockResolvedValueOnce({ event: Event.ForgotPassword }); + getInteractionStorage.mockResolvedValueOnce({ event: Event.ForgotPassword }); const body = { event: Event.SignIn, @@ -200,7 +204,7 @@ describe('session -> interactionRoutes', () => { }); it('Forgot event with forgot password event interaction session in record should call methods properly', async () => { - getInteractionStorageMock.mockResolvedValueOnce({ event: Event.ForgotPassword }); + getInteractionStorage.mockResolvedValueOnce({ event: Event.ForgotPassword }); const body = { event: Event.ForgotPassword, @@ -216,7 +220,7 @@ describe('session -> interactionRoutes', () => { }); it('Forgot event with sign-in event interaction session in record should call methods properly', async () => { - getInteractionStorageMock.mockResolvedValueOnce({ event: Event.SignIn }); + getInteractionStorage.mockResolvedValueOnce({ event: Event.SignIn }); const body = { event: Event.ForgotPassword, diff --git a/packages/core/src/routes/interaction/middleware/koa-interaction-body-guard.test.ts b/packages/core/src/routes/interaction/middleware/koa-interaction-body-guard.test.ts index 2fe0bc0d9..1be87b4cc 100644 --- a/packages/core/src/routes/interaction/middleware/koa-interaction-body-guard.test.ts +++ b/packages/core/src/routes/interaction/middleware/koa-interaction-body-guard.test.ts @@ -1,20 +1,17 @@ import { Event } from '@logto/schemas'; +import { mockEsmDefault, pickDefault } from '@logto/shared/esm'; import type { Context } from 'koa'; import { interactionMocks } from '#src/__mocks__/interactions.js'; import { emptyMiddleware, createContextWithRouteParameters } from '#src/utils/test-utils.js'; import type { WithGuardedIdentifierPayloadContext } from './koa-interaction-body-guard.js'; -import koaInteractionBodyGuard from './koa-interaction-body-guard.js'; -jest.mock('koa-body', () => emptyMiddleware); +const { jest } = import.meta; -// User this to bypass the context type assertion -const mockIdentifierPayload = Object.freeze({ - type: 'username_password', - username: 'username', - password: 'password', -}); +mockEsmDefault('koa-body', () => emptyMiddleware); + +const koaInteractionBodyGuard = await pickDefault(import('./koa-interaction-body-guard.js')); describe('koaInteractionBodyGuard', () => { const baseCtx = createContextWithRouteParameters(); diff --git a/packages/core/src/routes/interaction/middleware/koa-session-sign-inexperience-guard.test.ts b/packages/core/src/routes/interaction/middleware/koa-session-sign-inexperience-guard.test.ts index 119a163a1..3633e81a8 100644 --- a/packages/core/src/routes/interaction/middleware/koa-session-sign-inexperience-guard.test.ts +++ b/packages/core/src/routes/interaction/middleware/koa-session-sign-inexperience-guard.test.ts @@ -1,31 +1,27 @@ import { Event } from '@logto/schemas'; -import { Provider } from 'oidc-provider'; +import { mockEsm, pickDefault } from '@logto/shared/esm'; import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; -import { - signInModeValidation, - identifierValidation, - profileValidation, -} from '../utils/sign-in-experience-validation.js'; -import koaSessionSignInExperienceGuard from './koa-session-sign-in-experience-guard.js'; +const { jest } = import.meta; -jest.mock('#src/lib/sign-in-experience/index.js', () => ({ +mockEsm('#src/lib/sign-in-experience/index.js', () => ({ getSignInExperienceForApplication: jest.fn().mockResolvedValue(mockSignInExperience), })); -jest.mock('../utils/sign-in-experience-validation.js', () => ({ +const mockUtils = { signInModeValidation: jest.fn(), identifierValidation: jest.fn(), profileValidation: jest.fn(), -})); +}; -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails: jest.fn(async () => ({ params: {} })), - })), -})); +mockEsm('../utils/sign-in-experience-validation.js', () => mockUtils); + +const koaSessionSignInExperienceGuard = await pickDefault( + import('./koa-session-sign-in-experience-guard.js') +); describe('koaSessionSignInExperienceGuard', () => { const baseCtx = createContextWithRouteParameters(); @@ -41,14 +37,15 @@ describe('koaSessionSignInExperienceGuard', () => { }), signInExperience: mockSignInExperience, }; + const provider = createMockProvider(); - await koaSessionSignInExperienceGuard(new Provider(''))(ctx, next); + await koaSessionSignInExperienceGuard(provider)(ctx, next); - expect(signInModeValidation).toBeCalledWith(Event.SignIn, mockSignInExperience); - expect(identifierValidation).toBeCalledWith( + expect(mockUtils.signInModeValidation).toBeCalledWith(Event.SignIn, mockSignInExperience); + expect(mockUtils.identifierValidation).toBeCalledWith( { username: 'username', password: 'password' }, mockSignInExperience ); - expect(profileValidation).toBeCalledWith({ email: 'email' }, mockSignInExperience); + expect(mockUtils.profileValidation).toBeCalledWith({ email: 'email' }, mockSignInExperience); }); }); 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 f24f3f837..3ac8af3a6 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,43 +1,41 @@ -import { getLogtoConnectorById } from '#src/connectors/index.js'; -import { - findUserByEmail, - findUserByUsername, - findUserByPhone, - findUserByIdentity, -} from '#src/queries/user.js'; +import { mockEsm, pickDefault } from '@logto/shared/esm'; -import findUserByIdentifier from './find-user-by-identifier.js'; +const { jest } = import.meta; -jest.mock('#src/queries/user.js', () => ({ +const queries = { findUserByEmail: jest.fn(), findUserByUsername: jest.fn(), findUserByPhone: jest.fn(), findUserByIdentity: jest.fn(), -})); +}; -jest.mock('#src/connectors/index.js', () => ({ +mockEsm('#src/queries/user.js', () => queries); + +const { getLogtoConnectorById } = mockEsm('#src/connectors/index.js', () => ({ getLogtoConnectorById: jest.fn().mockResolvedValue({ metadata: { target: 'logto' } }), })); +const findUserByIdentifier = await pickDefault(import('./find-user-by-identifier.js')); + describe('findUserByIdentifier', () => { it('username', async () => { await findUserByIdentifier({ username: 'foo' }); - expect(findUserByUsername).toBeCalledWith('foo'); + expect(queries.findUserByUsername).toBeCalledWith('foo'); }); it('email', async () => { await findUserByIdentifier({ email: 'foo@logto.io' }); - expect(findUserByEmail).toBeCalledWith('foo@logto.io'); + expect(queries.findUserByEmail).toBeCalledWith('foo@logto.io'); }); it('phone', async () => { await findUserByIdentifier({ phone: '123456' }); - expect(findUserByPhone).toBeCalledWith('123456'); + expect(queries.findUserByPhone).toBeCalledWith('123456'); }); it('social', async () => { await findUserByIdentifier({ connectorId: 'connector', userInfo: { id: 'foo' } }); expect(getLogtoConnectorById).toBeCalledWith('connector'); - expect(findUserByIdentity).toBeCalledWith('logto', 'foo'); + expect(queries.findUserByIdentity).toBeCalledWith('logto', 'foo'); }); }); diff --git a/packages/core/src/routes/interaction/utils/passcode-validation.test.ts b/packages/core/src/routes/interaction/utils/passcode-validation.test.ts index e8c5d27f2..3c2f9c51d 100644 --- a/packages/core/src/routes/interaction/utils/passcode-validation.test.ts +++ b/packages/core/src/routes/interaction/utils/passcode-validation.test.ts @@ -1,14 +1,17 @@ import { PasscodeType, Event } from '@logto/schemas'; - -import { createPasscode, sendPasscode } from '#src/lib/passcode.js'; +import { mockEsmWithActual } from '@logto/shared/esm'; import type { SendPasscodePayload } from '../types/index.js'; -import { sendPasscodeToIdentifier } from './passcode-validation.js'; -jest.mock('#src/lib/passcode.js', () => ({ +const { jest } = import.meta; +const passcode = { createPasscode: jest.fn(() => ({})), sendPasscode: jest.fn().mockResolvedValue({ dbEntry: { id: 'foo' } }), -})); +}; + +await mockEsmWithActual('#src/lib/passcode.js', () => passcode); + +const { sendPasscodeToIdentifier } = await import('./passcode-validation.js'); const sendPasscodeTestCase = [ { @@ -38,8 +41,6 @@ const sendPasscodeTestCase = [ ]; describe('passcode-validation utils', () => { - const createPasscodeMock = createPasscode as jest.Mock; - const sendPasscodeMock = sendPasscode as jest.Mock; const log = jest.fn(); afterEach(() => { @@ -50,8 +51,8 @@ describe('passcode-validation utils', () => { 'send passcode successfully', async ({ payload, createPasscodeParams }) => { await sendPasscodeToIdentifier(payload as SendPasscodePayload, 'jti', log); - expect(createPasscodeMock).toBeCalledWith('jti', ...createPasscodeParams); - expect(sendPasscodeMock).toBeCalled(); + expect(passcode.createPasscode).toBeCalledWith('jti', ...createPasscodeParams); + expect(passcode.sendPasscode).toBeCalled(); } ); }); diff --git a/packages/core/src/routes/interaction/utils/social-verification.test.ts b/packages/core/src/routes/interaction/utils/social-verification.test.ts index 58181c06b..42a2ce18d 100644 --- a/packages/core/src/routes/interaction/utils/social-verification.test.ts +++ b/packages/core/src/routes/interaction/utils/social-verification.test.ts @@ -1,14 +1,13 @@ import { ConnectorType } from '@logto/connector-kit'; +import { mockEsm } from '@logto/shared/esm'; -import { getUserInfoByAuthCode } from '#src/lib/social.js'; +const { jest } = import.meta; -import { verifySocialIdentity } from './social-verification.js'; - -jest.mock('#src/lib/social.js', () => ({ +const { getUserInfoByAuthCode } = mockEsm('#src/lib/social.js', () => ({ getUserInfoByAuthCode: jest.fn().mockResolvedValue({ id: 'foo' }), })); -jest.mock('#src/connectors.js', () => ({ +mockEsm('#src/connectors.js', () => ({ getLogtoConnectorById: jest.fn().mockResolvedValue({ metadata: { id: 'social', @@ -18,6 +17,7 @@ jest.mock('#src/connectors.js', () => ({ }), })); +const { verifySocialIdentity } = await import('./social-verification.js'); const log = jest.fn(); describe('social-verification', () => { 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 c515f4f4e..5c25c59f3 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 @@ -1,55 +1,47 @@ import { Event } from '@logto/schemas'; -import { Provider } from 'oidc-provider'; +import { mockEsm, mockEsmDefault, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; -import { verifyUserPassword } from '#src/lib/user.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import type { AnonymousInteractionResult, VerifiedPhoneIdentifier } from '../types/index.js'; -import findUserByIdentifier from '../utils/find-user-by-identifier.js'; -import { verifyIdentifierByPasscode } from '../utils/passcode-validation.js'; -import { verifySocialIdentity } from '../utils/social-verification.js'; -import identifierPayloadVerification from './identifier-payload-verification.js'; -jest.mock('#src/lib/user.js', () => ({ +const { jest } = import.meta; + +const { verifyUserPassword } = mockEsm('#src/lib/user.js', () => ({ verifyUserPassword: jest.fn(), })); -jest.mock('../utils/find-user-by-identifier.js', () => jest.fn()); +const findUserByIdentifier = mockEsmDefault('../utils/find-user-by-identifier.js', () => jest.fn()); -jest.mock('../utils/interaction.js', () => ({ - ...jest.requireActual('../utils/interaction.js'), +await mockEsmWithActual('../utils/interaction.js', () => ({ storeInteractionResult: jest.fn(), })); -jest.mock('../utils/passcode-validation.js', () => ({ +const { verifyIdentifierByPasscode } = mockEsm('../utils/passcode-validation.js', () => ({ verifyIdentifierByPasscode: jest.fn(), })); -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })), - })), -})); - -jest.mock('../utils/social-verification.js', () => ({ +const { verifySocialIdentity } = mockEsm('../utils/social-verification.js', () => ({ verifySocialIdentity: jest.fn().mockResolvedValue({ id: 'foo' }), })); +const identifierPayloadVerification = await pickDefault( + import('./identifier-payload-verification.js') +); + const log = jest.fn(); describe('identifier verification', () => { const baseCtx = { ...createContextWithRouteParameters(), log }; - const verifyUserPasswordMock = verifyUserPassword as jest.Mock; - const findUserByIdentifierMock = findUserByIdentifier as jest.Mock; - const verifyIdentifierByPasscodeMock = verifyIdentifierByPasscode as jest.Mock; afterEach(() => { jest.clearAllMocks(); }); it('username password user not found', async () => { - findUserByIdentifierMock.mockResolvedValueOnce(null); + findUserByIdentifier.mockResolvedValueOnce(null); const identifier = { username: 'username', @@ -64,14 +56,14 @@ describe('identifier verification', () => { }), }; - await expect(identifierPayloadVerification(ctx, new Provider(''))).rejects.toThrow(); + await expect(identifierPayloadVerification(ctx, createMockProvider())).rejects.toThrow(); expect(findUserByIdentifier).toBeCalledWith({ username: 'username' }); expect(verifyUserPassword).toBeCalledWith(null, 'password'); }); it('username password user is suspended', async () => { - findUserByIdentifierMock.mockResolvedValueOnce({ id: 'foo' }); - verifyUserPasswordMock.mockResolvedValueOnce({ id: 'foo', isSuspended: true }); + findUserByIdentifier.mockResolvedValueOnce({ id: 'foo' }); + verifyUserPassword.mockResolvedValueOnce({ id: 'foo', isSuspended: true }); const identifier = { username: 'username', password: 'password', @@ -85,7 +77,7 @@ describe('identifier verification', () => { }), }; - await expect(identifierPayloadVerification(ctx, new Provider(''))).rejects.toMatchError( + await expect(identifierPayloadVerification(ctx, createMockProvider())).rejects.toMatchError( new RequestError({ code: 'user.suspended', status: 401 }) ); @@ -94,8 +86,8 @@ describe('identifier verification', () => { }); it('email password', async () => { - findUserByIdentifierMock.mockResolvedValueOnce({ id: 'foo' }); - verifyUserPasswordMock.mockResolvedValueOnce({ id: 'foo', isSuspended: false }); + findUserByIdentifier.mockResolvedValueOnce({ id: 'foo' }); + verifyUserPassword.mockResolvedValueOnce({ id: 'foo', isSuspended: false }); const identifier = { email: 'email', @@ -110,7 +102,7 @@ describe('identifier verification', () => { }), }; - const result = await identifierPayloadVerification(ctx, new Provider('')); + const result = await identifierPayloadVerification(ctx, createMockProvider()); expect(findUserByIdentifier).toBeCalledWith({ email: 'email' }); expect(verifyUserPassword).toBeCalledWith({ id: 'foo' }, 'password'); expect(result).toEqual({ @@ -120,8 +112,8 @@ describe('identifier verification', () => { }); it('phone password', async () => { - findUserByIdentifierMock.mockResolvedValueOnce({ id: 'foo' }); - verifyUserPasswordMock.mockResolvedValueOnce({ id: 'foo', isSuspended: false }); + findUserByIdentifier.mockResolvedValueOnce({ id: 'foo' }); + verifyUserPassword.mockResolvedValueOnce({ id: 'foo', isSuspended: false }); const identifier = { phone: 'phone', @@ -136,7 +128,7 @@ describe('identifier verification', () => { }), }; - const result = await identifierPayloadVerification(ctx, new Provider('')); + const result = await identifierPayloadVerification(ctx, createMockProvider()); expect(findUserByIdentifier).toBeCalledWith({ phone: 'phone' }); expect(verifyUserPassword).toBeCalledWith({ id: 'foo' }, 'password'); expect(result).toEqual({ @@ -156,8 +148,8 @@ describe('identifier verification', () => { }), }; - const result = await identifierPayloadVerification(ctx, new Provider('')); - expect(verifyIdentifierByPasscodeMock).toBeCalledWith( + const result = await identifierPayloadVerification(ctx, createMockProvider()); + expect(verifyIdentifierByPasscode).toBeCalledWith( { ...identifier, event: Event.SignIn }, 'jti', log @@ -180,8 +172,8 @@ describe('identifier verification', () => { }), }; - const result = await identifierPayloadVerification(ctx, new Provider('')); - expect(verifyIdentifierByPasscodeMock).toBeCalledWith( + const result = await identifierPayloadVerification(ctx, createMockProvider()); + expect(verifyIdentifierByPasscode).toBeCalledWith( { ...identifier, event: Event.SignIn }, 'jti', log @@ -204,10 +196,10 @@ describe('identifier verification', () => { }), }; - const result = await identifierPayloadVerification(ctx, new Provider('')); + const result = await identifierPayloadVerification(ctx, createMockProvider()); expect(verifySocialIdentity).toBeCalledWith(identifier, log); - expect(findUserByIdentifierMock).not.toBeCalled(); + expect(findUserByIdentifier).not.toBeCalled(); expect(result).toEqual({ event: Event.SignIn, @@ -242,7 +234,11 @@ describe('identifier verification', () => { }), }; - const result = await identifierPayloadVerification(ctx, new Provider(''), interactionRecord); + const result = await identifierPayloadVerification( + ctx, + createMockProvider(), + interactionRecord + ); expect(result).toEqual({ event: Event.SignIn, identifiers: [ @@ -273,7 +269,7 @@ describe('identifier verification', () => { }), }; - await expect(identifierPayloadVerification(ctx, new Provider(''))).rejects.toMatchError( + await expect(identifierPayloadVerification(ctx, createMockProvider())).rejects.toMatchError( new RequestError('session.connector_session_not_found') ); }); @@ -303,7 +299,7 @@ describe('identifier verification', () => { }; await expect( - identifierPayloadVerification(ctx, new Provider(''), interactionRecord) + identifierPayloadVerification(ctx, createMockProvider(), interactionRecord) ).rejects.toMatchError(new RequestError('session.connector_session_not_found')); }); @@ -319,12 +315,12 @@ describe('identifier verification', () => { }), }; - const result = await identifierPayloadVerification(ctx, new Provider(''), { + const result = await identifierPayloadVerification(ctx, createMockProvider(), { event: Event.Register, identifiers: [oldIdentifier], }); - expect(verifyIdentifierByPasscodeMock).toBeCalledWith( + expect(verifyIdentifierByPasscode).toBeCalledWith( { ...identifier, event: Event.SignIn }, 'jti', log diff --git a/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts b/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts index 9e0eca6f0..4318fb820 100644 --- a/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts +++ b/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts @@ -1,28 +1,26 @@ import { Event, MissingProfile, SignInIdentifier } from '@logto/schemas'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { findUserById } from '#src/queries/user.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import type { IdentifierVerifiedInteractionResult } from '../types/index.js'; -import { isUserPasswordSet } from '../utils/index.js'; -import validateMandatoryUserProfile from './mandatory-user-profile-validation.js'; -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })), - })), -})); +const { jest } = import.meta; -jest.mock('#src/queries/user.js', () => ({ +const { findUserById } = await mockEsmWithActual('#src/queries/user.js', () => ({ findUserById: jest.fn(), })); -jest.mock('../utils/index.js', () => ({ +const { isUserPasswordSet } = mockEsm('../utils/index.js', () => ({ isUserPasswordSet: jest.fn(), })); +const validateMandatoryUserProfile = await pickDefault( + import('./mandatory-user-profile-validation.js') +); + describe('validateMandatoryUserProfile', () => { const baseCtx = createContextWithRouteParameters(); const interaction: IdentifierVerifiedInteractionResult = { @@ -55,10 +53,10 @@ describe('validateMandatoryUserProfile', () => { }); it('user account has username and password', async () => { - (findUserById as jest.Mock).mockResolvedValueOnce({ + findUserById.mockResolvedValueOnce({ username: 'foo', }); - (isUserPasswordSet as jest.Mock).mockResolvedValueOnce(true); + isUserPasswordSet.mockResolvedValueOnce(true); const ctx = { ...baseCtx, @@ -86,7 +84,7 @@ describe('validateMandatoryUserProfile', () => { }); it('user account has email', async () => { - (findUserById as jest.Mock).mockResolvedValueOnce({ + findUserById.mockResolvedValueOnce({ primaryEmail: 'email', }); @@ -119,7 +117,7 @@ describe('validateMandatoryUserProfile', () => { }); it('user account has phone', async () => { - (findUserById as jest.Mock).mockResolvedValueOnce({ + findUserById.mockResolvedValueOnce({ primaryPhone: 'phone', }); 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 index 0e1e2a1c5..8647d8aff 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 @@ -1,35 +1,30 @@ import { Event } from '@logto/schemas'; -import { argon2Verify } from 'hash-wasm'; -import { Provider } from 'oidc-provider'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; -import { findUserById } from '#src/queries/user.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import type { InteractionContext } from '../types/index.js'; -import { storeInteractionResult } from '../utils/interaction.js'; -import verifyProfile from './profile-verification.js'; -jest.mock('../utils/interaction.js', () => ({ +const { jest } = import.meta; + +const { storeInteractionResult } = mockEsm('../utils/interaction.js', () => ({ storeInteractionResult: jest.fn(), })); -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })), - })), -})); - -jest.mock('#src/queries/user.js', () => ({ +const { findUserById } = await mockEsmWithActual('#src/queries/user.js', () => ({ findUserById: jest.fn().mockResolvedValue({ id: 'foo', passwordEncrypted: 'passwordHash' }), })); -jest.mock('hash-wasm', () => ({ +const { argon2Verify } = mockEsm('hash-wasm', () => ({ argon2Verify: jest.fn(), })); +const verifyProfile = await pickDefault(import('./profile-verification.js')); + describe('forgot password interaction profile verification', () => { - const provider = new Provider(''); + const provider = createMockProvider(); const baseCtx = createContextWithRouteParameters(); const interaction = { @@ -55,7 +50,7 @@ describe('forgot password interaction profile verification', () => { }); it('same password', async () => { - (argon2Verify as jest.Mock).mockResolvedValueOnce(true); + argon2Verify.mockResolvedValueOnce(true); const ctx: InteractionContext = { ...baseCtx, interactionPayload: { 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 index 57bc9d52f..992b361cb 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 @@ -1,8 +1,8 @@ import { Event } from '@logto/schemas'; -import { Provider } from 'oidc-provider'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; -import { findUserById } from '#src/queries/user.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import type { @@ -10,32 +10,28 @@ import type { IdentifierVerifiedInteractionResult, InteractionContext, } from '../types/index.js'; -import { storeInteractionResult } from '../utils/interaction.js'; -import verifyProfile from './profile-verification.js'; -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })), - })), -})); +const { jest } = import.meta; -jest.mock('#src/queries/user.js', () => ({ +const { findUserById } = 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), })); -jest.mock('../utils/interaction.js', () => ({ +const { storeInteractionResult } = mockEsm('../utils/interaction.js', () => ({ storeInteractionResult: jest.fn(), })); -jest.mock('../utils/index.js', () => ({ +mockEsm('../utils/index.js', () => ({ isUserPasswordSet: jest.fn().mockResolvedValueOnce(true), })); +const verifyProfile = await pickDefault(import('./profile-verification.js')); + describe('Should throw when providing existing identifiers in profile', () => { - const provider = new Provider(''); + const provider = createMockProvider(); const baseCtx = createContextWithRouteParameters(); const identifiers: Identifier[] = [ { key: 'accountId', value: 'foo' }, @@ -54,7 +50,7 @@ describe('Should throw when providing existing identifiers in profile', () => { }); it('username exists', async () => { - (findUserById as jest.Mock).mockResolvedValueOnce({ id: 'foo', username: 'foo' }); + findUserById.mockResolvedValueOnce({ id: 'foo', username: 'foo' }); const ctx: InteractionContext = { ...baseCtx, @@ -75,7 +71,7 @@ describe('Should throw when providing existing identifiers in profile', () => { }); it('email exists', async () => { - (findUserById as jest.Mock).mockResolvedValueOnce({ id: 'foo', primaryEmail: 'email' }); + findUserById.mockResolvedValueOnce({ id: 'foo', primaryEmail: 'email' }); const ctx: InteractionContext = { ...baseCtx, @@ -96,7 +92,7 @@ describe('Should throw when providing existing identifiers in profile', () => { }); it('phone exists', async () => { - (findUserById as jest.Mock).mockResolvedValueOnce({ id: 'foo', primaryPhone: 'phone' }); + findUserById.mockResolvedValueOnce({ id: 'foo', primaryPhone: 'phone' }); const ctx: InteractionContext = { ...baseCtx, @@ -117,7 +113,7 @@ describe('Should throw when providing existing identifiers in profile', () => { }); it('password exists', async () => { - (findUserById as jest.Mock).mockResolvedValueOnce({ id: 'foo' }); + findUserById.mockResolvedValueOnce({ id: 'foo' }); const ctx: InteractionContext = { ...baseCtx, 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 index 0af867ba5..295af32bd 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,13 +1,8 @@ import { Event } from '@logto/schemas'; -import { Provider } from 'oidc-provider'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; -import { - hasUser, - hasUserWithEmail, - hasUserWithPhone, - hasUserWithIdentity, -} from '#src/queries/user.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import type { @@ -15,33 +10,30 @@ import type { InteractionContext, IdentifierVerifiedInteractionResult, } from '../types/index.js'; -import { storeInteractionResult } from '../utils/interaction.js'; -import verifyProfile from './profile-verification.js'; -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })), - })), -})); +const { jest } = import.meta; -jest.mock('../utils/interaction.js', () => ({ +const { storeInteractionResult } = mockEsm('../utils/interaction.js', () => ({ storeInteractionResult: jest.fn(), })); -jest.mock('#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 { 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), + })); -jest.mock('#src/connectors/index.js', () => ({ +mockEsm('#src/connectors/index.js', () => ({ getLogtoConnectorById: jest.fn().mockResolvedValue({ metadata: { target: 'logto' }, }), })); +const verifyProfile = await pickDefault(import('./profile-verification.js')); + const baseCtx = createContextWithRouteParameters(); const identifiers: Identifier[] = [ { key: 'accountId', value: 'foo' }, @@ -49,7 +41,7 @@ const identifiers: Identifier[] = [ { key: 'phoneVerified', value: '123456' }, { key: 'social', connectorId: 'connectorId', userInfo: { id: 'foo' } }, ]; -const provider = new Provider(''); +const provider = createMockProvider(); const interaction: IdentifierVerifiedInteractionResult = { event: Event.Register, @@ -140,7 +132,7 @@ describe('register payload guard', () => { describe('profile registered validation', () => { it('username is registered', async () => { - (hasUser as jest.Mock).mockResolvedValueOnce(true); + hasUser.mockResolvedValueOnce(true); const ctx: InteractionContext = { ...baseCtx, @@ -163,7 +155,7 @@ describe('profile registered validation', () => { }); it('email is registered', async () => { - (hasUserWithEmail as jest.Mock).mockResolvedValueOnce(true); + hasUserWithEmail.mockResolvedValueOnce(true); const ctx: InteractionContext = { ...baseCtx, @@ -185,7 +177,7 @@ describe('profile registered validation', () => { }); it('phone is registered', async () => { - (hasUserWithPhone as jest.Mock).mockResolvedValueOnce(true); + hasUserWithPhone.mockResolvedValueOnce(true); const ctx: InteractionContext = { ...baseCtx, @@ -207,7 +199,7 @@ describe('profile registered validation', () => { }); it('connector identity exist', async () => { - (hasUserWithIdentity as jest.Mock).mockResolvedValueOnce(true); + hasUserWithIdentity.mockResolvedValueOnce(true); const ctx: InteractionContext = { ...baseCtx, 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 index 626cac78b..571a94aad 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,40 +1,37 @@ import { Event } from '@logto/schemas'; -import { Provider } from 'oidc-provider'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import type { Identifier, InteractionContext } from '../types/index.js'; -import { storeInteractionResult } from '../utils/interaction.js'; -import verifyProfile from './profile-verification.js'; -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })), - })), -})); +const { jest } = import.meta; -jest.mock('../utils/interaction.js', () => ({ +const { storeInteractionResult } = mockEsm('../utils/interaction.js', () => ({ storeInteractionResult: jest.fn(), })); -jest.mock('#src/queries/user.js', () => ({ +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), })); -jest.mock('#src/connectors/index.js', () => ({ +mockEsm('#src/connectors/index.js', () => ({ getLogtoConnectorById: jest.fn().mockResolvedValue({ metadata: { target: 'logto' }, }), })); +const verifyProfile = await pickDefault(import('./profile-verification.js')); + describe('profile protected identifier verification', () => { const baseCtx = createContextWithRouteParameters(); const interaction = { event: Event.SignIn, accountId: 'foo' }; - const provider = new Provider(''); + const provider = createMockProvider(); afterEach(() => { jest.clearAllMocks(); 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 8ac0bc348..bc3c305a6 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 @@ -1,32 +1,28 @@ import { Event } from '@logto/schemas'; -import { Provider } from 'oidc-provider'; +import { mockEsm, mockEsmDefault, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import RequestError from '#src/errors/RequestError/index.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; import type { InteractionContext, PayloadVerifiedInteractionResult } from '../types/index.js'; -import findUserByIdentifier from '../utils/find-user-by-identifier.js'; -import { storeInteractionResult } from '../utils/interaction.js'; -import userAccountVerification from './user-identity-verification.js'; -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails: jest.fn(async () => ({ params: {}, jti: 'jti' })), - })), -})); +const { jest } = import.meta; -jest.mock('../utils/find-user-by-identifier.js', () => jest.fn()); -jest.mock('#src/lib/social.js', () => ({ +const findUserByIdentifier = mockEsmDefault('../utils/find-user-by-identifier.js', () => jest.fn()); + +mockEsm('#src/lib/social.js', () => ({ findSocialRelatedUser: jest.fn().mockResolvedValue(null), })); -jest.mock('../utils/interaction.js', () => ({ - ...jest.requireActual('../utils/interaction.js'), +const { storeInteractionResult } = await mockEsmWithActual('../utils/interaction.js', () => ({ storeInteractionResult: jest.fn(), })); +const userAccountVerification = await pickDefault(import('./user-identity-verification.js')); + describe('userAccountVerification', () => { - const findUserByIdentifierMock = findUserByIdentifier as jest.Mock; + const findUserByIdentifierMock = findUserByIdentifier; const ctx: InteractionContext = { ...createContextWithRouteParameters(), @@ -34,7 +30,7 @@ describe('userAccountVerification', () => { event: Event.SignIn, }, }; - const provider = new Provider(''); + const provider = createMockProvider(); afterEach(() => { jest.clearAllMocks(); diff --git a/packages/core/src/routes/log.test.ts b/packages/core/src/routes/log.test.ts index 4914c0b84..ae3d50504 100644 --- a/packages/core/src/routes/log.test.ts +++ b/packages/core/src/routes/log.test.ts @@ -1,25 +1,21 @@ -import type { LogCondition } from '#src/queries/log.js'; -import logRoutes from '#src/routes/log.js'; +import { mockEsm, pickDefault } from '@logto/shared/esm'; + import { createRequester } from '#src/utils/test-utils.js'; +const { jest } = import.meta; + const mockBody = { type: 'a', payload: {}, createdAt: 123 }; const mockLog = { id: '1', ...mockBody }; const mockLogs = [mockLog, { id: '2', ...mockBody }]; -const countLogs = jest.fn(async (condition: LogCondition) => ({ - count: mockLogs.length, -})); -const findLogs = jest.fn( - async (limit: number, offset: number, condition: LogCondition) => mockLogs -); -const findLogById = jest.fn(async (id: string) => mockLog); - -jest.mock('#src/queries/log.js', () => ({ - countLogs: async (condition: LogCondition) => countLogs(condition), - findLogs: async (limit: number, offset: number, condition: LogCondition) => - findLogs(limit, offset, condition), - findLogById: async (id: string) => findLogById(id), +const { countLogs, findLogs, findLogById } = mockEsm('#src/queries/log.js', () => ({ + countLogs: jest.fn().mockResolvedValue({ + count: mockLogs.length, + }), + findLogs: jest.fn().mockResolvedValue(mockLogs), + findLogById: jest.fn().mockResolvedValue(mockLog), })); +const logRoutes = await pickDefault(import('./log.js')); describe('logRoutes', () => { const logRequest = createRequester({ authedRoutes: logRoutes }); diff --git a/packages/core/src/routes/phrase.content-language.test.ts b/packages/core/src/routes/phrase.content-language.test.ts index 39e01b79c..642596862 100644 --- a/packages/core/src/routes/phrase.content-language.test.ts +++ b/packages/core/src/routes/phrase.content-language.test.ts @@ -1,51 +1,42 @@ import en from '@logto/phrases-ui/lib/locales/en.js'; -import { Provider } from 'oidc-provider'; +import { mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { trTrTag, zhCnTag, zhHkTag } from '#src/__mocks__/custom-phrase.js'; import { mockSignInExperience } from '#src/__mocks__/index.js'; -import phraseRoutes from '#src/routes/phrase.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createRequester } from '#src/utils/test-utils.js'; -const mockApplicationId = 'mockApplicationIdValue'; - -const interactionDetails: jest.MockedFunction<() => Promise> = jest.fn(async () => ({ - params: { client_id: mockApplicationId }, -})); - -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails, - })), -})); +const { jest } = import.meta; const fallbackLanguage = trTrTag; const unsupportedLanguageX = 'xx-XX'; const unsupportedLanguageY = 'yy-YY'; -const findDefaultSignInExperience = jest.fn(async () => ({ - ...mockSignInExperience, - languageInfo: { - autoDetect: true, - fallbackLanguage, - }, -})); +const { findDefaultSignInExperience } = await mockEsmWithActual( + '#src/queries/sign-in-experience.js', + () => ({ + findDefaultSignInExperience: jest.fn(async () => ({ + ...mockSignInExperience, + languageInfo: { + autoDetect: true, + fallbackLanguage, + }, + })), + }) +); -jest.mock('#src/queries/sign-in-experience.js', () => ({ - findDefaultSignInExperience: async () => findDefaultSignInExperience(), -})); - -jest.mock('#src/queries/custom-phrase.js', () => ({ +await mockEsmWithActual('#src/queries/custom-phrase.js', () => ({ findAllCustomLanguageTags: async () => [trTrTag, zhCnTag], })); -jest.mock('#src/lib/phrase.js', () => ({ - ...jest.requireActual('#src/lib/phrase.js'), +await mockEsmWithActual('#src/lib/phrase.js', () => ({ getPhrase: jest.fn().mockResolvedValue(en), })); +const phraseRoutes = await pickDefault(import('./phrase.js')); const phraseRequest = createRequester({ anonymousRoutes: phraseRoutes, - provider: new Provider(''), + provider: createMockProvider(), }); afterEach(() => { diff --git a/packages/core/src/routes/phrase.test.ts b/packages/core/src/routes/phrase.test.ts index 03a1a21aa..413d28755 100644 --- a/packages/core/src/routes/phrase.test.ts +++ b/packages/core/src/routes/phrase.test.ts @@ -4,67 +4,48 @@ import { adminConsoleApplicationId, adminConsoleSignInExperience, } from '@logto/schemas/lib/seeds/index.js'; -import { Provider } from 'oidc-provider'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { zhCnTag } from '#src/__mocks__/custom-phrase.js'; import { mockSignInExperience } from '#src/__mocks__/index.js'; -import * as detectLanguage from '#src/i18n/detect-language.js'; -import phraseRoutes from '#src/routes/phrase.js'; -import { createRequester } from '#src/utils/test-utils.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; -const mockApplicationId = 'mockApplicationIdValue'; - -const interactionDetails: jest.MockedFunction<() => Promise> = jest.fn(async () => ({ - params: { client_id: mockApplicationId }, -})); - -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails, - })), -})); +const { jest } = import.meta; const customizedLanguage = zhCnTag; -const findDefaultSignInExperience = jest.fn( - async (): Promise => ({ - ...mockSignInExperience, - languageInfo: { - autoDetect: true, - fallbackLanguage: customizedLanguage, - }, - }) -); - -jest.mock('#src/queries/sign-in-experience.js', () => ({ - findDefaultSignInExperience: async () => findDefaultSignInExperience(), +const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience.js', () => ({ + findDefaultSignInExperience: jest.fn( + async (): Promise => ({ + ...mockSignInExperience, + languageInfo: { + autoDetect: true, + fallbackLanguage: customizedLanguage, + }, + }) + ), })); -const detectLanguageSpy = jest.spyOn(detectLanguage, 'default'); - -const findAllCustomLanguageTags = jest.fn(async () => [customizedLanguage]); -const findCustomPhraseByLanguageTag = jest.fn(async (tag: string) => ({})); - -jest.mock('#src/queries/custom-phrase.js', () => ({ - findAllCustomLanguageTags: async () => findAllCustomLanguageTags(), - findCustomPhraseByLanguageTag: async (tag: string) => findCustomPhraseByLanguageTag(tag), +const { default: detectLanguageSpy } = mockEsm('#src/i18n/detect-language.js', () => ({ + default: jest.fn().mockReturnValue([]), })); -const getPhrase = jest.fn(async (language: string, customLanguages: string[]) => zhCN); - -jest.mock('#src/lib/phrase.js', () => ({ - ...jest.requireActual('#src/lib/phrase.js'), - getPhrase: async (language: string, customLanguages: string[]) => - getPhrase(language, customLanguages), +const { findAllCustomLanguageTags } = mockEsm('#src/queries/custom-phrase.js', () => ({ + findAllCustomLanguageTags: jest.fn(async () => [customizedLanguage]), + findCustomPhraseByLanguageTag: jest.fn(async (tag: string) => ({})), })); +const { getPhrase } = await mockEsmWithActual('#src/lib/phrase.js', () => ({ + getPhrase: jest.fn(async () => zhCN), +})); + +const interactionDetails = jest.fn(); +const phraseRoutes = await pickDefault(import('./phrase.js')); + +const { createRequester } = await import('#src/utils/test-utils.js'); const phraseRequest = createRequester({ anonymousRoutes: phraseRoutes, - provider: new Provider(''), -}); - -afterEach(() => { - jest.clearAllMocks(); + provider: createMockProvider(interactionDetails), }); describe('when the application is admin-console', () => { @@ -74,6 +55,10 @@ describe('when the application is admin-console', () => { }); }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should call interactionDetails', async () => { await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200); expect(interactionDetails).toBeCalledTimes(1); @@ -104,6 +89,18 @@ describe('when the application is admin-console', () => { }); describe('when the application is not admin-console', () => { + beforeEach(() => { + interactionDetails.mockResolvedValue({ + params: {}, + jti: 'jti', + client_id: 'mockApplicationId', + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('should call interactionDetails', async () => { await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200); expect(interactionDetails).toBeCalledTimes(1); diff --git a/packages/core/src/routes/profile.test.ts b/packages/core/src/routes/profile.test.ts index bc6023022..5d2dcd2ac 100644 --- a/packages/core/src/routes/profile.test.ts +++ b/packages/core/src/routes/profile.test.ts @@ -1,8 +1,7 @@ -/* eslint-disable max-lines */ import type { CreateUser, User } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; +import { mockEsm, mockEsmWithActual } from '@logto/shared/esm'; import { getUnixTime } from 'date-fns'; -import { Provider } from 'oidc-provider'; import { mockLogtoConnectorList, @@ -11,101 +10,79 @@ import { mockUserResponse, } from '#src/__mocks__/index.js'; import type { SocialUserInfo } from '#src/connectors/types.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createRequester } from '#src/utils/test-utils.js'; -import profileRoutes, { profileRoute } from './profile.js'; +const { jest } = import.meta; -const mockFindUserById = jest.fn(async (): Promise => mockUser); -const mockHasUser = jest.fn(async () => false); -const mockHasUserWithEmail = jest.fn(async () => false); -const mockHasUserWithPhone = jest.fn(async () => false); -const mockUpdateUserById = jest.fn( - async (_, data: Partial): Promise => ({ - ...mockUser, - ...data, - }) -); -const mockDeleteUserIdentity = jest.fn(); -const encryptUserPassword = jest.fn(async (password: string) => ({ - passwordEncrypted: password + '_user1', - passwordEncryptionMethod: 'Argon2i', -})); -const mockArgon2Verify = jest.fn(async (password: string) => password === mockPasswordEncrypted); -const mockGetSession = jest.fn(); - -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - Session: { - get: async () => mockGetSession(), - }, - })), -})); - -jest.mock('#src/lib/user.js', () => ({ - ...jest.requireActual('#src/lib/user.js'), - encryptUserPassword: async (password: string) => encryptUserPassword(password), -})); - -const mockGetLogtoConnectorById = jest.fn(async () => ({ +const getLogtoConnectorById = jest.fn(async () => ({ dbEntry: { enabled: true }, metadata: { id: 'connectorId', target: 'mock_social' }, type: ConnectorType.Social, getAuthorizationUri: jest.fn(async () => ''), })); -jest.mock('#src/connectors/index.js', () => ({ - getLogtoConnectors: jest.fn(async () => mockLogtoConnectorList), - getLogtoConnectorById: jest.fn(async () => mockGetLogtoConnectorById()), +mockEsm('#src/connectors/index.js', () => ({ + getLogtoConnectors: mockLogtoConnectorList, + getLogtoConnectorById, })); -const mockFindSocialRelatedUser = jest.fn(async () => [ - { id: 'user1', identities: {}, isSuspended: false }, -]); -const mockGetUserInfoByAuthCode = jest.fn(); -jest.mock('#src/lib/social.js', () => ({ - ...jest.requireActual('#src/lib/social.js'), - findSocialRelatedUser: async () => mockFindSocialRelatedUser(), - getUserInfoByAuthCode: async () => mockGetUserInfoByAuthCode(), +const { getUserInfoByAuthCode } = await mockEsmWithActual('#src/lib/social.js', () => ({ + findSocialRelatedUser: jest.fn(async () => [{ id: 'user1', identities: {}, isSuspended: false }]), + getUserInfoByAuthCode: jest.fn(), })); -jest.mock('#src/queries/user.js', () => ({ - ...jest.requireActual('#src/queries/user.js'), - findUserById: async () => mockFindUserById(), - hasUser: async () => mockHasUser(), - hasUserWithEmail: async () => mockHasUserWithEmail(), - hasUserWithPhone: async () => mockHasUserWithPhone(), - updateUserById: async (id: string, data: Partial) => mockUpdateUserById(id, data), - deleteUserIdentity: async (...args: unknown[]) => mockDeleteUserIdentity(...args), +const { + findUserById, + hasUser, + hasUserWithEmail, + hasUserWithPhone, + updateUserById, + deleteUserIdentity, +} = await mockEsmWithActual('#src/queries/user.js', () => ({ + findUserById: jest.fn(async (): Promise => mockUser), + hasUser: jest.fn(async () => false), + hasUserWithEmail: jest.fn(async () => false), + hasUserWithPhone: jest.fn(async () => false), + updateUserById: jest.fn( + async (_, data: Partial): Promise => ({ + ...mockUser, + ...data, + }) + ), + deleteUserIdentity: jest.fn(), })); -const mockFindDefaultSignInExperience = jest.fn(async () => ({ - signUp: { - identifier: [], - password: false, - verify: false, - }, +const { encryptUserPassword } = await mockEsmWithActual('#src/lib/user.js', () => ({ + encryptUserPassword: jest.fn(async (password: string) => ({ + passwordEncrypted: password + '_user1', + passwordEncryptionMethod: 'Argon2i', + })), })); -jest.mock('#src/queries/sign-in-experience.js', () => ({ - findDefaultSignInExperience: jest.fn(async () => mockFindDefaultSignInExperience()), +mockEsm('#src/queries/sign-in-experience.js', () => ({ + findDefaultSignInExperience: async () => ({ + signUp: { + identifier: [], + password: false, + verify: false, + }, + }), })); -jest.mock('hash-wasm', () => ({ - argon2Verify: async (password: string) => mockArgon2Verify(password), +const { argon2Verify } = mockEsm('hash-wasm', () => ({ + argon2Verify: jest.fn(async (password: string) => password === mockPasswordEncrypted), })); +const { default: profileRoutes, profileRoute } = await import('./profile.js'); + describe('session -> profileRoutes', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockGetSession.mockImplementation(async () => ({ - accountId: 'id', - loginTs: getUnixTime(new Date()) - 60, - })); - }); - + const provider = createMockProvider(); + // @ts-expect-error for testing + const mockGetSession: jest.Mock = jest.spyOn(provider.Session, 'get'); const sessionRequest = createRequester({ anonymousRoutes: profileRoutes, - provider: new Provider(''), + provider, middlewares: [ async (ctx, next) => { ctx.addLogContext = jest.fn(); @@ -116,6 +93,14 @@ describe('session -> profileRoutes', () => { ], }); + beforeEach(() => { + jest.clearAllMocks(); + mockGetSession.mockImplementation(async () => ({ + accountId: 'id', + loginTs: getUnixTime(new Date()) - 60, + })); + }); + describe('GET /session/profile', () => { it('should return current user data', async () => { const response = await sessionRequest.get(profileRoute); @@ -146,7 +131,7 @@ describe('session -> profileRoutes', () => { const response = await sessionRequest.patch(profileRoute).send(updatedUserInfo); - expect(mockUpdateUserById).toBeCalledWith('id', expect.objectContaining(updatedUserInfo)); + expect(updateUserById).toBeCalledWith('id', expect.objectContaining(updatedUserInfo)); expect(response.statusCode).toEqual(204); }); @@ -175,7 +160,7 @@ describe('session -> profileRoutes', () => { .send({ username: 'test' }); expect(response.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should update username with the new value', async () => { @@ -190,7 +175,7 @@ describe('session -> profileRoutes', () => { }); it('should throw when username is already in use', async () => { - mockHasUser.mockImplementationOnce(async () => true); + hasUser.mockImplementationOnce(async () => true); const response = await sessionRequest .patch(`${profileRoute}/username`) @@ -212,7 +197,7 @@ describe('session -> profileRoutes', () => { .send({ password: mockPasswordEncrypted }); expect(response.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should update password with the new value', async () => { @@ -220,7 +205,7 @@ describe('session -> profileRoutes', () => { .patch(`${profileRoute}/password`) .send({ password: mockPasswordEncrypted }); - expect(mockUpdateUserById).toBeCalledWith( + expect(updateUserById).toBeCalledWith( 'id', expect.objectContaining({ passwordEncrypted: 'a1b2c3_user1', @@ -235,14 +220,14 @@ describe('session -> profileRoutes', () => { passwordEncrypted: password, passwordEncryptionMethod: 'Argon2i', })); - mockArgon2Verify.mockResolvedValueOnce(true); + argon2Verify.mockResolvedValueOnce(true); const response = await sessionRequest .patch(`${profileRoute}/password`) .send({ password: 'password' }); expect(response.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); }); @@ -258,7 +243,7 @@ describe('session -> profileRoutes', () => { .send({ primaryEmail: 'test@logto.io' }); expect(response.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should link email address to the user profile', async () => { @@ -267,7 +252,7 @@ describe('session -> profileRoutes', () => { .patch(`${profileRoute}/email`) .send({ primaryEmail: mockEmailAddress }); - expect(mockUpdateUserById).toBeCalledWith( + expect(updateUserById).toBeCalledWith( 'id', expect.objectContaining({ primaryEmail: mockEmailAddress, @@ -277,25 +262,25 @@ describe('session -> profileRoutes', () => { }); it('should throw when email address already exists', async () => { - mockHasUserWithEmail.mockImplementationOnce(async () => true); + hasUserWithEmail.mockImplementationOnce(async () => true); const response = await sessionRequest .patch(`${profileRoute}/email`) .send({ primaryEmail: mockUser.primaryEmail }); expect(response.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should throw when email address is invalid', async () => { - mockHasUserWithEmail.mockImplementationOnce(async () => true); + hasUserWithEmail.mockImplementationOnce(async () => true); const response = await sessionRequest .patch(`${profileRoute}/email`) .send({ primaryEmail: 'test' }); expect(response.statusCode).toEqual(400); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should throw if last authentication time is over 10 mins ago on unlinking email', async () => { @@ -307,13 +292,13 @@ describe('session -> profileRoutes', () => { const response = await sessionRequest.delete(`${profileRoute}/email`); expect(response.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should unlink email address from user', async () => { const response = await sessionRequest.delete(`${profileRoute}/email`); expect(response.statusCode).toEqual(204); - expect(mockUpdateUserById).toBeCalledWith( + expect(updateUserById).toBeCalledWith( 'id', expect.objectContaining({ primaryEmail: null, @@ -322,11 +307,11 @@ describe('session -> profileRoutes', () => { }); it('should throw when no email address found in user on unlinking email', async () => { - mockFindUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryEmail: null })); + findUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryEmail: null })); const response = await sessionRequest.delete(`${profileRoute}/email`); expect(response.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); }); @@ -342,7 +327,7 @@ describe('session -> profileRoutes', () => { .send({ primaryPhone: '6533333333' }); expect(updateResponse.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should link phone number to the user profile', async () => { @@ -351,7 +336,7 @@ describe('session -> profileRoutes', () => { .patch(`${profileRoute}/phone`) .send({ primaryPhone: mockPhoneNumber }); - expect(mockUpdateUserById).toBeCalledWith( + expect(updateUserById).toBeCalledWith( 'id', expect.objectContaining({ primaryPhone: mockPhoneNumber, @@ -361,25 +346,25 @@ describe('session -> profileRoutes', () => { }); it('should throw when phone number already exists on linking phone number', async () => { - mockHasUserWithPhone.mockImplementationOnce(async () => true); + hasUserWithPhone.mockImplementationOnce(async () => true); const response = await sessionRequest .patch(`${profileRoute}/phone`) .send({ primaryPhone: mockUser.primaryPhone }); expect(response.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should throw when phone number is invalid', async () => { - mockHasUserWithPhone.mockImplementationOnce(async () => true); + hasUserWithPhone.mockImplementationOnce(async () => true); const response = await sessionRequest .patch(`${profileRoute}/phone`) .send({ primaryPhone: 'invalid' }); expect(response.statusCode).toEqual(400); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should throw if last authentication time is over 10 mins ago on unlinking phone number', async () => { @@ -391,13 +376,13 @@ describe('session -> profileRoutes', () => { const response = await sessionRequest.delete(`${profileRoute}/phone`); expect(response.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should unlink phone number from user', async () => { const response = await sessionRequest.delete(`${profileRoute}/phone`); expect(response.statusCode).toEqual(204); - expect(mockUpdateUserById).toBeCalledWith( + expect(updateUserById).toBeCalledWith( 'id', expect.objectContaining({ primaryPhone: null, @@ -406,11 +391,11 @@ describe('session -> profileRoutes', () => { }); it('should throw when no phone number found in user on unlinking phone number', async () => { - mockFindUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryPhone: null })); + findUserById.mockImplementationOnce(async () => ({ ...mockUser, primaryPhone: null })); const response = await sessionRequest.delete(`${profileRoute}/phone`); expect(response.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); }); @@ -423,7 +408,7 @@ describe('session -> profileRoutes', () => { email: 'johndoe@social.com', phone: '123456789', }; - mockGetUserInfoByAuthCode.mockReturnValueOnce(mockSocialUserInfo); + getUserInfoByAuthCode.mockReturnValueOnce(mockSocialUserInfo); const response = await sessionRequest.patch(`${profileRoute}/identities`).send({ connectorId: 'connectorId', @@ -431,7 +416,7 @@ describe('session -> profileRoutes', () => { }); expect(response.statusCode).toEqual(204); - expect(mockUpdateUserById).toBeCalledWith( + expect(updateUserById).toBeCalledWith( 'id', expect.objectContaining({ identities: { @@ -456,7 +441,7 @@ describe('session -> profileRoutes', () => { }); expect(response.statusCode).toEqual(401); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should throw if last authentication time is over 10 mins ago on linking email', async () => { @@ -470,11 +455,11 @@ describe('session -> profileRoutes', () => { .send({ connectorId: 'connectorId', data: { code: '123456' } }); expect(response.statusCode).toEqual(422); - expect(mockUpdateUserById).not.toBeCalled(); + expect(updateUserById).not.toBeCalled(); }); it('should unlink social identities from user', async () => { - mockFindUserById.mockImplementationOnce(async () => ({ + findUserById.mockImplementationOnce(async () => ({ ...mockUser, identities: { mock_social: { @@ -490,8 +475,7 @@ describe('session -> profileRoutes', () => { const response = await sessionRequest.delete(`${profileRoute}/identities/mock_social`); expect(response.statusCode).toEqual(204); - expect(mockDeleteUserIdentity).toBeCalledWith('id', 'mock_social'); + expect(deleteUserIdentity).toBeCalledWith('id', 'mock_social'); }); }); }); -/* eslint-enable max-lines */ diff --git a/packages/core/src/routes/profile.ts b/packages/core/src/routes/profile.ts index b815c1f5a..991322411 100644 --- a/packages/core/src/routes/profile.ts +++ b/packages/core/src/routes/profile.ts @@ -65,15 +65,12 @@ export default function profileRoutes(router: T, prov }), async (ctx, next) => { const userId = await checkSessionHealth(ctx, provider, verificationTimeout); - assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 })); const { username } = ctx.guard.body; - await checkIdentifierCollision({ username }, userId); const user = await updateUserById(userId, { username }, 'replace'); - ctx.body = pick(user, ...userInfoSelectFields); return next(); diff --git a/packages/core/src/routes/resource.test.ts b/packages/core/src/routes/resource.test.ts index b65f32740..03da2cd92 100644 --- a/packages/core/src/routes/resource.test.ts +++ b/packages/core/src/routes/resource.test.ts @@ -1,34 +1,33 @@ import type { Resource, CreateResource } from '@logto/schemas'; +import { mockEsm, pickDefault } from '@logto/shared/esm'; import { mockResource } from '#src/__mocks__/index.js'; import { createRequester } from '#src/utils/test-utils.js'; -import resourceRoutes from './resource.js'; +const { jest } = import.meta; -jest.mock('#src/queries/resource.js', () => ({ - findTotalNumberOfResources: jest.fn(async () => ({ count: 10 })), - findAllResources: jest.fn(async (): Promise => [mockResource]), - findResourceById: jest.fn(async (): Promise => mockResource), - insertResource: jest.fn( - async (body: CreateResource): Promise => ({ - ...mockResource, - ...body, - }) - ), - updateResourceById: jest.fn( - async (_, data: Partial): Promise => ({ - ...mockResource, - ...data, - }) - ), +mockEsm('#src/queries/resource.js', () => ({ + findTotalNumberOfResources: async () => ({ count: 10 }), + findAllResources: async (): Promise => [mockResource], + findResourceById: async (): Promise => mockResource, + insertResource: async (body: CreateResource): Promise => ({ + ...mockResource, + ...body, + }), + updateResourceById: async (_: unknown, data: Partial): Promise => ({ + ...mockResource, + ...data, + }), deleteResourceById: jest.fn(), })); -jest.mock('@logto/shared', () => ({ +mockEsm('@logto/shared', () => ({ // eslint-disable-next-line unicorn/consistent-function-scoping - buildIdGenerator: jest.fn(() => () => 'randomId'), + buildIdGenerator: () => () => 'randomId', })); +const resourceRoutes = await pickDefault(import('./resource.js')); + describe('resource routes', () => { const resourceRequest = createRequester({ authedRoutes: resourceRoutes }); diff --git a/packages/core/src/routes/role.test.ts b/packages/core/src/routes/role.test.ts index 5d72ff2e8..a31447a40 100644 --- a/packages/core/src/routes/role.test.ts +++ b/packages/core/src/routes/role.test.ts @@ -1,13 +1,15 @@ import type { Role } from '@logto/schemas'; +import { mockEsm, pickDefault } from '@logto/shared/esm'; import { mockRole } from '#src/__mocks__/index.js'; import { createRequester } from '#src/utils/test-utils.js'; -import roleRoutes from './role.js'; +const { jest } = import.meta; -jest.mock('#src/queries/roles.js', () => ({ +mockEsm('#src/queries/roles.js', () => ({ findAllRoles: jest.fn(async (): Promise => [mockRole]), })); +const roleRoutes = await pickDefault(import('./role.js')); describe('role routes', () => { const roleRequester = createRequester({ authedRoutes: roleRoutes }); diff --git a/packages/core/src/routes/session/utils.test.ts b/packages/core/src/routes/session/utils.test.ts index fcf3488fd..b9ea84d72 100644 --- a/packages/core/src/routes/session/utils.test.ts +++ b/packages/core/src/routes/session/utils.test.ts @@ -1,11 +1,11 @@ import type { User } from '@logto/schemas'; import { UserRole, SignInIdentifier } from '@logto/schemas'; -import { createMockContext } from '@shopify/jest-koa-mocks'; import type { Nullable } from '@silverhand/essentials'; import { Provider } from 'oidc-provider'; import { mockSignInExperience, mockSignInMethod, mockUser } from '#src/__mocks__/index.js'; import RequestError from '#src/errors/RequestError/index.js'; +import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js'; import { checkRequiredProfile, signInWithPassword } from './utils.js'; diff --git a/packages/core/src/routes/setting.test.ts b/packages/core/src/routes/setting.test.ts index 44c51087d..f3ccc1e61 100644 --- a/packages/core/src/routes/setting.test.ts +++ b/packages/core/src/routes/setting.test.ts @@ -1,20 +1,19 @@ import type { Setting, CreateSetting } from '@logto/schemas'; +import { mockEsm, pickDefault } from '@logto/shared/esm'; import { mockSetting } from '#src/__mocks__/index.js'; import { createRequester } from '#src/utils/test-utils.js'; -import settingRoutes from './setting.js'; - -jest.mock('#src/queries/setting.js', () => ({ - getSetting: jest.fn(async (): Promise => mockSetting), - updateSetting: jest.fn( - async (data: Partial): Promise => ({ - ...mockSetting, - ...data, - }) - ), +mockEsm('#src/queries/setting.js', () => ({ + getSetting: async (): Promise => mockSetting, + updateSetting: async (data: Partial): Promise => ({ + ...mockSetting, + ...data, + }), })); +const settingRoutes = await pickDefault(import('./setting.js')); + describe('settings routes', () => { const roleRequester = createRequester({ authedRoutes: settingRoutes }); diff --git a/packages/core/src/routes/sign-in-experience.branding.guard.test.ts b/packages/core/src/routes/sign-in-experience.branding.guard.test.ts index f7aadbd4a..a0f2532ec 100644 --- a/packages/core/src/routes/sign-in-experience.branding.guard.test.ts +++ b/packages/core/src/routes/sign-in-experience.branding.guard.test.ts @@ -1,24 +1,24 @@ import type { CreateSignInExperience, SignInExperience } from '@logto/schemas'; import { BrandingStyle } from '@logto/schemas'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { mockBranding, mockSignInExperience } from '#src/__mocks__/index.js'; import { createRequester } from '#src/utils/test-utils.js'; -import signInExperiencesRoutes from './sign-in-experience.js'; - -jest.mock('#src/queries/sign-in-experience.js', () => ({ - updateDefaultSignInExperience: jest.fn( - async (data: Partial): Promise => ({ - ...mockSignInExperience, - ...data, - }) - ), +await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({ + updateDefaultSignInExperience: async ( + data: Partial + ): Promise => ({ + ...mockSignInExperience, + ...data, + }), })); -jest.mock('#src/connectors.js', () => ({ - getLogtoConnectors: jest.fn(async () => []), +mockEsm('#src/connectors.js', () => ({ + getLogtoConnectors: async () => [], })); +const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js')); const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes }); const expectPatchResponseStatus = async ( diff --git a/packages/core/src/routes/sign-in-experience.color.guard.test.ts b/packages/core/src/routes/sign-in-experience.color.guard.test.ts index a254234a4..d6295c2ed 100644 --- a/packages/core/src/routes/sign-in-experience.color.guard.test.ts +++ b/packages/core/src/routes/sign-in-experience.color.guard.test.ts @@ -1,23 +1,23 @@ import type { CreateSignInExperience, SignInExperience } from '@logto/schemas'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { mockColor, mockSignInExperience } from '#src/__mocks__/index.js'; import { createRequester } from '#src/utils/test-utils.js'; -import signInExperiencesRoutes from './sign-in-experience.js'; - -jest.mock('#src/queries/sign-in-experience.js', () => ({ - updateDefaultSignInExperience: jest.fn( - async (data: Partial): Promise => ({ - ...mockSignInExperience, - ...data, - }) - ), +await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({ + updateDefaultSignInExperience: async ( + data: Partial + ): Promise => ({ + ...mockSignInExperience, + ...data, + }), })); -jest.mock('#src/connectors.js', () => ({ - getLogtoConnectors: jest.fn(async () => []), +mockEsm('#src/connectors.js', () => ({ + getLogtoConnectors: async () => [], })); +const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js')); const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes }); const expectPatchResponseStatus = async ( diff --git a/packages/core/src/routes/sign-in-experience.guard.test.ts b/packages/core/src/routes/sign-in-experience.guard.test.ts index b76b8df00..063ea6ce0 100644 --- a/packages/core/src/routes/sign-in-experience.guard.test.ts +++ b/packages/core/src/routes/sign-in-experience.guard.test.ts @@ -1,4 +1,5 @@ -import type { CreateSignInExperience, LanguageInfo, SignInExperience } from '@logto/schemas'; +import type { CreateSignInExperience, SignInExperience } from '@logto/schemas'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { mockAliyunDmConnector, @@ -10,11 +11,10 @@ import { mockSignInExperience, mockTermsOfUse, } from '#src/__mocks__/index.js'; -import { createRequester } from '#src/utils/test-utils.js'; -import signInExperiencesRoutes from './sign-in-experience.js'; +const { jest } = import.meta; -jest.mock('#src/connectors.js', () => ({ +mockEsm('#src/connectors.js', () => ({ getLogtoConnectors: jest.fn(async () => [ mockAliyunDmConnector, mockAliyunSmsConnector, @@ -24,23 +24,21 @@ jest.mock('#src/connectors.js', () => ({ ]), })); -// eslint-disable-next-line @typescript-eslint/no-empty-function -const validateLanguageInfo = jest.fn(async (languageInfo: LanguageInfo): Promise => {}); - -jest.mock('#src/lib/sign-in-experience.js', () => ({ - ...jest.requireActual('#src/lib/sign-in-experience.js'), - validateLanguageInfo: async (languageInfo: LanguageInfo) => validateLanguageInfo(languageInfo), +const { validateLanguageInfo } = await mockEsmWithActual('#src/lib/sign-in-experience.js', () => ({ + validateLanguageInfo: jest.fn(), })); -jest.mock('#src/queries/sign-in-experience.js', () => ({ - updateDefaultSignInExperience: jest.fn( - async (data: Partial): Promise => ({ - ...mockSignInExperience, - ...data, - }) - ), +await mockEsmWithActual('#src/queries/sign-in-experience.js', () => ({ + updateDefaultSignInExperience: async ( + data: Partial + ): Promise => ({ + ...mockSignInExperience, + ...data, + }), })); +const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js')); +const { createRequester } = await import('#src/utils/test-utils.js'); const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes }); const expectPatchResponseStatus = async ( diff --git a/packages/core/src/routes/sign-in-experience.test.ts b/packages/core/src/routes/sign-in-experience.test.ts index 1cf33923d..9565d71c8 100644 --- a/packages/core/src/routes/sign-in-experience.test.ts +++ b/packages/core/src/routes/sign-in-experience.test.ts @@ -1,4 +1,5 @@ import type { SignInExperience, CreateSignInExperience, TermsOfUse } from '@logto/schemas'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { mockFacebookConnector, @@ -13,12 +14,23 @@ import { mockLanguageInfo, mockAliyunSmsConnector, } from '#src/__mocks__/index.js'; -import * as signInExpLib from '#src/lib/sign-in-experience/index.js'; -import * as signInLib from '#src/lib/sign-in-experience/sign-in.js'; -import * as signUpLib from '#src/lib/sign-in-experience/sign-up.js'; import { createRequester } from '#src/utils/test-utils.js'; -import signInExperiencesRoutes from './sign-in-experience.js'; +const { jest } = import.meta; + +const { + validateBranding, + validateLanguageInfo, + validateTermsOfUse, + validateSignIn, + validateSignUp, +} = await mockEsmWithActual('#src/lib/sign-in-experience/index.js', () => ({ + validateBranding: jest.fn(), + validateLanguageInfo: jest.fn(), + validateTermsOfUse: jest.fn(), + validateSignIn: jest.fn(), + validateSignUp: jest.fn(), +})); const logtoConnectors = [ mockFacebookConnector, @@ -28,33 +40,27 @@ const logtoConnectors = [ mockAliyunSmsConnector, ]; -const getLogtoConnectors = jest.fn(async () => logtoConnectors); - -jest.mock('#src/connectors.js', () => { - return { - ...jest.requireActual('#src/connectors.js'), - getLogtoConnectors: jest.fn(async () => getLogtoConnectors()), - }; -}); - -const findDefaultSignInExperience = jest.fn(async () => mockSignInExperience); - -jest.mock('#src/queries/sign-in-experience.js', () => ({ - findDefaultSignInExperience: jest.fn(async () => findDefaultSignInExperience()), - updateDefaultSignInExperience: jest.fn( - async (data: Partial): Promise => ({ - ...mockSignInExperience, - ...data, - }) - ), +await mockEsmWithActual('#src/connectors.js', () => ({ + getLogtoConnectors: async () => logtoConnectors, })); -const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes }); +const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience.js', () => ({ + findDefaultSignInExperience: jest.fn(async () => mockSignInExperience), + updateDefaultSignInExperience: async ( + data: Partial + ): Promise => ({ + ...mockSignInExperience, + ...data, + }), +})); -jest.mock('#src/queries/custom-phrase.js', () => ({ +mockEsm('#src/queries/custom-phrase.js', () => ({ findAllCustomLanguageTags: async () => [], })); +const signInExperiencesRoutes = await pickDefault(import('./sign-in-experience.js')); +const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes }); + describe('GET /sign-in-exp', () => { afterAll(() => { jest.clearAllMocks(); @@ -103,12 +109,6 @@ describe('PATCH /sign-in-exp', () => { const termsOfUse: TermsOfUse = { enabled: false }; const socialSignInConnectorTargets = ['github', 'facebook', 'wechat']; - const validateBranding = jest.spyOn(signInExpLib, 'validateBranding'); - const validateLanguageInfo = jest.spyOn(signInExpLib, 'validateLanguageInfo'); - const validateTermsOfUse = jest.spyOn(signInExpLib, 'validateTermsOfUse'); - const validateSignIn = jest.spyOn(signInLib, 'validateSignIn'); - const validateSignUp = jest.spyOn(signUpLib, 'validateSignUp'); - const response = await signInExperienceRequester.patch('/sign-in-exp').send({ color: mockColor, branding: mockBranding, diff --git a/packages/core/src/routes/swagger.test.ts b/packages/core/src/routes/swagger.test.ts index f4353931b..4fb2103af 100644 --- a/packages/core/src/routes/swagger.test.ts +++ b/packages/core/src/routes/swagger.test.ts @@ -1,4 +1,4 @@ -import { load } from 'js-yaml'; +import { mockEsm } from '@logto/shared/esm'; import Koa from 'koa'; import Router from 'koa-router'; import request from 'supertest'; @@ -8,12 +8,18 @@ import koaGuard from '#src/middleware/koa-guard.js'; import koaPagination from '#src/middleware/koa-pagination.js'; import type { AnonymousRouter } from '#src/routes/types.js'; -import swaggerRoutes, { defaultResponses, paginationParameters } from './swagger.js'; +const { jest } = import.meta; -jest.mock('js-yaml', () => ({ +const { load } = mockEsm('js-yaml', () => ({ load: jest.fn().mockReturnValue({ paths: {} }), })); +const { + default: swaggerRoutes, + defaultResponses, + paginationParameters, +} = await import('./swagger.js'); + export const createSwaggerRequest = ( allRouters: Router[], swaggerRouter: AnonymousRouter = new Router() @@ -221,7 +227,7 @@ describe('GET /swagger.json', () => { describe('should use correct responses', () => { it('should use "defaultResponses" if there is no custom "responses" from the additional swagger', async () => { - (load as jest.Mock).mockReturnValueOnce({ + load.mockReturnValueOnce({ paths: { '/api/mock': { delete: {} } }, }); @@ -235,7 +241,7 @@ describe('GET /swagger.json', () => { }); it('should use custom "responses" from the additional swagger if it exists', async () => { - (load as jest.Mock).mockReturnValueOnce({ + load.mockReturnValueOnce({ paths: { '/api/mock': { get: { diff --git a/packages/core/src/routes/well-known.test.ts b/packages/core/src/routes/well-known.test.ts index 532625d16..33c66db9d 100644 --- a/packages/core/src/routes/well-known.test.ts +++ b/packages/core/src/routes/well-known.test.ts @@ -3,7 +3,7 @@ import { adminConsoleApplicationId, adminConsoleSignInExperience, } from '@logto/schemas/lib/seeds/index.js'; -import { Provider } from 'oidc-provider'; +import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { mockAliyunDmConnector, @@ -15,50 +15,48 @@ import { mockWechatConnector, mockWechatNativeConnector, } from '#src/__mocks__/index.js'; -import * as signInExperienceQueries from '#src/queries/sign-in-experience.js'; -import wellKnownRoutes from '#src/routes/well-known.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { createRequester } from '#src/utils/test-utils.js'; -const getLogtoConnectors = jest.fn(async () => [ - mockAliyunDmConnector, - mockAliyunSmsConnector, - mockFacebookConnector, - mockGithubConnector, - mockGoogleConnector, - mockWechatConnector, - mockWechatNativeConnector, -]); - -jest.mock('#src/connectors.js', () => ({ - getLogtoConnectors: async () => getLogtoConnectors(), +const { jest } = import.meta; +await mockEsmWithActual('i18next', () => ({ + default: { + t: (key: string) => key, + }, })); -jest.mock('#src/queries/user.js', () => ({ +const { findDefaultSignInExperience } = mockEsm('#src/queries/sign-in-experience.js', () => ({ + updateDefaultSignInExperience: jest.fn(), + findDefaultSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience), +})); + +await mockEsmWithActual('#src/queries/user.js', () => ({ hasActiveUsers: jest.fn().mockResolvedValue(true), })); -const interactionDetails: jest.MockedFunction<() => Promise> = jest.fn(async () => ({ - params: {}, +mockEsm('#src/connectors.js', () => ({ + getLogtoConnectors: jest.fn(async () => [ + mockAliyunDmConnector, + mockAliyunSmsConnector, + mockFacebookConnector, + mockGithubConnector, + mockGoogleConnector, + mockWechatConnector, + mockWechatNativeConnector, + ]), })); -jest.mock('oidc-provider', () => ({ - Provider: jest.fn(() => ({ - interactionDetails, - })), -})); - -jest.mock('i18next', () => ({ - t: (key: string) => key, -})); +const wellKnownRoutes = await pickDefault(import('#src/routes/well-known.js')); describe('GET /.well-known/sign-in-exp', () => { afterEach(() => { jest.clearAllMocks(); }); + const provider = createMockProvider(); const sessionRequest = createRequester({ anonymousRoutes: wellKnownRoutes, - provider: new Provider(''), + provider, middlewares: [ async (ctx, next) => { ctx.addLogContext = jest.fn(); @@ -69,13 +67,9 @@ describe('GET /.well-known/sign-in-exp', () => { ], }); - const signInExperienceQuerySpyOn = jest - .spyOn(signInExperienceQueries, 'findDefaultSignInExperience') - .mockResolvedValue(mockSignInExperience); - it('should return github and facebook connector instances', async () => { const response = await sessionRequest.get('/.well-known/sign-in-exp'); - expect(signInExperienceQuerySpyOn).toHaveBeenCalledTimes(1); + expect(findDefaultSignInExperience).toHaveBeenCalledTimes(1); expect(response.status).toEqual(200); expect(response.body).toMatchObject({ ...mockSignInExperience, @@ -101,7 +95,10 @@ describe('GET /.well-known/sign-in-exp', () => { }); it('should return admin console settings', async () => { - interactionDetails.mockResolvedValue({ params: { client_id: adminConsoleApplicationId } }); + jest + .spyOn(provider, 'interactionDetails') + // @ts-expect-error for testing + .mockResolvedValue({ params: { client_id: adminConsoleApplicationId } }); const response = await sessionRequest.get('/.well-known/sign-in-exp'); expect(response.status).toEqual(200); diff --git a/packages/core/src/test-utils/jest-koa-mocks/LICENSE b/packages/core/src/test-utils/jest-koa-mocks/LICENSE new file mode 100644 index 000000000..9685c7522 --- /dev/null +++ b/packages/core/src/test-utils/jest-koa-mocks/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2018-present Shopify + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/core/src/test-utils/jest-koa-mocks/README.md b/packages/core/src/test-utils/jest-koa-mocks/README.md new file mode 100644 index 000000000..a46306564 --- /dev/null +++ b/packages/core/src/test-utils/jest-koa-mocks/README.md @@ -0,0 +1,3 @@ +# jest-koa-mocks + +Updated from https://github.com/Shopify/quilt/tree/main/packages/jest-koa-mocks since ESM tests need explicit `const { jest } = import.meta;` to fetch the global jest object. diff --git a/packages/core/src/test-utils/jest-koa-mocks/create-mock-context.ts b/packages/core/src/test-utils/jest-koa-mocks/create-mock-context.ts new file mode 100644 index 000000000..cd8b0a93d --- /dev/null +++ b/packages/core/src/test-utils/jest-koa-mocks/create-mock-context.ts @@ -0,0 +1,119 @@ +/* eslint-disable unicorn/no-abusive-eslint-disable */ +/* eslint-disable */ +import stream from 'stream'; +import { URL } from 'url'; + +import type { Context } from 'koa'; +import Koa from 'koa'; +import type { RequestMethod } from 'node-mocks-http'; +import httpMocks from 'node-mocks-http'; + +import type { MockCookies } from './create-mock-cookies.js'; +import createMockCookies from './create-mock-cookies.js'; + +const { jest } = import.meta; + +export type Dictionary = Record; + +export type MockContext = { + cookies: MockCookies; + request: Context['request'] & { + body?: any; + rawBody?: string; + session?: any; + }; +} & Context; + +export type Options, RequestBody = undefined> = { + url?: string; + method?: RequestMethod; + statusCode?: number; + session?: Dictionary; + headers?: Dictionary; + cookies?: Dictionary; + state?: Dictionary; + encrypted?: boolean; + host?: string; + requestBody?: RequestBody; + rawBody?: string; + throw?: Function; + redirect?: Function; + customProperties?: CustomProperties; +}; + +export default function createContext< + CustomProperties extends Record, + RequestBody = undefined +>(options: Options = {}) { + const app = new Koa(); + + const { + cookies, + method, + statusCode, + session, + requestBody, + rawBody = '', + url = '', + host = 'test.com', + encrypted = false, + throw: throwFunction = jest.fn(), + redirect = jest.fn(), + headers = {}, + state = {}, + customProperties = {}, + } = options; + + const extensions = { + ...customProperties, + throw: throwFunction, + session, + redirect, + state, + }; + + const protocolFallback = encrypted ? 'https' : 'http'; + const urlObject = new URL(url, `${protocolFallback}://${host}`); + + const request = httpMocks.createRequest({ + url: urlObject.toString(), + method, + statusCode, + session, + headers: { + // Koa determines protocol based on the `Host` header. + Host: urlObject.host, + ...headers, + }, + }); + + // Some functions we call in the implementations will perform checks for `req.encrypted`, which delegates to the socket. + // MockRequest doesn't set a fake socket itself, so we create one here. + request.socket = new stream.Duplex() as any; + Object.defineProperty(request.socket, 'encrypted', { + writable: false, + value: urlObject.protocol === 'https:', + }); + + const res = httpMocks.createResponse(); + + // Koa sets a default status code of 404, not the node default of 200 + // https://github.com/koajs/koa/blob/master/docs/api/response.md#responsestatus + res.statusCode = 404; + + // This is to get around an odd behavior in the `cookies` library, where if `res.set` is defined, it will use an internal + // node function to set headers, which results in them being set in the wrong place. + + res.set = undefined as any; + + const context = app.createContext(request, res) as MockContext & CustomProperties; + Object.assign(context, extensions); + context.cookies = createMockCookies(cookies); + + // Ctx.request.body is a common enough custom property for middleware to add that it's handy to just support it by default + context.request.body = requestBody; + context.request.rawBody = rawBody; + + return context as Context; +} +/* eslint-enable */ diff --git a/packages/core/src/test-utils/jest-koa-mocks/create-mock-cookies.ts b/packages/core/src/test-utils/jest-koa-mocks/create-mock-cookies.ts new file mode 100644 index 000000000..d4c9b185c --- /dev/null +++ b/packages/core/src/test-utils/jest-koa-mocks/create-mock-cookies.ts @@ -0,0 +1,37 @@ +/* eslint-disable unicorn/no-abusive-eslint-disable */ +/* eslint-disable */ +import type { Context } from 'koa'; + +const { jest } = import.meta; + +export type Cookies = Context['cookies']; + +export type Dictionary = Record; + +export type MockCookies = { + requestStore: Map; + responseStore: Map; +} & Cookies; + +export default function createMockCookies( + cookies: Record = {}, + secure = true +): MockCookies { + const cookieEntries = Object.keys(cookies).map((key) => [key, cookies[key]] as [string, string]); + + const requestStore = new Map(cookieEntries); + const responseStore = new Map(cookieEntries); + + return { + set: jest.fn((key, value) => { + return responseStore.set(key, value); + }), + get: jest.fn((key) => { + return requestStore.get(key); + }), + requestStore, + responseStore, + secure, + } as any; +} +/* eslint-enable */ diff --git a/packages/core/src/test-utils/oidc-provider.ts b/packages/core/src/test-utils/oidc-provider.ts new file mode 100644 index 000000000..4893d602b --- /dev/null +++ b/packages/core/src/test-utils/oidc-provider.ts @@ -0,0 +1,24 @@ +import { Provider } from 'oidc-provider'; + +const { jest } = import.meta; + +export const createMockProvider = (interactionDetails?: jest.Mock): Provider => { + const originalWarn = console.warn; + const warn = jest.spyOn(console, 'warn').mockImplementation((...args) => { + // Disable while creating. Too many warnings. + if (typeof args[0] === 'string' && args[0].includes('oidc-provider')) { + return; + } + + originalWarn(...args); + }); + const provider = new Provider('https://logto.test'); + + warn.mockRestore(); + jest.spyOn(provider, 'interactionDetails').mockImplementation( + // @ts-expect-error for testing + interactionDetails ?? (async () => ({ params: {}, jti: 'jti', client_id: 'mockApplicationId' })) + ); + + return provider; +}; diff --git a/packages/core/src/utils/oidc-provider-event-listener.test.ts b/packages/core/src/utils/oidc-provider-event-listener.test.ts index 3beefc51f..bea89da3c 100644 --- a/packages/core/src/utils/oidc-provider-event-listener.test.ts +++ b/packages/core/src/utils/oidc-provider-event-listener.test.ts @@ -9,36 +9,26 @@ import { } from '#src/utils/oidc-provider-event-listener.js'; import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; +const { jest } = import.meta; + const userId = 'userIdValue'; const sessionId = 'sessionIdValue'; const applicationId = 'applicationIdValue'; const addLogContext = jest.fn(); const log = jest.fn(); -const addListener = jest.fn(); - -jest.mock('oidc-provider', () => ({ Provider: jest.fn(() => ({ addListener })) })); describe('addOidcEventListeners', () => { afterEach(() => { jest.clearAllMocks(); }); - it('should add grantSuccessListener', () => { - const provider = new Provider(''); + it('should add proper listeners', () => { + const provider = new Provider('https://logto.test'); + const addListener = jest.spyOn(provider, 'addListener'); addOidcEventListeners(provider); expect(addListener).toHaveBeenCalledWith('grant.success', grantSuccessListener); - }); - - it('should add grantErrorListener', () => { - const provider = new Provider(''); - addOidcEventListeners(provider); expect(addListener).toHaveBeenCalledWith('grant.error', grantErrorListener); - }); - - it('should add grantRevokedListener', () => { - const provider = new Provider(''); - addOidcEventListeners(provider); expect(addListener).toHaveBeenCalledWith('grant.revoked', grantRevokedListener); }); }); diff --git a/packages/core/src/utils/test-utils.ts b/packages/core/src/utils/test-utils.ts index d85f0c956..cf0a1dd49 100644 --- a/packages/core/src/utils/test-utils.ts +++ b/packages/core/src/utils/test-utils.ts @@ -1,5 +1,3 @@ -import type { Options } from '@shopify/jest-koa-mocks'; -import { createMockContext } from '@shopify/jest-koa-mocks'; import type { MiddlewareType, Context, Middleware } from 'koa'; import Koa from 'koa'; import type { IRouterParamContext } from 'koa-router'; @@ -11,6 +9,8 @@ import type { PrimitiveValueExpression } from 'slonik/dist/src/types.js'; import request from 'supertest'; import type { AuthedRouter, AnonymousRouter } from '#src/routes/types.js'; +import type { Options } from '#src/test-utils/jest-koa-mocks/create-mock-context.js'; +import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js'; /** * Slonik Query Mock Utils diff --git a/packages/shared/package.json b/packages/shared/package.json index 4dc3644d5..5548c608c 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -8,6 +8,14 @@ "files": [ "lib" ], + "exports": { + ".": { + "import": "./lib/index.js" + }, + "./esm": { + "import": "./lib/esm/index.js" + } + }, "publishConfig": { "access": "public" }, diff --git a/packages/shared/src/esm/index.ts b/packages/shared/src/esm/index.ts new file mode 100644 index 000000000..c51c2b276 --- /dev/null +++ b/packages/shared/src/esm/index.ts @@ -0,0 +1,2 @@ +export { default as moduleProxy } from './module-proxy.js'; +export * from './mock-esm.js'; diff --git a/packages/shared/src/esm/mock-esm.ts b/packages/shared/src/esm/mock-esm.ts new file mode 100644 index 000000000..05e251405 --- /dev/null +++ b/packages/shared/src/esm/mock-esm.ts @@ -0,0 +1,72 @@ +import path from 'path'; + +const { jest } = import.meta; + +type MockParameters = Parameters<(moduleName: string, factory: () => T) => void>; + +// See https://github.com/sindresorhus/callsites +/* eslint-disable @silverhand/fp/no-mutation */ +const callSites = (): NodeJS.CallSite[] => { + const _prepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = (_, stack) => stack; + const stack = new Error().stack?.slice(1); // eslint-disable-line unicorn/error-message + Error.prepareStackTrace = _prepareStackTrace; + + // @ts-expect-error ignore the error since it has been replaced with the original stack array + return stack ?? []; +}; +/* eslint-enable @silverhand/fp/no-mutation */ + +// Depth default is 2 since it'll be called by `mockEsmXyz()` in this module. +// Need to trace one level deeper for the original caller. +const resolvePath = (pathOrModule: string, depth = 2): string => { + if (pathOrModule === '@logto/shared') { + return new URL('../../', import.meta.url).pathname; + } + + if (!pathOrModule.startsWith('.')) { + return pathOrModule; + } + + return path.join(path.dirname(callSites()[depth]?.getFileName() ?? ''), pathOrModule); +}; + +export const mockEsmWithActual: (...args: MockParameters) => Promise = async ( + moduleName, + factory +) => { + const resolvedModule = resolvePath(moduleName); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = await import(resolvedModule); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + jest.unstable_mockModule(resolvedModule, () => ({ + ...actual, + ...factory(), + })); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return import(resolvedModule); +}; + +export const mockEsm = (...args: MockParameters) => { + const mocked = args[1](); + jest.unstable_mockModule(resolvePath(args[0]), () => mocked); + + return mocked; +}; + +export const mockEsmDefault = (...args: MockParameters) => { + const mocked = args[1](); + + jest.unstable_mockModule(resolvePath(args[0]), () => ({ default: mocked })); + + return mocked; +}; + +export const pickDefault = async >( + promise: Promise +): Promise => { + const awaited = await promise; + + return awaited.default; +}; diff --git a/packages/shared/src/utils/module-proxy.ts b/packages/shared/src/esm/module-proxy.ts similarity index 91% rename from packages/shared/src/utils/module-proxy.ts rename to packages/shared/src/esm/module-proxy.ts index e97f4bc9f..c378377d9 100644 --- a/packages/shared/src/utils/module-proxy.ts +++ b/packages/shared/src/esm/module-proxy.ts @@ -1,3 +1,4 @@ +const { jest } = import.meta; // For testing // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment const proxy: ProxyConstructor = new Proxy( diff --git a/packages/shared/src/include.d/import-meta.d.ts b/packages/shared/src/include.d/import-meta.d.ts new file mode 100644 index 000000000..e016debb5 --- /dev/null +++ b/packages/shared/src/include.d/import-meta.d.ts @@ -0,0 +1,10 @@ +interface ImportMeta { + jest: typeof jest & { + // Almost same as `jest.mock()`, but factory is required + unstable_mockModule: ( + moduleName: string, + factory: () => T, + options?: jest.MockOptions + ) => typeof jest; + }; +} diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 7941ccd79..8bfcfa7f3 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -1,4 +1,3 @@ export * from './id.js'; export * from './function.js'; -export { default as moduleProxy } from './module-proxy.js'; export { default as findPackage } from './find-package.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1bf0e177..09663cb63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,13 +27,13 @@ importers: '@logto/shared': workspace:* '@silverhand/eslint-config': 1.3.0 '@silverhand/essentials': ^1.3.0 - '@silverhand/jest-config': 1.2.2 '@silverhand/ts-config': 1.2.1 '@types/fs-extra': ^9.0.13 '@types/inquirer': ^8.2.1 '@types/jest': ^29.1.2 '@types/node': ^16.0.0 '@types/semver': ^7.3.12 + '@types/sinon': ^10.0.13 '@types/tar': ^6.1.2 '@types/yargs': ^17.0.13 chalk: ^5.0.0 @@ -47,17 +47,17 @@ importers: jest: ^29.3.1 lint-staged: ^13.0.0 nanoid: ^3.3.4 - ora: ^5.0.0 + ora: ^6.1.2 p-retry: ^4.6.1 prettier: ^2.7.1 rimraf: ^3.0.2 roarr: ^7.11.0 semver: ^7.3.8 + sinon: ^15.0.0 slonik: ^30.0.0 slonik-interceptor-preset: ^1.2.10 slonik-sql-tag-raw: ^1.1.4 tar: ^6.1.11 - ts-node: ^10.9.1 typescript: ^4.7.4 yargs: ^17.6.0 zod: ^3.19.1 @@ -73,7 +73,7 @@ importers: hpagent: 1.2.0 inquirer: 8.2.2 nanoid: 3.3.4 - ora: 5.4.1 + ora: 6.1.2 p-retry: 4.6.1 roarr: 7.11.0 semver: 7.3.8 @@ -85,21 +85,21 @@ importers: zod: 3.19.1 devDependencies: '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni - '@silverhand/jest-config': 1.2.2_wkdujqsgbnfnnp5xidismkcn6e '@silverhand/ts-config': 1.2.1_typescript@4.7.4 '@types/fs-extra': 9.0.13 '@types/inquirer': 8.2.1 '@types/jest': 29.1.2 '@types/node': 16.11.12 '@types/semver': 7.3.12 + '@types/sinon': 10.0.13 '@types/tar': 6.1.2 '@types/yargs': 17.0.13 eslint: 8.21.0 - jest: 29.3.1_k5ytkvaprncdyzidqqws5bqksq + jest: 29.3.1_@types+node@16.11.12 lint-staged: 13.0.0 prettier: 2.7.1 rimraf: 3.0.2 - ts-node: 10.9.1_ccwudyfw5se7hgalwgkzhn2yp4 + sinon: 15.0.0 typescript: 4.7.4 packages/console: @@ -148,7 +148,7 @@ importers: lint-staged: ^13.0.0 lodash.get: ^4.4.2 lodash.kebabcase: ^4.1.1 - nanoid: ^3.1.23 + nanoid: ^3.3.4 parcel: 2.8.0 postcss: ^8.4.6 postcss-modules: ^4.3.0 @@ -220,7 +220,7 @@ importers: lint-staged: 13.0.0 lodash.get: 4.4.2 lodash.kebabcase: 4.1.1 - nanoid: 3.3.1 + nanoid: 3.3.4 parcel: 2.8.0_postcss@8.4.6 postcss: 8.4.6 postcss-modules: 4.3.0_postcss@8.4.6 @@ -258,10 +258,8 @@ importers: '@logto/phrases-ui': workspace:* '@logto/schemas': workspace:* '@logto/shared': workspace:* - '@shopify/jest-koa-mocks': ^5.0.1 '@silverhand/eslint-config': 1.3.0 '@silverhand/essentials': ^1.3.0 - '@silverhand/jest-config': 1.2.2 '@silverhand/ts-config': 1.2.1 '@types/debug': ^4.1.7 '@types/etag': ^1.8.1 @@ -277,6 +275,7 @@ importers: '@types/lodash.pick': ^4.4.6 '@types/node': ^16.0.0 '@types/oidc-provider': ^7.12.0 + '@types/sinon': ^10.0.13 '@types/supertest': ^2.0.11 chalk: ^5.0.0 clean-deep: ^3.4.0 @@ -308,8 +307,8 @@ importers: koa-send: ^5.0.1 lint-staged: ^13.0.0 lodash.pick: ^4.4.0 - nanoid: ^3.1.23 - nock: ^13.2.2 + nanoid: ^3.3.4 + node-mocks-http: ^1.12.1 nodemon: ^2.0.19 oidc-provider: ^7.13.0 openapi-types: ^12.0.0 @@ -317,6 +316,7 @@ importers: prettier: ^2.7.1 query-string: ^7.0.1 roarr: ^7.11.0 + sinon: ^15.0.0 slonik: ^30.0.0 slonik-interceptor-preset: ^1.2.10 slonik-sql-tag-raw: ^1.1.4 @@ -359,7 +359,7 @@ importers: koa-router: 12.0.0 koa-send: 5.0.1 lodash.pick: 4.4.0 - nanoid: 3.1.30 + nanoid: 3.3.4 oidc-provider: 7.13.0 p-retry: 4.6.1 query-string: 7.0.1 @@ -371,9 +371,7 @@ importers: snakecase-keys: 5.4.4 zod: 3.19.1 devDependencies: - '@shopify/jest-koa-mocks': 5.0.1 '@silverhand/eslint-config': 1.3.0_xygfz6avl43ipur7dlp2av7gnm - '@silverhand/jest-config': 1.2.2_gxkpbehbojmgu22invxph4jlwq '@silverhand/ts-config': 1.2.1_typescript@4.9.3 '@types/debug': 4.1.7 '@types/etag': 1.8.1 @@ -389,6 +387,7 @@ importers: '@types/lodash.pick': 4.4.6 '@types/node': 16.11.12 '@types/oidc-provider': 7.12.0 + '@types/sinon': 10.0.13 '@types/supertest': 2.0.11 copyfiles: 2.4.1 eslint: 8.21.0 @@ -396,10 +395,11 @@ importers: jest: 29.1.2_@types+node@16.11.12 jest-matcher-specific-error: 1.0.0 lint-staged: 13.0.0 - nock: 13.2.2 + node-mocks-http: 1.12.1 nodemon: 2.0.19 openapi-types: 12.0.0 prettier: 2.7.1 + sinon: 15.0.0 supertest: 6.2.2 typescript: 4.9.3 @@ -1905,7 +1905,7 @@ packages: - ts-node dev: true - /@jest/core/29.3.1_ts-node@10.9.1: + /@jest/core/29.3.1: resolution: {integrity: sha512-0ohVjjRex985w5MmO5L3u5GR1O30DexhBSpuwx2P+9ftyqHdJXnk7IUWiP80oHMvt7ubHCJHxV0a0vlKVuZirw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -1926,7 +1926,7 @@ packages: exit: 0.1.2 graceful-fs: 4.2.10 jest-changed-files: 29.2.0 - jest-config: 29.3.1_hvivgrlmkyd4vgu6rkkmg6acly + jest-config: 29.3.1_@types+node@17.0.23 jest-haste-map: 29.3.1 jest-message-util: 29.3.1 jest-regex-util: 29.2.0 @@ -2466,7 +2466,7 @@ packages: dependencies: '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 color: 4.2.3 - nanoid: 3.3.1 + nanoid: 3.3.4 zod: 3.19.1 dev: true @@ -2478,7 +2478,7 @@ packages: dependencies: '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 color: 4.2.3 - nanoid: 3.3.1 + nanoid: 3.3.4 zod: 3.19.1 /@logto/js/1.0.0-beta.13: @@ -3469,16 +3469,6 @@ packages: resolution: {integrity: sha512-Yykovind6xzqAqd0t5umrdAGPlGLTE80cy80UkEnbt8Zv5zEYTFzJSNPQ81TY8BSpRreubu1oE54iHBv2UVnTQ==} dev: true - /@shopify/jest-koa-mocks/5.0.1: - resolution: {integrity: sha512-4YskS9q8+TEHNoyopmuoy2XyhInyqeOl7CF5ShJs19sm6m0EA/jGGvgf/osv2PeTfuf42/L2G9CzWUSg49yTSg==} - engines: {node: ^14.17.0 || >=16.0.0} - dependencies: - koa: 2.13.4 - node-mocks-http: 1.11.0 - transitivePeerDependencies: - - supports-color - dev: true - /@sideway/address/4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} dependencies: @@ -3621,46 +3611,6 @@ packages: lodash.orderby: 4.6.0 lodash.pick: 4.4.0 - /@silverhand/jest-config/1.2.2_gxkpbehbojmgu22invxph4jlwq: - resolution: {integrity: sha512-sCOIHN3kIG9nyySkDao8nz6HK8VhGoUV4WG1CCriDDeGTqbHs4IprzTp1p+ChFdC8JGBCElQC0cIFrWYTFnTAQ==} - engines: {node: ^16.0.0 || ^18.0.0} - peerDependencies: - jest: ^29.0.0 || ^29.1.2 - dependencies: - '@jest/types': 29.1.2 - deepmerge: 4.2.2 - identity-obj-proxy: 3.0.0 - jest: 29.1.2_@types+node@16.11.12 - jest-matcher-specific-error: 1.0.0 - jest-transform-stub: 2.0.0 - ts-jest: 29.0.3_lr7fqxhx6o7ex6ma5v5npbw6ae - transitivePeerDependencies: - - '@babel/core' - - babel-jest - - esbuild - - typescript - dev: true - - /@silverhand/jest-config/1.2.2_wkdujqsgbnfnnp5xidismkcn6e: - resolution: {integrity: sha512-sCOIHN3kIG9nyySkDao8nz6HK8VhGoUV4WG1CCriDDeGTqbHs4IprzTp1p+ChFdC8JGBCElQC0cIFrWYTFnTAQ==} - engines: {node: ^16.0.0 || ^18.0.0} - peerDependencies: - jest: ^29.0.0 || ^29.1.2 - dependencies: - '@jest/types': 29.1.2 - deepmerge: 4.2.2 - identity-obj-proxy: 3.0.0 - jest: 29.3.1_k5ytkvaprncdyzidqqws5bqksq - jest-matcher-specific-error: 1.0.0 - jest-transform-stub: 2.0.0 - ts-jest: 29.0.3_o3wtcjdhyxuv43bggxcaucanwu - transitivePeerDependencies: - - '@babel/core' - - babel-jest - - esbuild - - typescript - dev: true - /@silverhand/jest-config/1.2.2_zapogttls25djihwjkusccjjym: resolution: {integrity: sha512-sCOIHN3kIG9nyySkDao8nz6HK8VhGoUV4WG1CCriDDeGTqbHs4IprzTp1p+ChFdC8JGBCElQC0cIFrWYTFnTAQ==} engines: {node: ^16.0.0 || ^18.0.0} @@ -3728,6 +3678,18 @@ packages: type-detect: 4.0.8 dev: true + /@sinonjs/commons/2.0.0: + resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers/7.1.2: + resolution: {integrity: sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==} + dependencies: + '@sinonjs/commons': 1.8.3 + dev: true + /@sinonjs/fake-timers/8.1.0: resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==} dependencies: @@ -3740,6 +3702,18 @@ packages: '@sinonjs/commons': 1.8.3 dev: true + /@sinonjs/samsam/7.0.1: + resolution: {integrity: sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==} + dependencies: + '@sinonjs/commons': 2.0.0 + lodash.get: 4.4.2 + type-detect: 4.0.8 + dev: true + + /@sinonjs/text-encoding/0.7.2: + resolution: {integrity: sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==} + dev: true + /@svgr/babel-plugin-add-jsx-attribute/6.5.1_@babel+core@7.20.2: resolution: {integrity: sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==} engines: {node: '>=10'} @@ -4416,6 +4390,16 @@ packages: '@types/node': 17.0.23 dev: true + /@types/sinon/10.0.13: + resolution: {integrity: sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==} + dependencies: + '@types/sinonjs__fake-timers': 8.1.2 + dev: true + + /@types/sinonjs__fake-timers/8.1.2: + resolution: {integrity: sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==} + dev: true + /@types/stack-utils/2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true @@ -4741,6 +4725,7 @@ packages: dependencies: mime-types: 2.1.35 negotiator: 0.6.2 + dev: false /accepts/1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} @@ -4848,7 +4833,6 @@ packages: /ansi-regex/6.0.1: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} - dev: true /ansi-styles/3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} @@ -5196,6 +5180,14 @@ packages: inherits: 2.0.4 readable-stream: 3.6.0 + /bl/5.1.0: + resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} + dependencies: + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: false + /bluebird/3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} @@ -5266,6 +5258,13 @@ packages: base64-js: 1.5.1 ieee754: 1.2.1 + /buffer/6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + /bufferput/0.1.3: resolution: {integrity: sha512-nmPV88vDNzf0VMU1bdQ4A1oBlRR9y+CXfwWKfyKUgI2ZIkvreNzLMM3tkz0Lapb6f+Cz1V001UWRBsoGVCjqdw==} engines: {node: '>=0.3.0'} @@ -5291,6 +5290,7 @@ packages: dependencies: mime-types: 2.1.35 ylru: 1.2.1 + dev: false /cacheable-lookup/5.0.4: resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} @@ -5512,6 +5512,13 @@ packages: dependencies: restore-cursor: 3.1.0 + /cli-cursor/4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + restore-cursor: 4.0.0 + dev: false + /cli-spinners/2.6.1: resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} engines: {node: '>=6'} @@ -5723,6 +5730,7 @@ packages: /content-type/1.0.4: resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} engines: {node: '>= 0.6'} + dev: false /conventional-changelog-angular/5.0.13: resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} @@ -5772,6 +5780,7 @@ packages: dependencies: depd: 2.0.0 keygrip: 1.1.0 + dev: false /copyfiles/2.4.1: resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} @@ -6107,18 +6116,6 @@ packages: supports-color: 5.5.0 dev: true - /debug/4.3.3: - resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: true - /debug/4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -6179,6 +6176,7 @@ packages: /deep-equal/1.0.1: resolution: {integrity: sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=} + dev: false /deep-is/0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -6232,6 +6230,7 @@ packages: /delegates/1.0.0: resolution: {integrity: sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=} + dev: false /depd/1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} @@ -6240,6 +6239,7 @@ packages: /depd/2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dev: false /dequal/2.0.2: resolution: {integrity: sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==} @@ -6248,6 +6248,7 @@ packages: /destroy/1.0.4: resolution: {integrity: sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=} + dev: false /detab/2.0.4: resolution: {integrity: sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g==} @@ -6410,6 +6411,7 @@ packages: /ee-first/1.1.1: resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} + dev: false /ejs/3.1.8: resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} @@ -6447,6 +6449,7 @@ packages: /encodeurl/1.0.2: resolution: {integrity: sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=} engines: {node: '>= 0.8'} + dev: false /end-of-stream/1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -6541,6 +6544,7 @@ packages: /escape-html/1.0.3: resolution: {integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=} + dev: false /escape-string-regexp/1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} @@ -8048,6 +8052,7 @@ packages: dependencies: deep-equal: 1.0.1 http-errors: 1.8.1 + dev: false /http-cache-semantics/4.1.0: resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} @@ -8460,6 +8465,7 @@ packages: engines: {node: '>= 0.4'} dependencies: has-tostringtag: 1.0.0 + dev: false /is-get-set-prop/1.0.0: resolution: {integrity: sha512-DvAYZ1ZgGUz4lzxKMPYlt08qAUqyG9ckSg2pIjfvcQ7+pkVNUHk8yVLXOnCLe5WKXhLop8oorWFBJHpwWQpszQ==} @@ -8484,6 +8490,11 @@ packages: engines: {node: '>=8'} dev: false + /is-interactive/2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + dev: false + /is-js-type/2.0.0: resolution: {integrity: sha512-Aj13l47+uyTjlQNHtXBV8Cji3jb037vxwMWCgopRR8h6xocgBGW3qG8qGlIOEmbXQtkKShKuBM9e8AA1OeQ+xw==} dependencies: @@ -8622,6 +8633,11 @@ packages: engines: {node: '>=10'} dev: false + /is-unicode-supported/1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: false + /is-weakref/1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: @@ -8654,7 +8670,7 @@ packages: dev: true /isarray/0.0.1: - resolution: {integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=} + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} /isarray/1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -8883,7 +8899,7 @@ packages: - ts-node dev: true - /jest-cli/29.3.1_k5ytkvaprncdyzidqqws5bqksq: + /jest-cli/29.3.1_@types+node@16.11.12: resolution: {integrity: sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -8893,14 +8909,14 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.3.1_ts-node@10.9.1 + '@jest/core': 29.3.1 '@jest/test-result': 29.3.1 '@jest/types': 29.3.1 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 import-local: 3.1.0 - jest-config: 29.3.1_k5ytkvaprncdyzidqqws5bqksq + jest-config: 29.3.1_@types+node@16.11.12 jest-util: 29.3.1 jest-validate: 29.3.1 prompts: 2.4.2 @@ -9107,47 +9123,7 @@ packages: - supports-color dev: true - /jest-config/29.3.1_hvivgrlmkyd4vgu6rkkmg6acly: - resolution: {integrity: sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.19.3 - '@jest/test-sequencer': 29.3.1 - '@jest/types': 29.3.1 - '@types/node': 17.0.23 - babel-jest: 29.3.1_@babel+core@7.19.3 - chalk: 4.1.2 - ci-info: 3.5.0 - deepmerge: 4.2.2 - glob: 7.2.3 - graceful-fs: 4.2.10 - jest-circus: 29.3.1 - jest-environment-node: 29.3.1 - jest-get-type: 29.2.0 - jest-regex-util: 29.2.0 - jest-resolve: 29.3.1 - jest-runner: 29.3.1 - jest-util: 29.3.1 - jest-validate: 29.3.1 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.3.1 - slash: 3.0.0 - strip-json-comments: 3.1.1 - ts-node: 10.9.1_ccwudyfw5se7hgalwgkzhn2yp4 - transitivePeerDependencies: - - supports-color - dev: true - - /jest-config/29.3.1_k5ytkvaprncdyzidqqws5bqksq: + /jest-config/29.3.1_@types+node@16.11.12: resolution: {integrity: sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -9182,7 +9158,45 @@ packages: pretty-format: 29.3.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1_ccwudyfw5se7hgalwgkzhn2yp4 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-config/29.3.1_@types+node@17.0.23: + resolution: {integrity: sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.19.3 + '@jest/test-sequencer': 29.3.1 + '@jest/types': 29.3.1 + '@types/node': 17.0.23 + babel-jest: 29.3.1_@babel+core@7.19.3 + chalk: 4.1.2 + ci-info: 3.5.0 + deepmerge: 4.2.2 + glob: 7.2.3 + graceful-fs: 4.2.10 + jest-circus: 29.3.1 + jest-environment-node: 29.3.1 + jest-get-type: 29.2.0 + jest-regex-util: 29.2.0 + jest-resolve: 29.3.1 + jest-runner: 29.3.1 + jest-util: 29.3.1 + jest-validate: 29.3.1 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.3.1 + slash: 3.0.0 + strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color dev: true @@ -9965,7 +9979,7 @@ packages: - ts-node dev: true - /jest/29.3.1_k5ytkvaprncdyzidqqws5bqksq: + /jest/29.3.1_@types+node@16.11.12: resolution: {integrity: sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -9975,10 +9989,10 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.3.1_ts-node@10.9.1 + '@jest/core': 29.3.1 '@jest/types': 29.3.1 import-local: 3.1.0 - jest-cli: 29.3.1_k5ytkvaprncdyzidqqws5bqksq + jest-cli: 29.3.1_@types+node@16.11.12 transitivePeerDependencies: - '@types/node' - supports-color @@ -10101,6 +10115,7 @@ packages: /json-stringify-safe/5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: false /json5/1.0.1: resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==} @@ -10141,11 +10156,16 @@ packages: object.assign: 4.1.4 dev: true + /just-extend/4.2.1: + resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==} + dev: true + /keygrip/1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} dependencies: tsscmp: 1.0.6 + dev: false /keyv/4.0.4: resolution: {integrity: sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg==} @@ -10201,6 +10221,7 @@ packages: /koa-compose/4.1.0: resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + dev: false /koa-convert/2.0.0: resolution: {integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==} @@ -10208,6 +10229,7 @@ packages: dependencies: co: 4.6.0 koa-compose: 4.1.0 + dev: false /koa-logger/3.2.1: resolution: {integrity: sha512-MjlznhLLKy9+kG8nAXKJLM0/ClsQp/Or2vI3a5rbSQmgl8IJBQO0KI5FA70BvW+hqjtxjp49SpH2E7okS6NmHg==} @@ -10291,6 +10313,7 @@ packages: vary: 1.1.2 transitivePeerDependencies: - supports-color + dev: false /ky/0.32.2: resolution: {integrity: sha512-eBJeF6IXNwX5rksdwBrE2rIJrU2d84GoTvdM7OmmTIwUVXEMd72wIwvT+nyhrqtv7AzbSNsWz7yRsHgVhj1uog==} @@ -10562,10 +10585,6 @@ packages: /lodash.pick/4.4.0: resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} - /lodash.set/4.3.2: - resolution: {integrity: sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=} - dev: true - /lodash.sortby/4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} @@ -10599,6 +10618,14 @@ packages: is-unicode-supported: 0.1.0 dev: false + /log-symbols/5.1.0: + resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} + engines: {node: '>=12'} + dependencies: + chalk: 5.1.2 + is-unicode-supported: 1.3.0 + dev: false + /log-update/4.0.0: resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} engines: {node: '>=10'} @@ -11339,17 +11366,6 @@ packages: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} dev: false - /nanoid/3.1.30: - resolution: {integrity: sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: false - - /nanoid/3.3.1: - resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - /nanoid/3.3.4: resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -11362,30 +11378,29 @@ packages: /negotiator/0.6.2: resolution: {integrity: sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==} engines: {node: '>= 0.6'} + dev: false /negotiator/0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} dev: true + /nise/5.1.3: + resolution: {integrity: sha512-U597iWTTBBYIV72986jyU382/MMZ70ApWcRmkoF1AZ75bpqOtI3Gugv/6+0jLgoDOabmcSwYBkSSAWIp1eA5cg==} + dependencies: + '@sinonjs/commons': 2.0.0 + '@sinonjs/fake-timers': 7.1.2 + '@sinonjs/text-encoding': 0.7.2 + just-extend: 4.2.1 + path-to-regexp: 1.8.0 + dev: true + /no-case/3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: lower-case: 2.0.2 tslib: 2.4.0 - /nock/13.2.2: - resolution: {integrity: sha512-PcBHuvl9i6zfaJ50A7LS55oU+nFLv8htXIhffJO+FxyfibdZ4jEvd9kTuvkrJireBFIGMZ+oUIRpMK5gU9h//g==} - engines: {node: '>= 10.13'} - dependencies: - debug: 4.3.3 - json-stringify-safe: 5.0.1 - lodash.set: 4.3.2 - propagate: 2.0.1 - transitivePeerDependencies: - - supports-color - dev: true - /node-addon-api/3.2.1: resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} dev: true @@ -11420,8 +11435,8 @@ packages: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true - /node-mocks-http/1.11.0: - resolution: {integrity: sha512-jS/WzSOcKbOeGrcgKbenZeNhxUNnP36Yw11+hL4TTxQXErGfqYZ+MaYNNvhaTiGIJlzNSqgQkk9j8dSu1YWSuw==} + /node-mocks-http/1.12.1: + resolution: {integrity: sha512-jrA7Sn3qI6GsHgWtUW3gMj0vO6Yz0nJjzg3jRZYjcfj4tzi8oWPauDK1qHVJoAxTbwuDHF1JiM9GISZ/ocI/ig==} engines: {node: '>=0.6'} dependencies: accepts: 1.3.8 @@ -11637,6 +11652,7 @@ packages: engines: {node: '>= 0.8'} dependencies: ee-first: 1.1.1 + dev: false /once/1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -11658,6 +11674,7 @@ packages: /only/0.0.2: resolution: {integrity: sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=} + dev: false /open/8.4.0: resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} @@ -11720,6 +11737,21 @@ packages: wcwidth: 1.0.1 dev: false + /ora/6.1.2: + resolution: {integrity: sha512-EJQ3NiP5Xo94wJXIzAyOtSb0QEIAUu7m8t6UZ9krbz0vAJqr92JpcK/lEXg91q6B9pEGqrykkd2EQplnifDSBw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + bl: 5.1.0 + chalk: 5.1.2 + cli-cursor: 4.0.0 + cli-spinners: 2.6.1 + is-interactive: 2.0.0 + is-unicode-supported: 1.3.0 + log-symbols: 5.1.0 + strip-ansi: 7.0.1 + wcwidth: 1.0.1 + dev: false + /ordered-binary/1.4.0: resolution: {integrity: sha512-EHQ/jk4/a9hLupIKxTfUsQRej1Yd/0QLQs3vGvIqg5ZtCYSzNhkzHoZc7Zf4e4kUlDaC3Uw8Q/1opOLNN2OKRQ==} dev: true @@ -11973,7 +12005,6 @@ packages: resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} dependencies: isarray: 0.0.1 - dev: false /path-to-regexp/6.2.1: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} @@ -12282,7 +12313,7 @@ packages: resolution: {integrity: sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.1 + nanoid: 3.3.4 picocolors: 1.0.0 source-map-js: 1.0.2 dev: true @@ -12482,11 +12513,6 @@ packages: react-is: 16.13.1 dev: true - /propagate/2.0.1: - resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} - engines: {node: '>= 8'} - dev: true - /property-information/5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} dependencies: @@ -13283,6 +13309,14 @@ packages: onetime: 5.1.2 signal-exit: 3.0.7 + /restore-cursor/4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: false + /retry/0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -13512,6 +13546,17 @@ packages: semver: 7.0.0 dev: true + /sinon/15.0.0: + resolution: {integrity: sha512-pV97G1GbslaSJoSdy2F2z8uh5F+uPGp3ddOzA4JsBOUBLEQRz2OAqlKGRFTSh2KiqUCmHkzyAeu7R4x1Hx0wwg==} + dependencies: + '@sinonjs/commons': 2.0.0 + '@sinonjs/fake-timers': 9.1.2 + '@sinonjs/samsam': 7.0.1 + diff: 5.1.0 + nise: 5.1.3 + supports-color: 7.2.0 + dev: true + /sisteransi/1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -13962,7 +14007,6 @@ packages: engines: {node: '>=12'} dependencies: ansi-regex: 6.0.1 - dev: true /strip-bom/3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} @@ -14494,74 +14538,6 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest/29.0.3_lr7fqxhx6o7ex6ma5v5npbw6ae: - resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 || ^29.1.2 - typescript: '>=4.3' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - dependencies: - '@jest/types': 29.1.2 - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 29.1.2_@types+node@16.11.12 - jest-util: 29.2.1 - json5: 2.2.1 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.3.8 - typescript: 4.9.3 - yargs-parser: 21.1.1 - dev: true - - /ts-jest/29.0.3_o3wtcjdhyxuv43bggxcaucanwu: - resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 || ^29.1.2 - typescript: '>=4.3' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - dependencies: - '@jest/types': 29.1.2 - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 29.3.1_k5ytkvaprncdyzidqqws5bqksq - jest-util: 29.2.1 - json5: 2.2.1 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.3.8 - typescript: 4.7.4 - yargs-parser: 21.1.1 - dev: true - /ts-node/10.7.0_bjctuninx3nzqxltyvshqte2ni: resolution: {integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==} hasBin: true @@ -14651,6 +14627,7 @@ packages: /tsscmp/1.0.6: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} + dev: false /tsutils/3.21.0_typescript@4.7.4: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -15041,6 +15018,7 @@ packages: /vary/1.1.2: resolution: {integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=} engines: {node: '>= 0.8'} + dev: false /vfile-location/3.2.0: resolution: {integrity: sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==} @@ -15403,6 +15381,7 @@ packages: /ylru/1.2.1: resolution: {integrity: sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==} engines: {node: '>= 4.0.0'} + dev: false /yn/3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} From 2c1640d889ad3dfcef36507e5c115c20cf4f4f8e Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Mon, 12 Dec 2022 13:53:47 +0800 Subject: [PATCH 150/166] refactor(console): `TabNavItem` props type (#2630) --- .../console/src/components/TabNav/TabNavItem.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/console/src/components/TabNav/TabNavItem.tsx b/packages/console/src/components/TabNav/TabNavItem.tsx index bf7e7bb8b..72fb788d5 100644 --- a/packages/console/src/components/TabNav/TabNavItem.tsx +++ b/packages/console/src/components/TabNav/TabNavItem.tsx @@ -6,14 +6,24 @@ import { onKeyDownHandler } from '@/utilities/a11y'; import * as styles from './TabNavItem.module.scss'; -type Props = { - href?: string; +type BaseProps = { isActive?: boolean; errorCount?: number; - onClick?: () => void; children: React.ReactNode; }; +type LinkStyleProps = { + href: string; +}; + +type TabStyleProps = { + onClick: () => void; +}; + +type Props = + | (BaseProps & LinkStyleProps & Partial>) + | (BaseProps & TabStyleProps & Partial>); + const TabNavItem = ({ children, href, isActive, errorCount = 0, onClick }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const location = useLocation(); From e255021501416a6fc479f916e18ba216693109bb Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Mon, 12 Dec 2022 14:28:18 +0800 Subject: [PATCH 151/166] chore: fix typos and errors in i18n phrases (#2632) --- .../pages/Connectors/components/ConnectorForm/index.tsx | 7 ++----- .../src/locales/de/translation/admin-console/connectors.ts | 6 +++--- .../src/locales/en/translation/admin-console/connectors.ts | 6 +++--- .../src/locales/fr/translation/admin-console/connectors.ts | 6 +++--- .../src/locales/ko/translation/admin-console/connectors.ts | 6 +++--- .../locales/pt-br/translation/admin-console/connectors.ts | 4 ++-- .../locales/pt-pt/translation/admin-console/connectors.ts | 6 +++--- .../locales/tr-tr/translation/admin-console/connectors.ts | 6 +++--- .../zh-cn/translation/admin-console/application-details.ts | 2 +- .../locales/zh-cn/translation/admin-console/connectors.ts | 4 ++-- .../locales/zh-cn/translation/admin-console/sign-in-exp.ts | 6 +++--- 11 files changed, 28 insertions(+), 31 deletions(-) diff --git a/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx b/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx index 16f8d9790..295c38b8a 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx +++ b/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx @@ -58,16 +58,13 @@ const ConnectorForm = ({ connector, isAllowEditTarget }: Props) => {
{t('connectors.guide.name_tip')}
- +
{t('connectors.guide.logo_tip')}
{darkVisible && (
{t('connectors.guide.logo_dark_tip')}
diff --git a/packages/phrases/src/locales/de/translation/admin-console/connectors.ts b/packages/phrases/src/locales/de/translation/admin-console/connectors.ts index 617d9341a..48cda6dd4 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/connectors.ts @@ -34,10 +34,10 @@ const connectors = { name: 'Connector name', // UNTRANSLATED name_tip: 'Connector button’s name will display as "Continue with {{Connector Name}}".', // UNTRANSLATED logo: 'Connector logo URL', // UNTRANSLATED - logo_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED + logo_placeholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED logo_tip: 'The logo image will also display on the connector button.', // UNTRANSLATED logo_dark: 'Connector logo URL (Dark mode)', // UNTRANSLATED - logo_dark_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED + logo_dark_placeholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED logo_dark_tip: 'This will be used when opening “Enable dark mode” in the setting of sign in experience.', // UNTRANSLATED logo_dark_collapse: 'Collapse', // UNTRANSLATED @@ -45,7 +45,7 @@ const connectors = { target: 'Connector identity target', // UNTRANSLATED target_tip: 'A unique identifier for the connector.', // UNTRANSLATED config: 'Enter your JSON here', // UNTRANSLATED - sync_profile: 'Sync profile information form the social provider', // UNTRANSLATED + sync_profile: 'Sync profile information from the social provider', // UNTRANSLATED sync_profile_only_at_register: 'Only sync at register', // UNTRANSLATED sync_profile_each_sign_in: 'Always sync at each sign-in', // UNTRANSLATED }, diff --git a/packages/phrases/src/locales/en/translation/admin-console/connectors.ts b/packages/phrases/src/locales/en/translation/admin-console/connectors.ts index 8b4b24443..2136e7cd3 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/connectors.ts @@ -34,10 +34,10 @@ const connectors = { name: 'Connector name', name_tip: 'Connector button’s name will display as "Continue with {{Connector Name}}".', logo: 'Connector logo URL', - logo_placelholder: 'https://your.cdn.domain/logo.png', + logo_placeholder: 'https://your.cdn.domain/logo.png', logo_tip: 'The logo image will also display on the connector button.', logo_dark: 'Connector logo URL (Dark mode)', - logo_dark_placelholder: 'https://your.cdn.domain/logo.png', + logo_dark_placeholder: 'https://your.cdn.domain/logo.png', logo_dark_tip: 'This will be used when opening “Enable dark mode” in the setting of sign in experience.', logo_dark_collapse: 'Collapse', @@ -45,7 +45,7 @@ const connectors = { target: 'Connector identity target', target_tip: 'A unique identifier for the connector.', config: 'Enter your JSON here', - sync_profile: 'Sync profile information form the social provider', + sync_profile: 'Sync profile information from the social provider', sync_profile_only_at_register: 'Only sync at register', sync_profile_each_sign_in: 'Always sync at each sign-in', }, diff --git a/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts b/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts index 927e1e56e..97b43283e 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts @@ -35,10 +35,10 @@ const connectors = { name: 'Connector name', // UNTRANSLATED name_tip: 'Connector button’s name will display as "Continue with {{Connector Name}}".', // UNTRANSLATED logo: 'Connector logo URL', // UNTRANSLATED - logo_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED + logo_placeholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED logo_tip: 'The logo image will also display on the connector button.', // UNTRANSLATED logo_dark: 'Connector logo URL (Dark mode)', // UNTRANSLATED - logo_dark_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED + logo_dark_placeholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED logo_dark_tip: 'This will be used when opening “Enable dark mode” in the setting of sign in experience.', // UNTRANSLATED logo_dark_collapse: 'Collapse', // UNTRANSLATED @@ -46,7 +46,7 @@ const connectors = { target: 'Connector identity target', // UNTRANSLATED target_tip: 'A unique identifier for the connector.', // UNTRANSLATED config: 'Enter your JSON here', // UNTRANSLATED - sync_profile: 'Sync profile information form the social provider', // UNTRANSLATED + sync_profile: 'Sync profile information from the social provider', // UNTRANSLATED sync_profile_only_at_register: 'Only sync at register', // UNTRANSLATED sync_profile_each_sign_in: 'Always sync at each sign-in', // UNTRANSLATED }, diff --git a/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts b/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts index 67fc23f80..2108727e9 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts @@ -34,10 +34,10 @@ const connectors = { name: 'Connector name', // UNTRANSLATED name_tip: 'Connector button’s name will display as "Continue with {{Connector Name}}".', // UNTRANSLATED logo: 'Connector logo URL', // UNTRANSLATED - logo_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED + logo_placeholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED logo_tip: 'The logo image will also display on the connector button.', // UNTRANSLATED logo_dark: 'Connector logo URL (Dark mode)', // UNTRANSLATED - logo_dark_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED + logo_dark_placeholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED logo_dark_tip: 'This will be used when opening “Enable dark mode” in the setting of sign in experience.', // UNTRANSLATED logo_dark_collapse: 'Collapse', // UNTRANSLATED @@ -45,7 +45,7 @@ const connectors = { target: 'Connector identity target', // UNTRANSLATED target_tip: 'A unique identifier for the connector.', // UNTRANSLATED config: 'Enter your JSON here', // UNTRANSLATED - sync_profile: 'Sync profile information form the social provider', // UNTRANSLATED + sync_profile: 'Sync profile information from the social provider', // UNTRANSLATED sync_profile_only_at_register: 'Only sync at register', // UNTRANSLATED sync_profile_each_sign_in: 'Always sync at each sign-in', // UNTRANSLATED }, diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/connectors.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/connectors.ts index 656ebeae9..54ff532b7 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/connectors.ts @@ -35,10 +35,10 @@ const connectors = { name: 'Nome do conector', name_tip: 'O nome do botão do conector será exibido como "Continue com {{Connector Name}}".', logo: 'URL do logotipo do conector', - logo_placelholder: 'https://your.cdn.domain/logo.png', + logo_placeholder: 'https://your.cdn.domain/logo.png', logo_tip: 'A imagem do logotipo também será exibida no botão do conector.', logo_dark: 'URL do logotipo do conector (modo escuro)', - logo_dark_placelholder: 'https://your.cdn.domain/logo.png', + logo_dark_placeholder: 'https://your.cdn.domain/logo.png', logo_dark_tip: 'Isso será usado ao abrir "Ativar modo escuro" na configuração da experiência de login.', logo_dark_collapse: 'Collapse', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts index 546b14bfd..2a80ee5d8 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts @@ -34,10 +34,10 @@ const connectors = { name: 'Connector name', // UNTRANSLATED name_tip: 'Connector button’s name will display as "Continue with {{Connector Name}}".', // UNTRANSLATED logo: 'Connector logo URL', // UNTRANSLATED - logo_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED + logo_placeholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED logo_tip: 'The logo image will also display on the connector button.', // UNTRANSLATED logo_dark: 'Connector logo URL (Dark mode)', // UNTRANSLATED - logo_dark_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED + logo_dark_placeholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED logo_dark_tip: 'This will be used when opening “Enable dark mode” in the setting of sign in experience.', // UNTRANSLATED logo_dark_collapse: 'Collapse', // UNTRANSLATED @@ -45,7 +45,7 @@ const connectors = { target: 'Connector identity target', // UNTRANSLATED target_tip: 'A unique identifier for the connector.', // UNTRANSLATED config: 'Enter your JSON here', // UNTRANSLATED - sync_profile: 'Sync profile information form the social provider', // UNTRANSLATED + sync_profile: 'Sync profile information from the social provider', // UNTRANSLATED sync_profile_only_at_register: 'Only sync at register', // UNTRANSLATED sync_profile_each_sign_in: 'Always sync at each sign-in', // UNTRANSLATED }, diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts index 5e2d22d08..35e120b22 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts @@ -35,10 +35,10 @@ const connectors = { name: 'Connector name', // UNTRANSLATED name_tip: 'Connector button’s name will display as "Continue with {{Connector Name}}".', // UNTRANSLATED logo: 'Connector logo URL', // UNTRANSLATED - logo_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED + logo_placeholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED logo_tip: 'The logo image will also display on the connector button.', // UNTRANSLATED logo_dark: 'Connector logo URL (Dark mode)', // UNTRANSLATED - logo_dark_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED + logo_dark_placeholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED logo_dark_tip: 'This will be used when opening “Enable dark mode” in the setting of sign in experience.', // UNTRANSLATED logo_dark_collapse: 'Collapse', // UNTRANSLATED @@ -46,7 +46,7 @@ const connectors = { target: 'Connector identity target', // UNTRANSLATED target_tip: 'A unique identifier for the connector.', // UNTRANSLATED config: 'Enter your JSON here', // UNTRANSLATED - sync_profile: 'Sync profile information form the social provider', // UNTRANSLATED + sync_profile: 'Sync profile information from the social provider', // UNTRANSLATED sync_profile_only_at_register: 'Only sync at register', // UNTRANSLATED sync_profile_each_sign_in: 'Always sync at each sign-in', // UNTRANSLATED }, diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts index cf83fd6a9..4f0821285 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts @@ -28,7 +28,7 @@ const application_details = { post_sign_out_redirect_uris: 'Post sign out redirect URIs', post_sign_out_redirect_uri_placeholder: 'https://your.website.com/home', post_sign_out_redirect_uri_tip: - '在用户登出后重定向的目标 URI(可选)。在某些应用类型中可能无实质作用。', + '在用户退出登录后重定向的目标 URI(可选)。在某些应用类型中可能无实质作用。', cors_allowed_origins: 'CORS Allowed Origins', cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts index c6c714552..107e93b28 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts @@ -33,10 +33,10 @@ const connectors = { name: '连接器名称', name_tip: '连接器按钮名将会是「通过 {{连接器名称}} 登录」。', logo: '连接器图标地址', - logo_placelholder: 'https://your.cdn.domain/logo.png', + logo_placeholder: 'https://your.cdn.domain/logo.png', logo_tip: '图标将会在连接器按钮中展示', logo_dark: '连接器图标地址(深色模式)', - logo_dark_placelholder: 'https://your.cdn.domain/logo.png', + logo_dark_placeholder: 'https://your.cdn.domain/logo.png', logo_dark_tip: '在登录体验设置中打开「启用深色模式」后生效', logo_dark_collapse: '折叠', logo_dark_show: '显示「深色模式图标」', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts index 5a4d0fa60..673be2b74 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts @@ -136,11 +136,11 @@ const sign_in_exp = { setup_warning: { no_connector: '', no_connector_sms: - '你尚未设置 SMS 短信连接器。在完成该配置前,你将无法登录。{{link}}其他社交连接器。', + '你尚未设置 SMS 短信连接器。在完成该配置前,你将无法登录。{{link}}连接器。', no_connector_email: - '你尚未设置电子邮件连接器。在完成该配置前,你将无法登录。{{link}}其他社交连接器。', + '你尚未设置电子邮件连接器。在完成该配置前,你将无法登录。{{link}}连接器。', no_connector_social: - '你尚未设置社交连接器。在完成该配置前,你将无法登录。{{link}}其他社交连接器。', + '你尚未设置社交连接器。在完成该配置前,你将无法登录。{{link}}连接器。', no_added_social_connector: '你已经成功设置了一些社交连接器。点按「+」添加一些到你的登录体验。', setup_link: '立即设置', }, From 57cd6f877d645d3ad1f4987b7267c5f0a4b76169 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Mon, 12 Dec 2022 16:40:27 +0800 Subject: [PATCH 152/166] style(console): update link styles in markdown contents (#2634) --- .../src/components/Markdown/index.module.scss | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/console/src/components/Markdown/index.module.scss b/packages/console/src/components/Markdown/index.module.scss index 05858a892..62922ed89 100644 --- a/packages/console/src/components/Markdown/index.module.scss +++ b/packages/console/src/components/Markdown/index.module.scss @@ -30,6 +30,21 @@ } } + a { + font: var(--font-body-medium); + color: var(--color-text-link); + text-decoration: none; + text-underline-offset: 2px; + + &:hover { + text-decoration: underline; + } + + &:active { + color: var(--color-primary-pressed); + } + } + h1 { font: var(--font-title-large); margin: _.unit(6) 0; From 81efde35d36d1c38d86ae7bef8b40a6b1ab08997 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Mon, 12 Dec 2022 16:40:45 +0800 Subject: [PATCH 153/166] style(console): add border to the color block of `ColorPicker` (#2635) --- packages/console/src/components/ColorPicker/index.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/console/src/components/ColorPicker/index.module.scss b/packages/console/src/components/ColorPicker/index.module.scss index 14dccb2d7..3bfd3568d 100644 --- a/packages/console/src/components/ColorPicker/index.module.scss +++ b/packages/console/src/components/ColorPicker/index.module.scss @@ -22,6 +22,7 @@ width: 24px; height: 24px; padding: 0; + border: 1px solid var(--color-divider); border-radius: 4px; overflow: hidden; From 3db51178a4ceb6db2a4e4721e1d4c0f5cb5392dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 21:51:01 +0800 Subject: [PATCH 154/166] chore(deps): update logto js sdk monorepo packages to v1.0.0-beta.14 (#2645) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/console/package.json | 2 +- packages/demo-app/package.json | 2 +- packages/integration-tests/package.json | 4 +- pnpm-lock.yaml | 53 +++++++++++-------------- 4 files changed, 28 insertions(+), 33 deletions(-) diff --git a/packages/console/package.json b/packages/console/package.json index 636d76828..f9f675097 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -23,7 +23,7 @@ "@logto/language-kit": "1.0.0-beta.28", "@logto/phrases": "workspace:*", "@logto/phrases-ui": "workspace:*", - "@logto/react": "1.0.0-beta.13", + "@logto/react": "1.0.0-beta.14", "@logto/schemas": "workspace:*", "@mdx-js/react": "^1.6.22", "@parcel/core": "2.8.0", diff --git a/packages/demo-app/package.json b/packages/demo-app/package.json index a0522afa0..2c5194f11 100644 --- a/packages/demo-app/package.json +++ b/packages/demo-app/package.json @@ -19,7 +19,7 @@ "@logto/core-kit": "1.0.0-beta.28", "@logto/language-kit": "1.0.0-beta.28", "@logto/phrases": "workspace:*", - "@logto/react": "1.0.0-beta.13", + "@logto/react": "1.0.0-beta.14", "@logto/schemas": "workspace:*", "@parcel/core": "2.8.0", "@parcel/transformer-sass": "2.8.0", diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index b3a3d596f..01749cc2d 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -15,8 +15,8 @@ }, "devDependencies": { "@jest/types": "^29.1.2", - "@logto/js": "1.0.0-beta.13", - "@logto/node": "1.0.0-beta.13", + "@logto/js": "1.0.0-beta.14", + "@logto/node": "1.0.0-beta.14", "@logto/schemas": "workspace:*", "@peculiar/webcrypto": "^1.3.3", "@silverhand/eslint-config": "1.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09663cb63..7d4883e22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,7 +109,7 @@ importers: '@logto/language-kit': 1.0.0-beta.28 '@logto/phrases': workspace:* '@logto/phrases-ui': workspace:* - '@logto/react': 1.0.0-beta.13 + '@logto/react': 1.0.0-beta.14 '@logto/schemas': workspace:* '@mdx-js/react': ^1.6.22 '@parcel/core': 2.8.0 @@ -181,7 +181,7 @@ importers: '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 '@logto/phrases': link:../phrases '@logto/phrases-ui': link:../phrases-ui - '@logto/react': 1.0.0-beta.13_react@18.2.0 + '@logto/react': 1.0.0-beta.14_react@18.2.0 '@logto/schemas': link:../schemas '@mdx-js/react': 1.6.22_react@18.2.0 '@parcel/core': 2.8.0 @@ -414,7 +414,7 @@ importers: '@logto/core-kit': 1.0.0-beta.28 '@logto/language-kit': 1.0.0-beta.28 '@logto/phrases': workspace:* - '@logto/react': 1.0.0-beta.13 + '@logto/react': 1.0.0-beta.14 '@logto/schemas': workspace:* '@parcel/core': 2.8.0 '@parcel/transformer-sass': 2.8.0 @@ -442,7 +442,7 @@ importers: '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 '@logto/phrases': link:../phrases - '@logto/react': 1.0.0-beta.13_react@18.2.0 + '@logto/react': 1.0.0-beta.14_react@18.2.0 '@logto/schemas': link:../schemas '@parcel/core': 2.8.0 '@parcel/transformer-sass': 2.8.0_@parcel+core@2.8.0 @@ -470,8 +470,8 @@ importers: packages/integration-tests: specifiers: '@jest/types': ^29.1.2 - '@logto/js': 1.0.0-beta.13 - '@logto/node': 1.0.0-beta.13 + '@logto/js': 1.0.0-beta.14 + '@logto/node': 1.0.0-beta.14 '@logto/schemas': workspace:* '@peculiar/webcrypto': ^1.3.3 '@silverhand/eslint-config': 1.3.0 @@ -496,8 +496,8 @@ importers: typescript: ^4.7.4 devDependencies: '@jest/types': 29.1.2 - '@logto/js': 1.0.0-beta.13 - '@logto/node': 1.0.0-beta.13 + '@logto/js': 1.0.0-beta.14 + '@logto/node': 1.0.0-beta.14 '@logto/schemas': link:../schemas '@peculiar/webcrypto': 1.3.3 '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni @@ -2428,22 +2428,22 @@ packages: dev: true optional: true - /@logto/browser/1.0.0-beta.13: - resolution: {integrity: sha512-ddAVggFcbS9yfG8Gvn2xknE2NZd6+lGxOQ6UbjIJKsYBAsJG95u1ITYaP7tNSDdxqZPmSBGXp4rfsQB+u0JPJQ==} + /@logto/browser/1.0.0-beta.14: + resolution: {integrity: sha512-yjD1qtRXbX2E5Jgr5F1BK4SRwNhIlbJZK1yZLZNvOltEG76NhfoqvCI8P5PGIiPwvunB2lqPNJFsNOSI3k0Q+w==} dependencies: - '@logto/client': 1.0.0-beta.13 + '@logto/client': 1.0.0-beta.14 '@silverhand/essentials': 1.3.0 js-base64: 3.7.2 dev: true - /@logto/client/1.0.0-beta.13: - resolution: {integrity: sha512-ddHILkcBW92p4x/TfUGqT3WXZzX14xgLd6lZZsoCgNZ9QWS7Jw+NsT/knJs96cd2A/jv9RZIGzh1g6+zlox7bw==} + /@logto/client/1.0.0-beta.14: + resolution: {integrity: sha512-quhQJ4rjb1Djhspeq2F5pFxXdgjN5UaWei6nnbUfp12CDhRojKrLJIGl+FDx/HSWuG0b93nwxKnJeJsiX/8E3Q==} dependencies: '@logto/core-kit': 1.0.0-beta.20 - '@logto/js': 1.0.0-beta.13 + '@logto/js': 1.0.0-beta.14 '@silverhand/essentials': 1.3.0 camelcase-keys: 7.0.2 - jose: 4.10.4 + jose: 4.11.0 lodash.get: 4.4.2 lodash.once: 4.1.1 dev: true @@ -2481,13 +2481,13 @@ packages: nanoid: 3.3.4 zod: 3.19.1 - /@logto/js/1.0.0-beta.13: - resolution: {integrity: sha512-a3dhoJre/VOXgGxFNon/xY5E4fVs0CiFW5Ci0gt2W7v2zD/1VmzIvcJnECTLocxyz9W2UTkfYzm7q/iXF48WXw==} + /@logto/js/1.0.0-beta.14: + resolution: {integrity: sha512-fMmZHfqkgpArcQxN8+Aj3hMJ+Gpyg7fg+iiyKKw4IOGzG0oe1D7R18EQ/jUTpw87mL4wxKpOOwsDpMgixfgQxQ==} dependencies: '@logto/core-kit': 1.0.0-beta.20 '@silverhand/essentials': 1.3.0 camelcase-keys: 7.0.2 - jose: 4.10.4 + jose: 4.11.0 lodash.get: 4.4.2 dev: true @@ -2499,10 +2499,10 @@ packages: dependencies: zod: 3.19.1 - /@logto/node/1.0.0-beta.13: - resolution: {integrity: sha512-e+Vspi17izZdt6PU2+uFXrD2XydZ8ohthtOMJqhrzpC1DiOSlmb47UXo3K+IS93eULiuPNo5mA04hRn3kt6rcQ==} + /@logto/node/1.0.0-beta.14: + resolution: {integrity: sha512-+0S6lBBcG3pOmjEMRQnD+6X0MJ3V3E/4In59ckl/uVr/UgIufvOKWJwWCfsVKyguaO3QweJn19x7YkF8FyO31g==} dependencies: - '@logto/client': 1.0.0-beta.13 + '@logto/client': 1.0.0-beta.14 '@silverhand/essentials': 1.3.0 js-base64: 3.7.2 node-fetch: 2.6.7 @@ -2510,12 +2510,12 @@ packages: - encoding dev: true - /@logto/react/1.0.0-beta.13_react@18.2.0: - resolution: {integrity: sha512-jG7rXm5aW/aJ+RN9Rw8tEo4c/12LzO99qt8eZ39xFWfMB+g06/rmxPZGMMkkqaNexhE2KLr4cASYeOafTR8wwQ==} + /@logto/react/1.0.0-beta.14_react@18.2.0: + resolution: {integrity: sha512-lHuwpHzJkIbHj/VvhzxmL7hWkyDYA8rInv0sm0M21br43OotgP7fMc62Wj78ty+QIj+or5UGwCcULBa8HySQcQ==} peerDependencies: react: '>=16.8.0 || ^18.0.0' dependencies: - '@logto/browser': 1.0.0-beta.13 + '@logto/browser': 1.0.0-beta.14 '@silverhand/essentials': 1.3.0 react: 18.2.0 dev: true @@ -10009,13 +10009,8 @@ packages: '@sideway/pinpoint': 2.0.0 dev: true - /jose/4.10.4: - resolution: {integrity: sha512-eBH77Xs9Yc/oTDvukhAEDVMijhekPuNktXJL4tUlB22jqKP1k48v5nmsUmc8feoJPsxB3HsfEt2LbVSoz+1mng==} - dev: true - /jose/4.11.0: resolution: {integrity: sha512-wLe+lJHeG8Xt6uEubS4x0LVjS/3kXXu9dGoj9BNnlhYq7Kts0Pbb2pvv5KiI0yaKH/eaiR0LUOBhOVo9ktd05A==} - dev: false /js-base64/3.7.2: resolution: {integrity: sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==} From 1d1e6e3244fd3a4459a911aa1827c27f38e4121e Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Sun, 11 Dec 2022 20:07:21 +0800 Subject: [PATCH 155/166] test: update to native ESM (batch 1) --- packages/cli/package.json | 7 ++++--- packages/cli/tsconfig.json | 3 +-- packages/core/package.json | 2 +- packages/core/tsconfig.json | 3 +-- packages/schemas/jest.config.js | 10 ++++++++++ packages/schemas/jest.config.ts | 16 ---------------- packages/schemas/package.json | 10 +++++----- packages/schemas/tsconfig.build.gen.json | 4 ++++ packages/schemas/tsconfig.json | 3 +-- packages/schemas/tsconfig.test.json | 7 ++++++- packages/shared/jest.config.js | 10 ++++++++++ packages/shared/jest.config.ts | 15 --------------- packages/shared/package.json | 7 ++++--- packages/shared/tsconfig.json | 3 +-- pnpm-lock.yaml | 8 +------- 15 files changed, 49 insertions(+), 59 deletions(-) create mode 100644 packages/schemas/jest.config.js delete mode 100644 packages/schemas/jest.config.ts create mode 100644 packages/schemas/tsconfig.build.gen.json create mode 100644 packages/shared/jest.config.js delete mode 100644 packages/shared/jest.config.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 6ddca2849..fb7843be7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -25,13 +25,14 @@ "precommit": "lint-staged", "prepare:package-json": "node -p \"'export const packageJson = ' + JSON.stringify(require('./package.json'), undefined, 2) + ';'\" > src/package-json.ts", "build": "rimraf lib && pnpm prepare:package-json && tsc -p tsconfig.build.json", - "build:test": "rm -rf build/ && tsc -p tsconfig.test.json --sourcemap", + "build:test": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental", "start": "node .", - "start:dev": "ts-node --files src/index.ts", + "start:dev": "pnpm build && node .", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", - "test": "pnpm build:test && NODE_OPTIONS=--experimental-vm-modules jest", + "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", + "test": "pnpm build:test && pnpm test:only", "test:ci": "pnpm run test", "prepack": "pnpm build" }, diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 8378efdc2..d706f486e 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -9,8 +9,7 @@ "types": ["node", "jest"] }, "include": [ - "src", - "jest.*.ts" + "src" ], "exclude": ["**/alteration-scripts"] } diff --git a/packages/core/package.json b/packages/core/package.json index 23ab9455a..298275bde 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -20,7 +20,7 @@ "dev": "rm -rf build/ && pnpm run copyfiles && nodemon", "start": "NODE_ENV=production node build/index.js", "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", - "test": "pnpm build:test && pnpm test:only build/", + "test": "pnpm build:test && pnpm test:only", "test:ci": "pnpm run test --coverage --silent", "test:report": "codecov -F core" }, diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 64517c9bb..2796b3926 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -8,7 +8,6 @@ ] }, "include": [ - "src", - "jest.*.ts" + "src" ] } diff --git a/packages/schemas/jest.config.js b/packages/schemas/jest.config.js new file mode 100644 index 000000000..652de5472 --- /dev/null +++ b/packages/schemas/jest.config.js @@ -0,0 +1,10 @@ +const config = { + coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'], + coverageReporters: ['text-summary', 'lcov'], + roots: ['./lib'], + moduleNameMapper: { + '^(chalk|inquirer)$': '/../shared/lib/esm/module-proxy.js', + }, +}; + +export default config; diff --git a/packages/schemas/jest.config.ts b/packages/schemas/jest.config.ts deleted file mode 100644 index e5f84f9f6..000000000 --- a/packages/schemas/jest.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Config } from '@silverhand/jest-config'; -import { merge } from '@silverhand/jest-config'; - -const config: Config.InitialOptions = { - ...merge({ - // Will update common config soon - transformIgnorePatterns: ['node_modules/(?!(.*(nanoid|jose|ky|@logto))/)'], - moduleNameMapper: { - '^(\\.{1,2}/.*)\\.js$': '$1', - }, - }), - // Will update common config soon - transformIgnorePatterns: ['node_modules/(?!(.*(nanoid|jose|ky|@logto))/)'], -}; - -export default config; diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 9fead7017..8ccf7a232 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -17,15 +17,17 @@ "scripts": { "precommit": "lint-staged", "version": "./update-next.sh && git add alterations/", - "generate": "rm -rf src/db-entries && ts-node-esm src/gen/index.ts && eslint \"src/db-entries/**\" --fix", + "generate": "rm -rf src/db-entries && tsc -p tsconfig.build.gen.json && node lib/src/gen/index.js && eslint \"src/db-entries/**\" --fix", "build:alterations": "rm -rf alterations-js && tsc -p tsconfig.build.alterations.json", "build": "pnpm generate && rm -rf lib/ && tsc -p tsconfig.build.json && pnpm build:alterations", + "build:test": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", "prepack": "pnpm build", - "test": "jest", - "test:ci": "jest" + "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", + "test": "pnpm build:test && pnpm test:only", + "test:ci": "pnpm run test" }, "engines": { "node": "^16.13.0 || ^18.12.0" @@ -33,7 +35,6 @@ "devDependencies": { "@silverhand/eslint-config": "1.3.0", "@silverhand/essentials": "^1.3.0", - "@silverhand/jest-config": "1.2.2", "@silverhand/ts-config": "1.2.1", "@types/jest": "^29.1.2", "@types/lodash.uniq": "^4.5.6", @@ -47,7 +48,6 @@ "pluralize": "^8.0.0", "prettier": "^2.7.1", "slonik": "^30.0.0", - "ts-node": "^10.9.1", "typescript": "^4.7.4" }, "eslintConfig": { diff --git a/packages/schemas/tsconfig.build.gen.json b/packages/schemas/tsconfig.build.gen.json new file mode 100644 index 000000000..eeed8fa29 --- /dev/null +++ b/packages/schemas/tsconfig.build.gen.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig", + "include": ["src/gen"] +} diff --git a/packages/schemas/tsconfig.json b/packages/schemas/tsconfig.json index bf5e57cc9..f07ef0259 100644 --- a/packages/schemas/tsconfig.json +++ b/packages/schemas/tsconfig.json @@ -8,7 +8,6 @@ }, "include": [ "src", - "alterations", - "jest.config.ts" + "alterations" ] } diff --git a/packages/schemas/tsconfig.test.json b/packages/schemas/tsconfig.test.json index 1c66acf6d..55de18c33 100644 --- a/packages/schemas/tsconfig.test.json +++ b/packages/schemas/tsconfig.test.json @@ -1,3 +1,8 @@ { - "extends": "./tsconfig" + "extends": "./tsconfig", + "compilerOptions": { + "isolatedModules": false, + "allowJs": true + }, + "include": ["src"] } diff --git a/packages/shared/jest.config.js b/packages/shared/jest.config.js new file mode 100644 index 000000000..652de5472 --- /dev/null +++ b/packages/shared/jest.config.js @@ -0,0 +1,10 @@ +const config = { + coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'], + coverageReporters: ['text-summary', 'lcov'], + roots: ['./lib'], + moduleNameMapper: { + '^(chalk|inquirer)$': '/../shared/lib/esm/module-proxy.js', + }, +}; + +export default config; diff --git a/packages/shared/jest.config.ts b/packages/shared/jest.config.ts deleted file mode 100644 index 97b9c20ae..000000000 --- a/packages/shared/jest.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Config } from '@silverhand/jest-config'; -import { merge } from '@silverhand/jest-config'; - -const config: Config.InitialOptions = { - ...merge({ - roots: ['./src'], - moduleNameMapper: { - '^(\\.{1,2}/.*)\\.js$': '$1', - }, - }), - // Will update common config soon - transformIgnorePatterns: ['node_modules/(?!(.*(nanoid|jose|ky|@logto))/)'], -}; - -export default config; diff --git a/packages/shared/package.json b/packages/shared/package.json index 5548c608c..740cf8aea 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -22,16 +22,17 @@ "scripts": { "precommit": "lint-staged", "build": "rm -rf lib/ && tsc -p tsconfig.build.json", + "build:test": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", "prepack": "pnpm build", - "test": "jest", - "test:ci": "jest" + "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", + "test": "pnpm build:test && pnpm test:only", + "test:ci": "pnpm run test" }, "devDependencies": { "@silverhand/eslint-config": "1.3.0", - "@silverhand/jest-config": "1.2.2", "@silverhand/ts-config": "1.2.1", "@types/jest": "^29.1.2", "@types/node": "^16.0.0", diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 3bccdbc71..fd9fd4d6f 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -8,7 +8,6 @@ "module": "es2022" }, "include": [ - "src", - "jest.config.ts" + "src" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d4883e22..6e86483aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -580,7 +580,6 @@ importers: '@logto/phrases-ui': workspace:* '@silverhand/eslint-config': 1.3.0 '@silverhand/essentials': ^1.3.0 - '@silverhand/jest-config': 1.2.2 '@silverhand/ts-config': 1.2.1 '@types/jest': ^29.1.2 '@types/lodash.uniq': ^4.5.6 @@ -594,7 +593,6 @@ importers: pluralize: ^8.0.0 prettier: ^2.7.1 slonik: ^30.0.0 - ts-node: ^10.9.1 typescript: ^4.7.4 zod: ^3.19.1 dependencies: @@ -607,7 +605,6 @@ importers: devDependencies: '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni '@silverhand/essentials': 1.3.0 - '@silverhand/jest-config': 1.2.2_zapogttls25djihwjkusccjjym '@silverhand/ts-config': 1.2.1_typescript@4.7.4 '@types/jest': 29.1.2 '@types/lodash.uniq': 4.5.6 @@ -615,13 +612,12 @@ importers: '@types/pluralize': 0.0.29 camelcase: 7.0.0 eslint: 8.21.0 - jest: 29.1.2_k5ytkvaprncdyzidqqws5bqksq + jest: 29.1.2_@types+node@16.11.12 lint-staged: 13.0.0 lodash.uniq: 4.5.0 pluralize: 8.0.0 prettier: 2.7.1 slonik: 30.1.2 - ts-node: 10.9.1_ccwudyfw5se7hgalwgkzhn2yp4 typescript: 4.7.4 packages/shared: @@ -629,7 +625,6 @@ importers: '@logto/schemas': workspace:* '@silverhand/eslint-config': 1.3.0 '@silverhand/essentials': ^1.3.0 - '@silverhand/jest-config': 1.2.2 '@silverhand/ts-config': 1.2.1 '@types/jest': ^29.1.2 '@types/node': ^16.0.0 @@ -649,7 +644,6 @@ importers: slonik: 30.1.2 devDependencies: '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni - '@silverhand/jest-config': 1.2.2_zapogttls25djihwjkusccjjym '@silverhand/ts-config': 1.2.1_typescript@4.7.4 '@types/jest': 29.1.2 '@types/node': 16.11.12 From 51647cf4ca9d9f3c51e3b32bd67123786a70f85e Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 12 Dec 2022 21:15:09 +0800 Subject: [PATCH 156/166] test: update to native ESM (batch 2) --- package.json | 2 +- packages/cli/package.json | 2 +- packages/console/package.json | 2 +- packages/core/package.json | 2 +- packages/core/tsconfig.base.json | 6 +- packages/demo-app/package.json | 2 +- packages/integration-tests/jest.config.js | 12 + packages/integration-tests/jest.config.ts | 12 - packages/integration-tests/jest.config.ui.js | 10 + packages/integration-tests/jest.config.ui.ts | 12 - packages/integration-tests/jest.setup.js | 4 +- packages/integration-tests/package.json | 16 +- .../integration-tests/src/api/admin-user.ts | 2 +- packages/integration-tests/src/api/api.ts | 4 +- .../integration-tests/src/api/application.ts | 2 +- .../integration-tests/src/api/connector.ts | 2 +- .../integration-tests/src/api/dashboard.ts | 2 +- packages/integration-tests/src/api/index.ts | 22 +- packages/integration-tests/src/api/logs.ts | 2 +- packages/integration-tests/src/api/me.ts | 2 +- .../integration-tests/src/api/resource.ts | 4 +- packages/integration-tests/src/api/session.ts | 2 +- .../src/api/sign-in-experience.ts | 2 +- .../integration-tests/src/api/wellknown.ts | 2 +- .../integration-tests/src/client/index.ts | 12 +- packages/integration-tests/src/constants.ts | 4 +- packages/integration-tests/src/helpers.ts | 6 +- .../include.d/openapi-schema-validator.d.ts | 32 ++ .../{ => src}/tests/api/admin-user.test.ts | 6 +- .../{ => src}/tests/api/application.test.ts | 2 +- .../{ => src}/tests/api/connector.test.ts | 4 +- .../{ => src}/tests/api/dashboard.test.ts | 10 +- .../tests/api/get-access-token.test.ts | 10 +- .../{ => src}/tests/api/health-check.test.ts | 2 +- .../{ => src}/tests/api/logs.test.ts | 8 +- .../{ => src}/tests/api/resource.test.ts | 4 +- .../{ => src}/tests/api/session.test.ts | 12 +- .../tests/api/sign-in-experience.test.ts | 2 +- .../tests/api/social-session.test.ts | 12 +- .../{ => src}/tests/api/swagger-check.test.ts | 6 +- .../{ => src}/tests/api/wellknown.test.ts | 6 +- .../{ => src}/tests/ui/smoke.test.ts | 2 +- packages/integration-tests/tsconfig.json | 8 +- packages/phrases-ui/package.json | 2 +- packages/phrases/package.json | 2 +- packages/schemas/generate.sh | 7 + packages/schemas/package.json | 4 +- packages/shared/package.json | 2 +- packages/ui/package.json | 2 +- pnpm-lock.yaml | 487 ++++++------------ 50 files changed, 339 insertions(+), 445 deletions(-) create mode 100644 packages/integration-tests/jest.config.js delete mode 100644 packages/integration-tests/jest.config.ts create mode 100644 packages/integration-tests/jest.config.ui.js delete mode 100644 packages/integration-tests/jest.config.ui.ts create mode 100644 packages/integration-tests/src/include.d/openapi-schema-validator.d.ts rename packages/integration-tests/{ => src}/tests/api/admin-user.test.ts (94%) rename packages/integration-tests/{ => src}/tests/api/application.test.ts (97%) rename packages/integration-tests/{ => src}/tests/api/connector.test.ts (98%) rename packages/integration-tests/{ => src}/tests/api/dashboard.test.ts (89%) rename packages/integration-tests/{ => src}/tests/api/get-access-token.test.ts (89%) rename packages/integration-tests/{ => src}/tests/api/health-check.test.ts (80%) rename packages/integration-tests/{ => src}/tests/api/logs.test.ts (77%) rename packages/integration-tests/{ => src}/tests/api/resource.test.ts (97%) rename packages/integration-tests/{ => src}/tests/api/session.test.ts (97%) rename packages/integration-tests/{ => src}/tests/api/sign-in-experience.test.ts (92%) rename packages/integration-tests/{ => src}/tests/api/social-session.test.ts (93%) rename packages/integration-tests/{ => src}/tests/api/swagger-check.test.ts (80%) rename packages/integration-tests/{ => src}/tests/api/wellknown.test.ts (88%) rename packages/integration-tests/{ => src}/tests/ui/smoke.test.ts (81%) create mode 100755 packages/schemas/generate.sh diff --git a/package.json b/package.json index de3801af8..c1f418579 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@commitlint/config-conventional": "^17.0.0", "@commitlint/types": "^17.0.0", "husky": "^8.0.0", - "typescript": "^4.7.4" + "typescript": "^4.9.4" }, "workspaces": { "packages": [ diff --git a/packages/cli/package.json b/packages/cli/package.json index fb7843be7..4829c6544 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -82,7 +82,7 @@ "prettier": "^2.7.1", "rimraf": "^3.0.2", "sinon": "^15.0.0", - "typescript": "^4.7.4" + "typescript": "^4.9.4" }, "eslintConfig": { "extends": "@silverhand", diff --git a/packages/console/package.json b/packages/console/package.json index f9f675097..89488b028 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -87,7 +87,7 @@ "snake-case": "^3.0.4", "stylelint": "^14.9.1", "swr": "^1.3.0", - "typescript": "^4.7.4", + "typescript": "^4.9.4", "zod": "^3.19.1" }, "engines": { diff --git a/packages/core/package.json b/packages/core/package.json index 298275bde..adc7260d9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -101,7 +101,7 @@ "prettier": "^2.7.1", "sinon": "^15.0.0", "supertest": "^6.2.2", - "typescript": "^4.9.3" + "typescript": "^4.9.4" }, "engines": { "node": "^16.13.0 || ^18.12.0" diff --git a/packages/core/tsconfig.base.json b/packages/core/tsconfig.base.json index 890550d0c..75db7efcc 100644 --- a/packages/core/tsconfig.base.json +++ b/packages/core/tsconfig.base.json @@ -1,6 +1,8 @@ { "extends": "@silverhand/ts-config/tsconfig.base", "compilerOptions": { + "moduleResolution": "nodenext", + "module": "es2022", "declaration": false, "outDir": "build", "baseUrl": ".", @@ -8,8 +10,6 @@ "#src/*": [ "src/*" ] - }, - "moduleResolution": "nodenext", - "module": "es2022" + } } } diff --git a/packages/demo-app/package.json b/packages/demo-app/package.json index 2c5194f11..58104384a 100644 --- a/packages/demo-app/package.json +++ b/packages/demo-app/package.json @@ -41,7 +41,7 @@ "react-dom": "^18.0.0", "react-i18next": "^11.18.3", "stylelint": "^14.9.1", - "typescript": "^4.7.4", + "typescript": "^4.9.4", "zod": "^3.19.1" }, "engines": { diff --git a/packages/integration-tests/jest.config.js b/packages/integration-tests/jest.config.js new file mode 100644 index 000000000..d6e8961c6 --- /dev/null +++ b/packages/integration-tests/jest.config.js @@ -0,0 +1,12 @@ +/** @type {import('jest').Config} */ +const config = { + testPathIgnorePatterns: ['/node_modules/'], + setupFilesAfterEnv: ['./jest.setup.js'], + roots: ['./lib'], + moduleNameMapper: { + '^#src/(.*)\\.js(x)?$': '/lib/$1', + '^(chalk|inquirer)$': '/../shared/lib/esm/module-proxy.js', + }, +}; + +export default config; diff --git a/packages/integration-tests/jest.config.ts b/packages/integration-tests/jest.config.ts deleted file mode 100644 index 435ef3f33..000000000 --- a/packages/integration-tests/jest.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Config } from '@silverhand/jest-config'; -import { merge } from '@silverhand/jest-config'; - -const config: Config.InitialOptions = { - ...merge({ - setupFilesAfterEnv: ['/jest.setup.js'], - }), - // Will update common config soon - transformIgnorePatterns: ['node_modules/(?!(.*(nanoid|jose|ky|@logto))/)'], -}; - -export default config; diff --git a/packages/integration-tests/jest.config.ui.js b/packages/integration-tests/jest.config.ui.js new file mode 100644 index 000000000..dddbf5004 --- /dev/null +++ b/packages/integration-tests/jest.config.ui.js @@ -0,0 +1,10 @@ +/** @type {import('jest').Config} */ +const config = { + preset: 'jest-puppeteer', + moduleNameMapper: { + '^#src/(.*)\\.js(x)?$': '/lib/$1', + '^(chalk|inquirer)$': '/../shared/lib/esm/module-proxy.js', + }, +}; + +export default config; diff --git a/packages/integration-tests/jest.config.ui.ts b/packages/integration-tests/jest.config.ui.ts deleted file mode 100644 index 81fd637df..000000000 --- a/packages/integration-tests/jest.config.ui.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Config } from '@silverhand/jest-config'; -import { merge } from '@silverhand/jest-config'; - -const config: Config.InitialOptions = { - // Will treat as CJS - // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires, unicorn/prefer-module - ...merge(require('jest-puppeteer/jest-preset')), - // Will update common config soon - transformIgnorePatterns: ['node_modules/(?!(.*(nanoid|jose|ky|@logto))/)'], -}; - -export default config; diff --git a/packages/integration-tests/jest.setup.js b/packages/integration-tests/jest.setup.js index 89e48b851..f60a41079 100644 --- a/packages/integration-tests/jest.setup.js +++ b/packages/integration-tests/jest.setup.js @@ -2,10 +2,10 @@ // https://github.com/jsdom/jsdom/issues/1612 import { Crypto } from '@peculiar/webcrypto'; import dotenv from 'dotenv'; +import fetch from 'node-fetch'; import { TextDecoder, TextEncoder } from 'text-encoder'; -// eslint-disable-next-line unicorn/prefer-module -const fetch = require('node-fetch'); +const { jest } = import.meta; dotenv.config(); diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 01749cc2d..d11ff0f5b 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -5,10 +5,16 @@ "author": "Silverhand Inc. ", "license": "MPL-2.0", "private": true, + "type": "module", + "imports": { + "#src/*": "./lib/*" + }, "scripts": { - "test": "pnpm test:api && pnpm test:ui", - "test:api": "jest -i ./tests/api", - "test:ui": "jest -i --config=jest.config.ui.ts ./tests/ui", + "build": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap", + "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", + "test": "pnpm build && pnpm test:api && pnpm test:ui", + "test:api": "pnpm test:only -i ./lib/tests/api", + "test:ui": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/ui", "lint": "eslint --ext .ts src tests", "lint:report": "pnpm lint --format json --output-file report.json", "start": "pnpm test" @@ -28,7 +34,7 @@ "@types/node": "^16.0.0", "dotenv": "^16.0.0", "eslint": "^8.21.0", - "got": "^11.8.5", + "got": "^12.5.3", "jest": "^29.1.2", "jest-puppeteer": "^6.1.1", "node-fetch": "^2.6.7", @@ -38,7 +44,7 @@ "puppeteer": "^19.0.0", "text-encoder": "^0.0.4", "ts-node": "^10.9.1", - "typescript": "^4.7.4" + "typescript": "^4.9.4" }, "engines": { "node": "^16.13.0 || ^18.12.0" diff --git a/packages/integration-tests/src/api/admin-user.ts b/packages/integration-tests/src/api/admin-user.ts index 4c7cf72f9..05bcbe4bf 100644 --- a/packages/integration-tests/src/api/admin-user.ts +++ b/packages/integration-tests/src/api/admin-user.ts @@ -1,6 +1,6 @@ import type { User } from '@logto/schemas'; -import { authedAdminApi } from './api'; +import { authedAdminApi } from './api.js'; type CreateUserPayload = { primaryEmail?: string; diff --git a/packages/integration-tests/src/api/api.ts b/packages/integration-tests/src/api/api.ts index ae3eb2bcf..22736a479 100644 --- a/packages/integration-tests/src/api/api.ts +++ b/packages/integration-tests/src/api/api.ts @@ -1,6 +1,6 @@ -import got from 'got'; +import { got } from 'got'; -import { logtoUrl } from '@/constants'; +import { logtoUrl } from '#src/constants.js'; export default got.extend({ prefixUrl: new URL('/api', logtoUrl) }); diff --git a/packages/integration-tests/src/api/application.ts b/packages/integration-tests/src/api/application.ts index f441e143f..f251d25c0 100644 --- a/packages/integration-tests/src/api/application.ts +++ b/packages/integration-tests/src/api/application.ts @@ -5,7 +5,7 @@ import type { OidcClientMetadata, } from '@logto/schemas'; -import { authedAdminApi } from './api'; +import { authedAdminApi } from './api.js'; export const createApplication = (name: string, type: ApplicationType) => authedAdminApi diff --git a/packages/integration-tests/src/api/connector.ts b/packages/integration-tests/src/api/connector.ts index 388659776..aff29c381 100644 --- a/packages/integration-tests/src/api/connector.ts +++ b/packages/integration-tests/src/api/connector.ts @@ -1,6 +1,6 @@ import type { Connector, ConnectorResponse } from '@logto/schemas'; -import { authedAdminApi } from './api'; +import { authedAdminApi } from './api.js'; export const listConnectors = async () => authedAdminApi.get('connectors').json(); diff --git a/packages/integration-tests/src/api/dashboard.ts b/packages/integration-tests/src/api/dashboard.ts index 11e3b1dce..8835bed54 100644 --- a/packages/integration-tests/src/api/dashboard.ts +++ b/packages/integration-tests/src/api/dashboard.ts @@ -1,4 +1,4 @@ -import { authedAdminApi } from './api'; +import { authedAdminApi } from './api.js'; export type StatisticsData = { count: number; diff --git a/packages/integration-tests/src/api/index.ts b/packages/integration-tests/src/api/index.ts index 6283bed00..18b84987b 100644 --- a/packages/integration-tests/src/api/index.ts +++ b/packages/integration-tests/src/api/index.ts @@ -1,12 +1,12 @@ -export * from './resource'; -export * from './connector'; -export * from './application'; -export * from './sign-in-experience'; -export * from './admin-user'; -export * from './session'; -export * from './logs'; -export * from './dashboard'; -export * from './me'; -export * from './wellknown'; +export * from './resource.js'; +export * from './connector.js'; +export * from './application.js'; +export * from './sign-in-experience.js'; +export * from './admin-user.js'; +export * from './session.js'; +export * from './logs.js'; +export * from './dashboard.js'; +export * from './me.js'; +export * from './wellknown.js'; -export { default as api, authedAdminApi } from './api'; +export { default as api, authedAdminApi } from './api.js'; diff --git a/packages/integration-tests/src/api/logs.ts b/packages/integration-tests/src/api/logs.ts index 7382c860f..22c0469a4 100644 --- a/packages/integration-tests/src/api/logs.ts +++ b/packages/integration-tests/src/api/logs.ts @@ -1,6 +1,6 @@ import type { Log } from '@logto/schemas'; -import { authedAdminApi } from './api'; +import { authedAdminApi } from './api.js'; export const getLogs = () => authedAdminApi.get('logs').json(); diff --git a/packages/integration-tests/src/api/me.ts b/packages/integration-tests/src/api/me.ts index be4ce11f1..28862076c 100644 --- a/packages/integration-tests/src/api/me.ts +++ b/packages/integration-tests/src/api/me.ts @@ -1,6 +1,6 @@ import type { ArbitraryObject, UserInfo } from '@logto/schemas'; -import api from './api'; +import api from './api.js'; export const getCurrentUserInfo = (userId: string) => api.get(`me`, { headers: { 'development-user-id': userId } }).json(); diff --git a/packages/integration-tests/src/api/resource.ts b/packages/integration-tests/src/api/resource.ts index e60049a89..5f988ce0f 100644 --- a/packages/integration-tests/src/api/resource.ts +++ b/packages/integration-tests/src/api/resource.ts @@ -1,9 +1,9 @@ import type { Resource, CreateResource } from '@logto/schemas'; import type { OptionsOfTextResponseBody } from 'got'; -import { generateResourceIndicator, generateResourceName } from '@/utils'; +import { generateResourceIndicator, generateResourceName } from '#src/utils.js'; -import { authedAdminApi } from './api'; +import { authedAdminApi } from './api.js'; export const createResource = (name?: string, indicator?: string) => authedAdminApi diff --git a/packages/integration-tests/src/api/session.ts b/packages/integration-tests/src/api/session.ts index b650c645a..66485df35 100644 --- a/packages/integration-tests/src/api/session.ts +++ b/packages/integration-tests/src/api/session.ts @@ -1,6 +1,6 @@ import { PasscodeType } from '@logto/schemas'; -import api from './api'; +import api from './api.js'; type RedirectResponse = { redirectTo: string; diff --git a/packages/integration-tests/src/api/sign-in-experience.ts b/packages/integration-tests/src/api/sign-in-experience.ts index 9fda9d08b..231f3a76e 100644 --- a/packages/integration-tests/src/api/sign-in-experience.ts +++ b/packages/integration-tests/src/api/sign-in-experience.ts @@ -1,6 +1,6 @@ import type { SignInExperience } from '@logto/schemas'; -import { authedAdminApi } from './api'; +import { authedAdminApi } from './api.js'; export const getSignInExperience = () => authedAdminApi.get('sign-in-exp').json(); diff --git a/packages/integration-tests/src/api/wellknown.ts b/packages/integration-tests/src/api/wellknown.ts index eac1cf112..29d9aa114 100644 --- a/packages/integration-tests/src/api/wellknown.ts +++ b/packages/integration-tests/src/api/wellknown.ts @@ -1,6 +1,6 @@ import type { SignInExperience } from '@logto/schemas'; -import api from './api'; +import api from './api.js'; export const getWellKnownSignInExperience = (interactionCookie: string) => api diff --git a/packages/integration-tests/src/client/index.ts b/packages/integration-tests/src/client/index.ts index e5129e839..ea4e886ff 100644 --- a/packages/integration-tests/src/client/index.ts +++ b/packages/integration-tests/src/client/index.ts @@ -1,14 +1,14 @@ import type { LogtoConfig } from '@logto/node'; import LogtoClient from '@logto/node'; -import { demoAppApplicationId } from '@logto/schemas/lib/seeds'; +import { demoAppApplicationId } from '@logto/schemas/lib/seeds/index.js'; import { assert } from '@silverhand/essentials'; -import got from 'got'; +import { got } from 'got'; -import { consent } from '@/api'; -import { demoAppRedirectUri, logtoUrl } from '@/constants'; -import { extractCookie } from '@/utils'; +import { consent } from '#src/api/index.js'; +import { demoAppRedirectUri, logtoUrl } from '#src/constants.js'; +import { extractCookie } from '#src/utils.js'; -import { MemoryStorage } from './storage'; +import { MemoryStorage } from './storage.js'; export const defaultConfig = { endpoint: logtoUrl, diff --git a/packages/integration-tests/src/constants.ts b/packages/integration-tests/src/constants.ts index 1faf4cf1f..0d0b31222 100644 --- a/packages/integration-tests/src/constants.ts +++ b/packages/integration-tests/src/constants.ts @@ -1,8 +1,8 @@ import { SignInIdentifier } from '@logto/schemas'; import { demoAppApplicationId } from '@logto/schemas/lib/seeds'; -import { getEnv } from '@silverhand/essentials'; +import { assertEnv } from '@silverhand/essentials'; -export const logtoUrl = getEnv('INTEGRATION_TESTS_LOGTO_URL'); +export const logtoUrl = assertEnv('INTEGRATION_TESTS_LOGTO_URL'); export const discoveryUrl = `${logtoUrl}/oidc/.well-known/openid-configuration`; diff --git a/packages/integration-tests/src/helpers.ts b/packages/integration-tests/src/helpers.ts index 17ebe83b2..97f51848b 100644 --- a/packages/integration-tests/src/helpers.ts +++ b/packages/integration-tests/src/helpers.ts @@ -13,9 +13,9 @@ import { getAuthWithSocial, signInWithSocial, updateSignInExperience, -} from '@/api'; -import MockClient from '@/client'; -import { generateUsername, generatePassword } from '@/utils'; +} from '#src/api/index.js'; +import MockClient from '#src/client/index.js'; +import { generateUsername, generatePassword } from '#src/utils.js'; export const createUserByAdmin = (username?: string, password?: string, primaryEmail?: string) => { return createUser({ diff --git a/packages/integration-tests/src/include.d/openapi-schema-validator.d.ts b/packages/integration-tests/src/include.d/openapi-schema-validator.d.ts new file mode 100644 index 000000000..753c04b90 --- /dev/null +++ b/packages/integration-tests/src/include.d/openapi-schema-validator.d.ts @@ -0,0 +1,32 @@ +/** + * There's an issue for `"moduleResolution": "nodenext"`, thus we need to copy type definitions to here. + * See: https://github.com/microsoft/TypeScript/issues/47848 https://github.com/microsoft/TypeScript/issues/49189 + */ + +declare module 'openapi-schema-validator' { + import type { ErrorObject } from 'ajv'; + import type { IJsonSchema, OpenAPI } from 'openapi-types'; + + export interface IOpenAPISchemaValidator { + /** + * Validate the provided OpenAPI doc against this validator's schema version and + * return the results. + */ + validate(document: OpenAPI.Document): OpenAPISchemaValidatorResult; + } + export interface OpenAPISchemaValidatorArgs { + version: number | string; + extensions?: IJsonSchema; + } + export interface OpenAPISchemaValidatorResult { + errors: ErrorObject[]; + } + class OpenAPISchemaValidator implements IOpenAPISchemaValidator { + private readonly validator; + constructor(args: OpenAPISchemaValidatorArgs); + validate(openapiDocument: OpenAPI.Document): OpenAPISchemaValidatorResult; + } + + // eslint-disable-next-line import/no-anonymous-default-export + export default { default: OpenAPISchemaValidator }; +} diff --git a/packages/integration-tests/tests/api/admin-user.test.ts b/packages/integration-tests/src/tests/api/admin-user.test.ts similarity index 94% rename from packages/integration-tests/tests/api/admin-user.test.ts rename to packages/integration-tests/src/tests/api/admin-user.test.ts index 266ee2005..3c7898d17 100644 --- a/packages/integration-tests/tests/api/admin-user.test.ts +++ b/packages/integration-tests/src/tests/api/admin-user.test.ts @@ -4,7 +4,7 @@ import { mockSocialConnectorConfig, mockSocialConnectorId, mockSocialConnectorTarget, -} from '@/__mocks__/connectors-mock'; +} from '#src/__mocks__/connectors-mock.js'; import { getUser, getUsers, @@ -15,8 +15,8 @@ import { postConnector, updateConnectorConfig, deleteConnectorById, -} from '@/api'; -import { createUserByAdmin, bindSocialToNewCreatedUser } from '@/helpers'; +} from '#src/api/index.js'; +import { createUserByAdmin, bindSocialToNewCreatedUser } from '#src/helpers.js'; describe('admin console user management', () => { it('should create user successfully', async () => { diff --git a/packages/integration-tests/tests/api/application.test.ts b/packages/integration-tests/src/tests/api/application.test.ts similarity index 97% rename from packages/integration-tests/tests/api/application.test.ts rename to packages/integration-tests/src/tests/api/application.test.ts index 2f089639a..0b27fc79f 100644 --- a/packages/integration-tests/tests/api/application.test.ts +++ b/packages/integration-tests/src/tests/api/application.test.ts @@ -2,7 +2,7 @@ import { ApplicationType } from '@logto/schemas'; import { demoAppApplicationId } from '@logto/schemas/lib/seeds'; import { HTTPError } from 'got'; -import { createApplication, getApplication, updateApplication, deleteApplication } from '@/api'; +import { createApplication, getApplication, updateApplication, deleteApplication } from '#src/api/index.js'; describe('admin console application', () => { it('should get demo app details successfully', async () => { diff --git a/packages/integration-tests/tests/api/connector.test.ts b/packages/integration-tests/src/tests/api/connector.test.ts similarity index 98% rename from packages/integration-tests/tests/api/connector.test.ts rename to packages/integration-tests/src/tests/api/connector.test.ts index ec5d65aca..00c3d1dca 100644 --- a/packages/integration-tests/tests/api/connector.test.ts +++ b/packages/integration-tests/src/tests/api/connector.test.ts @@ -9,7 +9,7 @@ import { mockSocialConnectorId, mockStandardEmailConnectorConfig, mockStandardEmailConnectorId, -} from '@/__mocks__/connectors-mock'; +} from '#src/__mocks__/connectors-mock.js'; import { deleteConnectorById, getConnector, @@ -18,7 +18,7 @@ import { sendEmailTestMessage, sendSmsTestMessage, updateConnectorConfig, -} from '@/api/connector'; +} from '#src/api/connector.js'; const connectorIdMap = new Map(); diff --git a/packages/integration-tests/tests/api/dashboard.test.ts b/packages/integration-tests/src/tests/api/dashboard.test.ts similarity index 89% rename from packages/integration-tests/tests/api/dashboard.test.ts rename to packages/integration-tests/src/tests/api/dashboard.test.ts index 596757279..235abd3a5 100644 --- a/packages/integration-tests/tests/api/dashboard.test.ts +++ b/packages/integration-tests/src/tests/api/dashboard.test.ts @@ -1,8 +1,8 @@ -import type { StatisticsData } from '@/api'; -import { getTotalUsersCount, getNewUsersData, getActiveUsersData } from '@/api'; -import { signUpIdentifiers } from '@/constants'; -import { createUserByAdmin, registerNewUser, setSignUpIdentifier, signIn } from '@/helpers'; -import { generateUsername, generatePassword } from '@/utils'; +import type { StatisticsData } from '#src/api/index.js'; +import { getTotalUsersCount, getNewUsersData, getActiveUsersData } from '#src/api/index.js'; +import { signUpIdentifiers } from '#src/constants.js'; +import { createUserByAdmin, registerNewUser, setSignUpIdentifier, signIn } from '#src/helpers.js'; +import { generateUsername, generatePassword } from '#src/utils.js'; describe('admin console dashboard', () => { beforeAll(async () => { diff --git a/packages/integration-tests/tests/api/get-access-token.test.ts b/packages/integration-tests/src/tests/api/get-access-token.test.ts similarity index 89% rename from packages/integration-tests/tests/api/get-access-token.test.ts rename to packages/integration-tests/src/tests/api/get-access-token.test.ts index e635e4993..38dd7ba7f 100644 --- a/packages/integration-tests/tests/api/get-access-token.test.ts +++ b/packages/integration-tests/src/tests/api/get-access-token.test.ts @@ -5,11 +5,11 @@ import { managementResource } from '@logto/schemas/lib/seeds'; import { assert } from '@silverhand/essentials'; import fetch from 'node-fetch'; -import { signInWithPassword } from '@/api'; -import MockClient, { defaultConfig } from '@/client'; -import { logtoUrl } from '@/constants'; -import { createUserByAdmin } from '@/helpers'; -import { generateUsername, generatePassword } from '@/utils'; +import { signInWithPassword } from '#src/api/index.js'; +import MockClient, { defaultConfig } from '#src/client/index.js'; +import { logtoUrl } from '#src/constants.js'; +import { createUserByAdmin } from '#src/helpers.js'; +import { generateUsername, generatePassword } from '#src/utils.js'; describe('get access token', () => { const username = generateUsername(); diff --git a/packages/integration-tests/tests/api/health-check.test.ts b/packages/integration-tests/src/tests/api/health-check.test.ts similarity index 80% rename from packages/integration-tests/tests/api/health-check.test.ts rename to packages/integration-tests/src/tests/api/health-check.test.ts index 32415b430..1c44838eb 100644 --- a/packages/integration-tests/tests/api/health-check.test.ts +++ b/packages/integration-tests/src/tests/api/health-check.test.ts @@ -1,4 +1,4 @@ -import { api } from '@/api'; +import { api } from '#src/api/index.js'; describe('Health check', () => { it('should have a health state', async () => { diff --git a/packages/integration-tests/tests/api/logs.test.ts b/packages/integration-tests/src/tests/api/logs.test.ts similarity index 77% rename from packages/integration-tests/tests/api/logs.test.ts rename to packages/integration-tests/src/tests/api/logs.test.ts index c40a15837..efcf89141 100644 --- a/packages/integration-tests/tests/api/logs.test.ts +++ b/packages/integration-tests/src/tests/api/logs.test.ts @@ -1,9 +1,9 @@ import { assert } from '@silverhand/essentials'; -import { getLogs, getLog } from '@/api'; -import { signUpIdentifiers } from '@/constants'; -import { registerNewUser, setSignUpIdentifier } from '@/helpers'; -import { generateUsername, generatePassword } from '@/utils'; +import { getLogs, getLog } from '#src/api/index.js'; +import { signUpIdentifiers } from '#src/constants.js'; +import { registerNewUser, setSignUpIdentifier } from '#src/helpers.js'; +import { generateUsername, generatePassword } from '#src/utils.js'; describe('admin console logs', () => { const username = generateUsername(); diff --git a/packages/integration-tests/tests/api/resource.test.ts b/packages/integration-tests/src/tests/api/resource.test.ts similarity index 97% rename from packages/integration-tests/tests/api/resource.test.ts rename to packages/integration-tests/src/tests/api/resource.test.ts index 83492b726..2391f8dfe 100644 --- a/packages/integration-tests/tests/api/resource.test.ts +++ b/packages/integration-tests/src/tests/api/resource.test.ts @@ -1,8 +1,8 @@ import { managementResource } from '@logto/schemas/lib/seeds'; import { HTTPError } from 'got'; -import { createResource, getResource, updateResource, deleteResource } from '@/api'; -import { generateResourceIndicator, generateResourceName } from '@/utils'; +import { createResource, getResource, updateResource, deleteResource } from '#src/api/index.js'; +import { generateResourceIndicator, generateResourceName } from '#src/utils.js'; describe('admin console api resources', () => { it('should get management api resource details successfully', async () => { diff --git a/packages/integration-tests/tests/api/session.test.ts b/packages/integration-tests/src/tests/api/session.test.ts similarity index 97% rename from packages/integration-tests/tests/api/session.test.ts rename to packages/integration-tests/src/tests/api/session.test.ts index 4b96b54b2..a581a021b 100644 --- a/packages/integration-tests/tests/api/session.test.ts +++ b/packages/integration-tests/src/tests/api/session.test.ts @@ -7,7 +7,7 @@ import { mockEmailConnectorConfig, mockSmsConnectorId, mockSmsConnectorConfig, -} from '@/__mocks__/connectors-mock'; +} from '#src/__mocks__/connectors-mock.js'; import { sendRegisterUserWithEmailPasscode, verifyRegisterUserWithEmailPasscode, @@ -23,9 +23,9 @@ import { deleteConnectorById, postConnector, updateConnectorConfig, -} from '@/api'; -import MockClient from '@/client'; -import { signUpIdentifiers } from '@/constants'; +} from '#src/api/index.js'; +import MockClient from '#src/client/index.js'; +import { signUpIdentifiers } from '#src/constants.js'; import { registerNewUser, signIn, @@ -33,8 +33,8 @@ import { createUserByAdmin, setSignUpIdentifier, setSignInMethod, -} from '@/helpers'; -import { generateUsername, generatePassword, generateEmail, generatePhone } from '@/utils'; +} from '#src/helpers.js'; +import { generateUsername, generatePassword, generateEmail, generatePhone } from '#src/utils.js'; const connectorIdMap = new Map(); diff --git a/packages/integration-tests/tests/api/sign-in-experience.test.ts b/packages/integration-tests/src/tests/api/sign-in-experience.test.ts similarity index 92% rename from packages/integration-tests/tests/api/sign-in-experience.test.ts rename to packages/integration-tests/src/tests/api/sign-in-experience.test.ts index 5893f6faa..2ba6e9025 100644 --- a/packages/integration-tests/tests/api/sign-in-experience.test.ts +++ b/packages/integration-tests/src/tests/api/sign-in-experience.test.ts @@ -1,6 +1,6 @@ import { BrandingStyle } from '@logto/schemas'; -import { getSignInExperience, updateSignInExperience } from '@/api'; +import { getSignInExperience, updateSignInExperience } from '#src/api/index.js'; describe('admin console sign-in experience', () => { it('should get sign-in experience successfully', async () => { diff --git a/packages/integration-tests/tests/api/social-session.test.ts b/packages/integration-tests/src/tests/api/social-session.test.ts similarity index 93% rename from packages/integration-tests/tests/api/social-session.test.ts rename to packages/integration-tests/src/tests/api/social-session.test.ts index 4c5bf484b..35e198f90 100644 --- a/packages/integration-tests/tests/api/social-session.test.ts +++ b/packages/integration-tests/src/tests/api/social-session.test.ts @@ -5,7 +5,7 @@ import { mockSocialConnectorId, mockSocialConnectorTarget, mockSocialConnectorConfig, -} from '@/__mocks__/connectors-mock'; +} from '#src/__mocks__/connectors-mock.js'; import { signInWithSocial, getAuthWithSocial, @@ -16,11 +16,11 @@ import { postConnector, updateConnectorConfig, deleteConnectorById, -} from '@/api'; -import MockClient from '@/client'; -import { signUpIdentifiers } from '@/constants'; -import { createUserByAdmin, setSignUpIdentifier } from '@/helpers'; -import { generateUsername, generatePassword } from '@/utils'; +} from '#src/api/index.js'; +import MockClient from '#src/client/index.js'; +import { signUpIdentifiers } from '#src/constants.js'; +import { createUserByAdmin, setSignUpIdentifier } from '#src/helpers.js'; +import { generateUsername, generatePassword } from '#src/utils.js'; const state = 'foo_state'; const redirectUri = 'http://foo.dev/callback'; diff --git a/packages/integration-tests/tests/api/swagger-check.test.ts b/packages/integration-tests/src/tests/api/swagger-check.test.ts similarity index 80% rename from packages/integration-tests/tests/api/swagger-check.test.ts rename to packages/integration-tests/src/tests/api/swagger-check.test.ts index c816bf614..cb25c7d3a 100644 --- a/packages/integration-tests/tests/api/swagger-check.test.ts +++ b/packages/integration-tests/src/tests/api/swagger-check.test.ts @@ -1,7 +1,9 @@ -import OpenApiSchemaValidator from 'openapi-schema-validator'; +import Validator from 'openapi-schema-validator'; import type { OpenAPI } from 'openapi-types'; -import { api } from '@/api'; +import { api } from '#src/api/index.js'; + +const { default: OpenApiSchemaValidator } = Validator; describe('Swagger check', () => { it('should provide a valid swagger.json', async () => { diff --git a/packages/integration-tests/tests/api/wellknown.test.ts b/packages/integration-tests/src/tests/api/wellknown.test.ts similarity index 88% rename from packages/integration-tests/tests/api/wellknown.test.ts rename to packages/integration-tests/src/tests/api/wellknown.test.ts index c350116db..8001f303f 100644 --- a/packages/integration-tests/tests/api/wellknown.test.ts +++ b/packages/integration-tests/src/tests/api/wellknown.test.ts @@ -1,9 +1,9 @@ import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds'; import { assert } from '@silverhand/essentials'; -import { getWellKnownSignInExperience } from '@/api'; -import MockClient from '@/client'; -import { adminConsoleRedirectUri } from '@/constants'; +import { getWellKnownSignInExperience } from '#src/api/index.js'; +import MockClient from '#src/client/index.js'; +import { adminConsoleRedirectUri } from '#src/constants.js'; describe('wellknown api', () => { it('get /.well-known/sign-in-exp for AC', async () => { diff --git a/packages/integration-tests/tests/ui/smoke.test.ts b/packages/integration-tests/src/tests/ui/smoke.test.ts similarity index 81% rename from packages/integration-tests/tests/ui/smoke.test.ts rename to packages/integration-tests/src/tests/ui/smoke.test.ts index e41051b01..9aa405bb8 100644 --- a/packages/integration-tests/tests/ui/smoke.test.ts +++ b/packages/integration-tests/src/tests/ui/smoke.test.ts @@ -1,4 +1,4 @@ -import { logtoUrl } from '@/constants'; +import { logtoUrl } from '#src/constants.js'; describe('smoke testing', () => { it('opens with app element', async () => { diff --git a/packages/integration-tests/tsconfig.json b/packages/integration-tests/tsconfig.json index d978a7287..7cb9489e4 100644 --- a/packages/integration-tests/tsconfig.json +++ b/packages/integration-tests/tsconfig.json @@ -1,15 +1,17 @@ { "extends": "@silverhand/ts-config/tsconfig.base", "compilerOptions": { + "moduleResolution": "nodenext", + "module": "esnext", "isolatedModules": false, "allowJs": true, - "noEmit": true, + "outDir": "lib", "baseUrl": ".", "paths": { - "@/*": [ + "#src/*": [ "src/*" ] } }, - "include": ["tests", "src", "jest.*.ts", "jest.setup.js"] + "include": ["src"] } diff --git a/packages/phrases-ui/package.json b/packages/phrases-ui/package.json index 27d0084ef..7a5f208cc 100644 --- a/packages/phrases-ui/package.json +++ b/packages/phrases-ui/package.json @@ -43,7 +43,7 @@ "eslint": "^8.21.0", "lint-staged": "^13.0.0", "prettier": "^2.7.1", - "typescript": "^4.7.4" + "typescript": "^4.9.4" }, "engines": { "node": "^16.13.0 || ^18.12.0" diff --git a/packages/phrases/package.json b/packages/phrases/package.json index 5819bf24d..75e20b23a 100644 --- a/packages/phrases/package.json +++ b/packages/phrases/package.json @@ -43,7 +43,7 @@ "eslint": "^8.21.0", "lint-staged": "^13.0.0", "prettier": "^2.7.1", - "typescript": "^4.7.4" + "typescript": "^4.9.4" }, "eslintConfig": { "extends": "@silverhand" diff --git a/packages/schemas/generate.sh b/packages/schemas/generate.sh new file mode 100755 index 000000000..f20e26984 --- /dev/null +++ b/packages/schemas/generate.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +rm -rf lib/ +pnpm exec tsc -p tsconfig.build.gen.json +rm -rf src/db-entries +node lib/index.js +pnpm exec eslint src/db-entries/** --fix diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 8ccf7a232..a4b093f4b 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -17,7 +17,7 @@ "scripts": { "precommit": "lint-staged", "version": "./update-next.sh && git add alterations/", - "generate": "rm -rf src/db-entries && tsc -p tsconfig.build.gen.json && node lib/src/gen/index.js && eslint \"src/db-entries/**\" --fix", + "generate": "./generate.sh", "build:alterations": "rm -rf alterations-js && tsc -p tsconfig.build.alterations.json", "build": "pnpm generate && rm -rf lib/ && tsc -p tsconfig.build.json && pnpm build:alterations", "build:test": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap", @@ -48,7 +48,7 @@ "pluralize": "^8.0.0", "prettier": "^2.7.1", "slonik": "^30.0.0", - "typescript": "^4.7.4" + "typescript": "^4.9.4" }, "eslintConfig": { "extends": "@silverhand", diff --git a/packages/shared/package.json b/packages/shared/package.json index 740cf8aea..3a30ecc66 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -40,7 +40,7 @@ "jest": "^29.1.2", "lint-staged": "^13.0.0", "prettier": "^2.7.1", - "typescript": "^4.7.4" + "typescript": "^4.9.4" }, "engines": { "node": "^16.13.0 || ^18.12.0" diff --git a/packages/ui/package.json b/packages/ui/package.json index 03af5220d..552bea80c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -67,7 +67,7 @@ "react-timer-hook": "^3.0.5", "stylelint": "^14.9.1", "superstruct": "^0.16.0", - "typescript": "^4.7.4", + "typescript": "^4.9.4", "use-debounced-loader": "^0.1.1", "zod": "^3.19.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e86483aa..bd5b3409e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: '@commitlint/types': ^17.0.0 '@logto/cli': ^1.0.0-beta.10 husky: ^8.0.0 - typescript: ^4.7.4 + typescript: ^4.9.4 dependencies: '@logto/cli': link:packages/cli devDependencies: @@ -19,7 +19,7 @@ importers: '@commitlint/config-conventional': 17.0.0 '@commitlint/types': 17.0.0 husky: 8.0.1 - typescript: 4.7.4 + typescript: 4.9.4 packages/cli: specifiers: @@ -58,7 +58,7 @@ importers: slonik-interceptor-preset: ^1.2.10 slonik-sql-tag-raw: ^1.1.4 tar: ^6.1.11 - typescript: ^4.7.4 + typescript: ^4.9.4 yargs: ^17.6.0 zod: ^3.19.1 dependencies: @@ -84,8 +84,8 @@ importers: yargs: 17.6.0 zod: 3.19.1 devDependencies: - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni - '@silverhand/ts-config': 1.2.1_typescript@4.7.4 + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 '@types/fs-extra': 9.0.13 '@types/inquirer': 8.2.1 '@types/jest': 29.1.2 @@ -100,7 +100,7 @@ importers: prettier: 2.7.1 rimraf: 3.0.2 sinon: 15.0.0 - typescript: 4.7.4 + typescript: 4.9.4 packages/console: specifiers: @@ -173,7 +173,7 @@ importers: snake-case: ^3.0.4 stylelint: ^14.9.1 swr: ^1.3.0 - typescript: ^4.7.4 + typescript: ^4.9.4 zod: ^3.19.1 devDependencies: '@fontsource/roboto-mono': 4.5.7 @@ -188,11 +188,11 @@ importers: '@parcel/transformer-mdx': 2.8.0_smofjnbzoy3mcjjin2zix4yahi '@parcel/transformer-sass': 2.8.0_@parcel+core@2.8.0 '@parcel/transformer-svg-react': 2.8.0_@parcel+core@2.8.0 - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni - '@silverhand/eslint-config-react': 1.3.0_3jdvf2aalbcoibv3m53iflhmym + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa + '@silverhand/eslint-config-react': 1.3.0_pzm7kshjahdwz2kcmmatnemr54 '@silverhand/essentials': 1.3.0 - '@silverhand/ts-config': 1.2.1_typescript@4.7.4 - '@silverhand/ts-config-react': 1.2.1_typescript@4.7.4 + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 + '@silverhand/ts-config-react': 1.2.1_typescript@4.9.4 '@tsconfig/docusaurus': 1.0.5 '@types/color': 3.0.3 '@types/lodash.get': 4.4.7 @@ -245,7 +245,7 @@ importers: snake-case: 3.0.4 stylelint: 14.9.1 swr: 1.3.0_react@18.2.0 - typescript: 4.7.4 + typescript: 4.9.4 zod: 3.19.1 packages/core: @@ -323,7 +323,7 @@ importers: snake-case: ^3.0.4 snakecase-keys: ^5.4.4 supertest: ^6.2.2 - typescript: ^4.9.3 + typescript: ^4.9.4 zod: ^3.19.1 dependencies: '@logto/cli': link:../cli @@ -371,8 +371,8 @@ importers: snakecase-keys: 5.4.4 zod: 3.19.1 devDependencies: - '@silverhand/eslint-config': 1.3.0_xygfz6avl43ipur7dlp2av7gnm - '@silverhand/ts-config': 1.2.1_typescript@4.9.3 + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 '@types/debug': 4.1.7 '@types/etag': 1.8.1 '@types/fs-extra': 9.0.13 @@ -401,7 +401,7 @@ importers: prettier: 2.7.1 sinon: 15.0.0 supertest: 6.2.2 - typescript: 4.9.3 + typescript: 4.9.4 packages/create: specifiers: @@ -436,7 +436,7 @@ importers: react-dom: ^18.0.0 react-i18next: ^11.18.3 stylelint: ^14.9.1 - typescript: ^4.7.4 + typescript: ^4.9.4 zod: ^3.19.1 devDependencies: '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 @@ -446,10 +446,10 @@ importers: '@logto/schemas': link:../schemas '@parcel/core': 2.8.0 '@parcel/transformer-sass': 2.8.0_@parcel+core@2.8.0 - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni - '@silverhand/eslint-config-react': 1.3.0_qoomm4vc6ijs52fnjlal4yoenm - '@silverhand/ts-config': 1.2.1_typescript@4.7.4 - '@silverhand/ts-config-react': 1.2.1_typescript@4.7.4 + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa + '@silverhand/eslint-config-react': 1.3.0_hwxyoluj7tfktess7f4itjwcee + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 + '@silverhand/ts-config-react': 1.2.1_typescript@4.9.4 '@types/react': 18.0.15 '@types/react-dom': 18.0.6 cross-env: 7.0.3 @@ -464,7 +464,7 @@ importers: react-dom: 18.2.0_react@18.2.0 react-i18next: 11.18.3_shxxmfhtk2bc4pbx5cyq3uoph4 stylelint: 14.9.1 - typescript: 4.7.4 + typescript: 4.9.4 zod: 3.19.1 packages/integration-tests: @@ -483,7 +483,7 @@ importers: '@types/node': ^16.0.0 dotenv: ^16.0.0 eslint: ^8.21.0 - got: ^11.8.5 + got: ^12.5.3 jest: ^29.1.2 jest-puppeteer: ^6.1.1 node-fetch: ^2.6.7 @@ -493,23 +493,23 @@ importers: puppeteer: ^19.0.0 text-encoder: ^0.0.4 ts-node: ^10.9.1 - typescript: ^4.7.4 + typescript: ^4.9.4 devDependencies: '@jest/types': 29.1.2 '@logto/js': 1.0.0-beta.14 '@logto/node': 1.0.0-beta.14 '@logto/schemas': link:../schemas '@peculiar/webcrypto': 1.3.3 - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa '@silverhand/essentials': 1.3.0 - '@silverhand/jest-config': 1.2.2_zapogttls25djihwjkusccjjym - '@silverhand/ts-config': 1.2.1_typescript@4.7.4 + '@silverhand/jest-config': 1.2.2_ky6c64xxalg2hsll4xx3evq2dy + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 '@types/jest': 29.1.2 '@types/jest-environment-puppeteer': 5.0.2 '@types/node': 16.11.12 dotenv: 16.0.0 eslint: 8.21.0 - got: 11.8.5 + got: 12.5.3 jest: 29.1.2_k5ytkvaprncdyzidqqws5bqksq jest-puppeteer: 6.1.1_puppeteer@19.2.2 node-fetch: 2.6.7 @@ -518,8 +518,8 @@ importers: prettier: 2.7.1 puppeteer: 19.2.2 text-encoder: 0.0.4 - ts-node: 10.9.1_ccwudyfw5se7hgalwgkzhn2yp4 - typescript: 4.7.4 + ts-node: 10.9.1_ace2mtubvwruu4qt46fm3vtq3a + typescript: 4.9.4 packages/phrases: specifiers: @@ -531,7 +531,7 @@ importers: eslint: ^8.21.0 lint-staged: ^13.0.0 prettier: ^2.7.1 - typescript: ^4.7.4 + typescript: ^4.9.4 zod: ^3.19.1 dependencies: '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 @@ -539,12 +539,12 @@ importers: '@silverhand/essentials': 1.3.0 zod: 3.19.1 devDependencies: - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni - '@silverhand/ts-config': 1.2.1_typescript@4.7.4 + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 eslint: 8.21.0 lint-staged: 13.0.0 prettier: 2.7.1 - typescript: 4.7.4 + typescript: 4.9.4 packages/phrases-ui: specifiers: @@ -556,7 +556,7 @@ importers: eslint: ^8.21.0 lint-staged: ^13.0.0 prettier: ^2.7.1 - typescript: ^4.7.4 + typescript: ^4.9.4 zod: ^3.19.1 dependencies: '@logto/core-kit': 1.0.0-beta.28_zod@3.19.1 @@ -564,12 +564,12 @@ importers: '@silverhand/essentials': 1.3.0 zod: 3.19.1 devDependencies: - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni - '@silverhand/ts-config': 1.2.1_typescript@4.7.4 + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 eslint: 8.21.0 lint-staged: 13.0.0 prettier: 2.7.1 - typescript: 4.7.4 + typescript: 4.9.4 packages/schemas: specifiers: @@ -593,7 +593,7 @@ importers: pluralize: ^8.0.0 prettier: ^2.7.1 slonik: ^30.0.0 - typescript: ^4.7.4 + typescript: ^4.9.4 zod: ^3.19.1 dependencies: '@logto/connector-kit': 1.0.0-beta.28_zod@3.19.1 @@ -603,9 +603,9 @@ importers: '@logto/phrases-ui': link:../phrases-ui zod: 3.19.1 devDependencies: - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa '@silverhand/essentials': 1.3.0 - '@silverhand/ts-config': 1.2.1_typescript@4.7.4 + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 '@types/jest': 29.1.2 '@types/lodash.uniq': 4.5.6 '@types/node': 16.11.12 @@ -618,7 +618,7 @@ importers: pluralize: 8.0.0 prettier: 2.7.1 slonik: 30.1.2 - typescript: 4.7.4 + typescript: 4.9.4 packages/shared: specifiers: @@ -635,7 +635,7 @@ importers: nanoid: ^3.3.4 prettier: ^2.7.1 slonik: ^30.0.0 - typescript: ^4.7.4 + typescript: ^4.9.4 dependencies: '@logto/schemas': link:../schemas '@silverhand/essentials': 1.3.0 @@ -643,15 +643,15 @@ importers: nanoid: 3.3.4 slonik: 30.1.2 devDependencies: - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni - '@silverhand/ts-config': 1.2.1_typescript@4.7.4 + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 '@types/jest': 29.1.2 '@types/node': 16.11.12 eslint: 8.21.0 jest: 29.1.2_@types+node@16.11.12 lint-staged: 13.0.0 prettier: 2.7.1 - typescript: 4.7.4 + typescript: 4.9.4 packages/ui: specifiers: @@ -705,7 +705,7 @@ importers: react-timer-hook: ^3.0.5 stylelint: ^14.9.1 superstruct: ^0.16.0 - typescript: ^4.7.4 + typescript: ^4.9.4 use-debounced-loader: ^0.1.1 zod: ^3.19.1 devDependencies: @@ -718,12 +718,12 @@ importers: '@parcel/transformer-sass': 2.8.0_@parcel+core@2.8.0 '@parcel/transformer-svg-react': 2.8.0_@parcel+core@2.8.0 '@peculiar/webcrypto': 1.3.3 - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni - '@silverhand/eslint-config-react': 1.3.0_3jdvf2aalbcoibv3m53iflhmym + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa + '@silverhand/eslint-config-react': 1.3.0_pzm7kshjahdwz2kcmmatnemr54 '@silverhand/essentials': 1.3.0 - '@silverhand/jest-config': 1.2.2_zapogttls25djihwjkusccjjym - '@silverhand/ts-config': 1.2.1_typescript@4.7.4 - '@silverhand/ts-config-react': 1.2.1_typescript@4.7.4 + '@silverhand/jest-config': 1.2.2_ky6c64xxalg2hsll4xx3evq2dy + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 + '@silverhand/ts-config-react': 1.2.1_typescript@4.9.4 '@testing-library/react': 13.3.0_biqbaboplfbrettd7655fr4n2y '@types/color': 3.0.3 '@types/jest': 29.1.2 @@ -759,7 +759,7 @@ importers: react-timer-hook: 3.0.5_biqbaboplfbrettd7655fr4n2y stylelint: 14.9.1 superstruct: 0.16.0 - typescript: 4.7.4 + typescript: 4.9.4 use-debounced-loader: 0.1.1_react@18.2.0 zod: 3.19.1 @@ -1620,10 +1620,10 @@ packages: '@types/node': 17.0.23 chalk: 4.1.2 cosmiconfig: 7.0.1 - cosmiconfig-typescript-loader: 2.0.0_bjctuninx3nzqxltyvshqte2ni + cosmiconfig-typescript-loader: 2.0.0_73inix45wpcdjnppmmovzbfudu lodash: 4.17.21 resolve-from: 5.0.0 - typescript: 4.7.4 + typescript: 4.9.4 transitivePeerDependencies: - '@swc/core' - '@swc/wasm' @@ -2427,7 +2427,7 @@ packages: dependencies: '@logto/client': 1.0.0-beta.14 '@silverhand/essentials': 1.3.0 - js-base64: 3.7.2 + js-base64: 3.7.3 dev: true /@logto/client/1.0.0-beta.14: @@ -2437,7 +2437,7 @@ packages: '@logto/js': 1.0.0-beta.14 '@silverhand/essentials': 1.3.0 camelcase-keys: 7.0.2 - jose: 4.11.0 + jose: 4.11.1 lodash.get: 4.4.2 lodash.once: 4.1.1 dev: true @@ -2458,10 +2458,10 @@ packages: resolution: {integrity: sha512-seYvL/aGYRfO4d0FYfKIW/Cu9PnFMRpRM5/oRXwXbcbv+LY1a3TcAX0itrVXeBygIrxiAmWd9DL7CGIWzb48Qg==} engines: {node: ^16.0.0} dependencies: - '@logto/language-kit': 1.0.0-beta.28_zod@3.19.1 + '@logto/language-kit': 1.0.0-beta.29_zod@3.20.0 color: 4.2.3 nanoid: 3.3.4 - zod: 3.19.1 + zod: 3.20.0 dev: true /@logto/core-kit/1.0.0-beta.28_zod@3.19.1: @@ -2481,7 +2481,7 @@ packages: '@logto/core-kit': 1.0.0-beta.20 '@silverhand/essentials': 1.3.0 camelcase-keys: 7.0.2 - jose: 4.11.0 + jose: 4.11.1 lodash.get: 4.4.2 dev: true @@ -2493,12 +2493,21 @@ packages: dependencies: zod: 3.19.1 + /@logto/language-kit/1.0.0-beta.29_zod@3.20.0: + resolution: {integrity: sha512-+YeAkawjEq0vwwnqq8RDrDKePWE6x1q2WdpLXtj0H6SQ3GB9pXcYLecjnANHOHB4Zp9Jnxd4eBGogNRWvspikg==} + engines: {node: ^16.13.0 || ^18.12.0} + peerDependencies: + zod: ^3.19.1 + dependencies: + zod: 3.20.0 + dev: true + /@logto/node/1.0.0-beta.14: resolution: {integrity: sha512-+0S6lBBcG3pOmjEMRQnD+6X0MJ3V3E/4In59ckl/uVr/UgIufvOKWJwWCfsVKyguaO3QweJn19x7YkF8FyO31g==} dependencies: '@logto/client': 1.0.0-beta.14 '@silverhand/essentials': 1.3.0 - js-base64: 3.7.2 + js-base64: 3.7.3 node-fetch: 2.6.7 transitivePeerDependencies: - encoding @@ -3477,35 +3486,13 @@ packages: resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} dev: true - /@silverhand/eslint-config-react/1.3.0_3jdvf2aalbcoibv3m53iflhmym: + /@silverhand/eslint-config-react/1.3.0_hwxyoluj7tfktess7f4itjwcee: resolution: {integrity: sha512-L6tzayeKo1RXTadIHbZKUJWzi5Pu0tjg6eoTw5mz+3FTLu4zBXK0+/jK6FeHwQ3lWt8Cj3HDIMvVG97yjoblPA==} engines: {node: ^16.0.0 || ^18.0.0} peerDependencies: stylelint: ^14.9.1 dependencies: - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni - eslint-config-xo-react: 0.27.0_nlhz3yu2pbp43ngjgjnh6mfwge - eslint-plugin-jsx-a11y: 6.6.1_eslint@8.21.0 - eslint-plugin-react: 7.31.10_eslint@8.21.0 - eslint-plugin-react-hooks: 4.6.0_eslint@8.21.0 - stylelint: 14.9.1 - stylelint-config-xo-scss: 0.15.0_eqpuutlgonckfyjzwkrpusdvaa - transitivePeerDependencies: - - eslint - - eslint-import-resolver-webpack - - postcss - - prettier - - supports-color - - typescript - dev: true - - /@silverhand/eslint-config-react/1.3.0_qoomm4vc6ijs52fnjlal4yoenm: - resolution: {integrity: sha512-L6tzayeKo1RXTadIHbZKUJWzi5Pu0tjg6eoTw5mz+3FTLu4zBXK0+/jK6FeHwQ3lWt8Cj3HDIMvVG97yjoblPA==} - engines: {node: ^16.0.0 || ^18.0.0} - peerDependencies: - stylelint: ^14.9.1 - dependencies: - '@silverhand/eslint-config': 1.3.0_swk2g7ygmfleszo5c33j4vooni + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa eslint-config-xo-react: 0.27.0_nlhz3yu2pbp43ngjgjnh6mfwge eslint-plugin-jsx-a11y: 6.6.1_eslint@8.21.0 eslint-plugin-react: 7.31.10_eslint@8.21.0 @@ -3521,39 +3508,29 @@ packages: - typescript dev: true - /@silverhand/eslint-config/1.3.0_swk2g7ygmfleszo5c33j4vooni: - resolution: {integrity: sha512-0+SXJXAkUe1pg2DNn3JCEo99Weev07chQsL2iSCramXeMKjEk1R1UKjgQJM9saUGF7ovY4hlE/JjFD3PFId4DQ==} + /@silverhand/eslint-config-react/1.3.0_pzm7kshjahdwz2kcmmatnemr54: + resolution: {integrity: sha512-L6tzayeKo1RXTadIHbZKUJWzi5Pu0tjg6eoTw5mz+3FTLu4zBXK0+/jK6FeHwQ3lWt8Cj3HDIMvVG97yjoblPA==} engines: {node: ^16.0.0 || ^18.0.0} peerDependencies: - eslint: ^8.21.0 - prettier: ^2.7.1 + stylelint: ^14.9.1 dependencies: - '@silverhand/eslint-plugin-fp': 2.5.0_eslint@8.21.0 - '@typescript-eslint/eslint-plugin': 5.40.0_bomoubwgcm5gub6ncofkqpat4u - '@typescript-eslint/parser': 5.40.0_qugx7qdu5zevzvxaiqyxfiwquq - eslint: 8.21.0 - eslint-config-prettier: 8.5.0_eslint@8.21.0 - eslint-config-xo: 0.42.0_eslint@8.21.0 - eslint-config-xo-typescript: 0.53.0_4y2fwmtil766jscqcpwrpkqfje - eslint-import-resolver-typescript: 3.5.1_jatgrcxl4x7ywe7ak6cnjca2ae - eslint-plugin-consistent-default-export-name: 0.0.15 - eslint-plugin-eslint-comments: 3.2.0_eslint@8.21.0 - eslint-plugin-import: 2.26.0_7tkpoacjify653e7qftl64vwym - eslint-plugin-no-use-extend-native: 0.5.0 - eslint-plugin-node: 11.1.0_eslint@8.21.0 - eslint-plugin-prettier: 4.2.1_h62lvancfh4b7r6zn2dgodrh5e - eslint-plugin-promise: 6.1.0_eslint@8.21.0 - eslint-plugin-sql: 2.1.0_eslint@8.21.0 - eslint-plugin-unicorn: 43.0.2_eslint@8.21.0 - eslint-plugin-unused-imports: 2.0.0_kjyxfvacupbf4yx7sz4dzjz4we - prettier: 2.7.1 + '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa + eslint-config-xo-react: 0.27.0_nlhz3yu2pbp43ngjgjnh6mfwge + eslint-plugin-jsx-a11y: 6.6.1_eslint@8.21.0 + eslint-plugin-react: 7.31.10_eslint@8.21.0 + eslint-plugin-react-hooks: 4.6.0_eslint@8.21.0 + stylelint: 14.9.1 + stylelint-config-xo-scss: 0.15.0_eqpuutlgonckfyjzwkrpusdvaa transitivePeerDependencies: + - eslint - eslint-import-resolver-webpack + - postcss + - prettier - supports-color - typescript dev: true - /@silverhand/eslint-config/1.3.0_xygfz6avl43ipur7dlp2av7gnm: + /@silverhand/eslint-config/1.3.0_eu7dlo3qq5moigliolva3udaxa: resolution: {integrity: sha512-0+SXJXAkUe1pg2DNn3JCEo99Weev07chQsL2iSCramXeMKjEk1R1UKjgQJM9saUGF7ovY4hlE/JjFD3PFId4DQ==} engines: {node: ^16.0.0 || ^18.0.0} peerDependencies: @@ -3561,12 +3538,12 @@ packages: prettier: ^2.7.1 dependencies: '@silverhand/eslint-plugin-fp': 2.5.0_eslint@8.21.0 - '@typescript-eslint/eslint-plugin': 5.40.0_ujnp3qqzcos2fcjl53ed5mxtmq - '@typescript-eslint/parser': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i + '@typescript-eslint/eslint-plugin': 5.40.0_ryuuuwjgkm45eys4ms27dfx6xm + '@typescript-eslint/parser': 5.40.0_ggwlz5rjjuds5feuls6rqqovzi eslint: 8.21.0 eslint-config-prettier: 8.5.0_eslint@8.21.0 eslint-config-xo: 0.42.0_eslint@8.21.0 - eslint-config-xo-typescript: 0.53.0_6262kjopfp2ssqpmwkpdbrlzgu + eslint-config-xo-typescript: 0.53.0_cqx3bgaw6gtoyrp3gyom2ucbru eslint-import-resolver-typescript: 3.5.1_jatgrcxl4x7ywe7ak6cnjca2ae eslint-plugin-consistent-default-export-name: 0.0.15 eslint-plugin-eslint-comments: 3.2.0_eslint@8.21.0 @@ -3605,7 +3582,7 @@ packages: lodash.orderby: 4.6.0 lodash.pick: 4.4.0 - /@silverhand/jest-config/1.2.2_zapogttls25djihwjkusccjjym: + /@silverhand/jest-config/1.2.2_ky6c64xxalg2hsll4xx3evq2dy: resolution: {integrity: sha512-sCOIHN3kIG9nyySkDao8nz6HK8VhGoUV4WG1CCriDDeGTqbHs4IprzTp1p+ChFdC8JGBCElQC0cIFrWYTFnTAQ==} engines: {node: ^16.0.0 || ^18.0.0} peerDependencies: @@ -3617,7 +3594,7 @@ packages: jest: 29.1.2_k5ytkvaprncdyzidqqws5bqksq jest-matcher-specific-error: 1.0.0 jest-transform-stub: 2.0.0 - ts-jest: 29.0.3_37jxomqt5oevoqzq6g3r6n3ili + ts-jest: 29.0.3_5xcodqox2j6ogkdcajmxw2vjdu transitivePeerDependencies: - '@babel/core' - babel-jest @@ -3625,32 +3602,23 @@ packages: - typescript dev: true - /@silverhand/ts-config-react/1.2.1_typescript@4.7.4: + /@silverhand/ts-config-react/1.2.1_typescript@4.9.4: resolution: {integrity: sha512-40BYg5gqzThCmXw+SJXnlWvSUWpFKsdfVHlguJXgdB1l8O6Yqe1jcwjHrNC/yBy8jgLInhLXuaFs86/p1g0m+Q==} engines: {node: ^16.0.0 || ^18.0.0} peerDependencies: typescript: ^4.7.4 dependencies: - '@silverhand/ts-config': 1.2.1_typescript@4.7.4 - typescript: 4.7.4 + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 + typescript: 4.9.4 dev: true - /@silverhand/ts-config/1.2.1_typescript@4.7.4: + /@silverhand/ts-config/1.2.1_typescript@4.9.4: resolution: {integrity: sha512-Lm5Ydb45qKmXvlOfQfSb+1WHrdL5IBtzt+AMOR5h528H073FLzaazLiaDo4noBVT9PAVtO7kG9qjwSPzHf0k9Q==} engines: {node: ^16.0.0 || ^18.0.0} peerDependencies: typescript: ^4.7.4 dependencies: - typescript: 4.7.4 - dev: true - - /@silverhand/ts-config/1.2.1_typescript@4.9.3: - resolution: {integrity: sha512-Lm5Ydb45qKmXvlOfQfSb+1WHrdL5IBtzt+AMOR5h528H073FLzaazLiaDo4noBVT9PAVtO7kG9qjwSPzHf0k9Q==} - engines: {node: ^16.0.0 || ^18.0.0} - peerDependencies: - typescript: ^4.7.4 - dependencies: - typescript: 4.9.3 + typescript: 4.9.4 dev: true /@sinclair/typebox/0.24.46: @@ -3660,11 +3628,11 @@ packages: /@sindresorhus/is/4.2.0: resolution: {integrity: sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==} engines: {node: '>=10'} + dev: false /@sindresorhus/is/5.3.0: resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} engines: {node: '>=14.16'} - dev: false /@sinonjs/commons/1.8.3: resolution: {integrity: sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==} @@ -3856,13 +3824,13 @@ packages: engines: {node: '>=10'} dependencies: defer-to-connect: 2.0.1 + dev: false /@szmarczak/http-timer/5.0.1: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} dependencies: defer-to-connect: 2.0.1 - dev: false /@testing-library/dom/8.11.1: resolution: {integrity: sha512-3KQDyx9r0RKYailW2MiYrSSKEfH0GTkI51UGEvJenvcoDoeRYs0PZpi2SXqtnMClQvCqdtTTpOfFETDTVADpAg==} @@ -3979,6 +3947,7 @@ packages: '@types/keyv': 3.1.3 '@types/node': 17.0.23 '@types/responselike': 1.0.0 + dev: false /@types/color-convert/2.0.0: resolution: {integrity: sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==} @@ -4163,6 +4132,7 @@ packages: resolution: {integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==} dependencies: '@types/node': 17.0.23 + dev: false /@types/koa-compose/3.2.5: resolution: {integrity: sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==} @@ -4360,6 +4330,7 @@ packages: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: '@types/node': 17.0.23 + dev: false /@types/retry/0.12.1: resolution: {integrity: sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==} @@ -4460,7 +4431,7 @@ packages: dev: true optional: true - /@typescript-eslint/eslint-plugin/5.40.0_bomoubwgcm5gub6ncofkqpat4u: + /@typescript-eslint/eslint-plugin/5.40.0_ryuuuwjgkm45eys4ms27dfx6xm: resolution: {integrity: sha512-FIBZgS3DVJgqPwJzvZTuH4HNsZhHMa9SjxTKAZTlMsPw/UzpEjcf9f4dfgDJEHjK+HboUJo123Eshl6niwEm/Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4471,48 +4442,22 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.40.0_qugx7qdu5zevzvxaiqyxfiwquq + '@typescript-eslint/parser': 5.40.0_ggwlz5rjjuds5feuls6rqqovzi '@typescript-eslint/scope-manager': 5.40.0 - '@typescript-eslint/type-utils': 5.40.0_qugx7qdu5zevzvxaiqyxfiwquq - '@typescript-eslint/utils': 5.40.0_qugx7qdu5zevzvxaiqyxfiwquq + '@typescript-eslint/type-utils': 5.40.0_ggwlz5rjjuds5feuls6rqqovzi + '@typescript-eslint/utils': 5.40.0_ggwlz5rjjuds5feuls6rqqovzi debug: 4.3.4 eslint: 8.21.0 ignore: 5.2.0 regexpp: 3.2.0 semver: 7.3.8 - tsutils: 3.21.0_typescript@4.7.4 - typescript: 4.7.4 + tsutils: 3.21.0_typescript@4.9.4 + typescript: 4.9.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/eslint-plugin/5.40.0_ujnp3qqzcos2fcjl53ed5mxtmq: - resolution: {integrity: sha512-FIBZgS3DVJgqPwJzvZTuH4HNsZhHMa9SjxTKAZTlMsPw/UzpEjcf9f4dfgDJEHjK+HboUJo123Eshl6niwEm/Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/parser': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i - '@typescript-eslint/scope-manager': 5.40.0 - '@typescript-eslint/type-utils': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i - '@typescript-eslint/utils': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i - debug: 4.3.4 - eslint: 8.21.0 - ignore: 5.2.0 - regexpp: 3.2.0 - semver: 7.3.8 - tsutils: 3.21.0_typescript@4.9.3 - typescript: 4.9.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/parser/5.40.0_4he5nxxgrmu5gxjroamasnmd3i: + /@typescript-eslint/parser/5.40.0_ggwlz5rjjuds5feuls6rqqovzi: resolution: {integrity: sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4524,30 +4469,10 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.40.0 '@typescript-eslint/types': 5.40.0 - '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.9.3 + '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.9.4 debug: 4.3.4 eslint: 8.21.0 - typescript: 4.9.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/parser/5.40.0_qugx7qdu5zevzvxaiqyxfiwquq: - resolution: {integrity: sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 5.40.0 - '@typescript-eslint/types': 5.40.0 - '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.7.4 - debug: 4.3.4 - eslint: 8.21.0 - typescript: 4.7.4 + typescript: 4.9.4 transitivePeerDependencies: - supports-color dev: true @@ -4560,7 +4485,7 @@ packages: '@typescript-eslint/visitor-keys': 5.40.0 dev: true - /@typescript-eslint/type-utils/5.40.0_4he5nxxgrmu5gxjroamasnmd3i: + /@typescript-eslint/type-utils/5.40.0_ggwlz5rjjuds5feuls6rqqovzi: resolution: {integrity: sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4570,32 +4495,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.9.3 - '@typescript-eslint/utils': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i + '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.9.4 + '@typescript-eslint/utils': 5.40.0_ggwlz5rjjuds5feuls6rqqovzi debug: 4.3.4 eslint: 8.21.0 - tsutils: 3.21.0_typescript@4.9.3 - typescript: 4.9.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/type-utils/5.40.0_qugx7qdu5zevzvxaiqyxfiwquq: - resolution: {integrity: sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.7.4 - '@typescript-eslint/utils': 5.40.0_qugx7qdu5zevzvxaiqyxfiwquq - debug: 4.3.4 - eslint: 8.21.0 - tsutils: 3.21.0_typescript@4.7.4 - typescript: 4.7.4 + tsutils: 3.21.0_typescript@4.9.4 + typescript: 4.9.4 transitivePeerDependencies: - supports-color dev: true @@ -4605,7 +4510,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree/5.40.0_typescript@4.7.4: + /@typescript-eslint/typescript-estree/5.40.0_typescript@4.9.4: resolution: {integrity: sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4620,34 +4525,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 - tsutils: 3.21.0_typescript@4.7.4 - typescript: 4.7.4 + tsutils: 3.21.0_typescript@4.9.4 + typescript: 4.9.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/typescript-estree/5.40.0_typescript@4.9.3: - resolution: {integrity: sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 5.40.0 - '@typescript-eslint/visitor-keys': 5.40.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.3.8 - tsutils: 3.21.0_typescript@4.9.3 - typescript: 4.9.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/utils/5.40.0_4he5nxxgrmu5gxjroamasnmd3i: + /@typescript-eslint/utils/5.40.0_ggwlz5rjjuds5feuls6rqqovzi: resolution: {integrity: sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4656,26 +4540,7 @@ packages: '@types/json-schema': 7.0.11 '@typescript-eslint/scope-manager': 5.40.0 '@typescript-eslint/types': 5.40.0 - '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.9.3 - eslint: 8.21.0 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.21.0 - semver: 7.3.8 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/utils/5.40.0_qugx7qdu5zevzvxaiqyxfiwquq: - resolution: {integrity: sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@types/json-schema': 7.0.11 - '@typescript-eslint/scope-manager': 5.40.0 - '@typescript-eslint/types': 5.40.0 - '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.7.4 + '@typescript-eslint/typescript-estree': 5.40.0_typescript@4.9.4 eslint: 8.21.0 eslint-scope: 5.1.1 eslint-utils: 3.0.0_eslint@8.21.0 @@ -5289,6 +5154,7 @@ packages: /cacheable-lookup/5.0.4: resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} engines: {node: '>=10.6.0'} + dev: false /cacheable-lookup/6.0.4: resolution: {integrity: sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A==} @@ -5298,7 +5164,6 @@ packages: /cacheable-lookup/7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} - dev: false /cacheable-request/10.2.2: resolution: {integrity: sha512-KxjQZM3UIo7/J6W4sLpwFvu1GB3Whv8NtZ8ZrUL284eiQjiXeeqWTdhixNrp/NLZ/JNuFBo6BD4ZaO8ZJ5BN8Q==} @@ -5311,7 +5176,6 @@ packages: mimic-response: 4.0.0 normalize-url: 7.2.0 responselike: 3.0.0 - dev: false /cacheable-request/7.0.2: resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==} @@ -5324,6 +5188,7 @@ packages: lowercase-keys: 2.0.0 normalize-url: 6.1.0 responselike: 2.0.0 + dev: false /call-bind/1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} @@ -5585,6 +5450,7 @@ packages: resolution: {integrity: sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==} dependencies: mimic-response: 1.0.1 + dev: false /clone/1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} @@ -5803,7 +5669,7 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: true - /cosmiconfig-typescript-loader/2.0.0_bjctuninx3nzqxltyvshqte2ni: + /cosmiconfig-typescript-loader/2.0.0_73inix45wpcdjnppmmovzbfudu: resolution: {integrity: sha512-2NlGul/E3vTQEANqPziqkA01vfiuUU8vT0jZAuUIjEW8u3eCcnCQWLggapCjhbF76s7KQF0fM0kXSKmzaDaG1g==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -5812,8 +5678,8 @@ packages: dependencies: '@types/node': 17.0.23 cosmiconfig: 7.0.1 - ts-node: 10.7.0_bjctuninx3nzqxltyvshqte2ni - typescript: 4.7.4 + ts-node: 10.7.0_73inix45wpcdjnppmmovzbfudu + typescript: 4.9.4 transitivePeerDependencies: - '@swc/core' - '@swc/wasm' @@ -6602,7 +6468,7 @@ packages: eslint-plugin-react-hooks: 4.6.0_eslint@8.21.0 dev: true - /eslint-config-xo-typescript/0.53.0_4y2fwmtil766jscqcpwrpkqfje: + /eslint-config-xo-typescript/0.53.0_cqx3bgaw6gtoyrp3gyom2ucbru: resolution: {integrity: sha512-IJ1n70egMPTou/41HoGGFbLf/2WCsVW5lSUxOSklrR8T1221fMRPVJxIVZ3evr8R+N5wR6uzg/0uzSymwWA5Bg==} engines: {node: '>=12'} peerDependencies: @@ -6611,25 +6477,10 @@ packages: eslint: '>=8.0.0' typescript: '>=4.4' dependencies: - '@typescript-eslint/eslint-plugin': 5.40.0_bomoubwgcm5gub6ncofkqpat4u - '@typescript-eslint/parser': 5.40.0_qugx7qdu5zevzvxaiqyxfiwquq + '@typescript-eslint/eslint-plugin': 5.40.0_ryuuuwjgkm45eys4ms27dfx6xm + '@typescript-eslint/parser': 5.40.0_ggwlz5rjjuds5feuls6rqqovzi eslint: 8.21.0 - typescript: 4.7.4 - dev: true - - /eslint-config-xo-typescript/0.53.0_6262kjopfp2ssqpmwkpdbrlzgu: - resolution: {integrity: sha512-IJ1n70egMPTou/41HoGGFbLf/2WCsVW5lSUxOSklrR8T1221fMRPVJxIVZ3evr8R+N5wR6uzg/0uzSymwWA5Bg==} - engines: {node: '>=12'} - peerDependencies: - '@typescript-eslint/eslint-plugin': '>=5.31.0' - '@typescript-eslint/parser': '>=5.31.0' - eslint: '>=8.0.0' - typescript: '>=4.4' - dependencies: - '@typescript-eslint/eslint-plugin': 5.40.0_ujnp3qqzcos2fcjl53ed5mxtmq - '@typescript-eslint/parser': 5.40.0_4he5nxxgrmu5gxjroamasnmd3i - eslint: 8.21.0 - typescript: 4.9.3 + typescript: 4.9.4 dev: true /eslint-config-xo/0.42.0_eslint@8.21.0: @@ -6692,7 +6543,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.40.0_qugx7qdu5zevzvxaiqyxfiwquq + '@typescript-eslint/parser': 5.40.0_ggwlz5rjjuds5feuls6rqqovzi debug: 3.2.7 eslint: 8.21.0 eslint-import-resolver-node: 0.3.6 @@ -6741,7 +6592,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.40.0_qugx7qdu5zevzvxaiqyxfiwquq + '@typescript-eslint/parser': 5.40.0_ggwlz5rjjuds5feuls6rqqovzi array-includes: 3.1.5 array.prototype.flat: 1.3.0 debug: 2.6.9 @@ -6916,7 +6767,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.40.0_bomoubwgcm5gub6ncofkqpat4u + '@typescript-eslint/eslint-plugin': 5.40.0_ryuuuwjgkm45eys4ms27dfx6xm eslint: 8.21.0 eslint-rule-composer: 0.3.0 dev: true @@ -7403,7 +7254,6 @@ packages: /form-data-encoder/2.1.3: resolution: {integrity: sha512-KqU0nnPMgIJcCOFTNJFEA8epcseEaoox4XZffTgy8jlI6pL/5EFyR54NRG7CnCJN0biY7q52DO3MH6/sJ/TKlQ==} engines: {node: '>= 14.17'} - dev: false /form-data/4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} @@ -7749,6 +7599,7 @@ packages: lowercase-keys: 2.0.0 p-cancelable: 2.1.1 responselike: 2.0.0 + dev: false /got/12.5.3: resolution: {integrity: sha512-8wKnb9MGU8IPGRIo+/ukTy9XLJBwDiCpIf5TVzQ9Cpol50eMTpBq2GAuDsuDIz7hTYmZgMgC1e9ydr6kSDWs3w==} @@ -7765,10 +7616,10 @@ packages: lowercase-keys: 3.0.0 p-cancelable: 3.0.0 responselike: 3.0.0 - dev: false /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: true /graceful-fs/4.2.9: resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==} @@ -8118,6 +7969,7 @@ packages: dependencies: quick-lru: 5.1.1 resolve-alpn: 1.2.1 + dev: false /http2-wrapper/2.2.0: resolution: {integrity: sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==} @@ -8125,7 +7977,6 @@ packages: dependencies: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - dev: false /https-proxy-agent/5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} @@ -9072,7 +8923,7 @@ packages: pretty-format: 29.2.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1_ccwudyfw5se7hgalwgkzhn2yp4 + ts-node: 10.9.1_ace2mtubvwruu4qt46fm3vtq3a transitivePeerDependencies: - supports-color dev: true @@ -9112,7 +8963,7 @@ packages: pretty-format: 29.2.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1_ccwudyfw5se7hgalwgkzhn2yp4 + ts-node: 10.9.1_ace2mtubvwruu4qt46fm3vtq3a transitivePeerDependencies: - supports-color dev: true @@ -10006,10 +9857,18 @@ packages: /jose/4.11.0: resolution: {integrity: sha512-wLe+lJHeG8Xt6uEubS4x0LVjS/3kXXu9dGoj9BNnlhYq7Kts0Pbb2pvv5KiI0yaKH/eaiR0LUOBhOVo9ktd05A==} + /jose/4.11.1: + resolution: {integrity: sha512-YRv4Tk/Wlug8qicwqFNFVEZSdbROCHRAC6qu/i0dyNKr5JQdoa2pIGoS04lLO/jXQX7Z9omoNewYIVIxqZBd9Q==} + dev: true + /js-base64/3.7.2: resolution: {integrity: sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==} dev: true + /js-base64/3.7.3: + resolution: {integrity: sha512-PAr6Xg2jvd7MCR6Ld9Jg3BmTcjYsHEBx1VlwEwULb/qowPf5VD9kEMagj23Gm7JRnSvE/Da/57nChZjnvL8v6A==} + dev: true + /js-tokens/4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -10130,7 +9989,7 @@ packages: dependencies: universalify: 2.0.0 optionalDependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.9 /jsonparse/1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} @@ -10160,12 +10019,12 @@ packages: resolution: {integrity: sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg==} dependencies: json-buffer: 3.0.1 + dev: false /keyv/4.5.2: resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==} dependencies: json-buffer: 3.0.1 - dev: false /kind-of/2.0.1: resolution: {integrity: sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==} @@ -10649,11 +10508,11 @@ packages: /lowercase-keys/2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} + dev: false /lowercase-keys/3.0.0: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false /lowlight/1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} @@ -11237,6 +11096,7 @@ packages: /mimic-response/1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} + dev: false /mimic-response/3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} @@ -11245,7 +11105,6 @@ packages: /mimic-response/4.0.0: resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false /min-indent/1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} @@ -11503,11 +11362,11 @@ packages: /normalize-url/6.1.0: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} + dev: false /normalize-url/7.2.0: resolution: {integrity: sha512-uhXOdZry0L6M2UIo9BTt7FdpBDiAGN/7oItedQwPKh8jh31ZlvC8U9Xl/EJ3aijDHaywXTW3QbZ6LuCocur1YA==} engines: {node: '>=12.20'} - dev: false /npm-run-path/4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} @@ -11761,11 +11620,11 @@ packages: /p-cancelable/2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} + dev: false /p-cancelable/3.0.0: resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} engines: {node: '>=12.20'} - dev: false /p-defer/3.0.0: resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==} @@ -13283,13 +13142,13 @@ packages: resolution: {integrity: sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==} dependencies: lowercase-keys: 2.0.0 + dev: false /responselike/3.0.0: resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} engines: {node: '>=14.16'} dependencies: lowercase-keys: 3.0.0 - dev: false /restore-cursor/3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} @@ -14493,7 +14352,7 @@ packages: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} dev: true - /ts-jest/29.0.3_37jxomqt5oevoqzq6g3r6n3ili: + /ts-jest/29.0.3_5xcodqox2j6ogkdcajmxw2vjdu: resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -14523,11 +14382,11 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.3.8 - typescript: 4.7.4 + typescript: 4.9.4 yargs-parser: 21.1.1 dev: true - /ts-node/10.7.0_bjctuninx3nzqxltyvshqte2ni: + /ts-node/10.7.0_73inix45wpcdjnppmmovzbfudu: resolution: {integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==} hasBin: true peerDependencies: @@ -14553,12 +14412,12 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.7.4 + typescript: 4.9.4 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true - /ts-node/10.9.1_ccwudyfw5se7hgalwgkzhn2yp4: + /ts-node/10.9.1_ace2mtubvwruu4qt46fm3vtq3a: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -14584,7 +14443,7 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.7.4 + typescript: 4.9.4 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true @@ -14618,24 +14477,14 @@ packages: engines: {node: '>=0.6.x'} dev: false - /tsutils/3.21.0_typescript@4.7.4: + /tsutils/3.21.0_typescript@4.9.4: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 4.7.4 - dev: true - - /tsutils/3.21.0_typescript@4.9.3: - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - dependencies: - tslib: 1.14.1 - typescript: 4.9.3 + typescript: 4.9.4 dev: true /tty-table/4.1.6: @@ -14723,14 +14572,8 @@ packages: /typedarray/0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - /typescript/4.7.4: - resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} - engines: {node: '>=4.2.0'} - hasBin: true - dev: true - - /typescript/4.9.3: - resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} + /typescript/4.9.4: + resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==} engines: {node: '>=4.2.0'} hasBin: true dev: true @@ -15384,6 +15227,10 @@ packages: /zod/3.19.1: resolution: {integrity: sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==} + /zod/3.20.0: + resolution: {integrity: sha512-ZWxs7oM5ixoo1BMoxTNeDMYSih/F/FUnExsnRtHT04rG6q0Bd74TKS45RGXw07TOalOZyyzdKaYH38k8yTEv9A==} + dev: true + /zwitch/1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} dev: true From 0558195a90e04bcf411a8549dc1dd73bf60873b5 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 12 Dec 2022 21:23:38 +0800 Subject: [PATCH 157/166] chore(console): add fixme comment --- packages/console/src/components/RadioGroup/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/console/src/components/RadioGroup/index.tsx b/packages/console/src/components/RadioGroup/index.tsx index a0061869b..cd5209316 100644 --- a/packages/console/src/components/RadioGroup/index.tsx +++ b/packages/console/src/components/RadioGroup/index.tsx @@ -26,6 +26,8 @@ const RadioGroup = ( return child; } + // FIXME: @Charles + // @ts-expect-error to be fixed return cloneElement(child, { name, isChecked: value === child.props.value, From a805124437e8066f1b39751908ff0279770a59e2 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 12 Dec 2022 21:52:17 +0800 Subject: [PATCH 158/166] refactor: misc. issue fix --- .github/workflows/main.yml | 4 ++-- packages/cli/jest.config.js | 1 + packages/cli/package.json | 4 ++-- packages/cli/tsconfig.json | 2 +- packages/core/package.json | 2 +- packages/core/tsconfig.base.json | 2 +- packages/integration-tests/package.json | 4 +--- .../integration-tests/src/tests/api/application.test.ts | 7 ++++++- packages/phrases-ui/package.json | 1 + packages/phrases-ui/tsconfig.json | 2 +- packages/phrases/package.json | 1 + packages/phrases/tsconfig.json | 2 +- packages/schemas/jest.config.js | 1 + packages/schemas/package.json | 4 ++-- packages/schemas/tsconfig.json | 2 +- packages/shared/jest.config.js | 1 + packages/shared/package.json | 2 +- packages/shared/src/database/utils.test.ts | 2 ++ packages/shared/tsconfig.json | 2 +- pnpm-lock.yaml | 6 +----- 20 files changed, 29 insertions(+), 23 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ce7f5aeb4..abca19613 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,8 +61,8 @@ jobs: with: node-version: ${{ matrix.node_version }} - - name: Prepack - run: pnpm prepack + - name: Build for test + run: pnpm -r build:test - name: Test run: pnpm ci:test diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js index 652de5472..3fd28f230 100644 --- a/packages/cli/jest.config.js +++ b/packages/cli/jest.config.js @@ -1,3 +1,4 @@ +/** @type {import('jest').Config} */ const config = { coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'], coverageReporters: ['text-summary', 'lcov'], diff --git a/packages/cli/package.json b/packages/cli/package.json index 4829c6544..b5494624c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -25,7 +25,7 @@ "precommit": "lint-staged", "prepare:package-json": "node -p \"'export const packageJson = ' + JSON.stringify(require('./package.json'), undefined, 2) + ';'\" > src/package-json.ts", "build": "rimraf lib && pnpm prepare:package-json && tsc -p tsconfig.build.json", - "build:test": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap", + "build:test": "rimraf lib/ && pnpm prepare:package-json && tsc -p tsconfig.test.json --sourcemap", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental", "start": "node .", "start:dev": "pnpm build && node .", @@ -33,7 +33,7 @@ "lint:report": "pnpm lint --format json --output-file report.json", "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", "test": "pnpm build:test && pnpm test:only", - "test:ci": "pnpm run test", + "test:ci": "pnpm run test:only", "prepack": "pnpm build" }, "engines": { diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index d706f486e..50fa1e84b 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "lib", "declaration": true, "moduleResolution": "nodenext", - "module": "es2022", + "module": "esnext", "target": "es2022", "types": ["node", "jest"] }, diff --git a/packages/core/package.json b/packages/core/package.json index adc7260d9..36d689e06 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -21,7 +21,7 @@ "start": "NODE_ENV=production node build/index.js", "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", "test": "pnpm build:test && pnpm test:only", - "test:ci": "pnpm run test --coverage --silent", + "test:ci": "pnpm run test:only --coverage --silent", "test:report": "codecov -F core" }, "dependencies": { diff --git a/packages/core/tsconfig.base.json b/packages/core/tsconfig.base.json index 75db7efcc..247fd3037 100644 --- a/packages/core/tsconfig.base.json +++ b/packages/core/tsconfig.base.json @@ -2,7 +2,7 @@ "extends": "@silverhand/ts-config/tsconfig.base", "compilerOptions": { "moduleResolution": "nodenext", - "module": "es2022", + "module": "esnext", "declaration": false, "outDir": "build", "baseUrl": ".", diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index d11ff0f5b..cb53e05d5 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -15,7 +15,7 @@ "test": "pnpm build && pnpm test:api && pnpm test:ui", "test:api": "pnpm test:only -i ./lib/tests/api", "test:ui": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/ui", - "lint": "eslint --ext .ts src tests", + "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", "start": "pnpm test" }, @@ -27,7 +27,6 @@ "@peculiar/webcrypto": "^1.3.3", "@silverhand/eslint-config": "1.3.0", "@silverhand/essentials": "^1.3.0", - "@silverhand/jest-config": "1.2.2", "@silverhand/ts-config": "1.2.1", "@types/jest": "^29.1.2", "@types/jest-environment-puppeteer": "^5.0.2", @@ -43,7 +42,6 @@ "prettier": "^2.7.1", "puppeteer": "^19.0.0", "text-encoder": "^0.0.4", - "ts-node": "^10.9.1", "typescript": "^4.9.4" }, "engines": { diff --git a/packages/integration-tests/src/tests/api/application.test.ts b/packages/integration-tests/src/tests/api/application.test.ts index 0b27fc79f..a9aed13f6 100644 --- a/packages/integration-tests/src/tests/api/application.test.ts +++ b/packages/integration-tests/src/tests/api/application.test.ts @@ -2,7 +2,12 @@ import { ApplicationType } from '@logto/schemas'; import { demoAppApplicationId } from '@logto/schemas/lib/seeds'; import { HTTPError } from 'got'; -import { createApplication, getApplication, updateApplication, deleteApplication } from '#src/api/index.js'; +import { + createApplication, + getApplication, + updateApplication, + deleteApplication, +} from '#src/api/index.js'; describe('admin console application', () => { it('should get demo app details successfully', async () => { diff --git a/packages/phrases-ui/package.json b/packages/phrases-ui/package.json index 7a5f208cc..04f6f4cdd 100644 --- a/packages/phrases-ui/package.json +++ b/packages/phrases-ui/package.json @@ -23,6 +23,7 @@ "scripts": { "precommit": "lint-staged", "build": "rm -rf lib/ && tsc", + "build:test": "pnpm build", "dev": "tsc --watch --preserveWatchOutput --incremental", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", diff --git a/packages/phrases-ui/tsconfig.json b/packages/phrases-ui/tsconfig.json index b28c5b67e..cd964b281 100644 --- a/packages/phrases-ui/tsconfig.json +++ b/packages/phrases-ui/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "lib", "declaration": true, "moduleResolution": "nodenext", - "module": "es2022" + "module": "esnext" }, "include": ["src"] } diff --git a/packages/phrases/package.json b/packages/phrases/package.json index 75e20b23a..8c0ae5f21 100644 --- a/packages/phrases/package.json +++ b/packages/phrases/package.json @@ -20,6 +20,7 @@ "scripts": { "precommit": "lint-staged", "build": "rm -rf lib/ && tsc", + "build:test": "pnpm build", "dev": "tsc --watch --preserveWatchOutput --incremental", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", diff --git a/packages/phrases/tsconfig.json b/packages/phrases/tsconfig.json index 255f91f40..41e099231 100644 --- a/packages/phrases/tsconfig.json +++ b/packages/phrases/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "lib", "declaration": true, "moduleResolution": "nodenext", - "module": "es2022" + "module": "esnext" }, "include": [ "src" diff --git a/packages/schemas/jest.config.js b/packages/schemas/jest.config.js index 652de5472..3fd28f230 100644 --- a/packages/schemas/jest.config.js +++ b/packages/schemas/jest.config.js @@ -1,3 +1,4 @@ +/** @type {import('jest').Config} */ const config = { coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'], coverageReporters: ['text-summary', 'lcov'], diff --git a/packages/schemas/package.json b/packages/schemas/package.json index a4b093f4b..457f716bb 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -20,14 +20,14 @@ "generate": "./generate.sh", "build:alterations": "rm -rf alterations-js && tsc -p tsconfig.build.alterations.json", "build": "pnpm generate && rm -rf lib/ && tsc -p tsconfig.build.json && pnpm build:alterations", - "build:test": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap", + "build:test": "pnpm generate && rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", "prepack": "pnpm build", "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", "test": "pnpm build:test && pnpm test:only", - "test:ci": "pnpm run test" + "test:ci": "pnpm run test:only" }, "engines": { "node": "^16.13.0 || ^18.12.0" diff --git a/packages/schemas/tsconfig.json b/packages/schemas/tsconfig.json index f07ef0259..05e3acb09 100644 --- a/packages/schemas/tsconfig.json +++ b/packages/schemas/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "lib", "declaration": true, "moduleResolution": "nodenext", - "module": "es2022" + "module": "esnext" }, "include": [ "src", diff --git a/packages/shared/jest.config.js b/packages/shared/jest.config.js index 652de5472..3fd28f230 100644 --- a/packages/shared/jest.config.js +++ b/packages/shared/jest.config.js @@ -1,3 +1,4 @@ +/** @type {import('jest').Config} */ const config = { coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'], coverageReporters: ['text-summary', 'lcov'], diff --git a/packages/shared/package.json b/packages/shared/package.json index 3a30ecc66..39e8b1601 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -29,7 +29,7 @@ "prepack": "pnpm build", "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", "test": "pnpm build:test && pnpm test:only", - "test:ci": "pnpm run test" + "test:ci": "pnpm run test:only" }, "devDependencies": { "@silverhand/eslint-config": "1.3.0", diff --git a/packages/shared/src/database/utils.test.ts b/packages/shared/src/database/utils.test.ts index 27937c833..9199c1021 100644 --- a/packages/shared/src/database/utils.test.ts +++ b/packages/shared/src/database/utils.test.ts @@ -11,6 +11,8 @@ import { conditionalSql, } from './utils.js'; +const { jest } = import.meta; + describe('conditionalSql()', () => { it('returns empty sql when value is falsy', () => { expect(conditionalSql(false, () => sql`select 1`)).toEqual({ diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index fd9fd4d6f..b41cf6355 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -5,7 +5,7 @@ "declaration": true, "types": ["node", "jest"], "moduleResolution": "nodenext", - "module": "es2022" + "module": "esnext" }, "include": [ "src" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd5b3409e..582774ba1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -476,7 +476,6 @@ importers: '@peculiar/webcrypto': ^1.3.3 '@silverhand/eslint-config': 1.3.0 '@silverhand/essentials': ^1.3.0 - '@silverhand/jest-config': 1.2.2 '@silverhand/ts-config': 1.2.1 '@types/jest': ^29.1.2 '@types/jest-environment-puppeteer': ^5.0.2 @@ -492,7 +491,6 @@ importers: prettier: ^2.7.1 puppeteer: ^19.0.0 text-encoder: ^0.0.4 - ts-node: ^10.9.1 typescript: ^4.9.4 devDependencies: '@jest/types': 29.1.2 @@ -502,7 +500,6 @@ importers: '@peculiar/webcrypto': 1.3.3 '@silverhand/eslint-config': 1.3.0_eu7dlo3qq5moigliolva3udaxa '@silverhand/essentials': 1.3.0 - '@silverhand/jest-config': 1.2.2_ky6c64xxalg2hsll4xx3evq2dy '@silverhand/ts-config': 1.2.1_typescript@4.9.4 '@types/jest': 29.1.2 '@types/jest-environment-puppeteer': 5.0.2 @@ -510,7 +507,7 @@ importers: dotenv: 16.0.0 eslint: 8.21.0 got: 12.5.3 - jest: 29.1.2_k5ytkvaprncdyzidqqws5bqksq + jest: 29.1.2_@types+node@16.11.12 jest-puppeteer: 6.1.1_puppeteer@19.2.2 node-fetch: 2.6.7 openapi-schema-validator: 12.0.0 @@ -518,7 +515,6 @@ importers: prettier: 2.7.1 puppeteer: 19.2.2 text-encoder: 0.0.4 - ts-node: 10.9.1_ace2mtubvwruu4qt46fm3vtq3a typescript: 4.9.4 packages/phrases: From 14f86c01d589bdc4a4275ab0715cf38badc71d07 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 13 Dec 2022 10:19:42 +0800 Subject: [PATCH 159/166] fix: target can not be updated (#2609) --- packages/core/src/routes/connector.test.ts | 11 ++--- packages/core/src/routes/connector.ts | 47 ++++++++++++------- .../core/src/routes/connector.update.test.ts | 14 +++--- .../integration-tests/src/api/connector.ts | 12 +++-- .../tests/api/connector.test.ts | 10 ++-- packages/phrases/src/locales/de/errors.ts | 2 + packages/phrases/src/locales/en/errors.ts | 2 + packages/phrases/src/locales/fr/errors.ts | 2 + packages/phrases/src/locales/ko/errors.ts | 2 + packages/phrases/src/locales/pt-br/errors.ts | 2 + packages/phrases/src/locales/pt-pt/errors.ts | 2 + packages/phrases/src/locales/tr-tr/errors.ts | 2 + packages/phrases/src/locales/zh-cn/errors.ts | 1 + 13 files changed, 70 insertions(+), 39 deletions(-) diff --git a/packages/core/src/routes/connector.test.ts b/packages/core/src/routes/connector.test.ts index f0fd3caa6..e4398ac9b 100644 --- a/packages/core/src/routes/connector.test.ts +++ b/packages/core/src/routes/connector.test.ts @@ -188,7 +188,6 @@ describe('connector route', () => { metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' }, }, ]); - countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); const response = await connectorRequest.post('/connectors').send({ connectorId: 'id0', config: { cliend_id: 'client_id', client_secret: 'client_secret' }, @@ -255,7 +254,7 @@ describe('connector route', () => { { ...mockConnectorFactory, type: ConnectorType.Sms, - metadata: { ...mockConnectorFactory.metadata, id: 'id0', isStandard: true }, + metadata: { ...mockConnectorFactory.metadata, id: 'id1' }, }, ]); getLogtoConnectors.mockResolvedValueOnce([ @@ -266,20 +265,19 @@ describe('connector route', () => { ...mockLogtoConnector, }, ]); + countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); const response = await connectorRequest.post('/connectors').send({ - connectorId: 'id0', + connectorId: 'id1', config: { cliend_id: 'client_id', client_secret: 'client_secret' }, - metadata: { target: 'target', name: { en: '' }, logo: '', logoDark: null }, }); expect(response).toHaveProperty('statusCode', 200); expect(response.body).toMatchObject( expect.objectContaining({ - connectorId: 'id0', + connectorId: 'id1', config: { cliend_id: 'client_id', client_secret: 'client_secret', }, - metadata: { target: 'target' }, }) ); expect(deleteConnectorByIds).toHaveBeenCalledWith(['id']); @@ -347,6 +345,7 @@ describe('connector route', () => { ]); const response = await connectorRequest.post('/connectors').send({ connectorId: 'id0', + metadata: { target: 'target' }, }); expect(response).toHaveProperty('statusCode', 422); }); diff --git a/packages/core/src/routes/connector.ts b/packages/core/src/routes/connector.ts index b30fb5e68..8673c615d 100644 --- a/packages/core/src/routes/connector.ts +++ b/packages/core/src/routes/connector.ts @@ -3,7 +3,6 @@ import { emailRegEx, phoneRegEx } from '@logto/core-kit'; import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas'; import { arbitraryObjectGuard, Connectors, ConnectorType } from '@logto/schemas'; import { buildIdGenerator } from '@logto/shared'; -import { conditional } from '@silverhand/essentials'; import cleanDeep from 'clean-deep'; import { object, string } from 'zod'; @@ -114,9 +113,9 @@ export default function connectorRoutes(router: T) { // eslint-disable-next-line complexity async (ctx, next) => { const { - body: { connectorId }, - body, + body: { connectorId, metadata, config, syncProfile }, } = ctx.guard; + const connectorFactories = await loadConnectorFactories(); const connectorFactory = connectorFactories.find( ({ metadata: { id } }) => id === connectorId @@ -129,6 +128,15 @@ export default function connectorRoutes(router: T) { }); } + assertThat( + connectorFactory.metadata.isStandard !== true || metadata?.target, + 'connector.can_not_modify_target' + ); + assertThat( + connectorFactory.metadata.isStandard === true || metadata === undefined, + 'connector.cannot_change_metadata_for_non_standard_connector' + ); + const { count } = await countConnectorByConnectorId(connectorId); assertThat( count === 0 || connectorFactory.metadata.isStandard === true, @@ -140,25 +148,23 @@ export default function connectorRoutes(router: T) { if (connectorFactory.type === ConnectorType.Social) { const connectors = await getLogtoConnectors(); - const connectorTarget = body.metadata?.target ?? connectorFactory.metadata.target; assertThat( !connectors .filter(({ type }) => type === ConnectorType.Social) .some( ({ metadata: { target, platform } }) => - target === connectorTarget && platform === connectorFactory.metadata.platform + target === cleanDeep(metadata)?.target && + platform === connectorFactory.metadata.platform ), new RequestError({ code: 'connector.multiple_target_with_same_platform', status: 422 }) ); } const insertConnectorId = generateConnectorId(); - const { metadata, ...rest } = body; - ctx.body = await insertConnector({ id: insertConnectorId, - ...conditional(metadata && { metadata: cleanDeep(metadata) }), - ...rest, + connectorId, + ...cleanDeep({ syncProfile, config, metadata }), }); /** @@ -194,17 +200,25 @@ export default function connectorRoutes(router: T) { .pick({ config: true, metadata: true, syncProfile: true }) .partial(), }), - async (ctx, next) => { const { params: { id }, - body: { config }, - body, + body: { config, metadata, syncProfile }, } = ctx.guard; - const { type, validateConfig } = await getLogtoConnectorById(id); + const { type, validateConfig, metadata: originalMetadata } = await getLogtoConnectorById(id); - if (body.syncProfile) { + assertThat( + originalMetadata.isStandard !== true || metadata?.target === originalMetadata.target, + 'connector.can_not_modify_target' + ); + + assertThat( + originalMetadata.isStandard === true || metadata === undefined, + 'connector.cannot_change_metadata_for_non_standard_connector' + ); + + if (syncProfile) { assertThat( type === ConnectorType.Social, new RequestError({ code: 'connector.invalid_type_for_syncing_profile', status: 422 }) @@ -214,12 +228,9 @@ export default function connectorRoutes(router: T) { if (config) { validateConfig(config); } - // Once created, target can not be modified. - assertThat(body.metadata?.target === undefined, 'connector.can_not_modify_target'); - const { metadata: databaseMetadata, ...rest } = body; await updateConnector({ - set: databaseMetadata ? { metadata: cleanDeep(databaseMetadata), ...rest } : rest, + set: cleanDeep({ config, metadata, syncProfile }), where: { id }, jsonbMode: 'replace', }); diff --git a/packages/core/src/routes/connector.update.test.ts b/packages/core/src/routes/connector.update.test.ts index b295476e5..f71e1bae0 100644 --- a/packages/core/src/routes/connector.update.test.ts +++ b/packages/core/src/routes/connector.update.test.ts @@ -120,7 +120,7 @@ describe('connector PATCH routes', () => { updateConnector.mockResolvedValueOnce({ ...mockConnector, metadata: { - target: 'target', + target: 'connector', name: { en: 'connector_name', fr: 'connector_name' }, logo: 'new_logo.png', }, @@ -131,6 +131,7 @@ describe('connector PATCH routes', () => { name: { en: 'connector_name', fr: 'connector_name' }, logo: 'new_logo.png', logoDark: null, + target: 'connector', }, }); expect(updateConnector).toHaveBeenCalledWith( @@ -141,6 +142,7 @@ describe('connector PATCH routes', () => { metadata: { name: { en: 'connector_name', fr: 'connector_name' }, logo: 'new_logo.png', + target: 'connector', }, }, jsonbMode: 'replace', @@ -161,24 +163,20 @@ describe('connector PATCH routes', () => { updateConnector.mockResolvedValueOnce({ ...mockConnector, metadata: { - target: '', + target: 'connector', name: { en: '' }, logo: '', logoDark: '', }, }); const response = await connectorRequest.patch('/connectors/id').send({ - metadata: { - name: { en: '' }, - logo: '', - logoDark: '', - }, + metadata: { target: 'connector', name: { en: '' }, logo: '', logoDark: '' }, }); expect(updateConnector).toHaveBeenCalledWith( expect.objectContaining({ where: { id: 'id' }, set: { - metadata: {}, + metadata: { target: 'connector' }, }, jsonbMode: 'replace', }) diff --git a/packages/integration-tests/src/api/connector.ts b/packages/integration-tests/src/api/connector.ts index 388659776..15bdad19f 100644 --- a/packages/integration-tests/src/api/connector.ts +++ b/packages/integration-tests/src/api/connector.ts @@ -9,22 +9,26 @@ export const getConnector = async (connectorId: string) => authedAdminApi.get(`connectors/${connectorId}`).json(); // FIXME @Darcy: correct use of `id` and `connectorId`. -export const postConnector = async (connectorId: string) => +export const postConnector = async (connectorId: string, metadata?: Record) => authedAdminApi .post({ url: `connectors`, - json: { connectorId }, + json: { connectorId, metadata }, }) .json(); export const deleteConnectorById = async (id: string) => authedAdminApi.delete({ url: `connectors/${id}` }).json(); -export const updateConnectorConfig = async (connectorId: string, config: Record) => +export const updateConnectorConfig = async ( + connectorId: string, + config: Record, + metadata?: Record +) => authedAdminApi .patch({ url: `connectors/${connectorId}`, - json: { config }, + json: { config, metadata }, }) .json(); diff --git a/packages/integration-tests/tests/api/connector.test.ts b/packages/integration-tests/tests/api/connector.test.ts index ec5d65aca..9738da3b6 100644 --- a/packages/integration-tests/tests/api/connector.test.ts +++ b/packages/integration-tests/tests/api/connector.test.ts @@ -71,13 +71,18 @@ test('connector set-up flow', async () => { /* * Change to another SMS/Email connector */ - const { id } = await postConnector(mockStandardEmailConnectorId); - await updateConnectorConfig(id, mockStandardEmailConnectorConfig); + const { id } = await postConnector(mockStandardEmailConnectorId, { + target: 'mock-standard-mail', + }); // TODO [LOG-4862]: update mock connector + await updateConnectorConfig(id, mockStandardEmailConnectorConfig, { + target: 'mock-standard-mail', + }); // TODO [LOG-4862]: update mock connector connectorIdMap.set(mockStandardEmailConnectorId, id); const currentConnectors = await listConnectors(); expect( currentConnectors.some((connector) => connector.connectorId === mockEmailConnectorId) ).toBeFalsy(); + connectorIdMap.delete(mockEmailConnectorId); expect( currentConnectors.some((connector) => connector.connectorId === mockStandardEmailConnectorId) ).toBeTruthy(); @@ -85,7 +90,6 @@ test('connector set-up flow', async () => { currentConnectors.find((connector) => connector.connectorId === mockStandardEmailConnectorId) ?.config ).toEqual(mockStandardEmailConnectorConfig); - connectorIdMap.delete(mockEmailConnectorId); /* * Delete (i.e. disable) a connector diff --git a/packages/phrases/src/locales/de/errors.ts b/packages/phrases/src/locales/de/errors.ts index 00bcdeefa..a5097f1dd 100644 --- a/packages/phrases/src/locales/de/errors.ts +++ b/packages/phrases/src/locales/de/errors.ts @@ -112,6 +112,8 @@ const errors = { can_not_modify_target: 'The connector target can not be modified.', multiple_target_with_same_platform: 'You can not have multiple social connectors that have same target and platform.', + cannot_change_metadata_for_non_standard_connector: + "This connector's `metadata` cannot be changed.", }, passcode: { phone_email_empty: 'Telefonnummer oder E-Mail darf nicht leer sein.', diff --git a/packages/phrases/src/locales/en/errors.ts b/packages/phrases/src/locales/en/errors.ts index ffe1a535e..eed909db1 100644 --- a/packages/phrases/src/locales/en/errors.ts +++ b/packages/phrases/src/locales/en/errors.ts @@ -111,6 +111,8 @@ const errors = { can_not_modify_target: 'The connector target can not be modified.', multiple_target_with_same_platform: 'You can not have multiple social connectors that have same target and platform.', + cannot_change_metadata_for_non_standard_connector: + "This connector's `metadata` cannot be changed.", }, passcode: { phone_email_empty: 'Both phone and email are empty.', diff --git a/packages/phrases/src/locales/fr/errors.ts b/packages/phrases/src/locales/fr/errors.ts index dd75d6499..97ff0bec0 100644 --- a/packages/phrases/src/locales/fr/errors.ts +++ b/packages/phrases/src/locales/fr/errors.ts @@ -118,6 +118,8 @@ const errors = { can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED multiple_target_with_same_platform: 'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED + cannot_change_metadata_for_non_standard_connector: + "This connector's `metadata` cannot be changed.", // UNTRANSLATED }, passcode: { phone_email_empty: "Le téléphone et l'email sont vides.", diff --git a/packages/phrases/src/locales/ko/errors.ts b/packages/phrases/src/locales/ko/errors.ts index 2cf32646b..f1ee88d42 100644 --- a/packages/phrases/src/locales/ko/errors.ts +++ b/packages/phrases/src/locales/ko/errors.ts @@ -110,6 +110,8 @@ const errors = { can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED multiple_target_with_same_platform: 'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED + cannot_change_metadata_for_non_standard_connector: + "This connector's `metadata` cannot be changed.", // UNTRANSLATED }, passcode: { phone_email_empty: '휴대전화번호 그리고 이메일이 비어있어요.', diff --git a/packages/phrases/src/locales/pt-br/errors.ts b/packages/phrases/src/locales/pt-br/errors.ts index fc22265e5..71439ef8a 100644 --- a/packages/phrases/src/locales/pt-br/errors.ts +++ b/packages/phrases/src/locales/pt-br/errors.ts @@ -115,6 +115,8 @@ const errors = { can_not_modify_target: 'O destino do conector não pode ser modificado.', multiple_target_with_same_platform: 'Você não pode ter vários conectores sociais com o mesmo destino e plataforma.', + cannot_change_metadata_for_non_standard_connector: + "This connector's `metadata` cannot be changed.", // UNTRANSLATED }, passcode: { phone_email_empty: 'Telefone e e-mail estão vazios.', diff --git a/packages/phrases/src/locales/pt-pt/errors.ts b/packages/phrases/src/locales/pt-pt/errors.ts index df664d633..919084e60 100644 --- a/packages/phrases/src/locales/pt-pt/errors.ts +++ b/packages/phrases/src/locales/pt-pt/errors.ts @@ -113,6 +113,8 @@ const errors = { can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED multiple_target_with_same_platform: 'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED + cannot_change_metadata_for_non_standard_connector: + "This connector's `metadata` cannot be changed.", // UNTRANSLATED }, passcode: { phone_email_empty: 'O campos telefone e email estão vazios.', diff --git a/packages/phrases/src/locales/tr-tr/errors.ts b/packages/phrases/src/locales/tr-tr/errors.ts index 5c8343819..a213c2254 100644 --- a/packages/phrases/src/locales/tr-tr/errors.ts +++ b/packages/phrases/src/locales/tr-tr/errors.ts @@ -112,6 +112,8 @@ const errors = { can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED multiple_target_with_same_platform: 'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED + cannot_change_metadata_for_non_standard_connector: + "This connector's `metadata` cannot be changed.", // UNTRANSLATED }, passcode: { phone_email_empty: 'Hem telefon hem de e-posta adresi yok.', diff --git a/packages/phrases/src/locales/zh-cn/errors.ts b/packages/phrases/src/locales/zh-cn/errors.ts index c432762e6..a4b3f66f4 100644 --- a/packages/phrases/src/locales/zh-cn/errors.ts +++ b/packages/phrases/src/locales/zh-cn/errors.ts @@ -103,6 +103,7 @@ const errors = { invalid_type_for_syncing_profile: '只有社交连接器可以开启用户档案同步。', can_not_modify_target: '不可修改连接器 target。', multiple_target_with_same_platform: '不能同时存在多个有相同 target 和平台类型的社交连接器。', + cannot_change_metadata_for_non_standard_connector: '不可配置该连接器的 metadata 参数。', }, passcode: { phone_email_empty: '手机号与邮箱地址均为空', From e8ee2a31b1a503c7542254914bbd38e53f95395a Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 13 Dec 2022 11:16:56 +0800 Subject: [PATCH 160/166] refactor(console): remove unnecessary required tag (#2638) --- .../pages/ConnectorDetails/components/SenderTester/index.tsx | 1 - .../src/pages/Connectors/components/ConnectorForm/index.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx b/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx index 7c94b5ace..a1692934b 100644 --- a/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx +++ b/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx @@ -77,7 +77,6 @@ const SenderTester = ({ connectorId, connectorType, config, className }: Props)
{ )} - + { )} /> - + Date: Tue, 13 Dec 2022 11:41:53 +0800 Subject: [PATCH 161/166] fix(console): navigate to the details page on connector created (#2640) --- .../console/src/pages/Connectors/components/Guide/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/console/src/pages/Connectors/components/Guide/index.tsx b/packages/console/src/pages/Connectors/components/Guide/index.tsx index ef7f791a0..fed34df2c 100644 --- a/packages/console/src/pages/Connectors/components/Guide/index.tsx +++ b/packages/console/src/pages/Connectors/components/Guide/index.tsx @@ -6,6 +6,7 @@ import i18next from 'i18next'; import { FormProvider, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; import Close from '@/assets/images/close.svg'; import Button from '@/components/Button'; @@ -30,6 +31,7 @@ type Props = { const Guide = ({ connector, onClose }: Props) => { const api = useApi(); + const navigate = useNavigate(); const { updateSettings } = useSettings(); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { id: connectorId, type: connectorType, name, readme, isStandard } = connector; @@ -65,7 +67,7 @@ const Guide = ({ connector, onClose }: Props) => { const { id: connectorId } = connector; - await api + const createdConnector = await api .post('/api/connectors', { json: { config: result.data, @@ -88,6 +90,7 @@ const Guide = ({ connector, onClose }: Props) => { onClose(); toast.success(t('general.saved')); + navigate(`/connectors/${createdConnector.id}`); }); return ( From 91b82fef3fb5e6a5c62f3bba767b1b24934281a1 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 13 Dec 2022 11:42:06 +0800 Subject: [PATCH 162/166] refactor(console): add a11y to the `SenderTester` (#2636) --- .../ConnectorDetails/components/SenderTester/index.tsx | 2 ++ packages/console/src/utilities/a11y.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx b/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx index a1692934b..26eed1138 100644 --- a/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx +++ b/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx @@ -12,6 +12,7 @@ import FormField from '@/components/FormField'; import TextInput from '@/components/TextInput'; import { Tooltip } from '@/components/Tip'; import useApi from '@/hooks/use-api'; +import { onKeyDownHandler } from '@/utilities/a11y'; import { safeParseJson } from '@/utilities/json'; import * as styles from './index.module.scss'; @@ -90,6 +91,7 @@ const SenderTester = ({ connectorId, connectorType, config, className }: Props) ? t('connector_details.test_sms_placeholder') : t('connector_details.test_email_placeholder') } + onKeyDown={onKeyDownHandler({ Enter: onSubmit })} {...register('sendTo', { required: true, pattern: { diff --git a/packages/console/src/utilities/a11y.ts b/packages/console/src/utilities/a11y.ts index e46b51f8b..6f65b62c1 100644 --- a/packages/console/src/utilities/a11y.ts +++ b/packages/console/src/utilities/a11y.ts @@ -15,7 +15,11 @@ export const onKeyDownHandler = } if (typeof callback === 'object') { - callback[key]?.(event); - event.preventDefault(); + const handler = callback[key]; + + if (handler) { + handler(event); + event.preventDefault(); + } } }; From 9345c103016d44bf92a910badc3f7b402a5a9458 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 13 Dec 2022 12:25:08 +0800 Subject: [PATCH 163/166] refactor(core): sort users data by `createdAt` (#2643) --- packages/core/src/queries/user.test.ts | 3 +++ packages/core/src/queries/user.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/packages/core/src/queries/user.test.ts b/packages/core/src/queries/user.test.ts index 523c514ca..98db64fd1 100644 --- a/packages/core/src/queries/user.test.ts +++ b/packages/core/src/queries/user.test.ts @@ -307,6 +307,7 @@ describe('user query', () => { where ${fields.primaryEmail} ilike $1 or ${fields.primaryPhone} ilike $2 or ${ fields.username } ilike $3 or ${fields.name} ilike $4 + order by "created_at" desc limit $5 offset $6 `; @@ -339,6 +340,7 @@ describe('user query', () => { and (${fields.primaryEmail} ilike $2 or ${fields.primaryPhone} ilike $3 or ${ fields.username } ilike $4 or ${fields.name} ilike $5) + order by "created_at" desc limit $6 offset $7 `; @@ -371,6 +373,7 @@ describe('user query', () => { where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${ fields.username } like $3 or ${fields.name} like $4 + order by "created_at" desc limit $5 offset $6 `; diff --git a/packages/core/src/queries/user.ts b/packages/core/src/queries/user.ts index 3f099f56a..a53fb165b 100644 --- a/packages/core/src/queries/user.ts +++ b/packages/core/src/queries/user.ts @@ -145,6 +145,7 @@ export const findUsers = async ( select ${sql.join(Object.values(fields), sql`,`)} from ${table} ${buildUserConditions(search, hideAdminUser, isCaseSensitive)} + order by ${fields.createdAt} desc limit ${limit} offset ${offset} ` From 6441a38c135e636088b989ae958a4820bd69474b Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 13 Dec 2022 13:03:36 +0800 Subject: [PATCH 164/166] refactor(console): add another button (#2653) --- packages/console/src/assets/images/circle-plus.svg | 4 +--- .../console/src/components/Button/index.module.scss | 11 ++++++++--- .../console/src/components/MultiTextInput/index.tsx | 2 ++ .../components/SignInMethodEditBox/AddButton.tsx | 2 ++ .../components/SocialConnectorEditBox/AddButton.tsx | 2 ++ .../translation/admin-console/application-details.ts | 1 - .../locales/de/translation/admin-console/general.ts | 2 +- .../translation/admin-console/application-details.ts | 1 - .../locales/en/translation/admin-console/general.ts | 2 +- .../translation/admin-console/application-details.ts | 1 - .../locales/fr/translation/admin-console/general.ts | 2 +- .../translation/admin-console/application-details.ts | 1 - .../locales/ko/translation/admin-console/general.ts | 2 +- .../translation/admin-console/application-details.ts | 1 - .../pt-br/translation/admin-console/general.ts | 2 +- .../translation/admin-console/application-details.ts | 1 - .../pt-pt/translation/admin-console/general.ts | 2 +- .../translation/admin-console/application-details.ts | 1 - .../tr-tr/translation/admin-console/general.ts | 2 +- .../translation/admin-console/application-details.ts | 1 - .../zh-cn/translation/admin-console/general.ts | 2 +- 21 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/console/src/assets/images/circle-plus.svg b/packages/console/src/assets/images/circle-plus.svg index 1d2315bc1..686f04f26 100644 --- a/packages/console/src/assets/images/circle-plus.svg +++ b/packages/console/src/assets/images/circle-plus.svg @@ -1,5 +1,3 @@ - + diff --git a/packages/console/src/components/Button/index.module.scss b/packages/console/src/components/Button/index.module.scss index d0a2c533e..dd2414f15 100644 --- a/packages/console/src/components/Button/index.module.scss +++ b/packages/console/src/components/Button/index.module.scss @@ -40,9 +40,8 @@ } .icon { - display: block; - width: 20px; - height: 20px; + display: flex; + align-items: center; &:not(:last-child) { margin-right: _.unit(2); @@ -62,6 +61,12 @@ &.text { height: 24px; } + + .icon { + &:not(:last-child) { + margin-right: _.unit(1); + } + } } &.medium { diff --git a/packages/console/src/components/MultiTextInput/index.tsx b/packages/console/src/components/MultiTextInput/index.tsx index 0e61ed2f8..dda2ec39b 100644 --- a/packages/console/src/components/MultiTextInput/index.tsx +++ b/packages/console/src/components/MultiTextInput/index.tsx @@ -5,6 +5,7 @@ import type { KeyboardEvent } from 'react'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import CirclePlus from '@/assets/images/circle-plus.svg'; import Minus from '@/assets/images/minus.svg'; import Button from '../Button'; @@ -106,6 +107,7 @@ const MultiTextInput = ({ type="text" title="general.add_another" className={styles.addAnother} + icon={} onClick={handleAdd} /> { type: 'text', size: 'small', title: 'general.add_another', + icon: , }; return ( diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SocialConnectorEditBox/AddButton.tsx b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SocialConnectorEditBox/AddButton.tsx index dbc1a50e1..08183c726 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SocialConnectorEditBox/AddButton.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SocialConnectorEditBox/AddButton.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; +import CirclePlus from '@/assets/images/circle-plus.svg'; import Plus from '@/assets/images/plus.svg'; import ActionMenu from '@/components/ActionMenu'; import type { Props as ButtonProps } from '@/components/Button'; @@ -32,6 +33,7 @@ const AddButton = ({ options, onSelected, hasSelectedConnectors }: Props) => { type: 'text', size: 'small', title: 'general.add_another', + icon: , }; return ( diff --git a/packages/phrases/src/locales/de/translation/admin-console/application-details.ts b/packages/phrases/src/locales/de/translation/admin-console/application-details.ts index 5a906234a..b804c71d4 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/application-details.ts @@ -33,7 +33,6 @@ const application_details = { cors_allowed_origins_placeholder: 'https://your.website.de', cors_allowed_origins_tip: 'Es sind standardmäßig alle Umleitungs-URI Origins erlaubt. Normalerweise ist dieses Feld nicht erforderlich. See the MDN doc for detailed info.', // UNTRANSLATED - add_another: 'Weitere hinzufügen', id_token_expiration: 'ID Token Ablaufzeit', refresh_token_expiration: 'Refresh Token Ablaufzeit', token_endpoint: 'Token Endpoint', diff --git a/packages/phrases/src/locales/de/translation/admin-console/general.ts b/packages/phrases/src/locales/de/translation/admin-console/general.ts index f8d3bbe52..03af0d8ec 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/general.ts @@ -30,7 +30,7 @@ const general = { copying: 'Kopiere', copied: 'Kopiert', required: 'Erforderlich', - add_another: '+ Weitere hinzufügen', + add_another: 'Weitere hinzufügen', deletion_confirmation: 'Willst du {{title}} wirklich löschen?', settings_nav: 'Einstellungen', unsaved_changes_warning: diff --git a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts index 71d51326c..4d6724103 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts @@ -33,7 +33,6 @@ const application_details = { cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: 'By default, all the origins of Redirect URIs will be allowed. Usually no action is required for this field. See the MDN doc for detailed info.', - add_another: 'Add Another', id_token_expiration: 'ID Token expiration', refresh_token_expiration: 'Refresh Token expiration', token_endpoint: 'Token Endpoint', diff --git a/packages/phrases/src/locales/en/translation/admin-console/general.ts b/packages/phrases/src/locales/en/translation/admin-console/general.ts index 8ee71ab3d..809110051 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/general.ts @@ -30,7 +30,7 @@ const general = { copying: 'Copying', copied: 'Copied', required: 'Required', - add_another: '+ Add Another', + add_another: 'Add Another', deletion_confirmation: 'Are you sure you want to delete this {{title}}?', settings_nav: 'Settings', unsaved_changes_warning: 'You have made some changes. Are you sure you want to leave this page?', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts b/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts index c927d50ad..73fb6ec15 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts @@ -33,7 +33,6 @@ const application_details = { cors_allowed_origins_placeholder: 'https://votre.site.com', cors_allowed_origins_tip: "Par défaut, toutes les origines des URI de redirection seront autorisées. En général, aucune action n'est requise pour ce champ. See the MDN doc for detailed info.", // UNTRANSLATED - add_another: 'Ajouter un autre', id_token_expiration: "Expiration du jeton d'identification", refresh_token_expiration: "Rafraîchir l'expiration du jeton", token_endpoint: 'Token Endpoint', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/general.ts b/packages/phrases/src/locales/fr/translation/admin-console/general.ts index 2c584e716..cf7a76f47 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/general.ts @@ -30,7 +30,7 @@ const general = { copying: 'Copie', copied: 'Copié', required: 'Requis', - add_another: '+ Ajouter un autre', + add_another: 'Ajouter un autre', deletion_confirmation: 'Êtes-vous sûr de vouloir supprimer ce {{title}} ?', settings_nav: 'Paramètres', unsaved_changes_warning: diff --git a/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts b/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts index f611c5229..dd92631c9 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts @@ -33,7 +33,6 @@ const application_details = { cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: '기본으로 모든 리다이렉트의 오리진들은 허용되요. 대체적으로 이 값을 건들 필요는 없어요. See the MDN doc for detailed info.', // UNTRANSLATED - add_another: '새로 추가', id_token_expiration: 'ID 토큰 만료', refresh_token_expiration: 'Refresh 토큰 만료', token_endpoint: '토큰 End-Point', diff --git a/packages/phrases/src/locales/ko/translation/admin-console/general.ts b/packages/phrases/src/locales/ko/translation/admin-console/general.ts index 65468a1f5..a0917946c 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/general.ts @@ -30,7 +30,7 @@ const general = { copying: '복사 중', copied: '복사됨', required: '필수', - add_another: '+ 새로 추가', + add_another: '새로 추가', deletion_confirmation: '정말로 {{title}}을/를 삭제할까요?', settings_nav: '설정', unsaved_changes_warning: '수정된 내용이 있어요. 정말로 현재 페이지를 벗어날까요?', diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts index daab4da54..e84b034c7 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/application-details.ts @@ -33,7 +33,6 @@ const application_details = { cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: 'Por padrão, todas as origens de URIs de redirecionamento serão permitidas. Normalmente, nenhuma ação é necessária para este campo. See the MDN doc for detailed info.', // UNTRANSLATED - add_another: 'Adicionar outro', id_token_expiration: 'Expiração do token de ID', refresh_token_expiration: 'Expiração Refresh Token', token_endpoint: 'Token Endpoint', diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/general.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/general.ts index ff8c68750..a640d2e5f 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/general.ts @@ -30,7 +30,7 @@ const general = { copying: 'Copiando', copied: 'Copiado', required: 'Obrigatório', - add_another: '+ Adicionar outro', + add_another: 'Adicionar outro', deletion_confirmation: 'Tem certeza de que deseja excluir este {{title}}?', settings_nav: 'Configurações', unsaved_changes_warning: diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts index 29f16112b..7bbbf69de 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts @@ -33,7 +33,6 @@ const application_details = { cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: 'Por padrão, todas as origens de redirecionamento serão permitidas. Recomenda-se restringir isto. See the MDN doc for detailed info.', // UNTRANSLATED - add_another: 'Adicionar outro', id_token_expiration: 'Expiração do token de ID', refresh_token_expiration: 'Expiração do token de atualização', token_endpoint: 'Endpoint Token', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/general.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/general.ts index b0560d298..becbc80e5 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/general.ts @@ -30,7 +30,7 @@ const general = { copying: 'Copiando', copied: 'Copiado', required: 'Necessário', - add_another: '+ Adicionar outro', + add_another: 'Adicionar outro', deletion_confirmation: 'Tem a certeza que deseja eliminar isso {{title}}?', settings_nav: 'Definições', unsaved_changes_warning: 'Fez algumas alterações. Tem a certeza que deseja sair desta página?', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts index 5b2d977fd..408fd0747 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts @@ -33,7 +33,6 @@ const application_details = { cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: 'Varsayılan olarak, Yönlendirme URIlerinin tüm originlerine izin verilir. Genellikle bu alan için herhangi bir işlem gerekmez. See the MDN doc for detailed info.', // UNTRANSLATED - add_another: 'Bir tane daha ekle', id_token_expiration: 'ID Token sona erme süresi', refresh_token_expiration: 'Refresh Token sona erme süresi', token_endpoint: 'Token bitiş noktası', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/general.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/general.ts index 59be64348..7e715c678 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/general.ts @@ -30,7 +30,7 @@ const general = { copying: 'Kopyalanıyor', copied: 'Kopyalandı', required: 'Gerekli', - add_another: '+ Bir tane daha ekle', + add_another: 'Bir tane daha ekle', deletion_confirmation: 'Bu dosyayı silmek istediğinize emin misiniz: {{title}}?', settings_nav: 'Ayarlar', unsaved_changes_warning: diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts index 4f0821285..a75e1a1b0 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts @@ -33,7 +33,6 @@ const application_details = { cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: '所有 Redirect URI 的 origin 将默认被允许。通常不需要对此字段进行操作。参见 MDN 文档以了解更多', - add_another: '新增', id_token_expiration: 'ID Token 过期时间', refresh_token_expiration: 'Refresh Token 过期时间', token_endpoint: 'Token Endpoint', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/general.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/general.ts index 112557c35..44c7c4a3f 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/general.ts @@ -30,7 +30,7 @@ const general = { copying: '复制中', copied: '已复制', required: '必填', - add_another: '+ 新增', + add_another: '新增', deletion_confirmation: '你确定要删除这个 {{title}} 吗?', settings_nav: '设置', unsaved_changes_warning: '还有未保存的变更, 确定要离开吗?', From 9e088085698e3d2bcfeb60adc21a6e713a37ace5 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 13 Dec 2022 13:03:50 +0800 Subject: [PATCH 165/166] fix(console): align the preview background color with the ui page (#2641) --- .../src/pages/SignInExperience/components/Preview/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/src/pages/SignInExperience/components/Preview/index.tsx b/packages/console/src/pages/SignInExperience/components/Preview/index.tsx index 27b42155f..cc9f7d355 100644 --- a/packages/console/src/pages/SignInExperience/components/Preview/index.tsx +++ b/packages/console/src/pages/SignInExperience/components/Preview/index.tsx @@ -194,7 +194,7 @@ const Preview = ({ signInExperience, className }: Props) => { style={conditional( platform === 'desktopWeb' && { // Set background color to match iframe's background color on both dark and light mode. - backgroundColor: mode === AppearanceMode.DarkMode ? '#2A2C31' : '#e5e1ec', + backgroundColor: mode === AppearanceMode.DarkMode ? '#000' : '#e5e1ec', } )} > From 83cb2ec37f5d4680f88da0f01120b337ac2f9e9a Mon Sep 17 00:00:00 2001 From: wangsijie Date: Tue, 13 Dec 2022 13:07:11 +0800 Subject: [PATCH 166/166] fix(console): remove sync profile field for non-social connector (#2637) --- .../components/ConnectorContent.tsx | 11 +++++--- .../components/ConnectorForm/index.tsx | 23 +++++++++------- .../Connectors/components/Guide/index.tsx | 27 +++++++++++-------- 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx b/packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx index 9403de115..baeb87e10 100644 --- a/packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx +++ b/packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx @@ -67,10 +67,13 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop return; } - const payload = { - config: result.data, - syncProfile: syncProfile === SyncProfileMode.EachSignIn, - }; + const payload = + connectorData.type === ConnectorType.Social + ? { + config: result.data, + syncProfile: syncProfile === SyncProfileMode.EachSignIn, + } + : { config: result.data }; const standardConnectorPayload = { ...payload, metadata: { ...metadata, name: { en: metadata.name } }, diff --git a/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx b/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx index 13eead5b0..389721784 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx +++ b/packages/console/src/pages/Connectors/components/ConnectorForm/index.tsx @@ -1,4 +1,5 @@ import type { ConnectorFactoryResponse } from '@logto/schemas'; +import { ConnectorType } from '@logto/schemas'; import { useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -102,16 +103,18 @@ const ConnectorForm = ({ connector, isAllowEditTarget }: Props) => { )} /> - - ( - + )} + /> + + )}
); }; diff --git a/packages/console/src/pages/Connectors/components/Guide/index.tsx b/packages/console/src/pages/Connectors/components/Guide/index.tsx index fed34df2c..8ee20b6e6 100644 --- a/packages/console/src/pages/Connectors/components/Guide/index.tsx +++ b/packages/console/src/pages/Connectors/components/Guide/index.tsx @@ -67,19 +67,24 @@ const Guide = ({ connector, onClose }: Props) => { const { id: connectorId } = connector; + const basePayload = { + config: result.data, + connectorId, + metadata: conditional( + isStandard && { + ...otherData, + name: { en: name }, + } + ), + }; + + const payload = isSocialConnector + ? { ...basePayload, syncProfile: syncProfile === SyncProfileMode.EachSignIn } + : basePayload; + const createdConnector = await api .post('/api/connectors', { - json: { - config: result.data, - connectorId, - syncProfile: syncProfile === SyncProfileMode.EachSignIn, - metadata: conditional( - isStandard && { - ...otherData, - name: { en: name }, - } - ), - }, + json: payload, }) .json();