mirror of
https://github.com/nocobase/nocobase.git
synced 2026-10-24 01:43:57 +08:00
Merge remote-tracking branch 'origin/main' into next
This commit is contained in:
@@ -370,7 +370,11 @@ export async function buildProPluginServer(cwd: string, userConfig: UserConfig,
|
||||
// other plugins build to a bundle just include plugin-commercial
|
||||
if (!cwd.includes(PLUGIN_COMMERCIAL)) {
|
||||
externalOptions.external = [/^[./]/];
|
||||
externalOptions.noExternal = [entryFile, /@nocobase\/plugin-commercial\/server/, /dist\/server\/index\.js/];
|
||||
externalOptions.noExternal = [
|
||||
entryFile,
|
||||
/@nocobase\/plugin-commercial\/server/,
|
||||
/dist\/server\/index\.js/,
|
||||
];
|
||||
externalOptions.onSuccess = async () => {
|
||||
const serverFiles = [path.join(cwd, target_dir, 'server', 'index.js')];
|
||||
serverFiles.forEach((file) => {
|
||||
@@ -379,6 +383,9 @@ export async function buildProPluginServer(cwd: string, userConfig: UserConfig,
|
||||
};
|
||||
externalOptions.esbuildPlugins = [pluginEsbuildCommercialInject];
|
||||
}
|
||||
if (cwd.includes(PLUGIN_COMMERCIAL)) {
|
||||
externalOptions.noExternal = [/@nocobase\/plugin-license/, /dist\/server\/index\.js/];
|
||||
}
|
||||
|
||||
// bundle all files、inject commercial code and obfuscate
|
||||
await tsupBuild(
|
||||
|
||||
@@ -44,10 +44,17 @@ export const PLUGINS_DIR = ['plugins', 'samples', 'pro-plugins']
|
||||
.map((name) => path.join(PACKAGES_PATH, name));
|
||||
export const PRESETS_DIR = path.join(PACKAGES_PATH, 'presets');
|
||||
export const PLUGIN_COMMERCIAL = '@nocobase/plugin-commercial';
|
||||
export const PLUGIN_LICENSE = '@nocobase/plugin-license';
|
||||
export const getPluginPackages = (packages: Package[]) =>
|
||||
packages.filter((item) => PLUGINS_DIR.some((pluginDir) => item.location.startsWith(pluginDir))).sort((a, b) => {
|
||||
return a.name === PLUGIN_COMMERCIAL ? -1 : 1;
|
||||
});
|
||||
packages
|
||||
.filter((item) => PLUGINS_DIR.some((pluginDir) => item.location.startsWith(pluginDir)))
|
||||
.sort((a, b) => {
|
||||
const priority = {
|
||||
[PLUGIN_LICENSE]: 0,
|
||||
[PLUGIN_COMMERCIAL]: 1,
|
||||
};
|
||||
return (priority[a.name] ?? 99) - (priority[b.name] ?? 99);
|
||||
});
|
||||
export const getPresetsPackages = (packages: Package[]) =>
|
||||
packages.filter((item) => item.location.startsWith(PRESETS_DIR));
|
||||
export const CORE_APP = path.join(PACKAGES_PATH, 'core/app');
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nocobase/app": "1.9.0-beta.18",
|
||||
"@nocobase/license-kit": "^0.2.17",
|
||||
"@nocobase/license-kit": "^0.3.2",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@umijs/utils": "3.5.20",
|
||||
"chalk": "^4.1.1",
|
||||
|
||||
@@ -13,6 +13,7 @@ const { run, isDev } = require('../util');
|
||||
const { getInstanceIdAsync } = require('@nocobase/license-kit');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { logger } = require('../logger');
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -24,11 +25,11 @@ module.exports = (cli) => {
|
||||
.description('Generate InstanceID')
|
||||
.option('--force', 'Force generate InstanceID')
|
||||
.action(async (options) => {
|
||||
console.log('Generating InstanceID...');
|
||||
logger.info('Generating InstanceID...');
|
||||
const dir = path.resolve(process.cwd(), 'storage/.license');
|
||||
const filePath = path.resolve(dir, 'instance-id');
|
||||
if (fs.existsSync(filePath) && !options.force) {
|
||||
console.log('InstanceID already exists at ' + filePath);
|
||||
logger.info('InstanceID already exists at ' + filePath);
|
||||
return;
|
||||
} else {
|
||||
if (!fs.existsSync(dir)) {
|
||||
@@ -37,9 +38,9 @@ module.exports = (cli) => {
|
||||
try {
|
||||
const instanceId = await getInstanceIdAsync();
|
||||
fs.writeFileSync(filePath, instanceId + '\n');
|
||||
console.log(chalk.greenBright(`InstanceID saved to ${filePath}`));
|
||||
logger.info(`InstanceID saved to ${filePath}`);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
logger.error('Failed to generate InstanceID', e.message || e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ const zlib = require('zlib');
|
||||
const tar = require('tar');
|
||||
const path = require('path');
|
||||
const { createStoragePluginsSymlink } = require('@nocobase/utils/plugin-symlink');
|
||||
const chalk = require('chalk');
|
||||
const { getAccessKeyPair, showLicenseInfo, LicenseKeyError } = require('../util');
|
||||
const { getAccessKeyPair, showLicenseInfo, LicenseKeyError } = require('../license');
|
||||
const { logger } = require('../logger');
|
||||
|
||||
class Package {
|
||||
data;
|
||||
@@ -78,7 +78,7 @@ class Package {
|
||||
}
|
||||
|
||||
if (!this.data.versions[version]) {
|
||||
console.log(chalk.redBright(`Download failed: ${this.packageName}@${version} package does not exist`));
|
||||
logger.error(`Download failed: ${this.packageName}@${version} package does not exist`);
|
||||
}
|
||||
|
||||
return [version, this.data.versions[version].dist.tarball];
|
||||
@@ -122,11 +122,11 @@ class Package {
|
||||
|
||||
async download(options = {}) {
|
||||
if (await this.isDevPackage()) {
|
||||
console.log(chalk.yellowBright(`Skipped: ${this.packageName} is dev package`));
|
||||
logger.info(`Skipped: ${this.packageName} is dev package`);
|
||||
return;
|
||||
}
|
||||
if (await this.isDepPackage()) {
|
||||
console.log(chalk.yellowBright(`Skipped: ${this.packageName} is dependency package`));
|
||||
logger.info(`Skipped: ${this.packageName} is dependency package`);
|
||||
return;
|
||||
}
|
||||
if (await this.isDownloaded(options.version)) {
|
||||
@@ -134,7 +134,7 @@ class Package {
|
||||
}
|
||||
await this.getInfo();
|
||||
if (!this.data) {
|
||||
console.log(chalk.redBright(`Download failed: ${this.packageName} package does not exist`));
|
||||
logger.error(`Download failed: ${this.packageName} package does not exist`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -158,7 +158,7 @@ class Package {
|
||||
.on('finish', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
console.log(chalk.greenBright(`Downloaded: ${this.packageName}@${version}`));
|
||||
logger.info(`Downloaded: ${this.packageName}@${version}`);
|
||||
} catch (error) {
|
||||
if (error?.response?.data && typeof error?.response?.data?.pipe === 'function') {
|
||||
let errorMessageBuffer = '';
|
||||
@@ -167,11 +167,11 @@ class Package {
|
||||
});
|
||||
error.response.data.on?.('end', () => {
|
||||
if (error.response.status === 403) {
|
||||
console.error(chalk.redBright('You do not have permission to download this package version.'));
|
||||
logger.error('You do not have permission to download this package version.');
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log(chalk.redBright(`Download failed: ${this.packageName}`));
|
||||
logger.error(`Download failed: ${this.packageName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,8 +206,8 @@ class PackageManager {
|
||||
if (error?.response?.data?.error === 'license not valid') {
|
||||
showLicenseInfo(LicenseKeyError.notValid);
|
||||
}
|
||||
console.error(chalk.redBright(`Login failed: ${this.baseURL}`));
|
||||
console.error(error);
|
||||
logger.error(`Login failed: ${this.baseURL}`);
|
||||
logger.error(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ class PackageManager {
|
||||
const dir = path.resolve(process.env.PLUGIN_STORAGE_PATH, packageName);
|
||||
const r = await fs.exists(dir);
|
||||
if (r) {
|
||||
console.log(chalk.yellowBright(`Removed: ${packageName}`));
|
||||
logger.info(`Removed: ${packageName}`);
|
||||
await fs.rm(dir, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
@@ -286,8 +286,14 @@ module.exports = (cli) => {
|
||||
try {
|
||||
({ accessKeyId, accessKeySecret } = await getAccessKeyPair());
|
||||
} catch (e) {
|
||||
// logger.error('Get AccessKey Pair error', e);
|
||||
return;
|
||||
}
|
||||
if (NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD && !accessKeyId && !accessKeySecret) {
|
||||
logger.warn(
|
||||
'NOCOBASE_PKG_USERNAME and NOCOBASE_PKG_PASSWORD will be deprecated in future versions. Please log in to NocoBase Service and refer to the documentation to learn how to install and upgrade commercial plugins.\n',
|
||||
);
|
||||
}
|
||||
if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD) && !(accessKeyId && accessKeySecret)) {
|
||||
return;
|
||||
}
|
||||
@@ -302,6 +308,6 @@ module.exports = (cli) => {
|
||||
await createStoragePluginsSymlink();
|
||||
});
|
||||
pkg.command('export-all').action(async () => {
|
||||
console.log('Todo...');
|
||||
logger.info('Todo...');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -57,7 +57,6 @@ module.exports = (cli) => {
|
||||
}
|
||||
|
||||
if (process.env.NO_WATCH_PLUGINS === true || process.env.NO_WATCH_PLUGINS === 'true') {
|
||||
|
||||
const restart = _.debounce(async () => {
|
||||
console.log('restarting...');
|
||||
await run('yarn', ['nocobase', 'pm2-restart']);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
const chalk = require('chalk');
|
||||
const { resolve } = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const { keyDecrypt, getEnvAsync } = require('@nocobase/license-kit');
|
||||
const { isEnvMatch } = require('@nocobase/plugin-license/utils/env');
|
||||
const { logger } = require('./logger');
|
||||
const { pick } = require('lodash');
|
||||
|
||||
exports.getAccessKeyPair = async function () {
|
||||
const keyFile = resolve(process.cwd(), 'storage/.license/license-key');
|
||||
if (!fs.existsSync(keyFile)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let keyData = {};
|
||||
try {
|
||||
const str = fs.readFileSync(keyFile, 'utf-8');
|
||||
const keyDataStr = keyDecrypt(str);
|
||||
keyData = JSON.parse(keyDataStr);
|
||||
} catch (error) {
|
||||
showLicenseInfo(LicenseKeyError.parseFailed);
|
||||
throw new Error(LicenseKeyError.parseFailed.title);
|
||||
}
|
||||
const env = await getEnvAsync();
|
||||
const isEnvMatched = await isEnvMatch(env, keyData);
|
||||
if (!isEnvMatched) {
|
||||
showLicenseInfo({
|
||||
...LicenseKeyError.notMatch,
|
||||
env,
|
||||
});
|
||||
throw new Error(LicenseKeyError.notMatch.title);
|
||||
}
|
||||
const { accessKeyId, accessKeySecret } = keyData;
|
||||
return { accessKeyId, accessKeySecret };
|
||||
};
|
||||
|
||||
const LicenseKeyError = {
|
||||
notExist: {
|
||||
title: 'License key not found',
|
||||
content:
|
||||
'Please go to the license settings page to obtain the Instance ID for the current environment, and then generate the license key on the service platform.',
|
||||
},
|
||||
parseFailed: {
|
||||
title: 'Invalid license key format',
|
||||
content: 'Please check your license key, or regenerate the license key on the service platform.',
|
||||
},
|
||||
notMatch: {
|
||||
title: 'License key not match current environment',
|
||||
content:
|
||||
'Please go to the license settings page to obtain the Instance ID for the current environment, and then regenerate the license key on the service platform.',
|
||||
},
|
||||
notValid: {
|
||||
title: 'Invalid license key',
|
||||
content:
|
||||
'Please go to the license settings page to obtain the Instance ID for the current environment, and then regenerate the license key on the service platform.',
|
||||
},
|
||||
};
|
||||
|
||||
exports.LicenseKeyError = LicenseKeyError;
|
||||
|
||||
function showLicenseInfo({ title, content, env }) {
|
||||
logger.error(title + '. ' + content);
|
||||
logger.error('Current environment', pick(env, ['sys', 'osVer', 'db']));
|
||||
}
|
||||
|
||||
exports.showLicenseInfo = showLicenseInfo;
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const winston = require('winston');
|
||||
require('winston-daily-rotate-file');
|
||||
|
||||
function ensureDir(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function createSystemLogger({ dirname, filename, defaultMeta = {} }) {
|
||||
ensureDir(dirname);
|
||||
|
||||
const commonFormat = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.printf((info) => {
|
||||
const meta = info.meta ? JSON.stringify(info.meta) : '';
|
||||
return `${info.timestamp} [${info.level}] ${info.message} ${meta}`;
|
||||
}),
|
||||
);
|
||||
|
||||
const consoleFormat = winston.format.combine(winston.format.colorize(), commonFormat);
|
||||
|
||||
const fileTransport = new winston.transports.DailyRotateFile({
|
||||
dirname,
|
||||
filename: `${filename}_%DATE%.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: false,
|
||||
format: commonFormat,
|
||||
});
|
||||
|
||||
const consoleTransport = new winston.transports.Console({
|
||||
format: consoleFormat,
|
||||
});
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
transports: [consoleTransport, fileTransport],
|
||||
defaultMeta,
|
||||
});
|
||||
|
||||
const wrap = (level) => (message, meta) => {
|
||||
logger.log({ level, message, meta });
|
||||
return logger;
|
||||
};
|
||||
|
||||
return {
|
||||
info: wrap('info'),
|
||||
warn: wrap('warn'),
|
||||
error: wrap('error'),
|
||||
};
|
||||
}
|
||||
|
||||
const getLoggerFilePath = (...paths) => {
|
||||
return path.resolve(process.env.LOGGER_BASE_PATH || path.resolve(process.cwd(), 'storage', 'logs'), ...paths);
|
||||
};
|
||||
|
||||
const logger = createSystemLogger({
|
||||
dirname: getLoggerFilePath('main'),
|
||||
filename: 'system',
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
logger,
|
||||
};
|
||||
@@ -18,8 +18,6 @@ const dotenv = require('dotenv');
|
||||
const fs = require('fs-extra');
|
||||
const os = require('os');
|
||||
const moment = require('moment-timezone');
|
||||
const { keyDecrypt, getEnvAsync } = require('@nocobase/license-kit');
|
||||
const omit = require('lodash/omit');
|
||||
|
||||
exports.isPackageValid = (pkg) => {
|
||||
try {
|
||||
@@ -490,89 +488,3 @@ exports.generatePlugins = function () {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
async function isEnvMatch(keyData) {
|
||||
const env = await getEnvAsync();
|
||||
if (env?.container?.id && keyData?.instanceData?.container?.id) {
|
||||
return (
|
||||
JSON.stringify(omit(env, ['timestamp', 'container', 'hostname', 'mac'])) ===
|
||||
JSON.stringify(omit(keyData?.instanceData, ['timestamp', 'container', 'hostname', 'mac']))
|
||||
);
|
||||
}
|
||||
return (
|
||||
JSON.stringify(omit(env, ['timestamp', 'mac'])) ===
|
||||
JSON.stringify(omit(keyData?.instanceData, ['timestamp', 'mac']))
|
||||
);
|
||||
}
|
||||
|
||||
exports.getAccessKeyPair = async function () {
|
||||
const keyFile = resolve(process.cwd(), 'storage/.license/license-key');
|
||||
if (!fs.existsSync(keyFile)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let keyData = {};
|
||||
try {
|
||||
const str = fs.readFileSync(keyFile, 'utf-8');
|
||||
const keyDataStr = keyDecrypt(str);
|
||||
keyData = JSON.parse(keyDataStr);
|
||||
} catch (error) {
|
||||
showLicenseInfo(LicenseKeyError.parseFailed);
|
||||
throw new Error(LicenseKeyError.parseFailed.title);
|
||||
}
|
||||
|
||||
const isEnvMatched = await isEnvMatch(keyData);
|
||||
if (!isEnvMatched) {
|
||||
showLicenseInfo(LicenseKeyError.notMatch);
|
||||
throw new Error(LicenseKeyError.notMatch.title);
|
||||
}
|
||||
|
||||
const { accessKeyId, accessKeySecret } = keyData;
|
||||
return { accessKeyId, accessKeySecret };
|
||||
};
|
||||
|
||||
const LicenseKeyError = {
|
||||
notExist: {
|
||||
title: 'License key not found',
|
||||
content:
|
||||
'Please go to the license settings page to obtain the Instance ID for the current environment, and then generate the license key on the service platform.',
|
||||
},
|
||||
parseFailed: {
|
||||
title: 'Invalid license key format',
|
||||
content: 'Please check your license key, or regenerate the license key on the service platform.',
|
||||
},
|
||||
notMatch: {
|
||||
title: 'License key mismatch',
|
||||
content:
|
||||
'Please go to the license settings page to obtain the Instance ID for the current environment, and then regenerate the license key on the service platform.',
|
||||
},
|
||||
notValid: {
|
||||
title: 'Invalid license key',
|
||||
content:
|
||||
'Please go to the license settings page to obtain the Instance ID for the current environment, and then regenerate the license key on the service platform.',
|
||||
},
|
||||
};
|
||||
|
||||
exports.LicenseKeyError = LicenseKeyError;
|
||||
|
||||
function showLicenseInfo({ title, content }) {
|
||||
const rows = [];
|
||||
const length = 80;
|
||||
let row = '';
|
||||
content.split(' ').forEach((word) => {
|
||||
if (row.length + word.length > length) {
|
||||
rows.push(row);
|
||||
row = '';
|
||||
}
|
||||
row += word + ' ';
|
||||
});
|
||||
if (row) {
|
||||
rows.push(row);
|
||||
}
|
||||
console.log(Array(length).fill('-').join(''));
|
||||
console.log(chalk.yellow(title));
|
||||
console.log(chalk.yellow(rows.join('\n')));
|
||||
console.log(Array(length).fill('-').join(''));
|
||||
}
|
||||
|
||||
exports.showLicenseInfo = showLicenseInfo;
|
||||
|
||||
@@ -56,7 +56,7 @@ const deps: Record<string, string> = {
|
||||
lodash: '4.x',
|
||||
'china-division': '2.x',
|
||||
cronstrue: '2.x',
|
||||
'@nocobase/license-kit': '0.2.x',
|
||||
'@nocobase/license-kit': '0.3.x',
|
||||
};
|
||||
|
||||
export default deps;
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
"description": "Instance ID and license key settings",
|
||||
"description.ru-RU": "Настройка идентификатора экземпляра и ключа лицензии",
|
||||
"description.zh-CN": "实例 ID 和授权密钥设置",
|
||||
"devDependencies": {
|
||||
"@nocobase/license-kit": "^0.2.17"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nocobase/client": "1.x",
|
||||
"@nocobase/server": "1.x",
|
||||
"@nocobase/test": "1.x"
|
||||
},
|
||||
"scripts": {
|
||||
"ts:env": "tsc -p tsconfig.env.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,16 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { SchemaComponent, useAPIClient, useFormBlockContext } from '@nocobase/client';
|
||||
import { Card, Typography, Spin, message, Input, Button } from 'antd';
|
||||
import { Card, Typography, Spin, message, Input, Button, Alert, Modal } from 'antd';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import { useT } from './locale';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { ServiceValidate } from './ServiceValidate';
|
||||
import { useForm } from '@formily/react';
|
||||
import { useSubmitProps } from './useSubmitProps';
|
||||
import { LicenseSettingContext } from './LicenseSettingContext';
|
||||
|
||||
const copyTextToClipboard = ({
|
||||
text,
|
||||
@@ -95,65 +99,31 @@ function InstanceId() {
|
||||
);
|
||||
}
|
||||
|
||||
const useSubmitProps = () => {
|
||||
const api = useAPIClient();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const ctx = useFormBlockContext();
|
||||
const t = useT();
|
||||
|
||||
const saveLicenseKey = async (licenseKey: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.request({
|
||||
url: '/license:license-key',
|
||||
method: 'POST',
|
||||
data: {
|
||||
licenseKey,
|
||||
},
|
||||
});
|
||||
setLoading(false);
|
||||
message.success(t('License key saved successfully, please re-run the plugin installation.'));
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
onClick: async () => {
|
||||
await ctx.form?.validate();
|
||||
const licenseKey = ctx.form?.values?.licenseKey;
|
||||
await saveLicenseKey(licenseKey);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const TextArea = (props) => {
|
||||
const [isExists, setIsExists] = useState(false);
|
||||
const api = useAPIClient();
|
||||
const ctx = useFormBlockContext();
|
||||
const TextArea = ({ ...props }) => {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const form = useForm();
|
||||
const t = useT();
|
||||
const { keyExist } = React.useContext(LicenseSettingContext);
|
||||
useAsyncEffect(async () => {
|
||||
const res = await api.request({
|
||||
url: '/license:is-exists',
|
||||
method: 'GET',
|
||||
});
|
||||
if (res?.data?.data) {
|
||||
ctx.form?.setFieldState('footer', (state) => {
|
||||
state.visible = false;
|
||||
if (keyExist === false) {
|
||||
setIsEdit(true);
|
||||
form?.setFieldState('footer', (state) => {
|
||||
state.visible = true;
|
||||
});
|
||||
}
|
||||
setIsExists(res?.data?.data);
|
||||
}, []);
|
||||
}, [keyExist]);
|
||||
|
||||
if (isExists) {
|
||||
if (isEdit) {
|
||||
return <Input.TextArea rows={4} {...props} />;
|
||||
}
|
||||
if (keyExist) {
|
||||
return (
|
||||
<>
|
||||
{t('License key has been set')}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsExists(false);
|
||||
ctx.form?.setFieldState('footer', (state) => {
|
||||
setIsEdit(true);
|
||||
form?.setFieldState('footer', (state) => {
|
||||
state.visible = true;
|
||||
});
|
||||
}}
|
||||
@@ -163,24 +133,50 @@ const TextArea = (props) => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Input.TextArea rows={4} {...props} />;
|
||||
return null;
|
||||
};
|
||||
|
||||
const LicenseCardWarp = () => {
|
||||
const { keyExist, refreshToken } = React.useContext(LicenseSettingContext);
|
||||
if (!keyExist) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SchemaComponent
|
||||
key={refreshToken}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
card: {
|
||||
type: 'void',
|
||||
'x-component': 'LicenseCard',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default function LicenseSetting() {
|
||||
const t = useT();
|
||||
const [renderKey, setRenderKey] = useState(0);
|
||||
const [keyExist, setKeyExist] = useState(null);
|
||||
const api = useAPIClient();
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const res = await api.request({
|
||||
url: '/license:is-exists',
|
||||
method: 'GET',
|
||||
});
|
||||
setKeyExist(res?.data?.data);
|
||||
}, []);
|
||||
|
||||
const createLabelSchema = {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
},
|
||||
'x-decorator': 'Form',
|
||||
properties: {
|
||||
form: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useFormBlockProps',
|
||||
properties: {
|
||||
label: {
|
||||
type: 'string',
|
||||
@@ -209,26 +205,49 @@ export default function LicenseSetting() {
|
||||
'x-use-component-props': 'useSubmitProps',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
style: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'x-visible': false,
|
||||
},
|
||||
licenseInfo: {
|
||||
type: 'void',
|
||||
// title: 'NocoBase ' + t('License information'),
|
||||
'x-component': 'LicenseCardWarp',
|
||||
// 'x-decorator': 'FormItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<SchemaComponent
|
||||
scope={{ useSubmitProps }}
|
||||
components={{ InstanceId, TextArea }}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
form: createLabelSchema,
|
||||
<LicenseSettingContext.Provider
|
||||
value={{
|
||||
onSaveSuccess: () => {
|
||||
setRenderKey((k) => k + 1);
|
||||
setKeyExist(true);
|
||||
},
|
||||
keyExist,
|
||||
refreshToken: renderKey,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<ServiceValidate refreshToken={renderKey} />
|
||||
<SchemaComponent
|
||||
scope={{ useSubmitProps }}
|
||||
components={{ InstanceId, TextArea, LicenseCardWarp }}
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
form: createLabelSchema,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</LicenseSettingContext.Provider>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface LicenseSettingContextType {
|
||||
onSaveSuccess: () => void;
|
||||
keyExist: boolean;
|
||||
refreshToken?: any;
|
||||
}
|
||||
|
||||
export const LicenseSettingContext = React.createContext<LicenseSettingContextType | null>(null);
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAPIClient } from '@nocobase/client';
|
||||
import { Alert } from 'antd';
|
||||
import { useT } from './locale';
|
||||
import { Env } from '@nocobase/license-kit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const ServiceValidate = ({ refreshToken }: { refreshToken: number }) => {
|
||||
const api = useAPIClient();
|
||||
const [state, setState] = useState<{
|
||||
keyStatus: string;
|
||||
licenseStatus: string;
|
||||
isPkgConnection: boolean;
|
||||
isPkgLogin: boolean;
|
||||
isServiceConnection: boolean;
|
||||
}>(null);
|
||||
|
||||
const t = useT();
|
||||
|
||||
useEffect(() => {
|
||||
setState(null);
|
||||
api
|
||||
.request({
|
||||
url: '/license:license-validate',
|
||||
method: 'get',
|
||||
})
|
||||
.then((res) => {
|
||||
setState(res?.data?.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}, [refreshToken]);
|
||||
|
||||
if (!state?.licenseStatus || state?.licenseStatus !== 'active') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (state?.isServiceConnection === false && state?.isPkgLogin === false) {
|
||||
return (
|
||||
<Alert
|
||||
message={t('Network error.')}
|
||||
description={
|
||||
<>
|
||||
{t(
|
||||
'Due to network issues, the license key cannot be updated automatically. Please update it manually if necessary.',
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'Plugins also cannot be updated automatically (they are still usable). To update plugins, please check your network connection or refer to the NocoBase Service documentation to upload plugins manually.',
|
||||
)}
|
||||
</>
|
||||
}
|
||||
type="warning"
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (state?.isServiceConnection === false) {
|
||||
return (
|
||||
<Alert
|
||||
message={t('Network error.')}
|
||||
description={
|
||||
<>
|
||||
{t(
|
||||
'Due to network issues, the license key cannot be updated automatically. Please update it manually if necessary.',
|
||||
)}
|
||||
</>
|
||||
}
|
||||
type="warning"
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (state?.isPkgLogin === false) {
|
||||
return (
|
||||
<Alert
|
||||
message={t('Network error.')}
|
||||
description={
|
||||
<>
|
||||
{t(
|
||||
'Due to network issues, plugins cannot be updated automatically (they are still usable). To update plugins, please check your network connection or refer to the NocoBase Service documentation to upload plugins manually.',
|
||||
)}
|
||||
</>
|
||||
}
|
||||
type="warning"
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -13,7 +13,7 @@ import { useApp } from '@nocobase/client';
|
||||
|
||||
export function useT() {
|
||||
const app = useApp();
|
||||
return (str: string) => app.i18n.t(str, { ns: [pkg.name, 'client'] });
|
||||
return (str: string, options?: any) => String(app.i18n.t(str, { ns: [pkg.name, 'client'], ...(options || {}) }));
|
||||
}
|
||||
|
||||
export function tStr(key: string) {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { SchemaComponent, useAPIClient, useFormBlockContext } from '@nocobase/client';
|
||||
import { Card, Typography, Spin, message, Input, Button, Alert, Modal } from 'antd';
|
||||
import { useT } from './locale';
|
||||
import { useForm } from '@formily/react';
|
||||
import { LicenseSettingContext } from './LicenseSettingContext';
|
||||
|
||||
export const useSubmitProps = () => {
|
||||
const api = useAPIClient();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const form = useForm();
|
||||
const t = useT();
|
||||
const { onSaveSuccess } = React.useContext(LicenseSettingContext);
|
||||
|
||||
const saveLicenseKey = async (licenseKey: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await api.request({
|
||||
url: '/license:license-key',
|
||||
method: 'POST',
|
||||
data: {
|
||||
licenseKey,
|
||||
},
|
||||
});
|
||||
setLoading(false);
|
||||
|
||||
const licenseValidateResult: any = res?.data?.data || {};
|
||||
|
||||
// save error
|
||||
if (licenseValidateResult.keyStatus === 'invalid') {
|
||||
Modal.error({
|
||||
title: t('Invalid license key.'),
|
||||
content: t('The license key is invalid. Please visit the NocoBase Service to obtain a new license key.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (licenseValidateResult.licenseStatus === 'invalid') {
|
||||
Modal.error({
|
||||
title: t('Invalid license key.'),
|
||||
content: t(
|
||||
'The current license key has been deprecated. Please visit the NocoBase Service to obtain a new license key.',
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (licenseValidateResult.envMatch === false) {
|
||||
Modal.error({
|
||||
title: t('Environment mismatch.'),
|
||||
content: (
|
||||
<>
|
||||
{licenseValidateResult.dbMatch === false && licenseValidateResult.sysMatch === false && (
|
||||
<>
|
||||
{t(
|
||||
'The current system and database do not match the licensed environment. Please use the new InstanceID to request a new license key.',
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{licenseValidateResult.dbMatch === true && licenseValidateResult.sysMatch === false && (
|
||||
<>
|
||||
{t(
|
||||
'The current system does not match the licensed system. Please use the new InstanceID to request a new license key.',
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{licenseValidateResult.dbMatch === false && licenseValidateResult.sysMatch === true && (
|
||||
<>
|
||||
{t(
|
||||
'The current database does not match the licensed database. Please use the new InstanceID to request a new license key.',
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (licenseValidateResult.domainMatch === false) {
|
||||
Modal.error({
|
||||
title: t('Domain mismatch.'),
|
||||
content: t(
|
||||
'The current domain ({{domain}}) does not match the licensed domain. Please use the current domain to request a new license key.',
|
||||
{
|
||||
domain: licenseValidateResult.current.domain,
|
||||
interpolation: { escapeValue: false },
|
||||
},
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// save key succes
|
||||
onSaveSuccess?.();
|
||||
|
||||
if (licenseValidateResult.isExpired === true) {
|
||||
message.success(t('The license key was saved successfully'), 5);
|
||||
Modal.warning({
|
||||
title: t('The license has exceeded the upgrade validity period.'),
|
||||
content: t(
|
||||
'Plugins bound to this license can still be used but cannot be upgraded. To upgrade, please renew or repurchase the license.',
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (licenseValidateResult.isPkgLogin === false) {
|
||||
message.success(t('The license key was saved successfully'), 5);
|
||||
Modal.warning({
|
||||
title: t('Network error.'),
|
||||
content: t(
|
||||
'Due to network issues, plugins cannot be updated automatically (they are still usable). To update plugins, please check your network connection or refer to the NocoBase Service documentation to upload plugins manually.',
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
message.success(t('License key saved successfully. Please retry the plugin installation.'));
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
message.error(t('Network error. Please try again.'));
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
onClick: async () => {
|
||||
await form?.validate();
|
||||
const licenseKey = form.values?.licenseKey;
|
||||
await saveLicenseKey(licenseKey);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -2,8 +2,32 @@
|
||||
"Change key": "Change key",
|
||||
"Copied": "Copied",
|
||||
"Copy": "Copy",
|
||||
"Enter license key": "Enter license key",
|
||||
"Failed to copy, please open ./storage/.license/instance-id and copy it": "Failed to copy, please open ./storage/.license/instance-id and copy it",
|
||||
"Invalid license key.": "Invalid license key.",
|
||||
"The license key is invalid. Please visit the NocoBase Service to obtain a new license key.": "The license key is invalid. Please visit the NocoBase Service to obtain a new license key.",
|
||||
"The current license key has been deprecated. Please visit the NocoBase Service to obtain a new license key.": "The current license key has been deprecated. Please visit the NocoBase Service to obtain a new license key.",
|
||||
"The license has exceeded the upgrade validity period.": "The license has exceeded the upgrade validity period.",
|
||||
"Plugins bound to this license can still be used but cannot be upgraded. To upgrade, please renew or repurchase the license.": "Plugins bound to this license can still be used but cannot be upgraded. To upgrade, please renew or repurchase the license.",
|
||||
"Environment mismatch.": "Environment mismatch.",
|
||||
"The current system does not match the licensed system. Please use the new InstanceID to request a new license key.": "The current system does not match the licensed system. Please use the new InstanceID to request a new license key.",
|
||||
"The current database does not match the licensed database. Please use the new InstanceID to request a new license key.": "The current database does not match the licensed database. Please use the new InstanceID to request a new license key.",
|
||||
"The current system and database do not match the licensed environment. Please use the new InstanceID to request a new license key.": "The current system and database do not match the licensed environment. Please use the new InstanceID to request a new license key.",
|
||||
"Domain mismatch.": "Domain mismatch.",
|
||||
"The current domain ({{domain}}) does not match the licensed domain. Please use the current domain to request a new license key.": "The current domain ({{domain}}) does not match the licensed domain. Please use the current domain to request a new license key.",
|
||||
"Plugin not licensed.": "Plugin not licensed.",
|
||||
"An unlicensed plugin has been detected. Please visit the NocoBase Service to complete the purchase or obtain a license.": "An unlicensed plugin has been detected. Please visit the NocoBase Service to complete the purchase or obtain a license.",
|
||||
"Network error.": "Network error.",
|
||||
"Due to network issues, the license key cannot be updated automatically. Please update it manually if necessary.": "Due to network issues, the license key cannot be updated automatically. Please update it manually if necessary.",
|
||||
"Due to network issues, plugins cannot be updated automatically (they are still usable). To update plugins, please check your network connection or refer to the NocoBase Service documentation to upload plugins manually.": "Due to network issues, plugins cannot be updated automatically (they are still usable). To update plugins, please check your network connection or refer to the NocoBase Service documentation to upload plugins manually.",
|
||||
"Plugins also cannot be updated automatically (they are still usable). To update plugins, please check your network connection or refer to the NocoBase Service documentation to upload plugins manually.": "Plugins also cannot be updated automatically (they are still usable). To update plugins, please check your network connection or refer to the NocoBase Service documentation to upload plugins manually.",
|
||||
"License key saved successfully. Please retry the plugin installation.": "License key saved successfully. Please retry the plugin installation.",
|
||||
"The license key was saved successfully": "The license key was saved successfully",
|
||||
"License key not found.": "License key not found.",
|
||||
"Please go to the": "Please go to the",
|
||||
"License Settings": "License Settings",
|
||||
"page to enter the license key.": "page to enter the license key.",
|
||||
"License information": "License information",
|
||||
"Enter license key": "Enter license key",
|
||||
"Instance ID": "Instance ID",
|
||||
"Invalid license key": "Invalid license key",
|
||||
"License key": "License key",
|
||||
|
||||
@@ -2,8 +2,32 @@
|
||||
"Change key": "更改密钥",
|
||||
"Copied": "复制成功",
|
||||
"Copy": "复制",
|
||||
"Enter license key": "输入授权密钥",
|
||||
"Failed to copy, please open ./storage/.license/instance-id and copy it": "复制失败,请打开 ./storage/.license/instance-id 并复制",
|
||||
"Invalid license key.": "授权密钥无效",
|
||||
"The license key is invalid. Please visit the NocoBase Service to obtain a new license key.": "无效的授权密钥,请前往 NocoBase Service 重新获取授权密钥。",
|
||||
"The current license key has been deprecated. Please visit the NocoBase Service to obtain a new license key.": "当前授权密钥已被废弃,请前往 NocoBase Service 重新获取授权密钥。",
|
||||
"The license has exceeded the upgrade validity period.": "许可证已过升级有效期",
|
||||
"Plugins bound to this license can still be used but cannot be upgraded. To upgrade, please renew or repurchase the license.": "许可证绑定的插件可以继续使用,但无法升级,如需升级请续费或重新购买许可证。",
|
||||
"Environment mismatch.": "环境不匹配",
|
||||
"The current system does not match the licensed system. Please use the new InstanceID to request a new license key.": "当前系统与许可绑定的系统不一致,请使用新的 InstanceID 重新申请授权密钥。",
|
||||
"The current database does not match the licensed database. Please use the new InstanceID to request a new license key.": "当前数据库与许可绑定的数据库不一致,请使用新的 InstanceID 重新申请授权密钥。",
|
||||
"The current system and database do not match the licensed environment. Please use the new InstanceID to request a new license key.": "当前系统和数据库与许可绑定的环境不一致,请使用新的 InstanceID 重新申请授权密钥。",
|
||||
"Domain mismatch.": "域名不匹配",
|
||||
"The current domain ({{domain}}) does not match the licensed domain. Please use the current domain to request a new license key.": "当前域名({{domain}})与许可绑定的域名不一致,请使用当前域名重新申请授权密钥。",
|
||||
"Plugin not licensed.": "插件未授权",
|
||||
"An unlicensed plugin has been detected. Please visit the NocoBase Service to complete the purchase or obtain a license.": "检测到你正在使用未获得许可的插件,请前往 NocoBase Service 完成购买或授权。",
|
||||
"Network error.": "网络异常",
|
||||
"Due to network issues, the license key cannot be updated automatically. Please update it manually if necessary.": "由于网络问题,授权密钥无法自动更新,如有需要请手动更新。",
|
||||
"Due to network issues, plugins cannot be updated automatically (they are still usable). To update plugins, please check your network connection or refer to the NocoBase Service documentation to upload plugins manually.": "由于网络问题,插件无法自动更新(可正常使用),如需要更新插件,请检查网络连接,或参考 NocoBase Service 文档手动上传插件。",
|
||||
"Plugins also cannot be updated automatically (they are still usable). To update plugins, please check your network connection or refer to the NocoBase Service documentation to upload plugins manually.": "插件也无法自动更新(可正常使用),如需要更新插件,请检查网络连接,或参考 NocoBase Service 文档手动上传插件。",
|
||||
"License key saved successfully. Please retry the plugin installation.": "授权密钥保存成功,请重新执行插件安装操作。",
|
||||
"The license key was saved successfully": "授权密钥保存成功",
|
||||
"License key not found.": "授权密钥未找到",
|
||||
"Please go to the": "请前往",
|
||||
"License Settings": "授权设置",
|
||||
"page to enter the license key.": "页面填写授权密钥。",
|
||||
"License information": "授权信息",
|
||||
"Enter license key": "输入授权密钥",
|
||||
"Instance ID": "实例 ID",
|
||||
"Invalid license key": "无效的授权密钥",
|
||||
"License key": "授权密钥",
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { isDomainMatch } from '../utils';
|
||||
|
||||
const wrapKeyData: (domain: string) => any = (domain: string) => ({
|
||||
licenseKey: { domain },
|
||||
});
|
||||
|
||||
describe('isDomainMatch - full domain format coverage', () => {
|
||||
// ========== 普通域名 ==========
|
||||
test('matches normal domain nocobase.com', () => {
|
||||
return expect(isDomainMatch('https://nocobase.com', wrapKeyData('nocobase.com'))).toBe(true);
|
||||
});
|
||||
|
||||
test('matches subdomain sub.nocobase.com', () => {
|
||||
expect(isDomainMatch('https://sub.nocobase.com', wrapKeyData('sub.nocobase.com'))).toBe(true);
|
||||
});
|
||||
|
||||
test('fails subdomain if not equal', () => {
|
||||
expect(isDomainMatch('https://sub.nocobase.com', wrapKeyData('nocobase.com'))).toBe(false);
|
||||
});
|
||||
|
||||
// ========== 带端口 ==========
|
||||
test('matches domain with port nocobase.com:13000', () => {
|
||||
expect(isDomainMatch('https://nocobase.com:13000', wrapKeyData('nocobase.com:13000'))).toBe(true);
|
||||
});
|
||||
|
||||
test('fails if port not equal', () => {
|
||||
expect(isDomainMatch('https://nocobase.com:13001', wrapKeyData('nocobase.com:13000'))).toBe(false);
|
||||
});
|
||||
|
||||
// ========== 泛域名 ==========
|
||||
test('matches wildcard *.nocobase.com', () => {
|
||||
expect(isDomainMatch('https://sub.nocobase.com', wrapKeyData('*.nocobase.com'))).toBe(true);
|
||||
});
|
||||
|
||||
test('matches wildcard *.sub.nocobase.com', () => {
|
||||
expect(isDomainMatch('https://x.sub.nocobase.com', wrapKeyData('*.sub.nocobase.com'))).toBe(true);
|
||||
});
|
||||
|
||||
test('wildcard fails when no base domain match', () => {
|
||||
expect(isDomainMatch('https://abc.com', wrapKeyData('*.nocobase.com'))).toBe(false);
|
||||
});
|
||||
|
||||
// ========== 泛域名 + 端口 ==========
|
||||
test('matches wildcard with port *.nocobase.com:13000', () => {
|
||||
expect(isDomainMatch('https://sub.nocobase.com:13000', wrapKeyData('*.nocobase.com:13000'))).toBe(true);
|
||||
});
|
||||
|
||||
test('fails wildcard when port mismatch', () => {
|
||||
expect(isDomainMatch('https://sub.nocobase.com:13001', wrapKeyData('*.nocobase.com:13000'))).toBe(false);
|
||||
});
|
||||
|
||||
// ========== IPv4 ==========
|
||||
test('matches IPv4 with port 127.0.0.1:13000', () => {
|
||||
expect(isDomainMatch('http://127.0.0.1:13000', wrapKeyData('127.0.0.1:13000'))).toBe(true);
|
||||
});
|
||||
|
||||
test('fails IPv4 if port mismatch', () => {
|
||||
expect(isDomainMatch('http://127.0.0.1:13001', wrapKeyData('127.0.0.1:13000'))).toBe(false);
|
||||
});
|
||||
|
||||
// ========== IPv6 ==========
|
||||
test('matches IPv6 format [::1]:13000', () => {
|
||||
expect(isDomainMatch('http://[::1]:13000', wrapKeyData('[::1]:13000'))).toBe(true);
|
||||
});
|
||||
|
||||
test('fails IPv6 when port mismatch', () => {
|
||||
expect(isDomainMatch('http://[::1]:13001', wrapKeyData('[::1]:13000'))).toBe(false);
|
||||
});
|
||||
|
||||
// ========== 多个域名(逗号分隔) ==========
|
||||
test('matches one of multiple domains', () => {
|
||||
expect(isDomainMatch('https://nocobase2.com', wrapKeyData('nocobase1.com,nocobase2.com'))).toBe(true);
|
||||
});
|
||||
|
||||
test('fails if none match in multiple domains', () => {
|
||||
expect(isDomainMatch('https://notmatched.com', wrapKeyData('nocobase1.com,nocobase2.com'))).toBe(false);
|
||||
});
|
||||
|
||||
// ========== 空值 ==========
|
||||
test('returns false if currentDomain empty', () => {
|
||||
expect(isDomainMatch('', wrapKeyData('nocobase.com'))).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if license domain empty', () => {
|
||||
expect(isDomainMatch('https://nocobase.com', wrapKeyData(''))).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if keyData null', () => {
|
||||
expect(isDomainMatch('https://nocobase.com', null as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { getClientDomain } from '../utils';
|
||||
|
||||
describe('getClientDomain', () => {
|
||||
test('should return origin from valid referer', () => {
|
||||
const ctx = {
|
||||
get: () => 'https://example.com/page?a=1',
|
||||
};
|
||||
expect(getClientDomain(ctx)).toBe('https://example.com');
|
||||
});
|
||||
|
||||
test('should ignore invalid referer and fallback', () => {
|
||||
const ctx = {
|
||||
get: () => 'not a valid url',
|
||||
protocol: 'http',
|
||||
host: 'localhost:3000',
|
||||
};
|
||||
expect(getClientDomain(ctx)).toBe('http://localhost:3000');
|
||||
});
|
||||
|
||||
test('fallback when no referer', () => {
|
||||
const ctx = {
|
||||
get: () => undefined,
|
||||
protocol: 'https',
|
||||
host: 'myapp.com',
|
||||
};
|
||||
expect(getClientDomain(ctx)).toBe('https://myapp.com');
|
||||
});
|
||||
|
||||
test('fallback using x-forwarded headers', () => {
|
||||
const ctx = {
|
||||
get: () => undefined,
|
||||
headers: {
|
||||
'x-forwarded-proto': 'https',
|
||||
'x-forwarded-host': 'forwarded.com',
|
||||
},
|
||||
};
|
||||
expect(getClientDomain(ctx)).toBe('https://forwarded.com');
|
||||
});
|
||||
|
||||
test('fallback when ctx.get is not a function', () => {
|
||||
const ctx = {
|
||||
get: 'not-a-function',
|
||||
protocol: 'http',
|
||||
host: 'domain.com',
|
||||
};
|
||||
expect(getClientDomain(ctx)).toBe('http://domain.com');
|
||||
});
|
||||
|
||||
test('fallback using ctx.request.*', () => {
|
||||
const ctx = {
|
||||
get: () => undefined,
|
||||
request: {
|
||||
protocol: 'https',
|
||||
host: 'reqhost.com',
|
||||
},
|
||||
};
|
||||
expect(getClientDomain(ctx)).toBe('https://reqhost.com');
|
||||
});
|
||||
|
||||
test('return empty string if no host found', () => {
|
||||
const ctx = {
|
||||
get: () => undefined,
|
||||
protocol: 'http',
|
||||
};
|
||||
expect(getClientDomain(ctx)).toBe('');
|
||||
});
|
||||
|
||||
test('ctx is null', () => {
|
||||
expect(getClientDomain(null)).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
import { testPkgConnection, testPkgLogin } from '../utils/pkg';
|
||||
import { isEnvMatch } from '../utils';
|
||||
|
||||
describe('conect to pkg.nocobase.com', () => {
|
||||
it('should connect to pkg.nocobase.com', async () => {
|
||||
const result = await testPkgConnection();
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
it('should login to pkg.nocobase.com', async () => {
|
||||
const result = await testPkgLogin({});
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('env match', () => {
|
||||
const envWithDbId = {
|
||||
sys: 'Windows',
|
||||
osVer: '10',
|
||||
db: { name: 'MySQL', oid: 1, ver: '5.7', id: '123' },
|
||||
};
|
||||
const envWithNewVersion = {
|
||||
sys: 'Windows',
|
||||
osVer: '10',
|
||||
db: { name: 'MySQL', oid: 1, ver: '5.7.2', id: '1234' },
|
||||
};
|
||||
const licenseKey = {
|
||||
instanceData: {
|
||||
sys: 'Windows',
|
||||
osVer: '10',
|
||||
db: { name: 'MySQL', oid: 1, ver: '5.7' },
|
||||
},
|
||||
};
|
||||
const licenseKeyWithId = {
|
||||
instanceData: {
|
||||
sys: 'Windows',
|
||||
osVer: '10',
|
||||
db: { name: 'MySQL', oid: 1, ver: '5.7', id: '1234' },
|
||||
},
|
||||
};
|
||||
it('new env match old key', async () => {
|
||||
const result = isEnvMatch(envWithDbId as any, licenseKey as any);
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
it('new env match old key with different Version', async () => {
|
||||
const result = isEnvMatch(envWithNewVersion as any, licenseKey as any);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
it('new env match new key', async () => {
|
||||
const result = isEnvMatch(envWithNewVersion as any, licenseKeyWithId as any);
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
const makeEnv = (env: any) => env;
|
||||
const makeKeyData = (instanceData: any) => ({
|
||||
instanceData,
|
||||
});
|
||||
|
||||
describe('isEnvMatch', () => {
|
||||
// ========== 1. db.id 存在时,只比较 id ==========
|
||||
test('matches when db.id exists on both sides and id is equal', () => {
|
||||
const env = makeEnv({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: { id: 'abc', type: 'pg', name: 'noco', ver: '14', oid: 1 },
|
||||
});
|
||||
|
||||
const keyData: any = makeKeyData({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: { id: 'abc', type: 'other', name: 'ignore', ver: 'ignore', oid: 999 },
|
||||
});
|
||||
|
||||
expect(isEnvMatch(env, keyData)).toBe(true);
|
||||
});
|
||||
|
||||
test('fails when db.id exists but not equal', () => {
|
||||
const env = makeEnv({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: { id: 'abc' },
|
||||
});
|
||||
const keyData: any = makeKeyData({
|
||||
db: { id: 'xyz' },
|
||||
});
|
||||
|
||||
expect(isEnvMatch(env, keyData)).toBe(false);
|
||||
});
|
||||
|
||||
// ========== 2. db.id 缺失 → 比较其它字段 ==========
|
||||
test('matches when db.id missing and full db fields match', () => {
|
||||
const env = makeEnv({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: {
|
||||
type: 'pg',
|
||||
name: 'noco',
|
||||
ver: '14',
|
||||
oid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const keyData: any = makeKeyData({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: {
|
||||
type: 'pg',
|
||||
name: 'noco',
|
||||
ver: '14',
|
||||
oid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(isEnvMatch(env, keyData)).toBe(true);
|
||||
});
|
||||
|
||||
test('fails when db.id missing and db fields differ', () => {
|
||||
const env = makeEnv({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: {
|
||||
type: 'pg',
|
||||
name: 'noco',
|
||||
ver: '14',
|
||||
oid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const keyData: any = makeKeyData({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: {
|
||||
type: 'pg',
|
||||
name: 'other', // diff
|
||||
ver: '14',
|
||||
oid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(isEnvMatch(env, keyData)).toBe(false);
|
||||
});
|
||||
|
||||
test('fails when sys or osVer differ (id missing)', () => {
|
||||
const env = makeEnv({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: { type: 'pg', name: 'a', ver: '14', oid: 1 },
|
||||
});
|
||||
|
||||
const keyData: any = makeKeyData({
|
||||
sys: 'windows',
|
||||
osVer: 'ubuntu',
|
||||
db: { type: 'pg', name: 'a', ver: '14', oid: 1 },
|
||||
});
|
||||
|
||||
expect(isEnvMatch(env, keyData)).toBe(false);
|
||||
});
|
||||
|
||||
// ========== 3. 字段顺序不同但内容一致(isEqual 保障) ==========
|
||||
test('matches when object field order differs', () => {
|
||||
const env = makeEnv({
|
||||
osVer: 'ubuntu',
|
||||
sys: 'linux',
|
||||
db: {
|
||||
name: 'noco',
|
||||
type: 'pg',
|
||||
oid: 1,
|
||||
ver: '14',
|
||||
},
|
||||
});
|
||||
|
||||
const keyData: any = makeKeyData({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: {
|
||||
type: 'pg',
|
||||
ver: '14',
|
||||
name: 'noco',
|
||||
oid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(isEnvMatch(env, keyData)).toBe(true);
|
||||
});
|
||||
|
||||
// ========== 4. env / keyData 不存在 ==========
|
||||
test('fails when env is null', () => {
|
||||
expect(isEnvMatch(null as any, makeKeyData({ db: {} })) as any).toBe(false);
|
||||
});
|
||||
|
||||
test('fails when keyData.instanceData is null', () => {
|
||||
expect(isEnvMatch(makeEnv({ db: {} }), null as any) as any).toBe(false);
|
||||
});
|
||||
|
||||
test('fails when both missing db', () => {
|
||||
const env = makeEnv({ sys: 'linux', osVer: 'ubuntu' });
|
||||
const keyData: any = makeKeyData({ sys: 'linux', osVer: 'ubuntu' });
|
||||
|
||||
// db is missing → normalize will produce null db
|
||||
expect(isEnvMatch(env, keyData)).toBe(false);
|
||||
});
|
||||
|
||||
// ========== 5. Only one side has db.id ==========
|
||||
test('fails when only env has db.id but keyData does not', () => {
|
||||
const env = makeEnv({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: { id: 'abc' },
|
||||
});
|
||||
|
||||
const keyData: any = makeKeyData({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: { type: 'pg' },
|
||||
});
|
||||
|
||||
expect(isEnvMatch(env, keyData)).toBe(false);
|
||||
});
|
||||
|
||||
test('fails when only keyData has db.id but env does not', () => {
|
||||
const env = makeEnv({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: { type: 'pg' },
|
||||
});
|
||||
|
||||
const keyData: any = makeKeyData({
|
||||
sys: 'linux',
|
||||
osVer: 'ubuntu',
|
||||
db: { id: 'abc' },
|
||||
});
|
||||
|
||||
expect(isEnvMatch(env, keyData)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -8,3 +8,4 @@
|
||||
*/
|
||||
|
||||
export { default } from './plugin';
|
||||
export * from './utils';
|
||||
|
||||
@@ -8,8 +8,19 @@
|
||||
*/
|
||||
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { getInstanceId, saveLicenseKey, isLicenseKeyExists } from './utils';
|
||||
import { keyDecrypt } from '@nocobase/license-kit';
|
||||
import {
|
||||
getInstanceId,
|
||||
saveLicenseKey,
|
||||
isLicenseKeyExists,
|
||||
getLicenseValidate,
|
||||
CACHE_KEY,
|
||||
getLicenseStatus,
|
||||
testPkgConnection,
|
||||
testPkgLogin,
|
||||
testServiceConnection,
|
||||
getLocalKeyData,
|
||||
} from './utils';
|
||||
import pick from 'lodash/pick';
|
||||
|
||||
export class PluginLicenseServer extends Plugin {
|
||||
async afterAdd() {}
|
||||
@@ -26,18 +37,72 @@ export class PluginLicenseServer extends Plugin {
|
||||
},
|
||||
'license-key': async (ctx, next) => {
|
||||
const { licenseKey } = ctx.request.body;
|
||||
try {
|
||||
keyDecrypt(licenseKey);
|
||||
} catch (e) {
|
||||
return ctx.throw(500, ctx.t('Invalid license key', { ns: '@nocobase/plugin-license' }));
|
||||
const licenseValidate = await getLicenseValidate({ key: licenseKey, ctx });
|
||||
const licenseStatus = await getLicenseStatus(licenseValidate.keyData);
|
||||
ctx.body = {
|
||||
...licenseValidate,
|
||||
licenseStatus,
|
||||
};
|
||||
if (ctx.body.envMatch && ctx.body.domainMatch && ctx.body.licenseStatus === 'active') {
|
||||
await saveLicenseKey(licenseKey, ctx);
|
||||
}
|
||||
await saveLicenseKey(licenseKey);
|
||||
await next();
|
||||
},
|
||||
'is-exists': async (ctx, next) => {
|
||||
ctx.body = await isLicenseKeyExists();
|
||||
await next();
|
||||
},
|
||||
'license-validate': async (ctx, next) => {
|
||||
const isExist = await isLicenseKeyExists();
|
||||
if (!isExist) {
|
||||
ctx.body = {
|
||||
keyExist: false,
|
||||
};
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
const licenseValidate = await getLicenseValidate({ ctx });
|
||||
ctx.body = {
|
||||
keyExist: true,
|
||||
...licenseValidate,
|
||||
keyData: pick(licenseValidate.keyData, ['service']),
|
||||
};
|
||||
await next();
|
||||
},
|
||||
'service-validate': async (ctx, next) => {
|
||||
const keyExist = await isLicenseKeyExists();
|
||||
if (!keyExist) {
|
||||
ctx.body = {
|
||||
keyExist,
|
||||
};
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
let isPkgConnection = false;
|
||||
try {
|
||||
isPkgConnection = await testPkgConnection();
|
||||
} catch (e) {
|
||||
isPkgConnection = false;
|
||||
}
|
||||
let isPkgLogin = false;
|
||||
let isServiceConnection = false;
|
||||
try {
|
||||
const keyData = await getLocalKeyData();
|
||||
isPkgLogin = await testPkgLogin(keyData);
|
||||
isServiceConnection = await testServiceConnection(keyData);
|
||||
} catch (e) {
|
||||
isPkgLogin = false;
|
||||
isServiceConnection = false;
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
keyExist,
|
||||
isPkgConnection,
|
||||
isPkgLogin,
|
||||
isServiceConnection,
|
||||
};
|
||||
await next();
|
||||
},
|
||||
},
|
||||
});
|
||||
this.app.acl.allow('license', '*', 'loggedIn');
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Env, getEnvAsync } from '@nocobase/license-kit';
|
||||
import { KeyData } from './interface';
|
||||
import omit from 'lodash/omit';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
export function getClientDomain(ctx: any) {
|
||||
if (!ctx) return '';
|
||||
|
||||
const referer = typeof ctx.get === 'function' ? ctx.get('referer') : null;
|
||||
if (referer) {
|
||||
try {
|
||||
return new URL(referer).origin;
|
||||
} catch {
|
||||
console.error('Invalid Referer URL:', referer);
|
||||
}
|
||||
}
|
||||
|
||||
const protocol = ctx.headers?.['x-forwarded-proto'] || ctx.protocol || ctx.request?.protocol || 'http';
|
||||
const host = ctx.headers?.['x-forwarded-host'] || ctx.host || ctx.request?.host || '';
|
||||
return host ? `${protocol}://${host}` : '';
|
||||
}
|
||||
|
||||
function matchSingleDomain(licenseDomain: string, currentDomain: string) {
|
||||
let hostname = '';
|
||||
let port = '';
|
||||
|
||||
try {
|
||||
const url = new URL(currentDomain);
|
||||
hostname = url.hostname;
|
||||
port = url.port ? `:${url.port}` : '';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fullDomain = hostname + port;
|
||||
|
||||
if (!licenseDomain.includes('*')) {
|
||||
return fullDomain === licenseDomain;
|
||||
}
|
||||
|
||||
const base = licenseDomain.replace('*', '');
|
||||
return fullDomain.endsWith(base);
|
||||
}
|
||||
|
||||
export function isDomainMatch(currentDomain: string, keyData: KeyData) {
|
||||
if (!keyData?.licenseKey?.domain || !currentDomain) return false;
|
||||
|
||||
const licenseDomains = keyData.licenseKey.domain
|
||||
.split(',')
|
||||
.map((d) => d.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return licenseDomains.some((licenseDomain) => matchSingleDomain(licenseDomain, currentDomain));
|
||||
}
|
||||
|
||||
export function isEnvMatch(env: Env, keyData: KeyData) {
|
||||
return isDbMatch(env, keyData) && isSysMatch(env, keyData);
|
||||
}
|
||||
|
||||
export function isDbMatch(env: Env, keyData: KeyData) {
|
||||
const a = env?.db;
|
||||
const b = keyData?.instanceData?.db;
|
||||
|
||||
if (!a || !b) return false;
|
||||
|
||||
// If both have id, compare by id only
|
||||
if (a.id && b.id) return a.id === b.id;
|
||||
|
||||
// Otherwise compare shallow equality excluding id
|
||||
return isEqual(omit(a, ['id']), omit(b, ['id']));
|
||||
}
|
||||
|
||||
export function isSysMatch(env: Env, keyData: KeyData) {
|
||||
const instance = keyData?.instanceData;
|
||||
if (!env || !instance) return false;
|
||||
|
||||
const normalize = (item: Env | null | undefined) => {
|
||||
if (!item) return null;
|
||||
|
||||
return {
|
||||
sys: item.sys ?? null,
|
||||
osVer: item.osVer ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
const a = normalize(env);
|
||||
const b = normalize(instance as Env);
|
||||
|
||||
return isEqual(a, b);
|
||||
}
|
||||
|
||||
let envPromise;
|
||||
export function getEnvOnce() {
|
||||
return envPromise ?? (envPromise = getEnvAsync());
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export * from './key';
|
||||
export * from './env';
|
||||
export * from './interface';
|
||||
export * from './license';
|
||||
export * from './instance';
|
||||
export * from './plugin';
|
||||
export * from './pkg';
|
||||
-6
@@ -31,12 +31,6 @@ export async function createInstanceId(force = false) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveLicenseKey(licenseKey: string) {
|
||||
const dir = path.resolve(process.cwd(), 'storage/.license');
|
||||
const filePath = path.resolve(dir, 'license-key');
|
||||
fs.writeFileSync(filePath, licenseKey);
|
||||
}
|
||||
|
||||
export async function isLicenseKeyExists() {
|
||||
const dir = path.resolve(process.cwd(), 'storage/.license');
|
||||
const filePath = path.resolve(dir, 'license-key');
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export interface KeyData {
|
||||
upgradeExpirationDate: string;
|
||||
licenseKey: {
|
||||
id: string;
|
||||
licensee: string;
|
||||
desc: string;
|
||||
type: string;
|
||||
domain: string;
|
||||
licenseStatus: string;
|
||||
};
|
||||
plugins: Array<{
|
||||
displayName: string;
|
||||
packageName: string;
|
||||
updateExpirationDate?: string;
|
||||
}>;
|
||||
instanceData: {
|
||||
timestamp: string;
|
||||
sys: string;
|
||||
osVer: string;
|
||||
kVer: string;
|
||||
hostname: string;
|
||||
mac: string;
|
||||
db: {
|
||||
type: string;
|
||||
name: string;
|
||||
oid: number;
|
||||
ver: string;
|
||||
id?: string;
|
||||
};
|
||||
container: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
service: {
|
||||
domain: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
accessKeyId: string;
|
||||
accessKeySecret: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export const enum LICENSE_ERROR {
|
||||
KEY_FORMAT_ERROR = 'KEY_FORMAT_ERROR',
|
||||
KEY_NOT_FOUND = 'KEY_NOT_FOUND',
|
||||
KEY_READ_ERROR = 'KEY_READ_ERROR',
|
||||
}
|
||||
|
||||
export const CACHE_KEY = 'license-key';
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { keyDecrypt } from '@nocobase/license-kit';
|
||||
import { Context } from 'koa';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { KeyData } from './interface';
|
||||
import { CACHE_KEY } from './interface';
|
||||
import axios from 'axios';
|
||||
import { logger } from '@nocobase/logger';
|
||||
|
||||
export async function request(
|
||||
{
|
||||
url,
|
||||
method = 'GET',
|
||||
body,
|
||||
headers,
|
||||
timeout = 5000,
|
||||
}: {
|
||||
url: string;
|
||||
method?: string;
|
||||
body?: Record<string, any>;
|
||||
headers?: any;
|
||||
timeout?: number;
|
||||
},
|
||||
ctx?: Context,
|
||||
) {
|
||||
const xHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Hostname': new URL(url).hostname,
|
||||
'X-Timezone': ctx?.request?.headers?.['x-timezone'],
|
||||
'X-Locale': ctx?.request?.headers?.['x-locale'],
|
||||
'X-Role': ctx?.request?.headers?.['x-role'],
|
||||
'X-Authenticator': ctx?.request?.headers?.['x-authenticator'],
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url,
|
||||
method,
|
||||
timeout,
|
||||
headers: {
|
||||
...xHeaders,
|
||||
...headers,
|
||||
},
|
||||
data: body || {},
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (err: any) {
|
||||
if (err.code === 'ECONNABORTED') {
|
||||
throw new Error(`Request timeout after ${timeout}ms`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveLicenseKey(licenseKey: string, ctx?: any) {
|
||||
const dir = path.resolve(process.cwd(), 'storage/.license');
|
||||
const filePath = path.resolve(dir, 'license-key');
|
||||
await fs.promises.writeFile(filePath, licenseKey);
|
||||
|
||||
if (ctx && ctx.cache) {
|
||||
await ctx.cache.set('license-key', licenseKey);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLocalKeyData() {
|
||||
const keyFile = path.resolve(process.cwd(), 'storage/.license/license-key');
|
||||
try {
|
||||
const key = (await fs.promises.readFile(keyFile, 'utf8')).trim();
|
||||
return JSON.parse(keyDecrypt(key));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getKey(ctx?: Context): Promise<string> {
|
||||
let key: string | undefined;
|
||||
const keyFile = path.resolve(process.cwd(), 'storage/.license/license-key');
|
||||
|
||||
if (ctx?.cache) {
|
||||
try {
|
||||
key = await ctx.cache.get(CACHE_KEY);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to get license key from cache', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
try {
|
||||
key = (await fs.promises.readFile(keyFile, 'utf8')).trim();
|
||||
} catch (e) {
|
||||
logger.error('Failed to read license key file', e);
|
||||
throw new Error('notfound');
|
||||
}
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
logger.error('License key not found');
|
||||
throw new Error('notfound');
|
||||
}
|
||||
|
||||
let keyData: KeyData;
|
||||
try {
|
||||
keyData = JSON.parse(keyDecrypt(key));
|
||||
} catch (e) {
|
||||
logger.error('Failed to parse license key');
|
||||
throw new Error('invalid');
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await request({
|
||||
url: `${keyData?.service?.domain}/api/license_keys:getKey`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
access_key_id: keyData?.accessKeyId,
|
||||
access_key_secret: keyData?.accessKeySecret,
|
||||
},
|
||||
headers: keyData?.service?.headers || {},
|
||||
});
|
||||
// logger.info('Successfully retrieved the remote license key');
|
||||
const { key: remoteKey } = data || {};
|
||||
if (remoteKey && typeof remoteKey === 'string') {
|
||||
await saveLicenseKey(remoteKey, ctx);
|
||||
return remoteKey;
|
||||
}
|
||||
} catch (e) {
|
||||
// logger.warn('Unable to retrieve the remote license key', e?.message ? { message: e?.message } : e);
|
||||
return key;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
export async function getLicenseStatus(keyData: KeyData): Promise<'active' | 'invalid'> {
|
||||
if (!keyData) {
|
||||
return 'invalid';
|
||||
}
|
||||
if (keyData?.licenseKey?.licenseStatus === 'invalid') {
|
||||
return 'invalid';
|
||||
}
|
||||
try {
|
||||
const { data } = await request({
|
||||
url: `${keyData?.service?.domain}/api/license_keys:getKeyStatus`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
access_key_id: keyData?.accessKeyId,
|
||||
access_key_secret: keyData?.accessKeySecret,
|
||||
},
|
||||
headers: keyData?.service?.headers || {},
|
||||
});
|
||||
if (data?.status === 'active') {
|
||||
return 'active';
|
||||
} else {
|
||||
return 'invalid';
|
||||
}
|
||||
} catch (e) {
|
||||
return 'active';
|
||||
}
|
||||
}
|
||||
|
||||
export function parseKey(key: string): KeyData {
|
||||
try {
|
||||
return JSON.parse(keyDecrypt(key));
|
||||
} catch (e) {
|
||||
throw new Error('invalid');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getKeyInfo(ctx?: Context): Promise<KeyData> {
|
||||
const key = await getKey(ctx);
|
||||
return parseKey(key);
|
||||
}
|
||||
|
||||
export function isDateExpired(upgradeExpirationDate: string): boolean {
|
||||
if (!upgradeExpirationDate) {
|
||||
return true;
|
||||
}
|
||||
return new Date() > new Date(upgradeExpirationDate);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { getEnvAsync, Env } from '@nocobase/license-kit';
|
||||
import { getKey, parseKey, isDateExpired } from './key';
|
||||
import { isDomainMatch, getClientDomain, isDbMatch, isSysMatch, getEnvOnce } from './env';
|
||||
import { getNocoBasePkgUrl, testPkgLogin, testServiceConnection } from './pkg';
|
||||
import { KeyData } from './interface';
|
||||
import { getPlugins, getPluginsLicenseStatus, PluginData } from './plugin';
|
||||
|
||||
export interface LicenseValidateResult {
|
||||
current: {
|
||||
env: Env;
|
||||
domain: string;
|
||||
};
|
||||
dbMatch: boolean;
|
||||
sysMatch: boolean;
|
||||
envMatch: boolean;
|
||||
domainMatch: boolean;
|
||||
isExpired: boolean;
|
||||
isServiceConnection: boolean;
|
||||
isPkgLogin: boolean;
|
||||
keyData: KeyData;
|
||||
pkgUrl: string;
|
||||
licenseStatus: 'active' | 'invalid';
|
||||
pluginsLicensed: boolean;
|
||||
plugins: PluginData[];
|
||||
keyStatus?: 'invalid' | 'notfound';
|
||||
}
|
||||
|
||||
export async function getLicenseValidate({ key, ctx }: { key?: string; ctx?: any }): Promise<LicenseValidateResult> {
|
||||
const currentDomain = getClientDomain(ctx);
|
||||
const currentEnv = await getEnvOnce();
|
||||
let keyStatus;
|
||||
let keyData;
|
||||
try {
|
||||
const keyStr = key || (await getKey(ctx));
|
||||
keyData = parseKey(keyStr);
|
||||
} catch (e) {
|
||||
keyStatus = e?.message;
|
||||
}
|
||||
// const licenseStatus = await getLicenseStatus(keyData);
|
||||
const domainMatch = isDomainMatch(currentDomain, keyData);
|
||||
const dbMatch = isDbMatch(currentEnv, keyData);
|
||||
const sysMatch = isSysMatch(currentEnv, keyData);
|
||||
const envMatch = dbMatch && sysMatch;
|
||||
const isExpired = isDateExpired(keyData?.upgradeExpirationDate);
|
||||
const plugins = await getPlugins({ keyData, ctx });
|
||||
const pluginsLicensed = getPluginsLicenseStatus({ plugins });
|
||||
const isPkgLogin = await testPkgLogin(keyData);
|
||||
const isServiceConnection = await testServiceConnection(keyData);
|
||||
|
||||
return {
|
||||
current: {
|
||||
env: currentEnv,
|
||||
domain: currentDomain ? new URL(currentDomain).host : '',
|
||||
},
|
||||
keyData,
|
||||
dbMatch,
|
||||
sysMatch,
|
||||
envMatch,
|
||||
domainMatch,
|
||||
isExpired,
|
||||
isPkgLogin,
|
||||
isServiceConnection,
|
||||
pkgUrl: getNocoBasePkgUrl(),
|
||||
plugins,
|
||||
pluginsLicensed,
|
||||
keyStatus,
|
||||
licenseStatus: keyData?.licenseKey?.licenseStatus || 'active',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import axios from 'axios';
|
||||
import { KeyData } from './interface';
|
||||
|
||||
export function getNocoBasePkgUrl() {
|
||||
return process.env?.NOCOBASE_PKG_URL || 'https://pkg.nocobase.com/';
|
||||
}
|
||||
|
||||
async function testDomain(url: string) {
|
||||
return new Promise((resolve) => {
|
||||
const client = url.startsWith('https') ? https : http;
|
||||
|
||||
const req = client.get(url, (res) => {
|
||||
resolve({
|
||||
reachable: true,
|
||||
statusCode: res.statusCode,
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
|
||||
req.on('error', (err: any) => {
|
||||
resolve({
|
||||
reachable: false,
|
||||
error: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
req.setTimeout(5000, () => {
|
||||
req.destroy();
|
||||
resolve({
|
||||
reachable: false,
|
||||
error: 'Timeout',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function testPkgConnection() {
|
||||
const res: any = await testDomain(getNocoBasePkgUrl());
|
||||
return res.reachable;
|
||||
}
|
||||
|
||||
export async function testPkgLogin(keyData: KeyData | Record<string, any>) {
|
||||
try {
|
||||
const NOCOBASE_PKG_URL = getNocoBasePkgUrl();
|
||||
const { accessKeyId, accessKeySecret } = keyData || {};
|
||||
if (!accessKeyId || !accessKeySecret) {
|
||||
return false;
|
||||
}
|
||||
const credentials = { username: accessKeyId, password: accessKeySecret };
|
||||
const res1 = await axios.post(`${NOCOBASE_PKG_URL}-/verdaccio/sec/login`, credentials, {
|
||||
responseType: 'json',
|
||||
});
|
||||
const token = res1.data.token;
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testServiceConnection(keyData: KeyData) {
|
||||
if (!keyData?.service?.domain) {
|
||||
return false;
|
||||
}
|
||||
const res: any = await testDomain(keyData?.service?.domain);
|
||||
return res.reachable;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { isDateExpired } from './key';
|
||||
import { KeyData } from './interface';
|
||||
|
||||
export interface PluginData {
|
||||
name: string;
|
||||
packageName: string;
|
||||
status: string;
|
||||
expirationDate: string;
|
||||
enabled: boolean;
|
||||
installed: boolean;
|
||||
}
|
||||
|
||||
export async function getPlugins({ keyData, ctx }: { keyData: KeyData; ctx: any }) {
|
||||
const locale = ctx.getCurrentLocale();
|
||||
// 获取所有已安装的插件
|
||||
const plugin = ctx.app.pm.get('nocobase') as any;
|
||||
const localPlugins = await plugin.getAllPlugins(locale);
|
||||
// 获取已启用的插件实例
|
||||
const currentPluginInstances = [];
|
||||
for (const [_, p] of ctx.app.pm.getPlugins()) {
|
||||
currentPluginInstances.push({
|
||||
name: p.options.name,
|
||||
__isCommercial: p.options.__isCommercial,
|
||||
packageName: p.options.packageName,
|
||||
enabled: p.options.enabled,
|
||||
});
|
||||
}
|
||||
// key的商业插件清单
|
||||
const licensedPlugins = keyData?.plugins || [];
|
||||
// 所有的商业插件,包含key中清单和本地 __isCommercial 标志的清单
|
||||
const allCommericalPluginNames = [...licensedPlugins.map((p) => p.packageName)];
|
||||
localPlugins.forEach((p) => {
|
||||
const instance = currentPluginInstances?.find((plugin) => plugin.packageName === p.packageName);
|
||||
if (instance?.__isCommercial) {
|
||||
allCommericalPluginNames.push(p.packageName);
|
||||
}
|
||||
});
|
||||
// 已安装的商业插件,后续校验
|
||||
const installedCommericalPlugins = [];
|
||||
for (const plugin of localPlugins) {
|
||||
if (allCommericalPluginNames.includes(plugin.packageName)) {
|
||||
installedCommericalPlugins.push(plugin.packageName);
|
||||
}
|
||||
}
|
||||
const plugins: PluginData[] = [];
|
||||
for (const name of installedCommericalPlugins) {
|
||||
let status = 'unlicensed';
|
||||
const expirationDate = licensedPlugins?.find((p) => p.packageName === name)?.updateExpirationDate;
|
||||
if (expirationDate) {
|
||||
const isExpired = isDateExpired(expirationDate);
|
||||
if (isExpired) {
|
||||
status = 'expired';
|
||||
} else {
|
||||
status = 'licensed';
|
||||
}
|
||||
}
|
||||
const plugin = localPlugins.find((p) => p.packageName === name);
|
||||
plugins.push({
|
||||
name: plugin?.displayName || plugin?.name || name,
|
||||
packageName: name,
|
||||
status,
|
||||
expirationDate,
|
||||
enabled: plugin?.enabled,
|
||||
installed: true,
|
||||
});
|
||||
}
|
||||
const statusOrder = {
|
||||
unlicensed: 0,
|
||||
trial: 1,
|
||||
expired: 2,
|
||||
suspended: 3,
|
||||
revoked: 4,
|
||||
invalid: 5,
|
||||
pending: 6,
|
||||
licensed: 7,
|
||||
};
|
||||
const list = sortBy(plugins, (plugin) => [statusOrder[plugin.status] ?? 999, plugin.enabled ? 0 : 1, plugin.name]);
|
||||
licensedPlugins.forEach((p) => {
|
||||
const plugin = plugins.find((plugin) => plugin.packageName === p.packageName);
|
||||
if (!plugin) {
|
||||
list.push({
|
||||
name: p.displayName || p.packageName,
|
||||
packageName: p.packageName,
|
||||
status: 'licensed',
|
||||
expirationDate: p.updateExpirationDate,
|
||||
enabled: false,
|
||||
installed: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
export function getPluginsLicenseStatus({ plugins }: { plugins: PluginData[] }) {
|
||||
return plugins.filter((p) => p.status === 'unlicensed').length === 0;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"rootDir": "src/server/utils",
|
||||
"outDir": "utils",
|
||||
"strict": true,
|
||||
// "declaration": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/server/utils/env.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
var __importDefault =
|
||||
(this && this.__importDefault) ||
|
||||
function (mod) {
|
||||
return mod && mod.__esModule ? mod : { default: mod };
|
||||
};
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.isEnvMatch = exports.isDomainMatch = exports.getClientDomain = void 0;
|
||||
const omit_1 = __importDefault(require('lodash/omit'));
|
||||
const isEqual_1 = __importDefault(require('lodash/isEqual'));
|
||||
function getClientDomain(ctx) {
|
||||
if (!ctx) return '';
|
||||
const referer = typeof ctx.get === 'function' ? ctx.get('referer') : null;
|
||||
if (referer) {
|
||||
try {
|
||||
return new URL(referer).origin;
|
||||
} catch {
|
||||
console.error('Invalid Referer URL:', referer);
|
||||
}
|
||||
}
|
||||
const protocol = ctx.headers?.['x-forwarded-proto'] || ctx.protocol || ctx.request?.protocol || 'http';
|
||||
const host = ctx.headers?.['x-forwarded-host'] || ctx.host || ctx.request?.host || '';
|
||||
return host ? `${protocol}://${host}` : '';
|
||||
}
|
||||
exports.getClientDomain = getClientDomain;
|
||||
function matchSingleDomain(licenseDomain, currentDomain) {
|
||||
let hostname = '';
|
||||
let port = '';
|
||||
try {
|
||||
const url = new URL(currentDomain);
|
||||
hostname = url.hostname;
|
||||
port = url.port ? `:${url.port}` : '';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const fullDomain = hostname + port;
|
||||
if (!licenseDomain.includes('*')) {
|
||||
return fullDomain === licenseDomain;
|
||||
}
|
||||
const base = licenseDomain.replace('*', '');
|
||||
return fullDomain.endsWith(base);
|
||||
}
|
||||
function isDomainMatch(currentDomain, keyData) {
|
||||
if (!keyData?.licenseKey?.domain || !currentDomain) return false;
|
||||
const licenseDomains = keyData.licenseKey.domain
|
||||
.split(',')
|
||||
.map((d) => d.trim())
|
||||
.filter(Boolean);
|
||||
return licenseDomains.some((licenseDomain) => matchSingleDomain(licenseDomain, currentDomain));
|
||||
}
|
||||
exports.isDomainMatch = isDomainMatch;
|
||||
function isEnvMatch(env, keyData) {
|
||||
if (!env || !keyData?.instanceData) return false;
|
||||
if (!env.db || !keyData.instanceData.db) return false;
|
||||
const envDbId = env?.db?.id;
|
||||
const keyDbId = keyData?.instanceData?.db?.id;
|
||||
const matchById = Boolean(envDbId && keyDbId);
|
||||
const normalizeEnvData = (envItem) => {
|
||||
if (!envItem) return null;
|
||||
const { sys, osVer, db } = envItem;
|
||||
return {
|
||||
sys,
|
||||
osVer,
|
||||
db: matchById ? { id: db?.id } : (0, omit_1.default)(db, ['id']),
|
||||
};
|
||||
};
|
||||
const a = normalizeEnvData(env);
|
||||
const b = normalizeEnvData(keyData.instanceData);
|
||||
return (0, isEqual_1.default)(a, b);
|
||||
}
|
||||
exports.isEnvMatch = isEnvMatch;
|
||||
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
Reference in New Issue
Block a user