0
Fork 0
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:
Darcy Ye 2022-12-07 11:57:20 +08:00 committed by GitHub
parent 29f2ce45d4
commit f989cc8ef1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 141 additions and 2 deletions

View file

@ -24,6 +24,8 @@ import {
} from './connector-base-data.js';
export {
mockConnector0,
mockConnector1,
mockMetadata,
mockMetadata0,
mockMetadata1,

View 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();
});
});

View 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);
}
};

View file

@ -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', () => {

View file

@ -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;

View file

@ -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.',

View file

@ -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.',

View file

@ -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.",

View file

@ -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: '휴대전화번호 그리고 이메일이 비어있어요.',

View file

@ -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.',

View file

@ -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.',

View file

@ -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: '手机号与邮箱地址均为空',