diff --git a/src/app.ts b/src/app.ts index 3224644..9f3fc7a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,8 +4,11 @@ import * as redisLib from './modules/redis.ts'; import * as minioLib from './modules/minio.ts'; import * as sequelizeLib from './modules/sequelize.ts'; import { useContextKey, useContext } from '@kevisual/use-config/context'; +import { SimpleRouter } from '@kevisual/router/simple'; useConfig(); +export const router = useContextKey('router', () => new SimpleRouter()); + export const redis = useContextKey('redis', () => redisLib.redis); export const redisPublisher = useContextKey('redisPublisher', () => redisLib.redisPublisher); export const redisSubscriber = useContextKey('redisSubscriber', () => redisLib.redisSubscriber); diff --git a/src/routes-simple/code/upload.ts b/src/routes-simple/code/upload.ts index f26a63b..b5121b8 100644 --- a/src/routes-simple/code/upload.ts +++ b/src/routes-simple/code/upload.ts @@ -1,9 +1,8 @@ import { IncomingForm } from 'formidable'; import { checkAuth } from '../middleware/auth.ts'; -import { router } from '../router.ts'; +import { router, clients, writeEvents } from '../router.ts'; import { error } from '../middleware/auth.ts'; import fs from 'fs'; -import { clients } from '../upload.ts'; import { useFileStore } from '@kevisual/use-config/file-store'; import { app, minioClient } from '@/app.ts'; import { bucketName } from '@/modules/minio.ts'; @@ -29,7 +28,6 @@ router.post('/api/micro-app/upload', async (req, res) => { keepExtensions: true, // 保留文件 hashAlgorithm: 'md5', // 文件哈希算法 }); - const taskId = req.headers['task-id'] as string; form.on('progress', (bytesReceived, bytesExpected) => { const progress = (bytesReceived / bytesExpected) * 100; console.log(`Upload progress: ${progress.toFixed(2)}%`); @@ -37,7 +35,7 @@ router.post('/api/micro-app/upload', async (req, res) => { progress: progress.toFixed(2), message: `Upload progress: ${progress.toFixed(2)}%`, }; - clients.get(taskId)?.client?.write?.(`${JSON.stringify(data)}\n`); + writeEvents(req, data); }); // 解析上传的文件 form.parse(req, async (err, fields, files) => { diff --git a/src/routes-simple/event.ts b/src/routes-simple/event.ts new file mode 100644 index 0000000..29cfef7 --- /dev/null +++ b/src/routes-simple/event.ts @@ -0,0 +1,22 @@ +import { router, error, checkAuth, clients, getTaskId } 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 tokenUser = await checkAuth(req, res); + if (!tokenUser) return; + const taskId = getTaskId(req); + if (!taskId) { + res.end(error('task-id is required')); + return; + } + // 将客户端连接推送到 clients 数组 + clients.set(taskId, { client: res, tokenUser }); + // 移除客户端连接 + req.on('close', () => { + clients.delete(taskId); + }); +}); diff --git a/src/routes-simple/middleware/auth.ts b/src/routes-simple/middleware/auth.ts index 9e8a361..d027104 100644 --- a/src/routes-simple/middleware/auth.ts +++ b/src/routes-simple/middleware/auth.ts @@ -1,24 +1,25 @@ import { User } from '@/models/user.ts'; import http from 'http'; - +import cookie from 'cookie'; 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 = ''; - const authroization = req.headers?.['authorization'] as string; + let token = (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 (authroization) { - // return resNoPermission(); - token = authroization.split(' ')[1]; - } else if (url.searchParams.get('token')) { + if (!token) { token = url.searchParams.get('token') || ''; - } else { + } + if (!token) { + const parsedCookies = cookie.parse(req.headers.cookie || ''); + token = parsedCookies.token || ''; + } + if (!token) { return resNoPermission(); } let tokenUser; diff --git a/src/routes-simple/router.ts b/src/routes-simple/router.ts index 7cb2f61..7c5d637 100644 --- a/src/routes-simple/router.ts +++ b/src/routes-simple/router.ts @@ -1,3 +1,31 @@ -import { SimpleRouter } from '@kevisual/router/simple'; +import { router } from '@/app.ts'; +import http from 'http'; import { useContextKey } from '@kevisual/use-config/context'; -export const router = useContextKey('router', () => new SimpleRouter()); +import { checkAuth, error } from './middleware/auth.ts'; +export { router, checkAuth, error }; + +/** + * 事件客户端 + */ +const eventClientsInit = () => { + const clients = new Map(); + return clients; +}; +export const clients = useContextKey('event-clients', () => eventClientsInit()); +/** + * 获取 task-id + * @param req + * @returns + */ +export const getTaskId = (req: http.IncomingMessage) => { + return req.headers['task-id'] as string; +}; +/** + * 写入事件 + * @param req + * @param data + */ +export const writeEvents = (req: http.IncomingMessage, data: any) => { + const taskId = getTaskId(req); + taskId && clients.get(taskId)?.client?.write?.(`${JSON.stringify(data)}\n`); +}; diff --git a/src/routes-simple/upload.ts b/src/routes-simple/upload.ts index 19ee692..462d84b 100644 --- a/src/routes-simple/upload.ts +++ b/src/routes-simple/upload.ts @@ -9,7 +9,7 @@ import { bucketName } from '@/modules/minio.ts'; import { getContentType } from '@/utils/get-content-type.ts'; import { User } from '@/models/user.ts'; import { getContainerById } from '@/routes/container/module/get-container-file.ts'; -import { router } from './router.ts'; +import { router, error, checkAuth, clients, writeEvents } from './router.ts'; import './index.ts'; const filePath = useFileStore('upload', { needExists: true }); @@ -21,29 +21,6 @@ const cacheFilePath = useFileStore('cache-file', { needExists: true }); // -F "description=This is a test upload" \ // -F "username=testuser" -export const clients = new Map(); - -const error = (msg: string, code = 500) => { - return JSON.stringify({ code, message: msg }); -}; -const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse) => { - const authroization = req.headers?.['authorization'] as string; - if (!authroization) { - res.statusCode = 401; - res.end(error('Invalid authorization')); - return { tokenUser: null, token: null }; - } - const token = authroization.split(' ')[1]; - let tokenUser; - try { - tokenUser = await User.verifyToken(token); - } catch (e) { - res.statusCode = 401; - res.end(error('Invalid token')); - return { tokenUser: null, token: null }; - } - return { tokenUser, token }; -}; router.get('/api/app/upload', async (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Upload API is ready'); @@ -59,6 +36,16 @@ router.post('/api/upload', async (req, res) => { uploadDir: filePath, // 上传文件存储目录 allowEmptyFiles: true, // 允许空文件 }); + form.on('progress', (bytesReceived, bytesExpected) => { + 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); + }); + // 解析上传的文件 form.parse(req, async (err, fields, files) => { if (err) { @@ -101,7 +88,6 @@ router.post('/api/app/upload', async (req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); const { tokenUser, token } = await checkAuth(req, res); if (!tokenUser) return; - // // 使用 formidable 解析 multipart/form-data const form = new IncomingForm({ multiples: true, // 支持多文件上传 @@ -119,8 +105,7 @@ router.post('/api/app/upload', async (req, res) => { progress: progress.toFixed(2), message: `Upload progress: ${progress.toFixed(2)}%`, }; - // 向所有连接的客户端推送进度信息 - clients.forEach((client) => client.write(`${JSON.stringify(data)}\n`)); + writeEvents(req, data); }); // 解析上传的文件 form.parse(req, async (err, fields, files) => { @@ -225,24 +210,7 @@ router.post('/api/app/upload', async (req, res) => { res.end(JSON.stringify(data)); }); }); -router.get('/api/events', async (req, res) => { - if (req.url === '/api/events') { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - }); - const tokenUser = await checkAuth(req, res); - if (!tokenUser) return; - const taskId = req.headers['task-id'] as string; - // 将客户端连接推送到 clients 数组 - clients.set(taskId, { client: res, tokenUser }); - // 移除客户端连接 - req.on('close', () => { - clients.delete(taskId); - }); - } -}); + router.get('/api/container/file/:id', async (req, res) => { const id = req.params.id; if (!id) { diff --git a/src/routes/config/index.ts b/src/routes/config/index.ts new file mode 100644 index 0000000..ecc82f0 --- /dev/null +++ b/src/routes/config/index.ts @@ -0,0 +1,2 @@ +import './list.ts'; +import './upload-config.ts'; diff --git a/src/routes/config/list.ts b/src/routes/config/list.ts new file mode 100644 index 0000000..b7c7f28 --- /dev/null +++ b/src/routes/config/list.ts @@ -0,0 +1,21 @@ +import { app } from '@/app.ts'; +import { ConfigModel } from './models/model.ts'; +app + .route({ + path: 'config', + key: 'list', + middleware: ['auth'], + }) + .define(async (ctx) => { + const { id } = ctx.state.tokenUser; + const config = await ConfigModel.findAll({ + where: { + uid: id, + }, + }); + ctx.body = { + list: config, + }; + }) + .addTo(app); + diff --git a/src/routes/config/models/model.ts b/src/routes/config/models/model.ts new file mode 100644 index 0000000..0b99d4b --- /dev/null +++ b/src/routes/config/models/model.ts @@ -0,0 +1,140 @@ +import { useContextKey } from '@kevisual/use-config/context'; +import { sequelize } from '../../../modules/sequelize.ts'; +import { DataTypes, Model } from 'sequelize'; + +export interface ConfigData { + key?: string; + version?: string; +} + +export type Config = Partial>; + +/** + * 用户配置 + */ +export class ConfigModel extends Model { + declare id: string; + declare title: string; + declare description: string; + declare tags: string[]; + declare key: string; + declare data: ConfigData; // files + declare uid: string; + /** + * 获取用户配置 + * @param key 配置key + * @param opts 配置选项 + * @param opts.uid 用户id + * @param opts.defaultData 默认数据 + * @returns 配置 + */ + static async getConfig(key: string, opts: { uid: string; defaultData?: any }) { + const [config, isNew] = await ConfigModel.findOrCreate({ + where: { key, uid: opts.uid }, + defaults: { + key, + title: key, + uid: opts.uid, + data: opts?.defaultData || {}, + }, + }); + return { + config: config, + isNew, + }; + } + static async setConfig(key: string, opts: { uid: string; data: any }) { + let config = await ConfigModel.findOne({ + where: { key, uid: opts.uid }, + }); + if (config) { + config.data = { ...config.data, ...opts.data }; + await config.save(); + } else { + config = await ConfigModel.create({ + title: key, + key, + uid: opts.uid, + data: opts.data, + }); + } + return config; + } + /** + * 获取上传配置 + * @param key 配置key + * @param opts 配置选项 + * @param opts.uid 用户id + * @returns 配置 + */ + static async getUploadConfig(opts: { uid: string }) { + const defaultConfig = { + key: 'upload', + type: 'upload', + version: '1.0.0', + }; + const config = await ConfigModel.getConfig('upload', { + uid: opts.uid, + defaultData: defaultConfig, + }); + const data = config.config.data; + const prefix = `/${data.key}/${data.version}`; + return { + config: config.config, + isNew: config.isNew, + prefix, + }; + } + static async setUploadConfig(opts: { uid: string; data: any }) { + const config = await ConfigModel.setConfig('upload', { + uid: opts.uid, + data: opts.data, + }); + 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: [], + }, + 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); diff --git a/src/routes/config/upload-config.ts b/src/routes/config/upload-config.ts new file mode 100644 index 0000000..4e0dc60 --- /dev/null +++ b/src/routes/config/upload-config.ts @@ -0,0 +1,34 @@ +import { app } from '../../app.ts'; +import { ConfigModel } from './models/model.ts'; + +app + .route({ + path: 'config', + key: 'getUploadConfig', + middleware: ['auth'], + }) + .define(async (ctx) => { + const { id } = ctx.state.tokenUser; + const config = await ConfigModel.getUploadConfig({ + uid: id, + }); + ctx.body = config; + }) + .addTo(app); + +app + .route({ + path: 'config', + key: 'setUploadConfig', + middleware: ['auth'], + }) + .define(async (ctx) => { + const { id } = ctx.state.tokenUser; + const data = ctx.query.data || {}; + const config = await ConfigModel.setUploadConfig({ + uid: id, + data, + }); + ctx.body = config; + }) + .addTo(app); diff --git a/src/routes/index.ts b/src/routes/index.ts index 2eee2aa..c9844c1 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -15,3 +15,5 @@ import './file/index.ts'; // import './packages/index.ts'; import './micro-app/index.ts'; + +import './config/index.ts'; diff --git a/src/routes/resource/list.ts b/src/routes/resource/list.ts index 30e8396..388cfae 100644 --- a/src/routes/resource/list.ts +++ b/src/routes/resource/list.ts @@ -1,6 +1,5 @@ -import { ResourceData, ResourceModel } from './models/index.ts'; +import { ResourceModel } from './models/index.ts'; import { app } from '../../app.ts'; -import { CustomError } from '@kevisual/router'; app .route({ @@ -29,11 +28,11 @@ app .define(async (ctx) => { const id = ctx.query.id; if (!id) { - throw new CustomError('id is required'); + ctx.throw('id is required'); } const rm = await ResourceModel.findByPk(id); if (!rm) { - throw new CustomError('resource not found'); + ctx.throw('resource not found'); } ctx.body = rm; return ctx; @@ -61,15 +60,16 @@ app .route({ path: 'resource', key: 'delete', + middleware: ['auth'], }) .define(async (ctx) => { const id = ctx.query.id; if (!id) { - throw new CustomError('id is required'); + ctx.throw('id is required'); } const resource = await ResourceModel.findByPk(id); if (!resource) { - throw new CustomError('resource not found'); + ctx.throw('resource not found'); } await resource.destroy(); ctx.body = 'success';