Compare commits
14 Commits
337abd2bc3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 266b7b33de | |||
| 5200cf4c38 | |||
| bf436f05e3 | |||
| bd7525efb0 | |||
| f616045625 | |||
| a51d04341e | |||
| 7bbefd8a4a | |||
| db5c5a89b3 | |||
| 86d4c7f75b | |||
| cbc9b54284 | |||
| b1d3ca241c | |||
| 158dd9e85c | |||
| 82e3392b36 | |||
| a0f0f65d20 |
16
package.json
16
package.json
@@ -49,7 +49,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kevisual/ai": "^0.0.24",
|
"@kevisual/ai": "^0.0.24",
|
||||||
"@kevisual/auth": "^2.0.3",
|
"@kevisual/auth": "^2.0.3",
|
||||||
"@kevisual/query": "^0.0.38",
|
"@kevisual/js-filter": "^0.0.5",
|
||||||
|
"@kevisual/query": "^0.0.39",
|
||||||
"@types/busboy": "^1.5.4",
|
"@types/busboy": "^1.5.4",
|
||||||
"@types/send": "^1.2.1",
|
"@types/send": "^1.2.1",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
@@ -71,22 +72,23 @@
|
|||||||
"zod-to-json-schema": "^3.25.1"
|
"zod-to-json-schema": "^3.25.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.980.0",
|
"@aws-sdk/client-s3": "^3.981.0",
|
||||||
|
"@kevisual/api": "^0.0.44",
|
||||||
"@kevisual/code-center-module": "0.0.24",
|
"@kevisual/code-center-module": "0.0.24",
|
||||||
"@kevisual/context": "^0.0.4",
|
"@kevisual/context": "^0.0.4",
|
||||||
"@kevisual/file-listener": "^0.0.2",
|
"@kevisual/file-listener": "^0.0.2",
|
||||||
"@kevisual/local-app-manager": "0.1.32",
|
"@kevisual/local-app-manager": "0.1.32",
|
||||||
"@kevisual/logger": "^0.0.4",
|
"@kevisual/logger": "^0.0.4",
|
||||||
"@kevisual/oss": "0.0.18",
|
"@kevisual/oss": "0.0.19",
|
||||||
"@kevisual/permission": "^0.0.3",
|
"@kevisual/permission": "^0.0.4",
|
||||||
"@kevisual/router": "0.0.65",
|
"@kevisual/router": "0.0.70",
|
||||||
"@kevisual/types": "^0.0.12",
|
"@kevisual/types": "^0.0.12",
|
||||||
"@kevisual/use-config": "^1.0.28",
|
"@kevisual/use-config": "^1.0.30",
|
||||||
"@types/archiver": "^7.0.0",
|
"@types/archiver": "^7.0.0",
|
||||||
"@types/bun": "^1.3.8",
|
"@types/bun": "^1.3.8",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^25.1.0",
|
"@types/node": "^25.2.0",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.16.0",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/xml2js": "^0.4.14",
|
"@types/xml2js": "^0.4.14",
|
||||||
|
|||||||
630
pnpm-lock.yaml
generated
630
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,13 @@ export class User extends Model {
|
|||||||
oauthUser.orgId = id;
|
oauthUser.orgId = id;
|
||||||
}
|
}
|
||||||
const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...expand });
|
const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...expand });
|
||||||
return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken };
|
return {
|
||||||
|
accessToken: token.accessToken,
|
||||||
|
refreshToken: token.refreshToken,
|
||||||
|
token: token.accessToken,
|
||||||
|
refreshTokenExpiresIn: token.refreshTokenExpiresIn,
|
||||||
|
accessTokenExpiresIn: token.accessTokenExpiresIn,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 验证token
|
* 验证token
|
||||||
|
|||||||
@@ -70,9 +70,16 @@ interface Store<T> {
|
|||||||
expire: (key: string, ttl?: number) => Promise<void>;
|
expire: (key: string, ttl?: number) => Promise<void>;
|
||||||
delObject: (value?: T) => Promise<void>;
|
delObject: (value?: T) => Promise<void>;
|
||||||
keys: (key?: string) => Promise<string[]>;
|
keys: (key?: string) => Promise<string[]>;
|
||||||
setToken: (value: { accessToken: string; refreshToken: string; value?: T }, opts?: StoreSetOpts) => Promise<void>;
|
setToken: (value: { accessToken: string; refreshToken: string; value?: T }, opts?: StoreSetOpts) => Promise<TokenData>;
|
||||||
delKeys: (keys: string[]) => Promise<number>;
|
delKeys: (keys: string[]) => Promise<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TokenData = {
|
||||||
|
accessToken: string;
|
||||||
|
accessTokenExpiresIn?: number;
|
||||||
|
refreshToken?: string;
|
||||||
|
refreshTokenExpiresIn?: number;
|
||||||
|
}
|
||||||
export class RedisTokenStore implements Store<OauthUser> {
|
export class RedisTokenStore implements Store<OauthUser> {
|
||||||
redis: Redis;
|
redis: Redis;
|
||||||
private prefix: string = 'oauth:';
|
private prefix: string = 'oauth:';
|
||||||
@@ -131,7 +138,7 @@ export class RedisTokenStore implements Store<OauthUser> {
|
|||||||
await this.del(userPrefix + ':token:' + accessToken);
|
await this.del(userPrefix + ':token:' + accessToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async setToken(data: { accessToken: string; refreshToken: string; value?: OauthUser }, opts?: StoreSetOpts) {
|
async setToken(data: { accessToken: string; refreshToken: string; value?: OauthUser }, opts?: StoreSetOpts): Promise<TokenData> {
|
||||||
const { accessToken, refreshToken, value } = data;
|
const { accessToken, refreshToken, value } = data;
|
||||||
let userPrefix = 'user:' + value?.id;
|
let userPrefix = 'user:' + value?.id;
|
||||||
if (value?.orgId) {
|
if (value?.orgId) {
|
||||||
@@ -163,14 +170,20 @@ export class RedisTokenStore implements Store<OauthUser> {
|
|||||||
|
|
||||||
await this.set(accessToken, JSON.stringify(value), expire);
|
await this.set(accessToken, JSON.stringify(value), expire);
|
||||||
await this.set(userPrefix + ':token:' + accessToken, accessToken, expire);
|
await this.set(userPrefix + ':token:' + accessToken, accessToken, expire);
|
||||||
|
let refreshTokenExpiresIn = Math.min(expire * 7, 60 * 60 * 24 * 30, 60 * 60 * 24 * 365); // 最大为一年
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
let refreshTokenExpire = Math.min(expire * 7, 60 * 60 * 24 * 30, 60 * 60 * 24 * 365); // 最大为一年
|
|
||||||
// 小于7天, 则设置为7天
|
// 小于7天, 则设置为7天
|
||||||
if (refreshTokenExpire < 60 * 60 * 24 * 7) {
|
if (refreshTokenExpiresIn < 60 * 60 * 24 * 7) {
|
||||||
refreshTokenExpire = 60 * 60 * 24 * 7;
|
refreshTokenExpiresIn = 60 * 60 * 24 * 7;
|
||||||
}
|
}
|
||||||
await this.set(refreshToken, JSON.stringify(value), refreshTokenExpire);
|
await this.set(refreshToken, JSON.stringify(value), refreshTokenExpiresIn);
|
||||||
await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpire);
|
await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpiresIn);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
accessTokenExpiresIn: expire,
|
||||||
|
refreshToken,
|
||||||
|
refreshTokenExpiresIn: refreshTokenExpiresIn,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async delKeys(keys: string[]) {
|
async delKeys(keys: string[]) {
|
||||||
@@ -206,10 +219,7 @@ export class OAuth<T extends OauthUser> {
|
|||||||
async generateToken(
|
async generateToken(
|
||||||
user: T,
|
user: T,
|
||||||
expandOpts?: StoreSetOpts,
|
expandOpts?: StoreSetOpts,
|
||||||
): Promise<{
|
): Promise<TokenData> {
|
||||||
accessToken: string;
|
|
||||||
refreshToken?: string;
|
|
||||||
}> {
|
|
||||||
// 拥有refreshToken 为 true 时,accessToken 为 st_ 开头,refreshToken 为 rk_开头
|
// 拥有refreshToken 为 true 时,accessToken 为 st_ 开头,refreshToken 为 rk_开头
|
||||||
// 意思是secretToken 和 secretKey的缩写
|
// 意思是secretToken 和 secretKey的缩写
|
||||||
const accessToken = expandOpts?.hasRefreshToken ? 'st_' + randomId32() : 'sk_' + randomId64();
|
const accessToken = expandOpts?.hasRefreshToken ? 'st_' + randomId32() : 'sk_' + randomId64();
|
||||||
@@ -227,9 +237,9 @@ export class OAuth<T extends OauthUser> {
|
|||||||
user.oauthExpand.refreshToken = refreshToken;
|
user.oauthExpand.refreshToken = refreshToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.store.setToken({ accessToken, refreshToken, value: user }, expandOpts);
|
const tokenData = await this.store.setToken({ accessToken, refreshToken, value: user }, expandOpts);
|
||||||
|
|
||||||
return { accessToken, refreshToken };
|
return tokenData;
|
||||||
}
|
}
|
||||||
async saveSecretKey(oauthUser: T, secretKey: string, opts?: StoreSetOpts) {
|
async saveSecretKey(oauthUser: T, secretKey: string, opts?: StoreSetOpts) {
|
||||||
// 生成一个secretKey
|
// 生成一个secretKey
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export const getTextContentType = (filePath: string, isFilePath = false) => {
|
|||||||
'.env',
|
'.env',
|
||||||
'.example',
|
'.example',
|
||||||
'.log',
|
'.log',
|
||||||
'.mjs',
|
|
||||||
'.map',
|
'.map',
|
||||||
'.json5',
|
'.json5',
|
||||||
'.pem',
|
'.pem',
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ export const getFileList = async (list: any, opts?: { objectName: string; app: s
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
// import { logger } from '@/module/logger.ts';
|
// import { logger } from '@/module/logger.ts';
|
||||||
|
/**
|
||||||
|
* GET 处理 AI 代理请求
|
||||||
|
* 1. 如果是目录请求,返回目录列表
|
||||||
|
* 2. 如果是文件请求,返回文件流
|
||||||
|
*
|
||||||
|
* 如果是 stat
|
||||||
|
* 只返回对应的 stat 信息
|
||||||
|
*/
|
||||||
const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: ProxyOptions) => {
|
const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: ProxyOptions) => {
|
||||||
const { createNotFoundPage } = opts;
|
const { createNotFoundPage } = opts;
|
||||||
const _u = new URL(req.url, 'http://localhost');
|
const _u = new URL(req.url, 'http://localhost');
|
||||||
@@ -63,6 +71,7 @@ const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: Proxy
|
|||||||
const hash = params.get('hash');
|
const hash = params.get('hash');
|
||||||
let dir = !!params.get('dir');
|
let dir = !!params.get('dir');
|
||||||
const recursive = !!params.get('recursive');
|
const recursive = !!params.get('recursive');
|
||||||
|
const showStat = !!params.get('stat');
|
||||||
const { objectName, app, owner, loginUser, isOwner } = await getObjectName(req);
|
const { objectName, app, owner, loginUser, isOwner } = await getObjectName(req);
|
||||||
if (!dir && _u.pathname.endsWith('/')) {
|
if (!dir && _u.pathname.endsWith('/')) {
|
||||||
dir = true; // 如果是目录请求,强制设置为true
|
dir = true; // 如果是目录请求,强制设置为true
|
||||||
@@ -89,10 +98,19 @@ const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: Proxy
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const stat = await oss.statObject(objectName);
|
const stat = await oss.statObject(objectName);
|
||||||
if (!stat) {
|
if (!stat && isOwner) {
|
||||||
createNotFoundPage('Invalid proxy url');
|
// createNotFoundPage('文件不存在');
|
||||||
|
res.writeHead(200, { 'content-type': 'application/json' });
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
code: 404,
|
||||||
|
message: 'object not found',
|
||||||
|
}),
|
||||||
|
);
|
||||||
logger.debug('no stat', objectName, owner, req.url);
|
logger.debug('no stat', objectName, owner, req.url);
|
||||||
return true;
|
return true;
|
||||||
|
} else if (!stat && !isOwner) {
|
||||||
|
return createNotFoundPage('Invalid ai proxy url');
|
||||||
}
|
}
|
||||||
const permissionInstance = new UserPermission({ permission: stat.metaData as Permission, owner: owner });
|
const permissionInstance = new UserPermission({ permission: stat.metaData as Permission, owner: owner });
|
||||||
const checkPermission = permissionInstance.checkPermissionSuccess({
|
const checkPermission = permissionInstance.checkPermissionSuccess({
|
||||||
@@ -103,6 +121,20 @@ const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: Proxy
|
|||||||
logger.info('no permission', checkPermission, loginUser, owner);
|
logger.info('no permission', checkPermission, loginUser, owner);
|
||||||
return createNotFoundPage('no permission');
|
return createNotFoundPage('no permission');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showStat) {
|
||||||
|
res.writeHead(200, { 'content-type': 'application/json' });
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
...stat,
|
||||||
|
metaData: filterKeys(stat.metaData, ['etag', 'size']),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (hash && stat.etag === hash) {
|
if (hash && stat.etag === hash) {
|
||||||
res.writeHead(304); // not modified
|
res.writeHead(304); // not modified
|
||||||
res.end('not modified');
|
res.end('not modified');
|
||||||
@@ -295,6 +327,48 @@ export const renameProxy = async (req: IncomingMessage, res: ServerResponse, opt
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateMetadataProxy = async (req: IncomingMessage, res: ServerResponse, opts: ProxyOptions) => {
|
||||||
|
const { objectName, isOwner } = await getObjectName(req);
|
||||||
|
let oss = opts.oss;
|
||||||
|
if (!isOwner) {
|
||||||
|
return opts?.createNotFoundPage?.('no permission');
|
||||||
|
}
|
||||||
|
const end = (data: any, message?: string, code = 200) => {
|
||||||
|
res.writeHead(code, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ code: code, data: data, message: message || 'success' }));
|
||||||
|
};
|
||||||
|
const _u = new URL(req.url, 'http://localhost');
|
||||||
|
const params = _u.searchParams;
|
||||||
|
const metaParam = params.get('meta');
|
||||||
|
if (!metaParam) {
|
||||||
|
return end({ success: false }, 'meta parameter required', 400);
|
||||||
|
}
|
||||||
|
const meta = parseSearchValue(metaParam, { decode: true });
|
||||||
|
try {
|
||||||
|
const stat = await oss.statObject(objectName);
|
||||||
|
if (!stat) {
|
||||||
|
return end({ success: false }, 'object not found', 404);
|
||||||
|
}
|
||||||
|
const newMeta = {
|
||||||
|
"app-source": "user-app",
|
||||||
|
...meta,
|
||||||
|
};
|
||||||
|
console.log('update metadata', objectName, newMeta);
|
||||||
|
// 过滤掉包含无效字符的 key(S3 元数据头不支持某些字符)
|
||||||
|
const filteredMeta: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(newMeta)) {
|
||||||
|
if (/^[\w\-]+$/.test(key)) {
|
||||||
|
filteredMeta[key] = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await oss.replaceObject(objectName, filteredMeta);
|
||||||
|
end({ success: true, objectName, meta }, 'update metadata success', 200);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('updateMetadataProxy error', error);
|
||||||
|
end({ success: false, error }, 'update metadata failed', 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export type ProxyOptions = {
|
export type ProxyOptions = {
|
||||||
createNotFoundPage: (msg?: string) => any;
|
createNotFoundPage: (msg?: string) => any;
|
||||||
oss?: OssBase;
|
oss?: OssBase;
|
||||||
@@ -303,8 +377,8 @@ export const aiProxy = async (req: IncomingMessage, res: ServerResponse, opts: P
|
|||||||
if (!opts.oss) {
|
if (!opts.oss) {
|
||||||
opts.oss = oss;
|
opts.oss = oss;
|
||||||
}
|
}
|
||||||
if (req.method === 'POST') {
|
|
||||||
const searchParams = new URL(req.url || '', 'http://localhost').searchParams;
|
const searchParams = new URL(req.url || '', 'http://localhost').searchParams;
|
||||||
|
if (req.method === 'POST') {
|
||||||
const chunk = searchParams.get('chunk');
|
const chunk = searchParams.get('chunk');
|
||||||
const chunked = searchParams.get('chunked');
|
const chunked = searchParams.get('chunked');
|
||||||
if (chunk !== null || chunked !== null) {
|
if (chunk !== null || chunked !== null) {
|
||||||
@@ -316,6 +390,10 @@ export const aiProxy = async (req: IncomingMessage, res: ServerResponse, opts: P
|
|||||||
return deleteProxy(req, res, opts);
|
return deleteProxy(req, res, opts);
|
||||||
}
|
}
|
||||||
if (req.method === 'PUT') {
|
if (req.method === 'PUT') {
|
||||||
|
const meta = searchParams.get('meta');
|
||||||
|
if (meta) {
|
||||||
|
return updateMetadataProxy(req, res, opts);
|
||||||
|
}
|
||||||
return renameProxy(req, res, opts);
|
return renameProxy(req, res, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ export const createStudioAppListHtml = (opts: StudioOpts) => {
|
|||||||
const user = opts.user!;
|
const user = opts.user!;
|
||||||
const userAppKey = opts?.userAppKey;
|
const userAppKey = opts?.userAppKey;
|
||||||
let showUserAppKey = userAppKey;
|
let showUserAppKey = userAppKey;
|
||||||
if (showUserAppKey && showUserAppKey.startsWith(user + '-')) {
|
if (showUserAppKey && showUserAppKey.startsWith(user + '--')) {
|
||||||
showUserAppKey = showUserAppKey.replace(user + '-', '');
|
showUserAppKey = showUserAppKey.replace(user + '--', '');
|
||||||
}
|
}
|
||||||
const pathApps = opts?.appIds?.map(appId => {
|
const pathApps = opts?.appIds?.map(appId => {
|
||||||
const shortAppId = appId.replace(opts!.user + '-', '')
|
const shortAppId = appId.replace(opts!.user + '--', '')
|
||||||
return {
|
return {
|
||||||
appId,
|
appId,
|
||||||
shortAppId,
|
shortAppId,
|
||||||
|
|||||||
@@ -20,8 +20,14 @@ export const wssFun: WebSocketListenerFun = async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const user = loginUser?.tokenUser?.username;
|
const user = loginUser?.tokenUser?.username;
|
||||||
const userApp = user + '-' + id;
|
const userApp = user + '--' + id;
|
||||||
logger.debug('注册 ws 连接', userApp);
|
logger.debug('注册 ws 连接', userApp);
|
||||||
|
const wsMessage = wsProxyManager.get(userApp);
|
||||||
|
if (wsMessage) {
|
||||||
|
logger.debug('ws 连接已存在,关闭旧连接', userApp);
|
||||||
|
wsMessage.ws.close();
|
||||||
|
wsProxyManager.unregister(userApp);
|
||||||
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
wsProxyManager.register(userApp, { user, ws });
|
wsProxyManager.register(userApp, { user, ws });
|
||||||
ws.send(
|
ws.send(
|
||||||
|
|||||||
@@ -2,21 +2,51 @@ import { nanoid } from 'nanoid';
|
|||||||
import { WebSocket } from 'ws';
|
import { WebSocket } from 'ws';
|
||||||
import { logger } from '../logger.ts';
|
import { logger } from '../logger.ts';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
import { set } from 'zod';
|
||||||
|
|
||||||
class WsMessage {
|
class WsMessage {
|
||||||
ws: WebSocket;
|
ws: WebSocket;
|
||||||
user?: string;
|
user?: string;
|
||||||
emitter: EventEmitter;;
|
emitter: EventEmitter;
|
||||||
|
private pingTimer?: NodeJS.Timeout;
|
||||||
|
private readonly PING_INTERVAL = 30000; // 30 秒发送一次 ping
|
||||||
|
|
||||||
constructor({ ws, user }: WssMessageOptions) {
|
constructor({ ws, user }: WssMessageOptions) {
|
||||||
this.ws = ws;
|
this.ws = ws;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.emitter = new EventEmitter();
|
this.emitter = new EventEmitter();
|
||||||
|
this.startPing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startPing() {
|
||||||
|
this.stopPing();
|
||||||
|
this.pingTimer = setInterval(() => {
|
||||||
|
if (this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.ping();
|
||||||
|
} else {
|
||||||
|
this.stopPing();
|
||||||
|
}
|
||||||
|
}, this.PING_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopPing() {
|
||||||
|
if (this.pingTimer) {
|
||||||
|
clearInterval(this.pingTimer);
|
||||||
|
this.pingTimer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.stopPing();
|
||||||
|
this.emitter.removeAllListeners();
|
||||||
|
}
|
||||||
|
|
||||||
async sendResponse(data: any) {
|
async sendResponse(data: any) {
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
this.emitter.emit(data.id, data?.data);
|
this.emitter.emit(data.id, data?.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async sendData(data: any, opts?: { timeout?: number }) {
|
async sendData(data: any, context?: any, opts?: { timeout?: number }) {
|
||||||
if (this.ws.readyState !== WebSocket.OPEN) {
|
if (this.ws.readyState !== WebSocket.OPEN) {
|
||||||
return { code: 500, message: 'WebSocket is not open' };
|
return { code: 500, message: 'WebSocket is not open' };
|
||||||
}
|
}
|
||||||
@@ -25,7 +55,10 @@ class WsMessage {
|
|||||||
const message = JSON.stringify({
|
const message = JSON.stringify({
|
||||||
id,
|
id,
|
||||||
type: 'proxy',
|
type: 'proxy',
|
||||||
data,
|
data: {
|
||||||
|
message: data,
|
||||||
|
context: context || {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
logger.info('ws-proxy sendData', message);
|
logger.info('ws-proxy sendData', message);
|
||||||
this.ws.send(message);
|
this.ws.send(message);
|
||||||
@@ -50,15 +83,22 @@ type WssMessageOptions = {
|
|||||||
};
|
};
|
||||||
export class WsProxyManager {
|
export class WsProxyManager {
|
||||||
wssMap: Map<string, WsMessage> = new Map();
|
wssMap: Map<string, WsMessage> = new Map();
|
||||||
constructor() { }
|
PING_INTERVAL = 30000; // 30 秒检查一次连接状态
|
||||||
|
constructor(opts?: { pingInterval?: number }) {
|
||||||
|
if (opts?.pingInterval) {
|
||||||
|
this.PING_INTERVAL = opts.pingInterval;
|
||||||
|
}
|
||||||
|
this.checkConnceted();
|
||||||
|
}
|
||||||
register(id: string, opts?: { ws: WebSocket; user: string }) {
|
register(id: string, opts?: { ws: WebSocket; user: string }) {
|
||||||
if (this.wssMap.has(id)) {
|
if (this.wssMap.has(id)) {
|
||||||
const value = this.wssMap.get(id);
|
const value = this.wssMap.get(id);
|
||||||
if (value) {
|
if (value) {
|
||||||
value.ws.close();
|
value.ws.close();
|
||||||
|
value.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [username, appId] = id.split('-');
|
const [username, appId] = id.split('--');
|
||||||
const url = new URL(`/${username}/v1/${appId}`, 'https://kevisual.cn/');
|
const url = new URL(`/${username}/v1/${appId}`, 'https://kevisual.cn/');
|
||||||
console.log('WsProxyManager register', id, '访问地址', url.toString());
|
console.log('WsProxyManager register', id, '访问地址', url.toString());
|
||||||
const value = new WsMessage({ ws: opts?.ws, user: opts?.user });
|
const value = new WsMessage({ ws: opts?.ws, user: opts?.user });
|
||||||
@@ -68,6 +108,7 @@ export class WsProxyManager {
|
|||||||
const value = this.wssMap.get(id);
|
const value = this.wssMap.get(id);
|
||||||
if (value) {
|
if (value) {
|
||||||
value.ws.close();
|
value.ws.close();
|
||||||
|
value.destroy();
|
||||||
}
|
}
|
||||||
this.wssMap.delete(id);
|
this.wssMap.delete(id);
|
||||||
}
|
}
|
||||||
@@ -80,4 +121,16 @@ export class WsProxyManager {
|
|||||||
get(id: string) {
|
get(id: string) {
|
||||||
return this.wssMap.get(id);
|
return this.wssMap.get(id);
|
||||||
}
|
}
|
||||||
|
checkConnceted() {
|
||||||
|
const that = this;
|
||||||
|
setTimeout(() => {
|
||||||
|
that.wssMap.forEach((value, key) => {
|
||||||
|
if (value.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
logger.debug('ws not connected, unregister', key);
|
||||||
|
that.unregister(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
that.checkConnceted();
|
||||||
|
}, this.PING_INTERVAL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { App } from '@kevisual/router';
|
|||||||
import { logger } from '../logger.ts';
|
import { logger } from '../logger.ts';
|
||||||
import { getLoginUser } from '@/modules/auth.ts';
|
import { getLoginUser } from '@/modules/auth.ts';
|
||||||
import { createStudioAppListHtml } from '../html/studio-app-list/index.ts';
|
import { createStudioAppListHtml } from '../html/studio-app-list/index.ts';
|
||||||
|
import { omit } from 'es-toolkit';
|
||||||
|
|
||||||
type ProxyOptions = {
|
type ProxyOptions = {
|
||||||
createNotFoundPage: (msg?: string) => any;
|
createNotFoundPage: (msg?: string) => any;
|
||||||
@@ -31,7 +32,7 @@ export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opt
|
|||||||
if (!userAppKey) {
|
if (!userAppKey) {
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
// 获取所有的管理员的应用列表
|
// 获取所有的管理员的应用列表
|
||||||
const ids = wsProxyManager.getIds(user + '-');
|
const ids = wsProxyManager.getIds(user + '--');
|
||||||
const html = createStudioAppListHtml({ user, appIds: ids, userAppKey });
|
const html = createStudioAppListHtml({ user, appIds: ids, userAppKey });
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
res.end(html);
|
res.end(html);
|
||||||
@@ -41,8 +42,8 @@ export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opt
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!userAppKey.includes('-')) {
|
if (!userAppKey.includes('--')) {
|
||||||
userAppKey = user + '-' + userAppKey;
|
userAppKey = user + '--' + userAppKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 如果不是管理员,是否需要添加其他人可以访问的逻辑?
|
// TODO: 如果不是管理员,是否需要添加其他人可以访问的逻辑?
|
||||||
@@ -50,12 +51,12 @@ export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opt
|
|||||||
opts?.createNotFoundPage?.('没有访问应用权限');
|
opts?.createNotFoundPage?.('没有访问应用权限');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!userAppKey.startsWith(user + '-')) {
|
if (!userAppKey.startsWith(user + '--')) {
|
||||||
userAppKey = user + '-' + userAppKey;
|
userAppKey = user + '--' + userAppKey;
|
||||||
}
|
}
|
||||||
logger.debug('data', data);
|
logger.debug('data', data);
|
||||||
const client = wsProxyManager.get(userAppKey);
|
const client = wsProxyManager.get(userAppKey);
|
||||||
const ids = wsProxyManager.getIds(user + '-');
|
const ids = wsProxyManager.getIds(user + '--');
|
||||||
if (!client) {
|
if (!client) {
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
const html = createStudioAppListHtml({ user, appIds: ids, userAppKey });
|
const html = createStudioAppListHtml({ user, appIds: ids, userAppKey });
|
||||||
@@ -74,7 +75,13 @@ export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opt
|
|||||||
res.end(await html);
|
res.end(await html);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const value = await client.sendData(data);
|
let message: any = data;
|
||||||
|
if (!isAdmin) {
|
||||||
|
message = omit(data, ['token', 'cookies']);
|
||||||
|
}
|
||||||
|
const value = await client.sendData(message, {
|
||||||
|
state: { tokenUser: omit(loginUser.tokenUser, ['oauthExpand']) },
|
||||||
|
});
|
||||||
if (value) {
|
if (value) {
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify(value));
|
res.end(JSON.stringify(value));
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
import Busboy from 'busboy';
|
|
||||||
import { checkAuth } from '../middleware/auth.ts';
|
|
||||||
import { router, clients, writeEvents } from '../router.ts';
|
|
||||||
import { error } from '../middleware/auth.ts';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { useFileStore } from '@kevisual/use-config';
|
|
||||||
import { app, oss } from '@/app.ts';
|
|
||||||
import { getContentType } from '@/utils/get-content-type.ts';
|
|
||||||
import path from 'path';
|
|
||||||
import { createWriteStream } from 'fs';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
import { pipeBusboy } from '@/modules/fm-manager/index.ts';
|
|
||||||
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
|
||||||
|
|
||||||
router.post('/api/micro-app/upload', async (req, res) => {
|
|
||||||
if (res.headersSent) return; // 如果响应已发送,不再处理
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
const { tokenUser, token } = await checkAuth(req, res);
|
|
||||||
if (!tokenUser) return;
|
|
||||||
|
|
||||||
// 使用 busboy 解析 multipart/form-data
|
|
||||||
const busboy = Busboy({ headers: req.headers, preservePath: true, defCharset: 'utf-8' });
|
|
||||||
const fields: any = {};
|
|
||||||
let file: any = null;
|
|
||||||
let filePromise: Promise<void> | null = null;
|
|
||||||
let bytesReceived = 0;
|
|
||||||
let bytesExpected = parseInt(req.headers['content-length'] || '0');
|
|
||||||
|
|
||||||
busboy.on('field', (fieldname, value) => {
|
|
||||||
fields[fieldname] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
busboy.on('file', (fieldname, fileStream, info) => {
|
|
||||||
const { filename, encoding, mimeType } = info;
|
|
||||||
// 处理 UTF-8 文件名编码
|
|
||||||
const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename;
|
|
||||||
const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`);
|
|
||||||
const writeStream = createWriteStream(tempPath);
|
|
||||||
const hash = crypto.createHash('md5');
|
|
||||||
let size = 0;
|
|
||||||
|
|
||||||
filePromise = new Promise<void>((resolve, reject) => {
|
|
||||||
fileStream.on('data', (chunk) => {
|
|
||||||
bytesReceived += chunk.length;
|
|
||||||
size += chunk.length;
|
|
||||||
hash.update(chunk);
|
|
||||||
if (bytesExpected > 0) {
|
|
||||||
const progress = (bytesReceived / bytesExpected) * 100;
|
|
||||||
console.log(`Upload progress: ${progress.toFixed(2)}%`);
|
|
||||||
const data = {
|
|
||||||
progress: progress.toFixed(2),
|
|
||||||
message: `Upload progress: ${progress.toFixed(2)}%`,
|
|
||||||
};
|
|
||||||
writeEvents(req, data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileStream.pipe(writeStream);
|
|
||||||
|
|
||||||
writeStream.on('finish', () => {
|
|
||||||
file = {
|
|
||||||
filepath: tempPath,
|
|
||||||
originalFilename: decodedFilename,
|
|
||||||
mimetype: mimeType,
|
|
||||||
hash: hash.digest('hex'),
|
|
||||||
size: size,
|
|
||||||
};
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
writeStream.on('error', (err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
busboy.on('finish', async () => {
|
|
||||||
// 等待文件写入完成
|
|
||||||
if (filePromise) {
|
|
||||||
try {
|
|
||||||
await filePromise;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`File write error: ${err.message}`);
|
|
||||||
res.end(error(`File write error: ${err.message}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const clearFiles = () => {
|
|
||||||
if (file?.filepath && fs.existsSync(file.filepath)) {
|
|
||||||
fs.unlinkSync(file.filepath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
res.end(error('No file uploaded'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let appKey, collection;
|
|
||||||
const { appKey: _appKey, collection: _collecion } = fields;
|
|
||||||
if (Array.isArray(_appKey)) {
|
|
||||||
appKey = _appKey?.[0];
|
|
||||||
} else {
|
|
||||||
appKey = _appKey;
|
|
||||||
}
|
|
||||||
if (Array.isArray(_collecion)) {
|
|
||||||
collection = _collecion?.[0];
|
|
||||||
} else {
|
|
||||||
collection = _collecion;
|
|
||||||
}
|
|
||||||
collection = parseIfJson(collection);
|
|
||||||
|
|
||||||
appKey = appKey || 'micro-app';
|
|
||||||
console.log('Appkey', appKey);
|
|
||||||
console.log('collection', collection);
|
|
||||||
|
|
||||||
// 处理上传的文件
|
|
||||||
const uploadResults = [];
|
|
||||||
const tempPath = file.filepath; // 文件上传时的临时路径
|
|
||||||
const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构)
|
|
||||||
// 比如 child2/b.txt
|
|
||||||
const minioPath = `private/${tokenUser.username}/${appKey}/${relativePath}`;
|
|
||||||
// 上传到 MinIO 并保留文件夹结构
|
|
||||||
const isHTML = relativePath.endsWith('.html');
|
|
||||||
await oss.fPutObject(minioPath, tempPath, {
|
|
||||||
'Content-Type': getContentType(relativePath),
|
|
||||||
'app-source': 'user-micro-app',
|
|
||||||
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年
|
|
||||||
});
|
|
||||||
uploadResults.push({
|
|
||||||
name: relativePath,
|
|
||||||
path: minioPath,
|
|
||||||
hash: file.hash,
|
|
||||||
size: file.size,
|
|
||||||
});
|
|
||||||
fs.unlinkSync(tempPath); // 删除临时文件
|
|
||||||
|
|
||||||
// 受控
|
|
||||||
const r = await app.call({
|
|
||||||
path: 'micro-app',
|
|
||||||
key: 'upload',
|
|
||||||
payload: {
|
|
||||||
token: token,
|
|
||||||
data: {
|
|
||||||
appKey,
|
|
||||||
collection,
|
|
||||||
files: uploadResults,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data: any = {
|
|
||||||
code: r.code,
|
|
||||||
data: r.body,
|
|
||||||
};
|
|
||||||
if (r.message) {
|
|
||||||
data.message = r.message;
|
|
||||||
}
|
|
||||||
res.end(JSON.stringify(data));
|
|
||||||
});
|
|
||||||
|
|
||||||
pipeBusboy(req, res, busboy);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function parseIfJson(collection: any): any {
|
|
||||||
try {
|
|
||||||
return JSON.parse(collection);
|
|
||||||
} catch (e) {
|
|
||||||
return collection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { router, error, checkAuth, clients, getTaskId, writeEvents, deleteOldClients } from './router.ts';
|
|
||||||
|
|
||||||
router.get('/api/events', async (req, res) => {
|
|
||||||
res.writeHead(200, {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
Connection: 'keep-alive',
|
|
||||||
});
|
|
||||||
const taskId = getTaskId(req);
|
|
||||||
if (!taskId) {
|
|
||||||
res.end(error('task-id is required'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 将客户端连接推送到 clients 数组
|
|
||||||
clients.set(taskId, { client: res, createTime: Date.now() });
|
|
||||||
// 移除客户端连接
|
|
||||||
req.on('close', () => {
|
|
||||||
clients.delete(taskId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/api/s1/events', async (req, res) => {
|
|
||||||
res.writeHead(200, {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
Connection: 'keep-alive',
|
|
||||||
});
|
|
||||||
const taskId = getTaskId(req);
|
|
||||||
if (!taskId) {
|
|
||||||
res.end(error('task-id is required'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 将客户端连接推送到 clients 数组
|
|
||||||
clients.set(taskId, { client: res, createTime: Date.now() });
|
|
||||||
writeEvents(req, { progress: 0, message: 'start' });
|
|
||||||
// 不自动关闭连接
|
|
||||||
// res.end('ok');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/api/s1/events/close', async (req, res) => {
|
|
||||||
const taskId = getTaskId(req);
|
|
||||||
if (!taskId) {
|
|
||||||
res.end(error('task-id is required'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
deleteOldClients();
|
|
||||||
clients.delete(taskId);
|
|
||||||
res.end('ok');
|
|
||||||
});
|
|
||||||
@@ -1,20 +1,9 @@
|
|||||||
import { useFileStore } from '@kevisual/use-config';
|
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import fs from 'fs';
|
import { router } from './router.ts';
|
||||||
import Busboy from 'busboy';
|
|
||||||
import { app, oss } from '@/app.ts';
|
|
||||||
|
|
||||||
import { getContentType } from '@/utils/get-content-type.ts';
|
|
||||||
import { User } from '@/models/user.ts';
|
|
||||||
import { router, error, checkAuth, writeEvents } from './router.ts';
|
|
||||||
import './index.ts';
|
import './index.ts';
|
||||||
import { handleRequest as PageProxy } from './page-proxy.ts';
|
import { handleRequest as PageProxy } from './page-proxy.ts';
|
||||||
|
|
||||||
const simpleAppsPrefixs = [
|
const simpleAppsPrefixs = [
|
||||||
"/api/micro-app/",
|
|
||||||
"/api/events",
|
|
||||||
"/api/s1/",
|
|
||||||
"/api/resource/",
|
|
||||||
"/api/wxmsg"
|
"/api/wxmsg"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
// import './code/upload.ts';
|
|
||||||
import './event.ts';
|
|
||||||
|
|
||||||
import './resources/upload.ts';
|
|
||||||
import './resources/chunk.ts';
|
|
||||||
// import './resources/get-resources.ts';
|
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import { User } from '@/models/user.ts';
|
|
||||||
import http from 'http';
|
|
||||||
import { parse } from '@kevisual/router/src/server/cookie.ts';
|
|
||||||
export const error = (msg: string, code = 500) => {
|
|
||||||
return JSON.stringify({ code, message: msg });
|
|
||||||
};
|
|
||||||
export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
|
||||||
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
|
|
||||||
const url = new URL(req.url || '', 'http://localhost');
|
|
||||||
const resNoPermission = () => {
|
|
||||||
res.statusCode = 401;
|
|
||||||
res.end(error('Invalid authorization'));
|
|
||||||
return { tokenUser: null, token: null };
|
|
||||||
};
|
|
||||||
if (!token) {
|
|
||||||
token = url.searchParams.get('token') || '';
|
|
||||||
}
|
|
||||||
if (!token) {
|
|
||||||
const parsedCookies = parse(req.headers.cookie || '');
|
|
||||||
token = parsedCookies.token || '';
|
|
||||||
}
|
|
||||||
if (!token) {
|
|
||||||
return resNoPermission();
|
|
||||||
}
|
|
||||||
if (token) {
|
|
||||||
token = token.replace('Bearer ', '');
|
|
||||||
}
|
|
||||||
let tokenUser;
|
|
||||||
try {
|
|
||||||
tokenUser = await User.verifyToken(token);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('checkAuth error', e);
|
|
||||||
res.statusCode = 401;
|
|
||||||
res.end(error('Invalid token'));
|
|
||||||
return { tokenUser: null, token: null };
|
|
||||||
}
|
|
||||||
return { tokenUser, token };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLoginUser = async (req: http.IncomingMessage) => {
|
|
||||||
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
|
|
||||||
const url = new URL(req.url || '', 'http://localhost');
|
|
||||||
if (!token) {
|
|
||||||
token = url.searchParams.get('token') || '';
|
|
||||||
}
|
|
||||||
if (!token) {
|
|
||||||
const parsedCookies = parse(req.headers.cookie || '');
|
|
||||||
token = parsedCookies.token || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
token = token.replace('Bearer ', '');
|
|
||||||
}
|
|
||||||
let tokenUser;
|
|
||||||
try {
|
|
||||||
tokenUser = await User.verifyToken(token);
|
|
||||||
return { tokenUser, token };
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './auth.ts'
|
|
||||||
@@ -298,6 +298,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
username: loginUser?.tokenUser?.username || '',
|
username: loginUser?.tokenUser?.username || '',
|
||||||
password: password,
|
password: password,
|
||||||
});
|
});
|
||||||
|
console.log('checkPermission', checkPermission, 'loginUser:', loginUser, password)
|
||||||
if (!checkPermission.success) {
|
if (!checkPermission.success) {
|
||||||
return createNotFoundPage('no permission');
|
return createNotFoundPage('no permission');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
import { useFileStore } from '@kevisual/use-config';
|
|
||||||
import { checkAuth, error, router, writeEvents, getKey, getTaskId } from '../router.ts';
|
|
||||||
import Busboy from 'busboy';
|
|
||||||
import { app, oss } from '@/app.ts';
|
|
||||||
|
|
||||||
import { getContentType } from '@/utils/get-content-type.ts';
|
|
||||||
import { User } from '@/models/user.ts';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { ConfigModel } from '@/routes/config/models/model.ts';
|
|
||||||
import { validateDirectory } from './util.ts';
|
|
||||||
import path from 'path';
|
|
||||||
import { createWriteStream } from 'fs';
|
|
||||||
import { pipeBusboy } from '@/modules/fm-manager/index.ts';
|
|
||||||
|
|
||||||
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
|
||||||
|
|
||||||
router.get('/api/s1/resources/upload/chunk', async (req, res) => {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end('Upload API is ready');
|
|
||||||
});
|
|
||||||
|
|
||||||
// /api/s1/resources/upload
|
|
||||||
router.post('/api/s1/resources/upload/chunk', async (req, res) => {
|
|
||||||
const { tokenUser, token } = await checkAuth(req, res);
|
|
||||||
if (!tokenUser) return;
|
|
||||||
const url = new URL(req.url || '', 'http://localhost');
|
|
||||||
const share = !!url.searchParams.get('public');
|
|
||||||
const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles');
|
|
||||||
|
|
||||||
const taskId = getTaskId(req);
|
|
||||||
const finalFilePath = `${cacheFilePath}/${taskId}`;
|
|
||||||
if (!taskId) {
|
|
||||||
res.end(error('taskId is required'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 busboy 解析 multipart/form-data
|
|
||||||
const busboy = Busboy({ headers: req.headers, preservePath: true, defCharset: 'utf-8' });
|
|
||||||
const fields: any = {};
|
|
||||||
let file: any = null;
|
|
||||||
let tempPath = '';
|
|
||||||
let filePromise: Promise<void> | null = null;
|
|
||||||
|
|
||||||
busboy.on('field', (fieldname, value) => {
|
|
||||||
fields[fieldname] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
busboy.on('file', (fieldname, fileStream, info) => {
|
|
||||||
const { filename, encoding, mimeType } = info;
|
|
||||||
// 处理 UTF-8 文件名编码
|
|
||||||
const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename;
|
|
||||||
tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`);
|
|
||||||
const writeStream = createWriteStream(tempPath);
|
|
||||||
|
|
||||||
filePromise = new Promise<void>((resolve, reject) => {
|
|
||||||
fileStream.pipe(writeStream);
|
|
||||||
|
|
||||||
writeStream.on('finish', () => {
|
|
||||||
file = {
|
|
||||||
filepath: tempPath,
|
|
||||||
originalFilename: decodedFilename,
|
|
||||||
mimetype: mimeType,
|
|
||||||
};
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
writeStream.on('error', (err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
busboy.on('finish', async () => {
|
|
||||||
// 等待文件写入完成
|
|
||||||
if (filePromise) {
|
|
||||||
try {
|
|
||||||
await filePromise;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`File write error: ${err.message}`);
|
|
||||||
res.end(error(`File write error: ${err.message}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const clearFiles = () => {
|
|
||||||
if (tempPath && fs.existsSync(tempPath)) {
|
|
||||||
fs.unlinkSync(tempPath);
|
|
||||||
}
|
|
||||||
if (fs.existsSync(finalFilePath)) {
|
|
||||||
fs.unlinkSync(finalFilePath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
res.end(error('No file uploaded'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle chunked upload logic here
|
|
||||||
let { chunkIndex, totalChunks, appKey, version, username, directory } = getKey(fields, [
|
|
||||||
'chunkIndex',
|
|
||||||
'totalChunks',
|
|
||||||
'appKey',
|
|
||||||
'version',
|
|
||||||
'username',
|
|
||||||
'directory',
|
|
||||||
]);
|
|
||||||
if (!chunkIndex || !totalChunks) {
|
|
||||||
res.end(error('chunkIndex, totalChunks is required'));
|
|
||||||
clearFiles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const relativePath = file.originalFilename;
|
|
||||||
|
|
||||||
const writeStream = fs.createWriteStream(finalFilePath, { flags: 'a' });
|
|
||||||
const readStream = fs.createReadStream(tempPath);
|
|
||||||
readStream.pipe(writeStream);
|
|
||||||
|
|
||||||
writeStream.on('finish', async () => {
|
|
||||||
fs.unlinkSync(tempPath); // 删除临时文件
|
|
||||||
|
|
||||||
// Write event for progress tracking
|
|
||||||
const progress = ((parseInt(chunkIndex) + 1) / parseInt(totalChunks)) * 100;
|
|
||||||
writeEvents(req, {
|
|
||||||
progress,
|
|
||||||
message: `Upload progress: ${progress}%`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parseInt(chunkIndex) + 1 === parseInt(totalChunks)) {
|
|
||||||
let uid = tokenUser.id;
|
|
||||||
if (username) {
|
|
||||||
const user = await User.getUserByToken(token);
|
|
||||||
const has = await user.hasUser(username, true);
|
|
||||||
if (!has) {
|
|
||||||
res.end(error('username is not found'));
|
|
||||||
clearFiles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const _user = await User.findOne({ where: { username } });
|
|
||||||
uid = _user?.id || '';
|
|
||||||
}
|
|
||||||
if (!appKey || !version) {
|
|
||||||
const config = await ConfigModel.getUploadConfig({ uid });
|
|
||||||
if (config) {
|
|
||||||
appKey = config.config?.data?.key || '';
|
|
||||||
version = config.config?.data?.version || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!appKey || !version) {
|
|
||||||
res.end(error('appKey or version is not found, please check the upload config.'));
|
|
||||||
clearFiles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { code, message } = validateDirectory(directory);
|
|
||||||
if (code !== 200) {
|
|
||||||
res.end(error(message));
|
|
||||||
clearFiles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`;
|
|
||||||
const metadata: any = {};
|
|
||||||
if (share) {
|
|
||||||
metadata.share = 'public';
|
|
||||||
}
|
|
||||||
// All chunks uploaded, now upload to MinIO
|
|
||||||
await oss.fPutObject(minioPath, finalFilePath, {
|
|
||||||
'Content-Type': getContentType(relativePath),
|
|
||||||
'app-source': 'user-app',
|
|
||||||
'Cache-Control': relativePath.endsWith('.html') ? 'no-cache' : 'max-age=31536000, immutable',
|
|
||||||
...metadata,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up the final file
|
|
||||||
fs.unlinkSync(finalFilePath);
|
|
||||||
const downloadBase = '/api/s1/share';
|
|
||||||
|
|
||||||
const uploadResult = {
|
|
||||||
name: relativePath,
|
|
||||||
path: `${downloadBase}/${minioPath}`,
|
|
||||||
appKey,
|
|
||||||
version,
|
|
||||||
username,
|
|
||||||
};
|
|
||||||
if (!noCheckAppFiles) {
|
|
||||||
// Notify the app
|
|
||||||
const r = await app.call({
|
|
||||||
path: 'app',
|
|
||||||
key: 'detectVersionList',
|
|
||||||
payload: {
|
|
||||||
token: token,
|
|
||||||
data: {
|
|
||||||
appKey,
|
|
||||||
version,
|
|
||||||
username,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data: any = {
|
|
||||||
code: r.code,
|
|
||||||
data: {
|
|
||||||
app: r.body,
|
|
||||||
upload: [uploadResult],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (r.message) {
|
|
||||||
data.message = r.message;
|
|
||||||
}
|
|
||||||
console.log('upload data', data);
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify(data));
|
|
||||||
} else {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
code: 200,
|
|
||||||
message: 'Chunk uploaded successfully',
|
|
||||||
data: { chunkIndex, totalChunks, upload: [uploadResult] },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
code: 200,
|
|
||||||
message: 'Chunk uploaded successfully',
|
|
||||||
data: {
|
|
||||||
chunkIndex,
|
|
||||||
totalChunks,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
pipeBusboy(req, res, busboy);
|
|
||||||
});
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
import { useFileStore } from '@kevisual/use-config';
|
|
||||||
import { checkAuth, error, router, writeEvents, getKey } from '../router.ts';
|
|
||||||
import Busboy from 'busboy';
|
|
||||||
import { app } from '@/app.ts';
|
|
||||||
|
|
||||||
import { getContentType } from '@/utils/get-content-type.ts';
|
|
||||||
import { User } from '@/models/user.ts';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { createWriteStream } from 'fs';
|
|
||||||
import { pipeBusboy } from '@/modules/fm-manager/pipe-busboy.ts';
|
|
||||||
import { ConfigModel } from '@/routes/config/models/model.ts';
|
|
||||||
import { validateDirectory } from './util.ts';
|
|
||||||
import { pick } from 'es-toolkit';
|
|
||||||
import { getFileStat } from '@/routes/file/index.ts';
|
|
||||||
import { logger } from '@/modules/logger.ts';
|
|
||||||
import { oss } from '@/modules/s3.ts';
|
|
||||||
|
|
||||||
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
|
||||||
|
|
||||||
router.get('/api/s1/resources/upload', async (req, res) => {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end('Upload API is ready');
|
|
||||||
});
|
|
||||||
export const parseIfJson = (data = '{}') => {
|
|
||||||
try {
|
|
||||||
const _data = JSON.parse(data);
|
|
||||||
if (typeof _data === 'object') return _data;
|
|
||||||
return {};
|
|
||||||
} catch (error) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
router.post('/api/s1/resources/upload/check', async (req, res) => {
|
|
||||||
const { tokenUser, token } = await checkAuth(req, res);
|
|
||||||
if (!tokenUser) {
|
|
||||||
res.end(error('Token is invalid.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('data', req.url);
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
const data = await router.getBody(req);
|
|
||||||
type Data = {
|
|
||||||
appKey: string;
|
|
||||||
version: string;
|
|
||||||
username: string;
|
|
||||||
directory: string;
|
|
||||||
files: { path: string; hash: string }[];
|
|
||||||
};
|
|
||||||
let { appKey, version, username, directory, files } = pick(data, ['appKey', 'version', 'username', 'directory', 'files']) as Data;
|
|
||||||
let uid = tokenUser.id;
|
|
||||||
if (username) {
|
|
||||||
const user = await User.getUserByToken(token);
|
|
||||||
const has = await user.hasUser(username, true);
|
|
||||||
if (!has) {
|
|
||||||
res.end(error('username is not found'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const _user = await User.findOne({ where: { username } });
|
|
||||||
uid = _user?.id || '';
|
|
||||||
}
|
|
||||||
if (!appKey || !version) {
|
|
||||||
res.end(error('appKey and version is required'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const { code, message } = validateDirectory(directory);
|
|
||||||
if (code !== 200) {
|
|
||||||
res.end(error(message));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
type CheckResult = {
|
|
||||||
path: string;
|
|
||||||
stat: any;
|
|
||||||
resourcePath: string;
|
|
||||||
hash: string;
|
|
||||||
uploadHash: string;
|
|
||||||
isUpload?: boolean;
|
|
||||||
};
|
|
||||||
const checkResult: CheckResult[] = [];
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
const file = files[i];
|
|
||||||
const relativePath = file.path;
|
|
||||||
const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`;
|
|
||||||
let stat = await getFileStat(minioPath, true);
|
|
||||||
const statHash = stat?.etag || '';
|
|
||||||
checkResult.push({
|
|
||||||
path: relativePath,
|
|
||||||
uploadHash: file.hash,
|
|
||||||
resourcePath: minioPath,
|
|
||||||
isUpload: statHash === file.hash,
|
|
||||||
stat,
|
|
||||||
hash: statHash,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.end(JSON.stringify({ code: 200, data: checkResult }));
|
|
||||||
});
|
|
||||||
|
|
||||||
// /api/s1/resources/upload
|
|
||||||
router.post('/api/s1/resources/upload', async (req, res) => {
|
|
||||||
const { tokenUser, token } = await checkAuth(req, res);
|
|
||||||
if (!tokenUser) {
|
|
||||||
res.end(error('Token is invalid.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = new URL(req.url || '', 'http://localhost');
|
|
||||||
const share = !!url.searchParams.get('public');
|
|
||||||
const meta = parseIfJson(url.searchParams.get('meta'));
|
|
||||||
const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles');
|
|
||||||
// 使用 busboy 解析 multipart/form-data
|
|
||||||
const busboy = Busboy({ headers: req.headers, preservePath: true, defCharset: 'utf-8' });
|
|
||||||
const fields: any = {};
|
|
||||||
const files: any[] = [];
|
|
||||||
const filePromises: Promise<void>[] = [];
|
|
||||||
let bytesReceived = 0;
|
|
||||||
let bytesExpected = parseInt(req.headers['content-length'] || '0');
|
|
||||||
busboy.on('field', (fieldname, value) => {
|
|
||||||
fields[fieldname] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
busboy.on('file', (fieldname, fileStream, info) => {
|
|
||||||
const { filename, encoding, mimeType } = info;
|
|
||||||
// 处理 UTF-8 文件名编码(busboy 可能返回 Latin-1 编码的缓冲区)
|
|
||||||
const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename;
|
|
||||||
const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`);
|
|
||||||
const writeStream = createWriteStream(tempPath);
|
|
||||||
const filePromise = new Promise<void>((resolve, reject) => {
|
|
||||||
fileStream.on('data', (chunk) => {
|
|
||||||
bytesReceived += chunk.length;
|
|
||||||
if (bytesExpected > 0) {
|
|
||||||
const progress = (bytesReceived / bytesExpected) * 100;
|
|
||||||
const data = {
|
|
||||||
progress: progress.toFixed(2),
|
|
||||||
message: `Upload progress: ${progress.toFixed(2)}%`,
|
|
||||||
};
|
|
||||||
console.log('progress-upload', data);
|
|
||||||
writeEvents(req, data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileStream.pipe(writeStream);
|
|
||||||
|
|
||||||
writeStream.on('finish', () => {
|
|
||||||
files.push({
|
|
||||||
filepath: tempPath,
|
|
||||||
originalFilename: decodedFilename,
|
|
||||||
mimetype: mimeType,
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
writeStream.on('error', (err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
filePromises.push(filePromise);
|
|
||||||
});
|
|
||||||
|
|
||||||
busboy.on('finish', async () => {
|
|
||||||
// 等待所有文件写入完成
|
|
||||||
try {
|
|
||||||
await Promise.all(filePromises);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`File write error: ${err.message}`);
|
|
||||||
res.end(error(`File write error: ${err.message}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const clearFiles = () => {
|
|
||||||
files.forEach((file) => {
|
|
||||||
if (file?.filepath && fs.existsSync(file.filepath)) {
|
|
||||||
fs.unlinkSync(file.filepath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查是否有文件上传
|
|
||||||
if (files.length === 0) {
|
|
||||||
res.end(error('files is required'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { appKey, version, username, directory, description } = getKey(fields, ['appKey', 'version', 'username', 'directory', 'description']);
|
|
||||||
let uid = tokenUser.id;
|
|
||||||
if (username) {
|
|
||||||
const user = await User.getUserByToken(token);
|
|
||||||
const has = await user.hasUser(username, true);
|
|
||||||
if (!has) {
|
|
||||||
res.end(error('username is not found'));
|
|
||||||
clearFiles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const _user = await User.findOne({ where: { username } });
|
|
||||||
uid = _user?.id || '';
|
|
||||||
}
|
|
||||||
if (!appKey || !version) {
|
|
||||||
const config = await ConfigModel.getUploadConfig({ uid });
|
|
||||||
if (config) {
|
|
||||||
appKey = config.config?.data?.key || '';
|
|
||||||
version = config.config?.data?.version || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!appKey || !version) {
|
|
||||||
res.end(error('appKey or version is not found, please check the upload config.'));
|
|
||||||
clearFiles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { code, message } = validateDirectory(directory);
|
|
||||||
if (code !== 200) {
|
|
||||||
res.end(error(message));
|
|
||||||
clearFiles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 逐个处理每个上传的文件
|
|
||||||
const uploadedFiles = files;
|
|
||||||
logger.info(
|
|
||||||
'upload files',
|
|
||||||
uploadedFiles.map((item) => {
|
|
||||||
return pick(item, ['filepath', 'originalFilename']);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const uploadResults = [];
|
|
||||||
for (let i = 0; i < uploadedFiles.length; i++) {
|
|
||||||
const file = uploadedFiles[i];
|
|
||||||
// @ts-ignore
|
|
||||||
const tempPath = file.filepath; // 文件上传时的临时路径
|
|
||||||
const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构)
|
|
||||||
// 比如 child2/b.txt
|
|
||||||
const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`;
|
|
||||||
// 上传到 MinIO 并保留文件夹结构
|
|
||||||
const isHTML = relativePath.endsWith('.html');
|
|
||||||
const metadata: any = {};
|
|
||||||
if (share) {
|
|
||||||
metadata.share = 'public';
|
|
||||||
}
|
|
||||||
Object.assign(metadata, meta);
|
|
||||||
await oss.fPutObject(minioPath, tempPath, {
|
|
||||||
'Content-Type': getContentType(relativePath),
|
|
||||||
'app-source': 'user-app',
|
|
||||||
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年
|
|
||||||
...metadata,
|
|
||||||
});
|
|
||||||
uploadResults.push({
|
|
||||||
name: relativePath,
|
|
||||||
path: minioPath,
|
|
||||||
});
|
|
||||||
fs.unlinkSync(tempPath); // 删除临时文件
|
|
||||||
}
|
|
||||||
if (!noCheckAppFiles) {
|
|
||||||
const _data = { appKey, version, username, files: uploadResults, description, }
|
|
||||||
if (_data.description) {
|
|
||||||
delete _data.description;
|
|
||||||
}
|
|
||||||
// 受控
|
|
||||||
const r = await app.call({
|
|
||||||
path: 'app',
|
|
||||||
key: 'uploadFiles',
|
|
||||||
payload: {
|
|
||||||
token: token,
|
|
||||||
data: _data,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data: any = {
|
|
||||||
code: r.code,
|
|
||||||
data: {
|
|
||||||
app: r.body,
|
|
||||||
upload: uploadResults,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (r.message) {
|
|
||||||
data.message = r.message;
|
|
||||||
}
|
|
||||||
console.log('upload data', data);
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify(data));
|
|
||||||
} else {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
code: 200,
|
|
||||||
data: {
|
|
||||||
detect: [],
|
|
||||||
upload: uploadResults,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
pipeBusboy(req, res, busboy);
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* 校验directory是否合法, 合法返回200, 不合法返回500
|
|
||||||
*
|
|
||||||
* directory 不能以/开头,不能以/结尾。不能以.开头,不能以.结尾。
|
|
||||||
* 把directory的/替换掉后,只能包含数字、字母、下划线、中划线
|
|
||||||
* @param directory 目录
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const validateDirectory = (directory?: string) => {
|
|
||||||
// 对directory进行校验,不能以/开头,不能以/结尾。不能以.开头,不能以.结尾。
|
|
||||||
if (directory && (directory.startsWith('/') || directory.endsWith('/') || directory.startsWith('..') || directory.endsWith('..'))) {
|
|
||||||
return {
|
|
||||||
code: 500,
|
|
||||||
message: 'directory is invalid',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 把directory的/替换掉后,只能包含数字、字母、下划线、中划线
|
|
||||||
// 可以包含.
|
|
||||||
let _directory = directory?.replace(/\//g, '');
|
|
||||||
if (_directory && !/^[a-zA-Z0-9_.-]+$/.test(_directory)) {
|
|
||||||
return {
|
|
||||||
code: 500,
|
|
||||||
message: 'directory is invalid, only number, letter, underline and hyphen are allowed',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
message: 'directory is valid',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { router } from '@/app.ts';
|
import { router } from '@/app.ts';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { useContextKey } from '@kevisual/context';
|
import { useContextKey } from '@kevisual/context';
|
||||||
import { checkAuth, error } from './middleware/auth.ts';
|
export { router, };
|
||||||
export { router, checkAuth, error };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事件客户端
|
* 事件客户端
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getUidByUsername, prefixFix } from './util.ts';
|
|||||||
import { deleteFiles, getMinioListAndSetToAppList } from '../file/index.ts';
|
import { deleteFiles, getMinioListAndSetToAppList } from '../file/index.ts';
|
||||||
import { setExpire } from './revoke.ts';
|
import { setExpire } from './revoke.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
|
import { callDetectAppVersion } from './export.ts';
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'app',
|
path: 'app',
|
||||||
@@ -43,7 +44,7 @@ app
|
|||||||
console.log('get app manager called');
|
console.log('get app manager called');
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const id = ctx.query.id;
|
const id = ctx.query.id;
|
||||||
const { key, version } = ctx.query?.data || {};
|
const { key, version, create = false } = ctx.query?.data || {};
|
||||||
if (!id && (!key || !version)) {
|
if (!id && (!key || !version)) {
|
||||||
throw new CustomError('id is required');
|
throw new CustomError('id is required');
|
||||||
}
|
}
|
||||||
@@ -59,8 +60,27 @@ app
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (!am && create) {
|
||||||
|
am = await AppListModel.create({
|
||||||
|
key,
|
||||||
|
version,
|
||||||
|
uid: tokenUser.id,
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
const res = await app.run({ path: 'app', key: "detectVersionList", payload: { data: { appKey: key, version, username: tokenUser.username }, token: ctx.query.token } });
|
||||||
|
if (res.code !== 200) {
|
||||||
|
ctx.throw(res.message || 'detect version list error');
|
||||||
|
}
|
||||||
|
am = await AppListModel.findOne({
|
||||||
|
where: {
|
||||||
|
key,
|
||||||
|
version,
|
||||||
|
uid: tokenUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!am) {
|
if (!am) {
|
||||||
throw new CustomError('app not found');
|
ctx.throw('app not found');
|
||||||
}
|
}
|
||||||
console.log('get app', am.id, am.key, am.version);
|
console.log('get app', am.id, am.key, am.version);
|
||||||
ctx.body = prefixFix(am, tokenUser.username);
|
ctx.body = prefixFix(am, tokenUser.username);
|
||||||
@@ -239,7 +259,7 @@ app
|
|||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const { id, username, appKey, version } = ctx.query.data;
|
const { id, username, appKey, version, detect } = ctx.query.data;
|
||||||
if (!id && !appKey) {
|
if (!id && !appKey) {
|
||||||
throw new CustomError('id or appKey is required');
|
throw new CustomError('id or appKey is required');
|
||||||
}
|
}
|
||||||
@@ -249,22 +269,33 @@ app
|
|||||||
if (id) {
|
if (id) {
|
||||||
appList = await AppListModel.findByPk(id);
|
appList = await AppListModel.findByPk(id);
|
||||||
if (appList?.uid !== uid) {
|
if (appList?.uid !== uid) {
|
||||||
throw new CustomError('no permission');
|
ctx.throw('no permission');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!appList && appKey) {
|
if (!appList && appKey) {
|
||||||
if (!version) {
|
if (!version) {
|
||||||
throw new CustomError('version is required');
|
ctx.throw('version is required');
|
||||||
}
|
}
|
||||||
appList = await AppListModel.findOne({ where: { key: appKey, version, uid } });
|
appList = await AppListModel.findOne({ where: { key: appKey, version, uid } });
|
||||||
}
|
}
|
||||||
if (!appList) {
|
if (!appList) {
|
||||||
throw new CustomError('app not found');
|
ctx.throw('app 未发现');
|
||||||
}
|
}
|
||||||
|
if (detect) {
|
||||||
|
const appKey = appList.key;
|
||||||
|
const version = appList.version;
|
||||||
|
// 自动检测最新版本
|
||||||
|
const res = await callDetectAppVersion({ appKey, version, username: username || tokenUser.username }, ctx.query.token);
|
||||||
|
if (res.code !== 200) {
|
||||||
|
ctx.throw(res.message || '检测版本列表失败');
|
||||||
|
}
|
||||||
|
appList = await AppListModel.findByPk(appList.id);
|
||||||
|
}
|
||||||
|
|
||||||
const files = appList.data.files || [];
|
const files = appList.data.files || [];
|
||||||
const am = await AppModel.findOne({ where: { key: appList.key, uid: uid } });
|
const am = await AppModel.findOne({ where: { key: appList.key, uid: uid } });
|
||||||
if (!am) {
|
if (!am) {
|
||||||
throw new CustomError('app not found');
|
ctx.throw('app 未发现');
|
||||||
}
|
}
|
||||||
await am.update({ data: { ...am.data, files }, version: appList.version });
|
await am.update({ data: { ...am.data, files }, version: appList.version });
|
||||||
setExpire(appList.key, am.user);
|
setExpire(appList.key, am.user);
|
||||||
@@ -366,7 +397,7 @@ app
|
|||||||
am = await AppModel.create({
|
am = await AppModel.create({
|
||||||
title: appKey,
|
title: appKey,
|
||||||
key: appKey,
|
key: appKey,
|
||||||
version: version || '0.0.0',
|
version: version || '0.0.1',
|
||||||
user: checkUsername,
|
user: checkUsername,
|
||||||
uid,
|
uid,
|
||||||
data: { files: needAddFiles },
|
data: { files: needAddFiles },
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { app } from '@/app.ts';
|
import { eq, and, inArray } from 'drizzle-orm';
|
||||||
import { ConfigModel } from './models/model.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { oss } from '@/app.ts';
|
import { oss } from '@/app.ts';
|
||||||
import { ConfigOssService } from '@kevisual/oss/services';
|
import { ConfigOssService } from '@kevisual/oss/services';
|
||||||
import { Op } from 'sequelize';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
@@ -20,14 +20,12 @@ app
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { list, keys, keyEtagMap } = await configOss.getList();
|
const { list, keys, keyEtagMap } = await configOss.getList();
|
||||||
const configList = await ConfigModel.findAll({
|
const configList = await db.select()
|
||||||
where: {
|
.from(schema.kvConfig)
|
||||||
key: {
|
.where(and(
|
||||||
[Op.in]: keys,
|
inArray(schema.kvConfig.key, keys),
|
||||||
},
|
eq(schema.kvConfig.uid, tokenUser.id)
|
||||||
uid: tokenUser.id,
|
));
|
||||||
},
|
|
||||||
});
|
|
||||||
const needUpdateList = list.filter((item) => {
|
const needUpdateList = list.filter((item) => {
|
||||||
const key = item.key;
|
const key = item.key;
|
||||||
const hash = keyEtagMap.get(key);
|
const hash = keyEtagMap.get(key);
|
||||||
@@ -43,30 +41,33 @@ app
|
|||||||
const keyETag = keyEtagMap.get(key);
|
const keyETag = keyEtagMap.get(key);
|
||||||
const configData = keyDataMap.get(key);
|
const configData = keyDataMap.get(key);
|
||||||
if (keyETag && configData) {
|
if (keyETag && configData) {
|
||||||
const [config, created] = await ConfigModel.findOrCreate({
|
const existing = await db.select()
|
||||||
where: {
|
.from(schema.kvConfig)
|
||||||
key,
|
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tokenUser.id)))
|
||||||
uid: tokenUser.id,
|
.limit(1);
|
||||||
},
|
|
||||||
defaults: {
|
let config;
|
||||||
|
if (existing.length === 0) {
|
||||||
|
const inserted = await db.insert(schema.kvConfig).values({
|
||||||
|
id: nanoid(),
|
||||||
key,
|
key,
|
||||||
title: key,
|
title: key,
|
||||||
description: `从${key}:${keyETag} 同步而来`,
|
description: `从${key}:${keyETag} 同步而来`,
|
||||||
uid: tokenUser.id,
|
uid: tokenUser.id,
|
||||||
hash: keyETag,
|
hash: keyETag,
|
||||||
data: configData,
|
data: configData,
|
||||||
},
|
}).returning();
|
||||||
});
|
config = inserted[0];
|
||||||
if (!created) {
|
} else {
|
||||||
await config.update(
|
const updated = await db.update(schema.kvConfig)
|
||||||
{
|
.set({
|
||||||
hash: keyETag,
|
hash: keyETag,
|
||||||
data: json,
|
data: json,
|
||||||
},
|
updatedAt: new Date().toISOString(),
|
||||||
{
|
})
|
||||||
fields: ['hash', 'data'],
|
.where(eq(schema.kvConfig.id, existing[0].id))
|
||||||
},
|
.returning();
|
||||||
);
|
config = updated[0];
|
||||||
}
|
}
|
||||||
updateList.push(config);
|
updateList.push(config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { app } from '@/app.ts';
|
import { eq, and } from 'drizzle-orm';
|
||||||
import { ConfigModel } from './models/model.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
import { defaultKeys } from './models/default-keys.ts';
|
import { defaultKeys } from './models/default-keys.ts';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
@@ -27,19 +28,28 @@ app
|
|||||||
}
|
}
|
||||||
const defaultConfig = defaultKeys.find((item) => item.key === configKey);
|
const defaultConfig = defaultKeys.find((item) => item.key === configKey);
|
||||||
|
|
||||||
const [config, created] = await ConfigModel.findOrCreate({
|
const existing = await db.select()
|
||||||
where: {
|
.from(schema.kvConfig)
|
||||||
key: configKey,
|
.where(and(
|
||||||
uid: tokenUser.id,
|
eq(schema.kvConfig.key, configKey),
|
||||||
},
|
eq(schema.kvConfig.uid, tokenUser.id)
|
||||||
defaults: {
|
))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let config;
|
||||||
|
if (existing.length === 0) {
|
||||||
|
const inserted = await db.insert(schema.kvConfig).values({
|
||||||
|
id: nanoid(),
|
||||||
title: defaultConfig?.key,
|
title: defaultConfig?.key,
|
||||||
description: defaultConfig?.description || '',
|
description: defaultConfig?.description || '',
|
||||||
key: configKey,
|
key: configKey,
|
||||||
uid: tokenUser.id,
|
uid: tokenUser.id,
|
||||||
data: defaultConfig?.data,
|
data: defaultConfig?.data,
|
||||||
},
|
}).returning();
|
||||||
});
|
config = inserted[0];
|
||||||
|
} else {
|
||||||
|
config = existing[0];
|
||||||
|
}
|
||||||
|
|
||||||
ctx.body = config;
|
ctx.body = config;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { app } from '@/app.ts';
|
import { eq, desc, and, inArray } from 'drizzle-orm';
|
||||||
import { ConfigModel } from './models/model.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { ShareConfigService } from './services/share.ts';
|
import { ShareConfigService } from './services/share.ts';
|
||||||
import { oss } from '@/app.ts';
|
import { oss } from '@/app.ts';
|
||||||
import { ConfigOssService } from '@kevisual/oss/services';
|
import { ConfigOssService } from '@kevisual/oss/services';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
@@ -13,12 +14,10 @@ app
|
|||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { id } = ctx.state.tokenUser;
|
const { id } = ctx.state.tokenUser;
|
||||||
const config = await ConfigModel.findAll({
|
const config = await db.select()
|
||||||
where: {
|
.from(schema.kvConfig)
|
||||||
uid: id,
|
.where(eq(schema.kvConfig.uid, id))
|
||||||
},
|
.orderBy(desc(schema.kvConfig.updatedAt));
|
||||||
order: [['updatedAt', 'DESC']],
|
|
||||||
});
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
list: config,
|
list: config,
|
||||||
};
|
};
|
||||||
@@ -36,9 +35,10 @@ app
|
|||||||
const tokernUser = ctx.state.tokenUser;
|
const tokernUser = ctx.state.tokenUser;
|
||||||
const tuid = tokernUser.id;
|
const tuid = tokernUser.id;
|
||||||
const { id, data, ...rest } = ctx.query?.data || {};
|
const { id, data, ...rest } = ctx.query?.data || {};
|
||||||
let config: ConfigModel;
|
let config: any;
|
||||||
if (id) {
|
if (id) {
|
||||||
config = await ConfigModel.findByPk(id);
|
const configs = await db.select().from(schema.kvConfig).where(eq(schema.kvConfig.id, id)).limit(1);
|
||||||
|
config = configs[0];
|
||||||
let keyIsChange = false;
|
let keyIsChange = false;
|
||||||
if (rest?.key) {
|
if (rest?.key) {
|
||||||
keyIsChange = rest.key !== config?.key;
|
keyIsChange = rest.key !== config?.key;
|
||||||
@@ -48,50 +48,57 @@ app
|
|||||||
}
|
}
|
||||||
if (keyIsChange) {
|
if (keyIsChange) {
|
||||||
const key = rest.key;
|
const key = rest.key;
|
||||||
const keyConfig = await ConfigModel.findOne({
|
const keyConfigs = await db.select()
|
||||||
where: {
|
.from(schema.kvConfig)
|
||||||
key,
|
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tuid)))
|
||||||
uid: tuid,
|
.limit(1);
|
||||||
},
|
const keyConfig = keyConfigs[0];
|
||||||
});
|
|
||||||
if (keyConfig && keyConfig.id !== id) {
|
if (keyConfig && keyConfig.id !== id) {
|
||||||
ctx.throw(403, 'key is already exists');
|
ctx.throw(403, 'key is already exists');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await config.update({
|
const updated = await db.update(schema.kvConfig)
|
||||||
data: {
|
.set({
|
||||||
...config.data,
|
data: data,
|
||||||
...data,
|
|
||||||
},
|
|
||||||
...rest,
|
...rest,
|
||||||
});
|
updatedAt: new Date().toISOString(),
|
||||||
if (config.data?.permission?.share === 'public') {
|
})
|
||||||
|
.where(eq(schema.kvConfig.id, id))
|
||||||
|
.returning();
|
||||||
|
config = updated[0];
|
||||||
|
if ((config.data as any)?.permission?.share === 'public') {
|
||||||
await ShareConfigService.expireShareConfig(config.key, tokernUser.username);
|
await ShareConfigService.expireShareConfig(config.key, tokernUser.username);
|
||||||
}
|
}
|
||||||
ctx.body = config;
|
ctx.body = config;
|
||||||
} else if (rest?.key) {
|
} else if (rest?.key) {
|
||||||
// id 不存在,key存在,则属于更新,key不能重复
|
// id 不存在,key存在,则属于更新,key不能重复
|
||||||
const key = rest.key;
|
const key = rest.key;
|
||||||
config = await ConfigModel.findOne({
|
const configs = await db.select()
|
||||||
where: {
|
.from(schema.kvConfig)
|
||||||
key,
|
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tuid)))
|
||||||
uid: tuid,
|
.limit(1);
|
||||||
},
|
config = configs[0];
|
||||||
});
|
|
||||||
if (config) {
|
if (config) {
|
||||||
await config.update({
|
const updated = await db.update(schema.kvConfig)
|
||||||
data: { ...config.data, ...data },
|
.set({
|
||||||
|
data: data,
|
||||||
...rest,
|
...rest,
|
||||||
});
|
updatedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.where(eq(schema.kvConfig.id, config.id))
|
||||||
|
.returning();
|
||||||
|
config = updated[0];
|
||||||
ctx.body = config;
|
ctx.body = config;
|
||||||
} else {
|
} else {
|
||||||
// 根据key创建一个配置
|
// 根据key创建一个配置
|
||||||
config = await ConfigModel.create({
|
const inserted = await db.insert(schema.kvConfig).values({
|
||||||
|
id: nanoid(),
|
||||||
key,
|
key,
|
||||||
...rest,
|
...rest,
|
||||||
data: data,
|
data: data,
|
||||||
uid: tuid,
|
uid: tuid,
|
||||||
});
|
}).returning();
|
||||||
|
config = inserted[0];
|
||||||
ctx.body = config;
|
ctx.body = config;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,22 +113,25 @@ app
|
|||||||
const data = config.data;
|
const data = config.data;
|
||||||
const hash = ossConfig.hash(data);
|
const hash = ossConfig.hash(data);
|
||||||
if (config.hash !== hash) {
|
if (config.hash !== hash) {
|
||||||
config.hash = hash;
|
await db.update(schema.kvConfig)
|
||||||
await config.save({
|
.set({
|
||||||
fields: ['hash'],
|
hash: hash,
|
||||||
});
|
updatedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.where(eq(schema.kvConfig.id, config.id));
|
||||||
await ossConfig.putJsonObject(key, data);
|
await ossConfig.putJsonObject(key, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (config) return;
|
if (config) return;
|
||||||
|
|
||||||
// id和key不存在。创建一个新的配置, 而且没有id的
|
// id和key不存在。创建一个新的配置, 而且没有id的
|
||||||
const newConfig = await ConfigModel.create({
|
const newConfig = await db.insert(schema.kvConfig).values({
|
||||||
|
id: nanoid(),
|
||||||
...rest,
|
...rest,
|
||||||
data: data,
|
data: data,
|
||||||
uid: tuid,
|
uid: tuid,
|
||||||
});
|
}).returning();
|
||||||
ctx.body = newConfig;
|
ctx.body = newConfig[0];
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|
||||||
@@ -139,17 +149,17 @@ app
|
|||||||
if (!id && !key) {
|
if (!id && !key) {
|
||||||
ctx.throw(400, 'id or key is required');
|
ctx.throw(400, 'id or key is required');
|
||||||
}
|
}
|
||||||
let config: ConfigModel;
|
let config: any;
|
||||||
if (id) {
|
if (id) {
|
||||||
config = await ConfigModel.findByPk(id);
|
const configs = await db.select().from(schema.kvConfig).where(eq(schema.kvConfig.id, id)).limit(1);
|
||||||
|
config = configs[0];
|
||||||
}
|
}
|
||||||
if (!config && key) {
|
if (!config && key) {
|
||||||
config = await ConfigModel.findOne({
|
const configs = await db.select()
|
||||||
where: {
|
.from(schema.kvConfig)
|
||||||
key,
|
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tuid)))
|
||||||
uid: tuid,
|
.limit(1);
|
||||||
},
|
config = configs[0];
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!config) {
|
if (!config) {
|
||||||
ctx.throw(404, 'config not found');
|
ctx.throw(404, 'config not found');
|
||||||
@@ -174,12 +184,9 @@ app
|
|||||||
const tuid = tokernUser.id;
|
const tuid = tokernUser.id;
|
||||||
const { id, key } = ctx.query?.data || {};
|
const { id, key } = ctx.query?.data || {};
|
||||||
if (id || key) {
|
if (id || key) {
|
||||||
const search: any = id ? { id } : { key };
|
const search: any = id ? eq(schema.kvConfig.id, id) : eq(schema.kvConfig.key, key);
|
||||||
const config = await ConfigModel.findOne({
|
const configs = await db.select().from(schema.kvConfig).where(search).limit(1);
|
||||||
where: {
|
const config = configs[0];
|
||||||
...search
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (config && config.uid === tuid) {
|
if (config && config.uid === tuid) {
|
||||||
const key = config.key;
|
const key = config.key;
|
||||||
const ossConfig = ConfigOssService.fromBase({
|
const ossConfig = ConfigOssService.fromBase({
|
||||||
@@ -193,7 +200,7 @@ app
|
|||||||
await ossConfig.deleteObject(key);
|
await ossConfig.deleteObject(key);
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
await config.destroy();
|
await db.delete(schema.kvConfig).where(eq(schema.kvConfig.id, config.id));
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(403, 'no permission');
|
ctx.throw(403, 'no permission');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useContextKey } from '@kevisual/context';
|
import { useContextKey } from '@kevisual/context';
|
||||||
import { sequelize } from '../../../modules/sequelize.ts';
|
|
||||||
import { DataTypes, Model } from 'sequelize';
|
|
||||||
import { Permission } from '@kevisual/permission';
|
import { Permission } from '@kevisual/permission';
|
||||||
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
import { db, schema } from '../../../app.ts';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export interface ConfigData {
|
export interface ConfigData {
|
||||||
key?: string;
|
key?: string;
|
||||||
@@ -9,23 +10,24 @@ export interface ConfigData {
|
|||||||
permission?: Permission;
|
permission?: Permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Config = Partial<InstanceType<typeof ConfigModel>>;
|
export type Config = {
|
||||||
|
id: string;
|
||||||
|
title: string | null;
|
||||||
|
description: string | null;
|
||||||
|
tags: unknown;
|
||||||
|
key: string | null;
|
||||||
|
data: unknown;
|
||||||
|
uid: string | null;
|
||||||
|
hash: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户配置
|
* 用户配置
|
||||||
*/
|
*/
|
||||||
export class ConfigModel extends Model {
|
export class ConfigModel {
|
||||||
declare id: string;
|
|
||||||
declare title: string;
|
|
||||||
declare description: string;
|
|
||||||
declare tags: string[];
|
|
||||||
/**
|
|
||||||
* @important 配置key, 默认可以为空,如何设置了,必须要唯一。
|
|
||||||
*/
|
|
||||||
declare key: string;
|
|
||||||
declare data: ConfigData; // files
|
|
||||||
declare uid: string;
|
|
||||||
declare hash: string;
|
|
||||||
/**
|
/**
|
||||||
* 获取用户配置
|
* 获取用户配置
|
||||||
* @param key 配置key
|
* @param key 配置key
|
||||||
@@ -35,37 +37,60 @@ export class ConfigModel extends Model {
|
|||||||
* @returns 配置
|
* @returns 配置
|
||||||
*/
|
*/
|
||||||
static async getConfig(key: string, opts: { uid: string; defaultData?: any }) {
|
static async getConfig(key: string, opts: { uid: string; defaultData?: any }) {
|
||||||
const [config, isNew] = await ConfigModel.findOrCreate({
|
const existing = await db.select()
|
||||||
where: { key, uid: opts.uid },
|
.from(schema.kvConfig)
|
||||||
defaults: {
|
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, opts.uid)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
return {
|
||||||
|
config: existing[0],
|
||||||
|
isNew: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const inserted = await db.insert(schema.kvConfig).values({
|
||||||
|
id: nanoid(),
|
||||||
key,
|
key,
|
||||||
title: key,
|
title: key,
|
||||||
uid: opts.uid,
|
uid: opts.uid,
|
||||||
data: opts?.defaultData || {},
|
data: opts?.defaultData || {},
|
||||||
},
|
}).returning();
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
config: config,
|
config: inserted[0],
|
||||||
isNew,
|
isNew: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static async setConfig(key: string, opts: { uid: string; data: any }) {
|
static async setConfig(key: string, opts: { uid: string; data: any }) {
|
||||||
let config = await ConfigModel.findOne({
|
const existing = await db.select()
|
||||||
where: { key, uid: opts.uid },
|
.from(schema.kvConfig)
|
||||||
});
|
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, opts.uid)))
|
||||||
if (config) {
|
.limit(1);
|
||||||
config.data = { ...config.data, ...opts.data };
|
|
||||||
await config.save();
|
if (existing.length > 0) {
|
||||||
|
const config = existing[0];
|
||||||
|
const updated = await db.update(schema.kvConfig)
|
||||||
|
.set({
|
||||||
|
data: { ...(config.data as any || {}), ...opts.data },
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.where(eq(schema.kvConfig.id, config.id))
|
||||||
|
.returning();
|
||||||
|
return updated[0];
|
||||||
} else {
|
} else {
|
||||||
config = await ConfigModel.create({
|
const inserted = await db.insert(schema.kvConfig).values({
|
||||||
|
id: nanoid(),
|
||||||
title: key,
|
title: key,
|
||||||
key,
|
key,
|
||||||
uid: opts.uid,
|
uid: opts.uid,
|
||||||
data: opts.data,
|
data: opts.data,
|
||||||
});
|
}).returning();
|
||||||
|
return inserted[0];
|
||||||
}
|
}
|
||||||
return config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取上传配置
|
* 获取上传配置
|
||||||
* @param key 配置key
|
* @param key 配置key
|
||||||
@@ -82,7 +107,7 @@ export class ConfigModel extends Model {
|
|||||||
uid: opts.uid,
|
uid: opts.uid,
|
||||||
defaultData: defaultConfig,
|
defaultData: defaultConfig,
|
||||||
});
|
});
|
||||||
const data = config.config.data;
|
const data = config.config.data as any;
|
||||||
const prefix = `/${data.key}/${data.version}`;
|
const prefix = `/${data.key}/${data.version}`;
|
||||||
return {
|
return {
|
||||||
config: config.config,
|
config: config.config,
|
||||||
@@ -90,6 +115,7 @@ export class ConfigModel extends Model {
|
|||||||
prefix,
|
prefix,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static async setUploadConfig(opts: { uid: string; data: { key?: string; version?: string } }) {
|
static async setUploadConfig(opts: { uid: string; data: { key?: string; version?: string } }) {
|
||||||
const config = await ConfigModel.setConfig('upload.json', {
|
const config = await ConfigModel.setConfig('upload.json', {
|
||||||
uid: opts.uid,
|
uid: opts.uid,
|
||||||
@@ -98,52 +124,5 @@ export class ConfigModel extends Model {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ConfigModel.init(
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
comment: 'id',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
key: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
defaultValue: [],
|
|
||||||
},
|
|
||||||
hash: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
defaultValue: {},
|
|
||||||
},
|
|
||||||
uid: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: 'kv_config',
|
|
||||||
paranoid: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// ConfigModel.sync({ alter: true, logging: false }).catch((e) => {
|
|
||||||
// console.error('ConfigModel sync', e);
|
|
||||||
// });
|
|
||||||
|
|
||||||
useContextKey('ConfigModel', () => ConfigModel);
|
useContextKey('ConfigModel', () => ConfigModel);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ConfigModel, Config } from '../models/model.ts';
|
import { Config } from '../models/model.ts';
|
||||||
import { CustomError } from '@kevisual/router';
|
import { CustomError } from '@kevisual/router';
|
||||||
import { redis } from '@/app.ts';
|
import { redis, db, schema } from '@/app.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { eq, and } from 'drizzle-orm';
|
||||||
import { UserPermission, UserPermissionOptions } from '@kevisual/permission';
|
import { UserPermission, UserPermissionOptions } from '@kevisual/permission';
|
||||||
|
|
||||||
export class ShareConfigService extends ConfigModel {
|
export class ShareConfigService {
|
||||||
/**
|
/**
|
||||||
* 获取分享的配置
|
* 获取分享的配置
|
||||||
* @param key 配置的key
|
* @param key 配置的key
|
||||||
@@ -22,26 +22,30 @@ export class ShareConfigService extends ConfigModel {
|
|||||||
}
|
}
|
||||||
const owner = username;
|
const owner = username;
|
||||||
if (shareCacheConfig) {
|
if (shareCacheConfig) {
|
||||||
const permission = new UserPermission({ permission: shareCacheConfig?.data?.permission, owner });
|
const permission = new UserPermission({ permission: (shareCacheConfig?.data as any)?.permission, owner });
|
||||||
const result = permission.checkPermissionSuccess(options);
|
const result = permission.checkPermissionSuccess(options);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new CustomError(403, 'no permission');
|
throw new CustomError(403, 'no permission');
|
||||||
}
|
}
|
||||||
return shareCacheConfig;
|
return shareCacheConfig;
|
||||||
}
|
}
|
||||||
const user = await User.findOne({
|
const users = await db.select()
|
||||||
where: { username },
|
.from(schema.cfUser)
|
||||||
});
|
.where(eq(schema.cfUser.username, username))
|
||||||
|
.limit(1);
|
||||||
|
const user = users[0];
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new CustomError(404, 'user not found');
|
throw new CustomError(404, 'user not found');
|
||||||
}
|
}
|
||||||
const config = await ConfigModel.findOne({
|
const configs = await db.select()
|
||||||
where: { key, uid: user.id },
|
.from(schema.kvConfig)
|
||||||
});
|
.where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, user.id)))
|
||||||
|
.limit(1);
|
||||||
|
const config = configs[0];
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new CustomError(404, 'config not found');
|
throw new CustomError(404, 'config not found');
|
||||||
}
|
}
|
||||||
const permission = new UserPermission({ permission: config?.data?.permission, owner });
|
const permission = new UserPermission({ permission: (config?.data as any)?.permission, owner });
|
||||||
const result = permission.checkPermissionSuccess(options);
|
const result = permission.checkPermissionSuccess(options);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new CustomError(403, 'no permission');
|
throw new CustomError(403, 'no permission');
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ app
|
|||||||
const config = await ConfigModel.getUploadConfig({
|
const config = await ConfigModel.getUploadConfig({
|
||||||
uid: tokenUser.id,
|
uid: tokenUser.id,
|
||||||
});
|
});
|
||||||
const key = config?.config?.data?.key || '';
|
const data: any = config?.config?.data || {};
|
||||||
const version = config?.config?.data?.version || '';
|
const key = data.key || '';
|
||||||
|
const version = data.version || '';
|
||||||
const username = tokenUser.username;
|
const username = tokenUser.username;
|
||||||
const prefix = `${key}/${version}/`;
|
const prefix = `${key}/${version}/`;
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
@@ -35,7 +36,7 @@ app
|
|||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { id } = ctx.state.tokenUser;
|
const { id } = ctx.state.tokenUser;
|
||||||
const data = ctx.query.data || {};
|
const data = ctx.query?.data || {};
|
||||||
const { key, version } = data;
|
const { key, version } = data;
|
||||||
if (!key && !version) {
|
if (!key && !version) {
|
||||||
ctx.throw(400, 'key or version is required');
|
ctx.throw(400, 'key or version is required');
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import './config/index.ts';
|
|||||||
|
|
||||||
// import './file-listener/index.ts';
|
// import './file-listener/index.ts';
|
||||||
|
|
||||||
|
import './mark/index.ts';
|
||||||
|
|
||||||
import './light-code/index.ts';
|
import './light-code/index.ts';
|
||||||
|
|
||||||
import './ai/index.ts';
|
import './ai/index.ts';
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { eq, desc, and, like, or } from 'drizzle-orm';
|
import { eq, desc, and, like, or } from 'drizzle-orm';
|
||||||
import { CustomError } from '@kevisual/router';
|
|
||||||
import { app, db, schema } from '../../app.ts';
|
import { app, db, schema } from '../../app.ts';
|
||||||
|
import { CustomError } from '@kevisual/router';
|
||||||
|
import { filter } from '@kevisual/js-filter'
|
||||||
|
import { z } from 'zod';
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'light-code',
|
path: 'light-code',
|
||||||
@@ -9,10 +10,22 @@ app
|
|||||||
description: `获取轻代码列表,参数
|
description: `获取轻代码列表,参数
|
||||||
type: 代码类型light-code, ts`,
|
type: 代码类型light-code, ts`,
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
|
metadata: {
|
||||||
|
args: {
|
||||||
|
type: z.string().optional().describe('代码类型light-code, ts'),
|
||||||
|
search: z.string().optional().describe('搜索关键词,匹配标题和描述'),
|
||||||
|
filter: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'过滤条件,SQL like格式字符串,例如:WHERE tags LIKE \'%tag1%\' AND tags LIKE \'%tag2%\'',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const { type, search } = ctx.query || {};
|
const { type, search, filter: filterQuery } = ctx.query || {};
|
||||||
const conditions = [eq(schema.kvContainer.uid, tokenUser.id)];
|
const conditions = [eq(schema.kvContainer.uid, tokenUser.id)];
|
||||||
if (type) {
|
if (type) {
|
||||||
conditions.push(eq(schema.kvContainer.type, type as string));
|
conditions.push(eq(schema.kvContainer.type, type as string));
|
||||||
@@ -43,7 +56,12 @@ app
|
|||||||
.from(schema.kvContainer)
|
.from(schema.kvContainer)
|
||||||
.where(and(...conditions))
|
.where(and(...conditions))
|
||||||
.orderBy(desc(schema.kvContainer.updatedAt));
|
.orderBy(desc(schema.kvContainer.updatedAt));
|
||||||
|
if (filterQuery) {
|
||||||
|
const filteredList = filter(list, filterQuery);
|
||||||
|
ctx.body = { list: filteredList }
|
||||||
|
} else {
|
||||||
ctx.body = { list };
|
ctx.body = { list };
|
||||||
|
}
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -140,7 +158,7 @@ app
|
|||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'container',
|
path: 'light-code',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
})
|
})
|
||||||
|
|||||||
1
src/routes/mark/index.ts
Normal file
1
src/routes/mark/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import './list.ts';
|
||||||
308
src/routes/mark/list.ts
Normal file
308
src/routes/mark/list.ts
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
import { eq, desc, and, like, or, count, sql } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '../../app.ts';
|
||||||
|
import { MarkServices } from './services/mark.ts';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'list',
|
||||||
|
description: 'mark list.',
|
||||||
|
middleware: ['auth'],
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
ctx.body = await MarkServices.getList({
|
||||||
|
uid: tokenUser.id,
|
||||||
|
query: ctx.query,
|
||||||
|
queryType: 'simple',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'getVersion',
|
||||||
|
middleware: ['auth'],
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id } = ctx.query;
|
||||||
|
if (id) {
|
||||||
|
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
|
||||||
|
const markModel = marks[0];
|
||||||
|
if (!markModel) {
|
||||||
|
ctx.throw(404, 'mark not found');
|
||||||
|
}
|
||||||
|
if (markModel.uid !== tokenUser.id) {
|
||||||
|
ctx.throw(403, 'no permission');
|
||||||
|
}
|
||||||
|
ctx.body = {
|
||||||
|
version: Number(markModel.version),
|
||||||
|
updatedAt: markModel.updatedAt,
|
||||||
|
createdAt: markModel.createdAt,
|
||||||
|
id: markModel.id,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
ctx.throw(400, 'id is required');
|
||||||
|
// const [markModel, created] = await MarkModel.findOrCreate({
|
||||||
|
// where: {
|
||||||
|
// uid: tokenUser.id,
|
||||||
|
// puid: tokenUser.uid,
|
||||||
|
// title: dayjs().format('YYYY-MM-DD'),
|
||||||
|
// },
|
||||||
|
// defaults: {
|
||||||
|
// title: dayjs().format('YYYY-MM-DD'),
|
||||||
|
// uid: tokenUser.id,
|
||||||
|
// markType: 'wallnote',
|
||||||
|
// tags: ['daily'],
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// ctx.body = {
|
||||||
|
// version: Number(markModel.version),
|
||||||
|
// updatedAt: markModel.updatedAt,
|
||||||
|
// createdAt: markModel.createdAt,
|
||||||
|
// id: markModel.id,
|
||||||
|
// created: created,
|
||||||
|
// };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'get',
|
||||||
|
middleware: ['auth'],
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id } = ctx.query;
|
||||||
|
if (id) {
|
||||||
|
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
|
||||||
|
const markModel = marks[0];
|
||||||
|
if (!markModel) {
|
||||||
|
ctx.throw(404, 'mark not found');
|
||||||
|
}
|
||||||
|
if (markModel.uid !== tokenUser.id) {
|
||||||
|
ctx.throw(403, 'no permission');
|
||||||
|
}
|
||||||
|
ctx.body = markModel;
|
||||||
|
} else {
|
||||||
|
ctx.throw(400, 'id is required');
|
||||||
|
// id 不存在,获取当天的title为 日期的一条数据
|
||||||
|
// const [markModel, created] = await MarkModel.findOrCreate({
|
||||||
|
// where: {
|
||||||
|
// uid: tokenUser.id,
|
||||||
|
// puid: tokenUser.uid,
|
||||||
|
// title: dayjs().format('YYYY-MM-DD'),
|
||||||
|
// },
|
||||||
|
// defaults: {
|
||||||
|
// title: dayjs().format('YYYY-MM-DD'),
|
||||||
|
// uid: tokenUser.id,
|
||||||
|
// markType: 'wallnote',
|
||||||
|
// tags: ['daily'],
|
||||||
|
// uname: tokenUser.username,
|
||||||
|
// puid: tokenUser.uid,
|
||||||
|
// version: 1,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// ctx.body = markModel;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'update',
|
||||||
|
middleware: ['auth'],
|
||||||
|
isDebug: true,
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id, createdAt, updatedAt, uid: _, puid: _2, uname: _3, data, ...rest } = ctx.query.data || {};
|
||||||
|
let markModel: any;
|
||||||
|
if (id) {
|
||||||
|
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
|
||||||
|
markModel = marks[0];
|
||||||
|
if (!markModel) {
|
||||||
|
ctx.throw(404, 'mark not found');
|
||||||
|
}
|
||||||
|
if (markModel.uid !== tokenUser.id) {
|
||||||
|
ctx.throw(403, 'no permission');
|
||||||
|
}
|
||||||
|
const version = Number(markModel.version) + 1;
|
||||||
|
const updated = await db.update(schema.microMark)
|
||||||
|
.set({
|
||||||
|
...rest,
|
||||||
|
data: {
|
||||||
|
...(markModel.data as any || {}),
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
version,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.where(eq(schema.microMark.id, id))
|
||||||
|
.returning();
|
||||||
|
markModel = updated[0];
|
||||||
|
} else {
|
||||||
|
const inserted = await db.insert(schema.microMark).values({
|
||||||
|
id: nanoid(),
|
||||||
|
data: data || {},
|
||||||
|
...rest,
|
||||||
|
uname: tokenUser.username,
|
||||||
|
uid: tokenUser.id,
|
||||||
|
puid: tokenUser.uid,
|
||||||
|
}).returning();
|
||||||
|
markModel = inserted[0];
|
||||||
|
}
|
||||||
|
ctx.body = markModel;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'updateNode',
|
||||||
|
middleware: ['auth'],
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const operate = ctx.query.operate || 'update';
|
||||||
|
const { id, node } = ctx.query.data || {};
|
||||||
|
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
|
||||||
|
const markModel = marks[0];
|
||||||
|
if (!markModel) {
|
||||||
|
ctx.throw(404, 'mark not found');
|
||||||
|
}
|
||||||
|
if (markModel.uid !== tokenUser.id) {
|
||||||
|
ctx.throw(403, 'no permission');
|
||||||
|
}
|
||||||
|
// Update JSON node logic with Drizzle
|
||||||
|
const currentData = markModel.data as any || {};
|
||||||
|
const nodes = currentData.nodes || [];
|
||||||
|
const nodeIndex = nodes.findIndex((n: any) => n.id === node.id);
|
||||||
|
|
||||||
|
let updatedNodes;
|
||||||
|
if (operate === 'delete') {
|
||||||
|
updatedNodes = nodes.filter((n: any) => n.id !== node.id);
|
||||||
|
} else if (nodeIndex >= 0) {
|
||||||
|
updatedNodes = [...nodes];
|
||||||
|
updatedNodes[nodeIndex] = { ...nodes[nodeIndex], ...node };
|
||||||
|
} else {
|
||||||
|
updatedNodes = [...nodes, node];
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = Number(markModel.version) + 1;
|
||||||
|
const updated = await db.update(schema.microMark)
|
||||||
|
.set({
|
||||||
|
data: { ...currentData, nodes: updatedNodes },
|
||||||
|
version,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.where(eq(schema.microMark.id, id))
|
||||||
|
.returning();
|
||||||
|
ctx.body = updated[0];
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'updateNodes',
|
||||||
|
middleware: ['auth'],
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id, nodeOperateList } = ctx.query.data || {};
|
||||||
|
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
|
||||||
|
const markModel = marks[0];
|
||||||
|
if (!markModel) {
|
||||||
|
ctx.throw(404, 'mark not found');
|
||||||
|
}
|
||||||
|
if (markModel.uid !== tokenUser.id) {
|
||||||
|
ctx.throw(403, 'no permission');
|
||||||
|
}
|
||||||
|
if (!nodeOperateList || !Array.isArray(nodeOperateList) || nodeOperateList.length === 0) {
|
||||||
|
ctx.throw(400, 'nodeOperateList is required');
|
||||||
|
}
|
||||||
|
if (nodeOperateList.some((item: any) => !item.node)) {
|
||||||
|
ctx.throw(400, 'nodeOperateList node is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update multiple JSON nodes logic with Drizzle
|
||||||
|
const currentData = markModel.data as any || {};
|
||||||
|
let nodes = currentData.nodes || [];
|
||||||
|
|
||||||
|
for (const item of nodeOperateList) {
|
||||||
|
const { node, operate = 'update' } = item;
|
||||||
|
const nodeIndex = nodes.findIndex((n: any) => n.id === node.id);
|
||||||
|
|
||||||
|
if (operate === 'delete') {
|
||||||
|
nodes = nodes.filter((n: any) => n.id !== node.id);
|
||||||
|
} else if (nodeIndex >= 0) {
|
||||||
|
nodes[nodeIndex] = { ...nodes[nodeIndex], ...node };
|
||||||
|
} else {
|
||||||
|
nodes.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = Number(markModel.version) + 1;
|
||||||
|
const updated = await db.update(schema.microMark)
|
||||||
|
.set({
|
||||||
|
data: { ...currentData, nodes },
|
||||||
|
version,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.where(eq(schema.microMark.id, id))
|
||||||
|
.returning();
|
||||||
|
ctx.body = updated[0];
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'delete',
|
||||||
|
middleware: ['auth'],
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const { id } = ctx.query;
|
||||||
|
const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1);
|
||||||
|
const markModel = marks[0];
|
||||||
|
if (!markModel) {
|
||||||
|
ctx.throw(404, 'mark not found');
|
||||||
|
}
|
||||||
|
if (markModel.uid !== tokenUser.id) {
|
||||||
|
ctx.throw(403, 'no permission');
|
||||||
|
}
|
||||||
|
await db.delete(schema.microMark).where(eq(schema.microMark.id, id));
|
||||||
|
ctx.body = markModel;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({ path: 'mark', key: 'getMenu', description: '获取菜单', middleware: ['auth'] })
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state.tokenUser;
|
||||||
|
const [rows, totalResult] = await Promise.all([
|
||||||
|
db.select({
|
||||||
|
id: schema.microMark.id,
|
||||||
|
title: schema.microMark.title,
|
||||||
|
summary: schema.microMark.summary,
|
||||||
|
tags: schema.microMark.tags,
|
||||||
|
thumbnail: schema.microMark.thumbnail,
|
||||||
|
link: schema.microMark.link,
|
||||||
|
createdAt: schema.microMark.createdAt,
|
||||||
|
updatedAt: schema.microMark.updatedAt,
|
||||||
|
}).from(schema.microMark).where(eq(schema.microMark.uid, tokenUser.id)),
|
||||||
|
db.select({ count: count() }).from(schema.microMark).where(eq(schema.microMark.uid, tokenUser.id))
|
||||||
|
]);
|
||||||
|
ctx.body = {
|
||||||
|
list: rows,
|
||||||
|
total: totalResult[0]?.count || 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
327
src/routes/mark/mark-model.ts
Normal file
327
src/routes/mark/mark-model.ts
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import { useContextKey } from '@kevisual/context';
|
||||||
|
import { nanoid, customAlphabet } from 'nanoid';
|
||||||
|
import { DataTypes, Model, ModelAttributes } from 'sequelize';
|
||||||
|
import type { Sequelize } from 'sequelize';
|
||||||
|
export const random = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||||
|
export type Mark = Partial<InstanceType<typeof MarkModel>>;
|
||||||
|
export type MarkData = {
|
||||||
|
md?: string; // markdown
|
||||||
|
mdList?: string[]; // markdown list
|
||||||
|
type?: string; // 类型 markdown | json | html | image | video | audio | code | link | file
|
||||||
|
data?: any;
|
||||||
|
key?: string; // 文件的名称, 唯一
|
||||||
|
push?: boolean; // 是否推送到elasticsearch
|
||||||
|
pushTime?: Date; // 推送时间
|
||||||
|
summary?: string; // 摘要
|
||||||
|
nodes?: MarkDataNode[]; // 节点
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
export type MarkFile = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
size: number;
|
||||||
|
type: 'self' | 'data' | 'generate'; // generate为生成文件
|
||||||
|
query: string; // 'data.nodes[id].content';
|
||||||
|
hash: string;
|
||||||
|
fileKey: string; // 文件的名称, 唯一
|
||||||
|
};
|
||||||
|
export type MarkDataNode = {
|
||||||
|
id?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
export type MarkConfig = {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
export type MarkAuth = {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 隐秘内容
|
||||||
|
* auth
|
||||||
|
* config
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class MarkModel extends Model {
|
||||||
|
declare id: string;
|
||||||
|
declare title: string; // 标题,可以ai生成
|
||||||
|
declare description: string; // 描述,可以ai生成
|
||||||
|
declare cover: string; // 封面,可以ai生成
|
||||||
|
declare thumbnail: string; // 缩略图
|
||||||
|
declare key: string; // 文件路径
|
||||||
|
declare markType: string; // markdown | json | html | image | video | audio | code | link | file
|
||||||
|
declare link: string; // 访问链接
|
||||||
|
declare tags: string[]; // 标签
|
||||||
|
declare summary: string; // 摘要, description的简化版
|
||||||
|
declare data: MarkData; // 数据
|
||||||
|
|
||||||
|
declare uid: string; // 操作用户的id
|
||||||
|
declare puid: string; // 父级用户的id, 真实用户
|
||||||
|
declare config: MarkConfig; // mark属于一定不会暴露的内容。
|
||||||
|
|
||||||
|
declare fileList: MarkFile[]; // 文件管理
|
||||||
|
declare uname: string; // 用户的名称, 或者着别名
|
||||||
|
|
||||||
|
declare markedAt: Date; // 标记时间
|
||||||
|
declare createdAt: Date;
|
||||||
|
declare updatedAt: Date;
|
||||||
|
declare version: number;
|
||||||
|
/**
|
||||||
|
* 加锁更新data中的node的节点,通过node的id
|
||||||
|
* @param param0
|
||||||
|
*/
|
||||||
|
static async updateJsonNode(id: string, node: MarkDataNode, opts?: { operate?: 'update' | 'delete'; Model?: any; sequelize?: Sequelize }) {
|
||||||
|
const sequelize = opts?.sequelize || (await useContextKey('sequelize'));
|
||||||
|
const transaction = await sequelize.transaction(); // 开启事务
|
||||||
|
const operate = opts.operate || 'update';
|
||||||
|
const isUpdate = operate === 'update';
|
||||||
|
const Model = opts.Model || MarkModel;
|
||||||
|
try {
|
||||||
|
// 1. 获取当前的 JSONB 字段值(加锁)
|
||||||
|
const mark = await Model.findByPk(id, {
|
||||||
|
transaction,
|
||||||
|
lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改
|
||||||
|
});
|
||||||
|
if (!mark) {
|
||||||
|
throw new Error('Mark not found');
|
||||||
|
}
|
||||||
|
// 2. 修改特定的数组元素
|
||||||
|
const data = mark.data as MarkData;
|
||||||
|
const items = data.nodes;
|
||||||
|
if (!node.id) {
|
||||||
|
node.id = random(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到要更新的元素
|
||||||
|
const itemIndex = items.findIndex((item) => item.id === node.id);
|
||||||
|
if (itemIndex === -1) {
|
||||||
|
isUpdate && items.push(node);
|
||||||
|
} else {
|
||||||
|
if (isUpdate) {
|
||||||
|
items[itemIndex] = node;
|
||||||
|
} else {
|
||||||
|
items.splice(itemIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const version = Number(mark.version) + 1;
|
||||||
|
// 4. 更新 JSONB 字段
|
||||||
|
const result = await mark.update(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
nodes: items,
|
||||||
|
},
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
{ transaction },
|
||||||
|
);
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async updateJsonNodes(id: string, nodes: { node: MarkDataNode; operate?: 'update' | 'delete' }[], opts?: { Model?: any; sequelize?: Sequelize }) {
|
||||||
|
const sequelize = opts?.sequelize || (await useContextKey('sequelize'));
|
||||||
|
const transaction = await sequelize.transaction(); // 开启事务
|
||||||
|
const Model = opts?.Model || MarkModel;
|
||||||
|
try {
|
||||||
|
const mark = await Model.findByPk(id, {
|
||||||
|
transaction,
|
||||||
|
lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改
|
||||||
|
});
|
||||||
|
if (!mark) {
|
||||||
|
throw new Error('Mark not found');
|
||||||
|
}
|
||||||
|
const data = mark.data as MarkData;
|
||||||
|
const _nodes = data.nodes || [];
|
||||||
|
// 过滤不在nodes中的节点
|
||||||
|
const blankNodes = nodes.filter((node) => !_nodes.find((n) => n.id === node.node.id)).map((node) => node.node);
|
||||||
|
// 更新或删除节点
|
||||||
|
const newNodes = _nodes
|
||||||
|
.map((node) => {
|
||||||
|
const nodeOperate = nodes.find((n) => n.node.id === node.id);
|
||||||
|
if (nodeOperate) {
|
||||||
|
if (nodeOperate.operate === 'delete') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return nodeOperate.node;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
})
|
||||||
|
.filter((node) => node !== null);
|
||||||
|
const version = Number(mark.version) + 1;
|
||||||
|
const result = await mark.update(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
nodes: [...blankNodes, ...newNodes],
|
||||||
|
},
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
{ transaction },
|
||||||
|
);
|
||||||
|
await transaction.commit();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async updateData(id: string, data: MarkData, opts: { Model?: any; sequelize?: Sequelize }) {
|
||||||
|
const sequelize = opts.sequelize || (await useContextKey('sequelize'));
|
||||||
|
const transaction = await sequelize.transaction(); // 开启事务
|
||||||
|
const Model = opts.Model || MarkModel;
|
||||||
|
const mark = await Model.findByPk(id, {
|
||||||
|
transaction,
|
||||||
|
lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改
|
||||||
|
});
|
||||||
|
if (!mark) {
|
||||||
|
throw new Error('Mark not found');
|
||||||
|
}
|
||||||
|
const version = Number(mark.version) + 1;
|
||||||
|
const result = await mark.update(
|
||||||
|
{
|
||||||
|
...mark.data,
|
||||||
|
...data,
|
||||||
|
data: {
|
||||||
|
...mark.data,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
{ transaction },
|
||||||
|
);
|
||||||
|
await transaction.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
static async createNew(data: any, opts: { Model?: any; sequelize?: Sequelize }) {
|
||||||
|
const sequelize = opts.sequelize || (await useContextKey('sequelize'));
|
||||||
|
const transaction = await sequelize.transaction(); // 开启事务
|
||||||
|
const Model = opts.Model || MarkModel;
|
||||||
|
const result = await Model.create({ ...data, version: 1 }, { transaction });
|
||||||
|
await transaction.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export type MarkInitOpts<T = any> = {
|
||||||
|
tableName: string;
|
||||||
|
sequelize?: Sequelize;
|
||||||
|
callInit?: (attribute: ModelAttributes) => ModelAttributes;
|
||||||
|
Model?: T extends typeof MarkModel ? T : typeof MarkModel;
|
||||||
|
};
|
||||||
|
export type Opts = {
|
||||||
|
sync?: boolean;
|
||||||
|
alter?: boolean;
|
||||||
|
logging?: boolean | ((...args: any) => any);
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
export const MarkMInit = async <T = any>(opts: MarkInitOpts<T>, sync?: Opts) => {
|
||||||
|
const sequelize = await useContextKey('sequelize');
|
||||||
|
opts.sequelize = opts.sequelize || sequelize;
|
||||||
|
const { callInit, Model, ...optsRest } = opts;
|
||||||
|
const modelAttribute = {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
comment: 'id',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
key: {
|
||||||
|
type: DataTypes.TEXT, // 对应的minio的文件路径
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
markType: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
defaultValue: 'md', // markdown | json | html | image | video | audio | code | link | file
|
||||||
|
comment: '类型',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
cover: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
defaultValue: '',
|
||||||
|
comment: '封面',
|
||||||
|
},
|
||||||
|
thumbnail: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
defaultValue: '',
|
||||||
|
comment: '缩略图',
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
defaultValue: '',
|
||||||
|
comment: '链接',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: [],
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
defaultValue: '',
|
||||||
|
comment: '摘要',
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: {},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: {},
|
||||||
|
},
|
||||||
|
fileList: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: [],
|
||||||
|
},
|
||||||
|
uname: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
defaultValue: '',
|
||||||
|
comment: '用户的名称, 更新后的用户的名称',
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
type: DataTypes.INTEGER, // 更新刷新版本,多人协作
|
||||||
|
defaultValue: 1,
|
||||||
|
},
|
||||||
|
markedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '标记时间',
|
||||||
|
},
|
||||||
|
uid: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
puid: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const InitModel = Model || MarkModel;
|
||||||
|
InitModel.init(callInit ? callInit(modelAttribute) : modelAttribute, {
|
||||||
|
sequelize,
|
||||||
|
paranoid: true,
|
||||||
|
...optsRest,
|
||||||
|
});
|
||||||
|
if (sync && sync.sync) {
|
||||||
|
const { sync: _, ...rest } = sync;
|
||||||
|
MarkModel.sync({ alter: true, logging: false, ...rest }).catch((e) => {
|
||||||
|
console.error('MarkModel sync', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markModelInit = MarkMInit;
|
||||||
|
|
||||||
|
export const syncMarkModel = async (sync?: Opts, tableName = 'micro_mark') => {
|
||||||
|
const sequelize = await useContextKey('sequelize');
|
||||||
|
await MarkMInit({ sequelize, tableName }, sync);
|
||||||
|
};
|
||||||
5
src/routes/mark/model.ts
Normal file
5
src/routes/mark/model.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from '@kevisual/code-center-module/src/mark/mark-model.ts';
|
||||||
|
import { markModelInit, MarkModel, syncMarkModel } from '@kevisual/code-center-module/src/mark/mark-model.ts';
|
||||||
|
export { markModelInit, MarkModel };
|
||||||
|
|
||||||
|
syncMarkModel({ sync: true, alter: true, logging: false });
|
||||||
85
src/routes/mark/services/mark.ts
Normal file
85
src/routes/mark/services/mark.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { eq, desc, asc, and, like, or, count } from 'drizzle-orm';
|
||||||
|
import { app, db, schema } from '../../../app.ts';
|
||||||
|
|
||||||
|
export class MarkServices {
|
||||||
|
static getList = async (opts: {
|
||||||
|
/** 查询用户的 */
|
||||||
|
uid?: string;
|
||||||
|
query?: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
search?: string;
|
||||||
|
markType?: string;
|
||||||
|
sort?: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 查询类型
|
||||||
|
* simple: 简单查询 默认
|
||||||
|
*/
|
||||||
|
queryType?: string;
|
||||||
|
}) => {
|
||||||
|
const { uid, query = {} } = opts;
|
||||||
|
const { page = 1, pageSize = 999, search, sort = 'DESC' } = query;
|
||||||
|
|
||||||
|
const conditions = [];
|
||||||
|
if (uid) {
|
||||||
|
conditions.push(eq(schema.microMark.uid, uid));
|
||||||
|
}
|
||||||
|
if (search) {
|
||||||
|
conditions.push(
|
||||||
|
or(
|
||||||
|
like(schema.microMark.title, `%${search}%`),
|
||||||
|
like(schema.microMark.summary, `%${search}%`)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (opts.query?.markType) {
|
||||||
|
conditions.push(eq(schema.microMark.markType, opts.query.markType));
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
||||||
|
|
||||||
|
const queryType = opts.queryType || 'simple';
|
||||||
|
let selectFields: any = {};
|
||||||
|
|
||||||
|
if (queryType === 'simple') {
|
||||||
|
// Exclude data, config, cover, description
|
||||||
|
selectFields = {
|
||||||
|
id: schema.microMark.id,
|
||||||
|
title: schema.microMark.title,
|
||||||
|
tags: schema.microMark.tags,
|
||||||
|
uname: schema.microMark.uname,
|
||||||
|
uid: schema.microMark.uid,
|
||||||
|
createdAt: schema.microMark.createdAt,
|
||||||
|
updatedAt: schema.microMark.updatedAt,
|
||||||
|
thumbnail: schema.microMark.thumbnail,
|
||||||
|
link: schema.microMark.link,
|
||||||
|
summary: schema.microMark.summary,
|
||||||
|
markType: schema.microMark.markType,
|
||||||
|
puid: schema.microMark.puid,
|
||||||
|
deletedAt: schema.microMark.deletedAt,
|
||||||
|
version: schema.microMark.version,
|
||||||
|
fileList: schema.microMark.fileList,
|
||||||
|
key: schema.microMark.key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderByField = sort === 'ASC' ? asc(schema.microMark.updatedAt) : desc(schema.microMark.updatedAt);
|
||||||
|
|
||||||
|
const [rows, totalResult] = await Promise.all([
|
||||||
|
queryType === 'simple'
|
||||||
|
? db.select(selectFields).from(schema.microMark).where(whereClause).orderBy(orderByField).limit(pageSize).offset((page - 1) * pageSize)
|
||||||
|
: db.select().from(schema.microMark).where(whereClause).orderBy(orderByField).limit(pageSize).offset((page - 1) * pageSize),
|
||||||
|
db.select({ count: count() }).from(schema.microMark).where(whereClause)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pagination: {
|
||||||
|
current: page,
|
||||||
|
pageSize,
|
||||||
|
total: totalResult[0]?.count || 0,
|
||||||
|
},
|
||||||
|
list: rows,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -153,7 +153,9 @@ app
|
|||||||
browser: someInfo['user-agent'],
|
browser: someInfo['user-agent'],
|
||||||
host: someInfo.host,
|
host: someInfo.host,
|
||||||
});
|
});
|
||||||
createCookie(token, ctx);
|
createCookie({
|
||||||
|
token: token.accessToken
|
||||||
|
}, ctx);
|
||||||
ctx.body = token;
|
ctx.body = token;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -259,7 +261,10 @@ app
|
|||||||
const refreshToken = accessUser.oauthExpand?.refreshToken;
|
const refreshToken = accessUser.oauthExpand?.refreshToken;
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
const result = await User.oauth.refreshToken(refreshToken);
|
const result = await User.oauth.refreshToken(refreshToken);
|
||||||
createCookie(result, ctx);
|
createCookie({
|
||||||
|
token: result.accessToken
|
||||||
|
}, ctx);
|
||||||
|
|
||||||
ctx.body = result;
|
ctx.body = result;
|
||||||
return;
|
return;
|
||||||
} else if (accessUser) {
|
} else if (accessUser) {
|
||||||
@@ -268,7 +273,9 @@ app
|
|||||||
...accessUser.oauthExpand,
|
...accessUser.oauthExpand,
|
||||||
hasRefreshToken: true,
|
hasRefreshToken: true,
|
||||||
});
|
});
|
||||||
createCookie(result, ctx);
|
createCookie({
|
||||||
|
token: result.accessToken
|
||||||
|
}, ctx);
|
||||||
ctx.body = result;
|
ctx.body = result;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -323,13 +330,17 @@ app
|
|||||||
if (orgsList.includes(username)) {
|
if (orgsList.includes(username)) {
|
||||||
if (tokenUsername === username) {
|
if (tokenUsername === username) {
|
||||||
const result = await User.oauth.resetToken(token);
|
const result = await User.oauth.resetToken(token);
|
||||||
createCookie(result, ctx);
|
createCookie({
|
||||||
|
token: result.accessToken,
|
||||||
|
}, ctx);
|
||||||
await User.oauth.delToken(token);
|
await User.oauth.delToken(token);
|
||||||
ctx.body = result;
|
ctx.body = result;
|
||||||
} else {
|
} else {
|
||||||
const user = await User.findOne({ where: { username } });
|
const user = await User.findOne({ where: { username } });
|
||||||
const result = await user.createToken(userId, 'default');
|
const result = await user.createToken(userId, 'default');
|
||||||
createCookie(result, ctx);
|
createCookie({
|
||||||
|
token: result.accessToken,
|
||||||
|
}, ctx);
|
||||||
ctx.body = result;
|
ctx.body = result;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -352,7 +363,9 @@ app
|
|||||||
const result = await User.oauth.refreshToken(refreshToken);
|
const result = await User.oauth.refreshToken(refreshToken);
|
||||||
if (result) {
|
if (result) {
|
||||||
console.log('refreshToken result', result);
|
console.log('refreshToken result', result);
|
||||||
createCookie(result, ctx);
|
createCookie({
|
||||||
|
token: result.accessToken,
|
||||||
|
}, ctx);
|
||||||
ctx.body = result;
|
ctx.body = result;
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(500, 'Refresh Token Failed, please login again');
|
ctx.throw(500, 'Refresh Token Failed, please login again');
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ app
|
|||||||
const token = JSON.parse(data);
|
const token = JSON.parse(data);
|
||||||
if (token.accessToken) {
|
if (token.accessToken) {
|
||||||
ctx.body = token;
|
ctx.body = token;
|
||||||
createCookie(token, ctx);
|
createCookie({
|
||||||
|
token: token.accessToken,
|
||||||
|
}, ctx);
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(500, 'Checked error Failed, login failed, please login again');
|
ctx.throw(500, 'Checked error Failed, login failed, please login again');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user