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;
-}