mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(core): update connector APIs for parsing config
This commit is contained in:
parent
44c09baba9
commit
5a9e85986b
4 changed files with 84 additions and 16 deletions
|
@ -4,22 +4,36 @@ import {
|
|||
type SmsConnector,
|
||||
type EmailConnector,
|
||||
demoConnectorIds,
|
||||
ServiceConnector,
|
||||
VerificationCodeType,
|
||||
} from '@logto/connector-kit';
|
||||
import { phoneRegEx, emailRegEx } from '@logto/core-kit';
|
||||
import { jsonObjectGuard, ConnectorType } from '@logto/schemas';
|
||||
import { jsonObjectGuard, ConnectorType, type JsonObject } from '@logto/schemas';
|
||||
import { string, object } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
import { loadConnectorFactories } from '#src/utils/connectors/index.js';
|
||||
import { getCloudConnectionEndpoints } from '#src/utils/endpoint.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||
|
||||
export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
|
||||
...[router]: RouterInitArgs<T>
|
||||
...[router, { libraries }]: RouterInitArgs<T>
|
||||
) {
|
||||
const {
|
||||
logtoConfigs: { getCloudConnectionData },
|
||||
} = libraries;
|
||||
const configPatcher = async (factoryId: string, config?: JsonObject) => {
|
||||
if (!config || ServiceConnector.Email !== factoryId) {
|
||||
return config;
|
||||
}
|
||||
const endpoints = getCloudConnectionEndpoints();
|
||||
const credentials = await getCloudConnectionData();
|
||||
return { ...endpoints, ...credentials, ...config };
|
||||
};
|
||||
|
||||
router.post(
|
||||
'/connectors/:factoryId/test',
|
||||
koaGuard({
|
||||
|
@ -35,7 +49,7 @@ export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
|
|||
params: { factoryId },
|
||||
body,
|
||||
} = ctx.guard;
|
||||
const { phone, email, config } = body;
|
||||
const { phone, email, config: originalConfig } = body;
|
||||
|
||||
const subject = phone ?? email;
|
||||
assertThat(subject, new RequestError({ code: 'guard.invalid_input' }));
|
||||
|
@ -64,6 +78,7 @@ export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
|
|||
rawConnector: { sendMessage },
|
||||
} = await buildRawConnector<SmsConnector | EmailConnector>(connectorFactory);
|
||||
|
||||
const config = await configPatcher(factoryId, originalConfig);
|
||||
await sendMessage(
|
||||
{
|
||||
to: subject,
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
/* eslint-disable max-lines */
|
||||
import { buildRawConnector } from '@logto/cli/lib/connector/index.js';
|
||||
import { demoConnectorIds, validateConfig } from '@logto/connector-kit';
|
||||
import {
|
||||
ServiceConnector,
|
||||
demoConnectorIds,
|
||||
emailServiceBrandingGuard,
|
||||
validateConfig,
|
||||
} from '@logto/connector-kit';
|
||||
import {
|
||||
connectorFactoryResponseGuard,
|
||||
Connectors,
|
||||
ConnectorType,
|
||||
connectorResponseGuard,
|
||||
type JsonObject,
|
||||
} from '@logto/schemas';
|
||||
import { buildIdGenerator } from '@logto/shared';
|
||||
import cleanDeep from 'clean-deep';
|
||||
|
@ -22,6 +28,7 @@ import {
|
|||
buildExtraInfoFromEmailServiceData,
|
||||
} from '#src/utils/connectors/index.js';
|
||||
import { checkSocialConnectorTargetAndPlatformUniqueness } from '#src/utils/connectors/platform.js';
|
||||
import { getCloudConnectionEndpoints } from '#src/utils/endpoint.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||
|
||||
|
@ -30,6 +37,20 @@ import connectorConfigTestingRoutes from './config-testing.js';
|
|||
|
||||
const generateConnectorId = buildIdGenerator(12);
|
||||
|
||||
const configPruner = (factoryId: string, config: JsonObject): JsonObject => {
|
||||
if (ServiceConnector.Email !== factoryId) {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can not use `pick()` from @silverhand/essentials since the pick returns 'undefined' and
|
||||
* this is value is not accepted by `config` return type JsonObject.
|
||||
*/
|
||||
return Object.fromEntries(
|
||||
Object.keys(emailServiceBrandingGuard.shape).map((key) => [key, config[key] ?? null])
|
||||
);
|
||||
};
|
||||
|
||||
export default function connectorRoutes<T extends AuthedRouter>(
|
||||
...[router, tenant]: RouterInitArgs<T>
|
||||
) {
|
||||
|
@ -44,6 +65,7 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
const { getLogtoConnectorById, getLogtoConnectors } = tenant.connectors;
|
||||
const {
|
||||
signInExperiences: { removeUnavailableSocialConnectorTargets },
|
||||
logtoConfigs: { getCloudConnectionData },
|
||||
} = tenant.libraries;
|
||||
|
||||
// Will accept other source of `extraInfo` in the future.
|
||||
|
@ -55,6 +77,15 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
return cleanDeep(extraInfo, { emptyObjects: false });
|
||||
};
|
||||
|
||||
const configPatcher = async (factoryId: string, config?: JsonObject) => {
|
||||
if (!config || ServiceConnector.Email !== factoryId) {
|
||||
return config;
|
||||
}
|
||||
const endpoints = getCloudConnectionEndpoints();
|
||||
const credentials = await getCloudConnectionData();
|
||||
return { ...endpoints, ...credentials, ...config };
|
||||
};
|
||||
|
||||
router.post(
|
||||
'/connectors',
|
||||
koaGuard({
|
||||
|
@ -71,7 +102,7 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
body: { id: proposedId, connectorId, metadata, config, syncProfile },
|
||||
body: { id: proposedId, connectorId, metadata, config: originalConfig, syncProfile },
|
||||
} = ctx.guard;
|
||||
|
||||
const connectorFactories = await loadConnectorFactories();
|
||||
|
@ -130,6 +161,7 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
);
|
||||
}
|
||||
|
||||
const config = await configPatcher(connectorFactory.metadata.id, originalConfig);
|
||||
if (config) {
|
||||
const { rawConnector } = await buildRawConnector(connectorFactory);
|
||||
validateConfig(config, rawConnector.configGuard);
|
||||
|
@ -164,7 +196,10 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
}
|
||||
|
||||
const connector = await getLogtoConnectorById(insertConnectorId);
|
||||
ctx.body = await transpileLogtoConnector(connector, buildExtraInfo(connector.metadata.id));
|
||||
ctx.body = await transpileLogtoConnector(connector, {
|
||||
extraInfo: buildExtraInfo(connector.metadata.id),
|
||||
configPruner: (config) => configPruner(connectorFactory.metadata.id, config),
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
|
@ -200,7 +235,10 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
|
||||
ctx.body = await Promise.all(
|
||||
filteredConnectors.map(async (connector) =>
|
||||
transpileLogtoConnector(connector, buildExtraInfo(connector.metadata.id))
|
||||
transpileLogtoConnector(connector, {
|
||||
extraInfo: buildExtraInfo(connector.metadata.id),
|
||||
configPruner: (config) => configPruner(connector.metadata.id, config),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -224,7 +262,10 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
// Hide demo connector
|
||||
assertThat(!demoConnectorIds.includes(connector.metadata.id), 'connector.not_found');
|
||||
|
||||
ctx.body = await transpileLogtoConnector(connector, buildExtraInfo(connector.metadata.id));
|
||||
ctx.body = await transpileLogtoConnector(connector, {
|
||||
extraInfo: buildExtraInfo(connector.metadata.id),
|
||||
configPruner: (config) => configPruner(connector.metadata.id, config),
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
|
@ -287,7 +328,7 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id },
|
||||
body: { config, metadata, syncProfile },
|
||||
body: { config: originalConfig, metadata, syncProfile },
|
||||
} = ctx.guard;
|
||||
|
||||
const { type, validateConfig, metadata: originalMetadata } = await getLogtoConnectorById(id);
|
||||
|
@ -314,6 +355,7 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
);
|
||||
}
|
||||
|
||||
const config = await configPatcher(originalMetadata.id, originalConfig);
|
||||
if (config) {
|
||||
validateConfig(config);
|
||||
}
|
||||
|
@ -324,7 +366,10 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
jsonbMode: 'replace',
|
||||
});
|
||||
const connector = await getLogtoConnectorById(id);
|
||||
ctx.body = await transpileLogtoConnector(connector, buildExtraInfo(connector.metadata.id));
|
||||
ctx.body = await transpileLogtoConnector(connector, {
|
||||
extraInfo: buildExtraInfo(connector.metadata.id),
|
||||
configPruner: (config) => configPruner(connector.metadata.id, config),
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
|
|
|
@ -14,7 +14,12 @@ import {
|
|||
type SmsConnector,
|
||||
ServiceConnector,
|
||||
} from '@logto/connector-kit';
|
||||
import type { ConnectorFactoryResponse, ConnectorResponse, EmailServiceData } from '@logto/schemas';
|
||||
import type {
|
||||
ConnectorFactoryResponse,
|
||||
ConnectorResponse,
|
||||
EmailServiceData,
|
||||
JsonObject,
|
||||
} from '@logto/schemas';
|
||||
import { findPackage } from '@logto/shared';
|
||||
import { conditional, deduplicate, pick, trySafe, type Optional } from '@silverhand/essentials';
|
||||
|
||||
|
@ -30,7 +35,10 @@ export const isPasswordlessLogtoConnector = (
|
|||
|
||||
export const transpileLogtoConnector = async (
|
||||
connector: LogtoConnector,
|
||||
extraInfo?: ConnectorResponse['extraInfo']
|
||||
payload?: {
|
||||
extraInfo?: ConnectorResponse['extraInfo'];
|
||||
configPruner?: (config: JsonObject) => JsonObject;
|
||||
}
|
||||
): Promise<ConnectorResponse> => {
|
||||
const usagePayload = conditional(
|
||||
/** Should do the check in advance since only passwordless connectors could have `getUsage` method. */
|
||||
|
@ -44,6 +52,8 @@ export const transpileLogtoConnector = async (
|
|||
/** Temporarily block entering Logto email connector as well until this feature is ready for prod. */
|
||||
const isDemo = demoConnectorIds.includes(id) || serviceConnectorIds.includes(id);
|
||||
|
||||
const { extraInfo, configPruner } = payload ?? {};
|
||||
|
||||
return {
|
||||
type,
|
||||
...metadata,
|
||||
|
@ -51,7 +61,7 @@ export const transpileLogtoConnector = async (
|
|||
isDemo,
|
||||
extraInfo,
|
||||
// Hide demo connector config
|
||||
config: isDemo ? {} : config,
|
||||
config: isDemo ? {} : configPruner ? configPruner(config) : config,
|
||||
...usagePayload,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -2,9 +2,7 @@ import { appendPath } from '@silverhand/essentials';
|
|||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
|
||||
/** Will use this method in upcoming changes. */
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export const getCloudConnectionEndpoints = async () => {
|
||||
export const getCloudConnectionEndpoints = () => {
|
||||
const { cloudUrlSet, adminUrlSet } = EnvSet.values;
|
||||
return {
|
||||
tokenEndpoint: appendPath(adminUrlSet.endpoint, 'oidc/token').toString(),
|
||||
|
|
Loading…
Reference in a new issue