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:
parent
736e99fd63
commit
e8492654e7
16 changed files with 132 additions and 34 deletions
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
11
packages/core/src/__mocks__/cloud-connection.ts
Normal file
11
packages/core/src/__mocks__/cloud-connection.ts
Normal 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',
|
||||
}),
|
||||
};
|
|
@ -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';
|
||||
|
||||
|
|
34
packages/core/src/libraries/cloud-connection.ts
Normal file
34
packages/core/src/libraries/cloud-connection.ts
Normal 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 };
|
||||
};
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
};
|
Loading…
Add table
Reference in a new issue