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

feat(connector): create SMSAero connector (#4096)

* feat(connector): create SMSAero connector

* fix(connector): smsaero connector code review fixes

* chore: add changeset for sms aero

---------

Co-authored-by: wangsijie <wangsijie94@gmail.com>
This commit is contained in:
Danil Tankov 2023-07-04 11:22:13 +07:00 committed by GitHub
parent 97fc519ac1
commit 3e72b6d56d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 436 additions and 2 deletions

View file

@ -0,0 +1,5 @@
---
"@logto/connector-smsaero": minor
---
Add SMSAero connector

View file

@ -0,0 +1,64 @@
# SMSAero short message service connector
The official Logto connector for SMSAero short message service.
**Table of contents**
- [SMSAero short message service connector](#smsaero-short-message-service-connector)
- [Register account](#register-account)
- [Set up senders' phone numbers](#set-up-senders-phone-numbers)
- [Get account credentials](#get-account-credentials)
- [Compose the connector JSON](#compose-the-connector-json)
- [Test SMSAero connector](#test-smsaero-connector)
- [Config types](#config-types)
- [Reference](#reference)
## Register account
Create a new account on [SMSAero](https://smsaero.ru/). (Jump to the next step if you already have one.)
## Get account credentials
We will need the API credentials to make the connector work. Let's begin from
the [API and SMPP](https://smsaero.ru/cabinet/settings/apikey/).
Copy "API-key" or generate new one.
## Compose the connector JSON
Fill out the _email_, _apiKey_ and _senderName_ fields with your email, API key and sender name.
You can fill sender name with "SMSAero" to use default sender name provided by SMSAero.
You can add multiple SMS connector templates for different cases. Here is an example of adding a single template:
- Fill out the `content` field with arbitrary string-typed contents. Do not forget to leave `{{code}}` placeholder for
random verification code.
- Fill out the `usageType` field with either `Register`, `SignIn`, `ForgotPassword`, `Generic` for different use cases.
In order to enable full user flows, templates with usageType `Register`, `SignIn`, `ForgotPassword` and `Generic` are
required.
### Test SMSAero connector
You can enter a phone number and click on "Send" to see whether the settings can work before "Save and Done".
That's it. Don't forget
to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in).
### Config types
| Name | Type |
|------------|-------------|
| email | string |
| apiKey | string |
| senderName | string |
| templates | Templates[] |
| Template Properties | Type | Enum values |
|---------------------|-------------|---------------------------------------------------------|
| content | string | N/A |
| usageType | enum string | 'Register' \| 'SignIn' \| 'ForgotPassword' \| 'Generic' |
## Reference
- [SMSAero API Documentation](https://smsaero.ru/integration/documentation/api/)

View file

@ -0,0 +1,8 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="body_1" width="192" height="192">
<g transform="matrix(1.3333334 0 0 1.3333334 0 0)">
<image x="0" y="0" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAYAAABS3GwHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAvbSURBVHhe7Z3RseQ0E4U3hA2BEAhhQyAEyIBX3shgQ9gQCIEQNgRCoIoE4JyLvJi5mukZu2VL8vdVddX/Lx5b6m51S3LL9wMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADA0Pz10y+fJL9J/r4R/9uPko/lUoC5kHP/Wpz9kfwp+SL5rvwMYHyKU9cc/pH8Lvmx3AJgTOTEP6+ceov8IXH2ICvAWMhpP0o8rak59hbxWuFTuT1A38hZvbCtOfJecVZwZmHRDP0iB90y939V/IzvyyMB+kGO6YVszWlbyFcJW6nQD3LGaAB4GvO95LMka63AVir0gZww2vv/uVz6hv6/I3hm1mArFc7Dzrdyxpr8Wi79H/r37yTZWcH3IyvAcdjhigPek9/KpVX0372N6kHk+X3t91vEW6k/lEcAtEXO5i3LmiO+SbksRNd6reC5fVZW4AUbtEcOFm2FvhSNdb2zghfPDwfWi0JWgDbYsVaOVpPP5dKX0W9dXZr5roEXbJCLnak41z35Wi7djO7hZ3g6k5kVPLAou4D9yJGiRWzaPFz3csapnTnYKmQF2Iecx1uQNedaJH2vXvf0DpSzQtai2UJWgNeR00TrgIfboXvR/bNfsDkrUHYBz7Nynpr8WS5rip7T4gWbswLFePAYOUkUgQ9zIj2rxQs2ivHgPnKMqC6oWhbRGj13ecFWa9MWISvAe+wQxUHuye7t0D3o+S1esJEV4D/kCNHcuwtHUTuyX7CRFeDNsaL9+a7KEdSeZSuVrAD7kdE9xag5xSJfyqXdobZlv2AjK1wNG7sY/578US7tFrUxeyvVQla4CjJy5DjDlCerrXbazBdsZIXZKQauGX+R/x2THAG12VnB/SIrwGOKUWsGX6RpWURL1PYWL9jICjMhQzpa1gz9TcqlQ6N+ZJ9gs5AVZkAGjLYVp6m4VF9aZQUO+Y+KDBetAzafEusZ9atFVuDTL6Mhg0Xl0aeWRbRG/WtRdkFWGAUZyQ5QM+JaLjHPVT+zyy4sHPLvHRkomhNfKq2rvy2ygu/Fp196REaJjkl2WxbRGvW9VVbgOGcvyBjROqD7sojWSAetsoLvyVbq2ayMck94+VOQLrKL8SzOMmSFs5Dyozqa4coiWiOdtCjRJiucgRQeHZMctiziCKQfSrRHRor2Yq9miG9SLoUHSE8c3BmVlcLvCXPUF5C+7LSUaI+CFBul8CnLIlojvTU7uFMeARlIoV581ZS9yNRlEUcgHbbICpRdZCAlRsckLcxDE5AeW2QFivH2IgVGBqGuJRnplKzQC1JatA64bFlEa6RbssLZWFkr5dXk8mURRyA9kxXOwAoqCnskKPEgpOsWB3ec5ZnK3kPKiV7ikFIPRjpvcZzTdqZE+xYpJCr/pSziRKR/skJLpIhoHXDIH9GAx8gOZIUWuPNFGY+E1/EdYXtIyApZqONRVKE8ukNklyUrZBbjXS8rqLPeMqspYxHWAZ0jG3HIfyvu5KrTNWEdMAiyFYf8X0Uds9JqHV8L64DBkM3ICs+iTkXrgFP+mB7sR7YjK0SoI9E64PdyKQyM7EhWqKEOcEzyQsie3v52BCcrLKw6ck94ezghtqvEUbxm860yXlZQg6OKRKZBEyP7XjsrlIbWOrGWpUMWT5uu88LkQsiu18sKaly4Dnggzh7uoAeGlce26QTIji0O7vSbFVaNzBJvry4Dw6/t+dzKoBT7ZR7csfSVFUqDag3NFkcBK/PbwJBwCH8AZKd5s4IaEH0upbVYqR4YVrDbwjqjY2SbubKCHvzM51LOEivaL3EcKRgYHWFbSObICnpgZieOkPU6gwX4yUj/zgqZB3csx2WF8rBaI0aTd+uM0kU4AOm7xcGd9llBNz97HdBaloGxXmewAG+EdStplRXyg5pu6vlc7YGzCxmjMdJpi6xgm+VmBN3QzlB72BWFgZGMdJidFTyg8tZ+ull2yeyMsh4YLL43Yr1JMrKC7ZEzldWNPDprD0Eei43otQVrihexziR7s0LOwS3d6KrrgCyhcnYH0t/WrJD3PVvf7ObmyGvClGgn0qGzgjNqTb9VKT/dj2720oORd8JiOQnp0gPBa60wKJef7Ec3m/19QGthACQhXXoA2B8PHQDsBO0TapV2Yh1KXqoxKj/dj262d1vqysJn5Xcg/fnt/JaSnJyvGOpGPVeF9ipOz45WRP6NSHfeBt2z+ZJTMKcbMf+/L86M1BIlId15muMF7t4ZR943bHUzG7j2kKuJ9eC1EI6eTNFn1jrTdsqzzc3NZ5d1RHcksmGYxjRAen16N+dJse1yP9+vG9oBag8bXZi6nETRdeauogeQbZhvP93UUbD20FEER+8A67zoPyvaW9qfDNMDsg8utBIr1o7uAftWpizB0U+m2CEz2jugHbO7pod41NYacaa8c/TSXOgE2aRFtHcgPvZ9ih7ouvZaY44QHH0wZKOlarNmzy3iaO/7nVNIqAc71dQalike2Z7L2dE5SDIYspej/d66/Vt5i/aSc6ewpSG1Bm6R24iOow+M7SdxdHaUrtn7VTk32t+ihmyd/zN1mRTZskW0t6/0VyulRj0z/19G7dv2YvkpTIZsm/2lNwdJB8h+XzCqcdH831GAbcaJkX0d7R2ha/bfIg6WYwRKNTTawurnM9aQhuzaItq3eUvbCjU2OgTPH8ueDNnUU95rRvtb1HCnvlqnFskrNYXTkB2X0uMo2z8rnhaPFe1rqAMevbUOLpJbcQeHIvs52md99Liv7csM1JkoIrCHPxiymbcws6O9ZwpzbYSoQ8z/J0L2yi5Gmyva36LOMf8fHNkouxhtvJ2craiTUbTgCwedItu0iPbXesGpDkdRg+OBHSF7EO2zUKej+X/ex0ZhF7JFdjHa9aL9LVKAR35NOYt8KZfCCUj/2cVojvbeGbpetK8hRUR7w8z/T0B6z4727c/SjoiUEimY+f9BSNfZ0d627bsC80ykmOjzh1/LpdAQ6Tk72vdZb98bUlI0//9cLoVkpNsW0d6DiGj/LFJWVAnInDEZ6dS7bpnR3ova+coTjmClxHuCUpOQLu2klB73gpW3UmZNmP/vRDp0tL/WscJRkBKj44/M/zci3WVHexa12Uih0eKL9PoC0ld2tPd9fD+ifTZSqncgakr/JuVSCJCusqP9nDX3PSHlRp8/4Y88P0D6aRHtvajl0NERSNHR/D/nz81PhvSSHe3ZwjwDKZz5/5NIF9nR3sIW5llI8cz/n0B6aBHtr1lz3xMyAPP/O6jvRPvZkTGY/9+gPreI9ryw6hEZJZr/X2InQv1sEe2pue8ZGSea/0//+RP1kWh/VWSkaP4/5edP1C+iPbw5ghdkNWMuMtXnD9Wf7GjvAeSBRLQfERnO6bpm2EWGn/+rDy2ivQcRxWgjIwPaMWrGXWTo+b/an/nxVwvRfiZkSE8HaoZeZLj5v9rsQe0FaJTZXhGi/YzIqNPM/9VWoj28how79Pxf7SPawzZk5Gj+3239j9qW/fFXov3VkLGjz590Vf+j9viFnducGe05aHJVZPRovtxF/Y/a0SLa+34cNLkyxRFqDrLIadWKenb2B6IsRHv4FzlB9PnDU+b/em725wCJ9vAeOURX8389L7s8gWgP95FjnD7/1zNalCcQ7SHmxmlq0mz+r3tnR3vvCnGsEJ5DjhJ9/tCS6ky6X6toz7FCeA05jd+c1hxqkbTvf+peLk8g2kM/yHkih9w1/9fvW5QneM1CtIf93DhWTTY5mn6XXYzmAcSxQshDzhTO/8ulT6HrW0V7jhVCPnKsaP7/1P6/rqP0GMZDDrZ5/q//1qIYze2h9BiO4cb5avJu/u9/k3jLsXb9FlnKE4j2cBxyuKfn//rflB7DXMjxwv1/ia/J3LenGA36QE4YDYBMIdpDX8gZWw8Aoj30ixwzKoHeKkR76B85aHgI5gUh2sN4yGH3LnCJ9jAuclyXLjh615z7nvh6v6Ul2sP42JEljuQ1Z18Lb2lhXuTcXhTfDoRlbs9bWgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAc/jw4R9oEwZlIHea6AAAAABJRU5ErkJggg==" preserveAspectRatio="none" width="144" height="144"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,51 @@
{
"name": "@logto/connector-smsaero",
"version": "1.0.0",
"description": "SMSAero connector implementation.",
"author": "Danil Tankov <danil.tankoff@yandex.ru>",
"dependencies": {
"@logto/connector-kit": "workspace:^1.1.0"
},
"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",
"build:test": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap",
"build": "rm -rf lib/ && tsc -p tsconfig.build.json --noEmit && rollup -c",
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
"lint": "eslint --ext .ts src",
"lint:report": "pnpm lint --format json --output-file report.json",
"test:only": "NODE_OPTIONS=--experimental-vm-modules jest",
"test": "pnpm build:test && pnpm test:only",
"test:ci": "pnpm test:only --silent --coverage",
"prepublishOnly": "pnpm build"
},
"engines": {
"node": "^18.12.0"
},
"eslintConfig": {
"extends": "@silverhand",
"settings": {
"import/core-modules": [
"@silverhand/essentials",
"got",
"nock",
"snakecase-keys",
"zod"
]
}
},
"prettier": "@silverhand/eslint-config/.prettierrc",
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,70 @@
import type { ConnectorMetadata } from '@logto/connector-kit';
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
export const endpoint = `https://gate.smsaero.ru/v2/sms/send`;
export const defaultMetadata: ConnectorMetadata = {
id: 'smsaero-short-message-service',
target: 'smsaero-sms',
platform: null,
name: {
en: 'SMS Aero service',
},
logo: './logo.svg',
logoDark: null,
description: {
en: 'SMS Aero offers users to use SMS-mailing in 5 minutes without viewing the contract. Developers are offered a convenient API with accessible classes and 24x7 chat support.',
},
readme: './README.md',
formItems: [
{
key: 'email',
label: 'Email',
type: ConnectorConfigFormItemType.Text,
required: true,
placeholder: '<account-email>',
},
{
key: 'apiKey',
label: 'API Key',
type: ConnectorConfigFormItemType.Text,
required: true,
placeholder: '<api-key>',
},
{
key: 'senderName',
label: 'Sender Name',
type: ConnectorConfigFormItemType.Text,
required: true,
placeholder: 'SMSAero',
},
{
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,12 @@
import createConnector from './index.js';
import { mockedConfig } from './mock.js';
const { jest } = import.meta;
const getConfig = jest.fn().mockResolvedValue(mockedConfig);
describe('SMSAero SMS connector', () => {
it('init without throwing errors', async () => {
await expect(createConnector({ getConfig })).resolves.not.toThrow();
});
});

View file

@ -0,0 +1,85 @@
import { assert } from '@silverhand/essentials';
import { got, HTTPError } from 'got';
import type {
CreateConnector,
GetConnectorConfig,
SendMessageFunction,
SmsConnector,
} from '@logto/connector-kit';
import {
ConnectorError,
ConnectorErrorCodes,
ConnectorType,
validateConfig,
} from '@logto/connector-kit';
import { defaultMetadata, endpoint } from './constant.js';
import type { PublicParameters, SmsAeroConfig } from './types.js';
import { smsAeroConfigGuard } from './types.js';
function sendMessage(getConfig: GetConnectorConfig): SendMessageFunction {
return async (data, inputConfig) => {
const { to, type, payload } = data;
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
validateConfig<SmsAeroConfig>(config, smsAeroConfigGuard);
const { email, apiKey, senderName, 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 = {
number: to,
sign: senderName,
text: template.content.replace(/{{code}}/g, payload.code),
};
const auth = Buffer.from(`${email}:${apiKey}`).toString('base64');
try {
return await got.post(endpoint, {
headers: {
Authorization: `Basic ${auth}`,
},
json: parameters,
});
} 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 createSmsAeroConnector: CreateConnector<SmsConnector> = async ({ getConfig }) => {
return {
metadata: defaultMetadata,
type: ConnectorType.Sms,
configGuard: smsAeroConfigGuard,
sendMessage: sendMessage(getConfig),
};
};
export default createSmsAeroConnector;

View file

@ -0,0 +1,17 @@
import type { SmsAeroConfig } from './types.js';
const mockedEmail = 'test@test.com';
const mockedApiKey = 'api-key';
const mockedSenderName = 'sender-name';
export const mockedConfig: SmsAeroConfig = {
email: mockedEmail,
apiKey: mockedApiKey,
senderName: mockedSenderName,
templates: [
{
usageType: 'Test',
content: 'This is for testing purposes only. Your verification code is {{code}}.',
},
],
};

View file

@ -0,0 +1,43 @@
import { z } from 'zod';
/**
* @doc https://smsaero.ru/integration/documentation/api/
*/
export type PublicParameters = {
number: string;
sign: string;
text: string;
};
/**
* UsageType here is used to specify the use case of the template, can be either
* 'Register', 'SignIn', 'ForgotPassword', 'Generic' or 'Test'.
*/
const requiredTemplateUsageTypes = ['Register', 'SignIn', 'ForgotPassword', 'Generic'];
const templateGuard = z.object({
usageType: z.string(),
content: z.string(),
});
export const smsAeroConfigGuard = z.object({
email: z.string().email(),
apiKey: z.string(),
senderName: 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 SmsAeroConfig = z.infer<typeof smsAeroConfigGuard>;

View file

@ -2263,6 +2263,85 @@ importers:
specifier: ^5.0.0
version: 5.0.2
packages/connectors/connector-smsaero:
dependencies:
'@logto/connector-kit':
specifier: workspace:^1.1.0
version: link:../../toolkit/connector-kit
'@silverhand/essentials':
specifier: ^2.5.0
version: 2.5.0
got:
specifier: ^13.0.0
version: 13.0.0
snakecase-keys:
specifier: ^5.4.4
version: 5.4.4
zod:
specifier: ^3.20.2
version: 3.20.2
devDependencies:
'@jest/types':
specifier: ^29.5.0
version: 29.5.0
'@rollup/plugin-commonjs':
specifier: ^25.0.0
version: 25.0.0(rollup@3.8.0)
'@rollup/plugin-json':
specifier: ^6.0.0
version: 6.0.0(rollup@3.8.0)
'@rollup/plugin-node-resolve':
specifier: ^15.0.1
version: 15.0.1(rollup@3.8.0)
'@rollup/plugin-typescript':
specifier: ^11.0.0
version: 11.0.0(rollup@3.8.0)(typescript@5.0.2)
'@silverhand/eslint-config':
specifier: 3.0.1
version: 3.0.1(eslint@8.34.0)(prettier@2.8.4)(typescript@5.0.2)
'@silverhand/ts-config':
specifier: 3.0.0
version: 3.0.0(typescript@5.0.2)
'@types/jest':
specifier: ^29.4.0
version: 29.4.0
'@types/node':
specifier: ^18.11.18
version: 18.11.18
'@types/supertest':
specifier: ^2.0.11
version: 2.0.11
eslint:
specifier: ^8.34.0
version: 8.34.0
jest:
specifier: ^29.5.0
version: 29.5.0(@types/node@18.11.18)
jest-matcher-specific-error:
specifier: ^1.0.0
version: 1.0.0
lint-staged:
specifier: ^13.0.0
version: 13.0.0
nock:
specifier: ^13.2.2
version: 13.2.2
prettier:
specifier: ^2.8.2
version: 2.8.4
rollup:
specifier: ^3.8.0
version: 3.8.0
rollup-plugin-summary:
specifier: ^2.0.0
version: 2.0.0(rollup@3.8.0)
supertest:
specifier: ^6.2.2
version: 6.2.2
typescript:
specifier: ^5.0.0
version: 5.0.2
packages/connectors/connector-smtp:
dependencies:
'@logto/connector-kit':
@ -10745,7 +10824,7 @@ packages:
dev: false
/concat-map@0.0.1:
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
/concat-stream@2.0.0:
@ -11328,7 +11407,7 @@ packages:
dev: true
/dezalgo@1.0.3:
resolution: {integrity: sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=}
resolution: {integrity: sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==}
dependencies:
asap: 2.0.6
wrappy: 1.0.2