diff --git a/packages/core/src/__mocks__/connector-base-data.ts b/packages/core/src/__mocks__/connector-base-data.ts new file mode 100644 index 000000000..71e294abd --- /dev/null +++ b/packages/core/src/__mocks__/connector-base-data.ts @@ -0,0 +1,125 @@ +import type { ConnectorMetadata } from '@logto/connector-kit'; +import { ConnectorPlatform } from '@logto/connector-kit'; +import type { Connector } from '@logto/schemas'; + +export const mockMetadata: ConnectorMetadata = { + id: 'id', + target: 'connector', + platform: null, + name: { + en: 'Connector', + 'pt-PT': 'Conector', + 'zh-CN': '连接器', + 'tr-TR': 'Connector', + ko: 'Connector', + }, + logo: './logo.png', + logoDark: './logo-dark.png', + description: { + en: 'Connector', + 'pt-PT': 'Conector', + 'zh-CN': '连接器', + 'tr-TR': 'Connector', + ko: 'Connector', + }, + readme: 'README.md', + configTemplate: 'config-template.json', +}; + +export const mockMetadata0: ConnectorMetadata = { + ...mockMetadata, + id: 'id0', + target: 'connector_0', + platform: ConnectorPlatform.Universal, +}; + +export const mockMetadata1: ConnectorMetadata = { + ...mockMetadata, + id: 'id1', + target: 'connector_1', + platform: ConnectorPlatform.Universal, +}; + +export const mockMetadata2: ConnectorMetadata = { + ...mockMetadata, + id: 'id2', + target: 'connector_2', + platform: ConnectorPlatform.Universal, +}; + +export const mockMetadata3: ConnectorMetadata = { + ...mockMetadata, + id: 'id3', + target: 'connector_3', + platform: ConnectorPlatform.Universal, +}; + +export const mockMetadata4: ConnectorMetadata = { + ...mockMetadata, + id: 'id4', + target: 'connector_4', + platform: ConnectorPlatform.Universal, +}; + +export const mockMetadata5: ConnectorMetadata = { + ...mockMetadata, + id: 'id5', + target: 'connector_5', + platform: ConnectorPlatform.Universal, +}; + +export const mockMetadata6: ConnectorMetadata = { + ...mockMetadata, + id: 'id6', + target: 'connector_6', + platform: ConnectorPlatform.Universal, +}; + +export const mockConnector0: Connector = { + id: 'id0', + enabled: true, + config: {}, + createdAt: 1_234_567_890_123, +}; + +export const mockConnector1: Connector = { + id: 'id1', + enabled: true, + config: {}, + createdAt: 1_234_567_890_234, +}; + +export const mockConnector2: Connector = { + id: 'id2', + enabled: true, + config: {}, + createdAt: 1_234_567_890_345, +}; + +export const mockConnector3: Connector = { + id: 'id3', + enabled: true, + config: {}, + createdAt: 1_234_567_890_456, +}; + +export const mockConnector4: Connector = { + id: 'id4', + enabled: true, + config: {}, + createdAt: 1_234_567_890_567, +}; + +export const mockConnector5: Connector = { + id: 'id5', + enabled: true, + config: {}, + createdAt: 1_234_567_890_567, +}; + +export const mockConnector6: Connector = { + id: 'id6', + enabled: true, + config: {}, + createdAt: 1_234_567_890_567, +}; diff --git a/packages/core/src/__mocks__/connector.ts b/packages/core/src/__mocks__/connector.ts index 69e6c99c2..84181b3f9 100644 --- a/packages/core/src/__mocks__/connector.ts +++ b/packages/core/src/__mocks__/connector.ts @@ -1,33 +1,29 @@ import { ConnectorPlatform } from '@logto/connector-kit'; -import type { Connector, ConnectorMetadata } from '@logto/schemas'; +import type { Connector } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; import { any } from 'zod'; import type { LogtoConnector } from '@/connectors/types'; -export const mockMetadata: ConnectorMetadata = { - id: 'id', - target: 'connector', - platform: null, - name: { - en: 'Connector', - 'pt-PT': 'Conector', - 'zh-CN': '连接器', - 'tr-TR': 'Connector', - ko: 'Connector', - }, - logo: './logo.png', - logoDark: './logo-dark.png', - description: { - en: 'Connector', - 'pt-PT': 'Conector', - 'zh-CN': '连接器', - 'tr-TR': 'Connector', - ko: 'Connector', - }, - readme: 'README.md', - configTemplate: 'config-template.json', -}; +import { + mockConnector0, + mockConnector1, + mockConnector2, + mockConnector3, + mockConnector4, + mockConnector5, + mockConnector6, + mockMetadata, + mockMetadata0, + mockMetadata1, + mockMetadata2, + mockMetadata3, + mockMetadata4, + mockMetadata5, + mockMetadata6, +} from './connector-base-data'; + +export { mockMetadata } from './connector-base-data'; export const mockConnector: Connector = { id: 'id', @@ -44,104 +40,6 @@ export const mockLogtoConnector = { configGuard: any(), }; -const mockMetadata0: ConnectorMetadata = { - ...mockMetadata, - id: 'id0', - target: 'connector_0', - platform: ConnectorPlatform.Universal, -}; - -const mockMetadata1: ConnectorMetadata = { - ...mockMetadata, - id: 'id1', - target: 'connector_1', - platform: ConnectorPlatform.Universal, -}; - -const mockMetadata2: ConnectorMetadata = { - ...mockMetadata, - id: 'id2', - target: 'connector_2', - platform: ConnectorPlatform.Universal, -}; - -const mockMetadata3: ConnectorMetadata = { - ...mockMetadata, - id: 'id3', - target: 'connector_3', - platform: ConnectorPlatform.Universal, -}; - -const mockMetadata4: ConnectorMetadata = { - ...mockMetadata, - id: 'id4', - target: 'connector_4', - platform: ConnectorPlatform.Universal, -}; - -const mockMetadata5: ConnectorMetadata = { - ...mockMetadata, - id: 'id5', - target: 'connector_5', - platform: ConnectorPlatform.Universal, -}; - -const mockMetadata6: ConnectorMetadata = { - ...mockMetadata, - id: 'id6', - target: 'connector_6', - platform: ConnectorPlatform.Universal, -}; - -const mockConnector0: Connector = { - id: 'id0', - enabled: true, - config: {}, - createdAt: 1_234_567_890_123, -}; - -const mockConnector1: Connector = { - id: 'id1', - enabled: true, - config: {}, - createdAt: 1_234_567_890_234, -}; - -const mockConnector2: Connector = { - id: 'id2', - enabled: true, - config: {}, - createdAt: 1_234_567_890_345, -}; - -const mockConnector3: Connector = { - id: 'id3', - enabled: true, - config: {}, - createdAt: 1_234_567_890_456, -}; - -const mockConnector4: Connector = { - id: 'id4', - enabled: true, - config: {}, - createdAt: 1_234_567_890_567, -}; - -const mockConnector5: Connector = { - id: 'id5', - enabled: true, - config: {}, - createdAt: 1_234_567_890_567, -}; - -const mockConnector6: Connector = { - id: 'id6', - enabled: true, - config: {}, - createdAt: 1_234_567_890_567, -}; - export const mockConnectorList: Connector[] = [ mockConnector0, mockConnector1, @@ -312,3 +210,52 @@ export const mockLogtoConnectors = [ mockWechatConnector, mockWechatNativeConnector, ]; + +export const disabledSocialTarget01 = 'disableSocialTarget-id01'; +export const disabledSocialTarget02 = 'disableSocialTarget-id02'; +export const enabledSocialTarget01 = 'enabledSocialTarget-id01'; + +export const mockSocialConnectors: LogtoConnector[] = [ + { + dbEntry: { + id: 'id0', + enabled: false, + config: {}, + createdAt: 1_234_567_890_123, + }, + metadata: { + ...mockMetadata, + target: disabledSocialTarget01, + }, + type: ConnectorType.Social, + ...mockLogtoConnector, + }, + { + dbEntry: { + id: 'id1', + enabled: true, + config: {}, + createdAt: 1_234_567_890_123, + }, + metadata: { + ...mockMetadata, + target: enabledSocialTarget01, + }, + type: ConnectorType.Social, + ...mockLogtoConnector, + }, + { + dbEntry: { + id: 'id2', + enabled: false, + config: {}, + createdAt: 1_234_567_890_123, + }, + metadata: { + ...mockMetadata, + target: disabledSocialTarget02, + }, + type: ConnectorType.Social, + ...mockLogtoConnector, + }, +]; 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 068ebd525..b871a0469 100644 --- a/packages/core/src/lib/sign-in-experience/index.test.ts +++ b/packages/core/src/lib/sign-in-experience/index.test.ts @@ -1,22 +1,53 @@ 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 { mockBranding } from '@/__mocks__'; +import { + disabledSocialTarget01, + disabledSocialTarget02, + enabledSocialTarget01, + mockBranding, + mockSignInExperience, + mockSocialConnectors, +} from '@/__mocks__'; +import type { LogtoConnector } from '@/connectors/types'; import RequestError from '@/errors/RequestError'; import { validateBranding, validateTermsOfUse, validateLanguageInfo, + removeUnavailableSocialConnectorTargets, } from '@/lib/sign-in-experience'; +import { updateDefaultSignInExperience } from '@/queries/sign-in-experience'; 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('@/queries/custom-phrase', () => ({ findAllCustomLanguageTags: async () => findAllCustomLanguageTags(), })); +jest.mock('@/connectors', () => ({ + getLogtoConnectors: async () => getLogtoConnectorsPlaceHolder(), +})); + +jest.mock('@/queries/sign-in-experience', () => ({ + findDefaultSignInExperience: async () => findDefaultSignInExperience(), + updateDefaultSignInExperience: jest.fn( + async (data: Partial): Promise => ({ + ...mockSignInExperience, + ...data, + }) + ), +})); + beforeEach(() => { jest.clearAllMocks(); }); @@ -123,3 +154,25 @@ describe('validate terms of use', () => { }).toMatchError(new RequestError('sign_in_experiences.empty_content_url_of_terms_of_use')); }); }); + +describe('remove unavailable social connector targets', () => { + test('should remove unavailable social connector targets in sign-in experience', async () => { + const mockSocialConnectorTargets = mockSocialConnectors.map( + ({ metadata: { target } }) => target + ); + findDefaultSignInExperience.mockResolvedValueOnce({ + ...mockSignInExperience, + socialSignInConnectorTargets: mockSocialConnectorTargets, + }); + getLogtoConnectorsPlaceHolder.mockResolvedValueOnce(mockSocialConnectors); + expect(mockSocialConnectorTargets).toEqual([ + disabledSocialTarget01, + enabledSocialTarget01, + disabledSocialTarget02, + ]); + await removeUnavailableSocialConnectorTargets(); + expect(updateDefaultSignInExperience).toBeCalledWith({ + socialSignInConnectorTargets: [enabledSocialTarget01], + }); + }); +}); diff --git a/packages/core/src/lib/sign-in-experience/index.ts b/packages/core/src/lib/sign-in-experience/index.ts index 81d522bde..304544434 100644 --- a/packages/core/src/lib/sign-in-experience/index.ts +++ b/packages/core/src/lib/sign-in-experience/index.ts @@ -1,9 +1,14 @@ import { builtInLanguages } from '@logto/phrases-ui'; import type { Branding, LanguageInfo, TermsOfUse } from '@logto/schemas'; -import { BrandingStyle } from '@logto/schemas'; +import { ConnectorType, BrandingStyle } from '@logto/schemas'; +import { getLogtoConnectors } from '@/connectors'; import RequestError from '@/errors/RequestError'; import { findAllCustomLanguageTags } from '@/queries/custom-phrase'; +import { + findDefaultSignInExperience, + updateDefaultSignInExperience, +} from '@/queries/sign-in-experience'; import assertThat from '@/utils/assert-that'; export * from './sign-up'; @@ -35,3 +40,19 @@ export const validateTermsOfUse = (termsOfUse: TermsOfUse) => { 'sign_in_experiences.empty_content_url_of_terms_of_use' ); }; + +export const removeUnavailableSocialConnectorTargets = async () => { + const connectors = await getLogtoConnectors(); + const availableSocialConnectorTargets = new Set( + connectors + .filter(({ type, dbEntry: { enabled } }) => enabled && type === ConnectorType.Social) + .map(({ metadata: { target } }) => target) + ); + + const { socialSignInConnectorTargets } = await findDefaultSignInExperience(); + await updateDefaultSignInExperience({ + socialSignInConnectorTargets: socialSignInConnectorTargets.filter((target) => + availableSocialConnectorTargets.has(target) + ), + }); +}; diff --git a/packages/core/src/routes/connector.ts b/packages/core/src/routes/connector.ts index 0663ed4f6..c03e25fbb 100644 --- a/packages/core/src/routes/connector.ts +++ b/packages/core/src/routes/connector.ts @@ -7,6 +7,7 @@ import { object, string } from 'zod'; import { getLogtoConnectorById, getLogtoConnectors } from '@/connectors'; import type { LogtoConnector } from '@/connectors/types'; import RequestError from '@/errors/RequestError'; +import { removeUnavailableSocialConnectorTargets } from '@/lib/sign-in-experience'; import koaGuard from '@/middleware/koa-guard'; import { updateConnector } from '@/queries/connector'; import assertThat from '@/utils/assert-that'; @@ -115,6 +116,12 @@ export default function connectorRoutes(router: T) { where: { id }, jsonbMode: 'merge', }); + + // Delete the social connector in the sign-in experience if it is disabled. + if (!enabled && type === ConnectorType.Social) { + await removeUnavailableSocialConnectorTargets(); + } + ctx.body = { ...connector, metadata, type }; return next(); diff --git a/packages/core/src/routes/connector.update.test.ts b/packages/core/src/routes/connector.update.test.ts index 2bd028b16..3e9eb3094 100644 --- a/packages/core/src/routes/connector.update.test.ts +++ b/packages/core/src/routes/connector.update.test.ts @@ -46,6 +46,10 @@ jest.mock('@/connectors', () => ({ getLogtoConnectorById: async (connectorId: string) => getLogtoConnectorByIdPlaceholder(connectorId), })); +jest.mock('@/lib/sign-in-experience', () => ({ + // eslint-disable-next-line @typescript-eslint/no-empty-function + removeUnavailableSocialConnectorTargets: async () => {}, +})); describe('connector PATCH routes', () => { const connectorRequest = createRequester({ authedRoutes: connectorRoutes });