diff --git a/packages/plugins/@hera/plugin-core/src/client/features/demo/index.tsx b/packages/plugins/@hera/plugin-core/src/client/features/demo/index.tsx deleted file mode 100644 index 883c10210..000000000 --- a/packages/plugins/@hera/plugin-core/src/client/features/demo/index.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import React, { useContext, useEffect } from 'react'; -import { - CollectionProvider, - css, - Grid, - i18n, - Plugin, - SchemaComponent, - SchemaComponentContext, - useAPIClient, - useRequest, - useTranslation, - useViewport, -} from '@tachybase/client'; -import { FormItem, FormLayout } from '@tachybase/components'; -import { formSchema } from '@tachybase/plugin-multi-app-manager/client'; -import { createForm, useField, useForm } from '@tachybase/schema'; - -import { Alert } from 'antd'; - -const ft = 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js'; -const sceneId = 'jruas0rs'; -const prefix = '1bze12'; -const changeLang = (locale = 'zh-CN') => { - const e = { language: 'cn', region: 'cn' }; - return { 'zh-CN': e, 'en-US': { language: 'en', region: 'sgp' } }[locale] || e; -}; -const Captcha = (t) => { - const { request, onSuccess, onError, language = 'zh-CN' } = t; - const getInstance = () => {}; - const captchaVerifyCallback = async (param) => { - try { - return await request({ captchaVerifyParam: param, sceneId }), { captchaResult: true }; - } catch (s) { - return onError == null || onError(s), { captchaResult: false }; - } - }; - const onBizResultCallback = () => { - onSuccess == null || onSuccess(); - }; - - useEffect(() => { - const dynamicScript = document.createElement('script'); - dynamicScript.type = 'text/javascript'; - dynamicScript.src = ft; - dynamicScript.onload = () => { - // @ts-ignore - window.initAliyunCaptcha({ - SceneId: sceneId, - prefix: prefix, - mode: 'embed', - element: '#captcha-element', - button: '#deploy', - captchaVerifyCallback: captchaVerifyCallback, - onBizResultCallback: onBizResultCallback, - getInstance: getInstance, - slideStyle: { width: 360, height: 40 }, - ...changeLang(language), - }); - }; - document.head.appendChild(dynamicScript); - }); - return ( -
- ); -}; -i18n.addResources('zh-CN', 'demo', { - 'Deploy a demo site': '开通 tachybase 体验站点', - Email: '电子邮箱', - Yes: '是', - No: '否', - 'Application template': '应用模板', - 'Are you a developer': '你是一名开发者吗', - 'Deploy Now': '立即开通', - Industry: '所在行业', - 'Please fill in the necessary information and the demo site will be automatically created within one minute.': - '请填写必要的信息,体验站将在一分钟内自动创建成功。', - 'Demo site deployed successfully!': '体验站点开通成功!', - 'What you want to do with tachybase': '你希望用 tachybase 来做什么', - 'Used to receive demo site information, please be sure to fill in correctly': '用于接收体验站点信息,请务必正确填写', - 'Please briefly describe the purpose': '请简要描述用途,方便我们后续针对性的开发', - 'Please briefly describe your industry': '请简要描述你所在的行业,方便我们后续针对性的开发', -}); -const DemoProvider = React.createContext(null); -function useApplicationAction() { - const api = useAPIClient(); - const form = useForm(); - const field = useField(); - const [, setSuccess] = useContext(DemoProvider); - return { - async run() { - await form.submit(); - field.data = field.data || {}; - field.data.loading = true; - try { - await api.request({ url: 'applicationForms:create', method: 'post', data: form.values }); - field.data.loading = false; - setSuccess(true); - } catch (a) { - field.data.loading = false; - } - }, - }; -} -export function DemoApplyForm() { - useViewport(); - const schemaComponentContext = React.useContext(SchemaComponentContext); - const [isSuccess, setSuccess] = React.useState(false); - const [loading, setLoading] = React.useState(false); - const api = useAPIClient(); - // @ts-ignore - const { t, i18n } = useTranslation('demo'); - const form = createForm(); - const { data, loading: isLoading } = useRequest({ url: 'applicationForms:getTemplateNameUiSchema' }); - const onRequest = async (requestData) => { - await form.validate(); - await api.request({ url: 'applicationForms:create', method: 'post', data: { ...form.values, ...requestData } }); - }; - return isLoading ? null : ( -
- {[t('Deploy a demo site'), ' - tachybase']} - {isSuccess ? ( - - 你将在几秒钟内收到邮件,其中包含体验站点的地址、账号和 密码。 -
- 该站点仅用于预览,你在体验站点所作的操作、所存储的数据 可能会丢失。 -
- 该站点将会在 48 小时后失效,之后你可以重新申请体验站点。 -
- ) : ( -
- You will receive an email in a few seconds with the URL, username and password of the demo site. -
- The site is for preview purposes only, your actions and data stored on the site may be lost. -
- The site will expire after 48 hours, after which you can reapply for the site. -
- ) - } - type="success" - /> - ) : ( -
- - - - ({ form: form }), - properties: { - layout: { - type: 'void', - 'x-component': 'FormLayout', - 'x-component-props': { layout: 'vertical' }, - properties: { div: (data as any)?.data?.schema }, - }, - captcha: { - type: 'void', - 'x-component': 'Captcha', - 'x-component-props': { - request: onRequest, - language: i18n.language, - onSuccess: () => { - setSuccess(true), setLoading(false); - }, - onError: () => { - setLoading(false); - }, - }, - }, - actions: { - type: 'void', - 'x-component': 'div', - 'x-component-props': { id: 'deploy' }, - properties: { - submit: { - type: 'void', - title: t('Deploy Now'), - 'x-component': 'Action', - 'x-component-props': { - block: true, - loading: loading, - type: 'primary', - style: { width: '100%' }, - }, - }, - }, - }, - }, - }} - /> - - - -
- )} -
- ); -} -export class PluginDemo extends Plugin { - async load() { - formSchema.properties['options.isTemplate'] = { - 'x-component': 'Checkbox', - 'x-decorator': 'FormItem', - 'x-content': 'As an application template', - }; - formSchema.properties['options.persistent'] = { - 'x-component': 'Checkbox', - 'x-decorator': 'FormItem', - 'x-content': 'Persistent', - }; - this.router.add('new', { path: '/new', Component: DemoApplyForm }); - } -} diff --git a/packages/plugins/@hera/plugin-core/src/server/features/demo/collections/application-from.ts b/packages/plugins/@hera/plugin-core/src/server/features/demo/collections/application-from.ts deleted file mode 100644 index b24ba4adf..000000000 --- a/packages/plugins/@hera/plugin-core/src/server/features/demo/collections/application-from.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { defineCollection } from '@tachybase/database'; - -export default defineCollection({ - name: 'applicationForms', - title: '申请表单', - fields: [ - { - name: 'application', - type: 'belongsTo', - target: 'applications', - foreignKey: 'applicationName', - targetKey: 'name', - onDelete: 'SET NULL', - }, - { - name: 'templateName', - type: 'string', - interface: 'select', - uiSchema: { - title: '模板', - type: 'string', - 'x-component': 'Select', - 'x-decorator': 'FormItem', - }, - }, - { - name: 'url', - type: 'string', - }, - { - name: 'email', - type: 'string', - interface: 'input', - uiSchema: { - type: 'string', - title: '邮箱', - 'x-component': 'Input', - }, - }, - { - name: 'note', - type: 'string', - interface: 'textarea', - uiSchema: { - type: 'string', - title: '你希望用来做什么', - 'x-component': 'Input.TextArea', - }, - }, - { - name: 'industry', - interface: 'input', - type: 'string', - uiSchema: { - title: '所在行业', - type: 'string', - 'x-component': 'Input', - 'x-decorator': 'FormItem', - }, - }, - { - name: 'isDeveloper', - interface: 'radioGroup', - type: 'string', - uiSchema: { - title: '你是一名开发者吗?', - type: 'string', - 'x-component': 'Radio.Group', - 'x-decorator': 'FormItem', - enum: [ - { - value: 'yes', - label: '是', - color: 'geekblue', - }, - { - value: 'no', - label: '不是', - color: 'cyan', - }, - ], - }, - }, - ], -}); diff --git a/packages/plugins/@hera/plugin-core/src/server/features/demo/create-notification.ts b/packages/plugins/@hera/plugin-core/src/server/features/demo/create-notification.ts deleted file mode 100644 index d89fc676a..000000000 --- a/packages/plugins/@hera/plugin-core/src/server/features/demo/create-notification.ts +++ /dev/null @@ -1,50 +0,0 @@ -function notificationTemplate(lang) { - return { - 'zh-CN': { - name: 'demo', - subject: 'TachyBase 体验站点开通成功!', - body: ` -

地址:https://<%= appId %>.v7.demo-cn.tachybase.com/

-

账号:admin@tachybase.com

-

密码:admin123

-

该站点仅用于体验,你在体验站点所做的操作、所存储的数据可能会丢失。

-

该站点将会在 48 小时后失效,之后你可以重新申请体验站点。

- `, - receiver_options: {}, - }, - 'en-US': { - name: 'demo', - subject: 'TachyBase demo site deployed successfully!', - body: ` -

URL: https://<%= appId %>.v7.demo.tachybase.com/

-

Account: admin@tachybase.com

-

Password: admin123

-

The site is for preview purposes only, your actions and data stored on the site may be lost.

-

The site will expire after 48 hours, after which you can reapply for the site.

- `, - receiver_options: {}, - }, - }[lang || 'en-US']; -} - -export async function createNotificationRecord(app) { - await app.db.getRepository('notifications').create({ - values: { - ...notificationTemplate(app.i18n.language || 'en-US'), - service: { - type: 'email', - title: '阿里云邮件推送', - options: { - auth: { - pass: process.env.ALIYUN_MAIL_AUTH_PASS, - user: 'noreply@demo.tachybase.com', - }, - from: 'TachyBase', - host: 'smtpdm.aliyun.com', - port: 465, - secure: true, - }, - }, - }, - }); -} diff --git a/packages/plugins/@hera/plugin-core/src/server/features/demo/plugin.ts b/packages/plugins/@hera/plugin-core/src/server/features/demo/plugin.ts deleted file mode 100644 index f320eef20..000000000 --- a/packages/plugins/@hera/plugin-core/src/server/features/demo/plugin.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { resolve } from 'path'; -import { CollectionRepository } from '@tachybase/plugin-collection-manager'; -import PluginMultiAppManager from '@tachybase/plugin-multi-app-manager'; -import { UiSchemaRepository } from '@tachybase/plugin-ui-schema-storage'; -import { AppSupervisor, Gateway, Plugin } from '@tachybase/server'; -import { uid } from '@tachybase/utils'; - -import { nanoid } from 'nanoid'; -import { Sequelize } from 'sequelize'; - -import { createNotificationRecord } from './create-notification'; -import { createAliCaptchaClient, createAliCaptchaRequest } from './services'; - -const userApplicationPrefix = 'a_'; - -export class PluginDemo extends Plugin { - async afterAdd() { - this.app.resourcer.use(async (ctx, next) => { - if (this.app.name !== 'main' && ctx.action.actionName === 'disable' && ctx.action.resourceName === 'pm') { - const { filterByTk } = ctx.action.params; - if (['disable-pm-add', '@tachybase/plugin-disable-pm-add'].includes(filterByTk)) { - ctx.throw(403, 'No permissions'); - } - } - await next(); - if (this.app.name === 'main') { - return; - } - if (ctx.action.actionName === 'list' && ctx.action.resourceName === 'pm') { - ctx.body = ctx.body?.filter((item) => { - return ![ - '@tachybase/plugin-verdaccio', - '@tachybase/plugin-demo-platform', - '@tachybase/plugin-disable-pm-add', - '@tachybase/plugin-notifications', - '@tachybase/plugin-multi-app-manager', - '@tachybase/plugin-multi-app-share-collection', - ].includes(item.packageName); - }); - } - }); - } - async beforeLoad() { - this.app.cronJobManager.addJob({ - cronTime: '0 0 * * *', - onTick: async () => { - await this.cleanExpiredApplications(); - }, - }); - this.app.cronJobManager.addJob({ - cronTime: '0 */30 * * * *', - onTick: async () => { - const expiredAt = new Date(new Date().getTime() - 30 * 60 * 1e3); - await this.removeAppInMemoryThatNotActiveAfterTime(expiredAt); - }, - }); - this.db.on('applications.beforeCreate', async (model) => { - if (!model.name) { - model.set('name', `${userApplicationPrefix}${uid()}`); - } - }); - this.db.on('applications.afterDestroy', async (model, options) => { - const transaction = options.transaction; - const deleteDatabase = async () => { - const sql = `DROP DATABASE IF EXISTS "${model.get('name')}"`; - await this.db.sequelize.query(sql); - }; - if (!transaction) { - await deleteDatabase(); - } else { - transaction.afterCommit(async () => { - await deleteDatabase(); - }); - } - }); - this.db.on('applicationForms.beforeCreate', async (model, options) => { - if (!model.get('templateName')) { - throw new Error('templateName is required'); - } - if (!model.get('email')) { - throw new Error('email is required'); - } - model.set('applicationName', `${userApplicationPrefix}${uid()}`); - model.set('url', `https://${model.get('applicationName')}.${process.env.DEMO_HOST}`); - }); - this.db.on('applicationForms.afterCreate', async (model, options) => { - const { transaction } = options; - transaction.afterCommit(() => { - this.db.sequelize.transaction(async (newTransaction) => { - try { - const application = await this.db.getRepository('applications').create({ - transaction: newTransaction, - values: { - name: model.get('applicationName'), - options: { - templateName: model.get('templateName'), - authManager: { - jwt: { - secret: nanoid(48), - }, - }, - }, - }, - context: { - templateName: model.get('templateName'), - }, - }); - await model.setApplication(application, { transaction: newTransaction }); - const notification = await this.db.getRepository('notifications').findOne({ transaction: newTransaction }); - notification - .send({ - to: model.get('email'), - appId: application.get('name'), - }) - .catch((e) => console.log(e)); - } catch (e) { - this.app.log.error(e); - } - }); - }); - }); - this.app.resourcer.registerActionHandler('updateTpl', async (ctx, next) => { - const { resourceName } = ctx.action; - if (resourceName !== 'applications') { - return await next(); - } - const { filterByTk } = ctx.action.params; - const applicationName = filterByTk; - if (!applicationName.startsWith('demo')) { - throw new Error('Only template application can be update to tpl'); - } - const templateName = `${applicationName}_tpl`; - await ctx.db.sequelize.query(`DROP DATABASE IF EXISTS "${templateName}"`); - await ctx.db.sequelize.query(`CREATE DATABASE "${templateName}" TEMPLATE "${applicationName}"`); - ctx.body = 'ok'; - await next(); - }); - } - async cleanExpiredApplications() { - const now = new Date(); - const expiredTime = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1e3); - const applications = await this.db.getRepository('applications').find({ - filter: { - 'createdAt.$lte': expiredTime, - // 创建时间超过两天 - 'options.templateName.$ne': null, - // 非模板应用 - 'options.persistent.$not': true, - // 非持久化应用 - }, - }); - await this.db.getRepository('applications').destroy({ - filterByTk: applications.map((application) => application.get('name')), - }); - } - async removeAppInMemoryThatNotActiveAfterTime(expiredAt) { - const expiredAtInUnix = Math.floor(expiredAt.getTime() / 1e3); - const applications = await this.db.getRepository('applications').find({ - filter: { - 'options.templateName.$ne': null, - 'options.persistent.$not': true, - }, - }); - for (const application of applications) { - const lastSeenAt = AppSupervisor.getInstance().lastSeenAt.get(application.get('name')); - if (lastSeenAt && lastSeenAt < expiredAtInUnix) { - await AppSupervisor.getInstance().removeApp(application.get('name')); - } - } - } - async load() { - await this.db.import({ directory: resolve(__dirname, 'collections') }); - Gateway.getInstance().addAppSelectorMiddleware(async (ctx, next) => { - if (!ctx.resolvedAppName) { - const req = ctx.req; - const headers = req.headers; - const hostname = headers['x-hostname'] || headers['host']; - const firstPart = hostname.split('.')[0]; - if (firstPart.startsWith(userApplicationPrefix)) { - ctx.resolvedAppName = firstPart; - } - } - await next(); - }); - this.app.resourcer.registerActionHandler('applicationForms:getTemplateNameUiSchema', async (ctx, next) => { - const uiSchemaTemplates = this.db.getRepository('uiSchemaTemplates'); - const uiSchemas = this.db.getRepository('uiSchemas'); - const collection = this.db.getRepository('collections'); - const instance = await uiSchemaTemplates.findOne({ - filter: { - name: 'demo', - collectionName: 'applicationForms', - }, - }); - ctx.body = { - collection: await collection.findOne({ - filter: { - name: 'applicationForms', - }, - appends: ['fields'], - }), - schema: await uiSchemas.getJsonSchema(instance['uid']), - }; - await next(); - }); - this.app.acl.allow('applicationForms', 'getTemplateNameUiSchema', 'public'); - this.app.acl.allow('applicationForms', 'create', 'public'); - const aliCaptchaClient = createAliCaptchaClient(); - this.app.resourcer.use(async (ctx, next) => { - if (ctx.action.actionName === 'create' && ctx.action.resourceName === 'applicationForms') { - const { senceId } = ctx.action.params; - const { captchaVerifyParam } = ctx.action.params.values; - delete ctx.action.params.values.captchaVerifyParam; - const request = createAliCaptchaRequest({ - senceId, - captchaVerifyParam, - }); - const resp = await aliCaptchaClient.verifyIntelligentCaptcha(request); - const captchaVerifyResult = resp.body.result.verifyResult; - if (!captchaVerifyResult) { - ctx.throw(400, 'captcha verify failed'); - } - } - await next(); - }); - const multiAppManager = this.pm.get('multi-app-manager') as PluginMultiAppManager; - multiAppManager.setAppDbCreator(async (app, options) => { - const { context = {} } = options; - const databaseOptions = app.options.database; - // @ts-ignore - const { host, port, username, password, database } = databaseOptions; - const { templateName } = context; - const client = new Sequelize({ - dialect: 'postgres', - host, - port, - username, - password, - database: 'postgres', - }); - if (templateName) { - let retryCount = 0; - let success = false; - const maxRetries = 5; - const retryDelay = 1e3; - while (retryCount < maxRetries && !success) { - try { - const res = await client.query(`CREATE DATABASE "${database}" TEMPLATE "${templateName}_tpl"`); - success = true; - console.log('Database created successfully'); - } catch (error) { - console.error('Failed to create database:', error); - retryCount++; - console.log(`Retrying (${retryCount})...`); - await new Promise((resolve) => setTimeout(resolve, retryDelay)); - } - } - if (!success) { - throw new Error('Failed to create database after multiple retries'); - } - } else { - await client.query(`DROP DATABASE IF EXISTS "${database}"`); - await client.query(`CREATE DATABASE "${database}"`); - } - await client.close(); - }); - multiAppManager.setSubAppUpgradeHandler(async (mainApp) => { - const templateApps = await mainApp.db.getRepository('applications').find({ - filter: { - 'options.isTemplate': true, - }, - }); - const appSupervisor = AppSupervisor.getInstance(); - for (const instance of templateApps) { - const subApp = await appSupervisor.getApp(instance.name, { - upgrading: true, - }); - await subApp.runAsCLI(['upgrade'], { from: 'user' }); - } - }); - } - async install(options) { - if (!this.db.inDialect('postgres')) { - return; - } - if (!this.pm.get('multi-app-manager').enabled) { - return; - } - await createNotificationRecord(this.app); - const repo = this.db.getRepository('collections'); - if (repo) { - await repo.db2cm('applicationForms'); - } - } - async beforeEnable() { - if (!this.db.inDialect('postgres')) { - throw new Error('demo-platform plugin only support postgres'); - } - if (!this.pm.get('multi-app-manager')?.enabled) { - throw new Error(`${this.name} plugin need multi-app-manager plugin enabled`); - } - if (!this.pm.get('notifications')?.enabled && !this.pm.get('@tachybase/plugin-notifications')?.enabled) { - throw new Error(`${this.name} plugin need notifications plugin enabled`); - } - } -} diff --git a/packages/plugins/@hera/plugin-core/src/server/features/demo/services.ts b/packages/plugins/@hera/plugin-core/src/server/features/demo/services.ts deleted file mode 100644 index c51bc8149..000000000 --- a/packages/plugins/@hera/plugin-core/src/server/features/demo/services.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Captcha, { VerifyIntelligentCaptchaRequest } from '@alicloud/captcha20230305'; -import { Config } from '@alicloud/openapi-client'; - -export function createAliCaptchaClient() { - const config = new Config({}); - config.accessKeyId = process.env['ALI_CAPTCHA_ACCESS_KEY_ID']; - config.accessKeySecret = process.env['ALI_CAPTCHA_ACCESS_KEY_SECRET']; - config.endpoint = process.env['ALI_CAPTCH_ENDPOINT'] - ? process.env['ALI_CAPTCH_ENDPOINT'] - : 'captcha.cn-shanghai.aliyuncs.com'; - config.connectTimeout = 5e3; - config.readTimeout = 5e3; - return new Captcha(config); -} - -export function createAliCaptchaRequest({ senceId, captchaVerifyParam }) { - const request = new VerifyIntelligentCaptchaRequest({}); - request.sceneId = senceId; - request.captchaVerifyParam = captchaVerifyParam; - return request; -}