Initial commit for open-source version

This commit is contained in:
jiangrui
2024-12-17 11:30:59 +08:00
commit 42c07ed34c
57 changed files with 10559 additions and 0 deletions

1694
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
backend/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "cloud-saver-server",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "nodemon --exec ts-node src/app.ts",
"build": "tsc",
"start": "node dist/app.js"
},
"dependencies": {
"axios": "^1.6.7",
"cheerio": "^1.0.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"rss-parser": "^3.13.0",
"socket.io": "^4.8.1",
"tunnel": "^0.0.6"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.7",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.11.25",
"@types/tunnel": "^0.0.7",
"nodemon": "^3.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.2"
}
}

30
backend/src/app.ts Normal file
View File

@@ -0,0 +1,30 @@
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import routes from "./routes/api";
import { errorHandler } from "./middleware/errorHandler";
const app = express();
app.use(
cors({
origin: "*",
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
})
);
app.use(cookieParser());
app.use(express.json());
app.use("/", routes);
// 错误处理
app.use(errorHandler);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
export default app;

View File

@@ -0,0 +1,92 @@
import dotenv from "dotenv";
// 加载.env文件
dotenv.config();
interface Channel {
id: string;
name: string;
}
interface CloudPatterns {
baiduPan: RegExp;
tianyi: RegExp;
weiyun: RegExp;
aliyun: RegExp;
pan115: RegExp;
quark: RegExp;
}
interface Cloud115Config {
userId: string;
cookie: string;
}
interface QuarkConfig {
userId: string;
cookie: string;
}
interface HttpProxyConfig {
host: string;
port: string;
}
interface Config {
rss: {
baseUrl: string;
channels: Channel[];
};
telegram: {
baseUrl: string;
};
httpProxy: HttpProxyConfig;
cloudPatterns: CloudPatterns;
cloud115: Cloud115Config;
quark: QuarkConfig;
}
export const config: Config = {
rss: {
baseUrl: process.env.RSS_BASE_URL || "https://rsshub.rssforever.com/telegram/channel",
channels: [
{
id: "guaguale115",
name: "115网盘资源分享",
},
{
id: "hao115",
name: "115网盘资源分享频道",
},
{
id: "yunpanshare",
name: "网盘资源收藏(夸克)",
},
],
},
telegram: {
baseUrl: process.env.TELEGRAM_BASE_URL || "https://t.me/s",
},
httpProxy: {
host: process.env.HTTP_PROXY_HOST || "127.0.0.1",
port: process.env.HTTP_PROXY_PORT || "7890",
},
cloudPatterns: {
baiduPan: /https?:\/\/(?:pan|yun)\.baidu\.com\/[^\s<>"]+/g,
tianyi: /https?:\/\/cloud\.189\.cn\/[^\s<>"]+/g,
weiyun: /https?:\/\/share\.weiyun\.com\/[^\s<>"]+/g,
aliyun: /https?:\/\/\w+\.aliyundrive\.com\/[^\s<>"]+/g,
// pan115有两个域名 115.com 和 anxia.com
pan115: /https?:\/\/(?:115|anxia)\.com\/s\/[^\s<>"]+/g,
quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g,
},
cloud115: {
userId: "",
cookie: process.env.CLOUD115_COOKIE || "",
},
quark: {
userId: process.env.QUARK_USER_ID || "",
cookie: process.env.QUARK_COOKIE || "",
},
};

View File

@@ -0,0 +1,46 @@
import { Request, Response, NextFunction } from "express";
import { Cloud115Service } from "../services/Cloud115Service";
import { config } from "../config";
import handleError from "../utils/handleError";
import { handleResponse } from "../utils/responseHandler";
const { cookie } = config.cloud115;
const cloud115 = new Cloud115Service(cookie);
export const cloud115Controller = {
async getShareInfo(req: Request, res: Response, next: NextFunction) {
try {
const { shareCode, receiveCode } = req.query;
const result = await cloud115.getShareInfo(shareCode as string, receiveCode as string);
handleResponse(res, result, true);
} catch (error) {
handleError(res, error, "获取分享信息失败", next);
}
},
async getFolderList(req: Request, res: Response, next: NextFunction) {
try {
const { parentCid } = req.query;
const result = await cloud115.getFolderList(parentCid as string);
handleResponse(res, result, true);
} catch (error) {
handleError(res, error, "获取目录列表失败", next);
}
},
async saveFile(req: Request, res: Response, next: NextFunction) {
try {
const { shareCode, receiveCode, fileId, folderId } = req.body;
const result = await cloud115.saveSharedFile({
shareCode,
receiveCode,
fileId,
cid: folderId,
});
handleResponse(res, result, true);
} catch (error) {
handleError(res, error, "保存文件失败", next);
}
},
};

View File

@@ -0,0 +1,40 @@
import { Request, Response, NextFunction } from "express";
import { QuarkService } from "../services/QuarkService";
import { config } from "../config";
import { handleResponse } from "../utils/responseHandler";
import handleError from "../utils/handleError";
const { cookie } = config.quark;
const quark = new QuarkService(cookie);
export const quarkController = {
async getShareInfo(req: Request, res: Response, next: NextFunction) {
try {
const { pwdId, passcode } = req.query;
const result = await quark.getShareInfo(pwdId as string, passcode as string);
handleResponse(res, result, true);
} catch (error) {
handleError(res, error, "获取分享信息失败", next);
}
},
async getFolderList(req: Request, res: Response, next: NextFunction) {
try {
const { parentCid } = req.query;
const result = await quark.getFolderList(parentCid as string);
handleResponse(res, result, true);
} catch (error) {
handleError(res, error, "获取目录列表失败", next);
}
},
async saveFile(req: Request, res: Response, next: NextFunction) {
try {
const result = await quark.saveSharedFile(req.body);
handleResponse(res, result, true);
} catch (error) {
handleError(res, error, "保存文件失败", next);
}
},
};

View File

@@ -0,0 +1,32 @@
import { Request, Response, NextFunction } from "express";
import { RSSSearcher } from "../services/RSSSearcher";
import { Searcher } from "../services/Searcher";
import { handleResponse } from "../utils/responseHandler";
import handleError from "../utils/handleError";
export const resourceController = {
async rssSearch(req: Request, res: Response, next: NextFunction) {
try {
const { keyword } = req.query;
const searcher = new RSSSearcher();
const result = await searcher.searchAll(keyword as string);
handleResponse(res, result, true);
} catch (error) {
handleError(res, error, "获取资源发生未知错误", next);
}
},
async search(req: Request, res: Response, next: NextFunction) {
try {
const { keyword, channelId = "", lastMessageId = "" } = req.query; // Remove `: string` from here
const searcher = new Searcher();
const result = await searcher.searchAll(
keyword as string,
channelId as string,
lastMessageId as string
);
handleResponse(res, result, true);
} catch (error) {
handleError(res, error, "获取资源发生未知错误", next);
}
},
};

View File

@@ -0,0 +1,9 @@
import { Request, Response, NextFunction } from "express";
export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
console.error(err);
res.status(err.status || 500).json({
success: false,
error: err.message || "服务器内部错误",
});
};

View File

@@ -0,0 +1,14 @@
import { Request, Response, NextFunction } from "express";
export const validateRequest = (requiredParams: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
const missingParams = requiredParams.filter((param) => !req.query[param] && !req.body[param]);
if (missingParams.length > 0) {
return res.status(400).json({
success: false,
error: `缺少必要的参数: ${missingParams.join(", ")}`,
});
}
next();
};
};

22
backend/src/routes/api.ts Normal file
View File

@@ -0,0 +1,22 @@
import express from "express";
import { cloud115Controller } from "../controllers/cloud115";
import { quarkController } from "../controllers/quark";
import { resourceController } from "../controllers/resource";
const router = express.Router();
// 资源搜索
router.get("/search", resourceController.search);
router.get("/rssSearch", resourceController.rssSearch);
// 115网盘相关
router.get("/cloud115/share-info", cloud115Controller.getShareInfo);
router.get("/cloud115/folders", cloud115Controller.getFolderList);
router.post("/cloud115/save", cloud115Controller.saveFile);
// 夸克网盘相关
router.get("/quark/share-info", quarkController.getShareInfo);
router.get("/quark/folders", quarkController.getFolderList);
router.post("/quark/save", quarkController.saveFile);
export default router;

View File

@@ -0,0 +1,148 @@
import { AxiosHeaders, AxiosInstance } from "axios"; // 导入 AxiosHeaders
import { createAxiosInstance } from "../utils/axiosInstance";
import { Logger } from "../utils/logger";
import { config } from "../config/index";
import { ShareInfoResponse } from "../types/cloud115";
export class Cloud115Service {
private api: AxiosInstance;
constructor(cookie: string) {
if (!cookie) {
throw new Error("115网盘需要提供cookie进行身份验证");
}
this.api = createAxiosInstance(
"https://webapi.115.com",
AxiosHeaders.from({
Host: "webapi.115.com",
Connection: "keep-alive",
xweb_xhr: "1",
Origin: "",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 MicroMessenger/6.8.0(0x16080000) NetType/WIFI MiniProgramEnv/Mac MacWechat/WMPF MacWechat/3.8.9(0x13080910) XWEB/1227",
Accept: "*/*",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
Referer: "https://servicewechat.com/wx2c744c010a61b0fa/94/page-frame.html",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
cookie: cookie,
})
);
}
async getShareInfo(shareCode: string, receiveCode = ""): Promise<ShareInfoResponse> {
try {
const response = await this.api.get("/share/snap", {
params: {
share_code: shareCode,
receive_code: receiveCode,
offset: 0,
limit: 20,
cid: "",
},
});
if (response.data?.state && response.data.data?.list?.length > 0) {
return {
success: true,
data: {
list: response.data.data.list.map((item: any) => ({
fileId: item.cid,
fileName: item.n,
fileSize: item.s,
})),
},
};
}
return {
success: false,
error: "未找到文件信息",
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "未知错误",
};
}
}
async getFolderList(parentCid = "0") {
try {
const response = await this.api.get("/files", {
params: {
aid: 1,
cid: parentCid,
o: "user_ptime",
asc: 0,
offset: 0,
show_dir: 1,
limit: 50,
type: 0,
format: "json",
star: 0,
suffix: "",
natsort: 1,
},
});
if (response.data?.state) {
return {
success: true,
data: response.data.data
.filter((item: any) => item.cid)
.map((folder: any) => ({
cid: folder.cid,
name: folder.n,
path: response.data.path,
})),
};
} else {
Logger.error("获取目录列表失败:", response.data.error);
return {
success: false,
error: "获取115pan目录列表失败:" + response.data.error,
};
}
} catch (error) {
Logger.error("获取目录列表失败:", error);
return {
success: false,
error: "获取115pan目录列表失败",
};
}
}
async saveSharedFile(params: {
cid: string;
shareCode: string;
receiveCode: string;
fileId: string;
}) {
try {
const param = new URLSearchParams({
cid: params.cid,
user_id: config.cloud115.userId,
share_code: params.shareCode,
receive_code: params.receiveCode,
file_id: params.fileId,
});
const response = await this.api.post("/share/receive", param.toString());
return {
success: response.data.state,
error: response.data.error,
data: response.data,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "未知错误",
};
}
}
}

View File

@@ -0,0 +1,178 @@
import { AxiosInstance, AxiosHeaders } from "axios";
import { Logger } from "../utils/logger";
import { createAxiosInstance } from "../utils/axiosInstance";
export class QuarkService {
private api: AxiosInstance;
constructor(cookie: string) {
if (!cookie) {
throw new Error("115网盘需要提供cookie进行身份验证");
}
this.api = createAxiosInstance(
"https://drive-h.quark.cn",
AxiosHeaders.from({
cookie: cookie,
accept: "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"content-type": "application/json",
priority: "u=1, i",
"sec-ch-ua": '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
})
);
}
async getShareInfo(pwdId: string, passcode = "") {
try {
const response = await this.api.post(
`/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc&uc_param_str=&__dt=994&__t=${Date.now()}`,
{
pwd_id: pwdId,
passcode: "",
}
);
if (response.data?.status === 200 && response.data.data) {
const fileInfo = response.data.data;
if (fileInfo.stoken) {
let res = await this.getShareList(pwdId, fileInfo.stoken);
return {
success: true,
data: res,
};
}
}
return {
success: false,
error: "未找到文件信息",
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "未知错误",
};
}
}
async getShareList(pwdId: string, stoken: string) {
try {
const response = await this.api.get("/1/clouddrive/share/sharepage/detail", {
params: {
pr: "ucpro",
fr: "pc",
uc_param_str: "",
pwd_id: pwdId,
stoken: stoken,
pdir_fid: "0",
force: "0",
_page: "1",
_size: "50",
_fetch_banner: "1",
_fetch_share: "1",
_fetch_total: "1",
_sort: "file_type:asc,updated_at:desc",
__dt: "1589",
__t: Date.now(),
},
});
if (response.data?.data) {
const list = response.data.data.list
.filter((item: any) => item.fid)
.map((folder: any) => ({
fileId: folder.fid,
fileName: folder.file_name,
fileIdToken: folder.share_fid_token,
}));
return {
list,
pwdId,
stoken: stoken,
};
} else {
return {
list: [],
};
}
} catch (error) {
Logger.error("获取目录列表失败:", error);
return [];
}
}
async getFolderList(parentCid = "0") {
try {
const response = await this.api.get("/1/clouddrive/file/sort", {
params: {
pr: "ucpro",
fr: "pc",
uc_param_str: "",
pdir_fid: parentCid,
_page: "1",
_size: "100",
_fetch_total: "false",
_fetch_sub_dirs: "1",
_sort: "",
__dt: "2093126",
__t: Date.now(),
},
});
if (response.data?.data && response.data.data.list.length) {
return {
success: true,
data: response.data.data.list
.filter((item: any) => item.fid)
.map((folder: any) => ({
cid: folder.fid,
name: folder.file_name,
path: [],
})),
};
} else {
Logger.error("获取目录列表失败:", response.data.error);
return {
success: false,
error: "获取夸克目录列表失败:" + response.data.error,
};
}
} catch (error) {
Logger.error("获取目录列表失败:", error);
return {
success: false,
error: "获取夸克目录列表失败",
};
}
}
async saveSharedFile(params: {
fid_list: string[];
fid_token_list: string[];
to_pdir_fid: string;
pwd_id: string;
stoken: string;
pdir_fid: string;
scene: string;
}) {
try {
const response = await this.api.post(
`/1/clouddrive/share/sharepage/save?pr=ucpro&fr=pc&uc_param_str=&__dt=208097&__t=${Date.now()}`,
params
);
return {
success: response.data.code === 0,
error: response.data.message,
data: response.data.data,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "未知错误",
};
}
}
}

View File

@@ -0,0 +1,112 @@
import RSSParser from "rss-parser";
import { AxiosInstance, AxiosHeaders } from "axios";
import { config } from "../config";
import { Logger } from "../utils/logger";
import { createAxiosInstance } from "../utils/axiosInstance";
interface RSSItem {
title?: string;
link?: string;
pubDate?: string;
content?: string;
description?: string;
image?: string;
cloudLinks?: string[];
}
export class RSSSearcher {
private parser: RSSParser;
private axiosInstance: AxiosInstance;
constructor() {
this.parser = new RSSParser({
customFields: {
item: [
["content:encoded", "content"],
["description", "description"],
],
},
});
this.axiosInstance = createAxiosInstance(
config.rss.baseUrl,
AxiosHeaders.from({
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
Accept: "application/xml,application/xhtml+xml,text/html,application/rss+xml",
}),
true
);
}
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
const links: string[] = [];
let cloudType = "";
Object.values(config.cloudPatterns).forEach((pattern, index) => {
const matches = text.match(pattern);
if (matches) {
links.push(...matches);
cloudType = Object.keys(config.cloudPatterns)[index];
}
});
return {
links: [...new Set(links)],
cloudType,
};
}
async searchAll(keyword: string) {
const allResults = [];
for (let i = 0; i < config.rss.channels.length; i++) {
const channel = config.rss.channels[i];
try {
const rssUrl = `${config.rss.baseUrl}/${
channel.id
}${keyword ? `/searchQuery=${encodeURIComponent(keyword)}` : ""}`;
const results = await this.searchInRSSFeed(rssUrl);
if (results.items.length > 0) {
const channelResults = results.items
.filter((item: RSSItem) => item.cloudLinks && item.cloudLinks.length > 0)
.map((item: RSSItem) => ({
...item,
channel: channel.name + "(" + channel.id + ")",
}));
allResults.push(...channelResults);
}
} catch (error) {
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
}
}
return allResults;
}
async searchInRSSFeed(rssUrl: string) {
try {
const response = await this.axiosInstance.get(rssUrl);
const feed = await this.parser.parseString(response.data);
return {
items: feed.items.map((item: RSSItem) => {
const linkInfo = this.extractCloudLinks(item.content || item.description || "");
return {
title: item.title || "",
link: item.link || "",
pubDate: item.pubDate || "",
image: item.image || "",
cloudLinks: linkInfo.links,
cloudType: linkInfo.cloudType,
};
}),
};
} catch (error) {
Logger.error(`RSS源解析错误: ${rssUrl}`, error);
return {
items: [],
};
}
}
}

View File

@@ -0,0 +1,159 @@
import { AxiosInstance, AxiosHeaders } from "axios";
import { createAxiosInstance } from "../utils/axiosInstance";
import * as cheerio from "cheerio";
import { config } from "../config";
import { Logger } from "../utils/logger";
interface sourceItem {
messageId?: string;
title?: string;
link?: string;
pubDate?: string;
content?: string;
description?: string;
image?: string;
cloudLinks?: string[];
cloudType?: string;
}
export class Searcher {
private axiosInstance: AxiosInstance;
constructor() {
this.axiosInstance = createAxiosInstance(
config.telegram.baseUrl,
AxiosHeaders.from({
accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cache-control": "max-age=0",
priority: "u=0, i",
"sec-ch-ua": '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
}),
true
);
}
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
const links: string[] = [];
let cloudType = "";
Object.values(config.cloudPatterns).forEach((pattern, index) => {
const matches = text.match(pattern);
if (matches) {
links.push(...matches);
cloudType = Object.keys(config.cloudPatterns)[index];
}
});
return {
links: [...new Set(links)],
cloudType,
};
}
async searchAll(keyword: string, channelId?: string, messageId?: string) {
const allResults = [];
const totalChannels = config.rss.channels.length;
const channelList = channelId
? config.rss.channels.filter((channel) => channel.id === channelId)
: config.rss.channels;
for (let i = 0; i < channelList.length; i++) {
const channel = channelList[i];
try {
const messageIdparams = messageId ? `before=${messageId}` : "";
const url = `/${channel.id}${keyword ? `?q=${encodeURIComponent(keyword)}&${messageIdparams}` : `?${messageIdparams}`}`;
console.log(`Searching in channel ${channel.name} with URL: ${url}`);
const results = await this.searchInWeb(url, channel.id);
console.log(`Found ${results.items.length} items in channel ${channel.name}`);
if (results.items.length > 0) {
const channelResults = results.items
.filter((item: sourceItem) => item.cloudLinks && item.cloudLinks.length > 0)
.map((item: sourceItem) => ({
...item,
channel: channel.name,
channelId: channel.id,
}));
allResults.push(...channelResults);
}
} catch (error) {
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
}
}
return allResults;
}
async searchInWeb(url: string, channelId: string) {
try {
const response = await this.axiosInstance.get(url);
const html = response.data;
const $ = cheerio.load(html);
const items: sourceItem[] = [];
// 遍历每个消息容器
$(".tgme_widget_message_wrap").each((_, element) => {
const messageEl = $(element);
// 通过 data-post 属性来获取消息的链接 去除channelId 获得消息id
const messageId = messageEl
.find(".tgme_widget_message")
.data("post")
?.toString()
.split("/")[1];
// 提取标题 (消息截取100长度)
const title = messageEl.find(".js-message_text").text().trim().substring(0, 50) + "...";
// 提取链接 (消息中的链接)
// const link = messageEl.find('.tgme_widget_message').data('post');
// 提取发布时间
const pubDate = messageEl.find("time").attr("datetime");
// 提取内容 (完整消息文本)
const content = messageEl.find(".js-message_text").text();
// 提取描述 (消息文本中"描述:"后的内容)
const description = content.split("描述:")[1]?.split("\n")[0]?.trim();
// 提取图片
const image = messageEl
.find(".tgme_widget_message_photo_wrap")
.attr("style")
?.match(/url\('(.+?)'\)/)?.[1];
// 提取云盘链接
const links = messageEl
.find(".tgme_widget_message_text a")
.map((_, el) => $(el).attr("href"))
.get();
const cloudInfo = this.extractCloudLinks(links.join(" "));
// 添加到数组第一位
items.unshift({
messageId,
title,
pubDate,
content,
description,
image,
cloudLinks: cloudInfo.links,
cloudType: cloudInfo.cloudType,
});
});
return { items };
} catch (error) {
Logger.error(`RSS源解析错误: ${url}`, error);
return {
items: [],
};
}
}
}

View File

@@ -0,0 +1,13 @@
export interface ShareInfo {
fileId: string;
fileName: string;
fileSize: number;
}
export interface ShareInfoResponse {
success: boolean;
data?: {
list: ShareInfo[];
};
error?: string;
}

View File

@@ -0,0 +1,28 @@
import axios, { AxiosInstance, AxiosRequestHeaders } from "axios";
import tunnel from "tunnel";
import { config } from "../config";
export function createAxiosInstance(
baseURL: string,
headers: AxiosRequestHeaders,
useProxy: boolean = false
): AxiosInstance {
let agent;
if (useProxy) {
agent = tunnel.httpsOverHttp({
proxy: {
host: config.httpProxy.host,
port: Number(config.httpProxy.port),
},
});
}
return axios.create({
baseURL,
timeout: 30000,
headers,
httpsAgent: useProxy ? agent : undefined,
withCredentials: true,
});
}

View File

@@ -0,0 +1,11 @@
import { Response, NextFunction } from "express";
import { Logger } from "../utils/logger";
export default function handleError(
res: Response,
error: any,
message: string,
next: NextFunction
) {
Logger.error(message, error);
next(error || { success: false, message });
}

View File

@@ -0,0 +1,38 @@
type LogLevel = "info" | "success" | "warn" | "error";
export const Logger = {
info(...args: any[]) {
this.log("info", ...args);
},
success(...args: any[]) {
this.log("success", ...args);
},
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);
}
},
};

View File

@@ -0,0 +1,5 @@
import { Response } from "express";
export const handleResponse = (res: Response, data: any, success: boolean) => {
res.json({ success, data });
};

21
backend/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}