0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(core): refactor post connector endpoint (#4681)

* refactor(core): refactor post connector endpoint

refactor post connector endpoint

* fix(core): fix ut

fix ut
This commit is contained in:
simeng-li 2023-10-17 17:42:04 +08:00 committed by GitHub
parent 98b2eed6ec
commit 8abca1a5d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 78 additions and 40 deletions

View file

@ -23,11 +23,13 @@ export const loadConnectorFactories = async (
try {
const createConnector = await loadConnector(packagePath, ignoreVersionMismatch);
const rawConnector = await createConnector({ getConfig: notImplemented });
validateConnectorModule(rawConnector);
return {
metadata: await parseMetadata(rawConnector.metadata, packagePath),
type: rawConnector.type,
configGuard: rawConnector.configGuard,
createConnector,
path: packagePath,
};

View file

@ -8,7 +8,7 @@ export type ConnectorFactory<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Router<any, BaseRoutes, string>,
U extends AllConnector = AllConnector,
> = Pick<U, 'type' | 'metadata'> & {
> = Pick<U, 'type' | 'metadata' | 'configGuard'> & {
createConnector: CreateConnector<U, T>;
path: string;
};

View file

@ -59,6 +59,7 @@ export const mockConnectorFactory: ConnectorFactory<typeof router> = {
metadata: mockMetadata,
type: ConnectorType.Social,
path: 'random_path',
configGuard: any(),
createConnector: jest.fn(),
};

View file

@ -1,7 +1,7 @@
import { buildRawConnector, defaultConnectorMethods } from '@logto/cli/lib/connector/index.js';
import type { AllConnector } from '@logto/connector-kit';
import { validateConfig, ServiceConnector } from '@logto/connector-kit';
import { conditional, pick, trySafe } from '@silverhand/essentials';
import type { AllConnector, ConnectorPlatform } from '@logto/connector-kit';
import { validateConfig, ServiceConnector, ConnectorType } from '@logto/connector-kit';
import { type Nullable, conditional, pick, trySafe } from '@silverhand/essentials';
import RequestError from '#src/errors/RequestError/index.js';
import type Queries from '#src/tenants/Queries.js';
@ -119,10 +119,26 @@ export const createConnectorLibrary = (
return pickedConnector;
};
const getLogtoConnectorByTargetAndPlatform = async (
target: string,
platform: Nullable<ConnectorPlatform>
) => {
const connectors = await getLogtoConnectors();
return connectors.find(({ type, metadata }) => {
return (
type === ConnectorType.Social &&
metadata.target === target &&
metadata.platform === platform
);
});
};
return {
getConnectorConfig,
getLogtoConnectors,
getLogtoConnectorsWellKnown,
getLogtoConnectorById,
getLogtoConnectorByTargetAndPlatform,
};
};

View file

@ -2,6 +2,7 @@ import { ConnectorPlatform } from '@logto/connector-kit';
import type { Connector } from '@logto/schemas';
import { ConnectorType } from '@logto/schemas';
import { pickDefault, createMockUtils } from '@logto/shared/esm';
import { type Nullable } from '@silverhand/essentials';
import { any } from 'zod';
import {
@ -65,6 +66,20 @@ const tenantContext = new MockTenant(
return connector;
},
getLogtoConnectorByTargetAndPlatform: async (
target: string,
platform: Nullable<ConnectorPlatform>
) => {
const connectors = await getLogtoConnectors();
return connectors.find(({ type, metadata }) => {
return (
type === ConnectorType.Social &&
metadata.target === target &&
metadata.platform === platform
);
});
},
},
{
quota: createMockQuotaLibrary(),

View file

@ -1,4 +1,4 @@
import { type ConnectorFactory, buildRawConnector } from '@logto/cli/lib/connector/index.js';
import { type ConnectorFactory } from '@logto/cli/lib/connector/index.js';
import type router from '@logto/cloud/routes';
import { demoConnectorIds, validateConfig } from '@logto/connector-kit';
import { Connectors, ConnectorType, connectorResponseGuard, type JsonObject } from '@logto/schemas';
@ -33,6 +33,8 @@ const guardConnectorsQuota = async (
}
};
const passwordlessConnector = new Set([ConnectorType.Email, ConnectorType.Sms]);
export default function connectorRoutes<T extends AuthedRouter>(
...[router, tenant]: RouterInitArgs<T>
) {
@ -44,7 +46,8 @@ export default function connectorRoutes<T extends AuthedRouter>(
insertConnector,
updateConnector,
} = tenant.queries.connectors;
const { getLogtoConnectorById, getLogtoConnectors } = tenant.connectors;
const { getLogtoConnectorById, getLogtoConnectors, getLogtoConnectorByTargetAndPlatform } =
tenant.connectors;
const {
quota,
signInExperiences: { removeUnavailableSocialConnectorTargets },
@ -60,7 +63,13 @@ export default function connectorRoutes<T extends AuthedRouter>(
metadata: true,
syncProfile: true,
})
.merge(Connectors.createGuard.pick({ id: true }).partial()), // `id` is optional
/*
Currently the id can not be locked until the connector is successfully created.
Some connectors providers require a pre-generated id to complete the configuration at the IdP side.
Logto connector creation process currently has a hard dependency on the provider's config data.
A optional pre-generated id from the client side is required to complete the connector creation process.
*/
.merge(Connectors.createGuard.pick({ id: true }).partial()),
response: connectorResponseGuard,
status: [200, 422],
}),
@ -84,34 +93,31 @@ export default function connectorRoutes<T extends AuthedRouter>(
await guardConnectorsQuota(connectorFactory, quota);
assertThat(
connectorFactory.metadata.isStandard !== true || Boolean(metadata?.target),
'connector.should_specify_target'
);
assertThat(
connectorFactory.metadata.isStandard === true || metadata === undefined,
'connector.cannot_overwrite_metadata_for_non_standard_connector'
);
const { count } = await countConnectorByConnectorId(connectorId);
assertThat(
count === 0 || connectorFactory.metadata.isStandard === true,
new RequestError({
code: 'connector.multiple_instances_not_supported',
status: 422,
})
);
if (connectorFactory.type === ConnectorType.Social) {
const connectors = await getLogtoConnectors();
const duplicateConnector = connectors
.filter(({ type }) => type === ConnectorType.Social)
.find(
({ metadata: { target, platform } }) =>
target ===
(metadata ? cleanDeep(metadata).target : connectorFactory.metadata.target) &&
platform === connectorFactory.metadata.platform
);
assertThat(
connectorFactory.metadata.isStandard !== true || Boolean(metadata?.target),
'connector.should_specify_target'
);
assertThat(
connectorFactory.metadata.isStandard === true || metadata === undefined,
'connector.cannot_overwrite_metadata_for_non_standard_connector'
);
const { count } = await countConnectorByConnectorId(connectorId);
assertThat(
count === 0 || connectorFactory.metadata.isStandard === true,
new RequestError({
code: 'connector.multiple_instances_not_supported',
status: 422,
})
);
const duplicateConnector = await getLogtoConnectorByTargetAndPlatform(
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
metadata?.target || connectorFactory.metadata.target,
connectorFactory.metadata.platform
);
assertThat(
!duplicateConnector,
new RequestError(
@ -128,11 +134,11 @@ export default function connectorRoutes<T extends AuthedRouter>(
}
if (config) {
const { rawConnector } = await buildRawConnector(connectorFactory);
validateConfig(config, rawConnector.configGuard);
validateConfig(config, connectorFactory.configGuard);
}
const insertConnectorId = proposedId ?? generateStandardShortId();
await insertConnector({
id: insertConnectorId,
connectorId,
@ -142,11 +148,9 @@ export default function connectorRoutes<T extends AuthedRouter>(
/**
* We can have only one working email/sms connector:
* once we insert a new one, old connectors with same type should be deleted.
* TODO: should using transaction to ensure the atomicity of the operation. LOG-7260
*/
if (
connectorFactory.type === ConnectorType.Sms ||
connectorFactory.type === ConnectorType.Email
) {
if (passwordlessConnector.has(connectorFactory.type)) {
const logtoConnectors = await getLogtoConnectors();
const conflictingConnectorIds = logtoConnectors
.filter(