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:
parent
bc2a0ac039
commit
61aa13f8a9
9 changed files with 380 additions and 3 deletions
32
packages/connectors/connector-gatewayapi-sms/README.md
Normal file
32
packages/connectors/connector-gatewayapi-sms/README.md
Normal 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.
|
1
packages/connectors/connector-gatewayapi-sms/logo.svg
Normal file
1
packages/connectors/connector-gatewayapi-sms/logo.svg
Normal 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 |
69
packages/connectors/connector-gatewayapi-sms/package.json
Normal file
69
packages/connectors/connector-gatewayapi-sms/package.json
Normal 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"
|
||||
}
|
||||
}
|
70
packages/connectors/connector-gatewayapi-sms/src/constant.ts
Normal file
70
packages/connectors/connector-gatewayapi-sms/src/constant.ts
Normal 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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
81
packages/connectors/connector-gatewayapi-sms/src/index.ts
Normal file
81
packages/connectors/connector-gatewayapi-sms/src/index.ts
Normal 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;
|
17
packages/connectors/connector-gatewayapi-sms/src/mock.ts
Normal file
17
packages/connectors/connector-gatewayapi-sms/src/mock.ts
Normal 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}}.',
|
||||
},
|
||||
],
|
||||
};
|
39
packages/connectors/connector-gatewayapi-sms/src/types.ts
Normal file
39
packages/connectors/connector-gatewayapi-sms/src/types.ts
Normal 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 }>;
|
||||
};
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue