From 7b5277fb2ae637869de39886d6dd10f8130f212e Mon Sep 17 00:00:00 2001 From: Junyi Date: Mon, 28 Nov 2022 00:41:58 -0800 Subject: [PATCH] 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 --- docker/app-postgres/docker-compose.yml | 7 +- .../app/client/src/plugins/verification.ts | 1 + packages/core/client/src/locale/zh_CN.ts | 1 + packages/core/client/src/user/EditProfile.tsx | 16 + .../file-manager/src/client/FileStorage.tsx | 9 +- .../plugins/users/src/collections/users.ts | 4 +- packages/plugins/users/src/server.ts | 150 ++++---- packages/plugins/verification/client.d.ts | 4 + packages/plugins/verification/client.js | 30 ++ packages/plugins/verification/server.d.ts | 4 + packages/plugins/verification/server.js | 30 ++ .../src/client/ProviderOptions.tsx | 22 ++ .../verification/src/client/Shortcut.tsx | 20 ++ .../src/client/VerificationProviders.tsx | 19 + .../plugins/verification/src/client/index.tsx | 41 +++ .../verification/src/client/locale/index.ts | 16 + .../verification/src/client/locale/zh-CN.ts | 13 + .../src/client/providerTypes/index.ts | 9 + .../src/client/providerTypes/sms-aliyun.ts | 39 +++ .../src/client/schemas/providers.ts | 330 ++++++++++++++++++ packages/plugins/verification/src/index.ts | 6 +- .../verification/src/{ => server}/Plugin.ts | 14 +- .../src/{ => server}/__tests__/Plugin.test.ts | 4 +- .../__tests__/collections/authors.ts | 0 .../src/{ => server}/__tests__/index.ts | 0 .../src/{ => server}/actions/index.ts | 0 .../src/{ => server}/actions/verifications.ts | 8 +- .../{ => server}/collections/verifications.ts | 0 .../collections/verifications_providers.ts | 4 + .../src/{ => server}/constants.ts | 0 .../plugins/verification/src/server/index.ts | 5 + .../src/{ => server}/locale/index.ts | 0 .../src/{ => server}/locale/zh-CN.ts | 3 +- .../src/{ => server}/providers/index.ts | 0 .../src/{ => server}/providers/sms-aliyun.ts | 1 + .../workflow/src/client/WorkflowShortcut.tsx | 1 - 36 files changed, 707 insertions(+), 104 deletions(-) create mode 100644 packages/app/client/src/plugins/verification.ts create mode 100644 packages/plugins/verification/client.d.ts create mode 100644 packages/plugins/verification/client.js create mode 100644 packages/plugins/verification/server.d.ts create mode 100644 packages/plugins/verification/server.js create mode 100644 packages/plugins/verification/src/client/ProviderOptions.tsx create mode 100644 packages/plugins/verification/src/client/Shortcut.tsx create mode 100644 packages/plugins/verification/src/client/VerificationProviders.tsx create mode 100644 packages/plugins/verification/src/client/index.tsx create mode 100644 packages/plugins/verification/src/client/locale/index.ts create mode 100644 packages/plugins/verification/src/client/locale/zh-CN.ts create mode 100644 packages/plugins/verification/src/client/providerTypes/index.ts create mode 100644 packages/plugins/verification/src/client/providerTypes/sms-aliyun.ts create mode 100644 packages/plugins/verification/src/client/schemas/providers.ts rename packages/plugins/verification/src/{ => server}/Plugin.ts (89%) rename packages/plugins/verification/src/{ => server}/__tests__/Plugin.test.ts (98%) rename packages/plugins/verification/src/{ => server}/__tests__/collections/authors.ts (100%) rename packages/plugins/verification/src/{ => server}/__tests__/index.ts (100%) rename packages/plugins/verification/src/{ => server}/actions/index.ts (100%) rename packages/plugins/verification/src/{ => server}/actions/verifications.ts (92%) rename packages/plugins/verification/src/{ => server}/collections/verifications.ts (100%) rename packages/plugins/verification/src/{ => server}/collections/verifications_providers.ts (84%) rename packages/plugins/verification/src/{ => server}/constants.ts (100%) create mode 100644 packages/plugins/verification/src/server/index.ts rename packages/plugins/verification/src/{ => server}/locale/index.ts (100%) rename packages/plugins/verification/src/{ => server}/locale/zh-CN.ts (81%) rename packages/plugins/verification/src/{ => server}/providers/index.ts (100%) rename packages/plugins/verification/src/{ => server}/providers/sms-aliyun.ts (97%) diff --git a/docker/app-postgres/docker-compose.yml b/docker/app-postgres/docker-compose.yml index 37790dc0f..d3c12c903 100644 --- a/docker/app-postgres/docker-compose.yml +++ b/docker/app-postgres/docker-compose.yml @@ -1,7 +1,4 @@ version: "3" -networks: - nocobase: - driver: bridge services: app: image: nocobase/nocobase:0.8.0-alpha.13 @@ -23,10 +20,8 @@ services: postgres: image: postgres:10 restart: always - networks: - - nocobase command: postgres -c wal_level=logical environment: POSTGRES_USER: nocobase POSTGRES_DB: nocobase - POSTGRES_PASSWORD: nocobase \ No newline at end of file + POSTGRES_PASSWORD: nocobase diff --git a/packages/app/client/src/plugins/verification.ts b/packages/app/client/src/plugins/verification.ts new file mode 100644 index 000000000..e987ff3c9 --- /dev/null +++ b/packages/app/client/src/plugins/verification.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-verification/client'; diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 554885439..12aac6676 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -425,6 +425,7 @@ export default { 'DESC': '降序', 'Add sort field': '添加排序字段', 'ID': 'ID', + 'Identifier for program usage. Support letters, numbers and underscores, must start with an letter.': '用于程序使用的标识符,支持字母、数字和下划线,必须以字母开头。', 'Drawer': '抽屉', 'Dialog': '对话框', 'Delete action': '删除操作', diff --git a/packages/core/client/src/user/EditProfile.tsx b/packages/core/client/src/user/EditProfile.tsx index ff4d7f48d..03161d02a 100644 --- a/packages/core/client/src/user/EditProfile.tsx +++ b/packages/core/client/src/user/EditProfile.tsx @@ -72,6 +72,22 @@ const schema: ISchema = { 'x-component': 'Input', 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: { 'x-component': 'Action.Drawer.Footer', type: 'void', diff --git a/packages/plugins/file-manager/src/client/FileStorage.tsx b/packages/plugins/file-manager/src/client/FileStorage.tsx index 9ab11cf16..46a260cff 100644 --- a/packages/plugins/file-manager/src/client/FileStorage.tsx +++ b/packages/plugins/file-manager/src/client/FileStorage.tsx @@ -6,19 +6,12 @@ import { useTranslation } from 'react-i18next'; import { storageSchema } from './schemas/storage'; import { StorageOptions } from './StorageOptions'; -const schema = { - type: 'object', - properties: { - [uid()]: storageSchema, - }, -}; - export const FileStoragePane = () => { const [visible, setVisible] = useState(false); const { t } = useTranslation(); return ( - + ); }; diff --git a/packages/plugins/users/src/collections/users.ts b/packages/plugins/users/src/collections/users.ts index 6d4828261..17efb0557 100644 --- a/packages/plugins/users/src/collections/users.ts +++ b/packages/plugins/users/src/collections/users.ts @@ -38,7 +38,7 @@ export default { title: '{{t("Email")}}', 'x-component': 'Input', 'x-validator': 'email', - require: true, + required: true, }, }, { @@ -51,7 +51,7 @@ export default { title: '{{t("Phone")}}', 'x-component': 'Input', 'x-validator': 'phone', - require: true, + required: true, }, }, { diff --git a/packages/plugins/users/src/server.ts b/packages/plugins/users/src/server.ts index 96f66567d..808c8bb99 100644 --- a/packages/plugins/users/src/server.ts +++ b/packages/plugins/users/src/server.ts @@ -97,6 +97,8 @@ export default class UsersPlugin extends Plugin { publicActions.forEach((action) => this.app.acl.allow('users', action)); loggedInActions.forEach((action) => this.app.acl.allow('users', action, 'loggedIn')); + + this.app.on('beforeStart', () => this.initVerification()); } async load() { @@ -113,79 +115,6 @@ export default class UsersPlugin extends Plugin { }); 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 = {}) { @@ -218,4 +147,79 @@ export default class UsersPlugin extends Plugin { 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(); + }), + ); + } } diff --git a/packages/plugins/verification/client.d.ts b/packages/plugins/verification/client.d.ts new file mode 100644 index 000000000..765db9222 --- /dev/null +++ b/packages/plugins/verification/client.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/client'; +export { default } from './lib/client'; + diff --git a/packages/plugins/verification/client.js b/packages/plugins/verification/client.js new file mode 100644 index 000000000..238820257 --- /dev/null +++ b/packages/plugins/verification/client.js @@ -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]; + } + }); +}); diff --git a/packages/plugins/verification/server.d.ts b/packages/plugins/verification/server.d.ts new file mode 100644 index 000000000..e70edb928 --- /dev/null +++ b/packages/plugins/verification/server.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/server'; +export { default } from './lib/server'; + diff --git a/packages/plugins/verification/server.js b/packages/plugins/verification/server.js new file mode 100644 index 000000000..d06a7eb92 --- /dev/null +++ b/packages/plugins/verification/server.js @@ -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]; + } + }); +}); diff --git a/packages/plugins/verification/src/client/ProviderOptions.tsx b/packages/plugins/verification/src/client/ProviderOptions.tsx new file mode 100644 index 000000000..67a74ecf2 --- /dev/null +++ b/packages/plugins/verification/src/client/ProviderOptions.tsx @@ -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(); + const [s, setSchema] = useState(new Schema({})); + useEffect(() => { + form.clearFormGraph('options.*'); + setSchema(new Schema(providerTypes.get(form.values.type) || {})); + }, [form.values.type]); + return ( + + + + ); +}); diff --git a/packages/plugins/verification/src/client/Shortcut.tsx b/packages/plugins/verification/src/client/Shortcut.tsx new file mode 100644 index 000000000..faca2c388 --- /dev/null +++ b/packages/plugins/verification/src/client/Shortcut.tsx @@ -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 ( + } + title={t('Verification', { ns: NAMESPACE })} + onClick={() => { + history.push('/admin/settings/verification/providers'); + }} + /> + ); +}; diff --git a/packages/plugins/verification/src/client/VerificationProviders.tsx b/packages/plugins/verification/src/client/VerificationProviders.tsx new file mode 100644 index 000000000..18f730780 --- /dev/null +++ b/packages/plugins/verification/src/client/VerificationProviders.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/plugins/verification/src/client/index.tsx b/packages/plugins/verification/src/client/index.tsx new file mode 100644 index 000000000..b59c4e067 --- /dev/null +++ b/packages/plugins/verification/src/client/index.tsx @@ -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 ( + + + {props.children} + + + ); +}; + + diff --git a/packages/plugins/verification/src/client/locale/index.ts b/packages/plugins/verification/src/client/locale/index.ts new file mode 100644 index 000000000..e8f0d5007 --- /dev/null +++ b/packages/plugins/verification/src/client/locale/index.ts @@ -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); +} diff --git a/packages/plugins/verification/src/client/locale/zh-CN.ts b/packages/plugins/verification/src/client/locale/zh-CN.ts new file mode 100644 index 000000000..e1ba7540b --- /dev/null +++ b/packages/plugins/verification/src/client/locale/zh-CN.ts @@ -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': '模板代码', +}; diff --git a/packages/plugins/verification/src/client/providerTypes/index.ts b/packages/plugins/verification/src/client/providerTypes/index.ts new file mode 100644 index 000000000..ad9628e0b --- /dev/null +++ b/packages/plugins/verification/src/client/providerTypes/index.ts @@ -0,0 +1,9 @@ +import { ISchema } from '@formily/react'; +import { Registry } from "@nocobase/utils/client"; +import SMSAliyun from './sms-aliyun'; + +const providerTypes: Registry = new Registry(); + +providerTypes.register('sms-aliyun', SMSAliyun); + +export default providerTypes; diff --git a/packages/plugins/verification/src/client/providerTypes/sms-aliyun.ts b/packages/plugins/verification/src/client/providerTypes/sms-aliyun.ts new file mode 100644 index 000000000..4d29c546c --- /dev/null +++ b/packages/plugins/verification/src/client/providerTypes/sms-aliyun.ts @@ -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; diff --git a/packages/plugins/verification/src/client/schemas/providers.ts b/packages/plugins/verification/src/client/schemas/providers.ts new file mode 100644 index 000000000..9f59533de --- /dev/null +++ b/packages/plugins/verification/src/client/schemas/providers.ts @@ -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}}', + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/verification/src/index.ts b/packages/plugins/verification/src/index.ts index 0f8a6e2f7..7ddad5814 100644 --- a/packages/plugins/verification/src/index.ts +++ b/packages/plugins/verification/src/index.ts @@ -1,5 +1 @@ -export * from './constants'; -export { Provider } from './providers'; -export { Interceptor, default } from './Plugin'; - -export const namespace = require('../package.json').name; +export { default } from './server'; diff --git a/packages/plugins/verification/src/Plugin.ts b/packages/plugins/verification/src/server/Plugin.ts similarity index 89% rename from packages/plugins/verification/src/Plugin.ts rename to packages/plugins/verification/src/server/Plugin.ts index 6dbec4579..c487a7c75 100644 --- a/packages/plugins/verification/src/Plugin.ts +++ b/packages/plugins/verification/src/server/Plugin.ts @@ -14,7 +14,6 @@ import initProviders, { Provider } from './providers'; export interface Interceptor { manual?: boolean; - provider: string; expiresIn?: number; getReceiver(ctx): string; getCode?(ctx): string; @@ -56,7 +55,7 @@ export default class VerificationPlugin extends Plugin { }); 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 @@ -103,6 +102,7 @@ export default class VerificationPlugin extends Plugin { sign: INIT_ALI_SMS_VERIFY_CODE_SIGN, 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_providers', '*', 'allowConfigure'); + } + + async getDefault() { + const providerRepo = this.db.getRepository('verifications_providers'); + return providerRepo.findOne({ + filter: { + default: true, + } + }); } } diff --git a/packages/plugins/verification/src/__tests__/Plugin.test.ts b/packages/plugins/verification/src/server/__tests__/Plugin.test.ts similarity index 98% rename from packages/plugins/verification/src/__tests__/Plugin.test.ts rename to packages/plugins/verification/src/server/__tests__/Plugin.test.ts index b98285dca..d27ea159c 100644 --- a/packages/plugins/verification/src/__tests__/Plugin.test.ts +++ b/packages/plugins/verification/src/server/__tests__/Plugin.test.ts @@ -32,6 +32,7 @@ describe('verification > Plugin', () => { provider = await VerificationProviderModel.create({ id: 'fake1', type: 'fake', + default: true }); }); @@ -40,7 +41,6 @@ describe('verification > Plugin', () => { describe('auto intercept', () => { beforeEach(async () => { plugin.interceptors.register('authors:create', { - provider: 'fake1', getReceiver(ctx) { return ctx.action.params.values.phone; }, @@ -103,7 +103,6 @@ describe('verification > Plugin', () => { beforeEach(async () => { plugin.interceptors.register('authors:create', { manual: true, - provider: 'fake1', getReceiver(ctx) { return ctx.action.params.values.phone; }, @@ -131,7 +130,6 @@ describe('verification > Plugin', () => { describe('validate', () => { beforeEach(async () => { plugin.interceptors.register('authors:create', { - provider: 'fake1', getReceiver(ctx) { return ctx.action.params.values.phone; }, diff --git a/packages/plugins/verification/src/__tests__/collections/authors.ts b/packages/plugins/verification/src/server/__tests__/collections/authors.ts similarity index 100% rename from packages/plugins/verification/src/__tests__/collections/authors.ts rename to packages/plugins/verification/src/server/__tests__/collections/authors.ts diff --git a/packages/plugins/verification/src/__tests__/index.ts b/packages/plugins/verification/src/server/__tests__/index.ts similarity index 100% rename from packages/plugins/verification/src/__tests__/index.ts rename to packages/plugins/verification/src/server/__tests__/index.ts diff --git a/packages/plugins/verification/src/actions/index.ts b/packages/plugins/verification/src/server/actions/index.ts similarity index 100% rename from packages/plugins/verification/src/actions/index.ts rename to packages/plugins/verification/src/server/actions/index.ts diff --git a/packages/plugins/verification/src/actions/verifications.ts b/packages/plugins/verification/src/server/actions/verifications.ts similarity index 92% rename from packages/plugins/verification/src/actions/verifications.ts rename to packages/plugins/verification/src/server/actions/verifications.ts index 301c18e1f..055e1763e 100644 --- a/packages/plugins/verification/src/actions/verifications.ts +++ b/packages/plugins/verification/src/server/actions/verifications.ts @@ -21,7 +21,9 @@ export async function create(context: Context, next: Next) { const ProviderRepo = context.db.getRepository('verifications_providers'); const providerItem = await ProviderRepo.findOne({ - filterByTk: interceptor.provider + filter: { + default: true + } }); if (!providerItem) { 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); 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 record = await VerificationModel.findOne({ @@ -68,7 +70,7 @@ export async function create(context: Context, next: Next) { switch (error.name) { case 'InvalidReceiver': // 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': return context.throw(429, context.t('You are trying so frequently, please slow down', { ns: namespace })); default: diff --git a/packages/plugins/verification/src/collections/verifications.ts b/packages/plugins/verification/src/server/collections/verifications.ts similarity index 100% rename from packages/plugins/verification/src/collections/verifications.ts rename to packages/plugins/verification/src/server/collections/verifications.ts diff --git a/packages/plugins/verification/src/collections/verifications_providers.ts b/packages/plugins/verification/src/server/collections/verifications_providers.ts similarity index 84% rename from packages/plugins/verification/src/collections/verifications_providers.ts rename to packages/plugins/verification/src/server/collections/verifications_providers.ts index 6e37f643d..fa4c4b075 100644 --- a/packages/plugins/verification/src/collections/verifications_providers.ts +++ b/packages/plugins/verification/src/server/collections/verifications_providers.ts @@ -17,6 +17,10 @@ export default { { type: 'jsonb', name: 'options' + }, + { + type: 'radio', + name: 'default' } ] }; diff --git a/packages/plugins/verification/src/constants.ts b/packages/plugins/verification/src/server/constants.ts similarity index 100% rename from packages/plugins/verification/src/constants.ts rename to packages/plugins/verification/src/server/constants.ts diff --git a/packages/plugins/verification/src/server/index.ts b/packages/plugins/verification/src/server/index.ts new file mode 100644 index 000000000..5f205b2f2 --- /dev/null +++ b/packages/plugins/verification/src/server/index.ts @@ -0,0 +1,5 @@ +export * from './constants'; +export { Provider } from './providers'; +export { Interceptor, default } from './Plugin'; + +export const namespace = require('../../package.json').name; diff --git a/packages/plugins/verification/src/locale/index.ts b/packages/plugins/verification/src/server/locale/index.ts similarity index 100% rename from packages/plugins/verification/src/locale/index.ts rename to packages/plugins/verification/src/server/locale/index.ts diff --git a/packages/plugins/verification/src/locale/zh-CN.ts b/packages/plugins/verification/src/server/locale/zh-CN.ts similarity index 81% rename from packages/plugins/verification/src/locale/zh-CN.ts rename to packages/plugins/verification/src/server/locale/zh-CN.ts index 213a21608..a05b96826 100644 --- a/packages/plugins/verification/src/locale/zh-CN.ts +++ b/packages/plugins/verification/src/server/locale/zh-CN.ts @@ -2,5 +2,6 @@ export default { 'Verification send failed, please try later or contact to administrator': '验证码发送失败,请稍后重试或联系管理员', 'Not a valid cellphone number, please re-enter': '不是有效的手机号,请重新输入', "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': '无效的验证码' }; diff --git a/packages/plugins/verification/src/providers/index.ts b/packages/plugins/verification/src/server/providers/index.ts similarity index 100% rename from packages/plugins/verification/src/providers/index.ts rename to packages/plugins/verification/src/server/providers/index.ts diff --git a/packages/plugins/verification/src/providers/sms-aliyun.ts b/packages/plugins/verification/src/server/providers/sms-aliyun.ts similarity index 97% rename from packages/plugins/verification/src/providers/sms-aliyun.ts rename to packages/plugins/verification/src/server/providers/sms-aliyun.ts index f2f0c1fcd..4ed440dfb 100644 --- a/packages/plugins/verification/src/providers/sms-aliyun.ts +++ b/packages/plugins/verification/src/server/providers/sms-aliyun.ts @@ -49,6 +49,7 @@ export default class extends Provider { console.error(body); return Promise.reject(err); + // case 'isp.RAM_PERMISSION_DENY': default: // should not let user to know console.error(body); diff --git a/packages/plugins/workflow/src/client/WorkflowShortcut.tsx b/packages/plugins/workflow/src/client/WorkflowShortcut.tsx index 885393ae0..ae070ba61 100644 --- a/packages/plugins/workflow/src/client/WorkflowShortcut.tsx +++ b/packages/plugins/workflow/src/client/WorkflowShortcut.tsx @@ -13,7 +13,6 @@ import { ExecutionLink } from './ExecutionLink'; import { lang } from './locale'; - export const WorkflowPane = () => { return (