mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(toolkit,connector,core,cli): cache access token for logto email service (#4309)
This commit is contained in:
parent
bc5a797892
commit
77d274a3c3
14 changed files with 101 additions and 176 deletions
|
@ -7,6 +7,7 @@ import type {
|
||||||
BaseConnector,
|
BaseConnector,
|
||||||
ConnectorMetadata,
|
ConnectorMetadata,
|
||||||
GetConnectorConfig,
|
GetConnectorConfig,
|
||||||
|
GetCloudServiceClient,
|
||||||
} from '@logto/connector-kit';
|
} from '@logto/connector-kit';
|
||||||
import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit';
|
import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit';
|
||||||
|
|
||||||
|
@ -91,11 +92,13 @@ export const parseMetadata = async (
|
||||||
|
|
||||||
export const buildRawConnector = async <T extends AllConnector = AllConnector>(
|
export const buildRawConnector = async <T extends AllConnector = AllConnector>(
|
||||||
connectorFactory: ConnectorFactory<T>,
|
connectorFactory: ConnectorFactory<T>,
|
||||||
getConnectorConfig?: GetConnectorConfig
|
getConnectorConfig?: GetConnectorConfig,
|
||||||
|
getCloudServiceClient?: GetCloudServiceClient
|
||||||
): Promise<{ rawConnector: T; rawMetadata: ConnectorMetadata }> => {
|
): Promise<{ rawConnector: T; rawMetadata: ConnectorMetadata }> => {
|
||||||
const { createConnector, path: packagePath } = connectorFactory;
|
const { createConnector, path: packagePath } = connectorFactory;
|
||||||
const rawConnector = await createConnector({
|
const rawConnector = await createConnector({
|
||||||
getConfig: getConnectorConfig ?? notImplemented,
|
getConfig: getConnectorConfig ?? notImplemented,
|
||||||
|
getCloudServiceClient,
|
||||||
});
|
});
|
||||||
validateConnectorModule(rawConnector);
|
validateConnectorModule(rawConnector);
|
||||||
const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath);
|
const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath);
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import nock from 'nock';
|
|
||||||
|
|
||||||
import { grantAccessToken } from './grant-access-token.js';
|
|
||||||
import { mockedAccessTokenResponse, mockedConfig } from './mock.js';
|
|
||||||
|
|
||||||
describe('grantAccessToken()', () => {
|
|
||||||
it('should call got.post() and return access token', async () => {
|
|
||||||
nock(mockedConfig.tokenEndpoint).post('').reply(200, JSON.stringify(mockedAccessTokenResponse));
|
|
||||||
|
|
||||||
const response = await grantAccessToken(mockedConfig);
|
|
||||||
|
|
||||||
expect(response).toMatchObject(mockedAccessTokenResponse);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,42 +0,0 @@
|
||||||
import { got } from 'got';
|
|
||||||
|
|
||||||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
|
||||||
|
|
||||||
import { defaultTimeout, scope } from './constant.js';
|
|
||||||
import { accessTokenResponseGuard } from './types.js';
|
|
||||||
|
|
||||||
export type GrantAccessTokenParameters = {
|
|
||||||
tokenEndpoint: string;
|
|
||||||
resource: string;
|
|
||||||
appId: string;
|
|
||||||
appSecret: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const grantAccessToken = async ({
|
|
||||||
tokenEndpoint,
|
|
||||||
resource,
|
|
||||||
appId,
|
|
||||||
appSecret,
|
|
||||||
}: GrantAccessTokenParameters) => {
|
|
||||||
const httpResponse = await got.post({
|
|
||||||
url: tokenEndpoint,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
Authorization: `Basic ${Buffer.from(`${appId}:${appSecret}`).toString('base64')}`,
|
|
||||||
},
|
|
||||||
timeout: { request: defaultTimeout },
|
|
||||||
form: {
|
|
||||||
grant_type: 'client_credentials',
|
|
||||||
resource,
|
|
||||||
scope,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = accessTokenResponseGuard.safeParse(JSON.parse(httpResponse.body));
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.data;
|
|
||||||
};
|
|
|
@ -1,30 +1,56 @@
|
||||||
|
import { got } from 'got';
|
||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
|
|
||||||
import { VerificationCodeType } from '@logto/connector-kit';
|
import { VerificationCodeType } from '@logto/connector-kit';
|
||||||
|
|
||||||
import { emailEndpoint } from './constant.js';
|
import { emailEndpoint, usageEndpoint } from './constant.js';
|
||||||
import { mockedAccessTokenResponse, mockedConfig } from './mock.js';
|
import createConnector from './index.js';
|
||||||
|
|
||||||
const { jest } = import.meta;
|
const { jest } = import.meta;
|
||||||
|
|
||||||
const getConfig = jest.fn().mockResolvedValue(mockedConfig);
|
const endpoint = 'http://localhost:3003';
|
||||||
|
|
||||||
const { default: createConnector } = await import('./index.js');
|
const api = got.extend({ prefixUrl: endpoint });
|
||||||
|
const dropLeadingSlash = (path: string) => path.replace(/^\//, '');
|
||||||
|
const buildUrl = (path: string, endpoint: string) => new URL(`${endpoint}/api${path}`);
|
||||||
|
|
||||||
|
const getConfig = jest.fn().mockResolvedValue({});
|
||||||
|
const getCloudServiceClient = jest.fn().mockResolvedValue({
|
||||||
|
post: async (path: string, payload: { body: unknown }) => {
|
||||||
|
return api(dropLeadingSlash(path), {
|
||||||
|
method: 'POST',
|
||||||
|
json: payload.body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
get: async (path: string, payload: { search: Record<string, string> }) => {
|
||||||
|
return api(dropLeadingSlash(path), {
|
||||||
|
method: 'GET',
|
||||||
|
searchParams: payload.search,
|
||||||
|
}).json<{ count: number }>();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('sendMessage()', () => {
|
describe('sendMessage()', () => {
|
||||||
beforeAll(() => {
|
|
||||||
nock(mockedConfig.tokenEndpoint).post('').reply(200, JSON.stringify(mockedAccessTokenResponse));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should send message successfully', async () => {
|
it('should send message successfully', async () => {
|
||||||
nock(mockedConfig.endpoint).post(emailEndpoint).reply(200);
|
const url = buildUrl(emailEndpoint, endpoint);
|
||||||
const connector = await createConnector({ getConfig });
|
nock(url.origin).post(url.pathname).reply(204);
|
||||||
|
const { sendMessage } = await createConnector({ getConfig, getCloudServiceClient });
|
||||||
await expect(
|
await expect(
|
||||||
connector.sendMessage({
|
sendMessage({
|
||||||
to: 'wangsijie94@gmail.com',
|
to: 'wangsijie94@gmail.com',
|
||||||
type: VerificationCodeType.SignIn,
|
type: VerificationCodeType.SignIn,
|
||||||
payload: { code: '1234' },
|
payload: { code: '1234' },
|
||||||
})
|
})
|
||||||
).resolves.not.toThrow();
|
).resolves.not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should get usage successfully', async () => {
|
||||||
|
const date = new Date();
|
||||||
|
const url = buildUrl(usageEndpoint, endpoint);
|
||||||
|
nock(url.origin).get(url.pathname).query({ from: date.toISOString() }).reply(200, { count: 1 });
|
||||||
|
const connector = await createConnector({ getConfig, getCloudServiceClient });
|
||||||
|
expect(connector.getUsage).toBeDefined();
|
||||||
|
const usage = await connector.getUsage!(date);
|
||||||
|
expect(usage).toEqual(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { assert } from '@silverhand/essentials';
|
import { assert, conditional } from '@silverhand/essentials';
|
||||||
import { HTTPError, got } from 'got';
|
import { HTTPError } from 'got';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CreateConnector,
|
CreateConnector,
|
||||||
EmailConnector,
|
EmailConnector,
|
||||||
|
GetCloudServiceClient,
|
||||||
GetConnectorConfig,
|
GetConnectorConfig,
|
||||||
GetUsageFunction,
|
GetUsageFunction,
|
||||||
SendMessageFunction,
|
SendMessageFunction,
|
||||||
|
@ -14,53 +14,28 @@ import {
|
||||||
validateConfig,
|
validateConfig,
|
||||||
ConnectorError,
|
ConnectorError,
|
||||||
ConnectorErrorCodes,
|
ConnectorErrorCodes,
|
||||||
parseJson,
|
|
||||||
} from '@logto/connector-kit';
|
} from '@logto/connector-kit';
|
||||||
|
|
||||||
import { defaultMetadata, defaultTimeout, emailEndpoint, usageEndpoint } from './constant.js';
|
import { defaultMetadata, emailEndpoint, usageEndpoint } from './constant.js';
|
||||||
import { grantAccessToken } from './grant-access-token.js';
|
|
||||||
import { logtoEmailConfigGuard } from './types.js';
|
import { logtoEmailConfigGuard } from './types.js';
|
||||||
|
|
||||||
const sendMessage =
|
const sendMessage =
|
||||||
(getConfig: GetConnectorConfig): SendMessageFunction =>
|
(getConfig: GetConnectorConfig, getClient?: GetCloudServiceClient): SendMessageFunction =>
|
||||||
async (data, inputConfig) => {
|
async (data, inputConfig) => {
|
||||||
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
|
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
|
||||||
validateConfig(config, logtoEmailConfigGuard);
|
validateConfig(config, logtoEmailConfigGuard);
|
||||||
|
|
||||||
const {
|
const { companyInformation, senderName, appLogo } = config;
|
||||||
endpoint,
|
|
||||||
tokenEndpoint,
|
|
||||||
appId,
|
|
||||||
appSecret,
|
|
||||||
resource,
|
|
||||||
companyInformation,
|
|
||||||
senderName,
|
|
||||||
appLogo,
|
|
||||||
} = config;
|
|
||||||
const { to, type, payload } = data;
|
const { to, type, payload } = data;
|
||||||
|
|
||||||
assert(
|
assert(getClient, new ConnectorError(ConnectorErrorCodes.NotImplemented));
|
||||||
endpoint && tokenEndpoint && resource && appId && appSecret,
|
const client = await getClient();
|
||||||
new ConnectorError(ConnectorErrorCodes.InvalidConfig)
|
|
||||||
);
|
|
||||||
|
|
||||||
const accessTokenResponse = await grantAccessToken({
|
|
||||||
tokenEndpoint,
|
|
||||||
resource,
|
|
||||||
appId,
|
|
||||||
appSecret,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await got.post({
|
await client.post(`/api${emailEndpoint}`, {
|
||||||
url: `${endpoint}${emailEndpoint}`,
|
body: {
|
||||||
headers: {
|
|
||||||
Authorization: `${accessTokenResponse.token_type} ${accessTokenResponse.access_token}`,
|
|
||||||
},
|
|
||||||
json: {
|
|
||||||
data: { to, type, payload: { ...payload, senderName, companyInformation, appLogo } },
|
data: { to, type, payload: { ...payload, senderName, companyInformation, appLogo } },
|
||||||
},
|
},
|
||||||
timeout: { request: defaultTimeout },
|
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof HTTPError) {
|
if (error instanceof HTTPError) {
|
||||||
|
@ -72,46 +47,30 @@ const sendMessage =
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUsage =
|
const getUsage =
|
||||||
(getConfig: GetConnectorConfig): GetUsageFunction =>
|
(getConfig: GetConnectorConfig, getClient?: GetCloudServiceClient): GetUsageFunction =>
|
||||||
async (startFrom?: Date) => {
|
async (startFrom?: Date) => {
|
||||||
const config = await getConfig(defaultMetadata.id);
|
const config = await getConfig(defaultMetadata.id);
|
||||||
validateConfig(config, logtoEmailConfigGuard);
|
validateConfig(config, logtoEmailConfigGuard);
|
||||||
|
|
||||||
const { endpoint, tokenEndpoint, appId, appSecret, resource } = config;
|
assert(getClient, new ConnectorError(ConnectorErrorCodes.NotImplemented));
|
||||||
|
const client = await getClient();
|
||||||
|
|
||||||
assert(
|
const { count } = await client.get(`/api${usageEndpoint}`, {
|
||||||
endpoint && tokenEndpoint && resource && appId && appSecret,
|
search: conditional(startFrom && { from: startFrom.toISOString() }) ?? {},
|
||||||
new ConnectorError(ConnectorErrorCodes.InvalidConfig)
|
|
||||||
);
|
|
||||||
|
|
||||||
const accessTokenResponse = await grantAccessToken({
|
|
||||||
tokenEndpoint,
|
|
||||||
resource,
|
|
||||||
appId,
|
|
||||||
appSecret,
|
|
||||||
});
|
});
|
||||||
|
return count;
|
||||||
const httpResponse = await got.get({
|
|
||||||
url: `${endpoint}${usageEndpoint}`,
|
|
||||||
headers: {
|
|
||||||
Authorization: `${accessTokenResponse.token_type} ${accessTokenResponse.access_token}`,
|
|
||||||
},
|
|
||||||
timeout: { request: defaultTimeout },
|
|
||||||
searchParams: {
|
|
||||||
from: startFrom?.toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return z.object({ count: z.number() }).parse(parseJson(httpResponse.body)).count;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createLogtoEmailConnector: CreateConnector<EmailConnector> = async ({ getConfig }) => {
|
const createLogtoEmailConnector: CreateConnector<EmailConnector> = async ({
|
||||||
|
getConfig,
|
||||||
|
getCloudServiceClient: getClient,
|
||||||
|
}) => {
|
||||||
return {
|
return {
|
||||||
metadata: defaultMetadata,
|
metadata: defaultMetadata,
|
||||||
type: ConnectorType.Email,
|
type: ConnectorType.Email,
|
||||||
configGuard: logtoEmailConfigGuard,
|
configGuard: logtoEmailConfigGuard,
|
||||||
sendMessage: sendMessage(getConfig),
|
sendMessage: sendMessage(getConfig, getClient),
|
||||||
getUsage: getUsage(getConfig),
|
getUsage: getUsage(getConfig, getClient),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import type { AccessTokenResponse } from './types.js';
|
|
||||||
|
|
||||||
export const mockedConfig = {
|
|
||||||
appId: 'mfvnO3josReyBf9zhDnlr',
|
|
||||||
appSecret: 'lXNWW4wPj0Bq6msjIl6H3',
|
|
||||||
tokenEndpoint: 'http://localhost:3002/oidc/token',
|
|
||||||
endpoint: 'http://localhost:3003/api',
|
|
||||||
resource: 'https://cloud.logto.io/api',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mockedAccessTokenResponse: AccessTokenResponse = {
|
|
||||||
access_token: 'access_token',
|
|
||||||
scope: 'scope',
|
|
||||||
token_type: 'token_type',
|
|
||||||
expires_in: 3600,
|
|
||||||
};
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type router from '@logto/cloud/routes';
|
import type router from '@logto/cloud/routes';
|
||||||
import { cloudConnectionDataGuard } from '@logto/schemas';
|
import { cloudConnectionDataGuard, CloudScope } from '@logto/schemas';
|
||||||
import { appendPath } from '@silverhand/essentials';
|
import { appendPath } from '@silverhand/essentials';
|
||||||
import Client from '@withtyped/client';
|
import Client from '@withtyped/client';
|
||||||
import { got } from 'got';
|
import { got } from 'got';
|
||||||
|
@ -24,7 +24,11 @@ const accessTokenResponseGuard = z.object({
|
||||||
scope: z.string().optional(),
|
scope: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const scopes: string[] = [];
|
/**
|
||||||
|
* The scope here can be empty and still work, because the cloud API requests made using this client do not rely on scope verification.
|
||||||
|
* The `CloudScope.SendEmail` is added for now because it needs to call the cloud email service API.
|
||||||
|
*/
|
||||||
|
const scopes: string[] = [CloudScope.SendEmail];
|
||||||
const accessTokenExpirationMargin = 60;
|
const accessTokenExpirationMargin = 60;
|
||||||
|
|
||||||
/** The library for connecting to Logto Cloud service. */
|
/** The library for connecting to Logto Cloud service. */
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import type { Connector } from '@logto/schemas';
|
import type { Connector } from '@logto/schemas';
|
||||||
|
|
||||||
import { mockGetCloudConnectionData } from '#src/__mocks__/index.js';
|
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
import { MockQueries } from '#src/test-utils/tenant.js';
|
import { MockQueries } from '#src/test-utils/tenant.js';
|
||||||
|
|
||||||
|
const { jest } = import.meta;
|
||||||
|
|
||||||
const connectors: Connector[] = [
|
const connectors: Connector[] = [
|
||||||
{
|
{
|
||||||
tenantId: 'fake_tenant',
|
tenantId: 'fake_tenant',
|
||||||
|
@ -19,7 +20,7 @@ const connectors: Connector[] = [
|
||||||
const { createConnectorLibrary } = await import('./connector.js');
|
const { createConnectorLibrary } = await import('./connector.js');
|
||||||
const { getConnectorConfig } = createConnectorLibrary(
|
const { getConnectorConfig } = createConnectorLibrary(
|
||||||
new MockQueries({ connectors: { findAllConnectors: async () => connectors } }),
|
new MockQueries({ connectors: { findAllConnectors: async () => connectors } }),
|
||||||
{ getCloudConnectionData: mockGetCloudConnectionData }
|
{ getClient: jest.fn() }
|
||||||
);
|
);
|
||||||
|
|
||||||
it('getConnectorConfig() should return right config', async () => {
|
it('getConnectorConfig() should return right config', async () => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { buildRawConnector, defaultConnectorMethods } from '@logto/cli/lib/connector/index.js';
|
import { buildRawConnector, defaultConnectorMethods } from '@logto/cli/lib/connector/index.js';
|
||||||
import type { AllConnector } from '@logto/connector-kit';
|
import type { AllConnector } from '@logto/connector-kit';
|
||||||
import { validateConfig, ServiceConnector } from '@logto/connector-kit';
|
import { validateConfig, ServiceConnector } from '@logto/connector-kit';
|
||||||
import { pick, trySafe } from '@silverhand/essentials';
|
import { conditional, pick, trySafe } from '@silverhand/essentials';
|
||||||
|
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
import type Queries from '#src/tenants/Queries.js';
|
import type Queries from '#src/tenants/Queries.js';
|
||||||
|
@ -15,10 +15,10 @@ export type ConnectorLibrary = ReturnType<typeof createConnectorLibrary>;
|
||||||
|
|
||||||
export const createConnectorLibrary = (
|
export const createConnectorLibrary = (
|
||||||
queries: Queries,
|
queries: Queries,
|
||||||
cloudConnection: Pick<CloudConnectionLibrary, 'getCloudConnectionData'>
|
cloudConnection: Pick<CloudConnectionLibrary, 'getClient'>
|
||||||
) => {
|
) => {
|
||||||
const { findAllConnectors, findAllConnectorsWellKnown } = queries.connectors;
|
const { findAllConnectors, findAllConnectorsWellKnown } = queries.connectors;
|
||||||
const { getCloudConnectionData } = cloudConnection;
|
const { getClient } = cloudConnection;
|
||||||
|
|
||||||
const getConnectorConfig = async (id: string): Promise<unknown> => {
|
const getConnectorConfig = async (id: string): Promise<unknown> => {
|
||||||
const connectors = await findAllConnectors();
|
const connectors = await findAllConnectors();
|
||||||
|
@ -26,10 +26,6 @@ export const createConnectorLibrary = (
|
||||||
|
|
||||||
assertThat(connector, new RequestError({ code: 'entity.not_found', id, status: 404 }));
|
assertThat(connector, new RequestError({ code: 'entity.not_found', id, status: 404 }));
|
||||||
|
|
||||||
if (ServiceConnector.Email === connector.connectorId) {
|
|
||||||
return { ...connector.config, ...(await getCloudConnectionData()) };
|
|
||||||
}
|
|
||||||
|
|
||||||
return connector.config;
|
return connector.config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -81,7 +77,8 @@ export const createConnectorLibrary = (
|
||||||
try {
|
try {
|
||||||
const { rawConnector, rawMetadata } = await buildRawConnector(
|
const { rawConnector, rawMetadata } = await buildRawConnector(
|
||||||
connectorFactory,
|
connectorFactory,
|
||||||
async () => getConnectorConfig(id)
|
async () => getConnectorConfig(id),
|
||||||
|
conditional(connectorFactory.metadata.id === ServiceConnector.Email && getClient)
|
||||||
);
|
);
|
||||||
|
|
||||||
const connector: AllConnector = {
|
const connector: AllConnector = {
|
||||||
|
@ -123,7 +120,6 @@ export const createConnectorLibrary = (
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getCloudConnectionData,
|
|
||||||
getConnectorConfig,
|
getConnectorConfig,
|
||||||
getLogtoConnectors,
|
getLogtoConnectors,
|
||||||
getLogtoConnectorsWellKnown,
|
getLogtoConnectorsWellKnown,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
socialTarget02,
|
socialTarget02,
|
||||||
mockSignInExperience,
|
mockSignInExperience,
|
||||||
mockSocialConnectors,
|
mockSocialConnectors,
|
||||||
mockGetCloudConnectionData,
|
|
||||||
} from '#src/__mocks__/index.js';
|
} from '#src/__mocks__/index.js';
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
|
|
||||||
|
@ -39,7 +38,7 @@ const queries = new MockQueries({
|
||||||
signInExperiences,
|
signInExperiences,
|
||||||
});
|
});
|
||||||
const connectorLibrary = createConnectorLibrary(queries, {
|
const connectorLibrary = createConnectorLibrary(queries, {
|
||||||
getCloudConnectionData: mockGetCloudConnectionData,
|
getClient: jest.fn(),
|
||||||
});
|
});
|
||||||
const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors');
|
const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors');
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { buildRawConnector } from '@logto/cli/lib/connector/index.js';
|
import { buildRawConnector, notImplemented } from '@logto/cli/lib/connector/index.js';
|
||||||
import type { ConnectorFactory } from '@logto/cli/lib/connector/index.js';
|
import type { ConnectorFactory } from '@logto/cli/lib/connector/index.js';
|
||||||
import {
|
import {
|
||||||
type SmsConnector,
|
type SmsConnector,
|
||||||
|
@ -9,6 +9,7 @@ import {
|
||||||
import { ServiceConnector } from '@logto/connector-kit';
|
import { ServiceConnector } 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 } from '@logto/schemas';
|
||||||
|
import { conditional } from '@silverhand/essentials';
|
||||||
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';
|
||||||
|
@ -21,7 +22,7 @@ import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||||
export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
|
export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
|
||||||
...[router, { cloudConnection }]: RouterInitArgs<T>
|
...[router, { cloudConnection }]: RouterInitArgs<T>
|
||||||
) {
|
) {
|
||||||
const { getCloudConnectionData } = cloudConnection;
|
const { getClient } = cloudConnection;
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/connectors/:factoryId/test',
|
'/connectors/:factoryId/test',
|
||||||
|
@ -38,7 +39,7 @@ export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
|
||||||
params: { factoryId },
|
params: { factoryId },
|
||||||
body,
|
body,
|
||||||
} = ctx.guard;
|
} = ctx.guard;
|
||||||
const { phone, email, config: originalConfig } = body;
|
const { phone, email, config } = 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' }));
|
||||||
|
@ -65,17 +66,12 @@ export default function connectorConfigTestingRoutes<T extends AuthedRouter>(
|
||||||
|
|
||||||
const {
|
const {
|
||||||
rawConnector: { sendMessage },
|
rawConnector: { sendMessage },
|
||||||
} = await buildRawConnector<SmsConnector | EmailConnector>(connectorFactory);
|
} = await buildRawConnector<SmsConnector | EmailConnector>(
|
||||||
|
connectorFactory,
|
||||||
|
notImplemented,
|
||||||
|
conditional(ServiceConnector.Email === connectorFactory.metadata.id && getClient)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(
|
await sendMessage(
|
||||||
{
|
{
|
||||||
to: subject,
|
to: subject,
|
||||||
|
|
|
@ -37,12 +37,14 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@logto/language-kit": "workspace:^1.0.0",
|
"@logto/language-kit": "workspace:^1.0.0",
|
||||||
"@silverhand/essentials": "^2.5.0"
|
"@silverhand/essentials": "^2.5.0",
|
||||||
|
"@withtyped/client": "^0.7.21"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@logto/cloud": "0.2.5-33a6965",
|
||||||
"@jest/types": "^29.0.3",
|
"@jest/types": "^29.0.3",
|
||||||
"@silverhand/eslint-config": "4.0.1",
|
"@silverhand/eslint-config": "4.0.1",
|
||||||
"@silverhand/ts-config": "4.0.0",
|
"@silverhand/ts-config": "4.0.0",
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import type router from '@logto/cloud/routes';
|
||||||
import type { LanguageTag } from '@logto/language-kit';
|
import type { LanguageTag } from '@logto/language-kit';
|
||||||
import { isLanguageTag } from '@logto/language-kit';
|
import { isLanguageTag } from '@logto/language-kit';
|
||||||
|
import type Client from '@withtyped/client';
|
||||||
import type { ZodType } from 'zod';
|
import type { ZodType } from 'zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
@ -199,10 +201,13 @@ export type BaseConnector<Type extends ConnectorType> = {
|
||||||
|
|
||||||
export type CreateConnector<T extends AllConnector> = (options: {
|
export type CreateConnector<T extends AllConnector> = (options: {
|
||||||
getConfig: GetConnectorConfig;
|
getConfig: GetConnectorConfig;
|
||||||
|
getCloudServiceClient?: GetCloudServiceClient;
|
||||||
}) => Promise<T>;
|
}) => Promise<T>;
|
||||||
|
|
||||||
export type GetConnectorConfig = (id: string) => Promise<unknown>;
|
export type GetConnectorConfig = (id: string) => Promise<unknown>;
|
||||||
|
|
||||||
|
export type GetCloudServiceClient = () => Promise<Client<typeof router>>;
|
||||||
|
|
||||||
export type AllConnector = SmsConnector | EmailConnector | SocialConnector;
|
export type AllConnector = SmsConnector | EmailConnector | SocialConnector;
|
||||||
|
|
||||||
// MARK: SMS + Email connector
|
// MARK: SMS + Email connector
|
||||||
|
|
|
@ -3747,6 +3747,9 @@ importers:
|
||||||
'@silverhand/essentials':
|
'@silverhand/essentials':
|
||||||
specifier: ^2.5.0
|
specifier: ^2.5.0
|
||||||
version: 2.5.0
|
version: 2.5.0
|
||||||
|
'@withtyped/client':
|
||||||
|
specifier: ^0.7.21
|
||||||
|
version: 0.7.21(zod@3.20.2)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.20.2
|
specifier: ^3.20.2
|
||||||
|
@ -3755,6 +3758,9 @@ importers:
|
||||||
'@jest/types':
|
'@jest/types':
|
||||||
specifier: ^29.0.3
|
specifier: ^29.0.3
|
||||||
version: 29.1.2
|
version: 29.1.2
|
||||||
|
'@logto/cloud':
|
||||||
|
specifier: 0.2.5-33a6965
|
||||||
|
version: 0.2.5-33a6965(zod@3.20.2)
|
||||||
'@silverhand/eslint-config':
|
'@silverhand/eslint-config':
|
||||||
specifier: 4.0.1
|
specifier: 4.0.1
|
||||||
version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2)
|
version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2)
|
||||||
|
|
Loading…
Reference in a new issue