Compare commits

...

2 Commits

Author SHA1 Message Date
633eee4bee feat: add UserSecret 2025-06-20 16:22:59 +08:00
d29f69452c fix bugs 2025-06-19 00:18:53 +08:00
19 changed files with 590 additions and 528 deletions

View File

@ -10,6 +10,7 @@
"type": "pm2-system-app", "type": "pm2-system-app",
"key": "code-center", "key": "code-center",
"entry": "./dist/app.js", "entry": "./dist/app.js",
"engine": "bun",
"runtime": [ "runtime": [
"client" "client"
] ]
@ -47,13 +48,14 @@
"commander": "^14.0.0", "commander": "^14.0.0",
"ioredis": "^5.6.1", "ioredis": "^5.6.1",
"minio": "^8.0.5", "minio": "^8.0.5",
"pg": "^8.16.0", "pg": "^8.16.1",
"pm2": "^6.0.8", "pm2": "^6.0.8",
"sequelize": "^6.37.7", "sequelize": "^6.37.7",
"sqlite3": "^5.1.7" "sqlite3": "^5.1.7"
}, },
"devDependencies": { "devDependencies": {
"@kevisual/code-center-module": "workspace:*", "@kevisual/code-center-module": "workspace:*",
"@kevisual/context": "^0.0.3",
"@kevisual/file-listener": "^0.0.2", "@kevisual/file-listener": "^0.0.2",
"@kevisual/local-app-manager": "0.1.22", "@kevisual/local-app-manager": "0.1.22",
"@kevisual/logger": "^0.0.4", "@kevisual/logger": "^0.0.4",
@ -61,20 +63,20 @@
"@kevisual/permission": "^0.0.3", "@kevisual/permission": "^0.0.3",
"@kevisual/router": "0.0.22", "@kevisual/router": "0.0.22",
"@kevisual/types": "^0.0.10", "@kevisual/types": "^0.0.10",
"@kevisual/use-config": "^1.0.18", "@kevisual/use-config": "^1.0.19",
"@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"@rollup/plugin-typescript": "^12.1.2", "@rollup/plugin-typescript": "^12.1.3",
"@types/archiver": "^6.0.3", "@types/archiver": "^6.0.3",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/formidable": "^3.4.5", "@types/formidable": "^3.4.5",
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.10",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.15.30", "@types/node": "^24.0.3",
"@types/react": "^19.1.6", "@types/react": "^19.1.8",
"@types/semver": "^7.7.0", "@types/semver": "^7.7.0",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"archiver": "^7.0.1", "archiver": "^7.0.1",
@ -92,10 +94,10 @@
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"p-queue": "^8.1.0", "p-queue": "^8.1.0",
"pg": "^8.16.0", "pg": "^8.16.1",
"pm2": "^6.0.8", "pm2": "^6.0.8",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"rollup": "^4.42.0", "rollup": "^4.44.0",
"rollup-plugin-copy": "^3.5.0", "rollup-plugin-copy": "^3.5.0",
"rollup-plugin-dts": "^6.2.1", "rollup-plugin-dts": "^6.2.1",
"rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-esbuild": "^6.2.1",
@ -105,11 +107,11 @@
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"tape": "^5.9.0", "tape": "^5.9.0",
"tar": "^7.4.3", "tar": "^7.4.3",
"tsx": "^4.19.4", "tsx": "^4.20.3",
"turbo": "^2.5.4", "turbo": "^2.5.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"zod": "^3.25.56" "zod": "^3.25.67"
}, },
"resolutions": { "resolutions": {
"inflight": "latest", "inflight": "latest",
@ -117,5 +119,5 @@
"picomatch": "^4.0.2" "picomatch": "^4.0.2"
}, },
"pnpm": {}, "pnpm": {},
"packageManager": "pnpm@10.11.1" "packageManager": "pnpm@10.12.1"
} }

829
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ app
const { rows: appDemo, count } = await AppDemoModel.findAndCountAll({ const { rows: appDemo, count } = await AppDemoModel.findAndCountAll({
where: { where: {
uid: tokenUser.uid, uid: tokenUser.id,
...searchWhere, ...searchWhere,
}, },
offset: (page - 1) * pageSize, offset: (page - 1) * pageSize,
@ -90,7 +90,7 @@ app
ctx.throw(400, 'id is required'); ctx.throw(400, 'id is required');
} }
const appDemo = await AppDemoModel.findByPk(id); const appDemo = await AppDemoModel.findByPk(id);
if (appDemo.uid !== tokenUser.uid) { if (appDemo.uid !== tokenUser.id) {
ctx.throw(403, 'No permission'); ctx.throw(403, 'No permission');
} }
await appDemo.destroy({ force }); await appDemo.destroy({ force });
@ -111,7 +111,7 @@ app
ctx.throw(400, 'id is required'); ctx.throw(400, 'id is required');
} }
const appDemo = await AppDemoModel.findByPk(id); const appDemo = await AppDemoModel.findByPk(id);
if (appDemo.uid !== tokenUser.uid) { if (appDemo.uid !== tokenUser.id) {
ctx.throw(403, 'No permission'); ctx.throw(403, 'No permission');
} }
ctx.body = appDemo; ctx.body = appDemo;

View File

@ -2,7 +2,7 @@ import { App } from '@kevisual/router';
import * as redisLib from './modules/redis.ts'; import * as redisLib from './modules/redis.ts';
import * as minioLib from './modules/minio.ts'; import * as minioLib from './modules/minio.ts';
import * as sequelizeLib from './modules/sequelize.ts'; import * as sequelizeLib from './modules/sequelize.ts';
import { useContextKey, useContext } from '@kevisual/use-config/context'; import { useContextKey } from '@kevisual/context';
import { SimpleRouter } from '@kevisual/router/simple'; import { SimpleRouter } from '@kevisual/router/simple';
import { OssBase } from '@kevisual/oss/services'; import { OssBase } from '@kevisual/oss/services';
export const router = useContextKey('router', () => new SimpleRouter()); export const router = useContextKey('router', () => new SimpleRouter());

View File

@ -42,7 +42,5 @@
// }); // });
// useContextKey('OrgModel', () => Org); // useContextKey('OrgModel', () => Org);
import { Org, OrgInit } from '@kevisual/code-center-module/models'; import { Org } from '@kevisual/code-center-module/models';
export { Org }; export { Org };
OrgInit();

View File

@ -1,6 +1,7 @@
import { User, UserInit, UserServices } from '@kevisual/code-center-module/models'; import { User, UserInit, UserServices } from '@kevisual/code-center-module/models';
export { User, UserInit, UserServices }; import { UserSecretInit, UserSecret } from '@kevisual/code-center-module/models';
import { OrgInit } from '@kevisual/code-center-module/models'; import { OrgInit } from '@kevisual/code-center-module/models';
export { User, UserInit, UserServices, UserSecret };
const init = async () => { const init = async () => {
await OrgInit(null, null, { await OrgInit(null, null, {
alter: true, alter: true,
@ -14,5 +15,11 @@ const init = async () => {
}).catch((e) => { }).catch((e) => {
console.error('User sync', e); console.error('User sync', e);
}); });
await UserSecretInit(null, null, {
alter: true,
logging: false,
}).catch((e) => {
console.error('UserSecret sync', e);
});
}; };
init(); init();

View File

@ -97,7 +97,6 @@ export const authMinio = async (req: IncomingMessage, res: ServerResponse, objec
etag, etag,
'last-modified': lastModified, 'last-modified': lastModified,
'Content-Disposition': contentDisposition, 'Content-Disposition': contentDisposition,
'file-name': filename,
...filteredMetaData, ...filteredMetaData,
}); });
const objectStream = await minioClient.getObject(bucketName, objectName); const objectStream = await minioClient.getObject(bucketName, objectName);

View File

@ -1,9 +1,8 @@
import { useFileStore } from '@kevisual/use-config/file-store'; import { useFileStore } from '@kevisual/use-config/file-store';
import { checkAuth, error, router, writeEvents, getKey, getTaskId } from '../router.ts'; import { checkAuth, error, router, writeEvents, getKey, getTaskId } from '../router.ts';
import { IncomingForm } from 'formidable'; import { IncomingForm } from 'formidable';
import { app, minioClient } from '@/app.ts'; import { app, oss } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts';
import { getContentType } from '@/utils/get-content-type.ts'; import { getContentType } from '@/utils/get-content-type.ts';
import { User } from '@/models/user.ts'; import { User } from '@/models/user.ts';
import fs from 'fs'; import fs from 'fs';
@ -124,8 +123,9 @@ router.post('/api/s1/resources/upload/chunk', async (req, res) => {
if (share) { if (share) {
metadata.share = 'public'; metadata.share = 'public';
} }
const bucketName = oss.bucketName;
// All chunks uploaded, now upload to MinIO // All chunks uploaded, now upload to MinIO
await minioClient.fPutObject(bucketName, minioPath, finalFilePath, { await oss.client.fPutObject(bucketName, minioPath, finalFilePath, {
'Content-Type': getContentType(relativePath), 'Content-Type': getContentType(relativePath),
'app-source': 'user-app', 'app-source': 'user-app',
'Cache-Control': relativePath.endsWith('.html') ? 'no-cache' : 'max-age=31536000, immutable', 'Cache-Control': relativePath.endsWith('.html') ? 'no-cache' : 'max-age=31536000, immutable',

View File

@ -5,7 +5,8 @@ router.all('/api/s1/share/*splat', async (req, res) => {
try { try {
const url = req.url; const url = req.url;
const _url = new URL(url || '', 'http://localhost'); const _url = new URL(url || '', 'http://localhost');
const objectName = _url.pathname.replace('/api/s1/share/', ''); let objectName = _url.pathname.replace('/api/s1/share/', '');
objectName = decodeURIComponent(objectName);
await authMinio(req, res, objectName); await authMinio(req, res, objectName);
} catch (e) { } catch (e) {
console.log('get share resource error url', req.url); console.log('get share resource error url', req.url);

View File

@ -35,6 +35,7 @@ router.post('/api/s1/resources/upload/check', async (req, res) => {
return; return;
} }
console.log('data', req.url); console.log('data', req.url);
res.writeHead(200, { 'Content-Type': 'application/json' });
const data = await router.getBody(req); const data = await router.getBody(req);
type Data = { type Data = {
appKey: string; appKey: string;

View File

@ -1,3 +1,3 @@
import { FileSyncModel, FileSyncModelType } from '@kevisual/file-listener/src/file-sync/model.ts'; import { FileSyncModel } from '@kevisual/file-listener/src/file-sync/model.ts';
import type { FileSyncModelType } from '@kevisual/file-listener/src/file-sync/model.ts';
export { FileSyncModel, FileSyncModelType }; export { FileSyncModel, FileSyncModelType };

View File

@ -19,7 +19,7 @@ app
const tokenUser = ctx.state.tokenUser; const tokenUser = ctx.state.tokenUser;
const data = ctx.query?.data; const data = ctx.query?.data;
const { id, key, force, install, appKey: postAppKey, version: postVersion = '1.0.0' } = data; const { id, key, force, install, appKey: postAppKey, version: postVersion = '1.0.0' } = data;
if (!id || !postAppKey) { if (!id && !postAppKey) {
ctx.throw(400, 'Invalid id or postAppKey'); ctx.throw(400, 'Invalid id or postAppKey');
} }
let username = tokenUser.username; let username = tokenUser.username;

View File

@ -3,12 +3,14 @@ import './org.ts';
import './me.ts'; import './me.ts';
import './update.ts' import './update.ts';
import './init.ts' import './init.ts';
import './web-login.ts' import './web-login.ts';
import './org-user/list.ts' import './org-user/list.ts';
import './admin/user.ts'; import './admin/user.ts';
import './secret-key/list.ts';

View File

@ -0,0 +1,136 @@
import { Op } from 'sequelize';
import { User, UserSecret } from '@/models/user.ts';
import { app } from '@/app.ts';
app
.route({
path: 'secret',
key: 'list',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { page = 1, pageSize = 20, search, sort = 'DESC', orgId } = ctx.query;
const searchWhere: Record<string, any> = search
? {
[Op.or]: [{ title: { [Op.like]: `%${search}%` } }, { description: { [Op.like]: `%${search}%` } }],
}
: {};
if (orgId) {
searchWhere.orgId = orgId;
}
const { rows: secrets, count } = await UserSecret.findAndCountAll({
where: {
userId: tokenUser.userId,
...searchWhere,
},
offset: (page - 1) * pageSize,
limit: pageSize,
attributes: {
exclude: ['token'], // Exclude sensitive token field
},
order: [['updatedAt', sort]],
});
ctx.body = {
list: secrets,
pagination: {
page,
current: page,
pageSize,
total: count,
},
};
})
.addTo(app);
app
.route({
path: 'secret',
key: 'update',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id, updatedAt: _clear, createdAt: _clear2, token, ...rest } = ctx.query.data;
let secret: UserSecret;
let isNew = false;
if (id) {
secret = await UserSecret.findByPk(id);
if (!secret) {
ctx.throw(404, 'Secret not found');
}
if (secret.userId !== tokenUser.userId) {
ctx.throw(403, 'No permission');
}
} else {
secret = await UserSecret.createSecret(tokenUser);
isNew = true;
}
if (secret) {
secret = await secret.update({
...rest,
});
}
ctx.body = secret;
})
.addTo(app);
app
.route({
path: 'secret',
key: 'delete',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query.data || {};
if (!id) {
ctx.throw(400, 'id is required');
}
const secret = await UserSecret.findByPk(id);
if (!secret) {
ctx.throw(404, 'Secret not found');
}
if (secret.userId !== tokenUser.userId) {
ctx.throw(403, 'No permission');
}
await secret.destroy();
ctx.body = secret;
})
.addTo(app);
app
.route({
path: 'secret',
key: 'get',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query.data || {};
if (!id) {
ctx.throw(400, 'id is required');
}
const secret = await UserSecret.findByPk(id);
if (!secret) {
ctx.throw(404, 'Secret not found');
}
if (secret.userId !== tokenUser.uid) {
ctx.throw(403, 'No permission');
}
ctx.body = secret;
})
.addTo(app);

View File

@ -0,0 +1,3 @@
import * as redisLib from '../modules/redis.ts';
import { useContextKey, useContext } from '@kevisual/use-config/context';
export const redis = useContextKey('redis', () => redisLib.redis);

View File

@ -1,7 +1,8 @@
import { config } from '../modules/config.ts'; import { config } from '../modules/config.ts';
import { sequelize } from '../modules/sequelize.ts'; import { sequelize } from '../modules/sequelize.ts';
export { program, Command } from '../program.ts'; export { program, Command } from '../program.ts';
import { User, UserInit, OrgInit, Org } from '@kevisual/code-center-module/models'; // import { User, UserInit, OrgInit, Org, UserSecretInit, UserSecret } from '@kevisual/code-center-module/models';
import { User, UserInit, OrgInit, Org, UserSecretInit, UserSecret } from '@kevisual/code-center-module/src/core-models.ts';
import { Logger } from '@kevisual/logger'; import { Logger } from '@kevisual/logger';
export const close = async () => { export const close = async () => {
process.exit(0); process.exit(0);
@ -22,8 +23,13 @@ export const initUser = async () => {
alter: true, alter: true,
logging: false, logging: false,
}); });
await UserSecretInit(sequelize, undefined, {
alter: true,
logging: false,
});
return { return {
User: User, User: User,
Org: Org, Org: Org,
UserSecret: UserSecret,
}; };
}; };

View File

@ -0,0 +1,62 @@
import { sequelize } from '../modules/sequelize.ts';
import { initUser } from '../scripts/common.ts';
import '../scripts/common-redis.ts';
import { useContextKey } from '@kevisual/use-config/context';
export const main = async () => {
const models = await initUser();
const username = 'root';
const orgname = 'admin';
const user = await models.User.findOne({ where: { username } });
const org = await models.User.findOne({ where: { username: orgname } });
console.log('user.id', user?.id);
console.log('org.id', org?.id);
// const userSecret1 = await models.UserSecret.createSecret(user?.id!);
// userSecret1.title = 'root secret';
// await userSecret1.save();
// await models.UserSecret.destroy({
// where: {
// orgId: '16a496d4-8cd6-4e02-b403-c2adc006a53d',
// },
// });
const userSecret2 = await models.UserSecret.createSecret(user?.id!, org?.id!);
userSecret2.title = 'root org secret';
await userSecret2.save();
const secretList = await models.UserSecret.findAll();
for (const secret of secretList) {
console.log(`\nSecret ID: ${secret.id}, User ID: ${secret.userId}, Org ID: ${secret.orgId}, Token: ${secret.token}, Expired Time: ${secret.expiredTime}`);
}
process.exit(0);
};
main();
export const dropTable = async () => {
await sequelize.query('DROP TABLE IF EXISTS "cf_user_secrets"');
console.log('UserSecret table dropped');
process.exit(0);
};
// dropTable()
const token1 = 'sk_tvwzgp5lky8iupawh0encvd52vji4o8argvd2x668gn15q83xpgo8fe10ny7wfsq';
const orgToken2 = 'sk_x37p8iifh6k18c3f121w49nmfy1sbjqpyol9fcsz0lmc5dz493wrfwvtxc4gi9od';
export const main2 = async () => {
const redis = useContextKey('redis');
if (!redis) {
console.error('Redis is not initialized');
return;
}
const models = await initUser();
const UserSecret = models.UserSecret;
const v = await models.UserSecret.verifyToken(token1);
console.log('verifyToken', v);
process.exit(0);
};
// main2();

@ -1 +1 @@
Subproject commit 83f65e1554ef051e2164264df18496a90cede3c0 Subproject commit 922b0c421f02bf6b63a0d9a52ca5b8af38c591cf

@ -1 +1 @@
Subproject commit cd43d662653c8e87648a9ea8c3ff4aa63821ac70 Subproject commit 7e3b748d30a2be454edee8aebefb07b28901ec3a