mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-12 16:00:27 +00:00
refactor: 整体重构 (#1381)
* feat: pnpm new * Refactor build and release workflows, update dependencies Switch build scripts and workflows from npm to pnpm, update build and artifact paths, and simplify release workflow by removing version detection and changelog steps. Add new dependencies (silk-wasm, express, ws, node-pty-prebuilt-multiarch), update exports in package.json files, and add vite config for napcat-framework. Also, rename manifest.json for framework package and fix static asset copying in shell build config.
This commit is contained in:
152
packages/napcat-webui-backend/src/api/Auth.ts
Normal file
152
packages/napcat-webui-backend/src/api/Auth.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { RequestHandler } from 'express';
|
||||
|
||||
import { WebUiConfig, getInitialWebUiToken, setInitialWebUiToken } from '@/napcat-webui-backend/index';
|
||||
|
||||
import { AuthHelper } from '@/napcat-webui-backend/helper/SignToken';
|
||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/helper/Data';
|
||||
import { sendSuccess, sendError } from '@/napcat-webui-backend/utils/response';
|
||||
import { isEmpty } from '@/napcat-webui-backend/utils/check';
|
||||
|
||||
// 登录
|
||||
export const LoginHandler: RequestHandler = async (req, res) => {
|
||||
// 获取WebUI配置
|
||||
const WebUiConfigData = await WebUiConfig.GetWebUIConfig();
|
||||
// 获取请求体中的hash
|
||||
const { hash } = req.body;
|
||||
// 获取客户端IP
|
||||
const clientIP = req.ip || req.socket.remoteAddress || '';
|
||||
|
||||
// 如果token为空,返回错误信息
|
||||
if (isEmpty(hash)) {
|
||||
return sendError(res, 'token is empty');
|
||||
}
|
||||
// 检查登录频率
|
||||
if (!WebUiDataRuntime.checkLoginRate(clientIP, WebUiConfigData.loginRate)) {
|
||||
return sendError(res, 'login rate limit');
|
||||
}
|
||||
// 使用启动时缓存的token进行验证,而不是动态读取配置文件
|
||||
const initialToken = getInitialWebUiToken();
|
||||
if (!initialToken) {
|
||||
return sendError(res, 'Server token not initialized');
|
||||
}
|
||||
// 验证初始token hash是否等于提交的token hash
|
||||
if (!AuthHelper.comparePasswordHash(initialToken, hash)) {
|
||||
return sendError(res, 'token is invalid');
|
||||
}
|
||||
|
||||
// 签发凭证
|
||||
const signCredential = Buffer.from(JSON.stringify(AuthHelper.signCredential(hash))).toString(
|
||||
'base64'
|
||||
);
|
||||
// 返回成功信息
|
||||
return sendSuccess(res, {
|
||||
Credential: signCredential,
|
||||
});
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
export const LogoutHandler: RequestHandler = async (req, res) => {
|
||||
const authorization = req.headers.authorization;
|
||||
try {
|
||||
const CredentialBase64: string = authorization?.split(' ')[1] as string;
|
||||
const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString());
|
||||
AuthHelper.revokeCredential(Credential);
|
||||
return sendSuccess(res, 'Logged out successfully');
|
||||
} catch (_e) {
|
||||
return sendError(res, 'Logout failed');
|
||||
}
|
||||
};
|
||||
|
||||
// 检查登录状态
|
||||
export const checkHandler: RequestHandler = async (req, res) => {
|
||||
// 获取请求头中的Authorization
|
||||
const authorization = req.headers.authorization;
|
||||
// 检查凭证
|
||||
try {
|
||||
// 从Authorization中获取凭证
|
||||
const CredentialBase64: string = authorization?.split(' ')[1] as string;
|
||||
// 解析凭证
|
||||
const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString());
|
||||
|
||||
// 检查凭证是否已被注销
|
||||
if (AuthHelper.isCredentialRevoked(Credential)) {
|
||||
return sendError(res, 'Token has been revoked');
|
||||
}
|
||||
|
||||
// 使用启动时缓存的token进行验证
|
||||
const initialToken = getInitialWebUiToken();
|
||||
if (!initialToken) {
|
||||
return sendError(res, 'Server token not initialized');
|
||||
}
|
||||
// 验证凭证是否在一小时内有效
|
||||
const valid = AuthHelper.validateCredentialWithinOneHour(initialToken, Credential);
|
||||
// 返回成功信息
|
||||
if (valid) return sendSuccess(res, null);
|
||||
// 返回错误信息
|
||||
return sendError(res, 'Authorization Failed');
|
||||
} catch (_e) {
|
||||
// 返回错误信息
|
||||
return sendError(res, 'Authorization Failed');
|
||||
}
|
||||
};
|
||||
|
||||
// 修改密码(token)
|
||||
export const UpdateTokenHandler: RequestHandler = async (req, res) => {
|
||||
const { oldToken, newToken } = req.body;
|
||||
const authorization = req.headers.authorization;
|
||||
|
||||
if (isEmpty(newToken)) {
|
||||
return sendError(res, 'newToken is empty');
|
||||
}
|
||||
|
||||
// 强制要求旧密码
|
||||
if (isEmpty(oldToken)) {
|
||||
return sendError(res, 'oldToken is required');
|
||||
}
|
||||
|
||||
// 检查新旧密码是否相同
|
||||
if (oldToken === newToken) {
|
||||
return sendError(res, '新密码不能与旧密码相同');
|
||||
}
|
||||
|
||||
// 检查新密码强度
|
||||
if (newToken.length < 6) {
|
||||
return sendError(res, '新密码至少需要6个字符');
|
||||
}
|
||||
|
||||
// 检查是否包含字母
|
||||
if (!/[a-zA-Z]/.test(newToken)) {
|
||||
return sendError(res, '新密码必须包含字母');
|
||||
}
|
||||
|
||||
// 检查是否包含数字
|
||||
if (!/[0-9]/.test(newToken)) {
|
||||
return sendError(res, '新密码必须包含数字');
|
||||
}
|
||||
|
||||
try {
|
||||
// 注销当前的Token
|
||||
if (authorization) {
|
||||
const CredentialBase64: string = authorization.split(' ')[1] as string;
|
||||
const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString());
|
||||
AuthHelper.revokeCredential(Credential);
|
||||
}
|
||||
|
||||
// 使用启动时缓存的token进行验证
|
||||
const initialToken = getInitialWebUiToken();
|
||||
if (!initialToken) {
|
||||
return sendError(res, 'Server token not initialized');
|
||||
}
|
||||
if (initialToken !== oldToken) {
|
||||
return sendError(res, '旧 token 不匹配');
|
||||
}
|
||||
// 直接更新配置文件中的token,不需要通过WebUiConfig.UpdateToken方法
|
||||
await WebUiConfig.UpdateWebUIConfig({ token: newToken });
|
||||
// 更新内存中的缓存token,使新密码立即生效
|
||||
setInitialWebUiToken(newToken);
|
||||
|
||||
return sendSuccess(res, 'Token updated successfully');
|
||||
} catch (e: any) {
|
||||
return sendError(res, `Failed to update token: ${e.message}`);
|
||||
}
|
||||
};
|
||||
26
packages/napcat-webui-backend/src/api/BaseInfo.ts
Normal file
26
packages/napcat-webui-backend/src/api/BaseInfo.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/helper/Data';
|
||||
|
||||
import { sendSuccess } from '@/napcat-webui-backend/utils/response';
|
||||
import { WebUiConfig } from '@/napcat-webui-backend/index';
|
||||
|
||||
export const PackageInfoHandler: RequestHandler = (_, res) => {
|
||||
const data = WebUiDataRuntime.getPackageJson();
|
||||
sendSuccess(res, data);
|
||||
};
|
||||
|
||||
export const QQVersionHandler: RequestHandler = (_, res) => {
|
||||
const data = WebUiDataRuntime.getQQVersion();
|
||||
sendSuccess(res, data);
|
||||
};
|
||||
|
||||
export const GetThemeConfigHandler: RequestHandler = async (_, res) => {
|
||||
const data = await WebUiConfig.GetTheme();
|
||||
sendSuccess(res, data);
|
||||
};
|
||||
|
||||
export const SetThemeConfigHandler: RequestHandler = async (req, res) => {
|
||||
const { theme } = req.body;
|
||||
await WebUiConfig.UpdateTheme(theme);
|
||||
sendSuccess(res, { message: '更新成功' });
|
||||
};
|
||||
655
packages/napcat-webui-backend/src/api/File.ts
Normal file
655
packages/napcat-webui-backend/src/api/File.ts
Normal file
@@ -0,0 +1,655 @@
|
||||
import type { RequestHandler } from 'express';
|
||||
import { sendError, sendSuccess } from '../utils/response';
|
||||
import fsProm from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import compressing from 'compressing';
|
||||
import { PassThrough } from 'stream';
|
||||
import multer from 'multer';
|
||||
import webUIFontUploader from '../uploader/webui_font';
|
||||
import diskUploader from '../uploader/disk';
|
||||
import { WebUiConfig, getInitialWebUiToken, webUiPathWrapper } from '@/napcat-webui-backend/index';
|
||||
|
||||
const isWindows = os.platform() === 'win32';
|
||||
|
||||
// 安全地从查询参数中提取字符串值,防止类型混淆
|
||||
const getQueryStringParam = (param: any): string => {
|
||||
if (Array.isArray(param)) {
|
||||
return String(param[0] || '');
|
||||
}
|
||||
return String(param || '');
|
||||
};
|
||||
|
||||
// 获取系统根目录列表(Windows返回盘符列表,其他系统返回['/'])
|
||||
const getRootDirs = async (): Promise<string[]> => {
|
||||
if (!isWindows) return ['/'];
|
||||
|
||||
// Windows 驱动器字母 (A-Z)
|
||||
const drives: string[] = [];
|
||||
for (let i = 65; i <= 90; i++) {
|
||||
const driveLetter = String.fromCharCode(i);
|
||||
try {
|
||||
await fsProm.access(`${driveLetter}:\\`);
|
||||
drives.push(`${driveLetter}:`);
|
||||
} catch {
|
||||
// 如果驱动器不存在或无法访问,跳过
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return drives.length > 0 ? drives : ['C:'];
|
||||
};
|
||||
|
||||
// 规范化路径并进行安全验证
|
||||
const normalizePath = (inputPath: string): string => {
|
||||
if (!inputPath) {
|
||||
// 对于空路径,Windows返回用户主目录,其他系统返回根目录
|
||||
return isWindows ? process.env['USERPROFILE'] || 'C:\\' : '/';
|
||||
}
|
||||
|
||||
// 对输入路径进行清理,移除潜在的危险字符
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const cleanedPath = inputPath.replace(/[\x01-\x1f\x7f]/g, ''); // 移除控制字符
|
||||
|
||||
// 如果是Windows且输入为纯盘符(可能带或不带斜杠),统一返回 "X:\"
|
||||
if (isWindows && /^[A-Z]:[\\/]*$/i.test(cleanedPath)) {
|
||||
return cleanedPath.slice(0, 2) + '\\';
|
||||
}
|
||||
|
||||
// 安全验证:检查是否包含危险的路径遍历模式(在规范化之前)
|
||||
if (containsPathTraversal(cleanedPath)) {
|
||||
throw new Error('Invalid path: path traversal detected');
|
||||
}
|
||||
|
||||
// 进行路径规范化
|
||||
const normalized = path.resolve(cleanedPath);
|
||||
|
||||
// 再次检查规范化后的路径,确保没有绕过安全检查
|
||||
if (containsPathTraversal(normalized)) {
|
||||
throw new Error('Invalid path: path traversal detected after normalization');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保规范化后的路径不包含连续的路径分隔符
|
||||
const finalPath = normalized.replace(/[\\/]+/g, path.sep);
|
||||
|
||||
return finalPath;
|
||||
};
|
||||
|
||||
// 检查路径是否包含路径遍历攻击模式
|
||||
const containsPathTraversal = (inputPath: string): boolean => {
|
||||
// 对输入进行URL解码,防止编码绕过
|
||||
let decodedPath = inputPath;
|
||||
try {
|
||||
decodedPath = decodeURIComponent(inputPath);
|
||||
} catch {
|
||||
// 如果解码失败,使用原始路径
|
||||
}
|
||||
|
||||
// 将路径统一为正斜杠格式进行检查
|
||||
const normalizedForCheck = decodedPath.replace(/\\/g, '/');
|
||||
|
||||
// 检查危险模式 - 更全面的路径遍历检测
|
||||
const dangerousPatterns = [
|
||||
/\.\.\//, // ../ 模式
|
||||
/\/\.\./, // /.. 模式
|
||||
/^\.\./, // 以.. 开头
|
||||
/\.\.$/, // 以.. 结尾
|
||||
/\.\.\\/, // ..\ 模式(Windows)
|
||||
/\\\.\./, // \.. 模式(Windows)
|
||||
/%2e%2e/i, // URL编码的..
|
||||
/%252e%252e/i, // 双重URL编码的..
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/\.\.\x00/, // null字节攻击
|
||||
/\0/, // null字节
|
||||
];
|
||||
|
||||
return dangerousPatterns.some(pattern => pattern.test(normalizedForCheck));
|
||||
};
|
||||
|
||||
interface FileInfo {
|
||||
name: string;
|
||||
isDirectory: boolean;
|
||||
size: number;
|
||||
mtime: Date;
|
||||
}
|
||||
|
||||
// 添加系统文件黑名单
|
||||
const SYSTEM_FILES = new Set(['pagefile.sys', 'swapfile.sys', 'hiberfil.sys', 'System Volume Information']);
|
||||
|
||||
// 检查是否为WebUI配置文件
|
||||
const isWebUIConfigFile = (filePath: string): boolean => {
|
||||
// 先用字符串快速筛选
|
||||
if (!filePath.includes('webui.json')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 进入更严格的路径判断 - 统一路径分隔符为 /
|
||||
const webUIConfigPath = path.resolve(webUiPathWrapper.configPath, 'webui.json').replace(/\\/g, '/');
|
||||
const targetPath = path.resolve(filePath).replace(/\\/g, '/');
|
||||
|
||||
// 统一分隔符后进行路径比较
|
||||
return targetPath === webUIConfigPath;
|
||||
};
|
||||
|
||||
// WebUI配置文件脱敏处理
|
||||
const sanitizeWebUIConfig = (content: string): string => {
|
||||
try {
|
||||
const config = JSON.parse(content);
|
||||
if (config.token) {
|
||||
config.token = '******';
|
||||
}
|
||||
return JSON.stringify(config, null, 4);
|
||||
} catch {
|
||||
// 如果解析失败,返回原内容
|
||||
return content;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查同类型的文件或目录是否存在
|
||||
const checkSameTypeExists = async (pathToCheck: string, isDirectory: boolean): Promise<boolean> => {
|
||||
try {
|
||||
const stat = await fsProm.stat(pathToCheck);
|
||||
// 只有当类型相同时才认为是冲突
|
||||
return stat.isDirectory() === isDirectory;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取目录内容
|
||||
export const ListFilesHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const requestPath = getQueryStringParam(req.query['path']) || (isWindows ? process.env['USERPROFILE'] || 'C:\\' : '/');
|
||||
|
||||
let normalizedPath: string;
|
||||
try {
|
||||
normalizedPath = normalizePath(requestPath);
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
const onlyDirectory = req.query['onlyDirectory'] === 'true';
|
||||
|
||||
// 如果是根路径且在Windows系统上,返回盘符列表
|
||||
if (isWindows && (!requestPath || requestPath === '/' || requestPath === '\\')) {
|
||||
const drives = await getRootDirs();
|
||||
const driveInfos: FileInfo[] = await Promise.all(
|
||||
drives.map(async (drive) => {
|
||||
try {
|
||||
const stat = await fsProm.stat(`${drive}\\`);
|
||||
return {
|
||||
name: drive,
|
||||
isDirectory: true,
|
||||
size: 0,
|
||||
mtime: stat.mtime,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
name: drive,
|
||||
isDirectory: true,
|
||||
size: 0,
|
||||
mtime: new Date(),
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
return sendSuccess(res, driveInfos);
|
||||
}
|
||||
|
||||
const files = await fsProm.readdir(normalizedPath);
|
||||
let fileInfos: FileInfo[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
// 跳过系统文件
|
||||
if (SYSTEM_FILES.has(file)) continue;
|
||||
|
||||
try {
|
||||
const fullPath = path.join(normalizedPath, file);
|
||||
const stat = await fsProm.stat(fullPath);
|
||||
fileInfos.push({
|
||||
name: file,
|
||||
isDirectory: stat.isDirectory(),
|
||||
size: stat.size,
|
||||
mtime: stat.mtime,
|
||||
});
|
||||
} catch (_error) {
|
||||
// 忽略无法访问的文件
|
||||
// console.warn(`无法访问文件 ${file}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果请求参数 onlyDirectory 为 true,则只返回目录信息
|
||||
if (onlyDirectory) {
|
||||
fileInfos = fileInfos.filter((info) => info.isDirectory);
|
||||
}
|
||||
|
||||
return sendSuccess(res, fileInfos);
|
||||
} catch (_error) {
|
||||
console.error('读取目录失败:', _error);
|
||||
return sendError(res, '读取目录失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 创建目录
|
||||
export const CreateDirHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { path: dirPath } = req.body;
|
||||
|
||||
let normalizedPath: string;
|
||||
try {
|
||||
normalizedPath = normalizePath(dirPath);
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保路径是绝对路径
|
||||
if (!path.isAbsolute(normalizedPath)) {
|
||||
return sendError(res, '路径必须是绝对路径');
|
||||
}
|
||||
|
||||
// 检查是否已存在同类型(目录)
|
||||
if (await checkSameTypeExists(normalizedPath, true)) {
|
||||
return sendError(res, '同名目录已存在');
|
||||
}
|
||||
|
||||
await fsProm.mkdir(normalizedPath, { recursive: true });
|
||||
return sendSuccess(res, true);
|
||||
} catch (_error) {
|
||||
return sendError(res, '创建目录失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 删除文件/目录
|
||||
export const DeleteHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { path: targetPath } = req.body;
|
||||
|
||||
let normalizedPath: string;
|
||||
try {
|
||||
normalizedPath = normalizePath(targetPath);
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保路径是绝对路径
|
||||
if (!path.isAbsolute(normalizedPath)) {
|
||||
return sendError(res, '路径必须是绝对路径');
|
||||
}
|
||||
|
||||
const stat = await fsProm.stat(normalizedPath);
|
||||
if (stat.isDirectory()) {
|
||||
await fsProm.rm(normalizedPath, { recursive: true });
|
||||
} else {
|
||||
await fsProm.unlink(normalizedPath);
|
||||
}
|
||||
return sendSuccess(res, true);
|
||||
} catch (_error) {
|
||||
return sendError(res, '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 批量删除文件/目录
|
||||
export const BatchDeleteHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { paths } = req.body;
|
||||
for (const targetPath of paths) {
|
||||
let normalizedPath: string;
|
||||
try {
|
||||
normalizedPath = normalizePath(targetPath);
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保路径是绝对路径
|
||||
if (!path.isAbsolute(normalizedPath)) {
|
||||
return sendError(res, '路径必须是绝对路径');
|
||||
}
|
||||
|
||||
const stat = await fsProm.stat(normalizedPath);
|
||||
if (stat.isDirectory()) {
|
||||
await fsProm.rm(normalizedPath, { recursive: true });
|
||||
} else {
|
||||
await fsProm.unlink(normalizedPath);
|
||||
}
|
||||
}
|
||||
return sendSuccess(res, true);
|
||||
} catch (_error) {
|
||||
return sendError(res, '批量删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 读取文件内容
|
||||
export const ReadFileHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
let filePath: string;
|
||||
try {
|
||||
filePath = normalizePath(getQueryStringParam(req.query['path']));
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保路径是绝对路径
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
return sendError(res, '路径必须是绝对路径');
|
||||
}
|
||||
|
||||
let content = await fsProm.readFile(filePath, 'utf-8');
|
||||
|
||||
// 如果是WebUI配置文件,对token进行脱敏处理
|
||||
if (isWebUIConfigFile(filePath)) {
|
||||
content = sanitizeWebUIConfig(content);
|
||||
}
|
||||
|
||||
return sendSuccess(res, content);
|
||||
} catch (_error) {
|
||||
return sendError(res, '读取文件失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 写入文件内容
|
||||
export const WriteFileHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { path: filePath, content } = req.body;
|
||||
|
||||
// 安全的路径规范化,如果检测到路径遍历攻击会抛出异常
|
||||
let normalizedPath: string;
|
||||
try {
|
||||
normalizedPath = normalizePath(filePath);
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保路径是绝对路径
|
||||
if (!path.isAbsolute(normalizedPath)) {
|
||||
return sendError(res, '路径必须是绝对路径');
|
||||
}
|
||||
|
||||
let finalContent = content;
|
||||
|
||||
// 检查是否为WebUI配置文件
|
||||
if (isWebUIConfigFile(normalizedPath)) {
|
||||
try {
|
||||
// 解析要写入的配置
|
||||
const configToWrite = JSON.parse(content);
|
||||
// 获取内存中的token,覆盖前端传来的token
|
||||
const memoryToken = getInitialWebUiToken();
|
||||
if (memoryToken) {
|
||||
configToWrite.token = memoryToken;
|
||||
finalContent = JSON.stringify(configToWrite, null, 4);
|
||||
}
|
||||
} catch (_e) {
|
||||
// 如果解析失败 说明不符合json格式 不允许写入
|
||||
return sendError(res, '写入的WebUI配置文件内容格式错误,必须是合法的JSON');
|
||||
}
|
||||
}
|
||||
|
||||
await fsProm.writeFile(normalizedPath, finalContent, 'utf-8');
|
||||
return sendSuccess(res, true);
|
||||
} catch (_error) {
|
||||
return sendError(res, '写入文件失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 创建新文件
|
||||
export const CreateFileHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { path: filePath } = req.body;
|
||||
|
||||
let normalizedPath: string;
|
||||
try {
|
||||
normalizedPath = normalizePath(filePath);
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保路径是绝对路径
|
||||
if (!path.isAbsolute(normalizedPath)) {
|
||||
return sendError(res, '路径必须是绝对路径');
|
||||
}
|
||||
|
||||
// 检查是否已存在同类型(文件)
|
||||
if (await checkSameTypeExists(normalizedPath, false)) {
|
||||
return sendError(res, '同名文件已存在');
|
||||
}
|
||||
|
||||
await fsProm.writeFile(normalizedPath, '', 'utf-8');
|
||||
return sendSuccess(res, true);
|
||||
} catch (_error) {
|
||||
return sendError(res, '创建文件失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 重命名文件/目录
|
||||
export const RenameHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { oldPath, newPath } = req.body;
|
||||
|
||||
let normalizedOldPath: string;
|
||||
let normalizedNewPath: string;
|
||||
try {
|
||||
normalizedOldPath = normalizePath(oldPath);
|
||||
normalizedNewPath = normalizePath(newPath);
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保路径是绝对路径
|
||||
if (!path.isAbsolute(normalizedOldPath) || !path.isAbsolute(normalizedNewPath)) {
|
||||
return sendError(res, '路径必须是绝对路径');
|
||||
}
|
||||
|
||||
await fsProm.rename(normalizedOldPath, normalizedNewPath);
|
||||
return sendSuccess(res, true);
|
||||
} catch (_error) {
|
||||
return sendError(res, '重命名失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 移动文件/目录
|
||||
export const MoveHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { sourcePath, targetPath } = req.body;
|
||||
|
||||
let normalizedSourcePath: string;
|
||||
let normalizedTargetPath: string;
|
||||
try {
|
||||
normalizedSourcePath = normalizePath(sourcePath);
|
||||
normalizedTargetPath = normalizePath(targetPath);
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保路径是绝对路径
|
||||
if (!path.isAbsolute(normalizedSourcePath) || !path.isAbsolute(normalizedTargetPath)) {
|
||||
return sendError(res, '路径必须是绝对路径');
|
||||
}
|
||||
|
||||
await fsProm.rename(normalizedSourcePath, normalizedTargetPath);
|
||||
return sendSuccess(res, true);
|
||||
} catch (_error) {
|
||||
return sendError(res, '移动失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 批量移动
|
||||
export const BatchMoveHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { items } = req.body;
|
||||
for (const { sourcePath, targetPath } of items) {
|
||||
let normalizedSourcePath: string;
|
||||
let normalizedTargetPath: string;
|
||||
try {
|
||||
normalizedSourcePath = normalizePath(sourcePath);
|
||||
normalizedTargetPath = normalizePath(targetPath);
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保路径是绝对路径
|
||||
if (!path.isAbsolute(normalizedSourcePath) || !path.isAbsolute(normalizedTargetPath)) {
|
||||
return sendError(res, '路径必须是绝对路径');
|
||||
}
|
||||
|
||||
await fsProm.rename(normalizedSourcePath, normalizedTargetPath);
|
||||
}
|
||||
return sendSuccess(res, true);
|
||||
} catch (_error) {
|
||||
return sendError(res, '批量移动失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 新增:文件下载处理方法(注意流式传输,不将整个文件读入内存)
|
||||
export const DownloadHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
let filePath: string;
|
||||
try {
|
||||
filePath = normalizePath(getQueryStringParam(req.query['path']));
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
return sendError(res, '参数错误');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保路径是绝对路径
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
return sendError(res, '路径必须是绝对路径');
|
||||
}
|
||||
|
||||
const stat = await fsProm.stat(filePath);
|
||||
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
let filename = path.basename(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
filename = path.basename(filePath) + '.zip';
|
||||
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
|
||||
const zipStream = new PassThrough();
|
||||
compressing.zip.compressDir(filePath, zipStream as unknown as fs.WriteStream).catch((err) => {
|
||||
console.error('压缩目录失败:', err);
|
||||
res.end();
|
||||
});
|
||||
zipStream.pipe(res);
|
||||
return;
|
||||
}
|
||||
res.setHeader('Content-Length', stat.size);
|
||||
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.pipe(res);
|
||||
} catch (_error) {
|
||||
return sendError(res, '下载失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 批量下载:将多个文件/目录打包为 zip 文件下载
|
||||
export const BatchDownloadHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { paths } = req.body as { paths: string[]; };
|
||||
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
||||
return sendError(res, '参数错误');
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=files.zip');
|
||||
|
||||
const zipStream = new compressing.zip.Stream();
|
||||
// 修改:根据文件类型设置 relativePath
|
||||
for (const filePath of paths) {
|
||||
let normalizedPath: string;
|
||||
try {
|
||||
normalizedPath = normalizePath(filePath);
|
||||
} catch (_pathError) {
|
||||
return sendError(res, '无效的文件路径');
|
||||
}
|
||||
|
||||
// 额外安全检查:确保规范化后的路径是绝对路径
|
||||
if (!path.isAbsolute(normalizedPath)) {
|
||||
return sendError(res, '路径必须是绝对路径');
|
||||
}
|
||||
|
||||
const stat = await fsProm.stat(normalizedPath);
|
||||
if (stat.isDirectory()) {
|
||||
zipStream.addEntry(normalizedPath, { relativePath: '' });
|
||||
} else {
|
||||
// 确保相对路径只使用文件名,防止路径遍历
|
||||
const safeName = path.basename(normalizedPath);
|
||||
zipStream.addEntry(normalizedPath, { relativePath: safeName });
|
||||
}
|
||||
}
|
||||
zipStream.pipe(res);
|
||||
res.on('finish', () => {
|
||||
zipStream.destroy();
|
||||
});
|
||||
} catch (_error) {
|
||||
return sendError(res, '下载失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 修改上传处理方法
|
||||
export const UploadHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
await diskUploader(req, res);
|
||||
return sendSuccess(res, true, '文件上传成功', true);
|
||||
} catch (_error) {
|
||||
let errorMessage = '文件上传失败';
|
||||
|
||||
if (_error instanceof multer.MulterError) {
|
||||
switch (_error.code) {
|
||||
case 'LIMIT_FILE_SIZE':
|
||||
errorMessage = '文件大小超过限制(40MB)';
|
||||
break;
|
||||
case 'LIMIT_UNEXPECTED_FILE':
|
||||
errorMessage = '无效的文件上传字段';
|
||||
break;
|
||||
default:
|
||||
errorMessage = `上传错误: ${_error.message}`;
|
||||
}
|
||||
} else if (_error instanceof Error) {
|
||||
errorMessage = _error.message;
|
||||
}
|
||||
return sendError(res, errorMessage, true);
|
||||
}
|
||||
};
|
||||
|
||||
// 上传WebUI字体文件处理方法
|
||||
export const UploadWebUIFontHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
await webUIFontUploader(req, res);
|
||||
return sendSuccess(res, true, '字体文件上传成功', true);
|
||||
} catch (error) {
|
||||
let errorMessage = '字体文件上传失败';
|
||||
|
||||
if (error instanceof multer.MulterError) {
|
||||
switch (error.code) {
|
||||
case 'LIMIT_FILE_SIZE':
|
||||
errorMessage = '字体文件大小超过限制(40MB)';
|
||||
break;
|
||||
case 'LIMIT_UNEXPECTED_FILE':
|
||||
errorMessage = '无效的文件上传字段';
|
||||
break;
|
||||
default:
|
||||
errorMessage = `上传错误: ${error.message}`;
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
return sendError(res, errorMessage, true);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除WebUI字体文件处理方法
|
||||
export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => {
|
||||
try {
|
||||
const fontPath = WebUiConfig.GetWebUIFontPath();
|
||||
const exists = await WebUiConfig.CheckWebUIFontExist();
|
||||
|
||||
if (!exists) {
|
||||
return sendSuccess(res, true);
|
||||
}
|
||||
|
||||
await fsProm.unlink(fontPath);
|
||||
return sendSuccess(res, true);
|
||||
} catch (_error) {
|
||||
return sendError(res, '删除字体文件失败');
|
||||
}
|
||||
};
|
||||
80
packages/napcat-webui-backend/src/api/Log.ts
Normal file
80
packages/napcat-webui-backend/src/api/Log.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { RequestHandler } from 'express';
|
||||
import { sendError, sendSuccess } from '../utils/response';
|
||||
import { logSubscription } from 'napcat-common/src/log';
|
||||
import { terminalManager } from '../terminal/terminal_manager';
|
||||
import { WebUiConfig } from '@/napcat-webui-backend/index';
|
||||
// 判断是否是 macos
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
|
||||
// 日志脱敏函数
|
||||
const sanitizeLog = (log: string): string => {
|
||||
// 脱敏 token 参数,将 token=xxx 替换为 token=***
|
||||
return log.replace(/token=[\w\d]+/gi, 'token=***');
|
||||
};
|
||||
// 日志记录
|
||||
export const LogHandler: RequestHandler = async (req, res) => {
|
||||
const filename = req.query['id'];
|
||||
if (!filename || typeof filename !== 'string') {
|
||||
return sendError(res, 'ID不能为空');
|
||||
}
|
||||
|
||||
if (filename.includes('..')) {
|
||||
return sendError(res, 'ID不合法');
|
||||
}
|
||||
const logContent = await WebUiConfig.GetLogContent(filename);
|
||||
const sanitizedLogContent = sanitizeLog(logContent);
|
||||
return sendSuccess(res, sanitizedLogContent);
|
||||
};
|
||||
|
||||
// 日志列表
|
||||
export const LogListHandler: RequestHandler = async (_, res) => {
|
||||
const logList = await WebUiConfig.GetLogsList();
|
||||
return sendSuccess(res, logList);
|
||||
};
|
||||
|
||||
// 实时日志(SSE)
|
||||
export const LogRealTimeHandler: RequestHandler = async (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
const listener = (log: string) => {
|
||||
try {
|
||||
const sanitizedLog = sanitizeLog(log);
|
||||
res.write(`data: ${sanitizedLog}\n\n`);
|
||||
} catch (error) {
|
||||
console.error('向客户端写入日志数据时出错:', error);
|
||||
}
|
||||
};
|
||||
logSubscription.subscribe(listener);
|
||||
req.on('close', () => {
|
||||
logSubscription.unsubscribe(listener);
|
||||
});
|
||||
};
|
||||
|
||||
// 终端相关处理器
|
||||
export const CreateTerminalHandler: RequestHandler = async (req, res) => {
|
||||
if (isMacOS) {
|
||||
return sendError(res, 'MacOS不支持终端');
|
||||
}
|
||||
try {
|
||||
const { cols, rows } = req.body;
|
||||
const { id } = terminalManager.createTerminal(cols, rows);
|
||||
return sendSuccess(res, { id });
|
||||
} catch (error) {
|
||||
console.error('Failed to create terminal:', error);
|
||||
return sendError(res, '创建终端失败');
|
||||
}
|
||||
};
|
||||
|
||||
export const GetTerminalListHandler: RequestHandler = (_, res) => {
|
||||
const list = terminalManager.getTerminalList();
|
||||
return sendSuccess(res, list);
|
||||
};
|
||||
|
||||
export const CloseTerminalHandler: RequestHandler = (req, res) => {
|
||||
const id = req.params['id'];
|
||||
if (!id) {
|
||||
return sendError(res, 'ID不能为空');
|
||||
}
|
||||
terminalManager.closeTerminal(id);
|
||||
return sendSuccess(res, {});
|
||||
};
|
||||
60
packages/napcat-webui-backend/src/api/OB11Config.ts
Normal file
60
packages/napcat-webui-backend/src/api/OB11Config.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { loadConfig, OneBotConfig } from 'napcat-onebot/config/config';
|
||||
import { webUiPathWrapper } from '@/napcat-webui-backend/index';
|
||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/helper/Data';
|
||||
import { sendError, sendSuccess } from '@/napcat-webui-backend/utils/response';
|
||||
import { isEmpty } from '@/napcat-webui-backend/utils/check';
|
||||
import json5 from 'json5';
|
||||
|
||||
// 获取OneBot11配置
|
||||
export const OB11GetConfigHandler: RequestHandler = (_, res) => {
|
||||
// 获取QQ登录状态
|
||||
const isLogin = WebUiDataRuntime.getQQLoginStatus();
|
||||
// 如果未登录,返回错误
|
||||
if (!isLogin) {
|
||||
return sendError(res, 'Not Login');
|
||||
}
|
||||
// 获取登录的QQ号
|
||||
const uin = WebUiDataRuntime.getQQLoginUin();
|
||||
// 读取配置文件路径
|
||||
const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`);
|
||||
// 尝试解析配置文件
|
||||
try {
|
||||
// 读取配置文件内容
|
||||
const configFileContent = existsSync(configFilePath)
|
||||
? readFileSync(configFilePath).toString()
|
||||
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString();
|
||||
// 解析配置文件并加载配置
|
||||
const data = loadConfig(json5.parse(configFileContent)) as OneBotConfig;
|
||||
// 返回配置文件
|
||||
return sendSuccess(res, data);
|
||||
} catch (_e) {
|
||||
return sendError(res, 'Config Get Error');
|
||||
}
|
||||
};
|
||||
|
||||
// 写入OneBot11配置
|
||||
export const OB11SetConfigHandler: RequestHandler = async (req, res) => {
|
||||
// 获取QQ登录状态
|
||||
const isLogin = WebUiDataRuntime.getQQLoginStatus();
|
||||
// 如果未登录,返回错误
|
||||
if (!isLogin) {
|
||||
return sendError(res, 'Not Login');
|
||||
}
|
||||
// 如果配置为空,返回错误
|
||||
if (isEmpty(req.body.config)) {
|
||||
return sendError(res, 'config is empty');
|
||||
}
|
||||
// 写入配置
|
||||
try {
|
||||
// 解析并加载配置
|
||||
const config = loadConfig(json5.parse(req.body.config)) as OneBotConfig;
|
||||
// 写入配置
|
||||
await WebUiDataRuntime.setOB11Config(config);
|
||||
return sendSuccess(res, null);
|
||||
} catch (e) {
|
||||
return sendError(res, 'Error: ' + e);
|
||||
}
|
||||
};
|
||||
14
packages/napcat-webui-backend/src/api/Proxy.ts
Normal file
14
packages/napcat-webui-backend/src/api/Proxy.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { RequestUtil } from '@/napcat-common/request';
|
||||
import { sendError, sendSuccess } from '../utils/response';
|
||||
|
||||
export const GetProxyHandler: RequestHandler = async (req, res) => {
|
||||
let { url } = req.query;
|
||||
if (url && typeof url === 'string') {
|
||||
url = decodeURIComponent(url);
|
||||
const responseText = await RequestUtil.HttpGetText(url);
|
||||
return sendSuccess(res, responseText);
|
||||
} else {
|
||||
return sendError(res, 'url参数不合法');
|
||||
}
|
||||
};
|
||||
90
packages/napcat-webui-backend/src/api/QQLogin.ts
Normal file
90
packages/napcat-webui-backend/src/api/QQLogin.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { RequestHandler } from 'express';
|
||||
|
||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/helper/Data';
|
||||
import { isEmpty } from '@/napcat-webui-backend/utils/check';
|
||||
import { sendError, sendSuccess } from '@/napcat-webui-backend/utils/response';
|
||||
import { WebUiConfig } from '@/napcat-webui-backend/index';
|
||||
|
||||
// 获取QQ登录二维码
|
||||
export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
|
||||
// 判断是否已经登录
|
||||
if (WebUiDataRuntime.getQQLoginStatus()) {
|
||||
// 已经登录
|
||||
return sendError(res, 'QQ Is Logined');
|
||||
}
|
||||
// 获取二维码
|
||||
const qrcodeUrl = WebUiDataRuntime.getQQLoginQrcodeURL();
|
||||
// 判断二维码是否为空
|
||||
if (isEmpty(qrcodeUrl)) {
|
||||
return sendError(res, 'QRCode Get Error');
|
||||
}
|
||||
// 返回二维码URL
|
||||
const data = {
|
||||
qrcode: qrcodeUrl,
|
||||
};
|
||||
return sendSuccess(res, data);
|
||||
};
|
||||
|
||||
// 获取QQ登录状态
|
||||
export const QQCheckLoginStatusHandler: RequestHandler = async (_, res) => {
|
||||
const data = {
|
||||
isLogin: WebUiDataRuntime.getQQLoginStatus(),
|
||||
qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(),
|
||||
};
|
||||
return sendSuccess(res, data);
|
||||
};
|
||||
|
||||
// 快速登录
|
||||
export const QQSetQuickLoginHandler: RequestHandler = async (req, res) => {
|
||||
// 获取QQ号
|
||||
const { uin } = req.body;
|
||||
// 判断是否已经登录
|
||||
const isLogin = WebUiDataRuntime.getQQLoginStatus();
|
||||
if (isLogin) {
|
||||
return sendError(res, 'QQ Is Logined');
|
||||
}
|
||||
// 判断QQ号是否为空
|
||||
if (isEmpty(uin)) {
|
||||
return sendError(res, 'uin is empty');
|
||||
}
|
||||
|
||||
// 获取快速登录状态
|
||||
const { result, message } = await WebUiDataRuntime.requestQuickLogin(uin);
|
||||
if (!result) {
|
||||
return sendError(res, message);
|
||||
}
|
||||
// 本来应该验证 但是http不宜这么搞 建议前端验证
|
||||
// isLogin = WebUiDataRuntime.getQQLoginStatus();
|
||||
return sendSuccess(res, null);
|
||||
};
|
||||
|
||||
// 获取快速登录列表
|
||||
export const QQGetQuickLoginListHandler: RequestHandler = async (_, res) => {
|
||||
const quickLoginList = WebUiDataRuntime.getQQQuickLoginList();
|
||||
return sendSuccess(res, quickLoginList);
|
||||
};
|
||||
|
||||
// 获取快速登录列表(新)
|
||||
export const QQGetLoginListNewHandler: RequestHandler = async (_, res) => {
|
||||
const newLoginList = WebUiDataRuntime.getQQNewLoginList();
|
||||
return sendSuccess(res, newLoginList);
|
||||
};
|
||||
|
||||
// 获取登录的QQ的信息
|
||||
export const getQQLoginInfoHandler: RequestHandler = async (_, res) => {
|
||||
const data = WebUiDataRuntime.getQQLoginInfo();
|
||||
return sendSuccess(res, data);
|
||||
};
|
||||
|
||||
// 获取自动登录QQ账号
|
||||
export const getAutoLoginAccountHandler: RequestHandler = async (_, res) => {
|
||||
const data = WebUiConfig.getAutoLoginAccount();
|
||||
return sendSuccess(res, data);
|
||||
};
|
||||
|
||||
// 设置自动登录QQ账号
|
||||
export const setAutoLoginAccountHandler: RequestHandler = async (req, res) => {
|
||||
const { uin } = req.body;
|
||||
await WebUiConfig.UpdateAutoLoginAccount(uin);
|
||||
return sendSuccess(res, null);
|
||||
};
|
||||
19
packages/napcat-webui-backend/src/api/Status.ts
Normal file
19
packages/napcat-webui-backend/src/api/Status.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { SystemStatus, statusHelperSubscription } from '@/napcat-core/helper/status';
|
||||
|
||||
export const StatusRealTimeHandler: RequestHandler = async (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
const sendStatus = (status: SystemStatus) => {
|
||||
try {
|
||||
res.write(`data: ${JSON.stringify(status)}\n\n`);
|
||||
} catch (e) {
|
||||
console.error(`An error occurred when writing sendStatus data to client: ${e}`);
|
||||
}
|
||||
};
|
||||
statusHelperSubscription.on('statusUpdate', sendStatus);
|
||||
req.on('close', () => {
|
||||
statusHelperSubscription.off('statusUpdate', sendStatus);
|
||||
res.end();
|
||||
});
|
||||
};
|
||||
127
packages/napcat-webui-backend/src/api/WebUIConfig.ts
Normal file
127
packages/napcat-webui-backend/src/api/WebUIConfig.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { WebUiConfig } from '@/napcat-webui-backend/index';
|
||||
import { sendError, sendSuccess } from '@/napcat-webui-backend/utils/response';
|
||||
import { isEmpty } from '@/napcat-webui-backend/utils/check';
|
||||
|
||||
// 获取WebUI基础配置
|
||||
export const GetWebUIConfigHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const config = await WebUiConfig.GetWebUIConfig();
|
||||
return sendSuccess(res, {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
loginRate: config.loginRate,
|
||||
disableWebUI: config.disableWebUI,
|
||||
disableNonLANAccess: config.disableNonLANAccess,
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
return sendError(res, `获取WebUI配置失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取是否禁用WebUI
|
||||
export const GetDisableWebUIHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const disable = await WebUiConfig.GetDisableWebUI();
|
||||
return sendSuccess(res, disable);
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
return sendError(res, `获取WebUI禁用状态失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新是否禁用WebUI
|
||||
export const UpdateDisableWebUIHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { disable } = req.body;
|
||||
|
||||
if (typeof disable !== 'boolean') {
|
||||
return sendError(res, 'disable参数必须是布尔值');
|
||||
}
|
||||
|
||||
await WebUiConfig.UpdateDisableWebUI(disable);
|
||||
return sendSuccess(res, null);
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
return sendError(res, `更新WebUI禁用状态失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取是否禁用非局域网访问
|
||||
export const GetDisableNonLANAccessHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const disable = await WebUiConfig.GetDisableNonLANAccess();
|
||||
return sendSuccess(res, disable);
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
return sendError(res, `获取非局域网访问禁用状态失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新是否禁用非局域网访问
|
||||
export const UpdateDisableNonLANAccessHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { disable } = req.body;
|
||||
|
||||
if (typeof disable !== 'boolean') {
|
||||
return sendError(res, 'disable参数必须是布尔值');
|
||||
}
|
||||
|
||||
await WebUiConfig.UpdateDisableNonLANAccess(disable);
|
||||
return sendSuccess(res, null);
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
return sendError(res, `更新非局域网访问禁用状态失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新WebUI基础配置
|
||||
export const UpdateWebUIConfigHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { host, port, loginRate, disableWebUI, disableNonLANAccess } = req.body;
|
||||
|
||||
const updateConfig: any = {};
|
||||
|
||||
if (host !== undefined) {
|
||||
if (isEmpty(host)) {
|
||||
return sendError(res, 'host不能为空');
|
||||
}
|
||||
updateConfig.host = host;
|
||||
}
|
||||
|
||||
if (port !== undefined) {
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
return sendError(res, 'port必须是1-65535之间的整数');
|
||||
}
|
||||
updateConfig.port = port;
|
||||
}
|
||||
|
||||
if (loginRate !== undefined) {
|
||||
if (!Number.isInteger(loginRate) || loginRate < 1) {
|
||||
return sendError(res, 'loginRate必须是大于0的整数');
|
||||
}
|
||||
updateConfig.loginRate = loginRate;
|
||||
}
|
||||
|
||||
if (disableWebUI !== undefined) {
|
||||
if (typeof disableWebUI !== 'boolean') {
|
||||
return sendError(res, 'disableWebUI必须是布尔值');
|
||||
}
|
||||
updateConfig.disableWebUI = disableWebUI;
|
||||
}
|
||||
|
||||
if (disableNonLANAccess !== undefined) {
|
||||
if (typeof disableNonLANAccess !== 'boolean') {
|
||||
return sendError(res, 'disableNonLANAccess必须是布尔值');
|
||||
}
|
||||
updateConfig.disableNonLANAccess = disableNonLANAccess;
|
||||
}
|
||||
|
||||
await WebUiConfig.UpdateWebUIConfig(updateConfig);
|
||||
return sendSuccess(res, null);
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
return sendError(res, `更新WebUI配置失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user