mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
feat: prohibit multiple same-platform social connectors to share same target (#2577)
This commit is contained in:
parent
29f2ce45d4
commit
f989cc8ef1
12 changed files with 141 additions and 2 deletions
|
@ -24,6 +24,8 @@ import {
|
|||
} from './connector-base-data.js';
|
||||
|
||||
export {
|
||||
mockConnector0,
|
||||
mockConnector1,
|
||||
mockMetadata,
|
||||
mockMetadata0,
|
||||
mockMetadata1,
|
||||
|
|
46
packages/core/src/lib/connector.test.ts
Normal file
46
packages/core/src/lib/connector.test.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { ConnectorType } from '@logto/schemas';
|
||||
|
||||
import {
|
||||
mockMetadata0,
|
||||
mockMetadata1,
|
||||
mockConnector0,
|
||||
mockConnector1,
|
||||
mockLogtoConnector,
|
||||
mockLogtoConnectorList,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
||||
import { checkSocialConnectorTargetAndPlatformUniqueness } from './connector.js';
|
||||
|
||||
describe('check social connector target and platform uniqueness', () => {
|
||||
it('throws if more than one same-platform social connectors sharing the same `target`', () => {
|
||||
const mockConnectors = [
|
||||
{
|
||||
dbEntry: mockConnector0,
|
||||
metadata: { ...mockMetadata0, target: 'target' },
|
||||
type: ConnectorType.Social,
|
||||
...mockLogtoConnector,
|
||||
},
|
||||
{
|
||||
dbEntry: mockConnector1,
|
||||
metadata: { ...mockMetadata1, target: 'target' },
|
||||
type: ConnectorType.Social,
|
||||
...mockLogtoConnector,
|
||||
},
|
||||
];
|
||||
expect(() => {
|
||||
checkSocialConnectorTargetAndPlatformUniqueness(mockConnectors);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'connector.multiple_target_with_same_platform',
|
||||
status: 400,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw when no multiple connectors sharing same target and platform', () => {
|
||||
expect(() => {
|
||||
checkSocialConnectorTargetAndPlatformUniqueness(mockLogtoConnectorList);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
27
packages/core/src/lib/connector.ts
Normal file
27
packages/core/src/lib/connector.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { ConnectorType } from '@logto/schemas';
|
||||
|
||||
import type { LogtoConnector } from '#src/connectors/types.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
export const checkSocialConnectorTargetAndPlatformUniqueness = (connectors: LogtoConnector[]) => {
|
||||
const targetAndPlatformObjectsInUse = connectors
|
||||
.filter(({ type }) => type === ConnectorType.Social)
|
||||
.map(({ metadata: { target, platform } }) => ({
|
||||
target,
|
||||
platform,
|
||||
}));
|
||||
|
||||
const targetAndPlatformSet = new Set<string>();
|
||||
|
||||
for (const targetAndPlatformObject of targetAndPlatformObjectsInUse) {
|
||||
const { target, platform } = targetAndPlatformObject;
|
||||
|
||||
if (platform === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const element = JSON.stringify([target, platform]);
|
||||
assertThat(!targetAndPlatformSet.has(element), 'connector.multiple_target_with_same_platform');
|
||||
targetAndPlatformSet.add(element);
|
||||
}
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import type { EmailConnector, SmsConnector } from '@logto/connector-kit';
|
||||
import { MessageTypes } from '@logto/connector-kit';
|
||||
import { ConnectorPlatform, MessageTypes } from '@logto/connector-kit';
|
||||
import { ConnectorType } from '@logto/schemas';
|
||||
import { any } from 'zod';
|
||||
|
||||
|
@ -36,6 +36,10 @@ const getLogtoConnectorsPlaceHolder = jest.fn() as jest.MockedFunction<
|
|||
() => Promise<LogtoConnector[]>
|
||||
>;
|
||||
|
||||
jest.mock('#src/lib/connector.js', () => ({
|
||||
checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('#src/lib/sign-in-experience/index.js', () => ({
|
||||
removeUnavailableSocialConnectorTargets: jest.fn(),
|
||||
}));
|
||||
|
@ -259,6 +263,39 @@ describe('connector route', () => {
|
|||
);
|
||||
expect(deleteConnectorByIds).toHaveBeenCalledWith(['id']);
|
||||
});
|
||||
|
||||
it('throws when add more than 1 social connector instance with same target and platform', async () => {
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
...mockConnectorFactory,
|
||||
metadata: {
|
||||
...mockConnectorFactory.metadata,
|
||||
id: 'id0',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
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',
|
||||
metadata: { target: 'target' },
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 422);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /connectors/:id/test', () => {
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from '#src/connectors/index.js';
|
||||
import type { LogtoConnector } from '#src/connectors/types.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { checkSocialConnectorTargetAndPlatformUniqueness } from '#src/lib/connector.js';
|
||||
import { removeUnavailableSocialConnectorTargets } from '#src/lib/sign-in-experience/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import {
|
||||
|
@ -52,6 +53,8 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
const { target: filterTarget } = ctx.query;
|
||||
const connectors = await getLogtoConnectors();
|
||||
|
||||
checkSocialConnectorTargetAndPlatformUniqueness(connectors);
|
||||
|
||||
assertThat(
|
||||
connectors.filter((connector) => connector.type === ConnectorType.Email).length <= 1,
|
||||
'connector.more_than_one_email'
|
||||
|
@ -108,12 +111,12 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
syncProfile: true,
|
||||
}),
|
||||
}),
|
||||
// eslint-disable-next-line complexity
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
body: { connectorId },
|
||||
body,
|
||||
} = ctx.guard;
|
||||
|
||||
const connectorFactories = await loadConnectorFactories();
|
||||
const connectorFactory = connectorFactories.find(
|
||||
({ metadata: { id } }) => id === connectorId
|
||||
|
@ -135,6 +138,17 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
})
|
||||
);
|
||||
|
||||
if (body.metadata?.target && connectorFactory.type === ConnectorType.Social) {
|
||||
const connectors = await getLogtoConnectors();
|
||||
assertThat(
|
||||
!connectors.some(
|
||||
({ metadata: { target, platform } }) =>
|
||||
target === body.metadata?.target && platform === connectorFactory.metadata.platform
|
||||
),
|
||||
new RequestError({ code: 'connector.multiple_target_with_same_platform', status: 422 })
|
||||
);
|
||||
}
|
||||
|
||||
const insertConnectorId = generateConnectorId();
|
||||
const { metadata, ...rest } = body;
|
||||
|
||||
|
|
|
@ -106,6 +106,8 @@ const errors = {
|
|||
'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.',
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.',
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: 'Telefonnummer oder E-Mail darf nicht leer sein.',
|
||||
|
|
|
@ -105,6 +105,8 @@ const errors = {
|
|||
'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.',
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.',
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: 'Both phone and email are empty.',
|
||||
|
|
|
@ -112,6 +112,8 @@ const errors = {
|
|||
'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
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: "Le téléphone et l'email sont vides.",
|
||||
|
|
|
@ -104,6 +104,8 @@ const errors = {
|
|||
'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
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: '휴대전화번호 그리고 이메일이 비어있어요.',
|
||||
|
|
|
@ -107,6 +107,8 @@ const errors = {
|
|||
'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
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: 'O campos telefone e email estão vazios.',
|
||||
|
|
|
@ -106,6 +106,8 @@ const errors = {
|
|||
'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
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: 'Hem telefon hem de e-posta adresi yok.',
|
||||
|
|
|
@ -101,6 +101,7 @@ const errors = {
|
|||
multiple_instances_not_supported: '你选择的连接器不支持创建多实例。',
|
||||
invalid_type_for_syncing_profile: '只有社交连接器可以开启用户档案同步。',
|
||||
can_not_modify_target: '不可修改连接器 target。',
|
||||
multiple_target_with_same_platform: '不能同时存在多个有相同 target 和平台类型的社交连接器。',
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: '手机号与邮箱地址均为空',
|
||||
|
|
Loading…
Add table
Reference in a new issue