mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
feat(core): update connector db schema (#732)
* feat(core): update connector db schema * feat(core): eliminate code redundancy for UTs * feat(core): delete insertConnector on conflict logic and fix UTs * feat(core): fix UI according to new connector type implementation * feat(core): removed unused getConnectorByTargetAndPlatform methods * feat(core): deprecate the function that updateConnector by giving target and platform * feat(core): keep SSOT on combination of connector and corresponding metadata * feat(core): rename index name in db
This commit is contained in:
parent
bafd09474c
commit
8e1533a702
45 changed files with 1306 additions and 1782 deletions
|
@ -1,6 +1,6 @@
|
|||
import path from 'path';
|
||||
|
||||
import { ConnectorType, ConnectorMetadata } from '@logto/connector-types';
|
||||
import { ConnectorType, ConnectorMetadata, ConnectorPlatform } from '@logto/connector-types';
|
||||
import { getFileContents } from '@logto/shared';
|
||||
|
||||
export const authorizationEndpoint = 'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm';
|
||||
|
@ -23,8 +23,9 @@ const readmeContentFallback = 'Please check README.md file directory.';
|
|||
const configTemplateFallback = 'Please check config-template.md file directory.';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'alipay',
|
||||
target: 'alipay',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Web,
|
||||
name: {
|
||||
en: 'Sign In with Alipay',
|
||||
'zh-CN': '支付宝登录',
|
||||
|
|
|
@ -56,7 +56,7 @@ export class AlipayConnector implements SocialConnector {
|
|||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (redirectUri, state) => {
|
||||
const { appId: app_id } = await this.getConfig(this.metadata.id);
|
||||
const { appId: app_id } = await this.getConfig(this.metadata.target, this.metadata.platform);
|
||||
|
||||
const redirect_uri = encodeURI(redirectUri);
|
||||
|
||||
|
@ -71,7 +71,7 @@ export class AlipayConnector implements SocialConnector {
|
|||
};
|
||||
|
||||
public getAccessToken: GetAccessToken = async (code): Promise<AccessTokenObject> => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
const config = await this.getConfig(this.metadata.target, this.metadata.platform);
|
||||
const initSearchParameters = {
|
||||
method: methodForAccessToken,
|
||||
format: 'JSON',
|
||||
|
@ -104,7 +104,7 @@ export class AlipayConnector implements SocialConnector {
|
|||
};
|
||||
|
||||
public getUserInfo: GetUserInfo = async (accessTokenObject) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
const config = await this.getConfig(this.metadata.target, this.metadata.platform);
|
||||
const { accessToken } = accessTokenObject;
|
||||
assert(
|
||||
accessToken && config,
|
||||
|
|
|
@ -36,8 +36,9 @@ const readmeContentFallback = 'Please check README.md file directory.';
|
|||
const configTemplateFallback = 'Please check config-template.md file directory.';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'aliyun-dm',
|
||||
target: 'aliyun-dm',
|
||||
type: ConnectorType.Email,
|
||||
platform: null,
|
||||
name: {
|
||||
en: 'Aliyun Direct Mail',
|
||||
'zh-CN': '阿里云邮件推送',
|
||||
|
|
|
@ -57,7 +57,7 @@ export class AliyunDmConnector implements EmailConnector {
|
|||
type,
|
||||
data
|
||||
) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
const config = await this.getConfig(this.metadata.target, this.metadata.platform);
|
||||
await this.validateConfig(config);
|
||||
const { accessKeyId, accessKeySecret, accountName, fromAlias, templates } = config;
|
||||
const template = templates.find((template) => template.usageType === type);
|
||||
|
|
|
@ -33,8 +33,9 @@ const readmeContentFallback = 'Please check README.md file directory.';
|
|||
const configTemplateFallback = 'Please check config-template.md file directory.';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'aliyun-sms',
|
||||
target: 'aliyun-sms',
|
||||
type: ConnectorType.SMS,
|
||||
platform: null,
|
||||
name: {
|
||||
en: 'Aliyun Short Message Service',
|
||||
'zh-CN': '阿里云短信服务',
|
||||
|
|
|
@ -77,7 +77,7 @@ export class AliyunSmsConnector implements SmsConnector {
|
|||
type,
|
||||
{ code }
|
||||
) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
const config = await this.getConfig(this.metadata.target, this.metadata.platform);
|
||||
await this.validateConfig(config);
|
||||
const { accessKeyId, accessKeySecret, signName, templates } = config;
|
||||
const template = templates.find(({ usageType }) => usageType === type);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import path from 'path';
|
||||
|
||||
import { ConnectorMetadata, ConnectorType } from '@logto/connector-types';
|
||||
import { ConnectorMetadata, ConnectorType, ConnectorPlatform } from '@logto/connector-types';
|
||||
import { getFileContents } from '@logto/shared';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -33,8 +33,9 @@ const readmeContentFallback = 'Please check README.md file directory.';
|
|||
const configTemplateFallback = 'Please check config-template.md file directory.';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'facebook',
|
||||
target: 'facebook',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Web,
|
||||
name: {
|
||||
en: 'Sign In with Facebook',
|
||||
'zh-CN': 'Facebook 登录',
|
||||
|
|
|
@ -46,7 +46,7 @@ export class FacebookConnector implements SocialConnector {
|
|||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (redirectUri, state) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
const config = await this.getConfig(this.metadata.target, this.metadata.platform);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
|
@ -67,7 +67,8 @@ export class FacebookConnector implements SocialConnector {
|
|||
};
|
||||
|
||||
const { clientId: client_id, clientSecret: client_secret } = await this.getConfig(
|
||||
this.metadata.id
|
||||
this.metadata.target,
|
||||
this.metadata.platform
|
||||
);
|
||||
|
||||
const { access_token: accessToken } = await got
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import path from 'path';
|
||||
|
||||
import { ConnectorMetadata, ConnectorType } from '@logto/connector-types';
|
||||
import { ConnectorMetadata, ConnectorType, ConnectorPlatform } from '@logto/connector-types';
|
||||
import { getFileContents } from '@logto/shared';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -24,8 +24,9 @@ const readmeContentFallback = 'Please check README.md file directory.';
|
|||
const configTemplateFallback = 'Please check config-template.md file directory.';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'github',
|
||||
target: 'github',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Web,
|
||||
name: {
|
||||
en: 'Sign In with GitHub',
|
||||
'zh-CN': 'GitHub登录',
|
||||
|
|
|
@ -41,7 +41,7 @@ export class GithubConnector implements SocialConnector {
|
|||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (redirectUri, state) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
const config = await this.getConfig(this.metadata.target, this.metadata.platform);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
|
@ -61,7 +61,8 @@ export class GithubConnector implements SocialConnector {
|
|||
};
|
||||
|
||||
const { clientId: client_id, clientSecret: client_secret } = await this.getConfig(
|
||||
this.metadata.id
|
||||
this.metadata.target,
|
||||
this.metadata.platform
|
||||
);
|
||||
|
||||
const { access_token: accessToken } = await got
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import path from 'path';
|
||||
|
||||
import { ConnectorMetadata, ConnectorType } from '@logto/connector-types';
|
||||
import { ConnectorMetadata, ConnectorType, ConnectorPlatform } from '@logto/connector-types';
|
||||
import { getFileContents } from '@logto/shared';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -24,8 +24,9 @@ const readmeContentFallback = 'Please check README.md file directory.';
|
|||
const configTemplateFallback = 'Please check config-template.md file directory.';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'google',
|
||||
target: 'google',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Web,
|
||||
name: {
|
||||
en: 'Sign In with Google',
|
||||
'zh-CN': 'Google登录',
|
||||
|
|
|
@ -45,7 +45,7 @@ export class GoogleConnector implements SocialConnector {
|
|||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (redirectUri, state) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
const config = await this.getConfig(this.metadata.target, this.metadata.platform);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
|
@ -65,7 +65,10 @@ export class GoogleConnector implements SocialConnector {
|
|||
token_type: string;
|
||||
};
|
||||
|
||||
const { clientId, clientSecret } = await this.getConfig(this.metadata.id);
|
||||
const { clientId, clientSecret } = await this.getConfig(
|
||||
this.metadata.target,
|
||||
this.metadata.platform
|
||||
);
|
||||
|
||||
// Note:Need to decodeURIComponent on code
|
||||
// https://stackoverflow.com/questions/51058256/google-api-node-js-invalid-grant-malformed-auth-code
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { Languages } from '@logto/phrases';
|
||||
import { Language } from '@logto/phrases';
|
||||
import { Nullable } from '@silverhand/essentials';
|
||||
|
||||
export enum ConnectorType {
|
||||
Email = 'Email',
|
||||
|
@ -6,12 +7,19 @@ export enum ConnectorType {
|
|||
Social = 'Social',
|
||||
}
|
||||
|
||||
export enum ConnectorPlatform {
|
||||
Native = 'Native',
|
||||
Universal = 'Universal',
|
||||
Web = 'Web',
|
||||
}
|
||||
|
||||
export interface ConnectorMetadata {
|
||||
id: string;
|
||||
target: string;
|
||||
type: ConnectorType;
|
||||
name: Record<Languages, string>;
|
||||
platform: Nullable<ConnectorPlatform>;
|
||||
name: Record<Language, string>;
|
||||
logo: string;
|
||||
description: Record<Languages, string>;
|
||||
description: Record<Language, string>;
|
||||
readme: string;
|
||||
configTemplate: string;
|
||||
}
|
||||
|
@ -94,4 +102,7 @@ export type GetUserInfo = (
|
|||
accessTokenObject: AccessTokenObject
|
||||
) => Promise<{ id: string } & Record<string, string | undefined>>;
|
||||
|
||||
export type GetConnectorConfig<T = Record<string, unknown>> = (id: string) => Promise<T>;
|
||||
export type GetConnectorConfig<T = Record<string, unknown>> = (
|
||||
target: string,
|
||||
platform: Nullable<ConnectorPlatform>
|
||||
) => Promise<T>;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import path from 'path';
|
||||
|
||||
import { ConnectorMetadata, ConnectorType } from '@logto/connector-types';
|
||||
import { ConnectorMetadata, ConnectorType, ConnectorPlatform } from '@logto/connector-types';
|
||||
import { getFileContents } from '@logto/shared';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -21,8 +21,9 @@ const readmeContentFallback = 'Please check README.md file directory.';
|
|||
const configTemplateFallback = 'Please check config-template.md file directory.';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'wechat-native',
|
||||
target: 'wechat-native',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Native,
|
||||
name: {
|
||||
en: 'Sign In with WeChat',
|
||||
'zh-CN': '微信登录',
|
||||
|
|
|
@ -49,7 +49,7 @@ export class WeChatNativeConnector implements SocialConnector {
|
|||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (redirectUri, state) => {
|
||||
const { appId } = await this.getConfig(this.metadata.id);
|
||||
const { appId } = await this.getConfig(this.metadata.target, this.metadata.platform);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
appid: appId,
|
||||
|
@ -71,7 +71,10 @@ export class WeChatNativeConnector implements SocialConnector {
|
|||
errcode?: number;
|
||||
};
|
||||
|
||||
const { appId: appid, appSecret: secret } = await this.getConfig(this.metadata.id);
|
||||
const { appId: appid, appSecret: secret } = await this.getConfig(
|
||||
this.metadata.target,
|
||||
this.metadata.platform
|
||||
);
|
||||
|
||||
const {
|
||||
access_token: accessToken,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import path from 'path';
|
||||
|
||||
import { ConnectorMetadata, ConnectorType } from '@logto/connector-types';
|
||||
import { ConnectorMetadata, ConnectorType, ConnectorPlatform } from '@logto/connector-types';
|
||||
import { getFileContents } from '@logto/shared';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -21,8 +21,9 @@ const readmeContentFallback = 'Please check README.md file directory.';
|
|||
const configTemplateFallback = 'Please check config-template.md file directory.';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'wechat',
|
||||
target: 'wechat',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Web,
|
||||
name: {
|
||||
en: 'Sign In with WeChat',
|
||||
'zh-CN': '微信登录',
|
||||
|
|
|
@ -49,7 +49,7 @@ export class WeChatConnector implements SocialConnector {
|
|||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (redirectUri, state) => {
|
||||
const { appId } = await this.getConfig(this.metadata.id);
|
||||
const { appId } = await this.getConfig(this.metadata.target, this.metadata.platform);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
appid: appId,
|
||||
|
@ -72,7 +72,10 @@ export class WeChatConnector implements SocialConnector {
|
|||
errcode?: number;
|
||||
};
|
||||
|
||||
const { appId: appid, appSecret: secret } = await this.getConfig(this.metadata.id);
|
||||
const { appId: appid, appSecret: secret } = await this.getConfig(
|
||||
this.metadata.target,
|
||||
this.metadata.platform
|
||||
);
|
||||
|
||||
const {
|
||||
access_token: accessToken,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Languages } from '@logto/phrases';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
resource: Record<Languages, string>;
|
||||
resource: Record<string, string>;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -37,12 +37,13 @@ const GuideModal = ({ connector, isOpen, onClose }: Props) => {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
id: connectorId,
|
||||
type: connectorType,
|
||||
metadata: { name, configTemplate, readme },
|
||||
metadata: { type: connectorType, name, configTemplate, readme },
|
||||
} = connector;
|
||||
|
||||
const locale = i18next.language;
|
||||
const connectorName = name[locale] ?? name.en;
|
||||
// TODO: LOG-2393 should fix name[locale] syntax error
|
||||
const foundName = Object.entries(name).find(([lang]) => lang === locale);
|
||||
const connectorName = foundName ? foundName[1] : name.en;
|
||||
const isSocialConnector =
|
||||
connectorType !== ConnectorType.SMS && connectorType !== ConnectorType.Email;
|
||||
const [activeStepIndex, setActiveStepIndex] = useState<number>(0);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Languages } from '@logto/phrases';
|
||||
import { Language } from '@logto/phrases';
|
||||
import { ConnectorDTO, Identities } from '@logto/schemas';
|
||||
import { Optional } from '@silverhand/essentials';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
@ -23,7 +23,7 @@ type DisplayConnector = {
|
|||
id: string;
|
||||
userId?: string;
|
||||
logo: string;
|
||||
name: Record<Languages, string>;
|
||||
name: Record<Language, string>;
|
||||
};
|
||||
|
||||
const UserConnectors = ({ userId, connectors, onDelete }: Props) => {
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"@logto/connector-wechat-native": "^0.1.0",
|
||||
"@logto/phrases": "^0.1.0",
|
||||
"@logto/schemas": "^0.1.0",
|
||||
"@silverhand/essentials": "^1.1.0",
|
||||
"@silverhand/essentials": "^1.1.6",
|
||||
"argon2": "^0.28.5",
|
||||
"chalk": "^4",
|
||||
"dayjs": "^1.10.5",
|
||||
|
|
|
@ -1,55 +1,152 @@
|
|||
import { ConnectorPlatform } from '@logto/connector-types';
|
||||
import { Connector, ConnectorMetadata, ConnectorType } from '@logto/schemas';
|
||||
|
||||
export const mockMetadata: ConnectorMetadata = {
|
||||
target: 'connector',
|
||||
type: ConnectorType.Email,
|
||||
platform: null,
|
||||
name: {
|
||||
en: 'Connector',
|
||||
'zh-CN': '连接器',
|
||||
},
|
||||
logo: './logo.png',
|
||||
description: {
|
||||
en: 'Connector',
|
||||
'zh-CN': '连接器',
|
||||
},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
};
|
||||
|
||||
export const mockConnector: Connector = {
|
||||
id: 'connector',
|
||||
target: 'connector',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
};
|
||||
|
||||
const mockMetadata0: ConnectorMetadata = {
|
||||
...mockMetadata,
|
||||
target: 'connector_0',
|
||||
type: ConnectorType.Email,
|
||||
platform: ConnectorPlatform.Universal,
|
||||
};
|
||||
|
||||
const mockMetadata1: ConnectorMetadata = {
|
||||
...mockMetadata,
|
||||
target: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
platform: ConnectorPlatform.Universal,
|
||||
};
|
||||
|
||||
const mockMetadata2: ConnectorMetadata = {
|
||||
...mockMetadata,
|
||||
target: 'connector_2',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Universal,
|
||||
};
|
||||
|
||||
const mockMetadata3: ConnectorMetadata = {
|
||||
...mockMetadata,
|
||||
target: 'connector_3',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Universal,
|
||||
};
|
||||
|
||||
const mockMetadata4: ConnectorMetadata = {
|
||||
...mockMetadata,
|
||||
target: 'connector_4',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Universal,
|
||||
};
|
||||
|
||||
const mockMetadata5: ConnectorMetadata = {
|
||||
...mockMetadata,
|
||||
target: 'connector_5',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Universal,
|
||||
};
|
||||
|
||||
const mockMetadata6: ConnectorMetadata = {
|
||||
...mockMetadata,
|
||||
target: 'connector_6',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Universal,
|
||||
};
|
||||
|
||||
const mockConnector0: Connector = {
|
||||
id: 'connector_0',
|
||||
target: 'connector_0',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
};
|
||||
|
||||
const mockConnector1: Connector = {
|
||||
id: 'connector_1',
|
||||
target: 'connector_1',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_234,
|
||||
};
|
||||
|
||||
const mockConnector2: Connector = {
|
||||
id: 'connector_2',
|
||||
target: 'connector_2',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_345,
|
||||
};
|
||||
|
||||
const mockConnector3: Connector = {
|
||||
id: 'connector_3',
|
||||
target: 'connector_3',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_456,
|
||||
};
|
||||
|
||||
const mockConnector4: Connector = {
|
||||
id: 'connector_4',
|
||||
target: 'connector_4',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_567,
|
||||
};
|
||||
|
||||
const mockConnector5: Connector = {
|
||||
id: 'connector_5',
|
||||
target: 'connector_5',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_567,
|
||||
};
|
||||
|
||||
const mockConnector6: Connector = {
|
||||
id: 'connector_6',
|
||||
target: 'connector_6',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_567,
|
||||
};
|
||||
|
||||
export const mockConnectorList: Connector[] = [
|
||||
{
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Email,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
},
|
||||
{
|
||||
id: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_234,
|
||||
},
|
||||
{
|
||||
id: 'connector_2',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_345,
|
||||
},
|
||||
{
|
||||
id: 'connector_3',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_456,
|
||||
},
|
||||
{
|
||||
id: 'connector_4',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_567,
|
||||
},
|
||||
{
|
||||
id: 'connector_5',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_567,
|
||||
},
|
||||
{
|
||||
id: 'connector_6',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_567,
|
||||
},
|
||||
mockConnector0,
|
||||
mockConnector1,
|
||||
mockConnector2,
|
||||
mockConnector3,
|
||||
mockConnector4,
|
||||
mockConnector5,
|
||||
mockConnector6,
|
||||
];
|
||||
|
||||
export const mockConnectorInstanceList: Array<{
|
||||
|
@ -57,189 +154,116 @@ export const mockConnectorInstanceList: Array<{
|
|||
metadata: ConnectorMetadata;
|
||||
}> = [
|
||||
{
|
||||
connector: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
connector: mockConnector0,
|
||||
metadata: { ...mockMetadata0, type: ConnectorType.Social },
|
||||
},
|
||||
{
|
||||
connector: mockConnector1,
|
||||
metadata: mockMetadata1,
|
||||
},
|
||||
{
|
||||
connector: mockConnector2,
|
||||
metadata: mockMetadata2,
|
||||
},
|
||||
{
|
||||
connector: mockConnector3,
|
||||
metadata: mockMetadata3,
|
||||
},
|
||||
{
|
||||
connector: {
|
||||
id: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_234,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
...mockConnector4,
|
||||
platform: null,
|
||||
},
|
||||
metadata: { ...mockMetadata4, type: ConnectorType.Email, platform: null },
|
||||
},
|
||||
{
|
||||
connector: {
|
||||
id: 'connector_2',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_345,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_2',
|
||||
type: ConnectorType.Social,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
...mockConnector5,
|
||||
platform: null,
|
||||
},
|
||||
metadata: { ...mockMetadata5, type: ConnectorType.SMS, platform: null },
|
||||
},
|
||||
{
|
||||
connector: {
|
||||
id: 'connector_3',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_456,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_3',
|
||||
type: ConnectorType.Social,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
},
|
||||
{
|
||||
connector: {
|
||||
id: 'connector_4',
|
||||
type: ConnectorType.Email,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_567,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_4',
|
||||
type: ConnectorType.Email,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
},
|
||||
{
|
||||
connector: {
|
||||
id: 'connector_5',
|
||||
type: ConnectorType.SMS,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_567,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_5',
|
||||
type: ConnectorType.SMS,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
},
|
||||
{
|
||||
connector: {
|
||||
id: 'connector_6',
|
||||
type: ConnectorType.Email,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_567,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_6',
|
||||
type: ConnectorType.Email,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
...mockConnector6,
|
||||
platform: null,
|
||||
},
|
||||
metadata: { ...mockMetadata6, type: ConnectorType.Email, platform: null },
|
||||
},
|
||||
];
|
||||
|
||||
export const mockAliyunDmConnectorInstance = {
|
||||
connector: {
|
||||
...mockConnector,
|
||||
id: 'aliyun-dm',
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_333,
|
||||
target: 'aliyun-dm',
|
||||
platform: null,
|
||||
},
|
||||
metadata: {
|
||||
...mockMetadata,
|
||||
target: 'aliyun-dm',
|
||||
type: ConnectorType.Email,
|
||||
platform: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const mockAliyunSmsConnectorInstance = {
|
||||
connector: {
|
||||
...mockConnector,
|
||||
id: 'aliyun-sms',
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_333,
|
||||
target: 'aliyun-sms',
|
||||
platform: null,
|
||||
},
|
||||
metadata: {
|
||||
...mockMetadata,
|
||||
target: 'aliyun-sms',
|
||||
type: ConnectorType.SMS,
|
||||
platform: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const mockFacebookConnectorInstance = {
|
||||
connector: {
|
||||
...mockConnector,
|
||||
id: 'facebook',
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_333,
|
||||
target: 'facebook',
|
||||
platform: ConnectorPlatform.Web,
|
||||
},
|
||||
metadata: {
|
||||
...mockMetadata,
|
||||
target: 'facebook',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Web,
|
||||
},
|
||||
};
|
||||
|
||||
export const mockGithubConnectorInstance = {
|
||||
connector: {
|
||||
...mockConnector,
|
||||
id: 'github',
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_000,
|
||||
target: 'github',
|
||||
platform: ConnectorPlatform.Web,
|
||||
},
|
||||
metadata: {
|
||||
...mockMetadata,
|
||||
target: 'github',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Web,
|
||||
},
|
||||
};
|
||||
|
||||
export const mockGoogleConnectorInstance = {
|
||||
connector: {
|
||||
...mockConnector,
|
||||
id: 'google',
|
||||
target: 'google',
|
||||
platform: ConnectorPlatform.Web,
|
||||
enabled: false,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_000,
|
||||
},
|
||||
metadata: {
|
||||
...mockMetadata,
|
||||
target: 'google',
|
||||
type: ConnectorType.Social,
|
||||
platform: ConnectorPlatform.Web,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ConnectorPlatform } from '@logto/connector-types';
|
||||
import { Connector, ConnectorType } from '@logto/schemas';
|
||||
import { NotFoundError } from 'slonik';
|
||||
|
||||
import {
|
||||
getConnectorInstanceById,
|
||||
|
@ -13,56 +13,64 @@ import RequestError from '@/errors/RequestError';
|
|||
|
||||
const alipayConnector = {
|
||||
id: 'alipay',
|
||||
type: ConnectorType.Social,
|
||||
target: 'alipay',
|
||||
platform: ConnectorPlatform.Web,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_911,
|
||||
};
|
||||
const aliyunDmConnector = {
|
||||
id: 'aliyun-dm',
|
||||
type: ConnectorType.Email,
|
||||
target: 'aliyun-dm',
|
||||
platform: null,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_911,
|
||||
};
|
||||
const aliyunSmsConnector = {
|
||||
id: 'aliyun-sms',
|
||||
type: ConnectorType.SMS,
|
||||
target: 'aliyun-sms',
|
||||
platform: null,
|
||||
enabled: false,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_666,
|
||||
};
|
||||
const facebookConnector = {
|
||||
id: 'facebook',
|
||||
type: ConnectorType.Social,
|
||||
target: 'facebook',
|
||||
platform: ConnectorPlatform.Web,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_333,
|
||||
};
|
||||
const githubConnector = {
|
||||
id: 'github',
|
||||
type: ConnectorType.Social,
|
||||
target: 'github',
|
||||
platform: ConnectorPlatform.Web,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_555,
|
||||
};
|
||||
const googleConnector = {
|
||||
id: 'google',
|
||||
type: ConnectorType.Social,
|
||||
target: 'google',
|
||||
platform: ConnectorPlatform.Web,
|
||||
enabled: false,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_000,
|
||||
};
|
||||
const wechatConnector = {
|
||||
id: 'wechat',
|
||||
type: ConnectorType.Social,
|
||||
target: 'wechat',
|
||||
platform: ConnectorPlatform.Web,
|
||||
enabled: false,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_000,
|
||||
};
|
||||
const wechatNativeConnector = {
|
||||
id: 'wechat-native',
|
||||
type: ConnectorType.Social,
|
||||
target: 'wechat-native',
|
||||
platform: ConnectorPlatform.Native,
|
||||
enabled: false,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_000,
|
||||
|
@ -78,24 +86,13 @@ const connectors = [
|
|||
wechatConnector,
|
||||
wechatNativeConnector,
|
||||
];
|
||||
const connectorMap = new Map(connectors.map((connector) => [connector.id, connector]));
|
||||
|
||||
const findAllConnectors = jest.fn(async () => connectors);
|
||||
const findConnectorById = jest.fn(async (id: string) => {
|
||||
const connector = connectorMap.get(id);
|
||||
|
||||
if (!connector) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
return connector;
|
||||
});
|
||||
const insertConnector = jest.fn(async (connector: Connector) => connector);
|
||||
|
||||
jest.mock('@/queries/connector', () => ({
|
||||
...jest.requireActual('@/queries/connector'),
|
||||
findAllConnectors: async () => findAllConnectors(),
|
||||
findConnectorById: async (id: string) => findConnectorById(id),
|
||||
insertConnector: async (connector: Connector) => insertConnector(connector),
|
||||
}));
|
||||
|
||||
|
@ -122,7 +119,7 @@ describe('getConnectorInstances', () => {
|
|||
|
||||
test('should access DB only once and should not throw', async () => {
|
||||
await expect(getConnectorInstances()).resolves.not.toThrow();
|
||||
expect(findAllConnectors).toHaveBeenCalledTimes(1);
|
||||
expect(findAllConnectors).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -131,15 +128,29 @@ describe('getConnectorInstances', () => {
|
|||
});
|
||||
|
||||
describe('getConnectorInstanceById', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should return the connector existing in DB', async () => {
|
||||
const connectorInstance = await getConnectorInstanceById('aliyun-dm');
|
||||
expect(connectorInstance).toHaveProperty('connector', aliyunDmConnector);
|
||||
});
|
||||
|
||||
test('should throw on invalid id', async () => {
|
||||
test('should throw on invalid id (on DB query)', async () => {
|
||||
const id = 'invalid_id';
|
||||
await expect(getConnectorInstanceById(id)).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should throw on invalid id (on finding metadata)', async () => {
|
||||
const id = 'invalid_id';
|
||||
await expect(getConnectorInstanceById(id)).rejects.toMatchError(
|
||||
new RequestError({ code: 'entity.not_found', id, status: 404 })
|
||||
new RequestError({
|
||||
code: 'entity.not_found',
|
||||
target: 'invalid_target',
|
||||
platfrom: ConnectorPlatform.Web,
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -150,10 +161,14 @@ describe('getSocialConnectorInstanceById', () => {
|
|||
expect(socialConnectorInstance).toHaveProperty('connector', googleConnector);
|
||||
});
|
||||
|
||||
test('should throw on invalid id', async () => {
|
||||
const id = 'invalid_id';
|
||||
test('should throw on non-social connector', async () => {
|
||||
const id = 'aliyun-dm';
|
||||
await expect(getSocialConnectorInstanceById(id)).rejects.toMatchError(
|
||||
new RequestError({ code: 'entity.not_found', id, status: 404 })
|
||||
new RequestError({
|
||||
code: 'entity.not_found',
|
||||
id,
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -186,8 +201,14 @@ describe('initConnectors', () => {
|
|||
expect(insertConnector).toHaveBeenCalledTimes(connectors.length);
|
||||
|
||||
for (const [i, connector] of connectors.entries()) {
|
||||
const { id, type } = connector;
|
||||
expect(insertConnector).toHaveBeenNthCalledWith(i + 1, { id, type });
|
||||
const { target, platform } = connector;
|
||||
expect(insertConnector).toHaveBeenNthCalledWith(
|
||||
i + 1,
|
||||
expect.objectContaining({
|
||||
target,
|
||||
platform,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -197,6 +218,7 @@ describe('initConnectors', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
findAllConnectors.mockClear();
|
||||
insertConnector.mockClear();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,12 +6,13 @@ import { GithubConnector } from '@logto/connector-github';
|
|||
import { GoogleConnector } from '@logto/connector-google';
|
||||
import { WeChatConnector } from '@logto/connector-wechat';
|
||||
import { WeChatNativeConnector } from '@logto/connector-wechat-native';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { findAllConnectors, findConnectorById, insertConnector } from '@/queries/connector';
|
||||
import { findAllConnectors, insertConnector } from '@/queries/connector';
|
||||
|
||||
import { ConnectorInstance, ConnectorType, IConnector, SocialConnectorInstance } from './types';
|
||||
import { getConnectorConfig } from './utilities';
|
||||
import { buildIndexWithTargetAndPlatform, getConnectorConfig } from './utilities';
|
||||
|
||||
const allConnectors: IConnector[] = [
|
||||
new AlipayConnector(getConnectorConfig),
|
||||
|
@ -26,14 +27,19 @@ const allConnectors: IConnector[] = [
|
|||
|
||||
export const getConnectorInstances = async (): Promise<ConnectorInstance[]> => {
|
||||
const connectors = await findAllConnectors();
|
||||
const connectorMap = new Map(connectors.map((connector) => [connector.id, connector]));
|
||||
const connectorMap = new Map(
|
||||
connectors.map((connector) => [
|
||||
buildIndexWithTargetAndPlatform(connector.target, connector.platform),
|
||||
connector,
|
||||
])
|
||||
);
|
||||
|
||||
return allConnectors.map((element) => {
|
||||
const { id } = element.metadata;
|
||||
const connector = connectorMap.get(id);
|
||||
const { target, platform } = element.metadata;
|
||||
const connector = connectorMap.get(buildIndexWithTargetAndPlatform(target, platform));
|
||||
|
||||
if (!connector) {
|
||||
throw new RequestError({ code: 'entity.not_found', id, status: 404 });
|
||||
throw new RequestError({ code: 'entity.not_found', target, platform, status: 404 });
|
||||
}
|
||||
|
||||
return { connector, ...element };
|
||||
|
@ -41,9 +47,10 @@ export const getConnectorInstances = async (): Promise<ConnectorInstance[]> => {
|
|||
};
|
||||
|
||||
export const getConnectorInstanceById = async (id: string): Promise<ConnectorInstance> => {
|
||||
const found = allConnectors.find((element) => element.metadata.id === id);
|
||||
const connectorInstances = await getConnectorInstances();
|
||||
const pickedConnectorInstance = connectorInstances.find(({ connector }) => connector.id === id);
|
||||
|
||||
if (!found) {
|
||||
if (!pickedConnectorInstance) {
|
||||
throw new RequestError({
|
||||
code: 'entity.not_found',
|
||||
id,
|
||||
|
@ -51,9 +58,7 @@ export const getConnectorInstanceById = async (id: string): Promise<ConnectorIns
|
|||
});
|
||||
}
|
||||
|
||||
const connector = await findConnectorById(id);
|
||||
|
||||
return { connector, ...found };
|
||||
return pickedConnectorInstance;
|
||||
};
|
||||
|
||||
const isSocialConnectorInstance = (
|
||||
|
@ -88,7 +93,7 @@ export const getEnabledSocialConnectorIds = async <T extends ConnectorInstance>(
|
|||
(instance): instance is T =>
|
||||
instance.connector.enabled && instance.metadata.type === ConnectorType.Social
|
||||
)
|
||||
.map((instance) => instance.metadata.id);
|
||||
.map((instance) => instance.connector.id);
|
||||
};
|
||||
|
||||
export const getConnectorInstanceByType = async <T extends ConnectorInstance>(
|
||||
|
@ -108,11 +113,26 @@ export const getConnectorInstanceByType = async <T extends ConnectorInstance>(
|
|||
|
||||
export const initConnectors = async () => {
|
||||
const connectors = await findAllConnectors();
|
||||
const existingConnectors = new Map(connectors.map((connector) => [connector.id, connector]));
|
||||
const newConnectors = allConnectors.filter(
|
||||
({ metadata: { id, type } }) => existingConnectors.get(id)?.type !== type
|
||||
const existingConnectors = new Map(
|
||||
connectors.map((connector) => [
|
||||
buildIndexWithTargetAndPlatform(connector.target, connector.platform),
|
||||
connector,
|
||||
])
|
||||
);
|
||||
const newConnectors = allConnectors.filter(
|
||||
({ metadata: { target, platform } }) =>
|
||||
existingConnectors.get(buildIndexWithTargetAndPlatform(target, platform))?.platform !==
|
||||
platform
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
newConnectors.map(async ({ metadata: { id, type } }) => insertConnector({ id, type }))
|
||||
newConnectors.map(async ({ metadata: { target, platform } }) => {
|
||||
const id = nanoid();
|
||||
await insertConnector({
|
||||
id,
|
||||
target,
|
||||
platform,
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,24 +1,35 @@
|
|||
import { ConnectorType } from '@logto/schemas';
|
||||
import { ConnectorPlatform } from '@logto/connector-types';
|
||||
import { Connector } from '@logto/schemas';
|
||||
|
||||
import { findConnectorById, updateConnector } from '@/queries/connector';
|
||||
import { buildIndexWithTargetAndPlatform, getConnectorConfig } from '.';
|
||||
|
||||
import { getConnectorConfig, updateConnectorConfig } from '.';
|
||||
|
||||
jest.mock('@/queries/connector');
|
||||
|
||||
it('getConnectorConfig()', async () => {
|
||||
(findConnectorById as jest.MockedFunction<typeof findConnectorById>).mockResolvedValueOnce({
|
||||
const connectors: Connector[] = [
|
||||
{
|
||||
id: 'id',
|
||||
type: ConnectorType.Social,
|
||||
target: 'target',
|
||||
platform: null,
|
||||
enabled: true,
|
||||
config: { foo: 'bar' },
|
||||
createdAt: 0,
|
||||
});
|
||||
const config = await getConnectorConfig('connectorId');
|
||||
expect(config).toMatchObject({ foo: 'bar' });
|
||||
},
|
||||
];
|
||||
|
||||
const findAllConnectors = jest.fn(async () => connectors);
|
||||
|
||||
jest.mock('@/queries/connector', () => ({
|
||||
...jest.requireActual('@/queries/connector'),
|
||||
findAllConnectors: async () => findAllConnectors(),
|
||||
}));
|
||||
|
||||
it('buildIndexWithTargetAndPlatform() with not-null `platform`', async () => {
|
||||
expect(buildIndexWithTargetAndPlatform('target', ConnectorPlatform.Web)).toEqual('target_Web');
|
||||
});
|
||||
|
||||
it('updateConnectorConfig() should call updateConnector()', async () => {
|
||||
await updateConnectorConfig('connectorId', { foo: 'bar' });
|
||||
expect(updateConnector).toHaveBeenCalled();
|
||||
it('buildIndexWithTargetAndPlatform() with null `platform`', async () => {
|
||||
expect(buildIndexWithTargetAndPlatform('target', null)).toEqual('target_null');
|
||||
});
|
||||
|
||||
it('getConnectorConfig()', async () => {
|
||||
const config = await getConnectorConfig('target', null);
|
||||
expect(config).toMatchObject({ foo: 'bar' });
|
||||
});
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
import { ArbitraryObject } from '@logto/schemas';
|
||||
import { ArbitraryObject, ConnectorPlatform } from '@logto/schemas';
|
||||
import { Nullable } from '@silverhand/essentials';
|
||||
|
||||
import { findConnectorById, updateConnector } from '@/queries/connector';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { findAllConnectors } from '@/queries/connector';
|
||||
|
||||
export const getConnectorConfig = async <T extends ArbitraryObject>(id: string): Promise<T> => {
|
||||
const connector = await findConnectorById(id);
|
||||
export const buildIndexWithTargetAndPlatform = (
|
||||
target: string,
|
||||
platform: Nullable<string>
|
||||
): string => [target, platform ?? 'null'].join('_');
|
||||
|
||||
export const getConnectorConfig = async <T extends ArbitraryObject>(
|
||||
target: string,
|
||||
platform: Nullable<ConnectorPlatform>
|
||||
): Promise<T> => {
|
||||
const connectors = await findAllConnectors();
|
||||
const connector = connectors.find(
|
||||
(connector) => connector.target === target && connector.platform === platform
|
||||
);
|
||||
|
||||
if (!connector) {
|
||||
throw new RequestError({ code: 'entity.not_found', target, platform, status: 404 });
|
||||
}
|
||||
|
||||
return connector.config as T;
|
||||
};
|
||||
|
||||
export const updateConnectorConfig = async <T extends ArbitraryObject>(
|
||||
id: string,
|
||||
config: T
|
||||
): Promise<void> => {
|
||||
await updateConnector({
|
||||
where: { id },
|
||||
set: { config },
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ConnectorType } from '@logto/connector-types';
|
||||
import { Passcode, PasscodeType } from '@logto/schemas';
|
||||
|
||||
import { mockConnector, mockMetadata } from '@/__mocks__';
|
||||
import { getConnectorInstanceByType } from '@/connectors';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import {
|
||||
|
@ -121,23 +122,20 @@ describe('sendPasscode', () => {
|
|||
|
||||
it('should call sendPasscode with params matching', async () => {
|
||||
const sendMessage = jest.fn();
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
id: 'id',
|
||||
type: ConnectorType.SMS,
|
||||
platform: null,
|
||||
};
|
||||
mockedGetConnectorInstanceByType.mockResolvedValue({
|
||||
connector: {
|
||||
...mockConnector,
|
||||
id: 'id',
|
||||
type: ConnectorType.SMS,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
metadata: {
|
||||
id: 'id',
|
||||
type: ConnectorType.SMS,
|
||||
name: {},
|
||||
logo: '',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
target: 'connector',
|
||||
platform: null,
|
||||
},
|
||||
metadata: mockedMetadata,
|
||||
sendMessage,
|
||||
validateConfig: jest.fn(),
|
||||
getConfig: jest.fn(),
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { Connectors, ConnectorType, CreateConnector } from '@logto/schemas';
|
||||
import { createMockPool, createMockQueryResult, sql, QueryResultRow } from 'slonik';
|
||||
import { Connectors } from '@logto/schemas';
|
||||
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
||||
|
||||
import { mockConnector } from '@/__mocks__';
|
||||
import { convertToIdentifiers } from '@/database/utils';
|
||||
import envSet from '@/env-set';
|
||||
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
|
||||
|
||||
import {
|
||||
findAllConnectors,
|
||||
findConnectorById,
|
||||
insertConnector,
|
||||
updateConnector,
|
||||
} from './connector';
|
||||
import { findAllConnectors, insertConnector, updateConnector } from './connector';
|
||||
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
||||
|
@ -43,51 +39,36 @@ describe('connector queries', () => {
|
|||
await expect(findAllConnectors()).resolves.toEqual([rowData]);
|
||||
});
|
||||
|
||||
it('findConnectorById', async () => {
|
||||
const id = 'foo';
|
||||
const rowData = { id };
|
||||
|
||||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.id}=$1
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([id]);
|
||||
|
||||
return createMockQueryResult([rowData]);
|
||||
});
|
||||
|
||||
await expect(findConnectorById(id)).resolves.toEqual(rowData);
|
||||
});
|
||||
|
||||
it('insertConnector', async () => {
|
||||
const connector: CreateConnector & QueryResultRow = {
|
||||
id: 'foo',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
const connector = {
|
||||
...mockConnector,
|
||||
config: JSON.stringify(mockConnector.config),
|
||||
};
|
||||
|
||||
const expectSql = `
|
||||
insert into "connectors" ("id", "type", "enabled")
|
||||
values ($1, $2, $3)
|
||||
insert into "connectors" ("id", "target", "platform", "enabled", "config")
|
||||
values ($1, $2, $3, $4, $5)
|
||||
returning *
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql);
|
||||
|
||||
expect(values).toEqual([connector.id, connector.type, connector.enabled]);
|
||||
expect(values).toEqual([
|
||||
connector.id,
|
||||
connector.target,
|
||||
connector.platform,
|
||||
connector.enabled,
|
||||
connector.config,
|
||||
]);
|
||||
|
||||
return createMockQueryResult([connector]);
|
||||
});
|
||||
|
||||
await expect(insertConnector(connector)).resolves.toEqual(connector);
|
||||
await expect(insertConnector(mockConnector)).resolves.toEqual(connector);
|
||||
});
|
||||
|
||||
it('updateConnector', async () => {
|
||||
it('updateConnector (with id)', async () => {
|
||||
const id = 'foo';
|
||||
const enabled = false;
|
||||
|
||||
|
|
|
@ -17,13 +17,6 @@ export const findAllConnectors = async () =>
|
|||
`)
|
||||
);
|
||||
|
||||
export const findConnectorById = async (id: string) =>
|
||||
envSet.pool.one<Connector>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.id}=${id}
|
||||
`);
|
||||
|
||||
export const insertConnector = buildInsertInto<CreateConnector, Connector>(Connectors, {
|
||||
returning: true,
|
||||
});
|
||||
|
|
|
@ -20,15 +20,34 @@ import { createRequester } from '@/utils/test-utils';
|
|||
|
||||
import connectorRoutes from './connector';
|
||||
|
||||
const mockMetadata: ConnectorMetadata = {
|
||||
target: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
platform: null,
|
||||
name: {
|
||||
en: 'Connector',
|
||||
'zh-CN': '连接器',
|
||||
},
|
||||
logo: './logo.png',
|
||||
description: { en: 'Connector', 'zh-CN': '连接器' },
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
};
|
||||
const mockConnector: Connector = {
|
||||
id: 'connector_0',
|
||||
target: 'connector_0',
|
||||
platform: null,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
};
|
||||
|
||||
type ConnectorInstance = {
|
||||
connector: Connector;
|
||||
metadata: ConnectorMetadata;
|
||||
validateConfig?: ValidateConfig;
|
||||
};
|
||||
|
||||
const findConnectorByIdPlaceHolder = jest.fn() as jest.MockedFunction<
|
||||
(connectorId: string) => Promise<Connector>
|
||||
>;
|
||||
const getConnectorInstanceByIdPlaceHolder = jest.fn() as jest.MockedFunction<
|
||||
(connectorId: string) => Promise<ConnectorInstance>
|
||||
>;
|
||||
|
@ -40,7 +59,6 @@ const getConnectorInstancesPlaceHolder = jest.fn() as jest.MockedFunction<
|
|||
>;
|
||||
|
||||
jest.mock('@/queries/connector', () => ({
|
||||
findConnectorById: async (connectorId: string) => findConnectorByIdPlaceHolder(connectorId),
|
||||
findAllConnectors: jest.fn(),
|
||||
updateConnector: jest.fn(),
|
||||
}));
|
||||
|
@ -95,7 +113,7 @@ describe('connector route', () => {
|
|||
it('throws when connector can not be found by given connectorId (locally)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
const found = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.metadata.id === 'connector'
|
||||
(connectorInstance) => connectorInstance.connector.id === 'connector'
|
||||
);
|
||||
assertThat(found, new RequestError({ code: 'entity.not_found', status: 404 }));
|
||||
|
||||
|
@ -108,7 +126,7 @@ describe('connector route', () => {
|
|||
it('throws when connector can not be found by given connectorId (remotely)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (id: string) => {
|
||||
const foundConnectorInstance = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.metadata.id === id
|
||||
(connectorInstance) => connectorInstance.connector.id === id
|
||||
);
|
||||
assertThat(
|
||||
foundConnectorInstance,
|
||||
|
@ -127,7 +145,7 @@ describe('connector route', () => {
|
|||
it('shows found connector information', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (id: string) => {
|
||||
const foundConnectorInstance = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.metadata.id === id
|
||||
(connectorInstance) => connectorInstance.connector.id === id
|
||||
);
|
||||
assertThat(
|
||||
foundConnectorInstance,
|
||||
|
@ -152,7 +170,7 @@ describe('connector route', () => {
|
|||
it('throws if connector can not be found (locally)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
const found = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.metadata.id === 'connector'
|
||||
(connectorInstance) => connectorInstance.connector.id === 'connector'
|
||||
);
|
||||
assertThat(found, new RequestError({ code: 'entity.not_found', status: 404 }));
|
||||
|
||||
|
@ -167,7 +185,7 @@ describe('connector route', () => {
|
|||
it('throws if connector can not be found (remotely)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (id: string) => {
|
||||
const foundConnectorInstance = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.metadata.id === id
|
||||
(connectorInstance) => connectorInstance.connector.id === id
|
||||
);
|
||||
assertThat(
|
||||
foundConnectorInstance,
|
||||
|
@ -188,22 +206,8 @@ describe('connector route', () => {
|
|||
it('enables one of the social connectors (with valid config)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
@ -217,15 +221,7 @@ describe('connector route', () => {
|
|||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
metadata: mockMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
@ -233,22 +229,8 @@ describe('connector route', () => {
|
|||
it('enables one of the social connectors (with invalid config)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
validateConfig: async () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
|
||||
},
|
||||
|
@ -263,21 +245,10 @@ describe('connector route', () => {
|
|||
it('disables one of the social connectors', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
validateConfig: async () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -291,38 +262,30 @@ describe('connector route', () => {
|
|||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
},
|
||||
metadata: mockMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
||||
it('enables one of the email/sms connectors (with valid config)', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce(mockConnectorInstanceList);
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
id: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
id: 'connector_1',
|
||||
name: 'connector_1',
|
||||
platform: null,
|
||||
type: ConnectorType.SMS,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: {
|
||||
id: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_234,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
@ -351,38 +314,30 @@ describe('connector route', () => {
|
|||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: {
|
||||
id: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
},
|
||||
metadata: mockedMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
||||
it('enables one of the email/sms connectors (with invalid config)', async () => {
|
||||
getConnectorInstancesPlaceHolder.mockResolvedValueOnce(mockConnectorInstanceList);
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
id: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
id: 'connector_1',
|
||||
name: 'connector_1',
|
||||
platform: null,
|
||||
type: ConnectorType.SMS,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: {
|
||||
id: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_234,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_1',
|
||||
type: ConnectorType.SMS,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
validateConfig: async () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
|
||||
},
|
||||
|
@ -395,24 +350,24 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('disables one of the email/sms connectors', async () => {
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
id: 'connector_4',
|
||||
type: ConnectorType.Email,
|
||||
platform: null,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
id: 'connector_4',
|
||||
name: 'connector_4',
|
||||
platform: null,
|
||||
type: ConnectorType.Email,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: {
|
||||
id: 'connector_4',
|
||||
type: ConnectorType.Email,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_567,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_4',
|
||||
type: ConnectorType.Email,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
});
|
||||
const response = await connectorRequest
|
||||
|
@ -425,14 +380,7 @@ describe('connector route', () => {
|
|||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: {
|
||||
id: 'connector_4',
|
||||
type: ConnectorType.Email,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
},
|
||||
metadata: mockedMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
@ -446,7 +394,7 @@ describe('connector route', () => {
|
|||
it('throws when connector can not be found by given connectorId (locally)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
const found = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.metadata.id === 'connector'
|
||||
(connectorInstance) => connectorInstance.connector.id === 'connector'
|
||||
);
|
||||
assertThat(found, new RequestError({ code: 'entity.not_found', status: 404 }));
|
||||
|
||||
|
@ -459,7 +407,7 @@ describe('connector route', () => {
|
|||
it('throws when connector can not be found by given connectorId (remotely)', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (id: string) => {
|
||||
const foundConnectorInstance = mockConnectorInstanceList.find(
|
||||
(connectorInstance) => connectorInstance.metadata.id === id
|
||||
(connectorInstance) => connectorInstance.connector.id === id
|
||||
);
|
||||
assertThat(
|
||||
foundConnectorInstance,
|
||||
|
@ -478,22 +426,8 @@ describe('connector route', () => {
|
|||
it('config validation fails', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
validateConfig: async () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig);
|
||||
},
|
||||
|
@ -508,22 +442,8 @@ describe('connector route', () => {
|
|||
it('successfully updates connector configs', async () => {
|
||||
getConnectorInstanceByIdPlaceHolder.mockImplementationOnce(async (_id: string) => {
|
||||
return {
|
||||
connector: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
connector: mockConnector,
|
||||
metadata: mockMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
@ -537,14 +457,7 @@ describe('connector route', () => {
|
|||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
metadata: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Social,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
},
|
||||
metadata: mockMetadata,
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
@ -556,23 +469,18 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('should get email connector and send message', async () => {
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
type: ConnectorType.Email,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
type: ConnectorType.Email,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
const mockedEmailConnector: EmailConnectorInstance = {
|
||||
connector: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Email,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.Email,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
getConfig: jest.fn(),
|
||||
sendMessage: async (
|
||||
|
@ -603,23 +511,18 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('should get SMS connector and send message', async () => {
|
||||
const mockedSmsConnector: SmsConnectorInstance = {
|
||||
connector: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.SMS,
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_234_567_890_123,
|
||||
},
|
||||
metadata: {
|
||||
id: 'connector_0',
|
||||
type: ConnectorType.SMS,
|
||||
name: {},
|
||||
logo: './logo.png',
|
||||
description: {},
|
||||
readme: 'README.md',
|
||||
configTemplate: 'config-template.md',
|
||||
},
|
||||
const mockedMetadata = {
|
||||
...mockMetadata,
|
||||
type: ConnectorType.SMS,
|
||||
};
|
||||
const mockedConnector = {
|
||||
...mockConnector,
|
||||
type: ConnectorType.SMS,
|
||||
metadata: mockedMetadata,
|
||||
};
|
||||
const mockedSmsConnectorInstance: SmsConnectorInstance = {
|
||||
connector: mockedConnector,
|
||||
metadata: mockedMetadata,
|
||||
validateConfig: jest.fn(),
|
||||
getConfig: jest.fn(),
|
||||
sendMessage: async (
|
||||
|
@ -631,10 +534,10 @@ describe('connector route', () => {
|
|||
};
|
||||
|
||||
getConnectorInstanceByTypePlaceHolder.mockImplementationOnce(async (_: ConnectorType) => {
|
||||
return mockedSmsConnector;
|
||||
return mockedSmsConnectorInstance;
|
||||
});
|
||||
|
||||
const sendMessageSpy = jest.spyOn(mockedSmsConnector, 'sendMessage');
|
||||
const sendMessageSpy = jest.spyOn(mockedSmsConnectorInstance, 'sendMessage');
|
||||
const response = await connectorRequest
|
||||
.post('/connectors/test/sms')
|
||||
.send({ phone: '12345678901' });
|
||||
|
|
|
@ -2,19 +2,14 @@ import { NormalizeKeyPaths } from '@silverhand/essentials';
|
|||
|
||||
import en from './locales/en';
|
||||
import zhCN from './locales/zh-cn';
|
||||
import { Resource } from './types';
|
||||
import { Resource, Language } from './types';
|
||||
|
||||
export { Language } from './types';
|
||||
export type LogtoErrorCode = NormalizeKeyPaths<typeof en.errors>;
|
||||
export type LogtoErrorI18nKey = `errors:${LogtoErrorCode}`;
|
||||
export type Languages = keyof Resource;
|
||||
export type I18nKey = NormalizeKeyPaths<typeof en.translation>;
|
||||
export type AdminConsoleKey = NormalizeKeyPaths<typeof en.translation.admin_console>;
|
||||
|
||||
export enum Language {
|
||||
English = 'en',
|
||||
Chinese = 'zh-CN',
|
||||
}
|
||||
|
||||
const resource: Resource = {
|
||||
[Language.English]: en,
|
||||
[Language.Chinese]: zhCN,
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/consistent-indexed-object-style */
|
||||
|
||||
/* Copied from i18next/index.d.ts */
|
||||
export interface Resource {
|
||||
[language: string]: ResourceLanguage;
|
||||
}
|
||||
export type Resource = Record<Language, ResourceLanguage>;
|
||||
|
||||
export interface ResourceLanguage {
|
||||
[namespace: string]: ResourceKey;
|
||||
|
@ -11,4 +9,9 @@ export interface ResourceLanguage {
|
|||
|
||||
export type ResourceKey = string | { [key: string]: any };
|
||||
|
||||
export enum Language {
|
||||
English = 'en',
|
||||
Chinese = 'zh-CN',
|
||||
}
|
||||
|
||||
/* eslint-enable @typescript-eslint/consistent-indexed-object-style */
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ConnectorMetadata } from '@logto/connector-types';
|
|||
import { Connector } from '../db-entries';
|
||||
|
||||
export type { ConnectorMetadata } from '@logto/connector-types';
|
||||
export { ConnectorType } from '@logto/connector-types';
|
||||
export { ConnectorType, ConnectorPlatform } from '@logto/connector-types';
|
||||
|
||||
export interface ConnectorDTO extends Connector {
|
||||
metadata: ConnectorMetadata;
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
create table connectors (
|
||||
id varchar(128) not null,
|
||||
type varchar(64) not null,
|
||||
target varchar(64) not null,
|
||||
platform varchar(64),
|
||||
enabled boolean not null default FALSE,
|
||||
config jsonb /* @use ArbitraryObject */ not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default(now()),
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
create index connectors__target_platform on connectors (target, platform);
|
||||
|
|
|
@ -7,38 +7,43 @@ export const appLogo = 'https://avatars.githubusercontent.com/u/88327661?s=200&v
|
|||
export const appHeadline = 'Build user identity in a modern way';
|
||||
export const socialConnectors = [
|
||||
{
|
||||
id: 'github',
|
||||
target: 'github',
|
||||
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
|
||||
name: {
|
||||
en: 'Sign in with GitHub',
|
||||
'zh-CN': '使用 GitHub 登录',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'alipay',
|
||||
target: 'alipay',
|
||||
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
|
||||
name: {
|
||||
en: 'Sign in with Alipay',
|
||||
'zh-CN': '使用 Alipay 登录',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'wechat',
|
||||
target: 'wechat',
|
||||
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
|
||||
name: {
|
||||
en: 'Sign in with WeChat',
|
||||
'zh-CN': '使用 WeChat 登录',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'google',
|
||||
target: 'google',
|
||||
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
|
||||
name: {
|
||||
en: 'Sign in with Google',
|
||||
'zh-CN': '使用 Google 登录',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'facebook',
|
||||
target: 'facebook',
|
||||
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
|
||||
name: {
|
||||
en: 'Sign in with Meta',
|
||||
'zh-CN': '使用 Meta 登录',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -6,16 +6,16 @@ import * as styles from './SocialIconButton.module.scss';
|
|||
|
||||
type Props = {
|
||||
className?: string;
|
||||
connector: Pick<ConnectorMetadata, 'id' | 'logo'>;
|
||||
connector: Pick<ConnectorMetadata, 'target' | 'logo'>;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const SocialIconButton = ({ className, connector, onClick }: Props) => {
|
||||
const { id, logo } = connector;
|
||||
const { target, logo } = connector;
|
||||
|
||||
return (
|
||||
<button className={classNames(styles.socialButton, className)} onClick={onClick}>
|
||||
{logo && <img src={logo} alt={id} className={styles.icon} />}
|
||||
{logo && <img src={logo} alt={target} className={styles.icon} />}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,17 +9,19 @@ import * as styles from './index.module.scss';
|
|||
export type Props = {
|
||||
isDisabled?: boolean;
|
||||
className?: string;
|
||||
connector: Pick<ConnectorMetadata, 'id' | 'name' | 'logo'>;
|
||||
connector: Pick<ConnectorMetadata, 'target' | 'name' | 'logo'>;
|
||||
onClick?: (id: string) => void;
|
||||
};
|
||||
|
||||
const SocialLinkButton = ({ isDisabled, className, connector, onClick }: Props) => {
|
||||
const { id, name, logo } = connector;
|
||||
const { target, name, logo } = connector;
|
||||
|
||||
const {
|
||||
i18n: { language },
|
||||
} = useTranslation();
|
||||
const localName = name[language] ?? name.en;
|
||||
// TODO: LOG-2393 should fix name[locale] syntax error
|
||||
const foundName = Object.entries(name).find(([lang]) => lang === language);
|
||||
const localName = foundName ? foundName[1] : name.en;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
@ -32,7 +34,7 @@ const SocialLinkButton = ({ isDisabled, className, connector, onClick }: Props)
|
|||
)}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onClick?.(id);
|
||||
onClick?.(target);
|
||||
}}
|
||||
>
|
||||
{logo && <img src={logo} alt={localName} className={socialLinkButtonStyles.icon} />}
|
||||
|
|
|
@ -25,9 +25,10 @@ describe('Button Component', () => {
|
|||
|
||||
it('render SocialLinkButton', () => {
|
||||
const connector = {
|
||||
id: 'foo',
|
||||
target: 'foo',
|
||||
name: {
|
||||
en: 'Sign in with Logto',
|
||||
'zh-CN': '使用 Logto 登录',
|
||||
},
|
||||
logo: 'http://logto.dev/logto.png',
|
||||
};
|
||||
|
|
|
@ -33,11 +33,11 @@ const PrimarySocialSignIn = ({ className, isPopup = false, onSocialSignInCallbac
|
|||
<div className={classNames(styles.socialLinkList, className)}>
|
||||
{displayConnectors.map((connector) => (
|
||||
<SocialLinkButton
|
||||
key={connector.id}
|
||||
key={connector.target}
|
||||
className={styles.socialLinkButton}
|
||||
connector={connector}
|
||||
onClick={() => {
|
||||
void invokeSocialSignIn(connector.id, onSocialSignInCallback);
|
||||
void invokeSocialSignIn(connector.target, onSocialSignInCallback);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('SecondarySocialSignIn', () => {
|
|||
platform: 'web',
|
||||
getPostMessage: jest.fn(() => jest.fn()),
|
||||
callbackLink: '/logto:',
|
||||
supportedSocialConnectorIds: socialConnectors.map(({ id }) => id),
|
||||
supportedSocialConnectorIds: socialConnectors.map(({ target }) => target),
|
||||
};
|
||||
/* eslint-enable @silverhand/fp/no-mutation */
|
||||
});
|
||||
|
|
|
@ -32,11 +32,11 @@ const SecondarySocialSignIn = ({ className }: Props) => {
|
|||
<div className={classNames(styles.socialIconList, className)}>
|
||||
{displayConnectors.map((connector) => (
|
||||
<SocialIconButton
|
||||
key={connector.id}
|
||||
key={connector.target}
|
||||
className={styles.socialButton}
|
||||
connector={connector}
|
||||
onClick={() => {
|
||||
void invokeSocialSignIn(connector.id);
|
||||
void invokeSocialSignIn(connector.target);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -90,8 +90,10 @@ const useSocial = () => {
|
|||
// Filter native supported social connectors
|
||||
const socialConnectors = useMemo(
|
||||
() =>
|
||||
(experienceSettings?.socialConnectors ?? []).filter(({ id }) => {
|
||||
return !isNativeWebview() || getLogtoNativeSdk()?.supportedSocialConnectorIds.includes(id);
|
||||
(experienceSettings?.socialConnectors ?? []).filter(({ target }) => {
|
||||
return (
|
||||
!isNativeWebview() || getLogtoNativeSdk()?.supportedSocialConnectorIds.includes(target)
|
||||
);
|
||||
}),
|
||||
[experienceSettings?.socialConnectors]
|
||||
);
|
||||
|
|
|
@ -19,7 +19,9 @@ const Callback = () => {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
|
||||
const connectorLabel = useMemo(() => {
|
||||
const connector = experienceSettings?.socialConnectors.find(({ id }) => id === connectorId);
|
||||
const connector = experienceSettings?.socialConnectors.find(
|
||||
({ target }) => target === connectorId
|
||||
);
|
||||
|
||||
if (connector) {
|
||||
return (
|
||||
|
|
|
@ -8,7 +8,7 @@ export enum SearchParameters {
|
|||
bindWithSocial = 'bw',
|
||||
}
|
||||
|
||||
type ConnectorData = Pick<ConnectorMetadata, 'id' | 'logo' | 'name'>;
|
||||
type ConnectorData = Pick<ConnectorMetadata, 'target' | 'logo' | 'name'>;
|
||||
|
||||
export type SignInExperienceSettings = {
|
||||
branding: Branding;
|
||||
|
|
1896
pnpm-lock.yaml
generated
1896
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue