diff --git a/.gitmodules b/.gitmodules index 0b5aae4..e78a78d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "submodules/oss"] path = submodules/oss url = git@git.xiongxiao.me:kevisual/kevisual-oss.git +[submodule "submodules/pay-center-code"] + path = submodules/pay-center-code + url = git@git.xiongxiao.me:kevisual/pay-center-code.git diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4643cc..9c5f4d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -301,6 +301,18 @@ importers: specifier: ^8.4.0 version: 8.4.0(tsx@4.19.3)(typescript@5.8.2) + submodules/pay-center-code: + devDependencies: + '@kevisual/router': + specifier: 0.0.10-beta.1 + version: 0.0.10-beta.1 + '@kevisual/use-config': + specifier: ^1.0.10 + version: 1.0.10(dotenv@16.4.7) + tsup: + specifier: ^8.4.0 + version: 8.4.0(tsx@4.19.3)(typescript@5.8.2) + submodules/permission: devDependencies: tsup: @@ -510,6 +522,9 @@ packages: '@kevisual/use-config': ^1.0.5 pm2: ^5.4.3 + '@kevisual/router@0.0.10-beta.1': + resolution: {integrity: sha512-jl3f6HMdEd0B/6y14w437NatvpOKQ7Gkkr9vFNXvJ3tnYk7ozwjtavLSP3k4MWr5Er9SCT0KBX7+FjnvslCsSw==} + '@kevisual/router@0.0.9': resolution: {integrity: sha512-qPyC2GVJ7iOIdJCCKNDsWMAKOQeSJW9HBpL5ZWKHTbi+t4jJBGTzIlXmjKeMHRd0lr/Qq1imQvlkSh4hlrbodA==} @@ -2739,6 +2754,18 @@ packages: utf-8-validate: optional: true + ws@8.18.1: + resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} @@ -2909,6 +2936,15 @@ snapshots: '@kevisual/use-config': 1.0.10(dotenv@16.4.7) pm2: 6.0.5 + '@kevisual/router@0.0.10-beta.1': + dependencies: + path-to-regexp: 8.2.0 + selfsigned: 2.4.1 + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@kevisual/router@0.0.9': dependencies: path-to-regexp: 8.2.0 @@ -5343,6 +5379,8 @@ snapshots: ws@8.18.0: {} + ws@8.18.1: {} + xml2js@0.5.0: dependencies: sax: 1.4.1 diff --git a/src/routes/app-manager/domain.ts b/src/routes/app-manager/domain/domain-self.ts similarity index 93% rename from src/routes/app-manager/domain.ts rename to src/routes/app-manager/domain/domain-self.ts index d601a5a..484a3f1 100644 --- a/src/routes/app-manager/domain.ts +++ b/src/routes/app-manager/domain/domain-self.ts @@ -1,6 +1,6 @@ import { app } from '@/app.ts'; -import { AppModel } from './module/app.ts'; -import { AppDomainModel } from './module/app-domain.ts'; +import { AppModel } from '../module/app.ts'; +import { AppDomainModel } from '../module/app-domain.ts'; app .route({ @@ -12,7 +12,7 @@ app // const query = { // } const domainInfo = await AppDomainModel.findOne({ where: { domain } }); - if (!domainInfo) { + if (!domainInfo || !domainInfo.appId) { ctx.throw(404, 'app not found'); } const app = await AppModel.findByPk(domainInfo.appId); diff --git a/src/routes/app-manager/domain/index.ts b/src/routes/app-manager/domain/index.ts new file mode 100644 index 0000000..e3f7039 --- /dev/null +++ b/src/routes/app-manager/domain/index.ts @@ -0,0 +1,2 @@ +import './domain-self.ts'; +import './manager.ts'; diff --git a/src/routes/app-manager/domain/manager.ts b/src/routes/app-manager/domain/manager.ts new file mode 100644 index 0000000..ef45159 --- /dev/null +++ b/src/routes/app-manager/domain/manager.ts @@ -0,0 +1,125 @@ +import { app } from '@/app.ts'; +import { AppDomainModel } from '../module/app-domain.ts'; +import { AppModel } from '../module/app.ts'; +import { CustomError } from '@kevisual/router'; + +app + .route({ + path: 'app.domain.manager', + key: 'list', + middleware: ['auth-admin'], + }) + .define(async (ctx) => { + const { page = 1, pageSize = 999 } = ctx.query.data || {}; + const { count, rows } = await AppDomainModel.findAndCountAll({ + offset: (page - 1) * pageSize, + limit: pageSize, + }); + ctx.body = { count, list: rows, pagination: { page, pageSize } }; + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'app.domain.manager', + key: 'update', + middleware: ['auth-admin'], + }) + .define(async (ctx) => { + const { domain, data, id, ...rest } = ctx.query.data || {}; + if (!domain) { + ctx.throw(400, 'domain is required'); + } + let domainInfo: AppDomainModel; + if (id) { + domainInfo = await AppDomainModel.findByPk(id); + } else { + domainInfo = await AppDomainModel.create({ domain }); + } + const checkAppId = async () => { + const isUUID = (id: string) => { + return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id); + }; + if (rest.appId) { + if (!isUUID(rest.appId)) { + ctx.throw(400, 'appId is not valid'); + } + const appInfo = await AppModel.findByPk(rest.appId); + if (!appInfo) { + ctx.throw(400, 'appId is not exist'); + } + } + }; + try { + if (!domainInfo) { + domainInfo = await AppDomainModel.create({ domain, data: {}, ...rest }); + await checkAppId(); + } else { + if (rest.status && domainInfo.status !== rest.status) { + await domainInfo.clearCache(); + } + await checkAppId(); + await domainInfo.update({ + domain, + data: { + ...domainInfo.data, + ...data, + }, + ...rest, + }); + } + ctx.body = domainInfo; + } catch (error) { + if (error.code) { + ctx.throw(error.code, error.message); + } + console.error(error); + ctx.throw(500, 'update domain failed, please check the data'); + } + + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'app.domain.manager', + key: 'delete', + middleware: ['auth-admin'], + }) + .define(async (ctx) => { + const { id, domain } = ctx.query.data || {}; + if (!id && !domain) { + ctx.throw(400, 'id or domain is required'); + } + if (id) { + await AppDomainModel.destroy({ where: { id }, force: true }); + } else { + await AppDomainModel.destroy({ where: { domain }, force: true }); + } + + ctx.body = { message: 'delete domain success' }; + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'app.domain.manager', + key: 'get', + middleware: ['auth-admin'], + }) + .define(async (ctx) => { + const { id, domain } = ctx.query.data || {}; + if (!id && !domain) { + ctx.throw(400, 'id or domain is required'); + } + const domainInfo = await AppDomainModel.findOne({ where: { id } }); + if (!domainInfo) { + ctx.throw(404, 'domain not found'); + } + ctx.body = domainInfo; + return ctx; + }) + .addTo(app); diff --git a/src/routes/app-manager/index.ts b/src/routes/app-manager/index.ts index 50d0a47..8d74b40 100644 --- a/src/routes/app-manager/index.ts +++ b/src/routes/app-manager/index.ts @@ -2,6 +2,6 @@ import './list.ts'; import './user-app.ts'; import './public/index.ts'; -import './domain.ts'; +import './domain/index.ts'; export * from './module/index.ts'; diff --git a/src/routes/app-manager/module/app-domain.ts b/src/routes/app-manager/module/app-domain.ts index 8532cf0..375f548 100644 --- a/src/routes/app-manager/module/app-domain.ts +++ b/src/routes/app-manager/module/app-domain.ts @@ -1,6 +1,7 @@ import { sequelize } from '../../../modules/sequelize.ts'; import { DataTypes, Model } from 'sequelize'; export type DomainList = Partial>; +import { redis } from '../../../modules/redis.ts'; // 审核,通过,驳回 const appDomainStatus = ['audit', 'auditReject', 'auditPending', 'running', 'stop'] as const; @@ -16,6 +17,7 @@ export class AppDomainModel extends Model { // 状态, declare status: AppDomainStatus; declare uid: string; + declare data: Record; declare createdAt: Date; declare updatedAt: Date; @@ -28,6 +30,18 @@ export class AppDomainModel extends Model { // 原本是审核状态,不能修改。 return false; } + async clearCache() { + // 清除缓存 + const cacheKey = `domain:${this.domain}`; + const checkHas = async () => { + const has = await redis.get(cacheKey); + return has; + }; + const has = await checkHas(); + if (has) { + await redis.set(cacheKey, '', 'EX', 1); + } + } } AppDomainModel.init( @@ -43,13 +57,22 @@ AppDomainModel.init( allowNull: false, unique: true, }, - appId: { + data: { + type: DataTypes.JSONB, + allowNull: true, + }, + status: { type: DataTypes.STRING, allowNull: false, + defaultValue: 'running', + }, + appId: { + type: DataTypes.STRING, + allowNull: true, }, uid: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, }, { diff --git a/src/routes/file/list.ts b/src/routes/file/list.ts index 5b87aa1..82d9558 100644 --- a/src/routes/file/list.ts +++ b/src/routes/file/list.ts @@ -1,5 +1,5 @@ import { app } from '@/app.ts'; -import { getFileStat, getMinioList, deleteFile, updateFileStat } from './module/get-minio-list.ts'; +import { getFileStat, getMinioList, deleteFile, updateFileStat, deleteFiles } from './module/get-minio-list.ts'; import path from 'path'; import { CustomError } from '@kevisual/router'; import { get } from 'http'; @@ -147,3 +147,37 @@ app return ctx; }) .addTo(app); + +app + .route({ + path: 'file', + key: 'delete-all', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + let directory = ctx.query.data?.directory as string; + if (!directory) { + ctx.throw(400, 'directory is required'); + } + if (directory.startsWith('/')) { + ctx.throw(400, 'directory is invalid, cannot start with /'); + } + if (directory.endsWith('/')) { + ctx.throw(400, 'directory is invalid, cannot end with /'); + } + const prefix = tokenUser.username + '/' + directory + '/'; + const list = await getMinioList({ prefix, recursive: true }); + if (list.length === 0) { + ctx.throw(400, 'directory is empty'); + } + const res = await deleteFiles(list.map((item) => item.name)); + if (!res) { + ctx.throw(500, 'delete all failed'); + } + ctx.body = { + deleted: list.length, + message: 'delete all success', + }; + }) + .addTo(app); diff --git a/src/routes/file/module/get-minio-list.ts b/src/routes/file/module/get-minio-list.ts index 625e0df..7a9f901 100644 --- a/src/routes/file/module/get-minio-list.ts +++ b/src/routes/file/module/get-minio-list.ts @@ -16,10 +16,10 @@ export type MinioDirectory = { size: number; }; export type MinioList = (MinioFile | MinioDirectory)[]; -export const getMinioList = async (opts: MinioListOpt): Promise => { +export const getMinioList = async (opts: MinioListOpt): Promise => { const prefix = opts.prefix; const recursive = opts.recursive ?? false; - return await new Promise((resolve, reject) => { + const res = await new Promise((resolve, reject) => { let res: any[] = []; let hasError = false; minioClient @@ -40,6 +40,7 @@ export const getMinioList = async (opts: MinioListOpt): Promise => { } }); }); + return res as IS_FILE extends true ? MinioFile[] : MinioDirectory[]; }; export const getFileStat = async (prefix: string, isFile?: boolean): Promise => { try { diff --git a/submodules/pay-center-code b/submodules/pay-center-code new file mode 160000 index 0000000..951280f --- /dev/null +++ b/submodules/pay-center-code @@ -0,0 +1 @@ +Subproject commit 951280f0975f9c46a465d45919250fd2d49503fd