0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

feat(core,connector): add cloud connection library (#4122)

This commit is contained in:
Darcy Ye 2023-07-05 20:52:14 +08:00 committed by GitHub
parent 736e99fd63
commit e8492654e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 132 additions and 34 deletions

View file

@ -1,3 +1,4 @@
import { assert } from '@silverhand/essentials';
import { HTTPError, got } from 'got';
import type {
@ -7,7 +8,12 @@ import type {
GetUsageFunction,
SendMessageFunction,
} from '@logto/connector-kit';
import { ConnectorType, validateConfig } from '@logto/connector-kit';
import {
ConnectorType,
validateConfig,
ConnectorError,
ConnectorErrorCodes,
} from '@logto/connector-kit';
import { defaultMetadata, defaultTimeout, emailEndpoint, usageEndpoint } from './constant.js';
import { grantAccessToken } from './grant-access-token.js';
@ -34,6 +40,11 @@ const sendMessage =
} = config;
const { to, type, payload } = data;
assert(
endpoint && tokenEndpoint && resource && appId && appSecret,
new ConnectorError(ConnectorErrorCodes.InvalidConfig)
);
const accessTokenResponse = await grantAccessToken({
tokenEndpoint,
resource,
@ -67,6 +78,11 @@ const getUsage =
const { endpoint, tokenEndpoint, appId, appSecret, resource } = config;
assert(
endpoint && tokenEndpoint && resource && appId && appSecret,
new ConnectorError(ConnectorErrorCodes.InvalidConfig)
);
const accessTokenResponse = await grantAccessToken({
tokenEndpoint,
resource,

View file

@ -1,6 +1,6 @@
import type { AccessTokenResponse, LogtoEmailConfig } from './types.js';
import type { AccessTokenResponse } from './types.js';
export const mockedConfig: LogtoEmailConfig = {
export const mockedConfig = {
appId: 'mfvnO3josReyBf9zhDnlr',
appSecret: 'lXNWW4wPj0Bq6msjIl6H3',
tokenEndpoint: 'http://localhost:3002/oidc/token',

View file

@ -2,13 +2,15 @@ import { z } from 'zod';
import { emailServiceBrandingGuard } from '@logto/connector-kit';
export const emailServiceBasicConfigGuard = z.object({
endpoint: z.string(),
tokenEndpoint: z.string(),
resource: z.string(),
appId: z.string(),
appSecret: z.string(),
});
export const emailServiceBasicConfigGuard = z
.object({
endpoint: z.string(),
tokenEndpoint: z.string(),
resource: z.string(),
appId: z.string(),
appSecret: z.string(),
})
.partial();
export type EmailServiceBasicConfig = z.infer<typeof emailServiceBasicConfigGuard>;

View file

@ -0,0 +1,11 @@
import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js';
export const mockCloudConnectionLibrary: CloudConnectionLibrary = {
getCloudConnectionData: async () => ({
resource: 'https://logto.dev',
appId: 'appId',
appSecret: 'appSecret',
endpoint: 'https://logto.dev/api',
tokenEndpoint: 'https://logto.dev/oidc/token',
}),
};

View file

@ -12,6 +12,7 @@ import { ApplicationType } from '@logto/schemas';
export * from './connector.js';
export * from './sign-in-experience.js';
export * from './cloud-connection.js';
export * from './user.js';
export * from './domain.js';

View file

@ -0,0 +1,34 @@
import { cloudConnectionDataGuard } from '@logto/schemas';
import { appendPath } from '@silverhand/essentials';
import { z } from 'zod';
import { EnvSet } from '#src/env-set/index.js';
import { type LogtoConfigLibrary } from './logto-config.js';
export type CloudConnectionLibrary = ReturnType<typeof createCloudConnectionLibrary>;
// eslint-disable-next-line import/no-unused-modules
export const cloudConnectionGuard = cloudConnectionDataGuard.extend({
tokenEndpoint: z.string(),
endpoint: z.string(),
});
// eslint-disable-next-line import/no-unused-modules
export type CloudConnection = z.infer<typeof cloudConnectionGuard>;
export const createCloudConnectionLibrary = (logtoConfigs: LogtoConfigLibrary) => {
const { getCloudConnectionData: getCloudServiceM2mCredentials } = logtoConfigs;
const getCloudConnectionData = async (): Promise<CloudConnection> => {
const credentials = await getCloudServiceM2mCredentials();
const { cloudUrlSet, adminUrlSet } = EnvSet.values;
return {
...credentials,
tokenEndpoint: appendPath(adminUrlSet.endpoint, 'oidc/token').toString(),
endpoint: appendPath(cloudUrlSet.endpoint, 'api').toString(),
};
};
return { getCloudConnectionData };
};

View file

@ -1,5 +1,6 @@
import type { Connector } from '@logto/schemas';
import { mockCloudConnectionLibrary } from '#src/__mocks__/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import { MockQueries } from '#src/test-utils/tenant.js';
@ -17,7 +18,8 @@ const connectors: Connector[] = [
const { createConnectorLibrary } = await import('./connector.js');
const { getConnectorConfig } = createConnectorLibrary(
new MockQueries({ connectors: { findAllConnectors: async () => connectors } })
new MockQueries({ connectors: { findAllConnectors: async () => connectors } }),
mockCloudConnectionLibrary
);
it('getConnectorConfig() should return right config', async () => {

View file

@ -1,6 +1,6 @@
import { buildRawConnector, defaultConnectorMethods } from '@logto/cli/lib/connector/index.js';
import type { AllConnector } from '@logto/connector-kit';
import { validateConfig } from '@logto/connector-kit';
import { validateConfig, ServiceConnector } from '@logto/connector-kit';
import { pick, trySafe } from '@silverhand/essentials';
import RequestError from '#src/errors/RequestError/index.js';
@ -9,10 +9,16 @@ import assertThat from '#src/utils/assert-that.js';
import { loadConnectorFactories } from '#src/utils/connectors/index.js';
import type { LogtoConnector, LogtoConnectorWellKnown } from '#src/utils/connectors/types.js';
import { type CloudConnectionLibrary } from './cloud-connection.js';
export type ConnectorLibrary = ReturnType<typeof createConnectorLibrary>;
export const createConnectorLibrary = (queries: Queries) => {
export const createConnectorLibrary = (
queries: Queries,
cloudConnection: CloudConnectionLibrary
) => {
const { findAllConnectors, findAllConnectorsWellKnown } = queries.connectors;
const { getCloudConnectionData } = cloudConnection;
const getConnectorConfig = async (id: string): Promise<unknown> => {
const connectors = await findAllConnectors();
@ -20,6 +26,10 @@ export const createConnectorLibrary = (queries: Queries) => {
assertThat(connector, new RequestError({ code: 'entity.not_found', id, status: 404 }));
if (ServiceConnector.Email === connector.connectorId) {
return { ...connector.config, ...(await getCloudConnectionData()) };
}
return connector.config;
};
@ -113,6 +123,7 @@ export const createConnectorLibrary = (queries: Queries) => {
};
return {
getCloudConnectionData,
getConnectorConfig,
getLogtoConnectors,
getLogtoConnectorsWellKnown,

View file

@ -11,6 +11,8 @@ import { z, ZodError } from 'zod';
import type Queries from '#src/tenants/Queries.js';
import { consoleLog } from '#src/utils/console.js';
export type LogtoConfigLibrary = ReturnType<typeof createLogtoConfigLibrary>;
export const createLogtoConfigLibrary = ({
logtoConfigs: { getRowsByKeys, getCloudConnectionData: queryCloudConnectionData },
}: Pick<Queries, 'logtoConfigs'>) => {
@ -47,7 +49,7 @@ export const createLogtoConfigLibrary = ({
const result = cloudConnectionDataGuard.safeParse(value);
if (!result.success) {
return;
throw new Error('Failed to get cloud connection data!');
}
return {

View file

@ -3,6 +3,7 @@ import { builtInLanguages } from '@logto/phrases-ui';
import type { CreateSignInExperience, SignInExperience } from '@logto/schemas';
import {
mockCloudConnectionLibrary,
socialTarget01,
socialTarget02,
mockSignInExperience,
@ -37,7 +38,7 @@ const queries = new MockQueries({
customPhrases,
signInExperiences,
});
const connectorLibrary = createConnectorLibrary(queries);
const connectorLibrary = createConnectorLibrary(queries, mockCloudConnectionLibrary);
const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors');
const { createSignInExperienceLibrary } = await import('./index.js');

View file

@ -6,6 +6,7 @@ import {
demoConnectorIds,
VerificationCodeType,
} from '@logto/connector-kit';
import { ServiceConnector } from '@logto/connector-kit';
import { phoneRegEx, emailRegEx } from '@logto/core-kit';
import { jsonObjectGuard, ConnectorType } from '@logto/schemas';
import { string, object } from 'zod';
@ -18,8 +19,10 @@ import { loadConnectorFactories } from '#src/utils/connectors/index.js';
import type { AuthedRouter, RouterInitArgs } from '../types.js';
export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
...[router]: RouterInitArgs<T>
...[router, { cloudConnection }]: RouterInitArgs<T>
) {
const { getCloudConnectionData } = cloudConnection;
router.post(
'/connectors/:factoryId/test',
koaGuard({
@ -35,7 +38,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 +67,15 @@ export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
rawConnector: { sendMessage },
} = await buildRawConnector<SmsConnector | EmailConnector>(connectorFactory);
/**
* Should manually attach cloud connection data to logto email connector since we directly use
* this `config` to test the `sendMessage` method.
* Logto email connector will no longer save cloud connection data to its `config` after this change.
*/
const config =
ServiceConnector.Email === connectorFactory.metadata.id
? { ...(await getCloudConnectionData()), ...originalConfig }
: originalConfig;
await sendMessage(
{
to: subject,

View file

@ -2,7 +2,6 @@ import { createApplicationLibrary } from '#src/libraries/application.js';
import type { ConnectorLibrary } from '#src/libraries/connector.js';
import { createDomainLibrary } from '#src/libraries/domain.js';
import { createHookLibrary } from '#src/libraries/hook/index.js';
import { createLogtoConfigLibrary } from '#src/libraries/logto-config.js';
import { createPasscodeLibrary } from '#src/libraries/passcode.js';
import { createPhraseLibrary } from '#src/libraries/phrase.js';
import { createResourceLibrary } from '#src/libraries/resource.js';
@ -24,7 +23,6 @@ export default class Libraries {
applications = createApplicationLibrary(this.queries);
verificationStatuses = createVerificationStatusLibrary(this.queries);
domains = createDomainLibrary(this.queries);
logtoConfigs = createLogtoConfigLibrary(this.queries);
constructor(
public readonly tenantId: string,

View file

@ -10,7 +10,9 @@ import type Provider from 'oidc-provider';
import { type RedisCache } from '#src/caches/index.js';
import { WellKnownCache } from '#src/caches/well-known.js';
import { AdminApps, EnvSet, UserApps } from '#src/env-set/index.js';
import { createCloudConnectionLibrary } from '#src/libraries/cloud-connection.js';
import { createConnectorLibrary } from '#src/libraries/connector.js';
import { createLogtoConfigLibrary } from '#src/libraries/logto-config.js';
import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js';
import koaConsoleRedirectProxy from '#src/middleware/koa-console-redirect-proxy.js';
import koaErrorHandler from '#src/middleware/koa-error-handler.js';
@ -52,7 +54,9 @@ export default class Tenant implements TenantContext {
public readonly id: string,
public readonly wellKnownCache: WellKnownCache,
public readonly queries = new Queries(envSet.pool, wellKnownCache),
public readonly connectors = createConnectorLibrary(queries),
public readonly logtoConfigs = createLogtoConfigLibrary(queries),
public readonly cloudConnection = createCloudConnectionLibrary(logtoConfigs),
public readonly connectors = createConnectorLibrary(queries, cloudConnection),
public readonly libraries = new Libraries(id, queries, connectors)
) {
const isAdminTenant = id === adminTenantId;
@ -83,6 +87,8 @@ export default class Tenant implements TenantContext {
id,
provider,
queries,
logtoConfigs,
cloudConnection,
connectors,
libraries,
envSet,

View file

@ -1,7 +1,9 @@
import type Provider from 'oidc-provider';
import type { EnvSet } from '#src/env-set/index.js';
import type { CloudConnectionLibrary } from '#src/libraries/cloud-connection.js';
import type { ConnectorLibrary } from '#src/libraries/connector.js';
import type { LogtoConfigLibrary } from '#src/libraries/logto-config.js';
import type Libraries from './Libraries.js';
import type Queries from './Queries.js';
@ -11,6 +13,8 @@ export default abstract class TenantContext {
public abstract readonly envSet: EnvSet;
public abstract readonly provider: Provider;
public abstract readonly queries: Queries;
public abstract readonly logtoConfigs: LogtoConfigLibrary;
public abstract readonly cloudConnection: CloudConnectionLibrary;
public abstract readonly connectors: ConnectorLibrary;
public abstract readonly libraries: Libraries;
}

View file

@ -2,8 +2,12 @@ import { TtlCache } from '@logto/shared';
import { createMockPool, createMockQueryResult } from 'slonik';
import { WellKnownCache } from '#src/caches/well-known.js';
import type { CloudConnectionLibrary } from '#src/libraries/cloud-connection.js';
import { createCloudConnectionLibrary } from '#src/libraries/cloud-connection.js';
import type { ConnectorLibrary } from '#src/libraries/connector.js';
import { createConnectorLibrary } from '#src/libraries/connector.js';
import { createLogtoConfigLibrary } from '#src/libraries/logto-config.js';
import { type LogtoConfigLibrary } from '#src/libraries/logto-config.js';
import Libraries from '#src/tenants/Libraries.js';
import Queries from '#src/tenants/Queries.js';
import type TenantContext from '#src/tenants/TenantContext.js';
@ -57,6 +61,8 @@ export class MockTenant implements TenantContext {
public id = 'mock_id';
public envSet = mockEnvSet;
public queries: Queries;
public logtoConfigs: LogtoConfigLibrary;
public cloudConnection: CloudConnectionLibrary;
public connectors: ConnectorLibrary;
public libraries: Libraries;
@ -67,7 +73,12 @@ export class MockTenant implements TenantContext {
librariesOverride?: Partial2<Libraries>
) {
this.queries = new MockQueries(queriesOverride);
this.connectors = { ...createConnectorLibrary(this.queries), ...connectorsOverride };
this.logtoConfigs = createLogtoConfigLibrary(this.queries);
this.cloudConnection = createCloudConnectionLibrary(this.logtoConfigs);
this.connectors = {
...createConnectorLibrary(this.queries, this.cloudConnection),
...connectorsOverride,
};
this.libraries = new Libraries(this.id, this.queries, this.connectors);
this.setPartial('libraries', librariesOverride);
}

View file

@ -1,13 +0,0 @@
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 () => {
const { cloudUrlSet, adminUrlSet } = EnvSet.values;
return {
tokenEndpoint: appendPath(adminUrlSet.endpoint, 'oidc/token').toString(),
endpoint: appendPath(cloudUrlSet.endpoint, 'api').toString(),
};
};