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:
parent
a0910f0e2e
commit
7b5277fb2a
@ -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
|
||||||
|
1
packages/app/client/src/plugins/verification.ts
Normal file
1
packages/app/client/src/plugins/verification.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@nocobase/plugin-verification/client';
|
@ -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': '删除操作',
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
4
packages/plugins/verification/client.d.ts
vendored
Normal file
4
packages/plugins/verification/client.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
export * from './lib/client';
|
||||||
|
export { default } from './lib/client';
|
||||||
|
|
30
packages/plugins/verification/client.js
Normal file
30
packages/plugins/verification/client.js
Normal 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];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
4
packages/plugins/verification/server.d.ts
vendored
Normal file
4
packages/plugins/verification/server.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
export * from './lib/server';
|
||||||
|
export { default } from './lib/server';
|
||||||
|
|
30
packages/plugins/verification/server.js
Normal file
30
packages/plugins/verification/server.js
Normal 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];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
22
packages/plugins/verification/src/client/ProviderOptions.tsx
Normal file
22
packages/plugins/verification/src/client/ProviderOptions.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
20
packages/plugins/verification/src/client/Shortcut.tsx
Normal file
20
packages/plugins/verification/src/client/Shortcut.tsx
Normal 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');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
41
packages/plugins/verification/src/client/index.tsx
Normal file
41
packages/plugins/verification/src/client/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
16
packages/plugins/verification/src/client/locale/index.ts
Normal file
16
packages/plugins/verification/src/client/locale/index.ts
Normal 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);
|
||||||
|
}
|
13
packages/plugins/verification/src/client/locale/zh-CN.ts
Normal file
13
packages/plugins/verification/src/client/locale/zh-CN.ts
Normal 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': '模板代码',
|
||||||
|
};
|
@ -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;
|
@ -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;
|
330
packages/plugins/verification/src/client/schemas/providers.ts
Normal file
330
packages/plugins/verification/src/client/schemas/providers.ts
Normal 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}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -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;
|
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
},
|
},
|
@ -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:
|
@ -17,6 +17,10 @@ export default {
|
|||||||
{
|
{
|
||||||
type: 'jsonb',
|
type: 'jsonb',
|
||||||
name: 'options'
|
name: 'options'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'radio',
|
||||||
|
name: 'default'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
5
packages/plugins/verification/src/server/index.ts
Normal file
5
packages/plugins/verification/src/server/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './constants';
|
||||||
|
export { Provider } from './providers';
|
||||||
|
export { Interceptor, default } from './Plugin';
|
||||||
|
|
||||||
|
export const namespace = require('../../package.json').name;
|
@ -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': '无效的验证码'
|
||||||
};
|
};
|
@ -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);
|
@ -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}>
|
||||||
|
Loading…
Reference in New Issue
Block a user