关于login重构

This commit is contained in:
xion 2025-03-21 20:41:01 +08:00
parent 0179fe73a3
commit 8053a3db64
28 changed files with 889 additions and 596 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "submodules/code-center-module"]
path = submodules/code-center-module
url = git@git.xiongxiao.me:kevisual/code-center-module.git

View File

@ -21,7 +21,8 @@
"start": "pm2 start dist/app.mjs --name codecenter",
"release": "node ./config/release/index.mjs",
"pub": "envision pack -p -u",
"ssl": "ssl -L 6379:localhost:6379 light "
"ssh": "ssh -L 6379:localhost:6379 light ",
"ssh:sky": "ssh -L 6379:172.21.32.13:6379 sky"
},
"keywords": [],
"types": "types/index.d.ts",
@ -32,7 +33,6 @@
],
"license": "UNLICENSED",
"dependencies": {
"@kevisual/auth": "1.0.5",
"@kevisual/local-app-manager": "0.1.9",
"@kevisual/router": "0.0.9",
"@kevisual/use-config": "^1.0.9",
@ -46,10 +46,10 @@
"jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21",
"minio": "^8.0.5",
"nanoid": "^5.1.3",
"nanoid": "^5.1.5",
"node-fetch": "^3.3.2",
"p-queue": "^8.1.0",
"pg": "^8.14.0",
"pg": "^8.14.1",
"pm2": "^6.0.5",
"rollup-plugin-esbuild": "^6.2.1",
"semver": "^7.7.1",
@ -61,7 +61,7 @@
"zod": "^3.24.2"
},
"devDependencies": {
"@kevisual/code-center-module": "0.0.13",
"@kevisual/code-center-module": "workspace:*",
"@kevisual/types": "^0.0.6",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.3",
@ -75,15 +75,15 @@
"@types/jsonwebtoken": "^9.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.13.10",
"@types/react": "^19.0.10",
"@types/react": "^19.0.12",
"@types/uuid": "^10.0.0",
"concurrently": "^9.1.2",
"cross-env": "^7.0.3",
"nodemon": "^3.1.9",
"rimraf": "^6.0.1",
"rollup": "^4.35.0",
"rollup": "^4.36.0",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-dts": "^6.2.0",
"tape": "^5.9.0",
"tsx": "^4.19.3",
"typescript": "^5.8.2"

562
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,2 @@
packages:
- 'submodules/*'

View File

View File

@ -1,14 +1,9 @@
import './routes/index.ts';
import { app } from './app.ts';
import { useConfig } from '@kevisual/use-config';
import { User } from './models/user.ts';
import { createAuthRoute } from '@kevisual/auth';
const config = useConfig<{ tokenSecret: string }>();
import { addAuth } from '@kevisual/code-center-module/models';
createAuthRoute({
app,
secret: config.tokenSecret,
});
addAuth(app);
app
.route({

View File

@ -7,8 +7,6 @@ import { useFileStore } from '@kevisual/use-config/file-store';
import { app, minioClient } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts';
import { getContentType } from '@/utils/get-content-type.ts';
import { hash } from 'crypto';
import { MicroAppUploadModel } from '@/routes/micro-app/models.ts';
const cacheFilePath = useFileStore('cache-file', { needExists: true });
router.post('/api/micro-app/upload', async (req, res) => {
@ -119,70 +117,7 @@ router.post('/api/micro-app/upload', async (req, res) => {
});
});
router.get('/api/micro-app/download/:id', async (req, res) => {
const { id } = req.params;
if (!id) {
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
res.end(error('Key parameter is required'));
return;
}
const query = new URL(req.url || '', 'http://localhost');
const notNeedToken = query.searchParams.get('notNeedToken') || '';
const fileTitle = query.searchParams.get('title') || '';
if (res.headersSent) return; // 如果响应已发送,不再处理
let tokenUser;
if (!DEV_SERVER && !notNeedToken) {
const auth = await checkAuth(req, res);
tokenUser = auth.tokenUser;
if (!tokenUser) return;
}
let file: MicroAppUploadModel | null = null;
if (!DEV_SERVER) {
// file.uid !== tokenUser.id && res.end(error('No permission', 403));
// return;
}
if (fileTitle) {
file = await MicroAppUploadModel.findOne({
where: { title: fileTitle },
});
} else if (id) {
file = await MicroAppUploadModel.findByPk(id);
}
if (!file) {
res.end(error('File not found'));
return;
}
const objectName = file.data?.file?.path;
const fileName = file.data?.file?.name;
if (!objectName) {
res.end(error('File not found'));
return;
}
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
res.setHeader('app-key', file.data?.key || id);
res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
try {
const stream = await minioClient.getObject(bucketName, objectName);
// 捕获流的错误,防止崩溃
stream.on('error', (err) => {
console.error('Error while streaming file:', err);
if (!res.headersSent) {
res.end(error('Error downloading file'));
}
});
stream.pipe(res).on('finish', () => {
console.log(`File download completed: ${id}`);
});
} catch (err) {
console.error('Error during download:', err);
if (!res.headersSent) {
res.end(error('Error downloading file'));
}
}
});
function parseIfJson(collection: any): any {
try {
return JSON.parse(collection);

View File

@ -15,8 +15,9 @@ export const validateDirectory = (directory?: string) => {
};
}
// 把directory的/替换掉后,只能包含数字、字母、下划线、中划线
// 可以包含.
let _directory = directory?.replace(/\//g, '');
if (_directory && !/^[a-zA-Z0-9_-]+$/.test(_directory)) {
if (_directory && !/^[a-zA-Z0-9_.-]+$/.test(_directory)) {
return {
code: 500,
message: 'directory is invalid, only number, letter, underline and hyphen are allowed',

View File

@ -176,7 +176,7 @@ app
uid,
version: version || '0.0.0',
title: appKey,
proxy: true,
proxy: appKey.includes('center') ? false : true,
data: {
files: files || [],
},
@ -352,7 +352,7 @@ app
user: checkUsername,
uid,
data: { files: needAddFiles },
proxy: true,
proxy: appKey.includes('center') ? false : true,
});
} else {
const appModel = await AppModel.findOne({ where: { key: appKey, version, uid } });

View File

@ -18,8 +18,9 @@ export interface AppData {
password?: string; // 受保护的访问密码
'expiration-time'?: string; // 受保护的访问过期时间
};
// 运行环境browser, node, 或者其他,是数组
runtime?: string[];
}
export type AppType = 'web-single' | 'web-module'; // 可以做到网页代理
export enum AppStatus {
running = 'running',
stop = 'stop',
@ -36,9 +37,7 @@ export class AppModel extends Model {
declare description: string;
declare version: string;
declare domain: string;
declare appType: string;
declare key: string;
declare type: string;
declare uid: string;
declare pid: string;
// 是否是history路由代理模式。静态的直接转minio而不需要缓存下来。
@ -74,18 +73,10 @@ AppModel.init(
type: DataTypes.STRING,
defaultValue: '',
},
appType: {
type: DataTypes.STRING,
defaultValue: '',
},
key: {
type: DataTypes.STRING,
// 和 uid 组合唯一
},
type: {
type: DataTypes.STRING,
defaultValue: '',
},
uid: {
type: DataTypes.UUID,
allowNull: true,

View File

@ -2,6 +2,7 @@ import { app } from '@/app.ts';
import { AppModel } from '../module/index.ts';
// curl http://localhost:4005/api/router?path=app&key=public-list
// TODO:
app
.route({
path: 'app',
@ -12,6 +13,9 @@ app
where: {
status: 'running',
},
// attributes: {
// exclude: ['data'],
// },
logging: false,
});
ctx.body = list;

View File

@ -1,9 +1,6 @@
import { CustomError } from '@kevisual/router';
import { AppModel, AppListModel } from './module/index.ts';
import { app } from '@/app.ts';
import { setExpire } from './revoke.ts';
import { getMinioListAndSetToAppList } from '../file/index.ts';
import { getUidByUsername } from './util.ts';
app
.route({
@ -18,6 +15,9 @@ app
where: {
uid: tokenUser.id,
},
attributes: {
exclude: ['data'],
},
});
ctx.body = list;
return ctx;
@ -29,24 +29,25 @@ app
path: 'user-app',
key: 'get',
middleware: ['auth'],
description: '获取用户应用,可以指定id或者key',
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const id = ctx.query.id;
const { key } = ctx.query.data || {};
if (!id && !key) {
throw new CustomError('id is required');
ctx.throw(500, 'id is required');
}
if (id) {
const am = await AppModel.findByPk(id);
if (!am) {
throw new CustomError('app not found');
ctx.throw(500, 'app not found');
}
ctx.body = am;
} else {
const am = await AppModel.findOne({ where: { key, uid: tokenUser.id } });
if (!am) {
throw new CustomError('app not found');
ctx.throw(500, 'app not found');
}
ctx.body = am;
}
@ -75,16 +76,16 @@ app
setExpire(newApp.key, app.user);
}
} else {
throw new CustomError('app not found');
ctx.throw(500, 'app not found');
}
return;
}
if (!rest.key) {
throw new CustomError('key is required');
ctx.throw(500, 'key is required');
}
const findApp = await AppModel.findOne({ where: { key: rest.key, uid: tokenUser.id } });
if (findApp) {
throw new CustomError('key already exists');
ctx.throw(500, 'key already exists');
}
const app = await AppModel.create({
data: { files: [] },
@ -107,14 +108,14 @@ app
const tokenUser = ctx.state.tokenUser;
const id = ctx.query.id;
if (!id) {
throw new CustomError('id is required');
ctx.throw(500, 'id is required');
}
const am = await AppModel.findByPk(id);
if (!am) {
throw new CustomError('app not found');
ctx.throw(500, 'app not found');
}
if (am.uid !== tokenUser.id) {
throw new CustomError('app not found');
ctx.throw(500, 'app not found');
}
const list = await AppListModel.findAll({ where: { key: am.key, uid: tokenUser.id } });
await am.destroy({ force: true });
@ -133,11 +134,11 @@ app
.define(async (ctx) => {
const id = ctx.query.id;
if (!id) {
throw new CustomError('id is required');
ctx.throw(500, 'id is required');
}
const am = await AppListModel.findByPk(id);
if (!am) {
throw new CustomError('app not found');
ctx.throw(500, 'app not found');
}
const amJson = am.toJSON();
ctx.body = {
@ -146,5 +147,3 @@ app
};
})
.addTo(app);

View File

@ -1,2 +1,3 @@
import './list.ts';
import './upload-config.ts';
import './share-config.ts';

View File

@ -1,5 +1,6 @@
import { app } from '@/app.ts';
import { ConfigModel } from './models/model.ts';
import { ShareConfigService } from './services/share.ts';
app
.route({
path: 'config',
@ -12,6 +13,7 @@ app
where: {
uid: id,
},
order: [['updatedAt', 'DESC']],
});
ctx.body = {
list: config,
@ -19,3 +21,123 @@ app
})
.addTo(app);
app
.route({
path: 'config',
key: 'update',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokernUser = ctx.state.tokenUser;
const tuid = tokernUser.id;
const { id, key, data, ...rest } = ctx.query?.data || {};
if (id) {
const config = await ConfigModel.findByPk(id);
if (config && config.uid === tuid) {
const keyConfig = await ConfigModel.findOne({
where: {
key,
uid: tuid,
},
});
if (keyConfig && keyConfig.id !== id) {
ctx.throw(403, 'key is already exists');
}
await config.update({
key,
data: {
...config.data,
...data,
},
...rest,
});
if (config.data?.permission?.share === 'public') {
await ShareConfigService.expireShareConfig(config.key, tokernUser.username);
}
ctx.body = config;
} else {
ctx.throw(403, 'no permission');
}
} else {
const keyConfig = await ConfigModel.findOne({
where: {
key,
uid: tuid,
},
});
if (keyConfig) {
ctx.throw(403, 'key is already exists');
}
const config = await ConfigModel.create({
key,
...rest,
data: data,
uid: tuid,
});
ctx.body = config;
}
})
.addTo(app);
app
.route({
path: 'config',
key: 'get',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokernUser = ctx.state.tokenUser;
const tuid = tokernUser.id;
const { id, key } = ctx.query?.data || {};
if (!id && !key) {
ctx.throw(400, 'id or key is required');
}
let config: ConfigModel;
if (id) {
config = await ConfigModel.findByPk(id);
}
if (!config && key) {
config = await ConfigModel.findOne({
where: {
key,
uid: tuid,
},
});
}
if (!config) {
ctx.throw(404, 'config not found');
}
if (config && config.uid === tuid) {
ctx.body = config;
} else {
ctx.throw(403, 'no permission');
}
})
.addTo(app);
app
.route({
path: 'config',
key: 'delete',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokernUser = ctx.state.tokenUser;
const tuid = tokernUser.id;
const { id } = ctx.query?.data || {};
if (id) {
const config = await ConfigModel.findOne({
where: {
id,
},
});
if (config && config.uid === tuid) {
await config.destroy();
} else {
ctx.throw(403, 'no permission');
}
} else {
ctx.throw(400, 'id is required');
}
})
.addTo(app);

View File

@ -5,6 +5,9 @@ import { DataTypes, Model } from 'sequelize';
export interface ConfigData {
key?: string;
version?: string;
permission?: {
share?: 'public' | 'private';
};
}
export type Config = Partial<InstanceType<typeof ConfigModel>>;
@ -70,7 +73,6 @@ export class ConfigModel extends Model {
static async getUploadConfig(opts: { uid: string }) {
const defaultConfig = {
key: 'upload',
type: 'upload',
version: '1.0.0',
};
const config = await ConfigModel.getConfig('upload', {

View File

@ -0,0 +1,41 @@
import { ConfigModel } from '../models/model.ts';
import { CustomError } from '@kevisual/router';
import { redis } from '@/app.ts';
import { User } from '@/models/user.ts';
export class ShareConfigService extends ConfigModel {
/**
*
* @param key key
* @param username username
* @returns
*/
static async getShareConfig(key: string, username: string) {
const shareCacheConfig = await redis.get(`config:share:${username}:${key}`);
if (shareCacheConfig) {
return JSON.parse(shareCacheConfig);
}
const user = await User.findOne({
where: { username },
});
if (!user) {
throw new CustomError(404, 'user not found');
}
const config = await ConfigModel.findOne({
where: { key, uid: user.id },
});
if (!config) {
throw new CustomError(404, 'config not found');
}
const configData = config?.data?.permission;
if (configData?.share !== 'public') {
throw new CustomError(403, 'no permission');
}
await redis.set(`config:share:${username}:${key}`, JSON.stringify(config), 'EX', 60 * 60 * 24 * 7); // 7天
return config;
}
static async expireShareConfig(key: string, username: string) {
if (key && username) {
await redis.del(`config:share:${username}:${key}`);
}
}
}

View File

@ -0,0 +1,27 @@
import { app } from '@/app.ts';
import { ShareConfigService } from './services/share.ts';
app
.route({
path: 'config',
key: 'shareConfig',
middleware: ['auth'],
})
.define(async (ctx) => {
const { key, username } = ctx.query?.data || {};
if (!key) {
ctx.throw(400, 'key is required');
}
if (!username) {
ctx.throw(400, 'username is required');
}
try {
const config = await ShareConfigService.getShareConfig(key, username);
ctx.body = config;
} catch (error) {
if (error?.code === 500) {
console.error('config get error', error);
}
ctx.throw(404, 'config not found');
}
})
.addTo(app);

View File

@ -1,2 +1 @@
import './list.ts';
import './upload-list.ts'
import './list.ts';

View File

@ -1,81 +1,8 @@
import { app } from '@/app.ts';
import { MicroAppUploadModel } from './models.ts';
import { appPathCheck, installApp } from './module/install-app.ts';
import { manager } from './manager-app.ts';
import { selfRestart } from '@/modules/self-restart.ts';
// 应用上传到 应用管理 的平台
app
.route({
path: 'micro-app',
key: 'upload',
middleware: ['auth'],
description: 'Upload micro app in server',
isDebug: true,
})
.define(async (ctx) => {
const { files, collection } = ctx.query?.data;
const { uid, username } = ctx.state.tokenUser;
const file = files[0];
console.log('File', files);
const { path, name, hash, size } = file;
const tags = [];
if (collection?.tags) {
tags.push(...collection.tags);
}
if (!name) {
ctx.throw(400, 'Invalid file');
}
let microApp = await MicroAppUploadModel.findOne({
where: { title: name, uid },
});
if (microApp) {
await MicroAppUploadModel.update(
{
type: 'micro-app',
tags,
data: {
...microApp.data,
file: {
path,
size,
name,
hash,
},
collection,
},
},
{ where: { title: name, uid } },
);
microApp = await MicroAppUploadModel.findOne({
where: { title: name, uid },
});
console.log('Update micro app', microApp.id);
} else {
microApp = await MicroAppUploadModel.create({
title: name,
description: collection?.readme || '',
type: 'micro-app',
tags: tags,
data: {
file: {
path,
size,
name,
hash,
},
collection,
},
uid,
share: false,
uname: username,
});
}
ctx.body = microApp;
})
.addTo(app);
import { AppListModel } from '../app-manager/module/index.ts';
// curl http://localhost:4002/api/router?path=micro-app&key=deploy
// 把对应的应用安装到系统的apps目录下并解压然后把配置项写入数据库配置
// key 是应用的唯一标识和package.json中的key一致绑定关系
@ -86,24 +13,25 @@ app
path: 'micro-app',
key: 'deploy',
description: 'Deploy micro app in server',
middleware: ['auth'],
})
.define(async (ctx) => {
const { id, key, force, install } = ctx.query?.data;
// const id = '10f03411-85fc-4d37-a4d3-e32b15566a6c';
// const key = 'envision-cli';
// const id = '7c54a6de-9171-4093-926d-67a035042c6c';
// const key = 'mark';
const tokenUser = ctx.state.tokenUser;
const data = ctx.query?.data;
const { id, key, force, install } = data;
if (!id) {
ctx.throw(400, 'Invalid id');
}
const microApp = await MicroAppUploadModel.findByPk(id);
const { file } = microApp.data || {};
const path = file?.path;
if (!path) {
ctx.throw(404, 'Invalid path');
let username = tokenUser.username;
if (data.username) {
// username = data.username;
}
console.log('path', path);
const check = await appPathCheck({ key });
const microApp = await AppListModel.findByPk(id);
if (!microApp) {
ctx.throw(400, 'Invalid id');
}
const { key: appKey, version } = microApp;
const check = await appPathCheck({ key: key });
let appType: string;
if (check) {
if (!force) {
@ -115,7 +43,7 @@ app
await manager.removeApp(key);
}
}
const installAppData = await installApp({ path, key, needInstallDeps: !!install });
const installAppData = await installApp({ username, appKey, version, key, needInstallDeps: !!install });
await manager.add(installAppData.showAppInfo);
ctx.body = installAppData;
if (appType === 'system-app') {

View File

@ -1,86 +0,0 @@
import { sequelize } from '@/modules/sequelize.ts';
import { DataTypes, Model } from 'sequelize';
export type MicroApp = Partial<InstanceType<typeof MicroAppUploadModel>>;
type MicroAppData = {
file?: {
path: string;
size: number;
hash: string;
name: string;
};
key?: string;
data?: any;
collection?: any; // 上传的信息汇总
};
export class MicroAppUploadModel extends Model {
declare id: string;
declare title: string;
declare description: string;
declare type: string;
declare tags: string[];
declare data: MicroAppData;
declare uid: string;
declare updatedAt: Date;
declare createdAt: Date;
declare source: string;
declare share: boolean;
declare uname: string;
}
MicroAppUploadModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
comment: 'id',
},
title: {
type: DataTypes.TEXT,
defaultValue: '',
},
description: {
type: DataTypes.TEXT,
defaultValue: '',
},
tags: {
type: DataTypes.JSONB,
defaultValue: [],
},
type: {
type: DataTypes.TEXT,
defaultValue: '',
},
source: {
type: DataTypes.TEXT,
defaultValue: '',
},
data: {
type: DataTypes.JSONB,
defaultValue: {},
},
share: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
uname: {
type: DataTypes.TEXT,
defaultValue: '',
},
uid: {
type: DataTypes.UUID,
allowNull: true,
},
},
{
sequelize,
tableName: 'micro_apps_upload',
// paranoid: true,
},
);
MicroAppUploadModel.sync({ alter: true, logging: false }).catch((e) => {
console.error('MicroAppUploadModel sync', e);
});

View File

@ -1,16 +1,21 @@
import { minioClient } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts';
import { fileIsExist } from '@kevisual/use-config';
import { spawn, spawnSync } from 'child_process';
import { getFileStat, getMinioList, MinioFile } from '@/routes/file/index.ts';
import fs from 'fs';
import path from 'path';
import * as tar from 'tar';
import { appsPath } from '../lib/index.ts';
import { installAppFromKey } from './manager.ts';
export type InstallAppOpts = {
path?: string;
key?: string;
needInstallDeps?: boolean;
// minio中
appKey?: string;
version?: string;
username?: string;
};
/**
*
@ -26,28 +31,37 @@ export const appPathCheck = async (opts: InstallAppOpts) => {
return false;
};
export const installApp = async (opts: InstallAppOpts) => {
const { key, needInstallDeps } = opts;
const fileStream = await minioClient.getObject(bucketName, opts.path);
const pathName = opts.path.split('/').pop();
const { key, needInstallDeps, appKey, version, username } = opts;
const prefix = `${username}/${appKey}/${version}`;
const pkgPrefix = prefix + '/package.json';
const stat = await getFileStat(pkgPrefix);
if (!stat) {
throw new Error('App not found');
}
const fileList = await getMinioList({
prefix,
recursive: true,
});
for (const file of fileList) {
const { name } = file as MinioFile;
const outputPath = path.join(appsPath, key, name.replace(prefix, ''));
const dir = path.dirname(outputPath);
if (!fileIsExist(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const fileStream = await minioClient.getObject(bucketName, `${name}`);
const writeStream = fs.createWriteStream(outputPath);
fileStream.pipe(writeStream);
await new Promise((resolve, reject) => {
writeStream.on('finish', () => resolve(true));
writeStream.on('error', reject);
});
}
const directory = path.join(appsPath, key);
if (!fileIsExist(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
const filePath = path.join(directory, pathName);
const writeStream = fs.createWriteStream(filePath);
fileStream.pipe(writeStream);
await new Promise((resolve, reject) => {
writeStream.on('finish', () => resolve(true));
writeStream.on('error', reject);
});
// 解压 tgz文件
const extractPath = path.join(directory);
await tar.x({
file: filePath,
cwd: extractPath,
});
if (needInstallDeps) {
try {
installDeps({ appPath: directory, isProduction: true, sync: true });
@ -57,7 +71,6 @@ export const installApp = async (opts: InstallAppOpts) => {
}
return installAppFromKey(key);
};
import { spawn, spawnSync } from 'child_process';
export const checkPnpm = () => {
try {
@ -76,13 +89,16 @@ type InstallDepsOptions = {
export const installDeps = async (opts: InstallDepsOptions) => {
const { appPath } = opts;
const isProduction = opts.isProduction ?? true;
const isPnpm = checkPnpm();
const params = ['i'];
if (isProduction) {
if (isProduction && isPnpm) {
params.push('--production');
} else {
params.push('--omit=dev');
}
console.log('installDeps', appPath, params);
const syncSpawn = opts.sync ? spawnSync : spawn;
if (checkPnpm()) {
if (isPnpm) {
syncSpawn('pnpm', params, { cwd: appPath, stdio: 'inherit', env: process.env });
} else {
syncSpawn('npm', params, { cwd: appPath, stdio: 'inherit', env: process.env });

View File

@ -10,13 +10,13 @@ export const installAppFromKey = async (key: string) => {
}
const pkgs = path.join(directory, 'package.json');
if (!fileIsExist(pkgs)) {
throw new Error('Invalid package.json');
throw new Error('Invalid package.json, must has pkg property');
}
const json = fs.readFileSync(pkgs, 'utf-8');
const pkg = JSON.parse(json);
const { name, version, app } = pkg;
if (!name || !version || !app) {
throw new Error('Invalid package.json');
throw new Error('Invalid package.json, must has name, version, app property');
}
const readmeFile = path.join(directory, 'README.md');
let readmeDesc = '';

View File

@ -1,44 +0,0 @@
import { App } from '@kevisual/router';
import { Manager } from '../module/manager.ts';
import { loadFileAppInfo } from '../module/load-app.ts';
import path from 'path';
const app = new App();
app
.route({
path: 'test',
key: 'test',
})
.define(async (ctx) => {
ctx.body = 'test';
});
const manager = new Manager({ mainApp: app });
await manager.load();
const { mainEntry, app: appConfig } = await loadFileAppInfo('mark');
// const appInfo = {
// key: 'mark',
// type: 'system-app',
// entry: mainEntry,
// status: 'running',
// path: path.dirname(mainEntry),
// };
// await manager.add(appInfo);
const client = await loadFileAppInfo('micro-client');
const appInfoMicro = {
key: 'micro-client',
type: 'micro-app',
entry: client.mainEntry,
status: 'running',
path: path.dirname(client.mainEntry),
};
await manager.add(appInfoMicro);
app.listen(3005);

View File

@ -1,8 +0,0 @@
import { getAppPathKeys } from '../module/install-app.ts';
const main = () => {
const keys = getAppPathKeys();
console.log('Keys', keys);
};
main();

View File

@ -1,62 +0,0 @@
import { app } from '@/app.ts';
import { MicroAppUploadModel } from './models.ts';
// 获取MicroAppUpload的uploadList的接口
app
.route({
path: 'micro-app-upload',
key: 'list',
middleware: ['auth'],
description: 'Get micro app upload list',
})
.define(async (ctx) => {
const { uid } = ctx.state.tokenUser;
const uploadList = await MicroAppUploadModel.findAll({
// where: { uid },
});
ctx.body = uploadList;
})
.addTo(app);
// 获取单个MicroAppUpload的接口
app
.route({
path: 'micro-app-upload',
key: 'get',
middleware: ['auth'],
description: 'Get a single micro app upload',
})
.define(async (ctx) => {
const { id, title } = ctx.query;
let upload: MicroAppUploadModel | null = null;
if (id) {
upload = await MicroAppUploadModel.findByPk(id);
} else if (title) {
upload = await MicroAppUploadModel.findOne({
where: { title },
});
}
if (upload) {
ctx.body = upload;
} else {
ctx.throw(404, 'Not found');
}
})
.addTo(app);
// 删除MicroAppUpload的接口
app
.route({
path: 'micro-app-upload',
key: 'delete',
middleware: ['auth'],
description: 'Delete a micro app upload',
})
.define(async (ctx) => {
const { id } = ctx.query;
const deleted = await MicroAppUploadModel.destroy({
where: { id },
});
ctx.body = { deleted };
})
.addTo(app);

View File

@ -12,12 +12,14 @@ export const createCookie = (token: any, ctx: any) => {
if (!domain) {
return;
}
ctx.res.cookie('token', token.token, {
maxAge: token.expireTime,
domain,
sameSite: 'lax',
httpOnly: true,
});
if (ctx.res.cookie) {
ctx.res.cookie('token', token.token, {
maxAge: 7 * 24 * 60 * 60 * 1000, // 过期时间, 设置7天
domain,
sameSite: 'lax',
httpOnly: true,
});
}
};
const clearCookie = (ctx: any) => {
if (!domain) {
@ -53,8 +55,11 @@ app
.route({
path: 'user',
key: 'login',
middleware: ['auth-can'],
})
.define(async (ctx) => {
const oldToken = ctx.query.token;
const tokenUser = ctx.state?.tokenUser || {};
const { username, email, password, loginType = 'default' } = ctx.query;
if (!username && !email) {
ctx.throw(400, 'username or email is required');
@ -69,6 +74,15 @@ app
if (!user) {
ctx.throw(500, 'Login Failed');
}
if (tokenUser.id === user.id) {
// 自己刷新自己的token
const token = await User.oauth.resetToken(oldToken, {
...tokenUser.oauthExpand,
});
createCookie(token, ctx);
ctx.body = token;
return;
}
if (!user.checkPassword(password)) {
ctx.throw(500, 'Password error');
}
@ -89,11 +103,18 @@ app
})
.addTo(app);
app
.route('user', 'auth')
.route({
path: 'user',
key: 'auth',
middleware: ['auth-can'],
})
.define(async (ctx) => {
const { checkToken: token } = ctx.query;
try {
const result = await User.verifyToken(token);
if (result) {
delete result.oauthExpand;
}
ctx.body = result || {};
} catch (e) {
ctx.throw(401, 'Token InValid ');
@ -102,7 +123,9 @@ app
.addTo(app);
app
.route('user', 'updateSelf', {
.route({
path: 'user',
key: 'updateSelf',
middleware: ['auth'],
})
.define(async (ctx) => {
@ -133,6 +156,60 @@ app
ctx.body = await user.getInfo();
})
.addTo(app);
app
.route({
path: 'user',
key: 'switchCheck',
middleware: ['auth'],
})
.define(async (ctx) => {
const token = ctx.query.token;
const { username, accessToken } = ctx.query.data || {};
if (accessToken && username) {
const accessUser = await User.verifyToken(accessToken);
const refreshToken = accessUser.oauthExpand?.refreshToken;
if (refreshToken) {
const result = await User.oauth.refreshToken(refreshToken);
createCookie(result, ctx);
ctx.body = result;
return;
} else if (accessUser) {
await User.oauth.delToken(accessToken);
const result = await User.oauth.generateToken(accessUser, {
...accessUser.oauthExpand,
hasRefreshToken: true,
});
createCookie(result, ctx);
ctx.body = result;
return;
}
} else {
const result = await ctx.call(
{
path: 'user',
key: 'switchOrg',
payload: {
data: {
username,
},
token,
},
},
{
res: ctx.res,
req: ctx.req,
},
);
if (result.code === 200) {
ctx.body = result.body;
} else {
ctx.throw(result.code, result.message);
}
}
})
.addTo(app);
app
.route({
path: 'user',
@ -141,63 +218,60 @@ app
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { username, type = 'org', loginType } = ctx.query.data || {};
if (!username && type === 'org') {
ctx.throw('username is required');
const token = ctx.query.token;
const tokenUsername = tokenUser.username;
const userId = tokenUser.userId;
let { username } = ctx.query.data || {};
const user = await User.findByPk(userId);
if (!user) {
ctx.throw('user not found');
}
if (tokenUser.username === username) {
// 自己刷新自己的token
const user = await User.findByPk(tokenUser.id);
if (!user) {
ctx.throw('user not found');
}
if (user.type === 'user') {
const token = await user.createToken(null, loginType);
createCookie(token, ctx);
ctx.body = token;
return;
} else if (user.type === 'org' && tokenUser.uid) {
const token = await user.createToken(tokenUser.uid, loginType);
createCookie(token, ctx);
ctx.body = token;
return;
}
if (!username) {
username = user.username;
}
let me: User;
if (tokenUser.uid) {
me = await User.findByPk(tokenUser.uid);
const orgs = await user.getOrgs();
const orgsList = [tokenUser.username, user.username, , ...orgs];
if (orgsList.includes(username)) {
if (tokenUsername === username) {
const result = await User.oauth.resetToken(token);
createCookie(result, ctx);
await User.oauth.delToken(token);
ctx.body = result;
} else {
const user = await User.findOne({ where: { username } });
const result = await user.createToken(userId, 'default');
createCookie(result, ctx);
ctx.body = result;
}
} else {
me = await User.findByPk(tokenUser.id); // 真实用户
ctx.throw(403, 'Permission denied');
}
})
.addTo(app);
app
.route({
path: 'user',
key: 'refreshToken',
})
.define(async (ctx) => {
const { refreshToken } = ctx.query.data || {};
try {
if (!refreshToken) {
ctx.throw(400, 'Refresh Token is required');
}
const result = await User.oauth.refreshToken(refreshToken);
if (result) {
console.log('refreshToken result', result);
createCookie(result, ctx);
ctx.body = result;
} else {
ctx.throw(500, 'Refresh Token Failed, please login again');
}
} catch (e) {
console.log('refreshToken error', e);
ctx.throw(400, 'Refresh Token Failed');
}
if (!me || me.type === 'org') {
console.log('switch Error ', me.username, me.type);
ctx.throw('Permission denied');
}
if (type === 'user') {
const token = await me.createToken(null, loginType);
createCookie(token, ctx);
ctx.body = token;
return;
}
const orgUser = await User.findOne({ where: { username } });
if (!orgUser) {
ctx.throw('org user not found');
}
if (orgUser.type === 'user') {
// 想转换的type===org, 但实际上这个用户名是一个用户, 比如org调用switchOrg root
const token = await orgUser.createToken(null, loginType);
createCookie(token, ctx);
ctx.body = token;
return;
}
const user = await Org.findOne({ where: { username } });
const users = user.users;
const index = users.findIndex((u) => u.uid === me.id);
if (index === -1) {
ctx.throw('Permission denied');
}
const token = await orgUser.createToken(me.id, loginType);
createCookie(token, ctx);
ctx.body = token;
})
.addTo(app);

View File

@ -52,6 +52,8 @@ app
await org.addUser(user, { needPermission: true, role: role || 'user', operateId, isAdmin: !!tokenAdmin });
} else if (action === 'remove') {
await org.removeUser(user, { needPermission: true, operateId, isAdmin: !!tokenAdmin });
} else if (action === 'update') {
await org.addUser(user, { needPermission: true, role: role || 'user', operateId, isAdmin: !!tokenAdmin });
} else {
ctx.throw('操作错误');
}

View File

@ -143,6 +143,7 @@ app
path: 'org',
key: 'hasUser',
middleware: ['auth'],
description: '判断当前username这个组织是否在当前用户的组织中。如果有返回当前组织的用户信息否则返回null',
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
@ -159,7 +160,13 @@ app
};
return;
}
const usernameUser = await User.findOne({ where: { username } });
const usernameUser = await User.findOne({
where: { username },
attributes: {
exclude: ['password', 'salt'],
},
});
if (!usernameUser) {
ctx.body = {
uid: null,