0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

fix(connector): fix connector getConfig and validateConfig type (#1530)

This commit is contained in:
Darcy Ye 2022-07-14 00:12:01 +08:00 committed by GitHub
parent 657bfa982a
commit 88a54aaa9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 463 additions and 235 deletions

View file

@ -1,12 +1,16 @@
import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types';
import {
ConnectorError,
ConnectorErrorCodes,
GetConnectorConfig,
ValidateConfig,
} from '@logto/connector-types';
import nock from 'nock';
import AlipayNativeConnector from '.';
import { alipayEndpoint } from './constant';
import { mockedAlipayNativeConfig, mockedAlipayNativeConfigWithValidPrivateKey } from './mock';
import { AlipayNativeConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<AlipayNativeConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const alipayNativeMethods = new AlipayNativeConnector(getConnectorConfig);
@ -15,18 +19,30 @@ describe('validateConfig', () => {
jest.clearAllMocks();
});
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(
alipayNativeMethods.validateConfig(mockedAlipayNativeConfig)
).resolves.not.toThrow();
const validator: ValidateConfig = alipayNativeMethods.validateConfig;
expect(() => {
validator(mockedAlipayNativeConfig);
}).not.toThrow();
});
it('should throw on empty config', async () => {
await expect(alipayNativeMethods.validateConfig({})).rejects.toThrowError();
it('should fail on empty config', async () => {
const validator: ValidateConfig = alipayNativeMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
});
it('should throw when missing required properties', async () => {
await expect(alipayNativeMethods.validateConfig({ appId: 'appId' })).rejects.toThrowError();
it('should fail when missing required properties', async () => {
const validator: ValidateConfig = alipayNativeMethods.validateConfig;
expect(() => {
validator({ appId: 'appId' });
}).toThrow();
});
});

View file

@ -13,7 +13,6 @@ import {
ConnectorMetadata,
GetAuthorizationUri,
GetUserInfo,
ValidateConfig,
SocialConnector,
GetConnectorConfig,
} from '@logto/connector-types';
@ -44,23 +43,27 @@ import { signingParameters } from './utils';
export type { AlipayNativeConfig } from './types';
export default class AlipayNativeConnector implements SocialConnector {
export default class AlipayNativeConnector implements SocialConnector<AlipayNativeConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
private readonly signingParameters = signingParameters;
constructor(public readonly getConfig: GetConnectorConfig<AlipayNativeConfig>) {}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is AlipayNativeConfig {
const result = alipayNativeConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public getAuthorizationUri: GetAuthorizationUri = async ({ state }) => {
const { appId } = await this.getConfig(this.metadata.id);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { appId } = config;
const queryParameters = new URLSearchParams({ app_id: appId, state });
@ -108,6 +111,9 @@ export default class AlipayNativeConnector implements SocialConnector {
public getUserInfo: GetUserInfo = async (data) => {
const { auth_code } = await this.authorizationCallbackHandler(data);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { accessToken } = await this.getAccessToken(auth_code, config);
assert(

View file

@ -1,12 +1,16 @@
import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types';
import {
ConnectorError,
ConnectorErrorCodes,
GetConnectorConfig,
ValidateConfig,
} from '@logto/connector-types';
import nock from 'nock';
import AlipayConnector from '.';
import { alipayEndpoint, authorizationEndpoint } from './constant';
import { mockedAlipayConfig, mockedAlipayConfigWithValidPrivateKey } from './mock';
import { AlipayConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<AlipayConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const alipayMethods = new AlipayConnector(getConnectorConfig);
@ -15,22 +19,34 @@ describe('validateConfig', () => {
jest.clearAllMocks();
});
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(
alipayMethods.validateConfig({
const validator: ValidateConfig = alipayMethods.validateConfig;
expect(() => {
validator({
appId: 'appId',
privateKey: 'privateKey',
signType: 'RSA',
})
).resolves.not.toThrow();
});
}).not.toThrow();
});
it('should throw on empty config', async () => {
await expect(alipayMethods.validateConfig({})).rejects.toThrowError();
it('should fail on empty config', async () => {
const validator: ValidateConfig = alipayMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
});
it('should throw when missing required properties', async () => {
await expect(alipayMethods.validateConfig({ appId: 'appId' })).rejects.toThrowError();
it('should fail when missing required properties', async () => {
const validator: ValidateConfig = alipayMethods.validateConfig;
expect(() => {
validator({ appId: 'appId' });
}).toThrow();
});
});

View file

@ -11,7 +11,6 @@ import {
ConnectorMetadata,
GetAuthorizationUri,
GetUserInfo,
ValidateConfig,
SocialConnector,
GetConnectorConfig,
} from '@logto/connector-types';
@ -44,23 +43,27 @@ import { signingParameters } from './utils';
export type { AlipayConfig } from './types';
export default class AlipayConnector implements SocialConnector {
export default class AlipayConnector implements SocialConnector<AlipayConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
private readonly signingParameters = signingParameters;
constructor(public readonly getConfig: GetConnectorConfig<AlipayConfig>) {}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is AlipayConfig {
const result = alipayConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
const { appId: app_id } = await this.getConfig(this.metadata.id);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { appId: app_id } = config;
const redirect_uri = encodeURI(redirectUri);
@ -116,6 +119,9 @@ export default class AlipayConnector implements SocialConnector {
public getUserInfo: GetUserInfo = async (data) => {
const { auth_code } = await this.authorizationCallbackHandler(data);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { accessToken } = await this.getAccessToken(auth_code, config);
assert(

View file

@ -1,11 +1,10 @@
import { GetConnectorConfig } from '@logto/connector-types';
import { GetConnectorConfig, ValidateConfig } from '@logto/connector-types';
import AliyunDmConnector from '.';
import { mockedConfig } from './mock';
import { singleSendMail } from './single-send-mail';
import { AliyunDmConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<AliyunDmConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const aliyunDmMethods = new AliyunDmConnector(getConnectorConfig);
@ -29,19 +28,28 @@ describe('validateConfig()', () => {
jest.clearAllMocks();
});
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(
aliyunDmMethods.validateConfig({
const validator: ValidateConfig = aliyunDmMethods.validateConfig;
expect(() => {
validator({
accessKeyId: 'accessKeyId',
accessKeySecret: 'accessKeySecret',
accountName: 'accountName',
templates: [],
})
).resolves.not.toThrow();
});
}).not.toThrow();
});
it('throws if config is invalid', async () => {
await expect(aliyunDmMethods.validateConfig({})).rejects.toThrow();
it('should fail if config is invalid', async () => {
const validator: ValidateConfig = aliyunDmMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
});
});

View file

@ -3,7 +3,6 @@ import {
ConnectorErrorCodes,
ConnectorMetadata,
EmailSendMessageFunction,
ValidateConfig,
EmailConnector,
GetConnectorConfig,
} from '@logto/connector-types';
@ -19,24 +18,24 @@ import {
sendMailErrorResponseGuard,
} from './types';
export default class AliyunDmConnector implements EmailConnector {
export default class AliyunDmConnector implements EmailConnector<AliyunDmConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig) {}
constructor(public readonly getConfig: GetConnectorConfig<AliyunDmConfig>) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is AliyunDmConfig {
const result = aliyunDmConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
// eslint-disable-next-line complexity
public sendMessage: EmailSendMessageFunction = async (address, type, data, config) => {
const emailConfig =
(config as AliyunDmConfig | undefined) ?? (await this.getConfig(this.metadata.id));
await this.validateConfig(emailConfig);
const emailConfig = config ?? (await this.getConfig(this.metadata.id));
this.validateConfig(emailConfig);
const { accessKeyId, accessKeySecret, accountName, fromAlias, templates } = emailConfig;
const template = templates.find((template) => template.usageType === type);

View file

@ -1,11 +1,10 @@
import { GetConnectorConfig } from '@logto/connector-types';
import { GetConnectorConfig, ValidateConfig } from '@logto/connector-types';
import AliyunSmsConnector from '.';
import { mockedConnectorConfig, mockedValidConnectorConfig, phoneTest, codeTest } from './mock';
import { sendSms } from './single-send-text';
import { AliyunSmsConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<AliyunSmsConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const aliyunSmsMethods = new AliyunSmsConnector(getConnectorConfig);
@ -25,14 +24,23 @@ describe('validateConfig()', () => {
jest.clearAllMocks();
});
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(
aliyunSmsMethods.validateConfig(mockedValidConnectorConfig)
).resolves.not.toThrow();
const validator: ValidateConfig = aliyunSmsMethods.validateConfig;
expect(() => {
validator(mockedValidConnectorConfig);
}).not.toThrow();
});
it('throws if config is invalid', async () => {
await expect(aliyunSmsMethods.validateConfig({})).rejects.toThrow();
it('should fail if config is invalid', async () => {
const validator: ValidateConfig = aliyunSmsMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
});
});

View file

@ -3,7 +3,6 @@ import {
ConnectorErrorCodes,
ConnectorMetadata,
SmsSendMessageFunction,
ValidateConfig,
SmsConnector,
GetConnectorConfig,
} from '@logto/connector-types';
@ -14,23 +13,23 @@ import { defaultMetadata } from './constant';
import { sendSms } from './single-send-text';
import { aliyunSmsConfigGuard, AliyunSmsConfig, sendSmsResponseGuard } from './types';
export default class AliyunSmsConnector implements SmsConnector {
export default class AliyunSmsConnector implements SmsConnector<AliyunSmsConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig) {}
constructor(public readonly getConfig: GetConnectorConfig<AliyunSmsConfig>) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is AliyunSmsConfig {
const result = aliyunSmsConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public sendMessage: SmsSendMessageFunction = async (phone, type, { code }, config) => {
const smsConfig =
(config as AliyunSmsConfig | undefined) ?? (await this.getConfig(this.metadata.id));
await this.validateConfig(smsConfig);
const smsConfig = config ?? (await this.getConfig(this.metadata.id));
this.validateConfig(smsConfig);
const { accessKeyId, accessKeySecret, signName, templates } = smsConfig;
const template = templates.find(({ usageType }) => usageType === type);

View file

@ -1,12 +1,16 @@
import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types';
import {
ConnectorError,
ConnectorErrorCodes,
GetConnectorConfig,
ValidateConfig,
} from '@logto/connector-types';
import { jwtVerify } from 'jose';
import AppleConnector from '.';
import { authorizationEndpoint } from './constant';
import { mockedConfig } from './mock';
import { AppleConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<AppleConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const appleMethods = new AppleConnector(getConnectorConfig);
@ -40,12 +44,23 @@ describe('validateConfig', () => {
jest.clearAllMocks();
});
it('should pass on valid config', async () => {
await expect(appleMethods.validateConfig({ clientId: 'clientId' })).resolves.not.toThrow();
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should be true on valid config', async () => {
const validator: ValidateConfig = appleMethods.validateConfig;
expect(() => {
validator({ clientId: 'clientId' });
}).not.toThrow();
});
it('should throw on empty config', async () => {
await expect(appleMethods.validateConfig({})).rejects.toThrowError();
it('should be false on empty config', async () => {
const validator: ValidateConfig = appleMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
});
});

View file

@ -1,7 +1,6 @@
import {
ConnectorMetadata,
GetAuthorizationUri,
ValidateConfig,
GetUserInfo,
ConnectorError,
ConnectorErrorCodes,
@ -14,22 +13,24 @@ import { scope, defaultMetadata, jwksUri, issuer, authorizationEndpoint } from '
import { appleConfigGuard, AppleConfig, dataGuard } from './types';
// TO-DO: support nonce validation
export default class AppleConnector implements SocialConnector {
export default class AppleConnector implements SocialConnector<AppleConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig<AppleConfig>) {}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is AppleConfig {
const result = appleConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const queryParameters = new URLSearchParams({
client_id: config.clientId,
redirect_uri: redirectUri,
@ -50,7 +51,11 @@ export default class AppleConnector implements SocialConnector {
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
}
const { clientId } = await this.getConfig(this.metadata.id);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { clientId } = config;
try {
const { payload } = await jwtVerify(idToken, createRemoteJWKSet(new URL(jwksUri)), {

View file

@ -1,12 +1,16 @@
import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types';
import {
ConnectorError,
ConnectorErrorCodes,
GetConnectorConfig,
ValidateConfig,
} from '@logto/connector-types';
import nock from 'nock';
import FacebookConnector from '.';
import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant';
import { clientId, clientSecret, code, dummyRedirectUri, fields, mockedConfig } from './mock';
import { FacebookConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<FacebookConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const facebookMethods = new FacebookConnector(getConnectorConfig);
@ -20,16 +24,29 @@ describe('facebook connector', () => {
jest.clearAllMocks();
});
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(
facebookMethods.validateConfig({ clientId, clientSecret })
).resolves.not.toThrow();
const validator: ValidateConfig = facebookMethods.validateConfig;
expect(() => {
validator({ clientId, clientSecret });
}).not.toThrow();
});
it('should throw on invalid config', async () => {
await expect(facebookMethods.validateConfig({})).rejects.toThrow();
await expect(facebookMethods.validateConfig({ clientId })).rejects.toThrow();
await expect(facebookMethods.validateConfig({ clientSecret })).rejects.toThrow();
it('should fail on invalid config', async () => {
const validator: ValidateConfig = facebookMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
expect(() => {
validator({ clientId });
}).toThrow();
expect(() => {
validator({ clientSecret });
}).toThrow();
});
});

View file

@ -9,7 +9,6 @@ import {
ConnectorMetadata,
GetAuthorizationUri,
GetUserInfo,
ValidateConfig,
SocialConnector,
GetConnectorConfig,
codeWithRedirectDataGuard,
@ -33,22 +32,24 @@ import {
userInfoResponseGuard,
} from './types';
export default class FacebookConnector implements SocialConnector {
export default class FacebookConnector implements SocialConnector<FacebookConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig<FacebookConfig>) {}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is FacebookConfig {
const result = facebookConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const queryParameters = new URLSearchParams({
client_id: config.clientId,
redirect_uri: redirectUri,
@ -61,9 +62,11 @@ export default class FacebookConnector implements SocialConnector {
};
public getAccessToken = async (code: string, redirectUri: string) => {
const { clientId: client_id, clientSecret: client_secret } = await this.getConfig(
this.metadata.id
);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { clientId: client_id, clientSecret: client_secret } = config;
const httpResponse = await got.get(accessTokenEndpoint, {
searchParams: {

View file

@ -1,13 +1,17 @@
import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types';
import {
ConnectorError,
ConnectorErrorCodes,
GetConnectorConfig,
ValidateConfig,
} from '@logto/connector-types';
import nock from 'nock';
import * as qs from 'query-string';
import GithubConnector from '.';
import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant';
import { mockedConfig } from './mock';
import { GithubConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<GithubConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const githubMethods = new GithubConnector(getConnectorConfig);
@ -67,18 +71,30 @@ describe('validateConfig', () => {
jest.clearAllMocks();
});
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(
githubMethods.validateConfig({ clientId: 'clientId', clientSecret: 'clientSecret' })
).resolves.not.toThrow();
const validator: ValidateConfig = githubMethods.validateConfig;
expect(() => {
validator({ clientId: 'clientId', clientSecret: 'clientSecret' });
}).not.toThrow();
});
it('should throw on empty config', async () => {
await expect(githubMethods.validateConfig({})).rejects.toThrowError();
it('should fail on empty config', async () => {
const validator: ValidateConfig = githubMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
});
it('should throw when missing clientSecret', async () => {
await expect(githubMethods.validateConfig({ clientId: 'clientId' })).rejects.toThrowError();
it('should fail when missing clientSecret', async () => {
const validator: ValidateConfig = githubMethods.validateConfig;
expect(() => {
validator({ clientId: 'clientId' });
}).toThrow();
});
});

View file

@ -1,7 +1,6 @@
import {
ConnectorMetadata,
GetAuthorizationUri,
ValidateConfig,
GetUserInfo,
ConnectorError,
ConnectorErrorCodes,
@ -29,22 +28,24 @@ import {
userInfoResponseGuard,
} from './types';
export default class GithubConnector implements SocialConnector {
export default class GithubConnector implements SocialConnector<GithubConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig<GithubConfig>) {}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is GithubConfig {
const result = githubConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const queryParameters = new URLSearchParams({
client_id: config.clientId,
redirect_uri: redirectUri,
@ -56,9 +57,11 @@ export default class GithubConnector implements SocialConnector {
};
public getAccessToken = async (code: string) => {
const { clientId: client_id, clientSecret: client_secret } = await this.getConfig(
this.metadata.id
);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { clientId: client_id, clientSecret: client_secret } = config;
const httpResponse = await got.post({
url: accessTokenEndpoint,

View file

@ -1,12 +1,16 @@
import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types';
import {
ConnectorError,
ConnectorErrorCodes,
GetConnectorConfig,
ValidateConfig,
} from '@logto/connector-types';
import nock from 'nock';
import GoogleConnector from '.';
import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant';
import { mockedConfig } from './mock';
import { GoogleConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<GoogleConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const googleMethods = new GoogleConnector(getConnectorConfig);
@ -20,18 +24,29 @@ describe('google connector', () => {
jest.clearAllMocks();
});
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(
googleMethods.validateConfig({ clientId: 'clientId', clientSecret: 'clientSecret' })
).resolves.not.toThrow();
const validator: ValidateConfig = googleMethods.validateConfig;
expect(() => {
validator({ clientId: 'clientId', clientSecret: 'clientSecret' });
}).not.toThrow();
});
it('should throw on invalid config', async () => {
await expect(googleMethods.validateConfig({})).rejects.toThrow();
await expect(googleMethods.validateConfig({ clientId: 'clientId' })).rejects.toThrow();
await expect(
googleMethods.validateConfig({ clientSecret: 'clientSecret' })
).rejects.toThrow();
it('should fail on invalid config', async () => {
const validator: ValidateConfig = googleMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
expect(() => {
validator({ clientId: 'clientId' });
}).toThrow();
expect(() => {
validator({ clientSecret: 'clientSecret' });
}).toThrow();
});
});

View file

@ -8,7 +8,6 @@ import {
GetAuthorizationUri,
GetUserInfo,
ConnectorMetadata,
ValidateConfig,
SocialConnector,
GetConnectorConfig,
codeWithRedirectDataGuard,
@ -31,22 +30,24 @@ import {
userInfoResponseGuard,
} from './types';
export default class GoogleConnector implements SocialConnector {
export default class GoogleConnector implements SocialConnector<GoogleConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig<GoogleConfig>) {}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is GoogleConfig {
const result = googleConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const queryParameters = new URLSearchParams({
client_id: config.clientId,
redirect_uri: redirectUri,
@ -59,7 +60,11 @@ export default class GoogleConnector implements SocialConnector {
};
public getAccessToken = async (code: string, redirectUri: string) => {
const { clientId, clientSecret } = await this.getConfig(this.metadata.id);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { clientId, clientSecret } = config;
// NoteNeed to decodeURIComponent on code
// https://stackoverflow.com/questions/51058256/google-api-node-js-invalid-grant-malformed-auth-code

View file

@ -1,10 +1,10 @@
import { GetConnectorConfig } from '@logto/connector-types';
import { GetConnectorConfig, ValidateConfig } from '@logto/connector-types';
import SendGridMailConnector from '.';
import { mockedConfig } from './mock';
import { ContextType, SendGridMailConfig } from './types';
import { ContextType } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<SendGridMailConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const sendGridMailMethods = new SendGridMailConnector(getConnectorConfig);
@ -19,9 +19,15 @@ describe('validateConfig()', () => {
jest.clearAllMocks();
});
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(
sendGridMailMethods.validateConfig({
const validator: ValidateConfig = sendGridMailMethods.validateConfig;
expect(() => {
validator({
apiKey: 'apiKey',
fromEmail: 'noreply@logto.test.io',
fromName: 'Logto Test',
@ -33,11 +39,14 @@ describe('validateConfig()', () => {
content: 'This is for testing purposes only. Your passcode is {{code}}.',
},
],
})
).resolves.not.toThrow();
});
}).not.toThrow();
});
it('throws if config is invalid', async () => {
await expect(sendGridMailMethods.validateConfig({})).rejects.toThrow();
it('should be false if config is invalid', async () => {
const validator: ValidateConfig = sendGridMailMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
});
});

View file

@ -3,7 +3,6 @@ import {
ConnectorErrorCodes,
ConnectorMetadata,
EmailSendMessageFunction,
ValidateConfig,
EmailConnector,
GetConnectorConfig,
} from '@logto/connector-types';
@ -20,22 +19,23 @@ import {
PublicParameters,
} from './types';
export default class SendGridMailConnector implements EmailConnector {
export default class SendGridMailConnector implements EmailConnector<SendGridMailConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig) {}
constructor(public readonly getConfig: GetConnectorConfig<SendGridMailConfig>) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is SendGridMailConfig {
const result = sendGridMailConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public sendMessage: EmailSendMessageFunction = async (address, type, data) => {
const config = await this.getConfig(this.metadata.id);
await this.validateConfig(config);
this.validateConfig(config);
const { apiKey, fromEmail, fromName, templates } = config;
const template = templates.find((template) => template.usageType === type);

View file

@ -1,9 +1,8 @@
import { GetConnectorConfig } from '@logto/connector-types';
import { GetConnectorConfig, ValidateConfig } from '@logto/connector-types';
import SmtpConnector from '.';
import { SmtpConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<SmtpConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const smtpMethods = new SmtpConnector(getConnectorConfig);
@ -12,9 +11,15 @@ describe('validateConfig()', () => {
jest.clearAllMocks();
});
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(
smtpMethods.validateConfig({
const validator: ValidateConfig = smtpMethods.validateConfig;
expect(() => {
validator({
host: 'smtp.testing.com',
port: 80,
password: 'password',
@ -46,11 +51,14 @@ describe('validateConfig()', () => {
usageType: 'ForgotPassword',
},
],
})
).resolves.not.toThrow();
});
}).not.toThrow();
});
it('throws if config is invalid', async () => {
await expect(smtpMethods.validateConfig({})).rejects.toThrow();
it('should be false if config is invalid', async () => {
const validator: ValidateConfig = smtpMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
});
});

View file

@ -3,7 +3,6 @@ import {
ConnectorErrorCodes,
ConnectorMetadata,
EmailSendMessageFunction,
ValidateConfig,
EmailConnector,
GetConnectorConfig,
} from '@logto/connector-types';
@ -14,22 +13,24 @@ import SMTPTransport from 'nodemailer/lib/smtp-transport';
import { defaultMetadata } from './constant';
import { ContextType, smtpConfigGuard, SmtpConfig } from './types';
export default class SmtpConnector implements EmailConnector {
export default class SmtpConnector implements EmailConnector<SmtpConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig<SmtpConfig>) {}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is SmtpConfig {
const result = smtpConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public sendMessage: EmailSendMessageFunction = async (address, type, data) => {
const config = await this.getConfig(this.metadata.id);
await this.validateConfig(config);
this.validateConfig(config);
const { host, port, username, password, fromEmail, replyTo, templates } = config;
const template = templates.find((template) => template.usageType === type);

View file

@ -1,10 +1,9 @@
import { GetConnectorConfig } from '@logto/connector-types';
import { GetConnectorConfig, ValidateConfig } from '@logto/connector-types';
import TwilioSmsConnector from '.';
import { mockedConfig } from './mock';
import { TwilioSmsConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<TwilioSmsConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const twilioSmsMethods = new TwilioSmsConnector(getConnectorConfig);
@ -13,11 +12,22 @@ describe('validateConfig()', () => {
jest.clearAllMocks();
});
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(twilioSmsMethods.validateConfig(mockedConfig)).resolves.not.toThrow();
const validator: ValidateConfig = twilioSmsMethods.validateConfig;
expect(() => {
validator(mockedConfig);
}).not.toThrow();
});
it('throws if config is invalid', async () => {
await expect(twilioSmsMethods.validateConfig({})).rejects.toThrow();
const validator: ValidateConfig = twilioSmsMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
});
});

View file

@ -3,7 +3,6 @@ import {
ConnectorErrorCodes,
ConnectorMetadata,
EmailSendMessageFunction,
ValidateConfig,
SmsConnector,
GetConnectorConfig,
} from '@logto/connector-types';
@ -13,22 +12,24 @@ import got, { HTTPError } from 'got';
import { defaultMetadata, endpoint } from './constant';
import { twilioSmsConfigGuard, TwilioSmsConfig, PublicParameters } from './types';
export default class TwilioSmsConnector implements SmsConnector {
export default class TwilioSmsConnector implements SmsConnector<TwilioSmsConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig<TwilioSmsConfig>) {}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is TwilioSmsConfig {
const result = twilioSmsConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public sendMessage: EmailSendMessageFunction = async (address, type, data) => {
const config = await this.getConfig(this.metadata.id);
await this.validateConfig(config);
this.validateConfig(config);
const { accountSID, authToken, fromMessagingServiceSID, templates } = config;
const template = templates.find((template) => template.usageType === type);

View file

@ -80,26 +80,26 @@ export type SmsSendMessageFunction<T = unknown> = (
config?: Record<string, unknown>
) => Promise<T>;
export interface BaseConnector {
export interface BaseConnector<T = unknown> {
metadata: ConnectorMetadata;
validateConfig: ValidateConfig;
getConfig: GetConnectorConfig;
validateConfig: ValidateConfig<T>;
}
export interface SmsConnector extends BaseConnector {
export interface SmsConnector<T = unknown> extends BaseConnector<T> {
sendMessage: SmsSendMessageFunction;
}
export interface EmailConnector extends BaseConnector {
export interface EmailConnector<T = unknown> extends BaseConnector<T> {
sendMessage: EmailSendMessageFunction;
}
export interface SocialConnector extends BaseConnector {
export interface SocialConnector<T = unknown> extends BaseConnector<T> {
getAuthorizationUri: GetAuthorizationUri;
getUserInfo: GetUserInfo;
}
export type ValidateConfig<T = Record<string, unknown>> = (config: T) => Promise<void>;
export type ValidateConfig<T = unknown> = (config: unknown) => asserts config is T;
export type GetAuthorizationUri = (payload: {
state: string;
@ -110,7 +110,7 @@ export type GetUserInfo = (
data: unknown
) => Promise<{ id: string } & Record<string, string | undefined>>;
export type GetConnectorConfig<T = Record<string, unknown>> = (id: string) => Promise<T>;
export type GetConnectorConfig = (id: string) => Promise<unknown>;
export const codeDataGuard = z.object({
code: z.string(),

View file

@ -1,12 +1,16 @@
import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types';
import {
ConnectorError,
ConnectorErrorCodes,
GetConnectorConfig,
ValidateConfig,
} from '@logto/connector-types';
import nock from 'nock';
import WechatNativeConnector from '.';
import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant';
import { mockedConfig } from './mock';
import { WechatNativeConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<WechatNativeConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const wechatNativeMethods = new WechatNativeConnector(getConnectorConfig);
@ -92,16 +96,30 @@ describe('getAccessToken', () => {
});
describe('validateConfig', () => {
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(
wechatNativeMethods.validateConfig({ appId: 'appId', appSecret: 'appSecret' })
).resolves.not.toThrow();
const validator: ValidateConfig = wechatNativeMethods.validateConfig;
expect(() => {
validator({ appId: 'appId', appSecret: 'appSecret' });
}).not.toThrow();
});
it('should throw on empty config', async () => {
await expect(wechatNativeMethods.validateConfig({})).rejects.toThrowError();
it('should fail on empty config', async () => {
const validator: ValidateConfig = wechatNativeMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
});
it('should throw when missing appSecret', async () => {
await expect(wechatNativeMethods.validateConfig({ appId: 'appId' })).rejects.toThrowError();
it('should fail when missing appSecret', async () => {
const validator: ValidateConfig = wechatNativeMethods.validateConfig;
expect(() => {
validator({ appId: 'appId' });
}).toThrow();
});
});

View file

@ -6,7 +6,6 @@
import {
ConnectorMetadata,
GetAuthorizationUri,
ValidateConfig,
GetUserInfo,
ConnectorError,
ConnectorErrorCodes,
@ -35,21 +34,25 @@ import {
WechatNativeConfig,
} from './types';
export default class WechatNativeConnector implements SocialConnector {
export default class WechatNativeConnector implements SocialConnector<WechatNativeConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig<WechatNativeConfig>) {}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is WechatNativeConfig {
const result = wechatNativeConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public getAuthorizationUri: GetAuthorizationUri = async ({ state }) => {
const { appId, universalLinks } = await this.getConfig(this.metadata.id);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { appId, universalLinks } = config;
const queryParameters = new URLSearchParams({
app_id: appId,
@ -65,7 +68,11 @@ export default class WechatNativeConnector implements SocialConnector {
public getAccessToken = async (
code: string
): Promise<{ accessToken: string; openid: string }> => {
const { appId: appid, appSecret: secret } = await this.getConfig(this.metadata.id);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { appId: appid, appSecret: secret } = config;
const httpResponse = await got.get(accessTokenEndpoint, {
searchParams: { appid, secret, code, grant_type: 'authorization_code' },

View file

@ -1,12 +1,16 @@
import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types';
import {
ConnectorError,
ConnectorErrorCodes,
GetConnectorConfig,
ValidateConfig,
} from '@logto/connector-types';
import nock from 'nock';
import WechatConnector from '.';
import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant';
import { mockedConfig } from './mock';
import { WechatConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<WechatConfig>;
const getConnectorConfig = jest.fn() as GetConnectorConfig;
const wechatMethods = new WechatConnector(getConnectorConfig);
@ -92,16 +96,30 @@ describe('getAccessToken', () => {
});
describe('validateConfig', () => {
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
it('should pass on valid config', async () => {
await expect(
wechatMethods.validateConfig({ appId: 'appId', appSecret: 'appSecret' })
).resolves.not.toThrow();
const validator: ValidateConfig = wechatMethods.validateConfig;
expect(() => {
validator({ appId: 'appId', appSecret: 'appSecret' });
}).not.toThrow();
});
it('should throw on empty config', async () => {
await expect(wechatMethods.validateConfig({})).rejects.toThrowError();
it('should fail on empty config', async () => {
const validator: ValidateConfig = wechatMethods.validateConfig;
expect(() => {
validator({});
}).toThrow();
});
it('should throw when missing appSecret', async () => {
await expect(wechatMethods.validateConfig({ appId: 'appId' })).rejects.toThrowError();
it('should fail when missing appSecret', async () => {
const validator: ValidateConfig = wechatMethods.validateConfig;
expect(() => {
validator({ appId: 'appId' });
}).toThrow();
});
});

View file

@ -6,7 +6,6 @@
import {
ConnectorMetadata,
GetAuthorizationUri,
ValidateConfig,
GetUserInfo,
ConnectorError,
ConnectorErrorCodes,
@ -36,21 +35,25 @@ import {
WechatConfig,
} from './types';
export default class WechatConnector implements SocialConnector {
export default class WechatConnector implements SocialConnector<WechatConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
constructor(public readonly getConfig: GetConnectorConfig<WechatConfig>) {}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig: ValidateConfig = async (config: unknown) => {
public validateConfig(config: unknown): asserts config is WechatConfig {
const result = wechatConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};
}
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
const { appId } = await this.getConfig(this.metadata.id);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { appId } = config;
const queryParameters = new URLSearchParams({
appid: appId,
@ -66,7 +69,11 @@ export default class WechatConnector implements SocialConnector {
public getAccessToken = async (
code: string
): Promise<{ accessToken: string; openid: string }> => {
const { appId: appid, appSecret: secret } = await this.getConfig(this.metadata.id);
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
const { appId: appid, appSecret: secret } = config;
const httpResponse = await got.get(accessTokenEndpoint, {
searchParams: { appid, secret, code, grant_type: 'authorization_code' },

View file

@ -1,14 +1,12 @@
import { ArbitraryObject } from '@logto/schemas';
import RequestError from '@/errors/RequestError';
import { findAllConnectors } from '@/queries/connector';
import assertThat from '@/utils/assert-that';
export const getConnectorConfig = async <T extends ArbitraryObject>(id: string): Promise<T> => {
export const getConnectorConfig = async (id: string): Promise<unknown> => {
const connectors = await findAllConnectors();
const connector = connectors.find((connector) => connector.id === id);
assertThat(connector, new RequestError({ code: 'entity.not_found', id, status: 404 }));
return connector.config as T;
return connector.config;
};

View file

@ -1,3 +1,4 @@
import { ValidateConfig } from '@logto/connector-types';
import { arbitraryObjectGuard, ConnectorDto, Connectors, ConnectorType } from '@logto/schemas';
import { emailRegEx, phoneRegEx } from '@logto/shared';
import { object, string } from 'zod';
@ -84,14 +85,22 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
params: { id },
body: { enabled },
} = ctx.guard;
const connectorInstance = await getConnectorInstanceById(id);
const {
connector: { config },
metadata,
validateConfig,
} = await getConnectorInstanceById(id);
} = connectorInstance;
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
// eslint-disable-next-line unicorn/consistent-destructuring
const validator: ValidateConfig = connectorInstance.validateConfig;
if (enabled) {
await validateConfig(config);
validator(config);
}
// Only allow one enabled connector for SMS and Email.
@ -135,10 +144,19 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
params: { id },
body,
} = ctx.guard;
const { metadata, validateConfig } = await getConnectorInstanceById(id);
const connectorInstance = await getConnectorInstanceById(id);
const { metadata } = connectorInstance;
/**
* Assertion functions always need explicit annotations.
* See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014
*/
// eslint-disable-next-line unicorn/consistent-destructuring
const validator: ValidateConfig = connectorInstance.validateConfig;
if (body.config) {
await validateConfig(body.config);
validator(body.config);
}
const connector = await updateConnector({ set: body, where: { id }, jsonbMode: 'replace' });
@ -187,10 +205,6 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
})
);
if (config) {
await connector.validateConfig(config);
}
await connector.sendMessage(
subject,
'Test',

View file

@ -38,7 +38,7 @@ const getConnectorInstanceByIdPlaceHolder = jest.fn(async (connectorId: string)
sendMessage: sendMessagePlaceHolder,
};
});
const validateConfigPlaceHolder = jest.fn();
const validateConfigPlaceHolder = jest.fn() as jest.MockedFunction<ValidateConfig>;
const sendMessagePlaceHolder = jest.fn();
jest.mock('@/queries/connector', () => ({
@ -98,7 +98,7 @@ describe('connector PATCH routes', () => {
});
it('enables one of the social connectors (with invalid config)', async () => {
validateConfigPlaceHolder.mockImplementationOnce(async () => {
validateConfigPlaceHolder.mockImplementationOnce(() => {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
});
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([
@ -187,7 +187,7 @@ describe('connector PATCH routes', () => {
});
it('enables one of the email/sms connectors (with invalid config)', async () => {
validateConfigPlaceHolder.mockImplementationOnce(async () => {
validateConfigPlaceHolder.mockImplementationOnce(() => {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
});
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([
@ -247,7 +247,7 @@ describe('connector PATCH routes', () => {
});
it('config validation fails', async () => {
validateConfigPlaceHolder.mockImplementationOnce(async () => {
validateConfigPlaceHolder.mockImplementationOnce(() => {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
});
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([