关于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", "start": "pm2 start dist/app.mjs --name codecenter",
"release": "node ./config/release/index.mjs", "release": "node ./config/release/index.mjs",
"pub": "envision pack -p -u", "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": [], "keywords": [],
"types": "types/index.d.ts", "types": "types/index.d.ts",
@ -32,7 +33,6 @@
], ],
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@kevisual/auth": "1.0.5",
"@kevisual/local-app-manager": "0.1.9", "@kevisual/local-app-manager": "0.1.9",
"@kevisual/router": "0.0.9", "@kevisual/router": "0.0.9",
"@kevisual/use-config": "^1.0.9", "@kevisual/use-config": "^1.0.9",
@ -46,10 +46,10 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"minio": "^8.0.5", "minio": "^8.0.5",
"nanoid": "^5.1.3", "nanoid": "^5.1.5",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"p-queue": "^8.1.0", "p-queue": "^8.1.0",
"pg": "^8.14.0", "pg": "^8.14.1",
"pm2": "^6.0.5", "pm2": "^6.0.5",
"rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-esbuild": "^6.2.1",
"semver": "^7.7.1", "semver": "^7.7.1",
@ -61,7 +61,7 @@
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@kevisual/code-center-module": "0.0.13", "@kevisual/code-center-module": "workspace:*",
"@kevisual/types": "^0.0.6", "@kevisual/types": "^0.0.6",
"@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-commonjs": "^28.0.3",
@ -75,15 +75,15 @@
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.13.10", "@types/node": "^22.13.10",
"@types/react": "^19.0.10", "@types/react": "^19.0.12",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"rollup": "^4.35.0", "rollup": "^4.36.0",
"rollup-plugin-copy": "^3.5.0", "rollup-plugin-copy": "^3.5.0",
"rollup-plugin-dts": "^6.1.1", "rollup-plugin-dts": "^6.2.0",
"tape": "^5.9.0", "tape": "^5.9.0",
"tsx": "^4.19.3", "tsx": "^4.19.3",
"typescript": "^5.8.2" "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 './routes/index.ts';
import { app } from './app.ts'; import { app } from './app.ts';
import { useConfig } from '@kevisual/use-config';
import { User } from './models/user.ts'; import { User } from './models/user.ts';
import { createAuthRoute } from '@kevisual/auth'; import { addAuth } from '@kevisual/code-center-module/models';
const config = useConfig<{ tokenSecret: string }>();
createAuthRoute({ addAuth(app);
app,
secret: config.tokenSecret,
});
app app
.route({ .route({

View File

@ -7,8 +7,6 @@ import { useFileStore } from '@kevisual/use-config/file-store';
import { app, minioClient } from '@/app.ts'; import { app, minioClient } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts'; import { bucketName } from '@/modules/minio.ts';
import { getContentType } from '@/utils/get-content-type.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 }); const cacheFilePath = useFileStore('cache-file', { needExists: true });
router.post('/api/micro-app/upload', async (req, res) => { 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 { function parseIfJson(collection: any): any {
try { try {
return JSON.parse(collection); return JSON.parse(collection);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { app } from '@/app.ts'; import { app } from '@/app.ts';
import { ConfigModel } from './models/model.ts'; import { ConfigModel } from './models/model.ts';
import { ShareConfigService } from './services/share.ts';
app app
.route({ .route({
path: 'config', path: 'config',
@ -12,6 +13,7 @@ app
where: { where: {
uid: id, uid: id,
}, },
order: [['updatedAt', 'DESC']],
}); });
ctx.body = { ctx.body = {
list: config, list: config,
@ -19,3 +21,123 @@ app
}) })
.addTo(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 { export interface ConfigData {
key?: string; key?: string;
version?: string; version?: string;
permission?: {
share?: 'public' | 'private';
};
} }
export type Config = Partial<InstanceType<typeof ConfigModel>>; export type Config = Partial<InstanceType<typeof ConfigModel>>;
@ -70,7 +73,6 @@ export class ConfigModel extends Model {
static async getUploadConfig(opts: { uid: string }) { static async getUploadConfig(opts: { uid: string }) {
const defaultConfig = { const defaultConfig = {
key: 'upload', key: 'upload',
type: 'upload',
version: '1.0.0', version: '1.0.0',
}; };
const config = await ConfigModel.getConfig('upload', { 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 './list.ts';
import './upload-list.ts'

View File

@ -1,81 +1,8 @@
import { app } from '@/app.ts'; import { app } from '@/app.ts';
import { MicroAppUploadModel } from './models.ts';
import { appPathCheck, installApp } from './module/install-app.ts'; import { appPathCheck, installApp } from './module/install-app.ts';
import { manager } from './manager-app.ts'; import { manager } from './manager-app.ts';
import { selfRestart } from '@/modules/self-restart.ts'; import { selfRestart } from '@/modules/self-restart.ts';
import { AppListModel } from '../app-manager/module/index.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);
// curl http://localhost:4002/api/router?path=micro-app&key=deploy // curl http://localhost:4002/api/router?path=micro-app&key=deploy
// 把对应的应用安装到系统的apps目录下并解压然后把配置项写入数据库配置 // 把对应的应用安装到系统的apps目录下并解压然后把配置项写入数据库配置
// key 是应用的唯一标识和package.json中的key一致绑定关系 // key 是应用的唯一标识和package.json中的key一致绑定关系
@ -86,24 +13,25 @@ app
path: 'micro-app', path: 'micro-app',
key: 'deploy', key: 'deploy',
description: 'Deploy micro app in server', description: 'Deploy micro app in server',
middleware: ['auth'],
}) })
.define(async (ctx) => { .define(async (ctx) => {
const { id, key, force, install } = ctx.query?.data; const tokenUser = ctx.state.tokenUser;
// const id = '10f03411-85fc-4d37-a4d3-e32b15566a6c'; const data = ctx.query?.data;
// const key = 'envision-cli'; const { id, key, force, install } = data;
// const id = '7c54a6de-9171-4093-926d-67a035042c6c';
// const key = 'mark';
if (!id) { if (!id) {
ctx.throw(400, 'Invalid id'); ctx.throw(400, 'Invalid id');
} }
const microApp = await MicroAppUploadModel.findByPk(id); let username = tokenUser.username;
const { file } = microApp.data || {}; if (data.username) {
const path = file?.path; // username = data.username;
if (!path) {
ctx.throw(404, 'Invalid path');
} }
console.log('path', path); const microApp = await AppListModel.findByPk(id);
const check = await appPathCheck({ key }); if (!microApp) {
ctx.throw(400, 'Invalid id');
}
const { key: appKey, version } = microApp;
const check = await appPathCheck({ key: key });
let appType: string; let appType: string;
if (check) { if (check) {
if (!force) { if (!force) {
@ -115,7 +43,7 @@ app
await manager.removeApp(key); 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); await manager.add(installAppData.showAppInfo);
ctx.body = installAppData; ctx.body = installAppData;
if (appType === 'system-app') { 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 { minioClient } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts'; import { bucketName } from '@/modules/minio.ts';
import { fileIsExist } from '@kevisual/use-config'; 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 fs from 'fs';
import path from 'path'; import path from 'path';
import * as tar from 'tar';
import { appsPath } from '../lib/index.ts'; import { appsPath } from '../lib/index.ts';
import { installAppFromKey } from './manager.ts'; import { installAppFromKey } from './manager.ts';
export type InstallAppOpts = { export type InstallAppOpts = {
path?: string; path?: string;
key?: string; key?: string;
needInstallDeps?: boolean; needInstallDeps?: boolean;
// minio中
appKey?: string;
version?: string;
username?: string;
}; };
/** /**
* *
@ -26,28 +31,37 @@ export const appPathCheck = async (opts: InstallAppOpts) => {
return false; return false;
}; };
export const installApp = async (opts: InstallAppOpts) => { export const installApp = async (opts: InstallAppOpts) => {
const { key, needInstallDeps } = opts; const { key, needInstallDeps, appKey, version, username } = opts;
const fileStream = await minioClient.getObject(bucketName, opts.path); const prefix = `${username}/${appKey}/${version}`;
const pathName = opts.path.split('/').pop(); 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); const directory = path.join(appsPath, key);
if (!fileIsExist(directory)) { if (!fileIsExist(directory)) {
fs.mkdirSync(directory, { recursive: true }); 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) { if (needInstallDeps) {
try { try {
installDeps({ appPath: directory, isProduction: true, sync: true }); installDeps({ appPath: directory, isProduction: true, sync: true });
@ -57,7 +71,6 @@ export const installApp = async (opts: InstallAppOpts) => {
} }
return installAppFromKey(key); return installAppFromKey(key);
}; };
import { spawn, spawnSync } from 'child_process';
export const checkPnpm = () => { export const checkPnpm = () => {
try { try {
@ -76,13 +89,16 @@ type InstallDepsOptions = {
export const installDeps = async (opts: InstallDepsOptions) => { export const installDeps = async (opts: InstallDepsOptions) => {
const { appPath } = opts; const { appPath } = opts;
const isProduction = opts.isProduction ?? true; const isProduction = opts.isProduction ?? true;
const isPnpm = checkPnpm();
const params = ['i']; const params = ['i'];
if (isProduction) { if (isProduction && isPnpm) {
params.push('--production'); params.push('--production');
} else {
params.push('--omit=dev');
} }
console.log('installDeps', appPath, params); console.log('installDeps', appPath, params);
const syncSpawn = opts.sync ? spawnSync : spawn; const syncSpawn = opts.sync ? spawnSync : spawn;
if (checkPnpm()) { if (isPnpm) {
syncSpawn('pnpm', params, { cwd: appPath, stdio: 'inherit', env: process.env }); syncSpawn('pnpm', params, { cwd: appPath, stdio: 'inherit', env: process.env });
} else { } else {
syncSpawn('npm', params, { cwd: appPath, stdio: 'inherit', env: process.env }); 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'); const pkgs = path.join(directory, 'package.json');
if (!fileIsExist(pkgs)) { 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 json = fs.readFileSync(pkgs, 'utf-8');
const pkg = JSON.parse(json); const pkg = JSON.parse(json);
const { name, version, app } = pkg; const { name, version, app } = pkg;
if (!name || !version || !app) { 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'); const readmeFile = path.join(directory, 'README.md');
let readmeDesc = ''; 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) { if (!domain) {
return; return;
} }
ctx.res.cookie('token', token.token, { if (ctx.res.cookie) {
maxAge: token.expireTime, ctx.res.cookie('token', token.token, {
domain, maxAge: 7 * 24 * 60 * 60 * 1000, // 过期时间, 设置7天
sameSite: 'lax', domain,
httpOnly: true, sameSite: 'lax',
}); httpOnly: true,
});
}
}; };
const clearCookie = (ctx: any) => { const clearCookie = (ctx: any) => {
if (!domain) { if (!domain) {
@ -53,8 +55,11 @@ app
.route({ .route({
path: 'user', path: 'user',
key: 'login', key: 'login',
middleware: ['auth-can'],
}) })
.define(async (ctx) => { .define(async (ctx) => {
const oldToken = ctx.query.token;
const tokenUser = ctx.state?.tokenUser || {};
const { username, email, password, loginType = 'default' } = ctx.query; const { username, email, password, loginType = 'default' } = ctx.query;
if (!username && !email) { if (!username && !email) {
ctx.throw(400, 'username or email is required'); ctx.throw(400, 'username or email is required');
@ -69,6 +74,15 @@ app
if (!user) { if (!user) {
ctx.throw(500, 'Login Failed'); 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)) { if (!user.checkPassword(password)) {
ctx.throw(500, 'Password error'); ctx.throw(500, 'Password error');
} }
@ -89,11 +103,18 @@ app
}) })
.addTo(app); .addTo(app);
app app
.route('user', 'auth') .route({
path: 'user',
key: 'auth',
middleware: ['auth-can'],
})
.define(async (ctx) => { .define(async (ctx) => {
const { checkToken: token } = ctx.query; const { checkToken: token } = ctx.query;
try { try {
const result = await User.verifyToken(token); const result = await User.verifyToken(token);
if (result) {
delete result.oauthExpand;
}
ctx.body = result || {}; ctx.body = result || {};
} catch (e) { } catch (e) {
ctx.throw(401, 'Token InValid '); ctx.throw(401, 'Token InValid ');
@ -102,7 +123,9 @@ app
.addTo(app); .addTo(app);
app app
.route('user', 'updateSelf', { .route({
path: 'user',
key: 'updateSelf',
middleware: ['auth'], middleware: ['auth'],
}) })
.define(async (ctx) => { .define(async (ctx) => {
@ -133,6 +156,60 @@ app
ctx.body = await user.getInfo(); ctx.body = await user.getInfo();
}) })
.addTo(app); .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 app
.route({ .route({
path: 'user', path: 'user',
@ -141,63 +218,60 @@ app
}) })
.define(async (ctx) => { .define(async (ctx) => {
const tokenUser = ctx.state.tokenUser; const tokenUser = ctx.state.tokenUser;
const { username, type = 'org', loginType } = ctx.query.data || {}; const token = ctx.query.token;
if (!username && type === 'org') { const tokenUsername = tokenUser.username;
ctx.throw('username is required'); 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) { if (!username) {
// 自己刷新自己的token username = user.username;
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;
}
} }
let me: User;
if (tokenUser.uid) { const orgs = await user.getOrgs();
me = await User.findByPk(tokenUser.uid); 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 { } 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); .addTo(app);

View File

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

View File

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