Refactoring the backend

This commit is contained in:
jiangrui
2025-03-10 18:33:47 +08:00
parent 755a424530
commit a78ea7e5bd
36 changed files with 974 additions and 474 deletions

View File

@@ -15,12 +15,14 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"inversify": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"rss-parser": "^3.13.0",
"sequelize": "^6.37.5",
"socket.io": "^4.8.1",
"sqlite3": "^5.1.7",
"tunnel": "^0.0.6"
"tunnel": "^0.0.6",
"winston": "^3.17.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",

View File

@@ -1,136 +1,46 @@
// filepath: /d:/code/CloudDiskDown/backend/src/app.ts
import "./types/express";
import express, { Application } from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import { QueryTypes } from "sequelize";
// 路由和中间件导入
import express from "express";
import { container } from "./core/container";
import { TYPES } from "./core/types";
import { DatabaseService } from "./services/DatabaseService";
import { setupMiddlewares } from "./middleware";
import routes from "./routes/api";
import { errorHandler } from "./middleware/errorHandler";
import { authMiddleware } from "./middleware/auth";
// 数据库和服务相关
import sequelize from "./config/database";
import GlobalSetting from "./models/GlobalSetting";
import Searcher from "./services/Searcher";
// 常量配置
const PUBLIC_ROUTES = ["/user/login", "/user/register"];
const IMAGE_PATH = "tele-images";
const DEFAULT_PORT = 8009;
// 全局设置默认值
const DEFAULT_GLOBAL_SETTINGS = {
httpProxyHost: "127.0.0.1",
httpProxyPort: 7890,
isProxyEnabled: false,
CommonUserCode: 9527,
AdminUserCode: 230713,
};
import { logger } from "./utils/logger";
class App {
private app: Application;
private app = express();
private databaseService = container.get<DatabaseService>(TYPES.DatabaseService);
constructor() {
this.app = express();
this.setupMiddlewares();
this.setupRoutes();
this.setupErrorHandling();
this.setupExpress();
}
private setupMiddlewares(): void {
// CORS 配置
this.app.use(
cors({
origin: "*",
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
})
);
private setupExpress(): void {
// 设置中间件
setupMiddlewares(this.app);
this.app.use(cookieParser());
this.app.use(express.json());
// 身份验证中间件
this.app.use((req, res, next) => {
if (PUBLIC_ROUTES.includes(req.path) || req.path.includes(IMAGE_PATH)) {
return next();
}
authMiddleware(req, res, next);
});
}
private setupRoutes(): void {
// 设置路由
this.app.use("/", routes);
}
private setupErrorHandling(): void {
this.app.use(errorHandler);
}
private async initializeGlobalSettings(): Promise<void> {
try {
const settings = await GlobalSetting.findOne();
if (!settings) {
await GlobalSetting.create(DEFAULT_GLOBAL_SETTINGS);
console.log("✅ Global settings initialized with default values.");
}
await Searcher.updateAxiosInstance();
} catch (error) {
console.error("❌ Failed to initialize global settings:", error);
throw error;
}
}
private async cleanupBackupTables(): Promise<void> {
try {
// 查询所有以 '_backup' 结尾的备份表
const backupTables = await sequelize.query<{ name: string }>(
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%\\_backup%' ESCAPE '\\'",
{ type: QueryTypes.SELECT }
);
// 逐个删除备份表
for (const table of backupTables) {
if (table?.name) {
await sequelize.query(`DROP TABLE IF EXISTS ${table.name}`);
console.log(`✅ Cleaned up backup table: ${table.name}`);
}
}
} catch (error) {
console.error("❌ Failed to cleanup backup tables:", error);
throw error;
}
}
public async start(): Promise<void> {
try {
// 数据库初始化流程
await sequelize.query("PRAGMA foreign_keys = OFF");
console.log("📝 Foreign keys disabled for initialization...");
await this.cleanupBackupTables();
console.log("🧹 Backup tables cleaned up");
await sequelize.sync({ alter: true });
console.log("📚 Database schema synchronized");
await sequelize.query("PRAGMA foreign_keys = ON");
console.log("🔐 Foreign keys re-enabled");
// 初始化数据库
await this.databaseService.initialize();
logger.info("数据库初始化成功");
// 启动服务器
const port = process.env.PORT || DEFAULT_PORT;
this.app.listen(port, async () => {
await this.initializeGlobalSettings();
console.log(`
🚀 Server is running on port ${port}
🔧 Environment: ${process.env.NODE_ENV || "development"}
const port = process.env.PORT || 8009;
this.app.listen(port, () => {
logger.info(`
🚀 服务器启动成功
🌍 监听端口: ${port}
🔧 运行环境: ${process.env.NODE_ENV || "development"}
`);
});
} catch (error) {
console.error("❌ Failed to start server:", error);
logger.error("服务器启动失败:", error);
process.exit(1);
}
}
@@ -139,7 +49,7 @@ class App {
// 创建并启动应用
const application = new App();
application.start().catch((error) => {
console.error("❌ Application failed to start:", error);
logger.error("应用程序启动失败:", error);
process.exit(1);
});

View File

@@ -25,6 +25,18 @@ interface Config {
channels: Channel[];
};
cloudPatterns: CloudPatterns;
app: {
port: number;
env: string;
};
database: {
type: string;
path: string;
};
jwt: {
secret: string;
expiresIn: string;
};
}
// 从环境变量读取频道配置
@@ -56,13 +68,24 @@ const getTeleChannels = (): Channel[] => {
};
export const config: Config = {
app: {
port: parseInt(process.env.PORT || "8009"),
env: process.env.NODE_ENV || "development",
},
database: {
type: "sqlite",
path: "./data/database.sqlite",
},
jwt: {
secret: process.env.JWT_SECRET || "your-secret-key",
expiresIn: "6h",
},
jwtSecret: process.env.JWT_SECRET || "uV7Y$k92#LkF^q1b!",
telegram: {
baseUrl: process.env.TELEGRAM_BASE_URL || "https://t.me/s",
channels: getTeleChannels(),
},
cloudPatterns: {
baiduPan: /https?:\/\/(?:pan|yun)\.baidu\.com\/[^\s<>"]+/g,
tianyi: /https?:\/\/cloud\.189\.cn\/[^\s<>"]+/g,
@@ -70,6 +93,7 @@ export const config: Config = {
// pan115有两个域名 115.com 和 anxia.com 和 115cdn.com
pan115: /https?:\/\/(?:115|anxia|115cdn)\.com\/s\/[^\s<>"]+/g,
// 修改为匹配所有以123开头的域名
// eslint-disable-next-line no-useless-escape
pan123: /https?:\/\/(?:www\.)?123[^\/\s<>"]+\.com\/s\/[^\s<>"]+/g,
quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g,
yidong: /https?:\/\/caiyun\.139\.com\/[^\s<>"]+/g,

View File

@@ -0,0 +1,17 @@
import { Request, Response } from "express";
import { ApiResponse } from "../core/ApiResponse";
export abstract class BaseController {
protected async handleRequest<T>(
req: Request,
res: Response,
action: () => Promise<T>
): Promise<void> {
try {
const result = await action();
res.json(ApiResponse.success(result));
} catch (error: any) {
res.status(500).json(ApiResponse.error(error?.message || "未知错误"));
}
}
}

View File

@@ -2,6 +2,9 @@ import { Request, Response } from "express";
import { Cloud115Service } from "../services/Cloud115Service";
import { sendSuccess, sendError } from "../utils/response";
import UserSetting from "../models/UserSetting";
import { BaseController } from "./BaseController";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
const cloud115 = new Cloud115Service();
const setCookie = async (req: Request): Promise<void> => {
@@ -16,18 +19,19 @@ const setCookie = async (req: Request): Promise<void> => {
}
};
export const cloud115Controller = {
@injectable()
export class Cloud115Controller extends BaseController {
constructor(@inject(TYPES.Cloud115Service) private cloud115Service: Cloud115Service) {
super();
}
async getShareInfo(req: Request, res: Response): Promise<void> {
try {
await this.handleRequest(req, res, async () => {
const { shareCode, receiveCode } = req.query;
await setCookie(req);
const result = await cloud115.getShareInfo(shareCode as string, receiveCode as string);
sendSuccess(res, result);
} catch (error) {
console.log(error);
sendError(res, { message: (error as Error).message || "获取分享信息失败" });
}
},
await this.cloud115Service.setCookie(req);
return await this.cloud115Service.getShareInfo(shareCode as string, receiveCode as string);
});
}
async getFolderList(req: Request, res: Response): Promise<void> {
try {
@@ -38,7 +42,7 @@ export const cloud115Controller = {
} catch (error) {
sendError(res, { message: (error as Error).message || "获取目录列表失败" });
}
},
}
async saveFile(req: Request, res: Response): Promise<void> {
try {
@@ -54,7 +58,7 @@ export const cloud115Controller = {
} catch (error) {
sendError(res, { message: (error as Error).message || "保存文件失败" });
}
},
};
}
}
export const Cloud115ServiceInstance = cloud115;

View File

@@ -1,22 +1,25 @@
import { Request, Response } from "express";
import DoubanService from "../services/DoubanService";
import { sendSuccess, sendError } from "../utils/response";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { DoubanService } from "../services/DoubanService";
import { BaseController } from "./BaseController";
const doubanService = new DoubanService();
@injectable()
export class DoubanController extends BaseController {
constructor(@inject(TYPES.DoubanService) private doubanService: DoubanService) {
super();
}
export const doubanController = {
async getDoubanHotList(req: Request, res: Response): Promise<void> {
try {
await this.handleRequest(req, res, async () => {
const { type = "movie", tag = "热门", page_limit = "50", page_start = "0" } = req.query;
const result = await doubanService.getHotList({
const result = await this.doubanService.getHotList({
type: type as string,
tag: tag as string,
page_limit: page_limit as string,
page_start: page_start as string,
});
sendSuccess(res, result);
} catch (error) {
sendError(res, { message: "获取热门列表失败" });
}
},
};
return result;
});
}
}

View File

@@ -1,52 +1,42 @@
import { Request, Response } from "express";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { QuarkService } from "../services/QuarkService";
import { sendSuccess, sendError } from "../utils/response";
import UserSetting from "../models/UserSetting";
import { BaseController } from "./BaseController";
import { sendSuccess } from "../utils/response";
const quark = new QuarkService();
const setCookie = async (req: Request): Promise<void> => {
const userId = req.user?.userId;
const userSetting = await UserSetting.findOne({
where: { userId },
});
if (userSetting && userSetting.dataValues.quarkCookie) {
quark.setCookie(userSetting.dataValues.quarkCookie);
} else {
throw new Error("请先设置夸克网盘cookie");
@injectable()
export class QuarkController extends BaseController {
constructor(@inject(TYPES.QuarkService) private quarkService: QuarkService) {
super();
}
};
export const quarkController = {
async getShareInfo(req: Request, res: Response): Promise<void> {
try {
const { pwdId, passcode } = req.query;
await setCookie(req);
const result = await quark.getShareInfo(pwdId as string, passcode as string);
await this.handleRequest(req, res, async () => {
const { shareCode, receiveCode } = req.query;
await this.quarkService.setCookie(req);
const result = await this.quarkService.getShareInfo(
shareCode as string,
receiveCode as string
);
sendSuccess(res, result);
} catch (error) {
sendError(res, { message: "获取分享信息失败" });
}
},
});
}
async getFolderList(req: Request, res: Response): Promise<void> {
try {
await this.handleRequest(req, res, async () => {
const { parentCid } = req.query;
await setCookie(req);
const result = await quark.getFolderList(parentCid as string);
await this.quarkService.setCookie(req);
const result = await this.quarkService.getFolderList(parentCid as string);
sendSuccess(res, result);
} catch (error) {
sendError(res, { message: (error as Error).message || "获取目录列表失败" });
}
},
});
}
async saveFile(req: Request, res: Response): Promise<void> {
try {
await setCookie(req);
const result = await quark.saveSharedFile(req.body);
await this.handleRequest(req, res, async () => {
await this.quarkService.setCookie(req);
const result = await this.quarkService.saveSharedFile(req.body);
sendSuccess(res, result);
} catch (error) {
sendError(res, { message: (error as Error).message || "保存文件失败" });
}
},
};
});
}
}

View File

@@ -1,21 +1,25 @@
import { Request, Response } from "express";
import Searcher from "../services/Searcher";
import { sendSuccess, sendError } from "../utils/response";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { Searcher } from "../services/Searcher";
import { BaseController } from "./BaseController";
import { sendSuccess } from "../utils/response";
@injectable()
export class ResourceController extends BaseController {
constructor(@inject(TYPES.Searcher) private searcher: Searcher) {
super();
}
export const resourceController = {
async search(req: Request, res: Response): Promise<void> {
try {
const { keyword, channelId = "", lastMessageId = "" } = req.query; // Remove `: string` from here
const result = await Searcher.searchAll(
await this.handleRequest(req, res, async () => {
const { keyword, channelId = "", lastMessageId = "" } = req.query;
const result = await this.searcher.searchAll(
keyword as string,
channelId as string,
lastMessageId as string
);
sendSuccess(res, result);
} catch (error) {
sendError(res, {
message: (error as Error).message || "搜索资源失败",
});
}
},
};
});
}
}

View File

@@ -1,59 +1,28 @@
import { Request, Response } from "express";
import { sendSuccess, sendError } from "../utils/response";
import Searcher from "../services/Searcher";
import UserSetting from "../models/UserSetting";
import GlobalSetting from "../models/GlobalSetting";
import { iamgesInstance } from "./teleImages";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { SettingService } from "../services/SettingService";
import { BaseController } from "./BaseController";
@injectable()
export class SettingController extends BaseController {
constructor(@inject(TYPES.SettingService) private settingService: SettingService) {
super();
}
export const settingController = {
async get(req: Request, res: Response): Promise<void> {
try {
const userId = req.user?.userId;
const role = req.user?.role;
if (userId !== null) {
let userSettings = await UserSetting.findOne({ where: { userId } });
if (!userSettings) {
userSettings = {
userId: userId,
cloud115Cookie: "",
quarkCookie: "",
} as UserSetting;
if (userSettings) {
await UserSetting.create(userSettings);
}
}
const globalSetting = await GlobalSetting.findOne();
sendSuccess(res, {
data: {
userSettings,
globalSetting: role === 1 ? globalSetting : null,
},
});
} else {
sendError(res, { message: "用户ID无效" });
}
} catch (error) {
console.log("获取设置失败:" + error);
sendError(res, { message: (error as Error).message || "获取设置失败" });
}
},
await this.handleRequest(req, res, async () => {
const userId = Number(req.user?.userId);
const role = Number(req.user?.role);
return await this.settingService.getSettings(userId, role);
});
}
async save(req: Request, res: Response): Promise<void> {
try {
const userId = req.user?.userId;
const role = req.user?.role;
if (userId !== null) {
const { userSettings, globalSetting } = req.body;
await UserSetting.update(userSettings, { where: { userId } });
if (role === 1 && globalSetting) await GlobalSetting.update(globalSetting, { where: {} });
Searcher.updateAxiosInstance();
iamgesInstance.updateProxyConfig();
sendSuccess(res, {
message: "保存成功",
});
}
} catch (error) {
console.log("保存设置失败:" + error);
sendError(res, { message: (error as Error).message || "保存设置失败" });
}
},
};
await this.handleRequest(req, res, async () => {
const userId = Number(req.user?.userId);
const role = Number(req.user?.role);
return await this.settingService.saveSettings(userId, role, req.body);
});
}
}

View File

@@ -1,78 +1,19 @@
import axios, { AxiosInstance } from "axios";
import e, { Request, Response } from "express";
import tunnel from "tunnel";
import GlobalSetting from "../models/GlobalSetting";
import { GlobalSettingAttributes } from "../models/GlobalSetting";
import { Request, Response } from "express";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { ImageService } from "../services/ImageService";
import { BaseController } from "./BaseController";
export class ImageControll {
private axiosInstance: AxiosInstance | null = null;
private settings: GlobalSetting | null = null;
constructor() {
this.initializeAxiosInstance();
@injectable()
export class ImageController extends BaseController {
constructor(@inject(TYPES.ImageService) private imageService: ImageService) {
super();
}
private async initializeAxiosInstance(): Promise<void> {
try {
this.settings = await GlobalSetting.findOne();
} catch (error) {
console.error("Error fetching global settings:", error);
}
const globalSetting = this.settings?.dataValues || ({} as GlobalSettingAttributes);
this.axiosInstance = axios.create({
timeout: 3000,
httpsAgent: globalSetting.isProxyEnabled
? tunnel.httpsOverHttp({
proxy: {
host: globalSetting.httpProxyHost,
port: globalSetting.httpProxyPort,
headers: {
"Proxy-Authorization": "",
},
},
})
: undefined,
withCredentials: true,
async getImages(req: Request, res: Response): Promise<void> {
await this.handleRequest(req, res, async () => {
const url = req.query.url as string;
return await this.imageService.getImages(url);
});
}
public async updateProxyConfig(): Promise<void> {
try {
this.settings = await GlobalSetting.findOne();
const globalSetting = this.settings?.dataValues || ({} as GlobalSettingAttributes);
if (this.axiosInstance) {
this.axiosInstance.defaults.httpsAgent = globalSetting.isProxyEnabled
? tunnel.httpsOverHttp({
proxy: {
host: globalSetting.httpProxyHost,
port: globalSetting.httpProxyPort,
headers: {
"Proxy-Authorization": "",
},
},
})
: undefined;
}
} catch (error) {
console.error("Error updating proxy config:", error);
}
}
async getImages(req: Request, res: Response, url: string): Promise<void> {
try {
const response = await this.axiosInstance?.get(url, { responseType: "stream" });
res.set("Content-Type", response?.headers["content-type"]);
response?.data.pipe(res);
} catch (error) {
res.status(500).send("Image fetch error");
}
}
}
export const iamgesInstance = new ImageControll();
export const imageControll = {
getImages: async (req: Request, res: Response): Promise<void> => {
const url = req.query.url as string;
iamgesInstance.getImages(req, res, url);
},
};

View File

@@ -1,62 +1,26 @@
import { Request, Response } from "express";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import GlobalSetting from "../models/GlobalSetting";
import User from "../models/User";
import { config } from "../config";
import { sendSuccess, sendError } from "../utils/response";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { UserService } from "../services/UserService";
import { BaseController } from "./BaseController";
@injectable()
export class UserController extends BaseController {
constructor(@inject(TYPES.UserService) private userService: UserService) {
super();
}
const isValidInput = (input: string): boolean => {
// 检查是否包含空格或汉字
const regex = /^[^\s\u4e00-\u9fa5]+$/;
return regex.test(input);
};
export const userController = {
async register(req: Request, res: Response): Promise<void> {
const { username, password, registerCode } = req.body;
const globalSetting = await GlobalSetting.findOne();
const registerCodeList = [
globalSetting?.dataValues.CommonUserCode,
globalSetting?.dataValues.AdminUserCode,
];
if (!registerCode || !registerCodeList.includes(Number(registerCode))) {
return sendError(res, { message: "注册码错误" });
}
// 验证输入
if (!isValidInput(username) || !isValidInput(password)) {
return sendError(res, { message: "用户名、密码或注册码不能包含空格或汉字" });
}
// 检查用户名是否已存在
const existingUser = await User.findOne({ where: { username } });
if (existingUser) {
return sendError(res, { message: "用户名已存在" });
}
const hashedPassword = await bcrypt.hash(password, 10);
try {
const role = registerCodeList.findIndex((x) => x === Number(registerCode));
const user = await User.create({ username, password: hashedPassword, role });
sendSuccess(res, {
data: user,
message: "用户注册成功",
});
} catch (error) {
sendError(res, { message: (error as Error).message || "用户注册失败" });
}
},
await this.handleRequest(req, res, async () => {
const { username, password, code } = req.body;
return await this.userService.register(username, password, code);
});
}
async login(req: Request, res: Response): Promise<void> {
const { username, password } = req.body;
const user = await User.findOne({ where: { username } });
if (!user || !(await bcrypt.compare(password, user.password))) {
return sendError(res, { message: "用户名或密码错误" });
}
const token = jwt.sign({ userId: user.userId, role: user.role }, config.jwtSecret, {
expiresIn: "6h",
await this.handleRequest(req, res, async () => {
const { username, password } = req.body;
return await this.userService.login(username, password);
});
sendSuccess(res, {
data: {
token,
},
});
},
};
}
}

View File

@@ -0,0 +1,21 @@
export class ApiResponse<T> {
success: boolean;
data?: T;
message?: string;
code: number;
private constructor(success: boolean, code: number, data?: T, message?: string) {
this.success = success;
this.code = code;
this.data = data;
this.message = message;
}
static success<T>(data?: T, message = "操作成功"): ApiResponse<T> {
return new ApiResponse(true, 200, data, message);
}
static error(message: string, code = 500): ApiResponse<null> {
return new ApiResponse(false, code, null, message);
}
}

View File

@@ -0,0 +1,19 @@
export class ServiceRegistry {
private static instance: ServiceRegistry;
private services: Map<string, any> = new Map();
static getInstance(): ServiceRegistry {
if (!ServiceRegistry.instance) {
ServiceRegistry.instance = new ServiceRegistry();
}
return ServiceRegistry.instance;
}
register(name: string, service: any): void {
this.services.set(name, service);
}
get<T>(name: string): T {
return this.services.get(name);
}
}

View File

@@ -0,0 +1,16 @@
import { Container } from "inversify";
import { TYPES } from "./types";
import { Cloud115Service } from "../services/Cloud115Service";
import { QuarkService } from "../services/QuarkService";
import { Searcher } from "../services/Searcher";
import { DatabaseService } from "../services/DatabaseService";
const container = new Container();
// 注册服务
container.bind<Cloud115Service>(TYPES.Cloud115Service).to(Cloud115Service);
container.bind<QuarkService>(TYPES.QuarkService).to(QuarkService);
container.bind<Searcher>(TYPES.Searcher).to(Searcher);
container.bind<DatabaseService>(TYPES.DatabaseService).to(DatabaseService);
export { container };

18
backend/src/core/types.ts Normal file
View File

@@ -0,0 +1,18 @@
export const TYPES = {
DatabaseService: Symbol.for("DatabaseService"),
Cloud115Service: Symbol.for("Cloud115Service"),
QuarkService: Symbol.for("QuarkService"),
Searcher: Symbol.for("Searcher"),
DoubanService: Symbol.for("DoubanService"),
ImageService: Symbol.for("ImageService"),
SettingService: Symbol.for("SettingService"),
UserService: Symbol.for("UserService"),
Cloud115Controller: Symbol.for("Cloud115Controller"),
QuarkController: Symbol.for("QuarkController"),
ResourceController: Symbol.for("ResourceController"),
DoubanController: Symbol.for("DoubanController"),
ImageController: Symbol.for("ImageController"),
SettingController: Symbol.for("SettingController"),
UserController: Symbol.for("UserController"),
};

View File

@@ -0,0 +1,12 @@
import {
ShareInfoResponse,
FolderListResponse,
SaveFileParams,
SaveFileResponse,
} from "../types/cloud";
export interface ICloudService {
getShareInfo(shareCode: string, receiveCode?: string): Promise<ShareInfoResponse>;
getFolderList(parentCid?: string): Promise<FolderListResponse>;
saveSharedFile(params: SaveFileParams): Promise<SaveFileResponse>;
}

View File

@@ -0,0 +1,40 @@
import { Container } from "inversify";
import { TYPES } from "./core/types";
// Services
import { DatabaseService } from "./services/DatabaseService";
import { Cloud115Service } from "./services/Cloud115Service";
import { QuarkService } from "./services/QuarkService";
import { Searcher } from "./services/Searcher";
import { DoubanService } from "./services/DoubanService";
import { UserService } from "./services/UserService";
// Controllers
import { Cloud115Controller } from "./controllers/cloud115";
import { QuarkController } from "./controllers/quark";
import { ResourceController } from "./controllers/resource";
import { DoubanController } from "./controllers/douban";
import { ImageController } from "./controllers/teleImages";
import { SettingController } from "./controllers/setting";
import { UserController } from "./controllers/user";
const container = new Container();
// Services
container.bind<DatabaseService>(TYPES.DatabaseService).to(DatabaseService).inSingletonScope();
container.bind<Cloud115Service>(TYPES.Cloud115Service).to(Cloud115Service).inSingletonScope();
container.bind<QuarkService>(TYPES.QuarkService).to(QuarkService).inSingletonScope();
container.bind<Searcher>(TYPES.Searcher).to(Searcher).inSingletonScope();
container.bind<DoubanService>(TYPES.DoubanService).to(DoubanService).inSingletonScope();
container.bind<UserService>(TYPES.UserService).to(UserService).inSingletonScope();
// Controllers
container.bind<Cloud115Controller>(TYPES.Cloud115Controller).to(Cloud115Controller);
container.bind<QuarkController>(TYPES.QuarkController).to(QuarkController);
container.bind<ResourceController>(TYPES.ResourceController).to(ResourceController);
container.bind<DoubanController>(TYPES.DoubanController).to(DoubanController);
container.bind<ImageController>(TYPES.ImageController).to(ImageController);
container.bind<SettingController>(TYPES.SettingController).to(SettingController);
container.bind<UserController>(TYPES.UserController).to(UserController);
export { container };

View File

@@ -0,0 +1,15 @@
import { Request, Response, NextFunction } from "express";
export const cors = () => {
return (req: Request, res: Response, next: NextFunction) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Cookie");
res.header("Access-Control-Allow-Credentials", "true");
if (req.method === "OPTIONS") {
return res.sendStatus(200);
}
next();
};
};

View File

@@ -0,0 +1,14 @@
import { Application } from "express";
import { errorHandler } from "./errorHandler";
import { authMiddleware } from "./auth";
import { requestLogger } from "./requestLogger";
import { rateLimiter } from "./rateLimiter";
import { cors } from "./cors";
export const setupMiddlewares = (app: Application) => {
app.use(cors());
app.use(requestLogger());
app.use(rateLimiter());
app.use(authMiddleware);
app.use(errorHandler);
};

View File

@@ -0,0 +1,27 @@
import { Request, Response, NextFunction } from "express";
const requestCounts = new Map<string, { count: number; timestamp: number }>();
const WINDOW_MS = 60 * 1000; // 1分钟窗口
const MAX_REQUESTS = 60; // 每个IP每分钟最多60个请求
export const rateLimiter = () => {
return (req: Request, res: Response, next: NextFunction) => {
const ip = req.ip || req.socket.remoteAddress || "unknown";
const now = Date.now();
const record = requestCounts.get(ip) || { count: 0, timestamp: now };
if (now - record.timestamp > WINDOW_MS) {
record.count = 0;
record.timestamp = now;
}
record.count++;
requestCounts.set(ip, record);
if (record.count > MAX_REQUESTS) {
return res.status(429).json({ message: "请求过于频繁,请稍后再试" });
}
next();
};
};

View File

@@ -0,0 +1,18 @@
import { Request, Response, NextFunction } from "express";
import { logger } from "../utils/logger";
export const requestLogger = () => {
return (req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
logger.info({
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
});
});
next();
};
};

View File

@@ -1,36 +1,50 @@
import express from "express";
import { cloud115Controller } from "../controllers/cloud115";
import { quarkController } from "../controllers/quark";
import { resourceController } from "../controllers/resource";
import { doubanController } from "../controllers/douban";
import { imageControll } from "../controllers/teleImages";
import settingRoutes from "./setting";
import userRoutes from "./user";
import { Router } from "express";
import { container } from "../inversify.config";
import { TYPES } from "../core/types";
import { Cloud115Controller } from "../controllers/cloud115";
import { QuarkController } from "../controllers/quark";
import { ResourceController } from "../controllers/resource";
import { DoubanController } from "../controllers/douban";
import { ImageController } from "../controllers/teleImages";
import { SettingController } from "../controllers/setting";
import { UserController } from "../controllers/user";
const router = express.Router();
const router = Router();
// 获取控制器实例
const cloud115Controller = container.get<Cloud115Controller>(TYPES.Cloud115Controller);
const quarkController = container.get<QuarkController>(TYPES.QuarkController);
const resourceController = container.get<ResourceController>(TYPES.ResourceController);
const doubanController = container.get<DoubanController>(TYPES.DoubanController);
const imageController = container.get<ImageController>(TYPES.ImageController);
const settingController = container.get<SettingController>(TYPES.SettingController);
const userController = container.get<UserController>(TYPES.UserController);
// 用户相关路由
router.use("/user", userRoutes);
router.post("/user/login", (req, res) => userController.login(req, res));
router.post("/user/register", (req, res) => userController.register(req, res));
router.use("/tele-images", imageControll.getImages);
// 图片相关路由
router.get("/tele-images", (req, res) => imageController.getImages(req, res));
// 设置相关路由
router.use("/setting", settingRoutes);
router.get("/setting/get", (req, res) => settingController.get(req, res));
router.post("/setting/save", (req, res) => settingController.save(req, res));
// 资源搜索
router.get("/search", resourceController.search);
router.get("/search", (req, res) => resourceController.search(req, res));
// 115网盘相关
router.get("/cloud115/share-info", cloud115Controller.getShareInfo);
router.get("/cloud115/folders", cloud115Controller.getFolderList);
router.post("/cloud115/save", cloud115Controller.saveFile);
router.get("/cloud115/share-info", (req, res) => cloud115Controller.getShareInfo(req, res));
router.get("/cloud115/folders", (req, res) => cloud115Controller.getFolderList(req, res));
router.post("/cloud115/save", (req, res) => cloud115Controller.saveFile(req, res));
// 夸克网盘相关
router.get("/quark/share-info", quarkController.getShareInfo);
router.get("/quark/folders", quarkController.getFolderList);
router.post("/quark/save", quarkController.saveFile);
router.get("/quark/share-info", (req, res) => quarkController.getShareInfo(req, res));
router.get("/quark/folders", (req, res) => quarkController.getFolderList(req, res));
router.post("/quark/save", (req, res) => quarkController.saveFile(req, res));
// 获取豆瓣热门列表
router.get("/douban/hot", doubanController.getDoubanHotList);
router.get("/douban/hot", (req, res) => doubanController.getDoubanHotList(req, res));
export default router;

View File

@@ -1,7 +1,11 @@
import { AxiosHeaders, AxiosInstance } from "axios"; // 导入 AxiosHeaders
import { createAxiosInstance } from "../utils/axiosInstance";
import { Logger } from "../utils/logger";
import { ShareInfoResponse } from "../types/cloud115";
import { injectable } from "inversify";
import { Request } from "express";
import UserSetting from "../models/UserSetting";
import { ICloudService } from "../types/services";
import { logger } from "@/utils/logger";
interface Cloud115ListItem {
cid: string;
@@ -20,11 +24,12 @@ interface Cloud115PathItem {
name: string;
}
export class Cloud115Service {
@injectable()
export class Cloud115Service implements ICloudService {
private api: AxiosInstance;
private cookie: string = "";
constructor(cookie?: string) {
constructor() {
this.api = createAxiosInstance(
"https://webapi.115.com",
AxiosHeaders.from({
@@ -44,19 +49,23 @@ export class Cloud115Service {
"Accept-Language": "zh-CN,zh;q=0.9",
})
);
if (cookie) {
this.setCookie(cookie);
} else {
console.log("请注意:115网盘需要提供cookie进行身份验证");
}
this.api.interceptors.request.use((config) => {
config.headers.cookie = cookie || this.cookie;
config.headers.cookie = this.cookie;
return config;
});
}
public setCookie(cookie: string): void {
this.cookie = cookie;
async setCookie(req: Request): Promise<void> {
const userId = req.user?.userId;
const userSetting = await UserSetting.findOne({
where: { userId },
});
if (userSetting && userSetting.dataValues.cloud115Cookie) {
this.cookie = userSetting.dataValues.cloud115Cookie;
} else {
throw new Error("请先设置115网盘cookie");
}
}
async getShareInfo(shareCode: string, receiveCode = ""): Promise<ShareInfoResponse> {
@@ -114,7 +123,7 @@ export class Cloud115Service {
})),
};
} else {
Logger.error("获取目录列表失败:", response.data.error);
logger.error("获取目录列表失败:", response.data.error);
throw new Error("获取115pan目录列表失败:" + response.data.error);
}
}
@@ -132,14 +141,14 @@ export class Cloud115Service {
file_id: params.fileId,
});
const response = await this.api.post("/share/receive", param.toString());
Logger.info("保存文件:", response.data);
logger.info("保存文件:", response.data);
if (response.data.state) {
return {
message: response.data.error,
data: response.data.data,
};
} else {
Logger.error("保存文件失败:", response.data.error);
logger.error("保存文件失败:", response.data.error);
throw new Error("保存115pan文件失败:" + response.data.error);
}
}

View File

@@ -0,0 +1,38 @@
import { Sequelize, QueryTypes } from "sequelize";
export class DatabaseService {
private sequelize: Sequelize;
constructor() {
this.sequelize = new Sequelize({
dialect: "sqlite",
storage: "./data/database.sqlite",
});
}
async initialize(): Promise<void> {
try {
await this.sequelize.query("PRAGMA foreign_keys = OFF");
await this.cleanupBackupTables();
await this.sequelize.sync({ alter: true });
await this.sequelize.query("PRAGMA foreign_keys = ON");
} catch (error) {
throw new Error(`数据库初始化失败: ${(error as Error).message}`);
}
}
private async cleanupBackupTables(): Promise<void> {
const backupTables = await this.sequelize.query<{ name: string }>(
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%\\_backup%' ESCAPE '\\'",
{ type: QueryTypes.SELECT }
);
for (const table of backupTables) {
if (table?.name) {
await this.sequelize.query(`DROP TABLE IF EXISTS ${table.name}`);
}
}
}
// ... 其他数据库相关方法
}

View File

@@ -10,7 +10,7 @@ interface DoubanSubject {
is_new: boolean;
}
class DoubanService {
export class DoubanService {
private baseUrl: string;
private api: AxiosInstance;
@@ -62,5 +62,3 @@ class DoubanService {
}
}
}
export default DoubanService;

View File

@@ -0,0 +1,42 @@
import { injectable } from "inversify";
import axios, { AxiosInstance } from "axios";
import tunnel from "tunnel";
import GlobalSetting from "../models/GlobalSetting";
import { GlobalSettingAttributes } from "../models/GlobalSetting";
@injectable()
export class ImageService {
private axiosInstance: AxiosInstance | null = null;
constructor() {
this.initializeAxiosInstance();
}
private async initializeAxiosInstance(): Promise<void> {
const settings = await GlobalSetting.findOne();
const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
this.axiosInstance = axios.create({
timeout: 3000,
httpsAgent: globalSetting.isProxyEnabled
? tunnel.httpsOverHttp({
proxy: {
host: globalSetting.httpProxyHost,
port: globalSetting.httpProxyPort,
headers: {
"Proxy-Authorization": "",
},
},
})
: undefined,
withCredentials: true,
});
}
async getImages(url: string): Promise<any> {
if (!this.axiosInstance) {
throw new Error("Axios instance not initialized");
}
return await this.axiosInstance.get(url, { responseType: "stream" });
}
}

View File

@@ -1,6 +1,9 @@
import { AxiosInstance, AxiosHeaders } from "axios";
import { Logger } from "../utils/logger";
import { logger } from "../utils/logger";
import { createAxiosInstance } from "../utils/axiosInstance";
import { injectable } from "inversify";
import { Request } from "express";
import UserSetting from "../models/UserSetting";
interface QuarkShareInfo {
stoken?: string;
@@ -20,11 +23,12 @@ interface QuarkFolderItem {
file_type: number;
}
@injectable()
export class QuarkService {
private api: AxiosInstance;
private cookie: string = "";
constructor(cookie?: string) {
constructor() {
this.api = createAxiosInstance(
"https://drive-h.quark.cn",
AxiosHeaders.from({
@@ -41,19 +45,23 @@ export class QuarkService {
"sec-fetch-site": "same-site",
})
);
if (cookie) {
this.setCookie(cookie);
} else {
console.log("请注意:夸克网盘需要提供cookie进行身份验证");
}
this.api.interceptors.request.use((config) => {
config.headers.cookie = cookie || this.cookie;
config.headers.cookie = this.cookie;
return config;
});
}
public setCookie(cookie: string): void {
this.cookie = cookie;
async setCookie(req: Request): Promise<void> {
const userId = req.user?.userId;
const userSetting = await UserSetting.findOne({
where: { userId },
});
if (userSetting && userSetting.dataValues.quarkCookie) {
this.cookie = userSetting.dataValues.quarkCookie;
} else {
throw new Error("请先设置夸克网盘cookie");
}
}
async getShareInfo(pwdId: string, passcode = ""): Promise<{ data: QuarkShareInfo }> {
@@ -148,7 +156,7 @@ export class QuarkService {
};
} else {
const message = "获取夸克目录列表失败:" + response.data.error;
Logger.error(message);
logger.error(message);
throw new Error(message);
}
}

View File

@@ -4,7 +4,8 @@ import GlobalSetting from "../models/GlobalSetting";
import { GlobalSettingAttributes } from "../models/GlobalSetting";
import * as cheerio from "cheerio";
import { config } from "../config";
import { Logger } from "../utils/logger";
import { logger } from "../utils/logger";
import { injectable } from "inversify";
interface sourceItem {
messageId?: string;
@@ -20,20 +21,20 @@ interface sourceItem {
cloudType?: string;
}
@injectable()
export class Searcher {
private axiosInstance: AxiosInstance | null = null;
private static instance: Searcher;
private api: AxiosInstance;
constructor() {
this.initializeAxiosInstance();
this.initAxiosInstance();
Searcher.instance = this;
}
private async initializeAxiosInstance(isUpdate = false): Promise<void> {
let settings = null;
if (isUpdate) {
settings = await GlobalSetting.findOne();
}
private async initAxiosInstance() {
const settings = await GlobalSetting.findOne();
const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
this.axiosInstance = createAxiosInstance(
this.api = createAxiosInstance(
config.telegram.baseUrl,
AxiosHeaders.from({
accept:
@@ -56,8 +57,11 @@ export class Searcher {
: undefined
);
}
public async updateAxiosInstance() {
await this.initializeAxiosInstance(true);
public static async updateAxiosInstance(): Promise<void> {
if (Searcher.instance) {
await Searcher.instance.initAxiosInstance();
}
}
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
@@ -111,7 +115,7 @@ export class Searcher {
}
});
} catch (error) {
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
logger.error(`搜索频道 ${channel.name} 失败:`, error);
}
});
@@ -125,10 +129,10 @@ export class Searcher {
async searchInWeb(url: string) {
try {
if (!this.axiosInstance) {
if (!this.api) {
throw new Error("Axios instance is not initialized");
}
const response = await this.axiosInstance.get(url);
const response = await this.api.get(url);
const html = response.data;
const $ = cheerio.load(html);
const items: sourceItem[] = [];
@@ -205,7 +209,7 @@ export class Searcher {
});
return { items: items, channelLogo };
} catch (error) {
Logger.error(`搜索错误: ${url}`, error);
logger.error(`搜索错误: ${url}`, error);
return {
items: [],
channelLogo: "",

View File

@@ -0,0 +1,46 @@
import { injectable } from "inversify";
import UserSetting from "../models/UserSetting";
import GlobalSetting from "../models/GlobalSetting";
import { Searcher } from "./Searcher";
@injectable()
export class SettingService {
async getSettings(userId: number | undefined, role: number | undefined) {
if (!userId) {
throw new Error("用户ID无效");
}
let userSettings = await UserSetting.findOne({ where: { userId: userId.toString() } });
if (!userSettings) {
userSettings = await UserSetting.create({
userId: userId.toString(),
cloud115Cookie: "",
quarkCookie: "",
});
}
const globalSetting = await GlobalSetting.findOne();
return {
data: {
userSettings,
globalSetting: role === 1 ? globalSetting : null,
},
};
}
async saveSettings(userId: number | undefined, role: number | undefined, settings: any) {
if (!userId) {
throw new Error("用户ID无效");
}
const { userSettings, globalSetting } = settings;
await UserSetting.update(userSettings, { where: { userId: userId.toString() } });
if (role === 1 && globalSetting) {
await GlobalSetting.update(globalSetting, { where: {} });
}
await Searcher.updateAxiosInstance();
return { message: "保存成功" };
}
}

View File

@@ -0,0 +1,64 @@
import { injectable } from "inversify";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { config } from "../config";
import User from "../models/User";
import GlobalSetting from "../models/GlobalSetting";
@injectable()
export class UserService {
private isValidInput(input: string): boolean {
// 检查是否包含空格或汉字
const regex = /^[^\s\u4e00-\u9fa5]+$/;
return regex.test(input);
}
async register(username: string, password: string, registerCode: string) {
const globalSetting = await GlobalSetting.findOne();
const registerCodeList = [
globalSetting?.dataValues.CommonUserCode,
globalSetting?.dataValues.AdminUserCode,
];
if (!registerCode || !registerCodeList.includes(Number(registerCode))) {
throw new Error("注册码错误");
}
// 验证输入
if (!this.isValidInput(username) || !this.isValidInput(password)) {
throw new Error("用户名、密码或注册码不能包含空格或汉字");
}
// 检查用户名是否已存在
const existingUser = await User.findOne({ where: { username } });
if (existingUser) {
throw new Error("用户名已存在");
}
const hashedPassword = await bcrypt.hash(password, 10);
const role = registerCodeList.findIndex((x) => x === Number(registerCode));
const user = await User.create({ username, password: hashedPassword, role });
return {
data: user,
message: "用户注册成功",
};
}
async login(username: string, password: string) {
const user = await User.findOne({ where: { username } });
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new Error("用户名或密码错误");
}
const token = jwt.sign({ userId: user.userId, role: user.role }, config.jwtSecret, {
expiresIn: "6h",
});
return {
data: {
token,
},
};
}
}

View File

@@ -0,0 +1,27 @@
export interface ShareInfoResponse {
data: {
fileId: string;
fileName: string;
fileSize: number;
}[];
}
export interface FolderListResponse {
data: {
cid: string;
name: string;
path: { cid: string; name: string }[];
}[];
}
export interface SaveFileParams {
shareCode: string;
receiveCode?: string;
fileId: string;
cid?: string;
}
export interface SaveFileResponse {
message: string;
data: unknown;
}

View File

@@ -0,0 +1,15 @@
export interface Config {
app: {
port: number;
env: string;
};
database: {
type: string;
path: string;
};
jwt: {
secret: string;
expiresIn: string;
};
// ... 其他配置类型
}

View File

@@ -0,0 +1,9 @@
import { Request } from "express";
import { ShareInfoResponse } from "./cloud115";
export interface ICloudService {
setCookie(req: Request): Promise<void>;
getShareInfo(shareCode: string, receiveCode?: string): Promise<ShareInfoResponse>;
getFolderList(parentCid?: string): Promise<any>;
saveSharedFile(params: any): Promise<any>;
}

View File

@@ -1,38 +1,21 @@
type LogLevel = "info" | "success" | "warn" | "error";
import winston from "winston";
import { Config } from "../config";
export const Logger = {
info(...args: any[]) {
this.log("info", ...args);
},
const logger = winston.createLogger({
level: Config.app.env === "development" ? "debug" : "info",
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.File({ filename: "logs/error.log", level: "error" }),
new winston.transports.File({ filename: "logs/combined.log" }),
],
});
success(...args: any[]) {
this.log("success", ...args);
},
if (Config.app.env !== "production") {
logger.add(
new winston.transports.Console({
format: winston.format.simple(),
})
);
}
warn(...args: any[]) {
this.log("warn", ...args);
},
error(...args: any[]) {
this.log("error", ...args);
},
log(level: LogLevel, ...args: any[]) {
const timestamp = new Date().toISOString();
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
switch (level) {
case "success":
console.log(prefix, ...args);
break;
case "warn":
console.warn(prefix, ...args);
break;
case "error":
console.error(prefix, ...args);
break;
default:
console.log(prefix, ...args);
}
},
};
export { logger };

View File

@@ -15,7 +15,9 @@
"typeRoots": ["./node_modules/@types", "./src/types"],
"paths": {
"@/*": ["src/*"]
}
},
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]

213
pnpm-lock.yaml generated
View File

@@ -62,6 +62,9 @@ importers:
express:
specifier: ^4.18.3
version: 4.21.2
inversify:
specifier: ^7.1.0
version: 7.1.0(reflect-metadata@0.2.2)
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
@@ -80,6 +83,9 @@ importers:
tunnel:
specifier: ^0.0.6
version: 0.0.6
winston:
specifier: ^3.17.0
version: 3.17.0
devDependencies:
'@types/bcrypt':
specifier: ^5.0.2
@@ -673,6 +679,10 @@ packages:
resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==}
engines: {node: '>=6.9.0'}
'@colors/colors@1.6.0':
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
engines: {node: '>=0.1.90'}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -681,6 +691,9 @@ packages:
resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
engines: {node: '>=10'}
'@dabh/diagnostics@2.0.3':
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
'@element-plus/icons-vue@2.3.1':
resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==}
peerDependencies:
@@ -867,6 +880,25 @@ packages:
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
deprecated: Use @eslint/object-schema instead
'@inversifyjs/common@1.5.0':
resolution: {integrity: sha512-Qj5BELk11AfI2rgZEAaLPmOftmQRLLmoCXgAjmaF0IngQN5vHomVT5ML7DZ3+CA5fgGcEVMcGbUDAun+Rz+oNg==}
'@inversifyjs/container@1.5.4':
resolution: {integrity: sha512-mHAaWjAQb8m6TJksm5EJXW/kPcZFVEc1UKkWv5OnLbwbU0QvxM2UbEsuXzusGVHcrNY4TQp9Uh2wkRY6TN2WJg==}
peerDependencies:
reflect-metadata: ~0.2.2
'@inversifyjs/core@5.0.0':
resolution: {integrity: sha512-axOl+VZFGVA3nAMbs6RuHhQ8HvgO6/tKjlWJk4Nt0rUqed+1ksak4p5yZNtown1Kdm0GV2Oc57qLqqWd943hgA==}
'@inversifyjs/prototype-utils@0.1.0':
resolution: {integrity: sha512-lNz1yyajMRDXBHLvJsYYX81FcmeD15e5Qz1zAZ/3zeYTl+u7ZF/GxNRKJzNOloeMPMtuR8BnvzHA1SZxjR+J9w==}
'@inversifyjs/reflect-metadata-utils@1.1.0':
resolution: {integrity: sha512-jmuAuC3eL1GnFAYfJGJOMKRDL9q1mgzOyrban6zxfM8Yg1FUHsj25h27bW2G7p8X1Amvhg3MLkaOuogszkrofA==}
peerDependencies:
reflect-metadata: 0.2.2
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -1262,6 +1294,9 @@ packages:
'@types/serve-static@1.15.7':
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
'@types/triple-beam@1.3.5':
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -1682,10 +1717,19 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
color-support@1.1.3:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
color@3.2.1:
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
colorspace@1.1.4:
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@@ -1927,6 +1971,9 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
encodeurl@1.0.2:
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
engines: {node: '>= 0.8'}
@@ -2134,6 +2181,9 @@ packages:
picomatch:
optional: true
fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -2163,6 +2213,9 @@ packages:
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
@@ -2414,6 +2467,11 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
inversify@7.1.0:
resolution: {integrity: sha512-f8SlUTgecMZGr/rsFK36PD84/mH0+sp0/P/TuiGo3CcJywmF5kgoXeE2RW5IjaIt1SlqS5c5V9RuQc1+B8mw4Q==}
peerDependencies:
reflect-metadata: ~0.2.2
ip-address@9.0.5:
resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
engines: {node: '>= 12'}
@@ -2429,6 +2487,9 @@ packages:
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
is-async-function@2.1.1:
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
engines: {node: '>= 0.4'}
@@ -2631,6 +2692,9 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
kuler@2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'}
@@ -2702,6 +2766,10 @@ packages:
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
logform@2.7.0:
resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==}
engines: {node: '>= 12.0.0'}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -2961,6 +3029,9 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -3196,6 +3267,9 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
@@ -3292,6 +3366,10 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'}
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -3434,6 +3512,9 @@ packages:
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
@@ -3513,6 +3594,9 @@ packages:
resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==}
engines: {node: '>= 8'}
stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
@@ -3619,6 +3703,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
text-hex@1.0.0:
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@@ -3647,6 +3734,10 @@ packages:
tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
triple-beam@1.4.1:
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
engines: {node: '>= 14.0.0'}
ts-api-utils@1.4.3:
resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
engines: {node: '>=16'}
@@ -3973,6 +4064,14 @@ packages:
wide-align@1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
winston-transport@4.9.0:
resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==}
engines: {node: '>= 12.0.0'}
winston@3.17.0:
resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==}
engines: {node: '>= 12.0.0'}
wkx@0.5.0:
resolution: {integrity: sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==}
@@ -4744,12 +4843,20 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@colors/colors@1.6.0': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@ctrl/tinycolor@3.6.1': {}
'@dabh/diagnostics@2.0.3':
dependencies:
colorspace: 1.1.4
enabled: 2.0.0
kuler: 2.0.0
'@element-plus/icons-vue@2.3.1(vue@3.5.13(typescript@5.8.2))':
dependencies:
vue: 3.5.13(typescript@5.8.2)
@@ -4872,6 +4979,31 @@ snapshots:
'@humanwhocodes/object-schema@2.0.3': {}
'@inversifyjs/common@1.5.0': {}
'@inversifyjs/container@1.5.4(reflect-metadata@0.2.2)':
dependencies:
'@inversifyjs/common': 1.5.0
'@inversifyjs/core': 5.0.0(reflect-metadata@0.2.2)
'@inversifyjs/reflect-metadata-utils': 1.1.0(reflect-metadata@0.2.2)
reflect-metadata: 0.2.2
'@inversifyjs/core@5.0.0(reflect-metadata@0.2.2)':
dependencies:
'@inversifyjs/common': 1.5.0
'@inversifyjs/prototype-utils': 0.1.0
'@inversifyjs/reflect-metadata-utils': 1.1.0(reflect-metadata@0.2.2)
transitivePeerDependencies:
- reflect-metadata
'@inversifyjs/prototype-utils@0.1.0':
dependencies:
'@inversifyjs/common': 1.5.0
'@inversifyjs/reflect-metadata-utils@1.1.0(reflect-metadata@0.2.2)':
dependencies:
reflect-metadata: 0.2.2
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -5221,6 +5353,8 @@ snapshots:
'@types/node': 20.17.23
'@types/send': 0.17.4
'@types/triple-beam@1.3.5': {}
'@types/trusted-types@2.0.7': {}
'@types/tunnel@0.0.7':
@@ -5773,8 +5907,23 @@ snapshots:
color-name@1.1.4: {}
color-string@1.9.1:
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
color-support@1.1.3: {}
color@3.2.1:
dependencies:
color-convert: 1.9.3
color-string: 1.9.1
colorspace@1.1.4:
dependencies:
color: 3.2.1
text-hex: 1.0.0
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@@ -5999,6 +6148,8 @@ snapshots:
emoji-regex@9.2.2: {}
enabled@2.0.0: {}
encodeurl@1.0.2: {}
encodeurl@2.0.0: {}
@@ -6343,6 +6494,8 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fecha@4.2.3: {}
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.2.0
@@ -6382,6 +6535,8 @@ snapshots:
flatted@3.3.3: {}
fn.name@1.1.0: {}
follow-redirects@1.15.9: {}
for-each@0.3.5:
@@ -6658,6 +6813,13 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
inversify@7.1.0(reflect-metadata@0.2.2):
dependencies:
'@inversifyjs/common': 1.5.0
'@inversifyjs/container': 1.5.4(reflect-metadata@0.2.2)
'@inversifyjs/core': 5.0.0(reflect-metadata@0.2.2)
reflect-metadata: 0.2.2
ip-address@9.0.5:
dependencies:
jsbn: 1.1.0
@@ -6674,6 +6836,8 @@ snapshots:
is-arrayish@0.2.1: {}
is-arrayish@0.3.2: {}
is-async-function@2.1.1:
dependencies:
async-function: 1.0.0
@@ -6874,6 +7038,8 @@ snapshots:
dependencies:
json-buffer: 3.0.1
kuler@2.0.0: {}
leven@3.1.0: {}
levn@0.4.1:
@@ -6935,6 +7101,15 @@ snapshots:
lodash@4.17.21: {}
logform@2.7.0:
dependencies:
'@colors/colors': 1.6.0
'@types/triple-beam': 1.3.5
fecha: 4.2.3
ms: 2.1.3
safe-stable-stringify: 2.5.0
triple-beam: 1.4.1
lru-cache@10.4.3: {}
lru-cache@5.1.1:
@@ -7219,6 +7394,10 @@ snapshots:
dependencies:
wrappy: 1.0.2
one-time@1.0.0:
dependencies:
fn.name: 1.1.0
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -7449,6 +7628,8 @@ snapshots:
readdirp@4.1.2: {}
reflect-metadata@0.2.2: {}
reflect.getprototypeof@1.0.10:
dependencies:
call-bind: 1.0.8
@@ -7580,6 +7761,8 @@ snapshots:
es-errors: 1.3.0
is-regex: 1.2.1
safe-stable-stringify@2.5.0: {}
safer-buffer@2.1.2: {}
sass@1.85.1:
@@ -7736,6 +7919,10 @@ snapshots:
once: 1.4.0
simple-concat: 1.0.1
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
simple-update-notifier@2.0.0:
dependencies:
semver: 7.7.1
@@ -7852,6 +8039,8 @@ snapshots:
minipass: 3.3.6
optional: true
stack-trace@0.0.10: {}
statuses@2.0.1: {}
string-width@4.2.3:
@@ -7997,6 +8186,8 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
text-hex@1.0.0: {}
text-table@0.2.0: {}
tinyglobby@0.2.12:
@@ -8020,6 +8211,8 @@ snapshots:
dependencies:
punycode: 2.3.1
triple-beam@1.4.1: {}
ts-api-utils@1.4.3(typescript@5.8.2):
dependencies:
typescript: 5.8.2
@@ -8373,6 +8566,26 @@ snapshots:
dependencies:
string-width: 4.2.3
winston-transport@4.9.0:
dependencies:
logform: 2.7.0
readable-stream: 3.6.2
triple-beam: 1.4.1
winston@3.17.0:
dependencies:
'@colors/colors': 1.6.0
'@dabh/diagnostics': 2.0.3
async: 3.2.6
is-stream: 2.0.1
logform: 2.7.0
one-time: 1.0.0
readable-stream: 3.6.2
safe-stable-stringify: 2.5.0
stack-trace: 0.0.10
triple-beam: 1.4.1
winston-transport: 4.9.0
wkx@0.5.0:
dependencies:
'@types/node': 20.17.23