Feat: plugin verification config (#1129)

* feat(plugin-verification): add client config

* feat(plugin-verification): add config ui

* fix(plugin-verification): fix schema

* refactor(plugin-verification): add default for verification providers

* fix(plugin-users): fix initVerification in lifecycle

* fix(plugin-users): fix initVerification in lifecycle

* fix(plugin-verification): fix locale and default provider

* fix(plugin-verification): fix test case

* fix(plugin-verification): fix locale
This commit is contained in:
Junyi 2022-11-28 00:41:58 -08:00 committed by GitHub
parent a0910f0e2e
commit 7b5277fb2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 707 additions and 104 deletions

View File

@ -1,7 +1,4 @@
version: "3" version: "3"
networks:
nocobase:
driver: bridge
services: services:
app: app:
image: nocobase/nocobase:0.8.0-alpha.13 image: nocobase/nocobase:0.8.0-alpha.13
@ -23,10 +20,8 @@ services:
postgres: postgres:
image: postgres:10 image: postgres:10
restart: always restart: always
networks:
- nocobase
command: postgres -c wal_level=logical command: postgres -c wal_level=logical
environment: environment:
POSTGRES_USER: nocobase POSTGRES_USER: nocobase
POSTGRES_DB: nocobase POSTGRES_DB: nocobase
POSTGRES_PASSWORD: nocobase POSTGRES_PASSWORD: nocobase

View File

@ -0,0 +1 @@
export { default } from '@nocobase/plugin-verification/client';

View File

@ -425,6 +425,7 @@ export default {
'DESC': '降序', 'DESC': '降序',
'Add sort field': '添加排序字段', 'Add sort field': '添加排序字段',
'ID': 'ID', 'ID': 'ID',
'Identifier for program usage. Support letters, numbers and underscores, must start with an letter.': '用于程序使用的标识符,支持字母、数字和下划线,必须以字母开头。',
'Drawer': '抽屉', 'Drawer': '抽屉',
'Dialog': '对话框', 'Dialog': '对话框',
'Delete action': '删除操作', 'Delete action': '删除操作',

View File

@ -72,6 +72,22 @@ const schema: ISchema = {
'x-component': 'Input', 'x-component': 'Input',
required: true, required: true,
}, },
email: {
type: 'string',
title: '{{t("Email")}}',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'email',
required: true,
},
phone: {
type: 'string',
title: '{{t("Phone")}}',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'phone',
required: true,
},
footer: { footer: {
'x-component': 'Action.Drawer.Footer', 'x-component': 'Action.Drawer.Footer',
type: 'void', type: 'void',

View File

@ -6,19 +6,12 @@ import { useTranslation } from 'react-i18next';
import { storageSchema } from './schemas/storage'; import { storageSchema } from './schemas/storage';
import { StorageOptions } from './StorageOptions'; import { StorageOptions } from './StorageOptions';
const schema = {
type: 'object',
properties: {
[uid()]: storageSchema,
},
};
export const FileStoragePane = () => { export const FileStoragePane = () => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Card bordered={false}> <Card bordered={false}>
<SchemaComponent components={{ StorageOptions }} schema={schema} /> <SchemaComponent components={{ StorageOptions }} schema={storageSchema} />
</Card> </Card>
); );
}; };

View File

@ -38,7 +38,7 @@ export default {
title: '{{t("Email")}}', title: '{{t("Email")}}',
'x-component': 'Input', 'x-component': 'Input',
'x-validator': 'email', 'x-validator': 'email',
require: true, required: true,
}, },
}, },
{ {
@ -51,7 +51,7 @@ export default {
title: '{{t("Phone")}}', title: '{{t("Phone")}}',
'x-component': 'Input', 'x-component': 'Input',
'x-validator': 'phone', 'x-validator': 'phone',
require: true, required: true,
}, },
}, },
{ {

View File

@ -97,6 +97,8 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
publicActions.forEach((action) => this.app.acl.allow('users', action)); publicActions.forEach((action) => this.app.acl.allow('users', action));
loggedInActions.forEach((action) => this.app.acl.allow('users', action, 'loggedIn')); loggedInActions.forEach((action) => this.app.acl.allow('users', action, 'loggedIn'));
this.app.on('beforeStart', () => this.initVerification());
} }
async load() { async load() {
@ -113,79 +115,6 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
}); });
initAuthenticators(this); initAuthenticators(this);
// TODO(module): should move to preset
const verificationPlugin = this.app.getPlugin('verification') as any;
if (verificationPlugin && process.env.DEFAULT_SMS_VERIFY_CODE_PROVIDER) {
verificationPlugin.interceptors.register('users:signin', {
manual: true,
provider: process.env.DEFAULT_SMS_VERIFY_CODE_PROVIDER,
getReceiver(ctx) {
return ctx.action.params.values.phone;
},
expiresIn: 120,
validate: async (ctx, phone) => {
if (!phone) {
throw new Error(ctx.t('Not a valid cellphone number, please re-enter'));
}
const User = this.db.getCollection('users');
const exists = await User.model.count({
where: {
phone,
},
});
if (!exists) {
throw new Error(ctx.t('The phone number is not registered, please register first', { ns: namespace }));
}
return true;
},
});
verificationPlugin.interceptors.register('users:signup', {
provider: process.env.DEFAULT_SMS_VERIFY_CODE_PROVIDER,
getReceiver(ctx) {
return ctx.action.params.values.phone;
},
expiresIn: 120,
validate: async (ctx, phone) => {
if (!phone) {
throw new Error(ctx.t('Not a valid cellphone number, please re-enter', { ns: namespace }));
}
const User = this.db.getCollection('users');
const exists = await User.model.count({
where: {
phone,
},
});
if (exists) {
throw new Error(ctx.t('The phone number has been registered, please login directly', { ns: namespace }));
}
return true;
},
});
this.authenticators.register('sms', (ctx, next) =>
verificationPlugin.intercept(ctx, async () => {
const { values } = ctx.action.params;
const User = ctx.db.getCollection('users');
const user = await User.model.findOne({
where: {
phone: values.phone,
},
});
if (!user) {
return ctx.throw(404, ctx.t('The phone number is incorrect, please re-enter', { ns: namespace }));
}
ctx.state.currentUser = user;
return next();
}),
);
}
} }
getInstallingData(options: any = {}) { getInstallingData(options: any = {}) {
@ -218,4 +147,79 @@ export default class UsersPlugin extends Plugin<UserPluginConfig> {
await repo.db2cm('users'); await repo.db2cm('users');
} }
} }
// TODO(module): should move to preset or dynamic configuration panel
async initVerification() {
const verificationPlugin = this.app.getPlugin('verification') as any;
if (!verificationPlugin) {
return;
}
verificationPlugin.interceptors.register('users:signin', {
manual: true,
getReceiver(ctx) {
return ctx.action.params.values.phone;
},
expiresIn: 120,
validate: async (ctx, phone) => {
if (!phone) {
throw new Error(ctx.t('Not a valid cellphone number, please re-enter'));
}
const User = this.db.getCollection('users');
const exists = await User.model.count({
where: {
phone,
},
});
if (!exists) {
throw new Error(ctx.t('The phone number is not registered, please register first', { ns: namespace }));
}
return true;
},
});
verificationPlugin.interceptors.register('users:signup', {
getReceiver(ctx) {
return ctx.action.params.values.phone;
},
expiresIn: 120,
validate: async (ctx, phone) => {
if (!phone) {
throw new Error(ctx.t('Not a valid cellphone number, please re-enter', { ns: namespace }));
}
const User = this.db.getCollection('users');
const exists = await User.model.count({
where: {
phone,
},
});
if (exists) {
throw new Error(ctx.t('The phone number has been registered, please login directly', { ns: namespace }));
}
return true;
},
});
this.authenticators.register('sms', (ctx, next) =>
verificationPlugin.intercept(ctx, async () => {
const { values } = ctx.action.params;
const User = ctx.db.getCollection('users');
const user = await User.model.findOne({
where: {
phone: values.phone,
},
});
if (!user) {
return ctx.throw(404, ctx.t('The phone number is incorrect, please re-enter', { ns: namespace }));
}
ctx.state.currentUser = user;
return next();
}),
);
}
} }

View File

@ -0,0 +1,4 @@
// @ts-nocheck
export * from './lib/client';
export { default } from './lib/client';

View File

@ -0,0 +1,30 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/client"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -0,0 +1,4 @@
// @ts-nocheck
export * from './lib/server';
export { default } from './lib/server';

View File

@ -0,0 +1,30 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/server"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -0,0 +1,22 @@
import React, { useEffect, useState } from 'react';
import { FormLayout } from '@formily/antd';
import { Field } from '@formily/core';
import { observer, RecursionField, Schema, useField, useForm } from '@formily/react';
import providerTypes from './providerTypes';
export default observer((props) => {
const form = useForm();
const field = useField<Field>();
const [s, setSchema] = useState(new Schema({}));
useEffect(() => {
form.clearFormGraph('options.*');
setSchema(new Schema(providerTypes.get(form.values.type) || {}));
}, [form.values.type]);
return (
<FormLayout layout={'vertical'}>
<RecursionField key={form.values.type || 'sms-aliyun'} basePath={field.address} onlyRenderProperties schema={s} />
</FormLayout>
);
});

View File

@ -0,0 +1,20 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { CheckCircleOutlined } from '@ant-design/icons';
import { PluginManager } from '@nocobase/client';
import { NAMESPACE } from './locale';
export const Shortcut = () => {
const { t } = useTranslation();
const history = useHistory();
return (
<PluginManager.Toolbar.Item
icon={<CheckCircleOutlined />}
title={t('Verification', { ns: NAMESPACE })}
onClick={() => {
history.push('/admin/settings/verification/providers');
}}
/>
);
};

View File

@ -0,0 +1,19 @@
import { SchemaComponent } from "@nocobase/client";
import React from "react";
import { Card } from 'antd';
import providers from "./schemas/providers";
import ProviderOptions from "./ProviderOptions";
export function VerificationProviders() {
return (
<Card bordered={false}>
<SchemaComponent
schema={providers}
components={{
ProviderOptions
}}
/>
</Card>
);
};

View File

@ -0,0 +1,41 @@
import React, { useContext } from 'react';
import { PluginManagerContext, SettingsCenterProvider } from '@nocobase/client';
import { NAMESPACE } from './locale';
import { VerificationProviders } from './VerificationProviders';
export { default as verificationProviderTypes } from './providerTypes';
export default function(props) {
const ctx = useContext(PluginManagerContext);
return (
<SettingsCenterProvider
settings={{
verification: {
icon: 'CheckCircleOutlined',
title: `{{t("Verification", { ns: "${NAMESPACE}" })}}`,
tabs: {
providers: {
title: `{{t("Verification providers", { ns: "${NAMESPACE}" })}}`,
component: VerificationProviders,
},
},
},
}}
>
<PluginManagerContext.Provider
value={{
components: {
...ctx?.components,
},
}}
>
{props.children}
</PluginManagerContext.Provider>
</SettingsCenterProvider>
);
};

View File

@ -0,0 +1,16 @@
import { useTranslation } from 'react-i18next';
import { i18n } from '@nocobase/client';
import zhCN from './zh-CN';
export const NAMESPACE = 'verification';
i18n.addResources('zh-CN', NAMESPACE, zhCN);
export function lang(key: string) {
return i18n.t(key, { ns: NAMESPACE });
}
export function useVerificationTranslation() {
return useTranslation(NAMESPACE);
}

View File

@ -0,0 +1,13 @@
export default {
'Verification': '验证码',
'Verification providers': '验证码提供商',
'Provider type': '提供商类型',
// aliyun sms
'Aliyun SMS': '阿里云短信服务',
'Access Key ID': 'Access Key ID',
'Access Key Secret': 'Access Key Secret',
'Endpoint': '接入点',
'Sign': '签名',
'Template code': '模板代码',
};

View File

@ -0,0 +1,9 @@
import { ISchema } from '@formily/react';
import { Registry } from "@nocobase/utils/client";
import SMSAliyun from './sms-aliyun';
const providerTypes: Registry<ISchema> = new Registry();
providerTypes.register('sms-aliyun', SMSAliyun);
export default providerTypes;

View File

@ -0,0 +1,39 @@
import { ISchema } from '@formily/react';
import { NAMESPACE } from '../locale';
export default {
type: 'object',
properties: {
accessKeyId: {
title: `{{t("Access Key ID", { ns: "${NAMESPACE}" })}}`,
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
accessKeySecret: {
title: `{{t("Access Key Secret", { ns: "${NAMESPACE}" })}}`,
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Password',
},
endpoint: {
title: `{{t("Endpoint", { ns: "${NAMESPACE}" })}}`,
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
sign: {
title: `{{t("Sign", { ns: "${NAMESPACE}" })}}`,
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
template: {
title: `{{t("Template code", { ns: "${NAMESPACE}" })}}`,
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
}
}
} as ISchema;

View File

@ -0,0 +1,330 @@
import { uid } from '@formily/shared';
import { useActionContext, useRequest } from '@nocobase/client';
import { NAMESPACE } from '../locale';
const collection = {
name: 'verifications_providers',
fields: [
{
type: 'string',
name: 'id',
interface: 'input',
uiSchema: {
title: '{{t("ID")}}',
type: 'string',
'x-component': 'Input',
required: true,
}
},
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
title: '{{t("Title")}}',
type: 'string',
'x-component': 'Input',
required: true,
}
},
{
type: 'string',
name: 'type',
interface: 'select',
uiSchema: {
title: `{{t("Provider type", { ns: "${NAMESPACE}" })}}`,
type: 'string',
'x-component': 'Select',
required: true,
enum: [
{ label: `{{t("Aliyun SMS", { ns: "${NAMESPACE}" })}}`, value: 'sms-aliyun' },
],
},
},
{
type: 'radio',
name: 'default',
interface: 'checkbox',
uiSchema: {
title: '{{t("Default")}}',
type: 'boolean',
'x-component': 'Checkbox',
}
}
]
};
export default {
type: 'void',
name: 'providers',
'x-decorator': 'ResourceActionProvider',
'x-decorator-props': {
collection,
resourceName: 'verifications_providers',
request: {
resource: 'verifications_providers',
action: 'list',
params: {
pageSize: 50,
sort: ['-default', 'id'],
appends: [],
},
},
},
'x-component': 'CollectionProvider',
'x-component-props': {
collection,
},
properties: {
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 16,
},
},
properties: {
delete: {
type: 'void',
title: '{{t("Delete")}}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useBulkDestroyAction }}',
confirm: {
title: "{{t('Delete')}}",
content: "{{t('Are you sure you want to delete it?')}}",
},
},
},
create: {
type: 'void',
title: '{{t("Add new")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
},
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
'x-decorator-props': {
useValues(options) {
const ctx = useActionContext();
return useRequest(
() =>
Promise.resolve({
data: {
name: `s_${uid()}`,
},
}),
{ ...options, refreshDeps: [ctx.visible] },
);
},
},
title: '{{t("Add new")}}',
properties: {
id: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
description: '{{t("Identifier for program usage. Support letters, numbers and underscores, must start with an letter.")}}',
},
title: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
type: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
options: {
type: 'object',
'x-component': 'ProviderOptions',
},
default: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
cancel: {
title: '{{t("Cancel")}}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
},
},
submit: {
title: '{{t("Submit")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ cm.useCreateAction }}',
},
},
},
},
},
},
},
},
},
},
table: {
type: 'void',
'x-uid': 'input',
'x-component': 'Table.Void',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
useDataSource: '{{ cm.useDataSourceFromRAC }}',
},
properties: {
id: {
type: 'void',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
id: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
title: {
type: 'void',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
title: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
type: {
type: 'void',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
type: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
default: {
type: 'void',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
default: {
type: 'boolean',
'x-component': 'CollectionField',
'x-read-pretty': true,
}
}
},
actions: {
type: 'void',
title: '{{t("Actions")}}',
'x-component': 'Table.Column',
properties: {
actions: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {
update: {
type: 'void',
title: '{{t("Edit")}}',
'x-component': 'Action.Link',
'x-component-props': {
type: 'primary',
},
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
'x-decorator-props': {
useValues: '{{ cm.useValuesFromRecord }}',
},
title: '{{t("Edit")}}',
properties: {
id: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
title: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
type: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-disabled': true,
},
options: {
type: 'object',
'x-component': 'ProviderOptions',
},
default: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
cancel: {
title: '{{t("Cancel")}}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
},
},
submit: {
title: '{{t("Submit")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ cm.useUpdateAction }}',
},
},
},
},
},
},
},
},
delete: {
type: 'void',
title: '{{ t("Delete") }}',
'x-component': 'Action.Link',
'x-component-props': {
confirm: {
title: "{{t('Delete record')}}",
content: "{{t('Are you sure you want to delete it?')}}",
},
useAction: '{{cm.useDestroyAction}}',
},
},
},
},
},
},
},
},
},
};

View File

@ -1,5 +1 @@
export * from './constants'; export { default } from './server';
export { Provider } from './providers';
export { Interceptor, default } from './Plugin';
export const namespace = require('../package.json').name;

View File

@ -14,7 +14,6 @@ import initProviders, { Provider } from './providers';
export interface Interceptor { export interface Interceptor {
manual?: boolean; manual?: boolean;
provider: string;
expiresIn?: number; expiresIn?: number;
getReceiver(ctx): string; getReceiver(ctx): string;
getCode?(ctx): string; getCode?(ctx): string;
@ -56,7 +55,7 @@ export default class VerificationPlugin extends Plugin {
}); });
if (!item) { if (!item) {
return context.throw(400, { code: 'InvalidSMSCode', message: 'verify by sms code failed' }); return context.throw(400, { code: 'InvalidVerificationCode', message: context.t('Verification code is invalid', { ns: namespace }) });
} }
// TODO: code should be removed if exists in values // TODO: code should be removed if exists in values
@ -103,6 +102,7 @@ export default class VerificationPlugin extends Plugin {
sign: INIT_ALI_SMS_VERIFY_CODE_SIGN, sign: INIT_ALI_SMS_VERIFY_CODE_SIGN,
template: INIT_ALI_SMS_VERIFY_CODE_TEMPLATE, template: INIT_ALI_SMS_VERIFY_CODE_TEMPLATE,
}, },
default: true
}, },
}); });
} }
@ -133,5 +133,15 @@ export default class VerificationPlugin extends Plugin {
}); });
app.acl.allow('verifications', 'create'); app.acl.allow('verifications', 'create');
app.acl.allow('verifications_providers', '*', 'allowConfigure');
}
async getDefault() {
const providerRepo = this.db.getRepository('verifications_providers');
return providerRepo.findOne({
filter: {
default: true,
}
});
} }
} }

View File

@ -32,6 +32,7 @@ describe('verification > Plugin', () => {
provider = await VerificationProviderModel.create({ provider = await VerificationProviderModel.create({
id: 'fake1', id: 'fake1',
type: 'fake', type: 'fake',
default: true
}); });
}); });
@ -40,7 +41,6 @@ describe('verification > Plugin', () => {
describe('auto intercept', () => { describe('auto intercept', () => {
beforeEach(async () => { beforeEach(async () => {
plugin.interceptors.register('authors:create', { plugin.interceptors.register('authors:create', {
provider: 'fake1',
getReceiver(ctx) { getReceiver(ctx) {
return ctx.action.params.values.phone; return ctx.action.params.values.phone;
}, },
@ -103,7 +103,6 @@ describe('verification > Plugin', () => {
beforeEach(async () => { beforeEach(async () => {
plugin.interceptors.register('authors:create', { plugin.interceptors.register('authors:create', {
manual: true, manual: true,
provider: 'fake1',
getReceiver(ctx) { getReceiver(ctx) {
return ctx.action.params.values.phone; return ctx.action.params.values.phone;
}, },
@ -131,7 +130,6 @@ describe('verification > Plugin', () => {
describe('validate', () => { describe('validate', () => {
beforeEach(async () => { beforeEach(async () => {
plugin.interceptors.register('authors:create', { plugin.interceptors.register('authors:create', {
provider: 'fake1',
getReceiver(ctx) { getReceiver(ctx) {
return ctx.action.params.values.phone; return ctx.action.params.values.phone;
}, },

View File

@ -21,7 +21,9 @@ export async function create(context: Context, next: Next) {
const ProviderRepo = context.db.getRepository('verifications_providers'); const ProviderRepo = context.db.getRepository('verifications_providers');
const providerItem = await ProviderRepo.findOne({ const providerItem = await ProviderRepo.findOne({
filterByTk: interceptor.provider filter: {
default: true
}
}); });
if (!providerItem) { if (!providerItem) {
console.error(`[verification] no provider for action (${values.type}) provided`); console.error(`[verification] no provider for action (${values.type}) provided`);
@ -30,7 +32,7 @@ export async function create(context: Context, next: Next) {
const receiver = interceptor.getReceiver(context); const receiver = interceptor.getReceiver(context);
if (!receiver) { if (!receiver) {
return context.throw(400, { code: 'InvalidReceiver', message: 'Invalid receiver' }); return context.throw(400, { code: 'InvalidReceiver', message: context.t('Not a valid cellphone number, please re-enter', {ns: namespace }) });
} }
const VerificationModel = context.db.getModel('verifications'); const VerificationModel = context.db.getModel('verifications');
const record = await VerificationModel.findOne({ const record = await VerificationModel.findOne({
@ -68,7 +70,7 @@ export async function create(context: Context, next: Next) {
switch (error.name) { switch (error.name) {
case 'InvalidReceiver': case 'InvalidReceiver':
// TODO: message should consider email and other providers, maybe use "receiver" // TODO: message should consider email and other providers, maybe use "receiver"
return context.throw(400, context.t('Not a valid cellphone number, please re-enter', {ns: namespace })); return context.throw(400, { code: 'InvalidReceiver', message: context.t('Not a valid cellphone number, please re-enter', {ns: namespace })});
case 'RateLimit': case 'RateLimit':
return context.throw(429, context.t('You are trying so frequently, please slow down', { ns: namespace })); return context.throw(429, context.t('You are trying so frequently, please slow down', { ns: namespace }));
default: default:

View File

@ -17,6 +17,10 @@ export default {
{ {
type: 'jsonb', type: 'jsonb',
name: 'options' name: 'options'
},
{
type: 'radio',
name: 'default'
} }
] ]
}; };

View File

@ -0,0 +1,5 @@
export * from './constants';
export { Provider } from './providers';
export { Interceptor, default } from './Plugin';
export const namespace = require('../../package.json').name;

View File

@ -2,5 +2,6 @@ export default {
'Verification send failed, please try later or contact to administrator': '验证码发送失败,请稍后重试或联系管理员', 'Verification send failed, please try later or contact to administrator': '验证码发送失败,请稍后重试或联系管理员',
'Not a valid cellphone number, please re-enter': '不是有效的手机号,请重新输入', 'Not a valid cellphone number, please re-enter': '不是有效的手机号,请重新输入',
"Please don't retry in {{time}} seconds": '请 {{time}} 秒后再试', "Please don't retry in {{time}} seconds": '请 {{time}} 秒后再试',
'You are trying so frequently, please slow down': '您的操作太频繁,请稍后再试' 'You are trying so frequently, please slow down': '您的操作太频繁,请稍后再试',
'Verification code is invalid': '无效的验证码'
}; };

View File

@ -49,6 +49,7 @@ export default class extends Provider {
console.error(body); console.error(body);
return Promise.reject(err); return Promise.reject(err);
// case 'isp.RAM_PERMISSION_DENY':
default: default:
// should not let user to know // should not let user to know
console.error(body); console.error(body);

View File

@ -13,7 +13,6 @@ import { ExecutionLink } from './ExecutionLink';
import { lang } from './locale'; import { lang } from './locale';
export const WorkflowPane = () => { export const WorkflowPane = () => {
return ( return (
<Card bordered={false}> <Card bordered={false}>