0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(connector-twilio-sms): add twilio sms connector (#881)

* feat(connector-twilio-sms): add twilio sms connector

* feat(connector-twilio-sms): add twilio sms connector to initialization
This commit is contained in:
Darcy Ye 2022-05-20 09:56:16 +08:00 committed by GitHub
parent 17c63cd2d9
commit d7ce13d260
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 374 additions and 2 deletions

View file

@ -0,0 +1,2 @@
### Twilio SMS README
placeholder

View file

@ -0,0 +1,21 @@
```json
{
"accountSID": "<account-sid>",
"authToken": "<auth-token>",
"fromMessagingServiceSID": "<from-messaging-service-sid>",
"templates": [
{
"usageType": "SignIn",
"content": "This is for sign-in purposes only. Your passcode is {{code}}.",
},
{
"usageType": "Register",
"content": "This is for registering purposes only. Your passcode is {{code}}.",
},
{
"usageType": "Test",
"content": "This is for testing purposes only. Your passcode is {{code}}.",
}
]
}
```

View file

@ -0,0 +1,8 @@
import { Config, merge } from '@silverhand/jest-config';
const config: Config.InitialOptions = merge({
testEnvironment: 'node',
setupFilesAfterEnv: ['jest-matcher-specific-error'],
});
export default config;

View file

@ -0,0 +1,55 @@
{
"name": "@logto/connector-twilio-sms",
"version": "0.1.0",
"description": "Twilio SMS connector implementation.",
"main": "./lib/index.js",
"exports": "./lib/index.js",
"author": "Silverhand Inc. <contact@silverhand.io>",
"license": "MPL-2.0",
"files": [
"lib",
"docs"
],
"private": false,
"scripts": {
"preinstall": "npx only-allow pnpm",
"precommit": "lint-staged",
"build": "rm -rf lib/ && tsc -p tsconfig.build.json",
"lint": "eslint --ext .ts src",
"lint:report": "pnpm lint --format json --output-file report.json",
"dev": "rm -rf lib/ && tsc-watch -p tsconfig.build.json --preserveWatchOutput --onSuccess \"node ./lib/index.js\"",
"test": "jest",
"test:coverage": "jest --coverage --silent",
"prepack": "pnpm build"
},
"dependencies": {
"@logto/connector-types": "^0.1.0",
"@logto/shared": "^0.1.0",
"@silverhand/essentials": "^1.1.6",
"@silverhand/jest-config": "^0.14.0",
"got": "^11.8.2",
"zod": "^3.14.3"
},
"devDependencies": {
"@jest/types": "^27.5.1",
"@silverhand/eslint-config": "^0.14.0",
"@silverhand/ts-config": "^0.14.0",
"@types/jest": "^27.4.1",
"@types/node": "^16.3.1",
"eslint": "^8.10.0",
"jest": "^27.5.1",
"jest-matcher-specific-error": "^1.0.0",
"lint-staged": "^12.0.0",
"prettier": "^2.3.2",
"ts-jest": "^27.1.1",
"tsc-watch": "^5.0.0",
"typescript": "^4.6.2"
},
"engines": {
"node": "^16.0.0"
},
"eslintConfig": {
"extends": "@silverhand"
},
"prettier": "@silverhand/eslint-config/.prettierrc"
}

View file

@ -0,0 +1,30 @@
import path from 'path';
import { ConnectorType, ConnectorMetadata } from '@logto/connector-types';
import { getFileContents } from '@logto/shared';
export const endpoint = 'https://api.twilio.com/2010-04-01/Accounts/{{accountSID}}/Messages.json';
// eslint-disable-next-line unicorn/prefer-module
const currentPath = __dirname;
const pathToReadmeFile = path.join(currentPath, '..', 'README.md');
const pathToConfigTemplate = path.join(currentPath, '..', 'docs', 'config-template.md');
const readmeContentFallback = 'Please check README.md file directory.';
const configTemplateFallback = 'Please check config-template.md file directory.';
export const defaultMetadata: ConnectorMetadata = {
target: 'twilio-sms',
type: ConnectorType.SMS,
platform: null,
name: {
en: 'Twilio SMS Service',
'zh-CN': 'Twilio 短信服务',
},
logo: './logo.png',
description: {
en: 'Messaging APIs for reliable SMS delivery.',
'zh-CN': '可信赖的短信消息 API。',
},
readme: getFileContents(pathToReadmeFile, readmeContentFallback),
configTemplate: getFileContents(pathToConfigTemplate, configTemplateFallback),
};

View file

@ -0,0 +1,23 @@
import { GetConnectorConfig } from '@logto/connector-types';
import { TwilioSmsConnector } from '.';
import { mockedConfig } from './mock';
import { TwilioSmsConfig } from './types';
const getConnectorConfig = jest.fn() as GetConnectorConfig<TwilioSmsConfig>;
const twilioSmsMethods = new TwilioSmsConnector(getConnectorConfig);
describe('validateConfig()', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should pass on valid config', async () => {
await expect(twilioSmsMethods.validateConfig(mockedConfig)).resolves.not.toThrow();
});
it('throws if config is invalid', async () => {
await expect(twilioSmsMethods.validateConfig({})).rejects.toThrow();
});
});

View file

@ -0,0 +1,67 @@
import {
ConnectorError,
ConnectorErrorCodes,
ConnectorMetadata,
EmailSendMessageFunction,
ValidateConfig,
SmsConnector,
GetConnectorConfig,
} from '@logto/connector-types';
import { assert } from '@silverhand/essentials';
import got from 'got';
import { defaultMetadata, endpoint } from './constant';
import { twilioSmsConfigGuard, SendSmsResponse, TwilioSmsConfig, PublicParameters } from './types';
export class TwilioSmsConnector implements SmsConnector {
public metadata: ConnectorMetadata = defaultMetadata;
public readonly getConfig: GetConnectorConfig<TwilioSmsConfig>;
constructor(getConnectorConfig: GetConnectorConfig<TwilioSmsConfig>) {
this.getConfig = getConnectorConfig;
}
public validateConfig: ValidateConfig = async (config: unknown) => {
const result = twilioSmsConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error.message);
}
};
public sendMessage: EmailSendMessageFunction<SendSmsResponse> = async (address, type, data) => {
const config = await this.getConfig(this.metadata.target, this.metadata.platform);
await this.validateConfig(config);
const { accountSID, authToken, fromMessagingServiceSID, templates } = config;
const template = templates.find((template) => template.usageType === type);
assert(
template,
new ConnectorError(
ConnectorErrorCodes.TemplateNotFound,
`Cannot find template for type: ${type}`
)
);
const parameters: PublicParameters = {
To: address,
MessagingServiceSid: fromMessagingServiceSID,
Body:
typeof data.code === 'string'
? template.content.replace(/{{code}}/g, data.code)
: template.content,
};
return got
.post(endpoint.replace(/{{accountSID}}/g, accountSID), {
headers: {
Authorization:
'Basic ' + Buffer.from([accountSID, authToken].join(':')).toString('base64'),
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(parameters).toString(),
})
.json<SendSmsResponse>();
};
}

View file

@ -0,0 +1,17 @@
import { TwilioSmsConfig } from './types';
const mockedAccountSID = 'account-sid';
const mockedAuthToken = 'auth-token';
const mockedFromMessagingServiceSID = 'from-messaging-service-sid';
export const mockedConfig: TwilioSmsConfig = {
accountSID: mockedAccountSID,
authToken: mockedAuthToken,
fromMessagingServiceSID: mockedFromMessagingServiceSID,
templates: [
{
usageType: 'Test',
content: 'This is for testing purposes only. Your passcode is {{code}}.',
},
],
};

View file

@ -0,0 +1,63 @@
import { Nullable } from '@silverhand/essentials';
import { z } from 'zod';
/**
* @doc https://www.twilio.com/docs/sms/send-messages
*
* @doc https://www.twilio.com/docs/phone-numbers
* @doc https://www.twilio.com/phone-numbers/global-catalog
* @doc https://en.wikipedia.org/wiki/E.164
*/
export type PublicParameters = {
To: string;
MessagingServiceSid: string;
Body: string;
};
/**
* UsageType here is used to specify the use case of the template, can be either
* 'Register', 'SignIn', 'ForgotPassword' or 'Test'.
*/
const templateGuard = z.object({
usageType: z.string(),
content: z.string(),
});
export const twilioSmsConfigGuard = z.object({
accountSID: z.string(),
authToken: z.string(),
fromMessagingServiceSID: z.string(),
templates: z.array(templateGuard),
});
export type TwilioSmsConfig = z.infer<typeof twilioSmsConfigGuard>;
export type SendSmsResponse = {
account_sid: string;
api_version: string;
body: string;
code: number;
date_cereated: Nullable<string>;
date_sent: Nullable<string>;
date_updated: Nullable<string>;
direction: string;
error_code: Nullable<string>;
error_message: Nullable<string>;
from: Nullable<string>;
message: Nullable<string>;
messaging_service_sid: string;
more_info: Nullable<string>;
num_media: string;
num_segments: string;
price: Nullable<string>;
price_unit: Nullable<string>;
sid: string;
status: number;
subresource_uris: {
media?: string;
feedback?: string;
};
to: string;
uri: string;
};

View file

@ -0,0 +1,10 @@
{
"extends": "@silverhand/ts-config/tsconfig.base",
"compilerOptions": {
"outDir": "lib",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

View file

@ -0,0 +1,5 @@
{
"extends": "./tsconfig.base",
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}

View file

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.base",
"compilerOptions": {
"types": ["node", "jest", "jest-matcher-specific-error"]
},
"include": ["src", "jest.config.ts"]
}

View file

@ -0,0 +1,6 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"isolatedModules": false
}
}

View file

@ -27,6 +27,7 @@
"@logto/connector-github": "^0.1.0",
"@logto/connector-google": "^0.1.0",
"@logto/connector-sendgrid-email": "^0.1.0",
"@logto/connector-twilio-sms": "^0.1.0",
"@logto/connector-types": "^0.1.0",
"@logto/connector-wechat": "^0.1.0",
"@logto/connector-wechat-native": "^0.1.0",

View file

@ -67,6 +67,14 @@ const sendGridMailConnector = {
config: {},
createdAt: 1_646_382_233_111,
};
const twilioSmsConnector = {
id: 'twilio-sms',
target: 'twilio-sms',
platform: null,
enabled: false,
config: {},
createdAt: 1_646_382_233_000,
};
const wechatConnector = {
id: 'wechat',
target: 'wechat',
@ -92,6 +100,7 @@ const connectors = [
githubConnector,
googleConnector,
sendGridMailConnector,
twilioSmsConnector,
wechatConnector,
wechatNativeConnector,
];
@ -116,8 +125,9 @@ describe('getConnectorInstances', () => {
expect(connectorInstances[4]).toHaveProperty('connector', githubConnector);
expect(connectorInstances[5]).toHaveProperty('connector', googleConnector);
expect(connectorInstances[6]).toHaveProperty('connector', sendGridMailConnector);
expect(connectorInstances[7]).toHaveProperty('connector', wechatConnector);
expect(connectorInstances[8]).toHaveProperty('connector', wechatNativeConnector);
expect(connectorInstances[7]).toHaveProperty('connector', twilioSmsConnector);
expect(connectorInstances[8]).toHaveProperty('connector', wechatConnector);
expect(connectorInstances[9]).toHaveProperty('connector', wechatNativeConnector);
});
test('should throw if any required connector does not exist in DB', async () => {

View file

@ -5,6 +5,7 @@ import { FacebookConnector } from '@logto/connector-facebook';
import { GithubConnector } from '@logto/connector-github';
import { GoogleConnector } from '@logto/connector-google';
import { SendGridMailConnector } from '@logto/connector-sendgrid-email';
import { TwilioSmsConnector } from '@logto/connector-twilio-sms';
import { WeChatConnector } from '@logto/connector-wechat';
import { WeChatNativeConnector } from '@logto/connector-wechat-native';
import { nanoid } from 'nanoid';
@ -23,6 +24,7 @@ const allConnectors: IConnector[] = [
new GithubConnector(getConnectorConfig),
new GoogleConnector(getConnectorConfig),
new SendGridMailConnector(getConnectorConfig),
new TwilioSmsConnector(getConnectorConfig),
new WeChatConnector(getConnectorConfig),
new WeChatNativeConnector(getConnectorConfig),
];

View file

@ -349,6 +349,49 @@ importers:
tsc-watch: 5.0.3_typescript@4.6.4
typescript: 4.6.4
packages/connector-twilio-sms:
specifiers:
'@jest/types': ^27.5.1
'@logto/connector-types': ^0.1.0
'@logto/shared': ^0.1.0
'@silverhand/eslint-config': ^0.14.0
'@silverhand/essentials': ^1.1.6
'@silverhand/jest-config': ^0.14.0
'@silverhand/ts-config': ^0.14.0
'@types/jest': ^27.4.1
'@types/node': ^16.3.1
eslint: ^8.10.0
got: ^11.8.2
jest: ^27.5.1
jest-matcher-specific-error: ^1.0.0
lint-staged: ^12.0.0
prettier: ^2.3.2
ts-jest: ^27.1.1
tsc-watch: ^5.0.0
typescript: ^4.6.2
zod: ^3.14.3
dependencies:
'@logto/connector-types': link:../connector-types
'@logto/shared': link:../shared
'@silverhand/essentials': 1.1.7
'@silverhand/jest-config': 0.14.0_53ggqi2i4rbcfjtktmjua6zili
got: 11.8.3
zod: 3.14.3
devDependencies:
'@jest/types': 27.5.1
'@silverhand/eslint-config': 0.14.0_rqoong6vegs374egqglqjbgiwm
'@silverhand/ts-config': 0.14.0_typescript@4.6.4
'@types/jest': 27.4.1
'@types/node': 16.11.12
eslint: 8.10.0
jest: 27.5.1
jest-matcher-specific-error: 1.0.0
lint-staged: 12.4.0
prettier: 2.5.1
ts-jest: 27.1.1_53ggqi2i4rbcfjtktmjua6zili
tsc-watch: 5.0.3_typescript@4.6.4
typescript: 4.6.4
packages/connector-types:
specifiers:
'@jest/types': ^27.5.1
@ -599,6 +642,7 @@ importers:
'@logto/connector-github': ^0.1.0
'@logto/connector-google': ^0.1.0
'@logto/connector-sendgrid-email': ^0.1.0
'@logto/connector-twilio-sms': ^0.1.0
'@logto/connector-types': ^0.1.0
'@logto/connector-wechat': ^0.1.0
'@logto/connector-wechat-native': ^0.1.0
@ -671,6 +715,7 @@ importers:
'@logto/connector-github': link:../connector-github
'@logto/connector-google': link:../connector-google
'@logto/connector-sendgrid-email': link:../connector-sendgrid-mail
'@logto/connector-twilio-sms': link:../connector-twilio-sms
'@logto/connector-types': link:../connector-types
'@logto/connector-wechat': link:../connector-wechat
'@logto/connector-wechat-native': link:../connector-wechat-native