mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Merge pull request #567 from bietiaop/webapi-bietiaop
refactor:优化WebUI后端代码格式(无新功能添加)
This commit is contained in:
@@ -52,9 +52,9 @@
|
||||
"dependencies": {
|
||||
"express": "^5.0.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"piscina": "^4.7.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.0",
|
||||
"piscina": "^4.7.0"
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,20 @@
|
||||
/**
|
||||
* @file WebUI服务入口文件
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { ALLRouter } from './src/router';
|
||||
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { NapCatPathWrapper } from '@/common/path';
|
||||
import { WebUiConfigWrapper } from './src/helper/config';
|
||||
import { RequestUtil } from '@/common/request';
|
||||
import { isIP } from "node:net";
|
||||
|
||||
import { WebUiConfigWrapper } from '@webapi/helper/config';
|
||||
import { ALLRouter } from '@webapi/router';
|
||||
import { cors } from '@webapi/middleware/cors';
|
||||
import { createUrl } from '@webapi/utils/url';
|
||||
import { sendSuccess } from '@webapi/utils/response';
|
||||
|
||||
// 实例化Express
|
||||
const app = express();
|
||||
|
||||
/**
|
||||
@@ -26,49 +35,51 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
||||
log('[NapCat] [WebUi] Current WebUi is not run.');
|
||||
return;
|
||||
}
|
||||
|
||||
// ------------注册中间件------------
|
||||
// 使用express的json中间件
|
||||
app.use(express.json());
|
||||
// 初始服务
|
||||
|
||||
// CORS中间件
|
||||
// TODO:
|
||||
app.use(cors);
|
||||
// ------------中间件结束------------
|
||||
|
||||
// ------------挂载路由------------
|
||||
// 挂载静态路由(前端),路径为 [/前缀]/webui
|
||||
app.use(config.prefix + '/webui', express.static(pathWrapper.staticPath));
|
||||
// 挂载API接口
|
||||
app.use(config.prefix + '/api', ALLRouter);
|
||||
|
||||
// 初始服务(先放个首页)
|
||||
// WebUI只在config.prefix所示路径上提供服务,可配合Nginx挂载到子目录中
|
||||
app.all(config.prefix + '/', (_req, res) => {
|
||||
res.json({
|
||||
msg: 'NapCat WebAPI is now running!',
|
||||
sendSuccess(res, null, 'NapCat WebAPI is now running!');
|
||||
});
|
||||
});
|
||||
// 配置静态文件服务,提供./static目录下的文件服务,访问路径为/webui
|
||||
app.use(config.prefix + '/webui', express.static(pathWrapper.staticPath));
|
||||
//挂载API接口
|
||||
// 添加CORS支持
|
||||
// TODO:
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
||||
next();
|
||||
});
|
||||
app.use(config.prefix + '/api', ALLRouter);
|
||||
// ------------路由挂载结束------------
|
||||
|
||||
// ------------启动服务------------
|
||||
app.listen(config.port, config.host, async () => {
|
||||
const normalizeHost = (host: string) => {
|
||||
if (host === '0.0.0.0') return '127.0.0.1';
|
||||
if (isIP(host) === 6) return `[${host}]`;
|
||||
return host;
|
||||
};
|
||||
const createUrl = (host: string, path: string, token: string) => {
|
||||
const url = new URL(`http://${normalizeHost(host)}`);
|
||||
url.port = config.port.toString();
|
||||
url.pathname = `${config.prefix}${path}`;
|
||||
url.searchParams.set('token', token);
|
||||
return url.toString();
|
||||
};
|
||||
// 启动后打印出相关地址
|
||||
|
||||
const port = config.port.toString(),
|
||||
searchParams = { token: config.token },
|
||||
path = `${config.prefix}/webui`;
|
||||
|
||||
// 打印日志(地址、token)
|
||||
log(`[NapCat] [WebUi] Current WebUi is running at http://${config.host}:${config.port}${config.prefix}`);
|
||||
log(`[NapCat] [WebUi] Login Token is ${config.token}`);
|
||||
log(`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(config.host, '/webui', config.token)}`);
|
||||
log(`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', '/webui', config.token)}`);
|
||||
log(`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(config.host, port, path, searchParams)}`);
|
||||
log(`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', port, path, searchParams)}`);
|
||||
|
||||
// 获取公网地址
|
||||
try {
|
||||
const publishUrl = 'https://ip.011102.xyz/';
|
||||
const data = await RequestUtil.HttpGetJson<{ IP: { IP: string } }>(publishUrl, 'GET', {}, {}, true, true);
|
||||
log(`[NapCat] [WebUi] WebUi Publish Panel Url: ${createUrl(data.IP.IP, '/webui', config.token)}`);
|
||||
log(`[NapCat] [WebUi] WebUi Publish Panel Url: ${createUrl(data.IP.IP, port, path, searchParams)}`);
|
||||
} catch (err) {
|
||||
logger.logError(`[NapCat] [WebUi] Get Publish Panel Url Error: ${err}`);
|
||||
}
|
||||
});
|
||||
// ------------Over!------------
|
||||
}
|
||||
|
@@ -1,69 +1,64 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { AuthHelper } from '../helper/SignToken';
|
||||
import { WebUiDataRuntime } from '../helper/Data';
|
||||
|
||||
import { WebUiConfig } from '@/webui';
|
||||
|
||||
const isEmpty = (data: any) => data === undefined || data === null || data === '';
|
||||
import { AuthHelper } from '@webapi/helper/SignToken';
|
||||
import { WebUiDataRuntime } from '@webapi/helper/Data';
|
||||
import { sendSuccess, sendError } from '@webapi/utils/response';
|
||||
import { isEmpty } from '@webapi/utils/check';
|
||||
|
||||
// 登录
|
||||
export const LoginHandler: RequestHandler = async (req, res) => {
|
||||
// 获取WebUI配置
|
||||
const WebUiConfigData = await WebUiConfig.GetWebUIConfig();
|
||||
// 获取请求体中的token
|
||||
const { token } = req.body;
|
||||
// 如果token为空,返回错误信息
|
||||
if (isEmpty(token)) {
|
||||
res.json({
|
||||
code: -1,
|
||||
message: 'token is empty',
|
||||
});
|
||||
return;
|
||||
return sendError(res, 'token is empty');
|
||||
}
|
||||
if (!await WebUiDataRuntime.checkLoginRate(WebUiConfigData.loginRate)) {
|
||||
res.json({
|
||||
code: -1,
|
||||
message: 'login rate limit',
|
||||
});
|
||||
return;
|
||||
// 检查登录频率
|
||||
if (!(await WebUiDataRuntime.checkLoginRate(WebUiConfigData.loginRate))) {
|
||||
return sendError(res, 'login rate limit');
|
||||
}
|
||||
//验证config.token是否等于token
|
||||
if (WebUiConfigData.token !== token) {
|
||||
res.json({
|
||||
code: -1,
|
||||
message: 'token is invalid',
|
||||
});
|
||||
return;
|
||||
return sendError(res, 'token is invalid');
|
||||
}
|
||||
const signCredential = Buffer.from(JSON.stringify(await AuthHelper.signCredential(WebUiConfigData.token))).toString('base64');
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
'Credential': signCredential,
|
||||
},
|
||||
// 签发凭证
|
||||
const signCredential = Buffer.from(JSON.stringify(await AuthHelper.signCredential(WebUiConfigData.token))).toString(
|
||||
'base64'
|
||||
);
|
||||
// 返回成功信息
|
||||
return sendSuccess(res, {
|
||||
Credential: signCredential,
|
||||
});
|
||||
return;
|
||||
};
|
||||
export const LogoutHandler: RequestHandler = (req, res) => {
|
||||
// 这玩意无状态销毁个灯 得想想办法
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
});
|
||||
return;
|
||||
|
||||
// 退出登录
|
||||
export const LogoutHandler: RequestHandler = (_, res) => {
|
||||
// TODO: 这玩意无状态销毁个灯 得想想办法
|
||||
return sendSuccess(res, null);
|
||||
};
|
||||
|
||||
// 检查登录状态
|
||||
export const checkHandler: RequestHandler = async (req, res) => {
|
||||
// 获取WebUI配置
|
||||
const WebUiConfigData = await WebUiConfig.GetWebUIConfig();
|
||||
// 获取请求头中的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());
|
||||
// 验证凭证是否在一小时内有效
|
||||
await AuthHelper.validateCredentialWithinOneHour(WebUiConfigData.token, Credential);
|
||||
res.json({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
});
|
||||
return;
|
||||
// 返回成功信息
|
||||
return sendSuccess(res, null);
|
||||
} catch (e) {
|
||||
res.json({
|
||||
code: -1,
|
||||
message: 'failed',
|
||||
});
|
||||
// 返回错误信息
|
||||
return sendError(res, 'Authorization Faild');
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
@@ -1,14 +1,15 @@
|
||||
import { RequestHandler } from 'express';
|
||||
|
||||
export const LogFileListHandler: RequestHandler = async (req, res) => {
|
||||
res.send({
|
||||
code: 0,
|
||||
data: {
|
||||
import { sendSuccess } from '@webapi/utils/response';
|
||||
|
||||
// TODO: Implement LogFileListHandler
|
||||
export const LogFileListHandler: RequestHandler = async (_, res) => {
|
||||
const fakeData = {
|
||||
uin: 0,
|
||||
nick: 'NapCat',
|
||||
avatar: 'https://q1.qlogo.cn/g?b=qq&nk=0&s=640',
|
||||
status: 'online',
|
||||
boottime: Date.now()
|
||||
}
|
||||
});
|
||||
boottime: Date.now(),
|
||||
};
|
||||
sendSuccess(res, fakeData);
|
||||
};
|
||||
|
@@ -1,79 +1,58 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { WebUiDataRuntime } from '../helper/Data';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { OneBotConfig } from '@/onebot/config/config';
|
||||
import { resolve } from 'node:path';
|
||||
import { webUiPathWrapper } from '@/webui';
|
||||
|
||||
const isEmpty = (data: any) => data === undefined || data === null || data === '';
|
||||
export const OB11GetConfigHandler: RequestHandler = async (req, res) => {
|
||||
import { OneBotConfig } from '@/onebot/config/config';
|
||||
|
||||
import { webUiPathWrapper } from '@/webui';
|
||||
import { WebUiDataRuntime } from '@webapi/helper/Data';
|
||||
import { sendError, sendSuccess } from '@webapi/utils/response';
|
||||
import { isEmpty } from '@webapi/utils/check';
|
||||
|
||||
// 获取OneBot11配置
|
||||
export const OB11GetConfigHandler: RequestHandler = async (_, res) => {
|
||||
// 获取QQ登录状态
|
||||
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
|
||||
// 如果未登录,返回错误
|
||||
if (!isLogin) {
|
||||
res.send({
|
||||
code: -1,
|
||||
message: 'Not Login',
|
||||
});
|
||||
return;
|
||||
return sendError(res, 'Not Login');
|
||||
}
|
||||
// 获取登录的QQ号
|
||||
const uin = await WebUiDataRuntime.getQQLoginUin();
|
||||
// 读取配置文件
|
||||
const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`);
|
||||
//console.log(configFilePath);
|
||||
let data: OneBotConfig;
|
||||
// 尝试解析配置文件
|
||||
try {
|
||||
data = JSON.parse(
|
||||
// 读取配置文件
|
||||
const data = JSON.parse(
|
||||
existsSync(configFilePath)
|
||||
? readFileSync(configFilePath).toString()
|
||||
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString()
|
||||
);
|
||||
) as OneBotConfig;
|
||||
// 返回配置文件
|
||||
return sendSuccess(res, data);
|
||||
} catch (e) {
|
||||
data = {} as OneBotConfig;
|
||||
res.send({
|
||||
code: -1,
|
||||
message: 'Config Get Error',
|
||||
});
|
||||
return;
|
||||
return sendError(res, 'Config Get Error');
|
||||
}
|
||||
res.send({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: data,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
// 写入OneBot11配置
|
||||
export const OB11SetConfigHandler: RequestHandler = async (req, res) => {
|
||||
// 获取QQ登录状态
|
||||
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
|
||||
// 如果未登录,返回错误
|
||||
if (!isLogin) {
|
||||
res.send({
|
||||
code: -1,
|
||||
message: 'Not Login',
|
||||
});
|
||||
return;
|
||||
return sendError(res, 'Not Login');
|
||||
}
|
||||
// 如果配置为空,返回错误
|
||||
if (isEmpty(req.body.config)) {
|
||||
res.send({
|
||||
code: -1,
|
||||
message: 'config is empty',
|
||||
});
|
||||
return;
|
||||
return sendError(res, 'config is empty');
|
||||
}
|
||||
let SetResult;
|
||||
// 写入配置
|
||||
try {
|
||||
await WebUiDataRuntime.setOB11Config(JSON.parse(req.body.config));
|
||||
SetResult = true;
|
||||
return sendSuccess(res, null);
|
||||
} catch (e) {
|
||||
SetResult = false;
|
||||
return sendError(res, 'Config Set Error');
|
||||
}
|
||||
if (SetResult) {
|
||||
res.send({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
});
|
||||
} else {
|
||||
res.send({
|
||||
code: -1,
|
||||
message: 'Config Set Error',
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
@@ -1,78 +1,64 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { WebUiDataRuntime } from '../helper/Data';
|
||||
|
||||
const isEmpty = (data: any) => data === undefined || data === null || data === '';
|
||||
import { WebUiDataRuntime } from '@webapi/helper/Data';
|
||||
import { isEmpty } from '@webapi/utils/check';
|
||||
import { sendError, sendSuccess } from '@webapi/utils/response';
|
||||
|
||||
// 获取QQ登录二维码
|
||||
export const QQGetQRcodeHandler: RequestHandler = async (req, res) => {
|
||||
// 判断是否已经登录
|
||||
if (await WebUiDataRuntime.getQQLoginStatus()) {
|
||||
res.send({
|
||||
code: -1,
|
||||
message: 'QQ Is Logined',
|
||||
});
|
||||
return;
|
||||
// 已经登录
|
||||
return sendError(res, 'QQ Is Logined');
|
||||
}
|
||||
// 获取二维码
|
||||
const qrcodeUrl = await WebUiDataRuntime.getQQLoginQrcodeURL();
|
||||
// 判断二维码是否为空
|
||||
if (isEmpty(qrcodeUrl)) {
|
||||
res.send({
|
||||
code: -1,
|
||||
message: 'QRCode Get Error',
|
||||
});
|
||||
return;
|
||||
return sendError(res, 'QRCode Get Error');
|
||||
}
|
||||
res.send({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
// 返回二维码URL
|
||||
const data = {
|
||||
qrcode: qrcodeUrl,
|
||||
},
|
||||
});
|
||||
return;
|
||||
};
|
||||
return sendSuccess(res, data);
|
||||
};
|
||||
|
||||
// 获取QQ登录状态
|
||||
export const QQCheckLoginStatusHandler: RequestHandler = async (req, res) => {
|
||||
res.send({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
const data = {
|
||||
isLogin: await WebUiDataRuntime.getQQLoginStatus(),
|
||||
qrcodeurl: await WebUiDataRuntime.getQQLoginQrcodeURL()
|
||||
},
|
||||
});
|
||||
qrcodeurl: await WebUiDataRuntime.getQQLoginQrcodeURL(),
|
||||
};
|
||||
return sendSuccess(res, data);
|
||||
};
|
||||
|
||||
// 快速登录
|
||||
export const QQSetQuickLoginHandler: RequestHandler = async (req, res) => {
|
||||
// 获取QQ号
|
||||
const { uin } = req.body;
|
||||
// 判断是否已经登录
|
||||
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
|
||||
if (isLogin) {
|
||||
res.send({
|
||||
code: -1,
|
||||
message: 'QQ Is Logined',
|
||||
});
|
||||
return;
|
||||
return sendError(res, 'QQ Is Logined');
|
||||
}
|
||||
// 判断QQ号是否为空
|
||||
if (isEmpty(uin)) {
|
||||
res.send({
|
||||
code: -1,
|
||||
message: 'uin is empty',
|
||||
});
|
||||
return;
|
||||
return sendError(res, 'uin is empty');
|
||||
}
|
||||
|
||||
// 获取快速登录状态
|
||||
const { result, message } = await WebUiDataRuntime.requestQuickLogin(uin);
|
||||
if (!result) {
|
||||
res.send({
|
||||
code: -1,
|
||||
message: message,
|
||||
});
|
||||
return;
|
||||
return sendError(res, message);
|
||||
}
|
||||
//本来应该验证 但是http不宜这么搞 建议前端验证
|
||||
//isLogin = await WebUiDataRuntime.getQQLoginStatus();
|
||||
res.send({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
});
|
||||
return sendSuccess(res, null);
|
||||
};
|
||||
export const QQGetQuickLoginListHandler: RequestHandler = async (req, res) => {
|
||||
|
||||
// 获取快速登录列表
|
||||
export const QQGetQuickLoginListHandler: RequestHandler = async (_, res) => {
|
||||
const quickLoginList = await WebUiDataRuntime.getQQQuickLoginList();
|
||||
res.send({
|
||||
code: 0,
|
||||
data: quickLoginList,
|
||||
});
|
||||
return sendSuccess(res, quickLoginList);
|
||||
};
|
||||
|
13
src/webui/src/const/status.ts
Normal file
13
src/webui/src/const/status.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export enum HttpStatusCode {
|
||||
OK = 200,
|
||||
BadRequest = 400,
|
||||
Unauthorized = 401,
|
||||
Forbidden = 403,
|
||||
NotFound = 404,
|
||||
InternalServerError = 500,
|
||||
}
|
||||
|
||||
export enum ResponseCode {
|
||||
Success = 0,
|
||||
Error = -1,
|
||||
}
|
@@ -1,18 +1,5 @@
|
||||
import { OneBotConfig } from '@/onebot/config/config';
|
||||
|
||||
interface LoginRuntimeType {
|
||||
LoginCurrentTime: number;
|
||||
LoginCurrentRate: number;
|
||||
QQLoginStatus: boolean;
|
||||
QQQRCodeURL: string;
|
||||
QQLoginUin: string;
|
||||
NapCatHelper: {
|
||||
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>;
|
||||
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
|
||||
QQLoginList: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const LoginRuntime: LoginRuntimeType = {
|
||||
LoginCurrentTime: Date.now(),
|
||||
LoginCurrentRate: 0,
|
||||
|
@@ -1,15 +1,5 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
interface WebUiCredentialInnerJson {
|
||||
CreatedTime: number;
|
||||
TokenEncoded: string;
|
||||
}
|
||||
|
||||
interface WebUiCredentialJson {
|
||||
Data: WebUiCredentialInnerJson;
|
||||
Hmac: string;
|
||||
}
|
||||
|
||||
export class AuthHelper {
|
||||
private static readonly secretKey = Math.random().toString(36).slice(2);
|
||||
|
||||
@@ -24,9 +14,7 @@ export class AuthHelper {
|
||||
TokenEncoded: token,
|
||||
};
|
||||
const jsonString = JSON.stringify(innerJson);
|
||||
const hmac = crypto.createHmac('sha256', AuthHelper.secretKey)
|
||||
.update(jsonString, 'utf8')
|
||||
.digest('hex');
|
||||
const hmac = crypto.createHmac('sha256', AuthHelper.secretKey).update(jsonString, 'utf8').digest('hex');
|
||||
return { Data: innerJson, Hmac: hmac };
|
||||
}
|
||||
|
||||
@@ -38,7 +26,8 @@ export class AuthHelper {
|
||||
public static async checkCredential(credentialJson: WebUiCredentialJson): Promise<boolean> {
|
||||
try {
|
||||
const jsonString = JSON.stringify(credentialJson.Data);
|
||||
const calculatedHmac = crypto.createHmac('sha256', AuthHelper.secretKey)
|
||||
const calculatedHmac = crypto
|
||||
.createHmac('sha256', AuthHelper.secretKey)
|
||||
.update(jsonString, 'utf8')
|
||||
.digest('hex');
|
||||
return calculatedHmac === credentialJson.Hmac;
|
||||
@@ -53,7 +42,10 @@ export class AuthHelper {
|
||||
* @param credentialJson 已签名的凭证JSON对象。
|
||||
* @returns 布尔值,表示凭证是否有效且token匹配。
|
||||
*/
|
||||
public static async validateCredentialWithinOneHour(token: string, credentialJson: WebUiCredentialJson): Promise<boolean> {
|
||||
public static async validateCredentialWithinOneHour(
|
||||
token: string,
|
||||
credentialJson: WebUiCredentialJson
|
||||
): Promise<boolean> {
|
||||
const isValid = await AuthHelper.checkCredential(credentialJson);
|
||||
if (!isValid) {
|
||||
return false;
|
||||
|
@@ -3,7 +3,6 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import * as net from 'node:net';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
|
||||
// 限制尝试端口的次数,避免死循环
|
||||
const MAX_PORT_TRY = 100;
|
||||
|
||||
@@ -64,14 +63,6 @@ async function tryUsePort(port: number, host: string, tryCount: number = 0): Pro
|
||||
});
|
||||
}
|
||||
|
||||
export interface WebUiConfigType {
|
||||
host: string;
|
||||
port: number;
|
||||
prefix: string;
|
||||
token: string;
|
||||
loginRate: number;
|
||||
}
|
||||
|
||||
// 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件
|
||||
export class WebUiConfigWrapper {
|
||||
WebUiConfigData: WebUiConfigType | undefined = undefined;
|
||||
@@ -114,14 +105,18 @@ export class WebUiConfigWrapper {
|
||||
// 不希望回写的配置放后面
|
||||
|
||||
// 查询主机地址是否可用
|
||||
const [host_err, host] = await tryUseHost(parsedConfig.host).then(data => [null, data]).catch(err => [err, null]);
|
||||
const [host_err, host] = await tryUseHost(parsedConfig.host)
|
||||
.then((data) => [null, data])
|
||||
.catch((err) => [err, null]);
|
||||
if (host_err) {
|
||||
console.log('host不可用', host_err);
|
||||
parsedConfig.port = 0; // 设置为0,禁用WebUI
|
||||
} else {
|
||||
parsedConfig.host = host;
|
||||
// 修正端口占用情况
|
||||
const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host).then(data => [null, data]).catch(err => [err, null]);
|
||||
const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host)
|
||||
.then((data) => [null, data])
|
||||
.catch((err) => [err, null]);
|
||||
if (port_err) {
|
||||
console.log('port不可用', port_err);
|
||||
parsedConfig.port = 0; // 设置为0,禁用WebUI
|
||||
@@ -137,4 +132,3 @@ export class WebUiConfigWrapper {
|
||||
return defaultconfig; // 理论上这行代码到不了,到了只能返回默认配置了
|
||||
}
|
||||
}
|
||||
|
||||
|
46
src/webui/src/middleware/auth.ts
Normal file
46
src/webui/src/middleware/auth.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
import { WebUiConfig } from '@/webui';
|
||||
|
||||
import { AuthHelper } from '@webapi/helper/SignToken';
|
||||
import { sendError } from '@webapi/utils/response';
|
||||
|
||||
// 鉴权中间件
|
||||
export async function auth(req: Request, res: Response, next: NextFunction) {
|
||||
// 判断当前url是否为/login 如果是跳过鉴权
|
||||
if (req.url == '/auth/login') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 判断是否有Authorization头
|
||||
if (req.headers?.authorization) {
|
||||
// 切割参数以获取token
|
||||
const authorization = req.headers.authorization.split(' ');
|
||||
// 当Bearer后面没有参数时
|
||||
if (authorization.length < 2) {
|
||||
return sendError(res, 'Unauthorized');
|
||||
}
|
||||
// 获取token
|
||||
const token = authorization[1];
|
||||
// 解析token
|
||||
let Credential: WebUiCredentialJson;
|
||||
try {
|
||||
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
|
||||
} catch (e) {
|
||||
return sendError(res, 'Unauthorized');
|
||||
}
|
||||
// 获取配置
|
||||
const config = await WebUiConfig.GetWebUIConfig();
|
||||
// 验证凭证在1小时内有效且token与原始token相同
|
||||
const credentialJson = await AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
|
||||
if (credentialJson) {
|
||||
// 通过验证
|
||||
return next();
|
||||
}
|
||||
// 验证失败
|
||||
return sendError(res, 'Unauthorized');
|
||||
}
|
||||
|
||||
// 没有Authorization头
|
||||
return sendError(res, 'Unauthorized');
|
||||
}
|
9
src/webui/src/middleware/cors.ts
Normal file
9
src/webui/src/middleware/cors.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { RequestHandler } from 'express';
|
||||
|
||||
// CORS 中间件,跨域用
|
||||
export const cors: RequestHandler = (_, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
||||
next();
|
||||
};
|
@@ -1,7 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import { OB11GetConfigHandler, OB11SetConfigHandler } from '../api/OB11Config';
|
||||
|
||||
import { OB11GetConfigHandler, OB11SetConfigHandler } from '@webapi/api/OB11Config';
|
||||
|
||||
const router = Router();
|
||||
// router:读取配置
|
||||
router.post('/GetConfig', OB11GetConfigHandler);
|
||||
// router:写入配置
|
||||
router.post('/SetConfig', OB11SetConfigHandler);
|
||||
|
||||
export { router as OB11ConfigRouter };
|
||||
|
@@ -1,14 +1,20 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
import {
|
||||
QQCheckLoginStatusHandler,
|
||||
QQGetQRcodeHandler,
|
||||
QQGetQuickLoginListHandler,
|
||||
QQSetQuickLoginHandler,
|
||||
} from '../api/QQLogin';
|
||||
} from '@webapi/api/QQLogin';
|
||||
|
||||
const router = Router();
|
||||
// router:获取快速登录列表
|
||||
router.all('/GetQuickLoginList', QQGetQuickLoginListHandler);
|
||||
// router:检查QQ登录状态
|
||||
router.post('/CheckLoginStatus', QQCheckLoginStatusHandler);
|
||||
// router:获取QQ登录二维码
|
||||
router.post('/GetQQLoginQrcode', QQGetQRcodeHandler);
|
||||
// router:设置QQ快速登录
|
||||
router.post('/SetQuickLogin', QQSetQuickLoginHandler);
|
||||
|
||||
export { router as QQLoginRouter };
|
||||
|
@@ -1,9 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import { checkHandler, LoginHandler, LogoutHandler } from '../api/Auth';
|
||||
|
||||
import { checkHandler, LoginHandler, LogoutHandler } from '@webapi/api/Auth';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// router:登录
|
||||
router.post('/login', LoginHandler);
|
||||
// router:检查登录状态
|
||||
router.post('/check', checkHandler);
|
||||
// router:注销
|
||||
router.post('/logout', LogoutHandler);
|
||||
|
||||
export { router as AuthRouter };
|
||||
|
@@ -1,67 +1,30 @@
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import { AuthHelper } from '../../src/helper/SignToken';
|
||||
import { QQLoginRouter } from './QQLogin';
|
||||
import { AuthRouter } from './auth';
|
||||
import { OB11ConfigRouter } from './OB11Config';
|
||||
import { WebUiConfig } from '@/webui';
|
||||
/**
|
||||
* @file 所有路由的入口文件
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
|
||||
import { OB11ConfigRouter } from '@webapi/router/OB11Config';
|
||||
import { auth } from '@webapi/middleware/auth';
|
||||
import { sendSuccess } from '@webapi/utils/response';
|
||||
|
||||
import { QQLoginRouter } from '@webapi/router/QQLogin';
|
||||
import { AuthRouter } from '@webapi/router/auth';
|
||||
|
||||
const router = Router();
|
||||
|
||||
export async function AuthApi(req: Request, res: Response, next: NextFunction) {
|
||||
//判断当前url是否为/login 如果是跳过鉴权
|
||||
if (req.url == '/auth/login') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if (req.headers?.authorization) {
|
||||
const authorization = req.headers.authorization.split(' ');
|
||||
if (authorization.length < 2) {
|
||||
res.json({
|
||||
code: -1,
|
||||
msg: 'Unauthorized',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const token = authorization[1];
|
||||
let Credential: any;
|
||||
try {
|
||||
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
|
||||
} catch (e) {
|
||||
res.json({
|
||||
code: -1,
|
||||
msg: 'Unauthorized',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const config = await WebUiConfig.GetWebUIConfig();
|
||||
const credentialJson = await AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
|
||||
if (credentialJson) {
|
||||
//通过验证
|
||||
next();
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
code: -1,
|
||||
msg: 'Unauthorized',
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 鉴权中间件
|
||||
router.use(auth);
|
||||
|
||||
res.json({
|
||||
code: -1,
|
||||
msg: 'Server Error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
router.use(AuthApi);
|
||||
router.all('/test', (req, res) => {
|
||||
res.json({
|
||||
code: 0,
|
||||
msg: 'ok',
|
||||
});
|
||||
// router:测试用
|
||||
router.all('/test', (_, res) => {
|
||||
return sendSuccess(res);
|
||||
});
|
||||
// router:WebUI登录相关路由
|
||||
router.use('/auth', AuthRouter);
|
||||
// router:QQ登录相关路由
|
||||
router.use('/QQLogin', QQLoginRouter);
|
||||
// router:OB11配置相关路由
|
||||
router.use('/OB11Config', OB11ConfigRouter);
|
||||
|
||||
export { router as ALLRouter };
|
||||
|
7
src/webui/src/types/config.d.ts
vendored
Normal file
7
src/webui/src/types/config.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
interface WebUiConfigType {
|
||||
host: string;
|
||||
port: number;
|
||||
prefix: string;
|
||||
token: string;
|
||||
loginRate: number;
|
||||
}
|
12
src/webui/src/types/data.d.ts
vendored
Normal file
12
src/webui/src/types/data.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
interface LoginRuntimeType {
|
||||
LoginCurrentTime: number;
|
||||
LoginCurrentRate: number;
|
||||
QQLoginStatus: boolean;
|
||||
QQQRCodeURL: string;
|
||||
QQLoginUin: string;
|
||||
NapCatHelper: {
|
||||
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>;
|
||||
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
|
||||
QQLoginList: string[];
|
||||
};
|
||||
}
|
7
src/webui/src/types/server.d.ts
vendored
Normal file
7
src/webui/src/types/server.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
interface APIResponse<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
type Protocol = 'http' | 'https' | 'ws' | 'wss';
|
9
src/webui/src/types/sign_token.d.ts
vendored
Normal file
9
src/webui/src/types/sign_token.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
interface WebUiCredentialInnerJson {
|
||||
CreatedTime: number;
|
||||
TokenEncoded: string;
|
||||
}
|
||||
|
||||
interface WebUiCredentialJson {
|
||||
Data: WebUiCredentialInnerJson;
|
||||
Hmac: string;
|
||||
}
|
1
src/webui/src/utils/check.ts
Normal file
1
src/webui/src/utils/check.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const isEmpty = <T>(data: T) => data === undefined || data === null || data === '';
|
26
src/webui/src/utils/response.ts
Normal file
26
src/webui/src/utils/response.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Response } from 'express';
|
||||
|
||||
import { ResponseCode, HttpStatusCode } from '@webapi/const/status';
|
||||
|
||||
export const sendResponse = <T>(res: Response, data?: T, code: ResponseCode = 0, message = 'success') => {
|
||||
res.status(HttpStatusCode.OK).json({
|
||||
code,
|
||||
message,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendError = (res: Response, message = 'error') => {
|
||||
res.status(HttpStatusCode.OK).json({
|
||||
code: ResponseCode.Error,
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendSuccess = <T>(res: Response, data?: T, message = 'success') => {
|
||||
res.status(HttpStatusCode.OK).json({
|
||||
code: ResponseCode.Success,
|
||||
data,
|
||||
message,
|
||||
});
|
||||
};
|
47
src/webui/src/utils/url.ts
Normal file
47
src/webui/src/utils/url.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @file URL工具
|
||||
*/
|
||||
|
||||
import { isIP } from 'node:net';
|
||||
|
||||
/**
|
||||
* 将 host(主机地址) 转换为标准格式
|
||||
* @param host 主机地址
|
||||
* @returns 标准格式的IP地址
|
||||
* @example normalizeHost('10.0.3.2') => '10.0.3.2'
|
||||
* @example normalizeHost('0.0.0.0') => '127.0.0.1'
|
||||
* @example normalizeHost('2001:4860:4801:51::27') => '[2001:4860:4801:51::27]'
|
||||
*/
|
||||
export const normalizeHost = (host: string) => {
|
||||
if (host === '0.0.0.0') return '127.0.0.1';
|
||||
if (isIP(host) === 6) return `[${host}]`;
|
||||
return host;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建URL
|
||||
* @param host 主机地址
|
||||
* @param port 端口
|
||||
* @param path URL路径
|
||||
* @param search URL参数
|
||||
* @returns 完整URL
|
||||
* @example createUrl('127.0.0.1', '8080', '/api', { token: '123456' }) => 'http://127.0.0.1:8080/api?token=123456'
|
||||
* @example createUrl('baidu.com', '80', void 0, void 0, 'https') => 'https://baidu.com:80/'
|
||||
*/
|
||||
export const createUrl = (
|
||||
host: string,
|
||||
port: string,
|
||||
path = '/',
|
||||
search?: Record<string, any>,
|
||||
protocol: Protocol = 'http'
|
||||
) => {
|
||||
const url = new URL(`${protocol}://${normalizeHost(host)}`);
|
||||
url.port = port;
|
||||
url.pathname = path;
|
||||
if (search) {
|
||||
for (const key in search) {
|
||||
url.searchParams.set(key, search[key]);
|
||||
}
|
||||
}
|
||||
return url.toString();
|
||||
};
|
@@ -23,9 +23,12 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"paths": {
|
||||
"@*": [
|
||||
"./src*"
|
||||
]
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@webapi/*": [
|
||||
"./src/webui/src/*"
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
@@ -50,13 +50,15 @@ const ShellBaseConfigPlugin: PluginOption[] = [
|
||||
nodeResolve(),
|
||||
];
|
||||
|
||||
const ShellBaseConfig = () => defineConfig({
|
||||
const ShellBaseConfig = () =>
|
||||
defineConfig({
|
||||
resolve: {
|
||||
conditions: ['node', 'default'],
|
||||
alias: {
|
||||
'@/core': resolve(__dirname, './src/core'),
|
||||
'@': resolve(__dirname, './src'),
|
||||
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
|
||||
'@webapi': resolve(__dirname, './src/webui/src'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
@@ -65,7 +67,7 @@ const ShellBaseConfig = () => defineConfig({
|
||||
minify: false,
|
||||
lib: {
|
||||
entry: {
|
||||
'napcat': 'src/shell/napcat.ts',
|
||||
napcat: 'src/shell/napcat.ts',
|
||||
'audio-worker': 'src/common/audio-worker.ts',
|
||||
},
|
||||
formats: ['es'],
|
||||
@@ -75,15 +77,17 @@ const ShellBaseConfig = () => defineConfig({
|
||||
external: [...nodeModules, ...external],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const FrameworkBaseConfig = () => defineConfig({
|
||||
const FrameworkBaseConfig = () =>
|
||||
defineConfig({
|
||||
resolve: {
|
||||
conditions: ['node', 'default'],
|
||||
alias: {
|
||||
'@/core': resolve(__dirname, './src/core'),
|
||||
'@': resolve(__dirname, './src'),
|
||||
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
|
||||
'@webapi': resolve(__dirname, './src/webui/src'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
@@ -92,7 +96,7 @@ const FrameworkBaseConfig = () => defineConfig({
|
||||
minify: false,
|
||||
lib: {
|
||||
entry: {
|
||||
'napcat': 'src/framework/napcat.ts',
|
||||
napcat: 'src/framework/napcat.ts',
|
||||
'audio-worker': 'src/common/audio-worker.ts',
|
||||
},
|
||||
formats: ['es'],
|
||||
@@ -102,7 +106,7 @@ const FrameworkBaseConfig = () => defineConfig({
|
||||
external: [...nodeModules, ...external],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default defineConfig(({ mode }): UserConfig => {
|
||||
if (mode === 'shell') {
|
||||
|
Reference in New Issue
Block a user