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 SmsConnector,
|
||||||
type EmailConnector,
|
type EmailConnector,
|
||||||
demoConnectorIds,
|
demoConnectorIds,
|
||||||
|
ServiceConnector,
|
||||||
VerificationCodeType,
|
VerificationCodeType,
|
||||||
} from '@logto/connector-kit';
|
} from '@logto/connector-kit';
|
||||||
import { phoneRegEx, emailRegEx } from '@logto/core-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 { string, object } from 'zod';
|
||||||
|
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
import koaGuard from '#src/middleware/koa-guard.js';
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
import { loadConnectorFactories } from '#src/utils/connectors/index.js';
|
import { loadConnectorFactories } from '#src/utils/connectors/index.js';
|
||||||
|
import { getCloudConnectionEndpoints } from '#src/utils/endpoint.js';
|
||||||
|
|
||||||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||||
|
|
||||||
export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
|
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(
|
router.post(
|
||||||
'/connectors/:factoryId/test',
|
'/connectors/:factoryId/test',
|
||||||
koaGuard({
|
koaGuard({
|
||||||
|
@ -35,7 +49,7 @@ export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
|
||||||
params: { factoryId },
|
params: { factoryId },
|
||||||
body,
|
body,
|
||||||
} = ctx.guard;
|
} = ctx.guard;
|
||||||
const { phone, email, config } = body;
|
const { phone, email, config: originalConfig } = body;
|
||||||
|
|
||||||
const subject = phone ?? email;
|
const subject = phone ?? email;
|
||||||
assertThat(subject, new RequestError({ code: 'guard.invalid_input' }));
|
assertThat(subject, new RequestError({ code: 'guard.invalid_input' }));
|
||||||
|
@ -64,6 +78,7 @@ export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
|
||||||
rawConnector: { sendMessage },
|
rawConnector: { sendMessage },
|
||||||
} = await buildRawConnector<SmsConnector | EmailConnector>(connectorFactory);
|
} = await buildRawConnector<SmsConnector | EmailConnector>(connectorFactory);
|
||||||
|
|
||||||
|
const config = await configPatcher(factoryId, originalConfig);
|
||||||
await sendMessage(
|
await sendMessage(
|
||||||
{
|
{
|
||||||
to: subject,
|
to: subject,
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import { buildRawConnector } from '@logto/cli/lib/connector/index.js';
|
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 {
|
import {
|
||||||
connectorFactoryResponseGuard,
|
connectorFactoryResponseGuard,
|
||||||
Connectors,
|
Connectors,
|
||||||
ConnectorType,
|
ConnectorType,
|
||||||
connectorResponseGuard,
|
connectorResponseGuard,
|
||||||
|
type JsonObject,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import { buildIdGenerator } from '@logto/shared';
|
import { buildIdGenerator } from '@logto/shared';
|
||||||
import cleanDeep from 'clean-deep';
|
import cleanDeep from 'clean-deep';
|
||||||
|
@ -22,6 +28,7 @@ import {
|
||||||
buildExtraInfoFromEmailServiceData,
|
buildExtraInfoFromEmailServiceData,
|
||||||
} from '#src/utils/connectors/index.js';
|
} from '#src/utils/connectors/index.js';
|
||||||
import { checkSocialConnectorTargetAndPlatformUniqueness } from '#src/utils/connectors/platform.js';
|
import { checkSocialConnectorTargetAndPlatformUniqueness } from '#src/utils/connectors/platform.js';
|
||||||
|
import { getCloudConnectionEndpoints } from '#src/utils/endpoint.js';
|
||||||
|
|
||||||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||||
|
|
||||||
|
@ -30,6 +37,20 @@ import connectorConfigTestingRoutes from './config-testing.js';
|
||||||
|
|
||||||
const generateConnectorId = buildIdGenerator(12);
|
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>(
|
export default function connectorRoutes<T extends AuthedRouter>(
|
||||||
...[router, tenant]: RouterInitArgs<T>
|
...[router, tenant]: RouterInitArgs<T>
|
||||||
) {
|
) {
|
||||||
|
@ -44,6 +65,7 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
||||||
const { getLogtoConnectorById, getLogtoConnectors } = tenant.connectors;
|
const { getLogtoConnectorById, getLogtoConnectors } = tenant.connectors;
|
||||||
const {
|
const {
|
||||||
signInExperiences: { removeUnavailableSocialConnectorTargets },
|
signInExperiences: { removeUnavailableSocialConnectorTargets },
|
||||||
|
logtoConfigs: { getCloudConnectionData },
|
||||||
} = tenant.libraries;
|
} = tenant.libraries;
|
||||||
|
|
||||||
// Will accept other source of `extraInfo` in the future.
|
// 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 });
|
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(
|
router.post(
|
||||||
'/connectors',
|
'/connectors',
|
||||||
koaGuard({
|
koaGuard({
|
||||||
|
@ -71,7 +102,7 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
||||||
}),
|
}),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const {
|
const {
|
||||||
body: { id: proposedId, connectorId, metadata, config, syncProfile },
|
body: { id: proposedId, connectorId, metadata, config: originalConfig, syncProfile },
|
||||||
} = ctx.guard;
|
} = ctx.guard;
|
||||||
|
|
||||||
const connectorFactories = await loadConnectorFactories();
|
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) {
|
if (config) {
|
||||||
const { rawConnector } = await buildRawConnector(connectorFactory);
|
const { rawConnector } = await buildRawConnector(connectorFactory);
|
||||||
validateConfig(config, rawConnector.configGuard);
|
validateConfig(config, rawConnector.configGuard);
|
||||||
|
@ -164,7 +196,10 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
||||||
}
|
}
|
||||||
|
|
||||||
const connector = await getLogtoConnectorById(insertConnectorId);
|
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();
|
return next();
|
||||||
}
|
}
|
||||||
|
@ -200,7 +235,10 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
||||||
|
|
||||||
ctx.body = await Promise.all(
|
ctx.body = await Promise.all(
|
||||||
filteredConnectors.map(async (connector) =>
|
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
|
// Hide demo connector
|
||||||
assertThat(!demoConnectorIds.includes(connector.metadata.id), 'connector.not_found');
|
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();
|
return next();
|
||||||
}
|
}
|
||||||
|
@ -287,7 +328,7 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const {
|
const {
|
||||||
params: { id },
|
params: { id },
|
||||||
body: { config, metadata, syncProfile },
|
body: { config: originalConfig, metadata, syncProfile },
|
||||||
} = ctx.guard;
|
} = ctx.guard;
|
||||||
|
|
||||||
const { type, validateConfig, metadata: originalMetadata } = await getLogtoConnectorById(id);
|
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) {
|
if (config) {
|
||||||
validateConfig(config);
|
validateConfig(config);
|
||||||
}
|
}
|
||||||
|
@ -324,7 +366,10 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
||||||
jsonbMode: 'replace',
|
jsonbMode: 'replace',
|
||||||
});
|
});
|
||||||
const connector = await getLogtoConnectorById(id);
|
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();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,12 @@ import {
|
||||||
type SmsConnector,
|
type SmsConnector,
|
||||||
ServiceConnector,
|
ServiceConnector,
|
||||||
} from '@logto/connector-kit';
|
} 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 { findPackage } from '@logto/shared';
|
||||||
import { conditional, deduplicate, pick, trySafe, type Optional } from '@silverhand/essentials';
|
import { conditional, deduplicate, pick, trySafe, type Optional } from '@silverhand/essentials';
|
||||||
|
|
||||||
|
@ -30,7 +35,10 @@ export const isPasswordlessLogtoConnector = (
|
||||||
|
|
||||||
export const transpileLogtoConnector = async (
|
export const transpileLogtoConnector = async (
|
||||||
connector: LogtoConnector,
|
connector: LogtoConnector,
|
||||||
extraInfo?: ConnectorResponse['extraInfo']
|
payload?: {
|
||||||
|
extraInfo?: ConnectorResponse['extraInfo'];
|
||||||
|
configPruner?: (config: JsonObject) => JsonObject;
|
||||||
|
}
|
||||||
): Promise<ConnectorResponse> => {
|
): Promise<ConnectorResponse> => {
|
||||||
const usagePayload = conditional(
|
const usagePayload = conditional(
|
||||||
/** Should do the check in advance since only passwordless connectors could have `getUsage` method. */
|
/** 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. */
|
/** Temporarily block entering Logto email connector as well until this feature is ready for prod. */
|
||||||
const isDemo = demoConnectorIds.includes(id) || serviceConnectorIds.includes(id);
|
const isDemo = demoConnectorIds.includes(id) || serviceConnectorIds.includes(id);
|
||||||
|
|
||||||
|
const { extraInfo, configPruner } = payload ?? {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
...metadata,
|
...metadata,
|
||||||
|
@ -51,7 +61,7 @@ export const transpileLogtoConnector = async (
|
||||||
isDemo,
|
isDemo,
|
||||||
extraInfo,
|
extraInfo,
|
||||||
// Hide demo connector config
|
// Hide demo connector config
|
||||||
config: isDemo ? {} : config,
|
config: isDemo ? {} : configPruner ? configPruner(config) : config,
|
||||||
...usagePayload,
|
...usagePayload,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,9 +2,7 @@ import { appendPath } from '@silverhand/essentials';
|
||||||
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
|
|
||||||
/** Will use this method in upcoming changes. */
|
export const getCloudConnectionEndpoints = () => {
|
||||||
// eslint-disable-next-line import/no-unused-modules
|
|
||||||
export const getCloudConnectionEndpoints = async () => {
|
|
||||||
const { cloudUrlSet, adminUrlSet } = EnvSet.values;
|
const { cloudUrlSet, adminUrlSet } = EnvSet.values;
|
||||||
return {
|
return {
|
||||||
tokenEndpoint: appendPath(adminUrlSet.endpoint, 'oidc/token').toString(),
|
tokenEndpoint: appendPath(adminUrlSet.endpoint, 'oidc/token').toString(),
|
||||||
|
|
Loading…
Reference in a new issue