Merge branch 'master' into master

This commit is contained in:
Unitwk 2024-08-08 17:41:18 +08:00 committed by GitHub
commit 5943039c42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 269 additions and 94 deletions

View File

@ -16,8 +16,8 @@
[Official Website](http://mcsmanager.com/) | [Docs](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
[日本語](README_JP.md)
</div>
<br />

View File

@ -16,7 +16,8 @@
[Website Oficial](http://mcsmanager.com/) | [Documentação](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
[日本語](README_JP.md)
</div>

View File

@ -16,7 +16,8 @@
[官方網站](http://mcsmanager.com/) | [教學說明](https://docs.mcsmanager.com/#/zh-cn/) | [TG 群組](https://t.me/MCSManager_dev) | [成為贊助者](https://afdian.net/a/mcsmanager)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
[日本語](README_JP.md)
</div>

View File

@ -16,7 +16,8 @@
[官方网站](http://mcsmanager.com/) | [使用文档](https://docs.mcsmanager.com/#/zh-cn/) | [QQ 群](https://jq.qq.com/?_wv=1027&k=Pgl9ScGw) | [TG 群](https://t.me/MCSManager_dev) | [成为赞助者](https://afdian.net/a/mcsmanager)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md)
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
[日本語](README_JP.md)
</div>

View File

@ -1905,5 +1905,10 @@
"TXT_CODE_99ca8563": "Overwriting File",
"TXT_CODE_ec99ddaa": "File",
"TXT_CODE_8bd1f5d2": "is already exists in this folder, should overwrite it?",
"TXT_CODE_8b14426e": "File already exists in this folder, Upload skipped"
"TXT_CODE_8b14426e": "File already exists in this folder, Upload skipped",
"TXT_CODE_bed32084": "The remote node is offline, please contact the administrator to check the online status of the panel!",
"TXT_CODE_728fdabf": "Failed to create the instance, please try again later",
"TXT_CODE_348c9098": "Failed to obtain the details of the specified instance, please try again!",
"TXT_CODE_4aaec75c": "Wrong request type, please try again!",
"TXT_CODE_903b6c50": "User does not exist, please try again"
}

View File

@ -1905,5 +1905,10 @@
"TXT_CODE_99ca8563": "覆盖文件",
"TXT_CODE_ec99ddaa": "您上传的文件",
"TXT_CODE_8bd1f5d2": "已经在目录中存在, 是否覆盖原文件?",
"TXT_CODE_8b14426e": "文件已存在, 跳过上传"
"TXT_CODE_8b14426e": "文件已存在, 跳过上传",
"TXT_CODE_bed32084": "远程节点处于离线状态,请联系管理员检查面板在线状态!",
"TXT_CODE_728fdabf": "创建实例失败,请稍后重试",
"TXT_CODE_348c9098": "获取指定实例详情失败,请重试!",
"TXT_CODE_4aaec75c": "请求类型错误,请重试!",
"TXT_CODE_903b6c50": "用户不存在,请重试"
}

View File

@ -25,6 +25,7 @@
"log4js": "^6.4.0",
"md5": "^2.3.0",
"module-alias": "^2.2.3",
"nanoid": "^5.0.7",
"node-schedule": "^2.0.0",
"open": "^8.4.0",
"os-utils": "0.0.14",
@ -3268,6 +3269,23 @@
"mustache": "bin/mustache"
}
},
"node_modules/nanoid": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz",
"integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -7355,6 +7373,11 @@
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="
},
"nanoid": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz",
"integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ=="
},
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",

View File

@ -34,6 +34,7 @@
"log4js": "^6.4.0",
"md5": "^2.3.0",
"module-alias": "^2.2.3",
"nanoid": "^5.0.7",
"node-schedule": "^2.0.0",
"open": "^8.4.0",
"os-utils": "0.0.14",

View File

@ -1,39 +1,27 @@
// Define subsystem loading and routing loading for the application
import Koa from "koa";
import Router from "@koa/router";
// Load subsystem
import "./service/user_service";
import "./service/visual_data";
import "./service/remote_service";
import "./service/user_statistics";
// Load routes
import overviewRouter from "./routers/overview_router";
import userRouter from "./routers/user_overview_router";
import loginRouter from "./routers/login_router";
import lowUserRouter from "./routers/general_user_router";
import settingsRouter from "./routers/settings_router";
import instanceRouter from "./routers/instance_admin_router";
import userInstanceRouter from "./routers/instance_operate_router";
import serviceRouter from "./routers/daemon_router";
import filemanager_router from "./routers/filemananger_router";
import businessInstanceRouter from "./routers/business_instance_router";
import businessUserRouter from "./routers/manage_user_router";
import scheduleRouter from "./routers/schedule_router";
import environmentRouter from "./routers/environment_router";
import exchangeRouter from "./routers/instance_exchange_router";
// all routes load entry points
export function mountRouters(app: Koa<Koa.DefaultState, Koa.DefaultContext>) {
// API router
const apiRouter = new Router({ prefix: "/api" });
apiRouter.use(overviewRouter.routes()).use(overviewRouter.allowedMethods());
apiRouter.use(userInstanceRouter.routes()).use(userInstanceRouter.allowedMethods());
@ -48,7 +36,7 @@ export function mountRouters(app: Koa<Koa.DefaultState, Koa.DefaultContext>) {
apiRouter.use(scheduleRouter.routes()).use(scheduleRouter.allowedMethods());
apiRouter.use(settingsRouter.routes()).use(settingsRouter.allowedMethods());
apiRouter.use(environmentRouter.routes()).use(environmentRouter.allowedMethods());
apiRouter.use(exchangeRouter.routes()).use(exchangeRouter.allowedMethods());
// Top router
app.use(apiRouter.routes()).use(apiRouter.allowedMethods());
}

View File

@ -4,13 +4,13 @@ import permission from "../middleware/permission";
import { bind2FA, confirm2FaQRCode, getUserUuid, logout } from "../service/passport_service";
import userSystem from "../service/user_service";
import { getToken, isAjax } from "../service/passport_service";
import RemoteServiceSubsystem from "../service/remote_service";
import RemoteRequest from "../service/remote_command";
import { isTopPermissionByUuid } from "../service/permission_service";
import validator from "../middleware/validator";
import { v4 } from "uuid";
import { $t } from "../i18n";
import { ROLE } from "../entity/user";
import { getInstancesByUuid } from "../service/instance_service";
import { toBoolean } from "common";
const router = new Router({ prefix: "/auth" });
@ -36,79 +36,15 @@ router.get("/", permission({ level: ROLE.USER, token: false, speedLimit: false }
let uuid = getUserUuid(ctx);
// The front end can choose to require advanced data
const advanced = ctx.query.advanced;
// Admin permissions can be obtained from anyone
if (isTopPermissionByUuid(uuid)) {
if (ctx.query.uuid) uuid = String(ctx.query.uuid);
}
if (isTopPermissionByUuid(uuid) && ctx.query.uuid) uuid = String(ctx.query.uuid);
// Some and only Ajax requests grant access
if (isAjax(ctx)) {
const user = userSystem.getInstance(uuid);
if (!user) throw new Error("The UID does not exist");
// Advanced functions are optional, analyze each instance data
let resInstances = [];
if (advanced) {
const instances = user.instances;
for (const iterator of instances) {
const remoteService = RemoteServiceSubsystem.getInstance(iterator.daemonId);
// If the remote service doesn't exist at all, load a deleted prompt
if (!remoteService) {
resInstances.push({
hostIp: "-- Unknown --",
instanceUuid: iterator.instanceUuid,
daemonId: iterator.daemonId,
status: -1,
nickname: "--",
remarks: "--"
});
continue;
}
try {
// Note: UUID can be integrated here to save the returned traffic, and this optimization will not be done for the time being
let instancesInfo = await new RemoteRequest(remoteService).request("instance/section", {
instanceUuids: [iterator.instanceUuid]
});
instancesInfo = instancesInfo[0];
resInstances.push({
hostIp: `${remoteService.config.ip}:${remoteService.config.port}`,
remarks: remoteService.config.remarks,
instanceUuid: instancesInfo.instanceUuid,
daemonId: remoteService.uuid,
status: instancesInfo.status,
nickname: instancesInfo.config.nickname,
ie: instancesInfo.config.ie,
oe: instancesInfo.config.oe,
endTime: instancesInfo.config.endTime,
lastDatetime: instancesInfo.config.lastDatetime,
stopCommand: instancesInfo.config.stopCommand
});
} catch (error: any) {
resInstances.push({
hostIp: `${remoteService.config.ip}:${remoteService.config.port}`,
instanceUuid: iterator.instanceUuid,
daemonId: iterator.daemonId,
status: -1,
nickname: "--"
});
}
}
} else {
resInstances = user.instances;
}
// respond to user data
ctx.body = {
uuid: user.uuid,
userName: user.userName,
loginTime: user.loginTime,
registerTime: user.registerTime,
instances: resInstances,
permission: user.permission,
token: getToken(ctx),
apiKey: user.apiKey,
isInit: user.isInit,
open2FA: user.open2FA,
secret: user.secret
};
const res = await getInstancesByUuid(uuid, toBoolean(advanced) || false);
res.token = getToken(ctx);
ctx.body = res;
}
});

View File

@ -0,0 +1,38 @@
import Router from "@koa/router";
import permission from "../middleware/permission";
import validator from "../middleware/validator";
import { ROLE } from "../entity/user";
import {
buyOrRenewInstance,
queryInstanceByUserId,
RequestAction
} from "../service/exchange_service";
const router = new Router({ prefix: "/exchange" });
router.post(
"/",
permission({ level: ROLE.ADMIN }),
validator({
body: {
request_action: String
}
}),
async (ctx) => {
try {
const requestAction = ctx.request.body.request_action;
const params = ctx.request.body.data ?? {};
if ([RequestAction.RENEW, RequestAction.BUY].includes(requestAction)) {
ctx.body = await buyOrRenewInstance(requestAction, params);
return;
}
if (requestAction === RequestAction.QUERY_INSTANCE) {
ctx.body = await queryInstanceByUserId(params);
}
} catch (err) {
ctx.body = err;
}
}
);
export default router;

View File

@ -0,0 +1,97 @@
import RemoteServiceSubsystem from "../service/remote_service";
import RemoteRequest from "../service/remote_command";
import user_service from "../service/user_service";
import { customAlphabet } from "nanoid";
import { t } from "i18next";
import { toNumber, toText } from "common";
import { getInstancesByUuid } from "./instance_service";
const getNanoId = customAlphabet(
"1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
6
);
export enum RequestAction {
BUY = "buy",
RENEW = "sell",
QUERY_INSTANCE = "query_instance"
}
export async function buyOrRenewInstance(
request_action: RequestAction,
params: Record<string, any>
) {
const node_id = toText(params.node_id) ?? "";
const instance_id = toText(params.instance_id) ?? "";
const username = toText(params.username) ?? "";
const hours = toNumber(params.hours) ?? 0;
const payload = params.payload ?? {};
const remoteService = RemoteServiceSubsystem.getInstance(node_id || "");
if (!remoteService?.available) {
throw new Error(t("TXT_CODE_bed32084"));
}
const { request: remoteRequest } = new RemoteRequest(remoteService);
if (request_action === RequestAction.BUY) {
payload.endTime = (payload.endTime ? payload.endTime : Date.now()) + hours * 3600 * 1000;
const { instanceUuid: newInstanceId, config: newInstanceConfig } = await remoteRequest(
"instance/new",
payload
);
if (!newInstanceId) throw new Error(t("TXT_CODE_728fdabf"));
const newPassword = getNanoId(12);
const newUser = await user_service.create({
userName: username + "-" + getNanoId(6),
passWord: newPassword,
permission: 1,
instances: [
{
instanceUuid: newInstanceId,
daemonId: node_id
}
]
});
return {
instance_id: newInstanceId,
instance_config: newInstanceConfig,
username: newUser.userName,
password: newPassword,
uuid: newUser.uuid,
expire: toNumber(newInstanceConfig.endTime)
};
}
if (request_action === RequestAction.RENEW) {
const instanceInfo = await remoteRequest("instance/detail", {
instanceUuid: instance_id
});
if (!instanceInfo.config) throw new Error(t("TXT_CODE_348c9098"));
instanceInfo.config.endTime =
(instanceInfo.config?.endTime ? instanceInfo.config.endTime : Date.now()) +
hours * 3600 * 1000;
await remoteRequest("instance/update", {
instanceUuid: instance_id,
config: instanceInfo.config
});
return {
instance_id,
instance_config: instanceInfo.config,
expire: toNumber(instanceInfo.config.endTime),
username: "",
password: "",
uuid: ""
};
}
throw new Error(t("TXT_CODE_4aaec75c"));
}
export async function queryInstanceByUserId(params: Record<string, any>) {
const uuid = toText(params.uuid) || "";
const user = user_service.getInstance(uuid);
if (!user) throw new Error(t("TXT_CODE_903b6c50"));
return await getInstancesByUuid(uuid, false);
}

View File

@ -1,3 +1,7 @@
import userSystem from "../service/user_service";
import RemoteServiceSubsystem from "../service/remote_service";
import RemoteRequest from "../service/remote_command";
// Multi-forward operation method
export function multiOperationForwarding(
instances: any[],
@ -22,3 +26,73 @@ export function multiOperationForwarding(
callback(daemonId, instanceUuids);
}
}
export async function getInstancesByUuid(uuid: string, advanced: boolean = false) {
const user = userSystem.getInstance(uuid);
if (!user) throw new Error("The UID does not exist");
// Advanced functions are optional, analyze each instance data
let resInstances = [];
if (advanced) {
const instances = user.instances;
for (const iterator of instances) {
const remoteService = RemoteServiceSubsystem.getInstance(iterator.daemonId);
// If the remote service doesn't exist at all, load a deleted prompt
if (!remoteService) {
resInstances.push({
hostIp: "-- Unknown --",
instanceUuid: iterator.instanceUuid,
daemonId: iterator.daemonId,
status: -1,
nickname: "--",
remarks: "--"
});
continue;
}
try {
// Note: UUID can be integrated here to save the returned traffic, and this optimization will not be done for the time being
let instancesInfo = await new RemoteRequest(remoteService).request("instance/section", {
instanceUuids: [iterator.instanceUuid]
});
instancesInfo = instancesInfo[0];
resInstances.push({
hostIp: `${remoteService.config.ip}:${remoteService.config.port}`,
remarks: remoteService.config.remarks,
instanceUuid: instancesInfo.instanceUuid,
daemonId: remoteService.uuid,
status: instancesInfo.status,
nickname: instancesInfo.config.nickname,
ie: instancesInfo.config.ie,
oe: instancesInfo.config.oe,
endTime: instancesInfo.config.endTime,
lastDatetime: instancesInfo.config.lastDatetime,
stopCommand: instancesInfo.config.stopCommand
});
} catch (error: any) {
resInstances.push({
hostIp: `${remoteService.config.ip}:${remoteService.config.port}`,
instanceUuid: iterator.instanceUuid,
daemonId: iterator.daemonId,
status: -1,
nickname: "--"
});
}
}
} else {
resInstances = user.instances;
}
// respond to user data
return {
uuid: user.uuid,
userName: user.userName,
loginTime: user.loginTime,
registerTime: user.registerTime,
instances: resInstances,
permission: user.permission,
apiKey: user.apiKey,
isInit: user.isInit,
open2FA: user.open2FA,
secret: user.secret,
token: ""
};
}

View File

@ -23,7 +23,12 @@ export default class RemoteRequest {
}
// request to remote daemon
public async request(event: string, data?: any, timeout = 6000, force = false): Promise<any> {
public async request<T = any>(
event: string,
data?: any,
timeout = 6000,
force = false
): Promise<T> {
if (!this.rService || !this.rService.socket) throw new Error($t("TXT_CODE_3d94ea16"));
if (!this.rService.available && !force)
throw new Error($t("TXT_CODE_b7d38e78") + ` IP: ${this.rService.config.ip}`);