mirror of
https://github.com/nocobase/nocobase.git
synced 2026-09-21 13:52:17 +08:00
Feat/commercial (#6554)
* feat: add inst commond * feat: add inst commond and pkg commond support .key * fix: change isntanceid and key path * feat: support commerical plugin server inject * feat: support commerical for client build * feat: add compatibility code * fix: commercial plugin build * fix: build server/index * feat: add defaut txt match * feat: open obfuscate * feat: update license-kit 0.2.3 * fix: build include plugin-commercial * fix: plugin-commercial build * fix: test not include commercial * fix: remove log * fix: add license-kit to external * fix: obfuscate build * fix: client build external * fix: commercial build client * fix: plugin build just encrypt index * fix: instance id add line break * feat: add view-license-ke commond * feat: add upgrade to license * fix: change license key name * fix: pro plugin build * fix: remove tip in plugin page * fix: field rename * fix: remove unused code --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
@@ -26,12 +26,14 @@
|
||||
"@vercel/ncc": "0.36.1",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"bundle-require": "^5.1.0",
|
||||
"chalk": "2.4.2",
|
||||
"css-loader": "^6.8.1",
|
||||
"esbuild-register": "^3.4.2",
|
||||
"fast-glob": "^3.3.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-typescript": "6.0.0-alpha.1",
|
||||
"javascript-obfuscator": "^4.1.1",
|
||||
"less": "^4.2.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"postcss": "^8.4.29",
|
||||
@@ -49,7 +51,8 @@
|
||||
},
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"build": "tsup"
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch"
|
||||
},
|
||||
"gitHead": "d0b4efe4be55f8c79a98a331d99d9f8cf99021a1"
|
||||
}
|
||||
|
||||
@@ -14,9 +14,8 @@ import fg from 'fast-glob';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { build as tsupBuild } from 'tsup';
|
||||
|
||||
import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';
|
||||
import { EsbuildSupportExts, globExcludeFiles } from './constant';
|
||||
import * as bundleRequire from 'bundle-require';
|
||||
import { EsbuildSupportExts, globExcludeFiles, PLUGIN_COMMERCIAL } from './constant';
|
||||
import { PkgLog, UserConfig, getPackageJson } from './utils';
|
||||
import {
|
||||
buildCheck,
|
||||
@@ -27,6 +26,9 @@ import {
|
||||
getSourcePackages,
|
||||
} from './utils/buildPluginUtils';
|
||||
import { getDepPkgPath, getDepsConfig } from './utils/getDepsConfig';
|
||||
import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';
|
||||
import { obfuscate } from './utils/obfuscationResult';
|
||||
import pluginEsbuildCommercialInject from './plugins/pluginEsbuildCommercialInject';
|
||||
|
||||
const validExts = ['.ts', '.tsx', '.js', '.jsx', '.mjs'];
|
||||
const serverGlobalFiles: string[] = ['src/**', '!src/client/**', ...globExcludeFiles];
|
||||
@@ -55,6 +57,7 @@ const external = [
|
||||
'@nocobase/server',
|
||||
'@nocobase/test',
|
||||
'@nocobase/utils',
|
||||
'@nocobase/license-kit',
|
||||
|
||||
// @nocobase/auth
|
||||
'jsonwebtoken',
|
||||
@@ -311,10 +314,112 @@ export async function buildPluginServer(cwd: string, userConfig: UserConfig, sou
|
||||
await buildServerDeps(cwd, serverFiles, log);
|
||||
}
|
||||
|
||||
export async function buildPluginClient(cwd: string, userConfig: UserConfig, sourcemap: boolean, log: PkgLog) {
|
||||
export async function buildProPluginServer(cwd: string, userConfig: UserConfig, sourcemap: boolean, log: PkgLog) {
|
||||
log('build pro plugin server source');
|
||||
const packageJson = getPackageJson(cwd);
|
||||
const serverFiles = fg.globSync(serverGlobalFiles, { cwd, absolute: true });
|
||||
buildCheck({ cwd, packageJson, entry: 'server', files: serverFiles, log });
|
||||
const otherExts = Array.from(
|
||||
new Set(serverFiles.map((item) => path.extname(item)).filter((item) => !EsbuildSupportExts.includes(item))),
|
||||
);
|
||||
if (otherExts.length) {
|
||||
log('%s will not be processed, only be copied to the dist directory.', chalk.yellow(otherExts.join(',')));
|
||||
}
|
||||
|
||||
deleteServerFiles(cwd, log);
|
||||
|
||||
// remove compilerOptions.paths in tsconfig.json
|
||||
let tsconfig = bundleRequire.loadTsConfig(path.join(cwd, 'tsconfig.json'));
|
||||
fs.writeFileSync(path.join(cwd, 'tsconfig.json'), JSON.stringify({
|
||||
...tsconfig.data,
|
||||
compilerOptions: { ...tsconfig.data.compilerOptions, paths: [] }
|
||||
}, null, 2));
|
||||
tsconfig = bundleRequire.loadTsConfig(path.join(cwd, 'tsconfig.json'));
|
||||
|
||||
// convert all ts to js, some files may not be referenced by the entry file
|
||||
await tsupBuild(
|
||||
userConfig.modifyTsupConfig({
|
||||
entry: serverFiles,
|
||||
splitting: false,
|
||||
clean: false,
|
||||
bundle: false,
|
||||
silent: true,
|
||||
treeshake: false,
|
||||
target: 'node16',
|
||||
sourcemap,
|
||||
outDir: path.join(cwd, target_dir),
|
||||
format: 'cjs',
|
||||
skipNodeModulesBundle: true,
|
||||
loader: {
|
||||
...otherExts.reduce((prev, cur) => ({ ...prev, [cur]: 'copy' }), {}),
|
||||
'.json': 'copy',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const entryFile = path.join(cwd, 'src/server/index.ts');
|
||||
if (!fs.existsSync(entryFile)) {
|
||||
log('server entry file not found', entryFile);
|
||||
return;
|
||||
}
|
||||
|
||||
// plugin-commercial build to a bundle
|
||||
const externalOptions = {
|
||||
external: [],
|
||||
noExternal: [],
|
||||
onSuccess: async () => {},
|
||||
esbuildPlugins: [],
|
||||
};
|
||||
// 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.onSuccess = async () => {
|
||||
const serverFiles = [path.join(cwd, target_dir, 'server', 'index.js')];
|
||||
serverFiles.forEach((file) => {
|
||||
obfuscate(file);
|
||||
});
|
||||
};
|
||||
externalOptions.esbuildPlugins = [pluginEsbuildCommercialInject];
|
||||
}
|
||||
|
||||
// bundle all files、inject commercial code and obfuscate
|
||||
await tsupBuild(
|
||||
userConfig.modifyTsupConfig({
|
||||
entry: [entryFile],
|
||||
// minify: true,
|
||||
splitting: false,
|
||||
clean: false,
|
||||
bundle: true,
|
||||
silent: true,
|
||||
treeshake: false,
|
||||
target: 'node16',
|
||||
sourcemap,
|
||||
outDir: path.join(cwd, target_dir, 'server'),
|
||||
format: 'cjs',
|
||||
skipNodeModulesBundle: true,
|
||||
tsconfig: tsconfig.path,
|
||||
loader: {
|
||||
...otherExts.reduce((prev, cur) => ({ ...prev, [cur]: 'copy' }), {}),
|
||||
'.json': 'copy',
|
||||
},
|
||||
|
||||
...externalOptions,
|
||||
}),
|
||||
);
|
||||
fs.removeSync(tsconfig.path);
|
||||
|
||||
await buildServerDeps(cwd, serverFiles, log);
|
||||
}
|
||||
|
||||
export async function buildPluginClient(cwd: string, userConfig: any, sourcemap: boolean, log: PkgLog, isCommercial = false) {
|
||||
log('build plugin client');
|
||||
const packageJson = getPackageJson(cwd);
|
||||
const clientFiles = fg.globSync(clientGlobalFiles, { cwd, absolute: true });
|
||||
if (isCommercial) {
|
||||
const commercialFiles = fg.globSync(clientGlobalFiles, { cwd: path.join(process.cwd(), 'packages/pro-plugins', PLUGIN_COMMERCIAL), absolute: true });
|
||||
clientFiles.push(...commercialFiles);
|
||||
}
|
||||
const clientFileSource = clientFiles.map((item) => fs.readFileSync(item, 'utf-8'));
|
||||
const sourcePackages = getPackagesFromFiles(clientFileSource);
|
||||
const excludePackages = getExcludePackages(sourcePackages, external, pluginPrefix);
|
||||
@@ -440,31 +545,51 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou
|
||||
{
|
||||
test: /\.tsx$/,
|
||||
exclude: /[\\/]node_modules[\\/]/,
|
||||
loader: 'builtin:swc-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
tsx: true,
|
||||
use: [
|
||||
{
|
||||
loader: 'builtin:swc-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
tsx: true,
|
||||
},
|
||||
target: 'es5',
|
||||
},
|
||||
},
|
||||
target: 'es5',
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve('./plugins/pluginRspackCommercialLoader'),
|
||||
options: {
|
||||
isCommercial
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.ts$/,
|
||||
exclude: /[\\/]node_modules[\\/]/,
|
||||
loader: 'builtin:swc-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
use: [
|
||||
{
|
||||
loader: 'builtin:swc-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
},
|
||||
target: 'es5',
|
||||
},
|
||||
},
|
||||
target: 'es5',
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve('./plugins/pluginRspackCommercialLoader'),
|
||||
options: {
|
||||
isCommercial
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -571,7 +696,12 @@ __webpack_require__.p = (function() {
|
||||
}
|
||||
|
||||
export async function buildPlugin(cwd: string, userConfig: UserConfig, sourcemap: boolean, log: PkgLog) {
|
||||
await buildPluginClient(cwd, userConfig, sourcemap, log);
|
||||
await buildPluginServer(cwd, userConfig, sourcemap, log);
|
||||
if (cwd.includes('/pro-plugins/') && fs.existsSync(path.join(process.cwd(), 'packages/pro-plugins/', PLUGIN_COMMERCIAL))) {
|
||||
await buildPluginClient(cwd, userConfig, sourcemap, log, true);
|
||||
await buildProPluginServer(cwd, userConfig, sourcemap, log);
|
||||
} else {
|
||||
await buildPluginClient(cwd, userConfig, sourcemap, log);
|
||||
await buildPluginServer(cwd, userConfig, sourcemap, log);
|
||||
}
|
||||
writeExternalPackageVersion(cwd, log);
|
||||
}
|
||||
|
||||
@@ -43,8 +43,11 @@ export const PLUGINS_DIR = ['plugins', 'samples', 'pro-plugins']
|
||||
.filter(Boolean)
|
||||
.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 getPluginPackages = (packages: Package[]) =>
|
||||
packages.filter((item) => PLUGINS_DIR.some((pluginDir) => item.location.startsWith(pluginDir)));
|
||||
packages.filter((item) => PLUGINS_DIR.some((pluginDir) => item.location.startsWith(pluginDir))).sort((a, b) => {
|
||||
return a.name === PLUGIN_COMMERCIAL ? -1 : 1;
|
||||
});
|
||||
export const getPresetsPackages = (packages: Package[]) =>
|
||||
packages.filter((item) => item.location.startsWith(PRESETS_DIR));
|
||||
export const CORE_APP = path.join(PACKAGES_PATH, 'core/app');
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 fs from 'node:fs'
|
||||
|
||||
const pluginEsbuildCommercialInject = {
|
||||
name: 'plugin-esbuild-commercial-inject',
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /src\/server\/index\.ts$/ }, async (args) => {
|
||||
let source = fs.readFileSync(args.path, 'utf8');
|
||||
const regex = /export\s*\{\s*default\s*\}\s*from\s*(?:'([^']*)'|"([^"]*)");?/; // match: export { default } from './plugin';
|
||||
const regex2 = /export\s+default\s+([a-zA-Z_0-9]+)\s*;?/; // match: export default xxx;
|
||||
const match = source.match(regex);
|
||||
const match2 = source.match(regex2);
|
||||
if (match) {
|
||||
source = source.replace(regex, ``);
|
||||
const moduleName = match[1] || match[2];
|
||||
source =
|
||||
`
|
||||
import { withCommercial } from '@nocobase/plugin-commercial/server';
|
||||
import _plugin from '${moduleName}';
|
||||
export default withCommercial(_plugin);
|
||||
${source}
|
||||
`;
|
||||
console.log(`Insert commercial server code success`);
|
||||
} else if (match2) {
|
||||
source = source.replace(regex2, ``);
|
||||
const moduleName = match2[1] || match2[2];
|
||||
source =
|
||||
`
|
||||
import { withCommercial } from '@nocobase/plugin-commercial/server';
|
||||
${source}
|
||||
export default withCommercial(${moduleName});
|
||||
`;
|
||||
console.log(`Insert commercial server code success`);
|
||||
} else {
|
||||
console.error(`Insert commercial server code fail`);
|
||||
}
|
||||
|
||||
return {
|
||||
contents: source,
|
||||
loader: 'ts',
|
||||
}
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
export default pluginEsbuildCommercialInject
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 type { LoaderContext } from '@rspack/core';
|
||||
|
||||
export default function myLoader(
|
||||
this: LoaderContext<Record<string, unknown>>,
|
||||
source: string,
|
||||
) {
|
||||
const options = this.getOptions();
|
||||
if (!options?.isCommercial) {
|
||||
return source;
|
||||
}
|
||||
const isEntry = this.resourcePath.match(/client\/index\.(ts|tsx)/) && !this.resourcePath.includes('plugin-commercial');
|
||||
|
||||
if (isEntry) {
|
||||
const regex = /export\s+default\s+([a-zA-Z_0-9]+)\s*;?/; // match: export default xxx;
|
||||
const match = source.match(regex);
|
||||
if (match) {
|
||||
source = source.replace(regex, ``);
|
||||
const moduleName = match[1];
|
||||
source =
|
||||
`
|
||||
import { withCommercial } from '@nocobase/plugin-commercial/client';
|
||||
${source}
|
||||
export default withCommercial(${moduleName});
|
||||
`;
|
||||
console.log(`Insert commercial client code success`);
|
||||
} else {
|
||||
console.error(`Insert commercial client code fail`);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
return source;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 fs from 'fs-extra';
|
||||
import * as JavaScriptObfuscator from 'javascript-obfuscator';
|
||||
|
||||
export const obfuscate = (filePath: string) => {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const obfuscationResult = JavaScriptObfuscator.obfuscate(fileContent, {
|
||||
compact: true,
|
||||
controlFlowFlattening: false,
|
||||
deadCodeInjection: false,
|
||||
debugProtection: false,
|
||||
debugProtectionInterval: 0,
|
||||
disableConsoleOutput: true,
|
||||
identifierNamesGenerator: 'hexadecimal',
|
||||
log: false,
|
||||
numbersToExpressions: false,
|
||||
renameGlobals: false,
|
||||
selfDefending: true,
|
||||
simplify: true,
|
||||
splitStrings: false,
|
||||
stringArray: true,
|
||||
stringArrayCallsTransform: false,
|
||||
stringArrayEncoding: [],
|
||||
stringArrayIndexShift: true,
|
||||
stringArrayRotate: true,
|
||||
stringArrayShuffle: true,
|
||||
stringArrayWrappersCount: 1,
|
||||
stringArrayWrappersChainedCalls: true,
|
||||
stringArrayWrappersParametersMaxCount: 2,
|
||||
stringArrayWrappersType: 'variable',
|
||||
stringArrayThreshold: 0.75,
|
||||
unicodeEscapeSequence: false
|
||||
});
|
||||
fs.writeFileSync(filePath, obfuscationResult.getObfuscatedCode(), 'utf8');
|
||||
};
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nocobase/app": "1.7.0-alpha.13",
|
||||
"@nocobase/license-kit": "^0.2.3",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@umijs/utils": "3.5.20",
|
||||
"chalk": "^4.1.1",
|
||||
|
||||
@@ -35,6 +35,8 @@ module.exports = (cli) => {
|
||||
require('./upgrade')(cli);
|
||||
require('./postinstall')(cli);
|
||||
require('./pkg')(cli);
|
||||
require('./instance-id')(cli);
|
||||
require('./view-license-key')(cli);
|
||||
if (isPackageValid('@umijs/utils')) {
|
||||
require('./create-plugin')(cli);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 { Command } = require('commander');
|
||||
const { run, isDev } = require('../util');
|
||||
const { getInstanceIdAsync } = require('@nocobase/license-kit');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Command} cli
|
||||
*/
|
||||
module.exports = (cli) => {
|
||||
cli
|
||||
.command('generate-instance-id')
|
||||
.description('Generate InstanceID')
|
||||
.option('--force', 'Force generate InstanceID')
|
||||
.action(async (options) => {
|
||||
console.log('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);
|
||||
return;
|
||||
} else {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
try {
|
||||
const instanceId = await getInstanceIdAsync();
|
||||
fs.writeFileSync(filePath, instanceId + '\n');
|
||||
console.log(chalk.greenBright(`InstanceID saved to ${filePath}`));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -15,6 +15,7 @@ const tar = require('tar');
|
||||
const path = require('path');
|
||||
const { createStoragePluginsSymlink } = require('@nocobase/utils/plugin-symlink');
|
||||
const chalk = require('chalk');
|
||||
const { getAccessKeyPair } = require('../util');
|
||||
|
||||
class Package {
|
||||
data;
|
||||
@@ -248,10 +249,13 @@ module.exports = (cli) => {
|
||||
NOCOBASE_PKG_USERNAME,
|
||||
NOCOBASE_PKG_PASSWORD,
|
||||
} = process.env;
|
||||
if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD)) {
|
||||
const { accessKeyId, accessKeySecret } = getAccessKeyPair();
|
||||
if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD) && !(accessKeyId && accessKeySecret)) {
|
||||
return;
|
||||
}
|
||||
const credentials = { username: NOCOBASE_PKG_USERNAME, password: NOCOBASE_PKG_PASSWORD };
|
||||
const credentials = accessKeyId
|
||||
? { username: accessKeyId, password: accessKeySecret }
|
||||
: { username: NOCOBASE_PKG_USERNAME, password: NOCOBASE_PKG_PASSWORD };
|
||||
const pm = new PackageManager({ baseURL: NOCOBASE_PKG_URL });
|
||||
await pm.login(credentials);
|
||||
const file = path.resolve(__dirname, '../../package.json');
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 { Command } = require('commander');
|
||||
const { keyDecrypt } = require('@nocobase/license-kit');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Command} cli
|
||||
*/
|
||||
module.exports = (cli) => {
|
||||
cli
|
||||
.command('view-license-key')
|
||||
.description('View License Key')
|
||||
.action(async (options) => {
|
||||
const dir = path.resolve(process.cwd(), 'storage/.license');
|
||||
const filePath = path.resolve(dir, 'license-key');
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log('License key not found at ' + filePath);
|
||||
return;
|
||||
}
|
||||
const key = fs.readFileSync(filePath, 'utf-8');
|
||||
let keyDataStr;
|
||||
try {
|
||||
keyDataStr = keyDecrypt(key);
|
||||
} catch (e) {
|
||||
console.log('License key decrypt failed', e);
|
||||
return;
|
||||
}
|
||||
const keyData = JSON.parse(keyDataStr);
|
||||
const { accessKeyId, accessKeySecret } = keyData;
|
||||
console.log(chalk.greenBright(`Access Key ID: ${accessKeyId}`));
|
||||
console.log(chalk.greenBright(`Access Key Secret: ${accessKeySecret}`));
|
||||
});
|
||||
};
|
||||
@@ -18,6 +18,7 @@ const dotenv = require('dotenv');
|
||||
const fs = require('fs-extra');
|
||||
const os = require('os');
|
||||
const moment = require('moment-timezone');
|
||||
const { keyDecrypt } = require('@nocobase/license-kit');
|
||||
|
||||
exports.isPackageValid = (pkg) => {
|
||||
try {
|
||||
@@ -165,10 +166,11 @@ exports.promptForTs = () => {
|
||||
};
|
||||
|
||||
exports.downloadPro = async () => {
|
||||
const { NOCOBASE_PKG_USERNAME, NOCOBASE_PKG_PASSWORD } = process.env;
|
||||
if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD)) {
|
||||
return;
|
||||
}
|
||||
// 此处不再判定,由pkgg命令处理
|
||||
// const { NOCOBASE_PKG_USERNAME, NOCOBASE_PKG_PASSWORD } = process.env;
|
||||
// if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD)) {
|
||||
// return;
|
||||
// }
|
||||
await exports.run('yarn', ['nocobase', 'pkg', 'download-pro']);
|
||||
};
|
||||
|
||||
@@ -487,3 +489,21 @@ exports.generatePlugins = function () {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
exports.getAccessKeyPair = function () {
|
||||
const keyFile = resolve(process.cwd(), 'storage/.license/license-key');
|
||||
if (!fs.existsSync(keyFile)) {
|
||||
console.log(chalk.yellow('license-key file not found', keyFile));
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const str = fs.readFileSync(keyFile, 'utf-8');
|
||||
const keyDataStr = keyDecrypt(str);
|
||||
const keyData = JSON.parse(keyDataStr);
|
||||
const { accessKeyId, accessKeySecret } = keyData;
|
||||
return { accessKeyId, accessKeySecret };
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow('Key parse failed, please check your key'));
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1102,7 +1102,6 @@
|
||||
"Italic": "斜体",
|
||||
"Response record":"响应结果记录",
|
||||
"Colon":"冒号",
|
||||
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功后,会自动刷新这里选中的数据区块。",
|
||||
"No pages yet, please configure first": "暂无页面,请先配置",
|
||||
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "点击右上角的“界面配置”图标,进入界面配置模式",
|
||||
"Specifies a Permissions Policy for the <iframe>. The policy defines what features are available to the <iframe> (for example, access to the microphone, camera, battery, web-share, etc.) based on the origin of the request.": "用于为 <iframe> 指定其权限策略。该策略根据请求的来源规定 <iframe> 可以使用哪些特性(例如,访问麦克风、摄像头、电池、web 共享等)。",
|
||||
|
||||
@@ -270,7 +270,6 @@ const LocalPlugins = () => {
|
||||
// if (isRefresh) refresh();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ width: '100%' }}>
|
||||
<div
|
||||
style={{ marginBottom: theme.marginLG }}
|
||||
|
||||
@@ -279,7 +279,12 @@ const InternalAction: React.FC<InternalActionProps> = observer(function Com(prop
|
||||
const { modal } = App.useApp();
|
||||
const form = useForm();
|
||||
const aclCtx = useACLActionParamsContext();
|
||||
const { run, element, disabled: disableAction, loading: loadingOfUseAction } = useAction?.(actionCallback) || ({} as any);
|
||||
const {
|
||||
run,
|
||||
element,
|
||||
disabled: disableAction,
|
||||
loading: loadingOfUseAction,
|
||||
} = useAction?.(actionCallback) || ({} as any);
|
||||
const disabled = form.disabled || field.disabled || field.data?.disabled || propsDisabled || disableAction;
|
||||
const buttonStyle = useMemo(() => {
|
||||
return {
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* 变量:`系统设置`
|
||||
@@ -28,7 +28,7 @@ export const useSystemSettingsVariable = () => {
|
||||
label: t('System title'),
|
||||
isLeaf: true,
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -243,7 +243,6 @@ export class Auth {
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
async lostPassword(values: any): Promise<AxiosResponse<any>> {
|
||||
// 获取当前 URL 的查询参数
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { parsedValue } from "../parsedValue";
|
||||
import { parsedValue } from '../parsedValue';
|
||||
|
||||
describe('parsedValue', () => {
|
||||
it('should correctly parse simple templates', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parse } from "./json-templates";
|
||||
import { parse } from './json-templates';
|
||||
|
||||
function appendArrayColumn(scope, key) {
|
||||
const paths = key.split('.');
|
||||
|
||||
+14
-14
@@ -18,23 +18,23 @@ import { useSearchParams } from 'react-router-dom';
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useSearchParams: vi.fn(() => [
|
||||
{
|
||||
get: (key: string) => key === 'name' ? 'basic' : null
|
||||
}
|
||||
get: (key: string) => (key === 'name' ? 'basic' : null),
|
||||
},
|
||||
]),
|
||||
Navigate: vi.fn(() => <div data-testid="navigate">Navigate to not-found</div>)
|
||||
Navigate: vi.fn(() => <div data-testid="navigate">Navigate to not-found</div>),
|
||||
}));
|
||||
|
||||
vi.mock('../authenticator', () => ({
|
||||
useAuthenticator: vi.fn()
|
||||
useAuthenticator: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@nocobase/client', () => ({
|
||||
SchemaComponent: vi.fn(({ schema, scope }) => <div data-testid="schema-component">Schema Component</div>),
|
||||
useAPIClient: vi.fn()
|
||||
useAPIClient: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../locale', () => ({
|
||||
useAuthTranslation: vi.fn(() => ({ t: (key: string) => key }))
|
||||
useAuthTranslation: vi.fn(() => ({ t: (key: string) => key })),
|
||||
}));
|
||||
|
||||
describe('ForgotPasswordPage', () => {
|
||||
@@ -46,8 +46,8 @@ describe('ForgotPasswordPage', () => {
|
||||
// 模拟认证器允许重置密码
|
||||
vi.mocked(useAuthenticator).mockReturnValue({
|
||||
options: {
|
||||
enableResetPassword: true
|
||||
}
|
||||
enableResetPassword: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
render(<ForgotPasswordPage />);
|
||||
@@ -58,8 +58,8 @@ describe('ForgotPasswordPage', () => {
|
||||
// 模拟认证器不允许重置密码
|
||||
vi.mocked(useAuthenticator).mockReturnValue({
|
||||
options: {
|
||||
enableResetPassword: false
|
||||
}
|
||||
enableResetPassword: false,
|
||||
},
|
||||
} as any);
|
||||
|
||||
render(<ForgotPasswordPage />);
|
||||
@@ -79,15 +79,15 @@ describe('ForgotPasswordPage', () => {
|
||||
const mockName = 'custom-auth';
|
||||
vi.mocked(useSearchParams).mockReturnValue([
|
||||
{
|
||||
get: (key: string) => key === 'name' ? mockName : null
|
||||
}
|
||||
get: (key: string) => (key === 'name' ? mockName : null),
|
||||
},
|
||||
] as any);
|
||||
|
||||
// 模拟认证器允许重置密码
|
||||
vi.mocked(useAuthenticator).mockReturnValue({
|
||||
options: {
|
||||
enableResetPassword: true
|
||||
}
|
||||
enableResetPassword: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
render(<ForgotPasswordPage />);
|
||||
|
||||
+24
-24
@@ -23,28 +23,28 @@ vi.mock('react-router-dom', () => ({
|
||||
if (key === 'name') return 'basic';
|
||||
if (key === 'resetToken') return 'valid-token';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]),
|
||||
Navigate: vi.fn(() => <div data-testid="navigate">Navigate to not-found</div>)
|
||||
Navigate: vi.fn(() => <div data-testid="navigate">Navigate to not-found</div>),
|
||||
}));
|
||||
|
||||
vi.mock('../authenticator', () => ({
|
||||
useAuthenticator: vi.fn()
|
||||
useAuthenticator: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@nocobase/client', () => ({
|
||||
SchemaComponent: vi.fn(({ schema, scope }) => <div data-testid="schema-component">Schema Component</div>),
|
||||
useAPIClient: vi.fn(() => ({
|
||||
auth: {
|
||||
checkResetToken: vi.fn().mockResolvedValue(true)
|
||||
}
|
||||
checkResetToken: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
})),
|
||||
useNavigateNoUpdate: vi.fn(() => vi.fn())
|
||||
useNavigateNoUpdate: vi.fn(() => vi.fn()),
|
||||
}));
|
||||
|
||||
vi.mock('../locale', () => ({
|
||||
useAuthTranslation: vi.fn(() => ({ t: (key: string) => key }))
|
||||
useAuthTranslation: vi.fn(() => ({ t: (key: string) => key })),
|
||||
}));
|
||||
|
||||
vi.mock('antd', () => ({
|
||||
@@ -56,8 +56,8 @@ vi.mock('antd', () => ({
|
||||
</div>
|
||||
)),
|
||||
message: {
|
||||
success: vi.fn()
|
||||
}
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ResetPasswordPage', () => {
|
||||
@@ -69,8 +69,8 @@ describe('ResetPasswordPage', () => {
|
||||
// 模拟认证器允许重置密码
|
||||
vi.mocked(useAuthenticator).mockReturnValue({
|
||||
options: {
|
||||
enableResetPassword: true
|
||||
}
|
||||
enableResetPassword: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
// 模拟有效的重置令牌
|
||||
@@ -80,8 +80,8 @@ describe('ResetPasswordPage', () => {
|
||||
if (key === 'name') return 'basic';
|
||||
if (key === 'resetToken') return 'valid-token';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
] as any);
|
||||
|
||||
render(<ResetPasswordPage />);
|
||||
@@ -96,8 +96,8 @@ describe('ResetPasswordPage', () => {
|
||||
// 模拟认证器不允许重置密码
|
||||
vi.mocked(useAuthenticator).mockReturnValue({
|
||||
options: {
|
||||
enableResetPassword: false
|
||||
}
|
||||
enableResetPassword: false,
|
||||
},
|
||||
} as any);
|
||||
|
||||
render(<ResetPasswordPage />);
|
||||
@@ -118,15 +118,15 @@ describe('ResetPasswordPage', () => {
|
||||
// 模拟认证器允许重置密码
|
||||
vi.mocked(useAuthenticator).mockReturnValue({
|
||||
options: {
|
||||
enableResetPassword: true
|
||||
}
|
||||
enableResetPassword: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
// 模拟过期或无效的令牌
|
||||
vi.mocked(useAPIClient).mockReturnValue({
|
||||
auth: {
|
||||
checkResetToken: vi.fn().mockRejectedValue(new Error('Token expired'))
|
||||
}
|
||||
checkResetToken: vi.fn().mockRejectedValue(new Error('Token expired')),
|
||||
},
|
||||
} as any);
|
||||
|
||||
render(<ResetPasswordPage />);
|
||||
@@ -146,15 +146,15 @@ describe('ResetPasswordPage', () => {
|
||||
if (key === 'name') return mockName;
|
||||
if (key === 'resetToken') return 'valid-token';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
] as any);
|
||||
|
||||
// 模拟认证器允许重置密码
|
||||
vi.mocked(useAuthenticator).mockReturnValue({
|
||||
options: {
|
||||
enableResetPassword: true
|
||||
}
|
||||
enableResetPassword: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
render(<ResetPasswordPage />);
|
||||
|
||||
@@ -7,7 +7,15 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { SchemaComponent, useCollectionManager, useCurrentUserVariable, useDatetimeVariable, useGlobalVariable, useRecord, useSystemSettingsVariable } from '@nocobase/client';
|
||||
import {
|
||||
SchemaComponent,
|
||||
useCollectionManager,
|
||||
useCurrentUserVariable,
|
||||
useDatetimeVariable,
|
||||
useGlobalVariable,
|
||||
useRecord,
|
||||
useSystemSettingsVariable,
|
||||
} from '@nocobase/client';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { lang, useAuthTranslation } from '../locale';
|
||||
import { FormTab, ArrayTable } from '@formily/antd-v5';
|
||||
@@ -153,14 +161,20 @@ const useVariableOptionsOfForgetPassword = () => {
|
||||
const { currentUserSettings } = useCurrentUserVariable({ maxDepth: 1 });
|
||||
const { systemSettings } = useSystemSettingsVariable();
|
||||
|
||||
return [environmentVariables, currentUserSettings, systemSettings, {
|
||||
value: '$resetLink',
|
||||
label: t('Reset password link'),
|
||||
}, {
|
||||
return [
|
||||
environmentVariables,
|
||||
currentUserSettings,
|
||||
systemSettings,
|
||||
{
|
||||
value: '$resetLink',
|
||||
label: t('Reset password link'),
|
||||
},
|
||||
{
|
||||
value: '$resetLinkExpiration',
|
||||
label: t('Reset link expiration (minutes)'),
|
||||
}].filter(Boolean);
|
||||
}
|
||||
},
|
||||
].filter(Boolean);
|
||||
};
|
||||
|
||||
export const Options = () => {
|
||||
const { t } = useAuthTranslation();
|
||||
@@ -229,7 +243,11 @@ export const Options = () => {
|
||||
divider1: {
|
||||
type: 'void',
|
||||
'x-component': () => {
|
||||
return <Divider orientation="left" orientationMargin="0">{t('1. Select notification channel')}</Divider>;
|
||||
return (
|
||||
<Divider orientation="left" orientationMargin="0">
|
||||
{t('1. Select notification channel')}
|
||||
</Divider>
|
||||
);
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
@@ -276,12 +294,17 @@ export const Options = () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
description: '{{t("The notification channel used to send the reset password email, only support email channel")}}',
|
||||
description:
|
||||
'{{t("The notification channel used to send the reset password email, only support email channel")}}',
|
||||
},
|
||||
divider2: {
|
||||
type: 'void',
|
||||
'x-component': () => {
|
||||
return <Divider orientation="left" orientationMargin="0">{t('2. Configure reset email')}</Divider>;
|
||||
return (
|
||||
<Divider orientation="left" orientationMargin="0">
|
||||
{t('2. Configure reset email')}
|
||||
</Divider>
|
||||
);
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
@@ -399,7 +422,7 @@ export const Options = () => {
|
||||
suffix: t('Minutes'),
|
||||
style: {
|
||||
width: '100%',
|
||||
}
|
||||
},
|
||||
},
|
||||
default: 120,
|
||||
required: true,
|
||||
|
||||
@@ -116,7 +116,7 @@ const getPasswordForm = ({ showForgotPassword }: { showForgotPassword?: boolean
|
||||
'x-visible': showForgotPassword,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
export const SignInForm = (props: { authenticator: Authenticator }) => {
|
||||
@@ -131,5 +131,10 @@ export const SignInForm = (props: { authenticator: Authenticator }) => {
|
||||
const useBasicSignIn = () => {
|
||||
return useSignIn(name);
|
||||
};
|
||||
return <SchemaComponent schema={getPasswordForm({ showForgotPassword: !!options?.enableResetPassword })} scope={{ useBasicSignIn, allowSignUp, signUpLink, t, authenticator }} />;
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={getPasswordForm({ showForgotPassword: !!options?.enableResetPassword })}
|
||||
scope={{ useBasicSignIn, allowSignUp, signUpLink, t, authenticator }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ const getForgotPasswordForm = (): ISchema => ({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
message.success(t("Reset email sent successfully"));
|
||||
message.success(t('Reset email sent successfully'));
|
||||
form.reset();
|
||||
},
|
||||
loading,
|
||||
@@ -80,7 +80,6 @@ export const ForgotPasswordPage = () => {
|
||||
const name = searchParams.get('name');
|
||||
const authenticator = useAuthenticator(name);
|
||||
|
||||
|
||||
if (!authenticator?.options?.enableResetPassword) {
|
||||
return <Navigate to="/not-found" replace={true} />;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ const getResetPasswordForm = (): ISchema => ({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
message.success(t("Password reset successful"));
|
||||
message.success(t('Password reset successful'));
|
||||
setTimeout(() => {
|
||||
window.location.href = '/signin';
|
||||
}, 1000);
|
||||
@@ -96,11 +96,14 @@ export const ResetPasswordPage = () => {
|
||||
const authenticator = useAuthenticator(name);
|
||||
|
||||
useEffect(() => {
|
||||
api.auth.checkResetToken({ resetToken }).then(() => {
|
||||
setExpired(false);
|
||||
}).catch((error) => {
|
||||
setExpired(true);
|
||||
});
|
||||
api.auth
|
||||
.checkResetToken({ resetToken })
|
||||
.then(() => {
|
||||
setExpired(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setExpired(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!authenticator?.options?.enableResetPassword) {
|
||||
@@ -108,11 +111,17 @@ export const ResetPasswordPage = () => {
|
||||
}
|
||||
|
||||
if (!resetToken || expired) {
|
||||
return <Result
|
||||
status="403"
|
||||
title={t('Reset link has expired')}
|
||||
extra={<Button type="primary" onClick={() => navigate('/signin')}>{t('Go to login')}</Button>}
|
||||
/>;
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title={t('Reset link has expired')}
|
||||
extra={
|
||||
<Button type="primary" onClick={() => navigate('/signin')}>
|
||||
{t('Go to login')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <SchemaComponent schema={getResetPasswordForm()} scope={{ t }} />;
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('auth:lostPassword', () => {
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Add mock email channel
|
||||
@@ -307,7 +307,9 @@ describe('auth:lostPassword', () => {
|
||||
channelName: 'email',
|
||||
message: expect.objectContaining({
|
||||
to: ['test@example.com'],
|
||||
html: expect.stringContaining(`${baseURL}/reset-password?resetToken=mock-reset-token&name=${authenticatorName}`),
|
||||
html: expect.stringContaining(
|
||||
`${baseURL}/reset-password?resetToken=mock-reset-token&name=${authenticatorName}`,
|
||||
),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -80,11 +80,14 @@ describe('auth:resetPassword & auth:checkResetToken', () => {
|
||||
});
|
||||
|
||||
// Create a valid reset token
|
||||
validToken = await app.authManager.jwt.sign({
|
||||
resetPasswordUserId: testUser.id,
|
||||
}, {
|
||||
expiresIn: '1h',
|
||||
});
|
||||
validToken = await app.authManager.jwt.sign(
|
||||
{
|
||||
resetPasswordUserId: testUser.id,
|
||||
},
|
||||
{
|
||||
expiresIn: '1h',
|
||||
},
|
||||
);
|
||||
|
||||
// Create an expired token for testing
|
||||
expiredToken = 'expired.token.value';
|
||||
@@ -116,24 +119,18 @@ describe('auth:resetPassword & auth:checkResetToken', () => {
|
||||
// Tests for auth:checkResetToken
|
||||
describe('auth:checkResetToken', () => {
|
||||
it('should return true when token is valid', async () => {
|
||||
const res = await agent
|
||||
.post('/auth:checkResetToken')
|
||||
.set({ 'X-Authenticator': 'basic' })
|
||||
.send({
|
||||
resetToken: validToken,
|
||||
});
|
||||
const res = await agent.post('/auth:checkResetToken').set({ 'X-Authenticator': 'basic' }).send({
|
||||
resetToken: validToken,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body.data).toBe(true);
|
||||
});
|
||||
|
||||
it('should return an error when token is expired', async () => {
|
||||
const res = await agent
|
||||
.post('/auth:checkResetToken')
|
||||
.set({ 'X-Authenticator': 'basic' })
|
||||
.send({
|
||||
resetToken: expiredToken,
|
||||
});
|
||||
const res = await agent.post('/auth:checkResetToken').set({ 'X-Authenticator': 'basic' }).send({
|
||||
resetToken: expiredToken,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(res.error.text).toContain('Token expired');
|
||||
@@ -142,12 +139,9 @@ describe('auth:resetPassword & auth:checkResetToken', () => {
|
||||
it('should return an error when token is invalid', async () => {
|
||||
app.authManager.jwt.decode = vi.fn().mockRejectedValue(new Error('Invalid token'));
|
||||
|
||||
const res = await agent
|
||||
.post('/auth:checkResetToken')
|
||||
.set({ 'X-Authenticator': 'basic' })
|
||||
.send({
|
||||
resetToken: 'invalid.token',
|
||||
});
|
||||
const res = await agent.post('/auth:checkResetToken').set({ 'X-Authenticator': 'basic' }).send({
|
||||
resetToken: 'invalid.token',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(res.error.text).toContain('Token expired');
|
||||
@@ -157,13 +151,10 @@ describe('auth:resetPassword & auth:checkResetToken', () => {
|
||||
// Tests for auth:resetPassword
|
||||
describe('auth:resetPassword', () => {
|
||||
it('should successfully reset password with valid token', async () => {
|
||||
const res = await agent
|
||||
.post('/auth:resetPassword')
|
||||
.set({ 'X-Authenticator': 'basic' })
|
||||
.send({
|
||||
resetToken: validToken,
|
||||
password: 'newpassword123',
|
||||
});
|
||||
const res = await agent.post('/auth:resetPassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||
resetToken: validToken,
|
||||
password: 'newpassword123',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(204);
|
||||
|
||||
@@ -171,37 +162,28 @@ describe('auth:resetPassword & auth:checkResetToken', () => {
|
||||
expect(app.authManager.jwt.block).toHaveBeenCalledWith(validToken);
|
||||
|
||||
// Verify user password was changed by trying to sign in with the new password
|
||||
const signInRes = await agent
|
||||
.post('/auth:signIn')
|
||||
.set({ 'X-Authenticator': 'basic' })
|
||||
.send({
|
||||
account: 'test@example.com',
|
||||
password: 'newpassword123',
|
||||
});
|
||||
const signInRes = await agent.post('/auth:signIn').set({ 'X-Authenticator': 'basic' }).send({
|
||||
account: 'test@example.com',
|
||||
password: 'newpassword123',
|
||||
});
|
||||
|
||||
expect(signInRes.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('should return an error when resetToken is not provided', async () => {
|
||||
const res = await agent
|
||||
.post('/auth:resetPassword')
|
||||
.set({ 'X-Authenticator': 'basic' })
|
||||
.send({
|
||||
password: 'newpassword123',
|
||||
});
|
||||
const res = await agent.post('/auth:resetPassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||
password: 'newpassword123',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(res.error.text).toContain('Token expired');
|
||||
});
|
||||
|
||||
it('should return an error when resetToken is expired', async () => {
|
||||
const res = await agent
|
||||
.post('/auth:resetPassword')
|
||||
.set({ 'X-Authenticator': 'basic' })
|
||||
.send({
|
||||
resetToken: expiredToken,
|
||||
password: 'newpassword123',
|
||||
});
|
||||
const res = await agent.post('/auth:resetPassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||
resetToken: expiredToken,
|
||||
password: 'newpassword123',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(res.error.text).toContain('Token expired');
|
||||
@@ -219,13 +201,10 @@ describe('auth:resetPassword & auth:checkResetToken', () => {
|
||||
|
||||
app.authManager.jwt.blacklist.has = vi.fn().mockResolvedValue(false);
|
||||
|
||||
const res = await agent
|
||||
.post('/auth:resetPassword')
|
||||
.set({ 'X-Authenticator': 'basic' })
|
||||
.send({
|
||||
resetToken: nonExistentUserToken,
|
||||
password: 'newpassword123',
|
||||
});
|
||||
const res = await agent.post('/auth:resetPassword').set({ 'X-Authenticator': 'basic' }).send({
|
||||
resetToken: nonExistentUserToken,
|
||||
password: 'newpassword123',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(404);
|
||||
expect(res.error.text).toContain('User not found');
|
||||
|
||||
@@ -39,8 +39,8 @@ export class BasicAuth extends BaseAuth {
|
||||
const filter = email
|
||||
? { email }
|
||||
: {
|
||||
$or: [{ username: account }, { email: account }],
|
||||
};
|
||||
$or: [{ username: account }, { email: account }],
|
||||
};
|
||||
const user = await this.userRepository.findOne({
|
||||
filter,
|
||||
});
|
||||
@@ -182,23 +182,34 @@ export class BasicAuth extends BaseAuth {
|
||||
}
|
||||
|
||||
// 通过用户认证的接口获取邮件渠道、主题、内容等
|
||||
const { notificationChannel, emailContentType, emailContentHTML, emailContentText, emailSubject, enableResetPassword, resetTokenExpiresIn } = this.getEmailConfig();
|
||||
const {
|
||||
notificationChannel,
|
||||
emailContentType,
|
||||
emailContentHTML,
|
||||
emailContentText,
|
||||
emailSubject,
|
||||
enableResetPassword,
|
||||
resetTokenExpiresIn,
|
||||
} = this.getEmailConfig();
|
||||
|
||||
if (!enableResetPassword) {
|
||||
ctx.throw(403, ctx.t('Not allowed to reset password', { ns: namespace }));
|
||||
}
|
||||
|
||||
// 生成重置密码的 token
|
||||
const resetToken = await ctx.app.authManager.jwt.sign({
|
||||
resetPasswordUserId: user.id,
|
||||
}, {
|
||||
expiresIn: resetTokenExpiresIn * 60, // 配置的过期时间,单位分钟,需要转成秒
|
||||
});
|
||||
const resetToken = await ctx.app.authManager.jwt.sign(
|
||||
{
|
||||
resetPasswordUserId: user.id,
|
||||
},
|
||||
{
|
||||
expiresIn: resetTokenExpiresIn * 60, // 配置的过期时间,单位分钟,需要转成秒
|
||||
},
|
||||
);
|
||||
|
||||
// 构建重置密码链接
|
||||
const resetLink = `${baseURL}/reset-password?resetToken=${resetToken}&name=${authenticatorName}`;
|
||||
|
||||
const systemSettings = await ctx.db.getRepository('systemSettings')?.findOne() || {};
|
||||
const systemSettings = (await ctx.db.getRepository('systemSettings')?.findOne()) || {};
|
||||
|
||||
// 通过通知管理插件发送邮件
|
||||
const notificationManager = ctx.app.getPlugin('notification-manager');
|
||||
@@ -232,7 +243,7 @@ export class BasicAuth extends BaseAuth {
|
||||
subject: parsedSubject,
|
||||
contentType: emailContentType,
|
||||
...content,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
ctx.logger.info(`Password reset email sent to ${email}`);
|
||||
@@ -240,23 +251,29 @@ export class BasicAuth extends BaseAuth {
|
||||
ctx.logger.error(`Failed to send reset password email: ${error.message}`, {
|
||||
error,
|
||||
email,
|
||||
notificationChannel
|
||||
notificationChannel,
|
||||
});
|
||||
ctx.throw(500, ctx.t('Failed to send email. Error: {{error}}', {
|
||||
ns: namespace,
|
||||
error: error.message
|
||||
}));
|
||||
ctx.throw(
|
||||
500,
|
||||
ctx.t('Failed to send email. Error: {{error}}', {
|
||||
ns: namespace,
|
||||
error: error.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.logger.error(`Error parsing email template variables: ${error.message}`, {
|
||||
error,
|
||||
emailSubject,
|
||||
emailContentType
|
||||
emailContentType,
|
||||
});
|
||||
ctx.throw(500, ctx.t('Error parsing email template. Error: {{error}}', {
|
||||
ns: namespace,
|
||||
error: error.message
|
||||
}));
|
||||
ctx.throw(
|
||||
500,
|
||||
ctx.t('Error parsing email template. Error: {{error}}', {
|
||||
ns: namespace,
|
||||
error: error.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ctx.throw(400, ctx.t('Email channel not found', { ns: namespace }));
|
||||
|
||||
@@ -119,7 +119,9 @@ export class PluginAuthServer extends Plugin {
|
||||
// Set up ACL
|
||||
['signIn', 'signUp'].forEach((action) => this.app.acl.allow('auth', action));
|
||||
['check', 'signOut', 'changePassword'].forEach((action) => this.app.acl.allow('auth', action, 'loggedIn'));
|
||||
['lostPassword', 'resetPassword', 'checkResetToken'].forEach((action) => this.app.acl.allow('auth', action, 'public'));
|
||||
['lostPassword', 'resetPassword', 'checkResetToken'].forEach((action) =>
|
||||
this.app.acl.allow('auth', action, 'public'),
|
||||
);
|
||||
this.app.acl.allow('authenticators', 'publicList');
|
||||
this.app.acl.registerSnippet({
|
||||
name: `pm.${this.name}.authenticators`,
|
||||
@@ -322,7 +324,7 @@ export class PluginAuthServer extends Plugin {
|
||||
});
|
||||
}
|
||||
|
||||
async remove() { }
|
||||
async remove() {}
|
||||
}
|
||||
|
||||
export default PluginAuthServer;
|
||||
|
||||
Reference in New Issue
Block a user