Skip to content

Commit 947d2e4

Browse files
authored
feat(connector): add vonage connector (#6768)
1 parent 00e1752 commit 947d2e4

File tree

10 files changed

+662
-19
lines changed

10 files changed

+662
-19
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Vonage SMS connector
2+
3+
The official Logto connector for Vonage SMS.
4+
5+
## Get started
6+
7+
Vonage is a global communications provider, offering robust cloud-based communication services, including SMS (short message service). The Vonage SMS Connector is a plugin provided by the Logto team to enable Logto end-users to register and sign in to their Logto account via SMS verification codes.
8+
9+
## Set up in Vonage
10+
11+
> 💡 **Tip**
12+
>
13+
> You can skip this step if you have already completed them.
14+
15+
To work with this connector, you will need to [sign up for an account](https://developer.vonage.com/en/account/guides/dashboard-management#create-and-configure-a-vonage-account) in Vonage. This will give you an API key and secret that you can use to access the APIs through this connector.
16+
17+
Once you have an account, you can find your API key and API secret at the top of the Vonage API Dashboard.
18+
19+
And you may need to [rant a virtual number](https://developer.vonage.com/en/numbers/guides/number-management#rent-a-virtual-number) to send SMS messages.
20+
21+
See the [Vonage SMS API](https://developer.vonage.com/en/messaging/sms/overview) for more information.
22+
23+
## Set up in Logto
24+
25+
1. **API Key**: Your Vonage API key.
26+
2. **API Secret**: Your Vonage API secret.
27+
3. **Brand Name**: The brand name you want to use to send the SMS, this is also called the `from` field, see the [Sender Identity](https://developer.vonage.com/en/messaging/sms/guides/custom-sender-id) for more information.
28+
4. **Templates**: The templates you want to use to send the SMS, you can use the default templates or modify them as needed.
Lines changed: 9 additions & 0 deletions
Loading
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"name": "@logto/connector-vonage-sms",
3+
"version": "0.0.0",
4+
"description": "Vonage SMS connector implementation.",
5+
"author": "Silverhand Inc. <contact@silverhand.io>",
6+
"dependencies": {
7+
"@logto/connector-kit": "workspace:^4.0.0",
8+
"@silverhand/essentials": "^2.9.1",
9+
"@vonage/auth": "^1.12.0",
10+
"@vonage/server-sdk": "^3.19.0",
11+
"zod": "^3.23.8"
12+
},
13+
"main": "./lib/index.js",
14+
"module": "./lib/index.js",
15+
"exports": "./lib/index.js",
16+
"license": "MPL-2.0",
17+
"type": "module",
18+
"files": [
19+
"lib",
20+
"docs",
21+
"logo.svg",
22+
"logo-dark.svg"
23+
],
24+
"scripts": {
25+
"precommit": "lint-staged",
26+
"check": "tsc --noEmit",
27+
"build": "tsup",
28+
"dev": "tsup --watch",
29+
"lint": "eslint --ext .ts src",
30+
"lint:report": "pnpm lint --format json --output-file report.json",
31+
"test": "vitest src",
32+
"test:ci": "pnpm run test --silent --coverage",
33+
"prepublishOnly": "pnpm build"
34+
},
35+
"engines": {
36+
"node": "^20.9.0"
37+
},
38+
"eslintConfig": {
39+
"extends": "@silverhand",
40+
"settings": {
41+
"import/core-modules": [
42+
"@silverhand/essentials",
43+
"got",
44+
"nock",
45+
"snakecase-keys",
46+
"zod"
47+
]
48+
}
49+
},
50+
"prettier": "@silverhand/eslint-config/.prettierrc",
51+
"publishConfig": {
52+
"access": "public"
53+
},
54+
"devDependencies": {
55+
"@silverhand/eslint-config": "6.0.1",
56+
"@silverhand/ts-config": "6.0.0",
57+
"@types/node": "^20.11.20",
58+
"@types/supertest": "^6.0.2",
59+
"@vitest/coverage-v8": "^2.0.0",
60+
"eslint": "^8.56.0",
61+
"lint-staged": "^15.0.2",
62+
"prettier": "^3.0.0",
63+
"supertest": "^7.0.0",
64+
"tsup": "^8.3.0",
65+
"typescript": "^5.5.3",
66+
"vitest": "^2.0.0"
67+
}
68+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { ConnectorMetadata } from '@logto/connector-kit';
2+
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
3+
4+
export const defaultMetadata: ConnectorMetadata = {
5+
id: 'vonage-sms',
6+
target: 'vonage-sms',
7+
platform: null,
8+
name: {
9+
en: 'Vonage SMS Service',
10+
},
11+
logo: './logo.svg',
12+
logoDark: null,
13+
description: {
14+
en: 'Communications APIs to connect the world',
15+
},
16+
readme: './README.md',
17+
formItems: [
18+
{
19+
key: 'apiKey',
20+
label: 'API Key',
21+
type: ConnectorConfigFormItemType.Text,
22+
required: true,
23+
},
24+
{
25+
key: 'apiSecret',
26+
label: 'API Secret',
27+
type: ConnectorConfigFormItemType.Text,
28+
required: true,
29+
},
30+
{
31+
key: 'brandName',
32+
label: 'Brand Name',
33+
type: ConnectorConfigFormItemType.Text,
34+
required: true,
35+
},
36+
{
37+
key: 'templates',
38+
label: 'Templates',
39+
type: ConnectorConfigFormItemType.Json,
40+
required: true,
41+
defaultValue: [
42+
{
43+
usageType: 'SignIn',
44+
content:
45+
'Your Logto sign-in verification code is {{code}}. The code will remain active for 10 minutes.',
46+
},
47+
{
48+
usageType: 'Register',
49+
content:
50+
'Your Logto sign-up verification code is {{code}}. The code will remain active for 10 minutes.',
51+
},
52+
{
53+
usageType: 'ForgotPassword',
54+
content:
55+
'Your Logto password change verification code is {{code}}. The code will remain active for 10 minutes.',
56+
},
57+
{
58+
usageType: 'Generic',
59+
content:
60+
'Your Logto verification code is {{code}}. The code will remain active for 10 minutes.',
61+
},
62+
],
63+
},
64+
],
65+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import createConnector from './index.js';
2+
import { mockedConfig } from './mock.js';
3+
4+
const getConfig = vi.fn().mockResolvedValue(mockedConfig);
5+
6+
describe('Vonage SMS connector', () => {
7+
it('init without throwing errors', async () => {
8+
await expect(createConnector({ getConfig })).resolves.not.toThrow();
9+
});
10+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { assert } from '@silverhand/essentials';
2+
3+
import type {
4+
GetConnectorConfig,
5+
SendMessageFunction,
6+
CreateConnector,
7+
SmsConnector,
8+
} from '@logto/connector-kit';
9+
import {
10+
ConnectorError,
11+
ConnectorErrorCodes,
12+
validateConfig,
13+
ConnectorType,
14+
replaceSendMessageHandlebars,
15+
} from '@logto/connector-kit';
16+
import { Auth } from '@vonage/auth';
17+
import { Vonage } from '@vonage/server-sdk';
18+
19+
import { defaultMetadata } from './constant.js';
20+
import { vonageSmsConfigGuard } from './types.js';
21+
22+
const sendMessage =
23+
(getConfig: GetConnectorConfig): SendMessageFunction =>
24+
async (data, inputConfig) => {
25+
const { to, type, payload } = data;
26+
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
27+
validateConfig(config, vonageSmsConfigGuard);
28+
const { apiKey, apiSecret, brandName, templates } = config;
29+
const template = templates.find((template) => template.usageType === type);
30+
31+
assert(
32+
template,
33+
new ConnectorError(
34+
ConnectorErrorCodes.TemplateNotFound,
35+
`Cannot find template for type: ${type}`
36+
)
37+
);
38+
39+
const vonageAuth = new Auth({
40+
apiKey,
41+
apiSecret,
42+
});
43+
const vonage = new Vonage(vonageAuth);
44+
45+
try {
46+
return await vonage.sms.send({
47+
from: brandName,
48+
to,
49+
text: replaceSendMessageHandlebars(template.content, payload),
50+
});
51+
} catch (error: unknown) {
52+
if (error instanceof Error) {
53+
throw new ConnectorError(ConnectorErrorCodes.General, error.message);
54+
}
55+
56+
throw error;
57+
}
58+
};
59+
60+
const createVonageSmsConnector: CreateConnector<SmsConnector> = async ({ getConfig }) => {
61+
return {
62+
metadata: defaultMetadata,
63+
type: ConnectorType.Sms,
64+
configGuard: vonageSmsConfigGuard,
65+
sendMessage: sendMessage(getConfig),
66+
};
67+
};
68+
69+
export default createVonageSmsConnector;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { VonageSmsConfig } from './types.js';
2+
3+
const mockedApiKey = 'api-key';
4+
const mockedApiSecret = 'api-secret';
5+
const mockedBrandName = 'brand name';
6+
7+
export const mockedConfig: VonageSmsConfig = {
8+
apiKey: mockedApiKey,
9+
apiSecret: mockedApiSecret,
10+
brandName: mockedBrandName,
11+
templates: [
12+
{
13+
usageType: 'Generic',
14+
content: 'This is for testing purposes only. Your verification code is {{code}}.',
15+
},
16+
],
17+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { z } from 'zod';
2+
3+
/**
4+
* UsageType here is used to specify the use case of the template, can be either
5+
* 'Register', 'SignIn', 'ForgotPassword', 'Generic'.
6+
*/
7+
const requiredTemplateUsageTypes = ['Register', 'SignIn', 'ForgotPassword', 'Generic'];
8+
9+
const templateGuard = z.object({
10+
usageType: z.string(),
11+
content: z.string(),
12+
});
13+
14+
export const vonageSmsConfigGuard = z.object({
15+
apiKey: z.string(),
16+
apiSecret: z.string(),
17+
brandName: z.string(),
18+
templates: z.array(templateGuard).refine(
19+
(templates) =>
20+
requiredTemplateUsageTypes.every((requiredType) =>
21+
templates.map((template) => template.usageType).includes(requiredType)
22+
),
23+
(templates) => ({
24+
message: `Template with UsageType (${requiredTemplateUsageTypes
25+
.filter(
26+
(requiredType) => !templates.map((template) => template.usageType).includes(requiredType)
27+
)
28+
.join(', ')}) should be provided!`,
29+
})
30+
),
31+
});
32+
33+
export type VonageSmsConfig = z.infer<typeof vonageSmsConfigGuard>;

packages/console/src/components/CreateConnectorForm/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ function CreateConnectorForm({ onClose, isOpen: isFormOpen, type }: Props) {
5454
.filter(({ id }) => id !== 'saml')
5555
// Hide the entrance of adding HTTP Email connector
5656
.filter(({ id }) => id !== 'http-email')
57+
// Hide the entrance of adding Vonage SMS connector
58+
.filter(({ id }) => id !== 'vonage')
5759
);
5860

5961
return allGroups

0 commit comments

Comments
 (0)