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

feat: sendPasscode (#200)

* fix: consume

* feat: sendPasscode
This commit is contained in:
Wang Sijie 2022-01-28 15:39:38 +08:00 committed by GitHub
parent 0105d19d99
commit 02491adb5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 111 additions and 6 deletions

View file

@ -3,7 +3,7 @@ import { findConnectorById, hasConnector, insertConnector } from '@/queries/conn
import * as AliyunDM from './aliyun-dm';
import * as GitHub from './github';
import { ConnectorInstance } from './types';
import { ConnectorInstance, ConnectorType } from './types';
const allConnectors: ConnectorInstance[] = [AliyunDM, GitHub];
@ -33,6 +33,21 @@ export const getConnectorInstanceById = async (id: string): Promise<ConnectorIns
return { connector, ...found };
};
export const getConnectorInstanceByType = async <T extends ConnectorInstance>(
type: ConnectorType
): Promise<T> => {
const connectors = await getConnectorInstances();
const connector = connectors
.filter((connector) => connector.connector?.enabled)
.find<T>((connector): connector is T => connector.metadata.type === type);
if (!connector) {
throw new RequestError('connector.not_found', { type });
}
return connector;
};
export const initConnectors = async () => {
await Promise.all(
allConnectors.map(async ({ metadata: { id } }) => {

View file

@ -15,7 +15,7 @@ export interface ConnectorMetadata {
}
// The name `Connector` is used for database, use `ConnectorInstance` to avoid confusing.
export type ConnectorInstance = EmailConector | SocialConector;
export type ConnectorInstance = SmsConnector | EmailConector | SocialConector;
export interface BaseConnector {
connector?: Connector;
@ -23,6 +23,10 @@ export interface BaseConnector {
validateConfig: ValidateConfig;
}
export interface SmsConnector extends BaseConnector {
sendMessage: SmsSendMessageFunction;
}
export interface EmailConector extends BaseConnector {
sendMessage: EmailSendMessageFunction;
}
@ -46,12 +50,20 @@ export interface EmailMessageTypes {
Test: Record<string, unknown>;
}
type SmsMessageTypes = EmailMessageTypes;
export type EmailSendMessageFunction<T = unknown> = (
address: string,
type: keyof EmailMessageTypes,
payload: EmailMessageTypes[typeof type]
) => Promise<T>;
export type SmsSendMessageFunction<T = unknown> = (
address: string,
type: keyof SmsMessageTypes,
payload: SmsMessageTypes[typeof type]
) => Promise<T>;
export class ConnectorError extends Error {}
export class ConnectorConfigError extends ConnectorError {}

View file

@ -1,14 +1,17 @@
import { PasscodeType } from '@logto/schemas';
import { Passcode, PasscodeType } from '@logto/schemas';
import { getConnectorInstanceByType } from '@/connectors';
import { ConnectorType } from '@/connectors/types';
import {
deletePasscodesByIds,
findUnconsumedPasscodesBySessionIdAndType,
insertPasscode,
} from '@/queries/passcode';
import { createPasscode, passcodeLength } from './passcode';
import { createPasscode, passcodeLength, sendPasscode } from './passcode';
jest.mock('@/queries/passcode');
jest.mock('@/connectors');
const mockedFindUnconsumedPasscodesBySessionIdAndType =
findUnconsumedPasscodesBySessionIdAndType as jest.MockedFunction<
@ -18,6 +21,9 @@ const mockedDeletePasscodesByIds = deletePasscodesByIds as jest.MockedFunction<
typeof deletePasscodesByIds
>;
const mockedInsertPasscode = insertPasscode as jest.MockedFunction<typeof insertPasscode>;
const mockedGetConnectorInstanceByType = getConnectorInstanceByType as jest.MockedFunction<
typeof getConnectorInstanceByType
>;
beforeAll(() => {
mockedFindUnconsumedPasscodesBySessionIdAndType.mockResolvedValue([]);
@ -35,6 +41,7 @@ afterEach(() => {
mockedFindUnconsumedPasscodesBySessionIdAndType.mockClear();
mockedDeletePasscodesByIds.mockClear();
mockedInsertPasscode.mockClear();
mockedGetConnectorInstanceByType.mockClear();
});
describe('createPasscode', () => {
@ -78,3 +85,50 @@ describe('createPasscode', () => {
expect(mockedDeletePasscodesByIds).toHaveBeenCalledWith(['id']);
});
});
describe('sendPasscode', () => {
it('should throw error when email and phone are both empty', async () => {
const passcode: Passcode = {
id: 'id',
sessionId: 'sessionId',
phone: null,
email: null,
type: PasscodeType.SignIn,
code: '1234',
consumed: false,
tryCount: 0,
createdAt: Date.now(),
};
await expect(sendPasscode(passcode)).rejects.toThrowError('Both email and phone are empty.');
});
it('should call sendPasscode with params matching', async () => {
const sendMessage = jest.fn();
mockedGetConnectorInstanceByType.mockResolvedValue({
metadata: {
id: 'id',
type: ConnectorType.SMS,
name: {},
logo: '',
description: {},
},
sendMessage,
validateConfig: jest.fn(),
});
const passcode: Passcode = {
id: 'id',
sessionId: 'sessionId',
phone: 'phone',
email: null,
type: PasscodeType.SignIn,
code: '1234',
consumed: false,
tryCount: 0,
createdAt: Date.now(),
};
await sendPasscode(passcode);
expect(sendMessage).toHaveBeenCalledWith(passcode.phone, passcode.type, {
code: passcode.code,
});
});
});

View file

@ -1,6 +1,8 @@
import { PasscodeType } from '@logto/schemas';
import { Passcode, PasscodeType } from '@logto/schemas';
import { customAlphabet, nanoid } from 'nanoid';
import { getConnectorInstanceByType } from '@/connectors';
import { ConnectorType, EmailConector, SmsConnector } from '@/connectors/types';
import {
deletePasscodesByIds,
findUnconsumedPasscodesBySessionIdAndType,
@ -30,3 +32,19 @@ export const createPasscode = async (
...payload,
});
};
export const sendPasscode = async (passcode: Passcode) => {
const emailOrPhone = passcode.email ?? passcode.phone;
if (!emailOrPhone) {
throw new Error('Both email and phone are empty.');
}
const connector = passcode.email
? await getConnectorInstanceByType<EmailConector>(ConnectorType.Email)
: await getConnectorInstanceByType<SmsConnector>(ConnectorType.SMS);
return connector.sendMessage(emailOrPhone, passcode.type, {
code: passcode.code,
});
};

View file

@ -52,7 +52,7 @@ export const deletePasscodesByIds = async (ids: string[]) => {
where id in (${ids.join(',')})
`);
if (rowCount < 1) {
if (rowCount !== ids.length) {
throw new DeletionError();
}
};

View file

@ -43,6 +43,9 @@ const errors = {
invalid_sign_in_method: 'Current sign-in method is not available.',
insufficient_info: 'Insufficent sign-in info.',
},
connector: {
not_found: 'Can not find any available connector for type: {{type}}.',
},
swagger: {
invalid_zod_type: 'Invalid Zod type, please check route guard config.',
},

View file

@ -45,6 +45,9 @@ const errors = {
invalid_sign_in_method: '当前登录方式不可用。',
insufficient_info: '登录信息缺失,请检查您的输入。',
},
connector: {
not_found: '找不到可用的 {{type}} 类型的连接器.',
},
swagger: {
invalid_zod_type: '无效的 Zod 类型,请检查路由 guard 配置。',
},