0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(connector): add gatewayapi connector (#6691)

This commit is contained in:
wangsijie 2024-10-18 19:24:15 +08:00 committed by GitHub
parent bc2a0ac039
commit 61aa13f8a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 380 additions and 3 deletions

View file

@ -0,0 +1,32 @@
# GatewayAPI SMS connector
The official Logto connector for GatewayAPI SMS.
## Get started
GatewayAPI is a cloud service provider in Europe, offering many cloud services, including SMS (short message service). GatewayAPI SMS Connector is a plugin provided by the Logto team to call the GatewayAPI SMS service, with the help of which Logto end-users can register and sign in to their Logto account via SMS verification code.
## Set up in GatewayAPI
> 💡 **Tip**
>
> You can skip some sections if you have already finished.
### Create an GatewayAPI account
Go to the [GatewayAPI website](https://www.gatewayapi.com/) and register your GatewayAPI account if you don't have one.
### Enable account
You may need to enable your account before using the SMS service. You can contact the GatewayAPI customer service to enable your account.
### Get API token
Go to the API Keys page from the GatewayAPI console, and find the API token or create a new API token.
## Set up in Logto
1. **Endpoint**: If your GatewayAPI account is in the EU region, you should use the endpoint `https://gatewayapi.com/rest/mtsms`. If your GatewayAPI account is in the US region, you should use the endpoint `https://gatewayapi.com/rest/mtsms`.
2. **API Token**: The API token you created in the previous step.
3. **Sender**: The sender you want to use to send the SMS.
4. **Templates**: The templates you want to use to send the SMS, you can use the default templates or modify them as needed.

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 472.87"><defs><style>.cls-1{fill:#03a5ff;}.cls-2{fill:#0025bf;}.cls-3{fill:#067dff;}.cls-4{fill:#0b51ff;}</style></defs><g id="Group_224"><path id="Path_445" class="cls-2" d="M0,313.9C0,338.15,18.86,358.36,43.11,359.7H389.34L0,0V313.9Z"/><path id="Path_446" class="cls-4" d="M172.44,0H0L390.69,361.05l121.25,111.82v-158.97L172.44,0Z"/><path id="Path_447" class="cls-3" d="M172.44,0L511.94,313.9V154.93L344.88,0H172.44Z"/><path id="Path_448" class="cls-1" d="M468.83,0h-123.94l167.05,154.93V45.8c1.35-25.6-18.86-45.8-43.11-45.8Z"/></g></svg>

After

Width:  |  Height:  |  Size: 649 B

View file

@ -0,0 +1,69 @@
{
"name": "@logto/connector-gatewayapi-sms",
"version": "0.0.0",
"description": "GatewayAPI SMS connector implementation.",
"author": "Silverhand Inc. <contact@silverhand.io>",
"dependencies": {
"@logto/connector-kit": "workspace:^4.0.0",
"@silverhand/essentials": "^2.9.1",
"got": "^14.0.0",
"snakecase-keys": "^8.0.1",
"zod": "^3.23.8"
},
"main": "./lib/index.js",
"module": "./lib/index.js",
"exports": "./lib/index.js",
"license": "MPL-2.0",
"type": "module",
"files": [
"lib",
"docs",
"logo.svg",
"logo-dark.svg"
],
"scripts": {
"precommit": "lint-staged",
"check": "tsc --noEmit",
"build": "tsup",
"dev": "tsup --watch",
"lint": "eslint --ext .ts src",
"lint:report": "pnpm lint --format json --output-file report.json",
"test": "vitest src",
"test:ci": "pnpm run test --silent --coverage",
"prepublishOnly": "pnpm build"
},
"engines": {
"node": "^20.9.0"
},
"eslintConfig": {
"extends": "@silverhand",
"settings": {
"import/core-modules": [
"@silverhand/essentials",
"got",
"nock",
"snakecase-keys",
"zod"
]
}
},
"prettier": "@silverhand/eslint-config/.prettierrc",
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@silverhand/eslint-config": "6.0.1",
"@silverhand/ts-config": "6.0.0",
"@types/node": "^20.11.20",
"@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^2.0.0",
"eslint": "^8.56.0",
"lint-staged": "^15.0.2",
"nock": "^13.3.1",
"prettier": "^3.0.0",
"supertest": "^7.0.0",
"tsup": "^8.1.0",
"typescript": "^5.5.3",
"vitest": "^2.0.0"
}
}

View file

@ -0,0 +1,70 @@
import type { ConnectorMetadata } from '@logto/connector-kit';
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
export const endpoint = 'https://api.twilio.com/2010-04-01/Accounts/{{accountSID}}/Messages.json';
export const defaultMetadata: ConnectorMetadata = {
id: 'gatewayapi-sms',
target: 'gatewayapi-sms',
platform: null,
name: {
en: 'GatewayAPI SMS Service',
},
logo: './logo.svg',
logoDark: null,
description: {
en: 'GatewayAPI accelerates development by removing the learning curve and guesswork, so you can get down to building right away with our APIs.',
},
readme: './README.md',
formItems: [
{
key: 'endpoint',
label: 'Endpoint',
type: ConnectorConfigFormItemType.Text,
required: true,
placeholder: 'https://gatewayapi.com/rest/mtsms',
defaultValue: 'https://gatewayapi.com/rest/mtsms',
},
{
key: 'apiToken',
label: 'API Token',
type: ConnectorConfigFormItemType.Text,
required: true,
},
{
key: 'sender',
label: 'Sender',
type: ConnectorConfigFormItemType.Text,
required: true,
placeholder: 'ExampleSMS',
},
{
key: 'templates',
label: 'Templates',
type: ConnectorConfigFormItemType.Json,
required: true,
defaultValue: [
{
usageType: 'SignIn',
content:
'Your Logto sign-in verification code is {{code}}. The code will remain active for 10 minutes.',
},
{
usageType: 'Register',
content:
'Your Logto sign-up verification code is {{code}}. The code will remain active for 10 minutes.',
},
{
usageType: 'ForgotPassword',
content:
'Your Logto password change verification code is {{code}}. The code will remain active for 10 minutes.',
},
{
usageType: 'Generic',
content:
'Your Logto verification code is {{code}}. The code will remain active for 10 minutes.',
},
],
},
],
};

View file

@ -0,0 +1,10 @@
import createConnector from './index.js';
import { mockedConfig } from './mock.js';
const getConfig = vi.fn().mockResolvedValue(mockedConfig);
describe('GatewayAPI SMS connector', () => {
it('init without throwing errors', async () => {
await expect(createConnector({ getConfig })).resolves.not.toThrow();
});
});

View file

@ -0,0 +1,81 @@
import { assert } from '@silverhand/essentials';
import { got, HTTPError } from 'got';
import type {
GetConnectorConfig,
SendMessageFunction,
CreateConnector,
SmsConnector,
} from '@logto/connector-kit';
import {
ConnectorError,
ConnectorErrorCodes,
validateConfig,
ConnectorType,
replaceSendMessageHandlebars,
} from '@logto/connector-kit';
import { defaultMetadata } from './constant.js';
import { gatewayApiSmsConfigGuard, type GatewayApiSmsPayload } from './types.js';
const sendMessage =
(getConfig: GetConnectorConfig): SendMessageFunction =>
async (data, inputConfig) => {
const { to, type, payload } = data;
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
validateConfig(config, gatewayApiSmsConfigGuard);
const { endpoint, apiToken, sender, templates } = config;
const template = templates.find((template) => template.usageType === type);
assert(
template,
new ConnectorError(
ConnectorErrorCodes.TemplateNotFound,
`Cannot find template for type: ${type}`
)
);
const encodedAuth = Buffer.from(`${apiToken}:`).toString('base64');
const body: GatewayApiSmsPayload = {
sender,
message: replaceSendMessageHandlebars(template.content, payload),
recipients: [{ msisdn: to }],
};
try {
return await got.post(endpoint, {
headers: {
Authorization: `Basic ${encodedAuth}`,
},
json: body,
});
} catch (error: unknown) {
if (error instanceof HTTPError) {
const {
response: { body: rawBody },
} = error;
assert(
typeof rawBody === 'string',
new ConnectorError(
ConnectorErrorCodes.InvalidResponse,
`Invalid response raw body type: ${typeof rawBody}`
)
);
throw new ConnectorError(ConnectorErrorCodes.General, rawBody);
}
throw error;
}
};
const createGatewayApiSmsConnector: CreateConnector<SmsConnector> = async ({ getConfig }) => {
return {
metadata: defaultMetadata,
type: ConnectorType.Sms,
configGuard: gatewayApiSmsConfigGuard,
sendMessage: sendMessage(getConfig),
};
};
export default createGatewayApiSmsConnector;

View file

@ -0,0 +1,17 @@
import type { GatewayApiSmsConfig } from './types.js';
const mockedEndpoint = 'https://gatewayapi.com/rest/mtsms';
const mockedApiToken = 'api-token';
const mockedSender = 'sender';
export const mockedConfig: GatewayApiSmsConfig = {
endpoint: mockedEndpoint,
apiToken: mockedApiToken,
sender: mockedSender,
templates: [
{
usageType: 'Generic',
content: 'This is for testing purposes only. Your verification code is {{code}}.',
},
],
};

View file

@ -0,0 +1,39 @@
import { z } from 'zod';
/**
* UsageType here is used to specify the use case of the template, can be either
* 'Register', 'SignIn', 'ForgotPassword', 'Generic'.
*/
const requiredTemplateUsageTypes = ['Register', 'SignIn', 'ForgotPassword', 'Generic'];
const templateGuard = z.object({
usageType: z.string(),
content: z.string(),
});
export const gatewayApiSmsConfigGuard = z.object({
endpoint: z.string(),
apiToken: z.string(),
sender: z.string(),
templates: z.array(templateGuard).refine(
(templates) =>
requiredTemplateUsageTypes.every((requiredType) =>
templates.map((template) => template.usageType).includes(requiredType)
),
(templates) => ({
message: `Template with UsageType (${requiredTemplateUsageTypes
.filter(
(requiredType) => !templates.map((template) => template.usageType).includes(requiredType)
)
.join(', ')}) should be provided!`,
})
),
});
export type GatewayApiSmsConfig = z.infer<typeof gatewayApiSmsConfigGuard>;
export type GatewayApiSmsPayload = {
sender: string;
message: string;
recipients: Array<{ msisdn: string }>;
};

View file

@ -933,6 +933,64 @@ importers:
specifier: ^2.0.0
version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8)
packages/connectors/connector-gatewayapi-sms:
dependencies:
'@logto/connector-kit':
specifier: workspace:^4.0.0
version: link:../../toolkit/connector-kit
'@silverhand/essentials':
specifier: ^2.9.1
version: 2.9.1
got:
specifier: ^14.0.0
version: 14.0.0
snakecase-keys:
specifier: ^8.0.1
version: 8.0.1
zod:
specifier: ^3.23.8
version: 3.23.8
devDependencies:
'@silverhand/eslint-config':
specifier: 6.0.1
version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.5.3)
'@silverhand/ts-config':
specifier: 6.0.0
version: 6.0.0(typescript@5.5.3)
'@types/node':
specifier: ^20.11.20
version: 20.12.7
'@types/supertest':
specifier: ^6.0.2
version: 6.0.2
'@vitest/coverage-v8':
specifier: ^2.0.0
version: 2.0.0(vitest@2.0.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8))
eslint:
specifier: ^8.56.0
version: 8.57.0
lint-staged:
specifier: ^15.0.2
version: 15.0.2
nock:
specifier: ^13.3.1
version: 13.3.1
prettier:
specifier: ^3.0.0
version: 3.0.0
supertest:
specifier: ^7.0.0
version: 7.0.0
tsup:
specifier: ^8.1.0
version: 8.1.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.5.3))(typescript@5.5.3)
typescript:
specifier: ^5.5.3
version: 5.5.3
vitest:
specifier: ^2.0.0
version: 2.0.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8)
packages/connectors/connector-github:
dependencies:
'@logto/connector-kit':
@ -18809,7 +18867,7 @@ snapshots:
debug: 4.3.5
enhanced-resolve: 5.16.0
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.3
@ -18821,7 +18879,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7(supports-color@5.5.0)
optionalDependencies:
@ -18860,7 +18918,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3