Web interface #525
16
.github/workflows/spigotBuild.yaml
vendored
16
.github/workflows/spigotBuild.yaml
vendored
@ -51,10 +51,22 @@ jobs:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: artifacts
|
||||
path: SpigotBuild
|
||||
|
||||
- name: Rename files
|
||||
run: |
|
||||
cd SpigotBuild/latest
|
||||
OIFS="$IFS"
|
||||
IFS=$'\n'
|
||||
for file in $(find . -type f)
|
||||
do
|
||||
echo "Working on ${file} ..."
|
||||
mv -v "$file" "$(echo $file | sed 's|spigot-||g')"
|
||||
done
|
||||
IFS="$OIFS"
|
||||
|
||||
- name: Upload to actifial
|
||||
run: node .github/uploadToBucket.mjs artifacts:SpigotBuild
|
||||
run: node .github/uploadToBucket.mjs SpigotBuild/latest:SpigotBuild
|
||||
env:
|
||||
ociauth: "${{ secrets.OCI_AUTHKEY }}"
|
||||
OCI_AUTHKEY: "${{ secrets.OCI_AUTHKEY }}"
|
||||
|
1
.github/workflows/test.yaml
vendored
1
.github/workflows/test.yaml
vendored
@ -10,6 +10,7 @@ jobs:
|
||||
matrix:
|
||||
package:
|
||||
- "@the-bds-maneger/core"
|
||||
- "@the-bds-maneger/web"
|
||||
- "bds-maneger"
|
||||
- "@the-bds-maneger/verapi"
|
||||
name: "Testing \"${{ matrix.package }}\""
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
# System
|
||||
.DS_Store
|
||||
|
||||
# npm
|
||||
*.tgz
|
||||
|
||||
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM node:lts
|
||||
WORKDIR /app
|
||||
COPY ./ ./
|
||||
RUN npm install --no-save && npm run -w "@the-bds-maneger/web" build
|
||||
|
||||
FROM node:lts
|
||||
WORKDIR /app
|
||||
COPY --from=0 /app/package/docker ./
|
||||
RUN npm install
|
||||
EXPOSE 3000:3000/tcp
|
||||
ENV PORT=3000
|
||||
VOLUME [ "/data" ]
|
||||
ENTRYPOINT "bash -c 'BDSCOREROOT=/data node src/index.js'"
|
@ -15,6 +15,7 @@
|
||||
"workspaces": [
|
||||
"packages/core",
|
||||
"packages/cli",
|
||||
"packages/web",
|
||||
"packages/verapi"
|
||||
]
|
||||
}
|
||||
|
@ -92,6 +92,14 @@ export class Bedrock<P extends platforms> extends customEvent<bedrockEvents> {
|
||||
Object.defineProperty(this, "platform", { writable: false });
|
||||
}
|
||||
|
||||
getVersion(version: string | number) {
|
||||
if (this.platform === "mojang") return bedrockVersions.mojangCache.get(version);
|
||||
else if (this.platform === "pocketmine") return bedrockVersions.pocketmineCache.get(version);
|
||||
else if (this.platform === "cloudburst") return bedrockVersions.cloudburstCache.get(version);
|
||||
else if (this.platform === "nukkit") return bedrockVersions.nukkitCache.get(version);
|
||||
else return bedrockVersions.powernukkitCache.get(version);
|
||||
}
|
||||
|
||||
async installServer(version: string | number) {
|
||||
const { platform } = this;
|
||||
if (!(await extendsFS.exists(this.serverFolder))) await fs.mkdir(this.serverFolder, { recursive: true });
|
||||
|
@ -5,12 +5,12 @@ import semver from "semver";
|
||||
import { bdsFilesBucket } from "../../internalClouds.js";
|
||||
import { versionsStorages } from "../../serverRun.js";
|
||||
|
||||
interface baseDownload {
|
||||
export interface baseDownload {
|
||||
URL: string;
|
||||
releaseDate: Date;
|
||||
}
|
||||
|
||||
interface mojangInfo extends baseDownload {
|
||||
export interface mojangInfo extends baseDownload {
|
||||
release: "oficial" | "snapshot" | "beta" | "alpha";
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,16 @@ export class Java<P extends platform> extends customEvent<javaEvents> {
|
||||
Object.defineProperty(this, "platform", { writable: false });
|
||||
}
|
||||
|
||||
getVersion(version: number|string) {
|
||||
if (this.platform === "mojang") return javaVersions.mojangCache.get(version);
|
||||
else if (this.platform === "spigot") return javaVersions.spigotCache.get(version);
|
||||
else if (this.platform === "paper") return javaVersions.paperCache.get(version);
|
||||
else if (this.platform === "purpur") return javaVersions.purpurCache.get(version);
|
||||
else if (this.platform === "folia") return javaVersions.foliaCache.get(version);
|
||||
else if (this.platform === "cuberite") return javaVersions.cuberiteCache.get(version);
|
||||
else return javaVersions.glowstoneCache.get(version);
|
||||
}
|
||||
|
||||
async installServer(version: string | number) {
|
||||
const { platform } = this;
|
||||
if (!(await extendsFS.exists(this.serverFolder))) await fs.mkdir(this.serverFolder, { recursive: true });
|
||||
|
2
packages/web/.gitignore
vendored
Normal file
2
packages/web/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/*.env.*
|
||||
/*.env
|
8
packages/web/nodemon.json
Normal file
8
packages/web/nodemon.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"ext": "ts,cts,mts,json",
|
||||
"watch": ["src"],
|
||||
"exec": "ts-node",
|
||||
"args": [
|
||||
"src/index.ts"
|
||||
]
|
||||
}
|
37
packages/web/package.json
Normal file
37
packages/web/package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@the-bds-maneger/web",
|
||||
"version": "6.0.4",
|
||||
"type": "module",
|
||||
"author": "Matheus Sampaio Queiroga <srherobrine20@gmail.com>",
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"prepack": "tsc --build --clean && tsc --build && next build ./src/next/",
|
||||
"postpack": "tsc --build --clean",
|
||||
"dev": "nodemon"
|
||||
},
|
||||
"bin": {
|
||||
"bds-web": "src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@the-bds-maneger/core": "^6.0.4",
|
||||
"@types/express-session": "^1.17.7",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"express-session": "^1.17.3",
|
||||
"mongodb": "^5.5.0",
|
||||
"next": "^13.4.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"ssh2": "^1.13.0",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"xterm": "^5.1.0",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/react": "18.2.7",
|
||||
"@types/ssh2": "^1.11.11",
|
||||
"nodemon": "^2.0.22"
|
||||
}
|
||||
}
|
220
packages/web/src/auth.ts
Normal file
220
packages/web/src/auth.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import session from "express-session";
|
||||
import express from "express";
|
||||
import crypto from "node:crypto";
|
||||
import { localConfig } from "./config.js";
|
||||
import { mongoDatabase } from "./databaseConnect.js";
|
||||
|
||||
declare module "express-session" {
|
||||
interface SessionData {
|
||||
userID: string;
|
||||
}
|
||||
}
|
||||
|
||||
const cookieCollection = mongoDatabase.collection<{sid: string, session: session.SessionData}>("cookies");
|
||||
class SessionMongo extends session.Store {
|
||||
/* temporary storage session on sync to Database */
|
||||
readonly tmpSession = new Map<string, session.SessionData>();
|
||||
|
||||
async destroy(sid: string, callback?: (err?: any) => void) {
|
||||
try {
|
||||
cookieCollection.findOneAndDelete({sid});
|
||||
if (this.tmpSession.has(sid)) this.tmpSession.delete(sid);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
async set(sid: string, session: session.SessionData, callback?: (err?: any) => void) {
|
||||
if (this.tmpSession.has(sid)) return callback();
|
||||
this.tmpSession.set(sid, session);
|
||||
session = typeof session["toJSON"] === "function" ? session["toJSON"]() : session;
|
||||
try {
|
||||
if (await cookieCollection.findOne({sid})) await cookieCollection.findOneAndUpdate({sid}, {session});
|
||||
else await cookieCollection.insertOne({sid, session});
|
||||
this.tmpSession.delete(sid);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
async get(sid: string, callback: (err?: any, session?: session.SessionData) => void) {
|
||||
if (this.tmpSession.has(sid)) return callback(null, this.tmpSession.get(sid));
|
||||
try {
|
||||
await cookieCollection.findOne({sid}).then(res => !res ? callback() : callback(null, res.session));
|
||||
} catch (err) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
|
||||
async clear(callback?: (err?: any) => void) {
|
||||
try {
|
||||
await cookieCollection.deleteMany({});
|
||||
this.tmpSession.clear();
|
||||
callback();
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
async all(callback: (err?: any, obj?: session.SessionData[] | { [sid: string]: session.SessionData; }) => void) {
|
||||
try {
|
||||
await cookieCollection.find({}).toArray().then(cookies => callback(null, cookies.reduce((acc, cookie) => {acc[cookie.sid] = cookie.session; return acc;}, {})), err => callback(err));
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const cookie = session({
|
||||
secret: localConfig.cookieSecret,
|
||||
name: "bdsAuth",
|
||||
saveUninitialized: true,
|
||||
resave: true,
|
||||
unset: "destroy",
|
||||
cookie: {
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
signed: true,
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30 * 2,
|
||||
},
|
||||
store: new SessionMongo(),
|
||||
});
|
||||
|
||||
export async function passwordEncrypt(input: string): Promise<{hash: string, salt: string}> {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const secret = crypto.randomBytes(24);
|
||||
return new Promise((done, reject) => {
|
||||
crypto.scrypt(secret, "salt", 24, (err, key) => {
|
||||
if (err) return reject(err);
|
||||
const cipher = crypto.createCipheriv("aes-192-cbc", key, iv);
|
||||
cipher.on("error", reject);
|
||||
return done({
|
||||
hash: Buffer.from(cipher.update(input, "utf8", "hex") + cipher.final("hex"), "utf8").toString("base64"),
|
||||
salt: Buffer.from(iv.toString("hex") + "::::" + secret.toString("hex"), "utf8").toString("base64")
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function passwordDecrypt(hash: string, salt: string): Promise<string> {
|
||||
const hashSplit = Buffer.from(salt, "base64").toString("utf8").split("::::");
|
||||
return new Promise((done, reject) => {
|
||||
const iv = Buffer.from(hashSplit.at(0), "hex");
|
||||
const secret = Buffer.from(hashSplit.at(1), "hex");
|
||||
crypto.scrypt(secret, "salt", 24, (err, key) => {
|
||||
if (err) return reject(err);
|
||||
const decipher = crypto.createDecipheriv("aes-192-cbc", key, iv);
|
||||
decipher.on("error", reject);
|
||||
return done(decipher.update(Buffer.from(hash, "base64").toString(), "hex", "utf8") + decipher.final("utf8"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function createSSHKey(): Promise<{privateKey: string, publicKey: string}> {
|
||||
return new Promise((done, reject) => {
|
||||
crypto.generateKeyPair("rsa", {
|
||||
modulusLength: 3072,
|
||||
privateKeyEncoding: {
|
||||
format: "pem",
|
||||
type: "pkcs1"
|
||||
},
|
||||
publicKeyEncoding: {
|
||||
type: "pkcs1",
|
||||
format: "pem"
|
||||
}
|
||||
}, (err, publicKey: string, privateKey: string) => {
|
||||
if (err) return reject(err);
|
||||
done({
|
||||
privateKey,
|
||||
publicKey
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type userStorage = {
|
||||
/** Unique user ID */
|
||||
readonly userID: string;
|
||||
|
||||
/** User create date */
|
||||
readonly createAt: Date;
|
||||
|
||||
/** Email to auth */
|
||||
email: string;
|
||||
|
||||
/** Auth password */
|
||||
password: {hash: string, salt: string};
|
||||
|
||||
/** API Token auth */
|
||||
tokens: string[];
|
||||
|
||||
/** Minecraft username to maneger access list */
|
||||
mcUsername: {
|
||||
Bedrock: string;
|
||||
Java: string;
|
||||
};
|
||||
};
|
||||
export const usersCollection = mongoDatabase.collection<userStorage>("usersAuth");
|
||||
|
||||
export const random = () => {
|
||||
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
||||
return ([
|
||||
crypto.randomBytes(8).toString("hex"),
|
||||
crypto.randomBytes(4).toString("hex"),
|
||||
crypto.randomBytes(4).toString("hex"),
|
||||
crypto.randomBytes(4).toString("hex"),
|
||||
crypto.randomBytes(12).toString("hex"),
|
||||
]).join("-");
|
||||
}
|
||||
|
||||
export async function generateUserID() {
|
||||
let userID: string;
|
||||
while (true) if (!(await usersCollection.findOne({userID: (userID = random())}))) break;
|
||||
return userID;
|
||||
}
|
||||
|
||||
export async function generateToken() {
|
||||
const genToken = () => {
|
||||
let data = Array(crypto.randomInt(3, 8)+1).fill(null).map(() => crypto.randomBytes(crypto.randomInt(1, 6)).toString("hex"));
|
||||
let scg = "tk_";
|
||||
|
||||
scg += data.shift() + data.pop();
|
||||
scg += "0" + data.join("-")
|
||||
|
||||
return scg;
|
||||
};
|
||||
|
||||
let token: string;
|
||||
while (true) if (!(await usersCollection.findOne({tokens: [(token = genToken())]}))) break;
|
||||
return token;
|
||||
}
|
||||
|
||||
declare module "express-serve-static-core" {
|
||||
interface Request {
|
||||
userInfo?: userStorage;
|
||||
}
|
||||
}
|
||||
|
||||
export const authRoute: express.RequestHandler = async (req, res, next) => {
|
||||
if (typeof req.headers.authorization === "string" && (req.headers.authorization = req.headers.authorization.trim()).length > 0) {
|
||||
let userInfo: userStorage;
|
||||
const { authorization } = req.headers;
|
||||
if (authorization.startsWith("Basic ")) {
|
||||
const decode64 = Buffer.from(authorization.slice(5).trim(), "base64").toString("utf8");
|
||||
let index: number, email = decode64.slice(0, (index = decode64.indexOf(":"))), password = decode64.slice(index+1);
|
||||
userInfo = await usersCollection.findOne({email});
|
||||
if (await passwordDecrypt(userInfo.password.hash, userInfo.password.salt) !== password) userInfo = undefined;
|
||||
} else if (authorization.startsWith("Token ")||authorization.startsWith("Bearer ")) {
|
||||
const token = authorization.slice(6).trim();
|
||||
userInfo = await usersCollection.findOne({tokens: [token]});
|
||||
}
|
||||
|
||||
if (!userInfo) return res.status(401).json({error: "invalid authentication"});
|
||||
req.userInfo = userInfo;
|
||||
req.session.userID = userInfo.userID;
|
||||
return req.session.save(next);
|
||||
}
|
||||
|
||||
if (typeof req.session.userID === "string" && !req.userInfo) req.userInfo = await usersCollection.findOne({userID: req.session.userID});
|
||||
return next();
|
||||
}
|
35
packages/web/src/config.ts
Normal file
35
packages/web/src/config.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { createSSHKey } from "./auth.js";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
export type configFile = {
|
||||
serversPath: string;
|
||||
/** HTTP port listen */
|
||||
portListen: number;
|
||||
/** Super cookie secret */
|
||||
cookieSecret: string;
|
||||
/** MongoDB URI connection */
|
||||
mongoConnection: string;
|
||||
mongoDatabase?: string;
|
||||
/** SSH Server config */
|
||||
ssh?: {
|
||||
port: number;
|
||||
hostKeys: string[];
|
||||
};
|
||||
};
|
||||
|
||||
const sshKeys = Object.keys(process.env).filter(name => name.startsWith("SSH_HOST")).map(name => path.resolve(process.cwd(), process.env[name]));
|
||||
|
||||
export const localConfig: configFile = {
|
||||
serversPath: process.env.SERVER_PATH ? path.resolve(process.cwd(), process.env.SERVER_PATH) : path.join(homedir(), ".bdsManeger"),
|
||||
cookieSecret: process.env.COOKIE_SECRET || randomBytes(8).toString("hex"),
|
||||
mongoConnection: process.env.MONGO_URI || "mongodb://127.0.0.1",
|
||||
mongoDatabase: (typeof process.env.MONGO_DB === "string" && process.env.MONGO_DB.length >= 2) ? process.env.MONGO_DB : undefined,
|
||||
portListen: Number(process.env.PORT || 3000),
|
||||
ssh: {
|
||||
port: Number(process.env.SSH_PORT || 3001),
|
||||
hostKeys: sshKeys.length > 0 ? await Promise.all(sshKeys.map(async path => fs.readFile(path, "utf8"))) : [ (await createSSHKey()).privateKey ]
|
||||
}
|
||||
};
|
5
packages/web/src/databaseConnect.ts
Normal file
5
packages/web/src/databaseConnect.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
import { localConfig } from "./config.js";
|
||||
|
||||
export const connection = await (new MongoClient(localConfig.mongoConnection)).connect();
|
||||
export const mongoDatabase = connection.db(localConfig.mongoDatabase);
|
34
packages/web/src/index.ts
Normal file
34
packages/web/src/index.ts
Normal file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
import express from "express";
|
||||
import http from "node:http";
|
||||
import { authRoute, cookie } from "./auth.js";
|
||||
import { localConfig } from "./config.js";
|
||||
import mcserver from "./mcserver.js";
|
||||
import { nextHandler, nextUpgarde } from "./reactServer.js";
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer();
|
||||
server.on("upgrade", nextUpgarde);
|
||||
server.on("request", app);
|
||||
|
||||
app.disable("etag").disable("x-powered-by");
|
||||
app.use(cookie, authRoute, express.json(), express.urlencoded({ extended: true }));
|
||||
|
||||
// API 404
|
||||
app.use("/api/mcserver", mcserver);
|
||||
app.use("/api", ({res}) => res.status(404).json({error: "endpoint not exists!"}));
|
||||
|
||||
// Page render
|
||||
app.all("*", (req, res) => nextHandler(req, res));
|
||||
|
||||
// 500 error
|
||||
app.use((err, _req, res, _next) => {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: err?.message || err })
|
||||
});
|
||||
|
||||
// Server listen
|
||||
server.listen(localConfig.portListen, () => {
|
||||
const addr = server.address();
|
||||
console.log("HTTP Listen on %s", typeof addr === "object" ? addr.port : addr);
|
||||
});
|
85
packages/web/src/mcserver.ts
Normal file
85
packages/web/src/mcserver.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import express from "express";
|
||||
import { random } from "./auth.js";
|
||||
import { mongoDatabase } from "./databaseConnect.js";
|
||||
import bdsCore from "@the-bds-maneger/core";
|
||||
import path from "node:path";
|
||||
import { localConfig } from "./config.js";
|
||||
|
||||
type serverStor = {
|
||||
/** Unique ID to identify server */
|
||||
readonly ID: string;
|
||||
|
||||
readonly platform: `bedrock-${bdsCore.Bedrock.platforms}` | `java-${bdsCore.Java.platform}`;
|
||||
|
||||
public: boolean;
|
||||
|
||||
/** user allowed to modify server */
|
||||
usersID: string[];
|
||||
};
|
||||
|
||||
export const serverCollection = mongoDatabase.collection<serverStor>("servers");
|
||||
export async function generateID() {
|
||||
let ID: string;
|
||||
while (true) if (!(await serverCollection.findOne({ ID: (ID = random()) }))) break;
|
||||
return ID;
|
||||
}
|
||||
|
||||
const app = express.Router();
|
||||
export default app;
|
||||
|
||||
export const serverSessions = new Map<string, bdsCore.Bedrock.Bedrock<any> | bdsCore.Java.Java<any>>();
|
||||
app.get("/public", (_req, res, next) => serverCollection.find({ public: true }).toArray().then(data => res.json(data.map(v => ({ ID: v.ID, serverPlatform: v.platform }))), next));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (!req.userInfo) return res.status(401).json({ error: "need authorization" });
|
||||
return next();
|
||||
});
|
||||
|
||||
app.get("/", (req, res, next) => serverCollection.find({ usersID: [req.session.userID] }).toArray().then(res.json, next));
|
||||
app.post("/", async (req, res) => {
|
||||
if (typeof req.body !== "object") return res.status(400).json({ error: "Require body to install platform" });
|
||||
const { version, platform } = req.body as { version?: string | number, platform: serverStor["platform"] };
|
||||
|
||||
if (!platform) return res.status(400).json({ error: "require platform" });
|
||||
if (!(([
|
||||
"bedrock-mojang",
|
||||
"java-mojang",
|
||||
"bedrock-pocketmine",
|
||||
"bedrock-cloudburst",
|
||||
"bedrock-nukkit",
|
||||
"bedrock-powernukkit",
|
||||
"java-spigot",
|
||||
"java-paper",
|
||||
"java-cuberite",
|
||||
"java-purpur",
|
||||
"java-folia",
|
||||
"java-glowstone"
|
||||
]).includes(platform))) res.status(400).json({ error: "invalid platform" });
|
||||
const ID = await generateID();
|
||||
await serverCollection.insertOne({
|
||||
ID,
|
||||
platform,
|
||||
public: false,
|
||||
usersID: [
|
||||
req.userInfo.userID,
|
||||
]
|
||||
});
|
||||
|
||||
const pathInstall = path.join(localConfig.serversPath, ID.split("-").join("_"));
|
||||
let serverManeger: bdsCore.Bedrock.Bedrock<any> | bdsCore.Java.Java<any>;
|
||||
if (platform === "bedrock-mojang") serverManeger = new bdsCore.Bedrock.Bedrock(pathInstall, "mojang");
|
||||
else if (platform === "java-mojang") serverManeger = new bdsCore.Java.Java(pathInstall, "mojang");
|
||||
else if (platform === "bedrock-pocketmine") serverManeger = new bdsCore.Bedrock.Bedrock(pathInstall, "pocketmine");
|
||||
else if (platform === "bedrock-cloudburst") serverManeger = new bdsCore.Bedrock.Bedrock(pathInstall, "cloudburst");
|
||||
else if (platform === "bedrock-nukkit") serverManeger = new bdsCore.Bedrock.Bedrock(pathInstall, "nukkit");
|
||||
else if (platform === "bedrock-powernukkit") serverManeger = new bdsCore.Bedrock.Bedrock(pathInstall, "powernukkit");
|
||||
else if (platform === "java-spigot") serverManeger = new bdsCore.Java.Java(pathInstall, "spigot");
|
||||
else if (platform === "java-paper") serverManeger = new bdsCore.Java.Java(pathInstall, "paper");
|
||||
else if (platform === "java-cuberite") serverManeger = new bdsCore.Java.Java(pathInstall, "cuberite");
|
||||
else if (platform === "java-purpur") serverManeger = new bdsCore.Java.Java(pathInstall, "purpur");
|
||||
else if (platform === "java-folia") serverManeger = new bdsCore.Java.Java(pathInstall, "folia");
|
||||
else if (platform === "java-glowstone") serverManeger = new bdsCore.Java.Java(pathInstall, "glowstone");
|
||||
|
||||
await serverManeger.installServer(version);
|
||||
return res.json(serverManeger.getVersion(version));
|
||||
});
|
5
packages/web/src/next/.gitignore
vendored
Normal file
5
packages/web/src/next/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Next
|
||||
.next/
|
||||
|
||||
# Include typescript
|
||||
!next-env.d.ts
|
219
packages/web/src/next/component/Xterm.tsx
Normal file
219
packages/web/src/next/component/Xterm.tsx
Normal file
@ -0,0 +1,219 @@
|
||||
import * as React from "react"
|
||||
import type { ITerminalAddon, ITerminalOptions, Terminal } from "xterm"
|
||||
import "xterm/css/xterm.css"
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
* Class name to add to the terminal container.
|
||||
*/
|
||||
className?: string
|
||||
|
||||
/**
|
||||
* Options to initialize the terminal with.
|
||||
*/
|
||||
options?: ITerminalOptions
|
||||
|
||||
/**
|
||||
* An array of XTerm addons to load along with the terminal.
|
||||
*/
|
||||
addons?: ITerminalAddon[]
|
||||
|
||||
/**
|
||||
* Adds an event listener for when a binary event fires. This is used to
|
||||
* enable non UTF-8 conformant binary messages to be sent to the backend.
|
||||
* Currently this is only used for a certain type of mouse reports that
|
||||
* happen to be not UTF-8 compatible.
|
||||
* The event value is a JS string, pass it to the underlying pty as
|
||||
* binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`.
|
||||
*/
|
||||
onBinary?(data: string): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for the cursor moves.
|
||||
*/
|
||||
onCursorMove?(): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for when a data event fires. This happens for
|
||||
* example when the user types or pastes into the terminal. The event value
|
||||
* is whatever `string` results, in a typical setup, this should be passed
|
||||
* on to the backing pty.
|
||||
*/
|
||||
onData?(data: string): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for when a key is pressed. The event value contains the
|
||||
* string that will be sent in the data event as well as the DOM event that
|
||||
* triggered it.
|
||||
*/
|
||||
onKey?(event: { key: string; domEvent: KeyboardEvent }): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for when a line feed is added.
|
||||
*/
|
||||
onLineFeed?(): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for when a scroll occurs. The event value is the
|
||||
* new position of the viewport.
|
||||
* @returns an `IDisposable` to stop listening.
|
||||
*/
|
||||
onScroll?(newPosition: number): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for when a selection change occurs.
|
||||
*/
|
||||
onSelectionChange?(): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for when rows are rendered. The event value
|
||||
* contains the start row and end rows of the rendered area (ranges from `0`
|
||||
* to `Terminal.rows - 1`).
|
||||
*/
|
||||
onRender?(event: { start: number; end: number }): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for when the terminal is resized. The event value
|
||||
* contains the new size.
|
||||
*/
|
||||
onResize?(event: { cols: number; rows: number }): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for when an OSC 0 or OSC 2 title change occurs.
|
||||
* The event value is the new title.
|
||||
*/
|
||||
onTitleChange?(newTitle: string): void
|
||||
|
||||
/**
|
||||
* Attaches a custom key event handler which is run before keys are
|
||||
* processed, giving consumers of xterm.js ultimate control as to what keys
|
||||
* should be processed by the terminal and what keys should not.
|
||||
*
|
||||
* @param event The custom KeyboardEvent handler to attach.
|
||||
* This is a function that takes a KeyboardEvent, allowing consumers to stop
|
||||
* propagation and/or prevent the default action. The function returns
|
||||
* whether the event should be processed by xterm.js.
|
||||
*/
|
||||
customKeyEventHandler?(event: KeyboardEvent): boolean
|
||||
}
|
||||
|
||||
export default class Xterm extends React.Component<IProps> {
|
||||
/**
|
||||
* The ref for the containing element.
|
||||
*/
|
||||
terminalRef: React.RefObject<HTMLDivElement>
|
||||
|
||||
/**
|
||||
* XTerm.js Terminal object.
|
||||
*/
|
||||
terminal!: Terminal // This is assigned in the setupTerminal() which is called from the constructor
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.terminalRef = React.createRef()
|
||||
|
||||
// Bind Methods
|
||||
this.onData = this.onData.bind(this)
|
||||
this.onCursorMove = this.onCursorMove.bind(this)
|
||||
this.onKey = this.onKey.bind(this)
|
||||
this.onBinary = this.onBinary.bind(this)
|
||||
this.onLineFeed = this.onLineFeed.bind(this)
|
||||
this.onScroll = this.onScroll.bind(this)
|
||||
this.onSelectionChange = this.onSelectionChange.bind(this)
|
||||
this.onRender = this.onRender.bind(this)
|
||||
this.onResize = this.onResize.bind(this)
|
||||
this.onTitleChange = this.onTitleChange.bind(this)
|
||||
|
||||
this.setupTerminal()
|
||||
}
|
||||
|
||||
async setupTerminal() {
|
||||
// Setup the XTerm terminal.
|
||||
// @ts-ignore
|
||||
this.terminal = new Terminal(this.props.options)
|
||||
|
||||
// Load addons if the prop exists.
|
||||
if (this.props.addons) {
|
||||
this.props.addons.forEach((addon) => {
|
||||
this.terminal.loadAddon(addon)
|
||||
})
|
||||
}
|
||||
|
||||
// Create Listeners
|
||||
this.terminal.onBinary(this.onBinary)
|
||||
this.terminal.onCursorMove(this.onCursorMove)
|
||||
this.terminal.onData(this.onData)
|
||||
this.terminal.onKey(this.onKey)
|
||||
this.terminal.onLineFeed(this.onLineFeed)
|
||||
this.terminal.onScroll(this.onScroll)
|
||||
this.terminal.onSelectionChange(this.onSelectionChange)
|
||||
this.terminal.onRender(this.onRender)
|
||||
this.terminal.onResize(this.onResize)
|
||||
this.terminal.onTitleChange(this.onTitleChange)
|
||||
|
||||
// Add Custom Key Event Handler
|
||||
if (this.props.customKeyEventHandler) {
|
||||
this.terminal.attachCustomKeyEventHandler(this.props.customKeyEventHandler)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.terminalRef.current) {
|
||||
// Creates the terminal within the container element.
|
||||
this.terminal.open(this.terminalRef.current)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// When the component unmounts dispose of the terminal and all of its listeners.
|
||||
this.terminal.dispose()
|
||||
}
|
||||
|
||||
private onBinary(data: string) {
|
||||
if (this.props.onBinary) this.props.onBinary(data)
|
||||
}
|
||||
|
||||
private onCursorMove() {
|
||||
if (this.props.onCursorMove) this.props.onCursorMove()
|
||||
}
|
||||
|
||||
private onData(data: string) {
|
||||
if (this.props.onData) this.props.onData(data)
|
||||
}
|
||||
|
||||
private onKey(event: { key: string; domEvent: KeyboardEvent }) {
|
||||
if (this.props.onKey) this.props.onKey(event)
|
||||
}
|
||||
|
||||
private onLineFeed() {
|
||||
if (this.props.onLineFeed) this.props.onLineFeed()
|
||||
}
|
||||
|
||||
private onScroll(newPosition: number) {
|
||||
if (this.props.onScroll) this.props.onScroll(newPosition)
|
||||
}
|
||||
|
||||
private onSelectionChange() {
|
||||
if (this.props.onSelectionChange) this.props.onSelectionChange()
|
||||
}
|
||||
|
||||
private onRender(event: { start: number; end: number }) {
|
||||
if (this.props.onRender) this.props.onRender(event)
|
||||
}
|
||||
|
||||
private onResize(event: { cols: number; rows: number }) {
|
||||
if (this.props.onResize) this.props.onResize(event)
|
||||
}
|
||||
|
||||
private onTitleChange(newTitle: string) {
|
||||
if (this.props.onTitleChange) this.props.onTitleChange(newTitle)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.1.0/lib/xterm.js"></script>
|
||||
<div className={this.props.className} ref={this.terminalRef} />
|
||||
</div>
|
||||
}
|
||||
}
|
64
packages/web/src/next/component/skeletonLoading.module.css
Normal file
64
packages/web/src/next/component/skeletonLoading.module.css
Normal file
@ -0,0 +1,64 @@
|
||||
.background-masker {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.btn-divide-left {
|
||||
top: 0;
|
||||
left: 25%;
|
||||
height: 100%;
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
@keyframes placeHolderShimmer {
|
||||
0% {
|
||||
background-position: -800px 0
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 800px 0
|
||||
}
|
||||
}
|
||||
|
||||
.animated-background {
|
||||
animation-duration: 2.5s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: placeHolderShimmer;
|
||||
animation-timing-function: linear;
|
||||
background-color: #f6f7f8;
|
||||
background: linear-gradient(to right, rgb(238, 238, 238) 8%, rgb(187, 187, 187) 18%, rgb(238, 238, 238) 33%);
|
||||
background-size: 800px 104px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.css-dom:empty {
|
||||
width: 280px;
|
||||
height: 220px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 10px 45px rgba(0, 0, 0, .2);
|
||||
background-repeat: no-repeat;
|
||||
|
||||
background-image:
|
||||
radial-gradient(circle 16px, lightgray 99%, transparent 0),
|
||||
linear-gradient(lightgray, lightgray),
|
||||
linear-gradient(lightgray, lightgray),
|
||||
linear-gradient(lightgray, lightgray),
|
||||
linear-gradient(lightgray, lightgray),
|
||||
linear-gradient(#fff, #fff);
|
||||
|
||||
background-size:
|
||||
32px 32px,
|
||||
200px 32px,
|
||||
180px 32px,
|
||||
230px 16px,
|
||||
100% 40px,
|
||||
280px 100%;
|
||||
|
||||
background-position:
|
||||
24px 30px,
|
||||
66px 30px,
|
||||
24px 90px,
|
||||
24px 142px,
|
||||
0 180px,
|
||||
0 0;
|
||||
}
|
10
packages/web/src/next/component/skeletonLoading.tsx
Normal file
10
packages/web/src/next/component/skeletonLoading.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||
import Style from "./skeletonLoading.module.css";
|
||||
|
||||
export default function Load(props?: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {
|
||||
return <div {...props}>
|
||||
<div className={Style["animated-background"]}>
|
||||
<div className={`${Style["background-masker"]} ${Style["btn-divide-left"]}`}></div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
5
packages/web/src/next/next-env.d.ts
vendored
Normal file
5
packages/web/src/next/next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
14
packages/web/src/next/next.config.cjs
Normal file
14
packages/web/src/next/next.config.cjs
Normal file
@ -0,0 +1,14 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
typedRoutes: true,
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
5
packages/web/src/next/pages/404.tsx
Normal file
5
packages/web/src/next/pages/404.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default function Page404() {
|
||||
return <div style={{textAlign: "center", marginTop: "25vh", fontSize: "xx-large", color: "#ec8080"}}>
|
||||
The page you requested does not exist
|
||||
</div>;
|
||||
}
|
90
packages/web/src/next/pages/Navbar.module.css
Normal file
90
packages/web/src/next/pages/Navbar.module.css
Normal file
@ -0,0 +1,90 @@
|
||||
.navigation {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
/* border-left: 6px; */
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.navigation {
|
||||
background-color: rgb(0, 0, 0);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
font-size: 1.3rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.navigation-menu {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.navigation-menu ul {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
.navigation-menu li {
|
||||
list-style-type: none;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
.navigation-menu li a {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hamburger {
|
||||
border: 0;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
padding: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background-color: #283b8b;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 25px;
|
||||
transform: translateY(-50%);
|
||||
display: none;
|
||||
}
|
||||
.hamburger:hover {
|
||||
background-color: #2642af;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.navigation-menu ul {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: calc(100vh - 77px);
|
||||
background-color: white;
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
.navigation-menu li {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
.navigation-menu li a {
|
||||
color: black;
|
||||
width: 100%;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
.navigation-menu li:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.navigation-menu ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navigation-menu.expanded ul {
|
||||
display: block;
|
||||
}
|
||||
}
|
34
packages/web/src/next/pages/_app.tsx
Normal file
34
packages/web/src/next/pages/_app.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { AppProps } from "next/app";
|
||||
import "../style/main.css";
|
||||
import NavbarStyle from "./Navbar.module.css";
|
||||
|
||||
export default function BdsApp({ Component, pageProps }: AppProps) {
|
||||
const ExtraNavbar = Array.from<{ name: string, path?: string, action?: () => void }>(pageProps.navbarprops || Component["navBar"] || []);
|
||||
return <>
|
||||
<div>
|
||||
<nav className={NavbarStyle.navigation}>
|
||||
<a href="/dashboard" className="brand-name">
|
||||
Dashboard
|
||||
</a>
|
||||
<div
|
||||
className={NavbarStyle["navigation-menu"]}>
|
||||
<ul>
|
||||
{
|
||||
ExtraNavbar.map((value, index) => {
|
||||
return <li key={`keyNavbar_${index}`}>
|
||||
<a href={value.path||"#"} onClick={value.action}>{value.name}</a>
|
||||
</li>
|
||||
})
|
||||
}
|
||||
<li>
|
||||
<a href="/about">About</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="appBody">
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
</>;
|
||||
}
|
8
packages/web/src/next/pages/about.module.css
Normal file
8
packages/web/src/next/pages/about.module.css
Normal file
@ -0,0 +1,8 @@
|
||||
.about {
|
||||
text-align: center;
|
||||
margin-top: 15vh;
|
||||
}
|
||||
|
||||
.version {
|
||||
color: #0055e5;
|
||||
}
|
20
packages/web/src/next/pages/about.tsx
Normal file
20
packages/web/src/next/pages/about.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import corepkg from "@the-bds-maneger/core/package.json";
|
||||
// @ts-ignore
|
||||
import pkg from "../../../package.json";
|
||||
import Style from "./about.module.css";
|
||||
|
||||
export default function Home() {
|
||||
return <div className={Style.about}>
|
||||
<div>
|
||||
Bds maneger WEB Dashboard
|
||||
</div>
|
||||
<div>
|
||||
This is a project of several Projects by Matheus Sampaio Queiroga (<a href="https://github.com/Sirherobrine23">@Sirherobrine23</a>)
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<div>Dashboard version <span className={Style["version"]}>{pkg.version}</span></div>
|
||||
<div>Core version <span className={Style["version"]}>{corepkg.version}</span></div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
3
packages/web/src/next/pages/index.tsx
Normal file
3
packages/web/src/next/pages/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function Home() {
|
||||
return <>Works from Express + Next.js</>;
|
||||
}
|
22
packages/web/src/next/style/main.css
Normal file
22
packages/web/src/next/style/main.css
Normal file
@ -0,0 +1,22 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Karla", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.appBody {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: rgb(24, 24, 24);
|
||||
color: white;
|
||||
}
|
||||
}
|
28
packages/web/src/next/tsconfig.json
Normal file
28
packages/web/src/next/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"resolveJsonModule": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"DOM.Iterable"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"next.config.cjs",
|
||||
"**/next.config.*js",
|
||||
"**/.next/"
|
||||
],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
]
|
||||
}
|
30
packages/web/src/reactServer.ts
Normal file
30
packages/web/src/reactServer.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import _next, { NextConfig } from "next";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const next: typeof _next.default = _next as any;
|
||||
let _require = createRequire(import.meta.url);
|
||||
export const dev = import.meta.url.endsWith(".ts");
|
||||
|
||||
const dir = path.join(__dirname, "next");
|
||||
const nextConfig: NextConfig = _require(path.join(dir, "next.config.cjs"));
|
||||
nextConfig.env ||= {};
|
||||
|
||||
export const nextApp = next({
|
||||
customServer: true,
|
||||
quiet: true,
|
||||
conf: nextConfig,
|
||||
dir,
|
||||
dev,
|
||||
});
|
||||
|
||||
await nextApp.prepare();
|
||||
export const nextHandler = nextApp.getRequestHandler();
|
||||
export const nextUpgarde = nextApp.getUpgradeHandler();
|
||||
export const {
|
||||
render: pageRender,
|
||||
render404,
|
||||
renderError
|
||||
} = nextApp;
|
12
packages/web/tsconfig.json
Normal file
12
packages/web/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
}
|
||||
],
|
||||
"exclude": [
|
||||
"src/next/next.config.cjs",
|
||||
"src/next/**/*.tsx"
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user