Web interface #525
12
.github/workflows/phpBuild.yaml
vendored
12
.github/workflows/phpBuild.yaml
vendored
@ -215,6 +215,18 @@ jobs:
|
||||
with:
|
||||
path: ./phpOutput
|
||||
|
||||
- name: Rename files
|
||||
run: |
|
||||
cd phpOutput
|
||||
OIFS="$IFS"
|
||||
IFS=$'\n'
|
||||
for file in $(find . -type f)
|
||||
do
|
||||
echo "Working on ${file} ..."
|
||||
mv -v "$file" "$(basename "$file")"
|
||||
done
|
||||
IFS="$OIFS"
|
||||
|
||||
- name: Upload to bucket
|
||||
run: node .github/uploadToBucket.mjs phpOutput:php_bin
|
||||
timeout-minutes: 25
|
||||
|
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 }}"
|
||||
|
48
packages/web/config.yml
Normal file
48
packages/web/config.yml
Normal file
@ -0,0 +1,48 @@
|
||||
port: 3000
|
||||
cookieSecret: 822d8c0f-85ee-4bb9-a3c6-7e4c7252fc81
|
||||
mongo:
|
||||
uri: mongodb://127.0.0.1
|
||||
databaseName: bdsWeb
|
||||
sshServer:
|
||||
hostKeys:
|
||||
- |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIG4gIBAAKCAYEAt8fZZ/+VysjN5FjoedhWzdzpQJvXcKlSgGUbUIv9ymhCJfGH
|
||||
UWdElFOdn2ffIWEL/rB6pu8wsLbUjhHOPyvRTuYYIzk4QjBcGhqR4sKWNiR00gAH
|
||||
FUicH7an7xG1itlRMZAIf5C5s0Wwo/X4s2WG28nR1l2dED2IxOqntE4egAeujymY
|
||||
ianvexVMXMJCCV4PSLMEQtwzbf2ZuYdxJuazkKYM5FOETSIG1ius42InYp75LXum
|
||||
cf844bnxF8VsWAvoEeNHihr7Vr+4BptUR32onF3C6PxR8cdG+0K/v5WoOPUHPvZQ
|
||||
hDh+qf9ig0aaXVUqezIYNPSwG2rUwvzVJZ7EtTq+7+j1Nr1zOpQl0BQIMvlt+PJy
|
||||
nS9uaElUSBGbjf6ffHZXfVJIsA1kY28BpbYd8Ie/y9YAeQeug+rKxVKoiFzFhnFp
|
||||
oi9OsUpbP9w05Ry0OAkgXfyNXGmY9c7j4wwy/Kf8fnWIRE3GuxGquHYm1I45Bd8Y
|
||||
eNQxq5c1ObpGIOlNAgMBAAECggGAHP9C7dZVZ6gUWG2wzJzWYWf0Q7XzIbsNoeWS
|
||||
EDzuX9PgcAUycSUmnHKvAZpNigI6dsaYq6M83v0R/5KOpMgAn/7NGy2pk0P3HEVJ
|
||||
9Gn4cnWBGytW8sRAof5bc+yq5MwSNAxCUwJeUotx6gTi50djJmWrHrQplojm/g76
|
||||
RAg5ZsEPiVYqU7pE53o+ULpz2e+F4NYiG3yL/tRPP4c/0r/+4BlbGmGVE+iFBKDi
|
||||
iAQSK8ziiwsiAYWgD3mbsUD8rc2+3TRaVe2Gf3Dom5G1Kf82QTOv7ndOlU5HClFZ
|
||||
R/LRrx916bS3oL5f8VQk53L8M3E9fAo9stKTuklsHtMtj99yVjDNTxbjDJzjYffz
|
||||
NnN888irXhbsOcQ6fW+PTJmq+X1kSo+72Xf2WK9nH35u9Ns0qaOppYXmDMlDMCzm
|
||||
ITcUG+CI9rMJNSHOD92FlabA68hJn9v+OPpFQiZMtIL5vWUEmEcypO++o1YKcl/3
|
||||
fIBMhCTkJNDC5S7meRJOxMWls/CfAoHBANq1TuFunwF8MZGl98DEAgGbPjMqACbd
|
||||
dvj/b0YzCs2FvBFal7min0VVJHxmktZZ6Q4dUTb0/o+VkLdwPg8Ulwqe5rWsageH
|
||||
11y0dqPMch7obFa45ndIH2aag3QZRf5xgUs75J12uLSrm82lcJ3UGEUq/ouyfhVG
|
||||
xbJgdrBFnjdpQzBy0Ec27Ul7luZrs0JHNu/wFhUmh1q0MH+L4TC2f2IJDPKe88W8
|
||||
8+wo1TRXeuIFwsFmRJAgdAeTvUm5LO1pzwKBwQDXHfCuH46UCZrenRWhJN4DTD0I
|
||||
7MvBXE4hkim8BCnhTKIIt5PeAduv3+wRAOQAs2mZjTOy3eYiLcMXj7MyyY0Z8zZR
|
||||
ud9oZlGDNI8qIhgYdTG+GBFyk8J7TO7atGyBsMxYw8DdzrfWTVny0qdRO+PBj7Kg
|
||||
fe679IDzs+/8nFBErjfZPm/1G25/bH7HISVdogWXFQo2Aiv5OlgwWxfHyvBQMaj/
|
||||
bnZpoRTy3wigtQVI1pze9UnaUsaqUdQUyG4j7iMCgcBwI5Sta+3lcgtsdZ/B2/53
|
||||
WmUbEMcBJn6xDy+728IHPTH/5+ZxzVtCznQlwOY7N+CSVx/kQNwhPPv1wnxfeRw0
|
||||
2uLKdfvrnpAjPXizZcmB5YRfNvEcagOHtWP/bFICM6qqq6v9vPjZ0j/Rwqkqk6xa
|
||||
EsBvWnbha+dEHcfde+Sy3qsPtV7rlNM34UGvJbvFGQSnI//+mkG+lTNkwpEVTA2m
|
||||
c6OK50twsQCUrx8adlxQdcm1Jj2zdKBpnivpGCRcGusCgcAsCiQg8bL12bWMB6rK
|
||||
78pH8I66Sgg40NXqB4tlw5BzYIX3tOWf2M+KBRTGMmF7Rz/CQOcpokYgDzBWnYO5
|
||||
TQwHGQSw3JXBQNlFPDhnDs3SDAQf/7tqspdpOMnZmoBwWKqtLX5Jqq12QSW+EaRR
|
||||
fCpXkTynoMCEWD0iJ19lXvcL6ILkheTT0Ebh7WtTBxUoQyhT0unhPraT8n7lf3Cx
|
||||
5XknYdNQX+P8Ig8w3bpddFHdpJo/BwaUnMexTMlXa+Uok/MCgcBQQ91L2dCtvYVB
|
||||
K9dBKJVLBhC1eS6tKgD4aRsRhMFnYIMaG3qR7Arstc2e1TousSZhJ0GLlB+Ds4Z5
|
||||
CD8DF5EoLxu3Bv1sGRUVePgx2P+fCwhb9wJnzoqRbgGo2+R8/S/89lkaQv+B6cHi
|
||||
p5m4XggwEDQmIUiIm24r26Ef43ZRh+6awN5VOxZ5/tKbb3XK7UwxOdUzMLs6N30Q
|
||||
NlUQYPdR1dVv5OSwbEgwjx1ROWU3mMfd1sK34cS7ahe8cg/l6fs=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
port: 8022
|
@ -24,6 +24,7 @@
|
||||
"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"
|
||||
},
|
||||
|
58
packages/web/src/config.ts
Normal file
58
packages/web/src/config.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import extendFs from "@sirherobrine23/extends";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import yaml from "yaml";
|
||||
import { createSSHKey } from "./db.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export type configSchema = {
|
||||
port: number;
|
||||
cookieSecret: string;
|
||||
sshServer: {
|
||||
hostKeys: string[];
|
||||
port?: number;
|
||||
banner?: string;
|
||||
};
|
||||
mongo?: {
|
||||
uri: string;
|
||||
databaseName?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export async function getConfig() {
|
||||
const configPath = path.resolve(process.cwd(), process.env.CONFIG_PATH || "./config.yml");
|
||||
let userConfig: configSchema|undefined;
|
||||
const config: configSchema = {
|
||||
port: Number(process.env.PORT || "3000"),
|
||||
cookieSecret: process.env.COOKIE_SECRET || randomUUID(),
|
||||
mongo: {
|
||||
uri: process.env.MONGO_URI || "mongodb://127.0.0.1",
|
||||
databaseName: process.env.DB_NAME || "bdsWeb"
|
||||
},
|
||||
sshServer: {
|
||||
hostKeys: [
|
||||
(await createSSHKey()).private,
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
if (await extendFs.exists(configPath)) {
|
||||
userConfig = yaml.parse(await fs.readFile(configPath, "utf8"));
|
||||
if (typeof userConfig.port === "number" && userConfig.port >= 0) config.port = userConfig.port;
|
||||
if (typeof userConfig.cookieSecret === "string") config.cookieSecret = userConfig.cookieSecret;
|
||||
if (typeof userConfig.mongo === "object" && !(Array.isArray(userConfig.mongo))) {
|
||||
if (typeof userConfig.mongo.databaseName === "string") config.mongo.databaseName = userConfig.mongo.databaseName;
|
||||
if (typeof userConfig.mongo.uri === "string") config.mongo.uri = userConfig.mongo.uri;
|
||||
}
|
||||
if (typeof userConfig.sshServer === "object" && !(Array.isArray(userConfig.sshServer))) {
|
||||
if (typeof userConfig.sshServer.banner === "string") config.sshServer.banner = userConfig.sshServer.banner;
|
||||
if (typeof userConfig.sshServer.port === "number" && userConfig.sshServer.port >= 0) config.sshServer.port = userConfig.sshServer.port;
|
||||
if (Array.isArray(userConfig.sshServer.hostKeys) && userConfig.sshServer.hostKeys.length > 0) config.sshServer.hostKeys = userConfig.sshServer.hostKeys;
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(configPath, yaml.stringify(config));
|
||||
return config;
|
||||
}
|
||||
|
||||
export const config = await getConfig();
|
@ -1,5 +1,6 @@
|
||||
import cookie, { Store } from "express-session";
|
||||
import { database } from "./db.js";
|
||||
import { config } from "./config.js";
|
||||
|
||||
declare module "express-session" {
|
||||
interface SessionData {
|
||||
@ -17,14 +18,27 @@ class bdsSession extends Store {
|
||||
nMap = new Map<string, cookie.SessionData>();
|
||||
destroy(sid: string, callback?: (err?: any) => void): void {
|
||||
if (this.nMap.has(sid)) this.nMap.delete(sid);
|
||||
cookieCollection.deleteOne({sid}).then(() => callback(), err => callback(err));
|
||||
cookieCollection.deleteOne({ sid }).then(() => callback(), err => callback(err));
|
||||
}
|
||||
|
||||
get(sid: string, callback: (err?: any, session?: cookie.SessionData) => void) {
|
||||
if (this.nMap.has(sid)) return callback(null, this.nMap.get(sid));
|
||||
(async () => {
|
||||
try {
|
||||
const inDb = await cookieCollection.findOne({sid});
|
||||
const inDb = await cookieCollection.findOne({ sid });
|
||||
if (inDb) return callback(null, inDb.session);
|
||||
return callback();
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
load(sid: string, callback: (err?: any, session?: cookie.SessionData) => any): void {
|
||||
if (this.nMap.has(sid)) return callback(null, this.nMap.get(sid));
|
||||
(async () => {
|
||||
try {
|
||||
const inDb = await cookieCollection.findOne({ sid });
|
||||
if (inDb) return callback(null, inDb.session);
|
||||
return callback();
|
||||
} catch (err) {
|
||||
@ -38,8 +52,8 @@ class bdsSession extends Store {
|
||||
try {
|
||||
if (this.nMap.has(sid)) return callback();
|
||||
this.nMap.set(sid, session);
|
||||
const existsInDb = await cookieCollection.findOne({sid});
|
||||
if (existsInDb) await cookieCollection.deleteOne({sid});
|
||||
const existsInDb = await cookieCollection.findOne({ sid });
|
||||
if (existsInDb) await cookieCollection.deleteOne({ sid });
|
||||
await cookieCollection.insertOne({
|
||||
sid,
|
||||
session: typeof session["toJSON"] === "function" ? session["toJSON"]() : session,
|
||||
@ -73,7 +87,7 @@ class bdsSession extends Store {
|
||||
|
||||
export default cookie({
|
||||
name: "bdsLogin",
|
||||
secret: process.env.COOKIE_SECRET,
|
||||
secret: config.cookieSecret,
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
@ -82,4 +96,4 @@ export default cookie({
|
||||
secure: "auto"
|
||||
},
|
||||
store: new bdsSession(),
|
||||
})
|
||||
});
|
@ -1,16 +1,18 @@
|
||||
import { serverManegerV1, bdsManegerRoot, runServer, runOptions } from "@the-bds-maneger/core";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { extendsFS } from "@sirherobrine23/extends";
|
||||
import { promisify } from "node:util";
|
||||
import { bdsManegerRoot, runOptions, runServer, serverManegerV1 } from "@the-bds-maneger/core";
|
||||
import { MongoClient } from "mongodb";
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
export const { MONGO_URI = "mongodb://127.0.0.1", DB_NAME = "bdsWeb" } = process.env;
|
||||
import path from "node:path";
|
||||
import util from "node:util";
|
||||
import { getConfig } from "./config.js";
|
||||
import { uniqueNamesGenerator, names, colors, animals, adjectives } from "unique-names-generator";
|
||||
|
||||
export const client = await (new MongoClient(MONGO_URI)).connect();
|
||||
export const database = client.db(DB_NAME);
|
||||
const config = await getConfig();
|
||||
export const client = await (new MongoClient(config.mongo.uri)).connect();
|
||||
export const database = client.db(config.mongo.databaseName);
|
||||
|
||||
export type userPermission = "root"|"admin"|"confirm";
|
||||
export type userPermission = "root" | "admin" | "confirm";
|
||||
export type userCollection = {
|
||||
ID: string;
|
||||
createAt: Date;
|
||||
@ -22,6 +24,10 @@ export type userCollection = {
|
||||
username: string;
|
||||
permissions: userPermission[];
|
||||
tokens: string[];
|
||||
sshKeys: {
|
||||
private: string;
|
||||
public: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const userCollection = database.collection<userCollection>("user");
|
||||
@ -32,18 +38,33 @@ export async function createToken() {
|
||||
let str: string = "";
|
||||
for (let i = 0; buf.length > i; i++) {
|
||||
if ((/[a-zA-Z0-9]/).test(String.fromCharCode(buf[i]))) str += String.fromCharCode(buf[i]);
|
||||
else str += randomInt(2, 20000);
|
||||
else str += crypto.randomInt(2, 20000);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
while (true) {
|
||||
if (await userCollection.findOne({tokens: [(token = "tk_"+bufToChar(randomBytes(16)))]})) continue;
|
||||
if (await userCollection.findOne({ tokens: [(token = "tk_" + bufToChar(crypto.randomBytes(16)))] })) continue;
|
||||
break;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function passworldSc(input: string): Promise<{hash: string, salt: string}> {
|
||||
export async function createSSHKey() {
|
||||
const generateKeyPair = util.promisify(crypto.generateKeyPair);
|
||||
return generateKeyPair("rsa", {
|
||||
modulusLength: 3072,
|
||||
privateKeyEncoding: {
|
||||
format: "pem",
|
||||
type: "pkcs1"
|
||||
},
|
||||
publicKeyEncoding: {
|
||||
type: "pkcs1",
|
||||
format: "pem"
|
||||
}
|
||||
}).then(a => ({ private: String(a.privateKey), public: String(a.publicKey) }));
|
||||
}
|
||||
|
||||
export async function passworldSc(input: string): Promise<{ hash: string, salt: string }> {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const secret = crypto.randomBytes(24);
|
||||
return new Promise((done, reject) => {
|
||||
@ -81,13 +102,15 @@ export async function passwordCheck(info: userCollection, password: string) {
|
||||
export type serverDB = {
|
||||
ID: string;
|
||||
platform: serverManegerV1["platform"];
|
||||
name: string;
|
||||
users: string[];
|
||||
};
|
||||
|
||||
export const serversIDs = database.collection<serverDB>("server");
|
||||
export const serveCollection = database.collection<serverDB>("server");
|
||||
export const serversIDs = serveCollection;
|
||||
|
||||
export async function getServerPaths(ID: string): Promise<serverManegerV1> {
|
||||
const info = await serversIDs.findOne({ID});
|
||||
const info = await serveCollection.findOne({ ID });
|
||||
if (!(info)) throw new Error("Server not exists!");
|
||||
|
||||
const rootPath = path.join(bdsManegerRoot, info.platform, ID);
|
||||
@ -96,7 +119,7 @@ export async function getServerPaths(ID: string): Promise<serverManegerV1> {
|
||||
const log = path.join(rootPath, "logs");
|
||||
|
||||
// Create folders
|
||||
for (const p of [serverFolder, backup, log]) if (!(await extendsFS.exists(p))) await fs.mkdir(p, {recursive: true});
|
||||
for (const p of [serverFolder, backup, log]) if (!(await extendsFS.exists(p))) await fs.mkdir(p, { recursive: true });
|
||||
|
||||
return {
|
||||
id: ID,
|
||||
@ -120,16 +143,21 @@ export async function createServerID(platform: serverManegerV1["platform"], user
|
||||
// Create Server ID
|
||||
let ID: string;
|
||||
while (true) {
|
||||
if (await userCollection.findOne({ID: (ID = randomUUID().split("-").join("_"))})) continue;
|
||||
if (await userCollection.findOne({ ID: (ID = crypto.randomUUID().split("-").join("_")) })) continue;
|
||||
else if (await extendsFS.exists(path.join(bdsManegerRoot, platform, ID))) continue;
|
||||
break;
|
||||
}
|
||||
|
||||
// Insert
|
||||
await serversIDs.insertOne({ID, platform, users: []});
|
||||
await serveCollection.insertOne({
|
||||
ID,
|
||||
name: uniqueNamesGenerator({dictionaries: [ names, colors, animals, adjectives ]}),
|
||||
platform,
|
||||
users: []
|
||||
});
|
||||
|
||||
// If seted user inject to DB
|
||||
if (usersIds && usersIds.length > 0) await serversIDs.findOneAndUpdate({ID}, {$set: {users: usersIds}});
|
||||
if (usersIds && usersIds.length > 0) await serveCollection.findOneAndUpdate({ ID }, { $set: { users: usersIds } });
|
||||
|
||||
return getServerPaths(ID);
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
import "dotenv/config.js";
|
||||
import express from "express";
|
||||
import cookie from "./cookie.js";
|
||||
import expressLayer from "express/lib/router/layer.js";
|
||||
import * as nextPage from "./reactServer.js";
|
||||
import mcserverAPI from "./mcserver.js";
|
||||
import { config } from "./config.js";
|
||||
import cookie from "./cookie.js";
|
||||
import loginRegisterRoute from "./login.js";
|
||||
import mcserverAPI from "./mcserver.js";
|
||||
import * as nextPage from "./reactServer.js";
|
||||
import { server as sshServer } from "./ssh.js";
|
||||
|
||||
// Patch express promise catch's
|
||||
expressLayer.prototype.handle_request = async function handle_request_promised(...args) {
|
||||
@ -18,7 +20,7 @@ expressLayer.prototype.handle_request = async function handle_request_promised(.
|
||||
const app = express();
|
||||
|
||||
app.disable("etag").disable("x-powered-by");
|
||||
app.use(cookie, express.json(), express.urlencoded({extended: true}));
|
||||
app.use(cookie, express.json(), express.urlencoded({ extended: true }));
|
||||
|
||||
// API
|
||||
app.use("/api/mcserver", mcserverAPI);
|
||||
@ -30,10 +32,17 @@ app.all("*", (req, res) => nextPage.nextHandler(req, res));
|
||||
// 500 error
|
||||
app.use((err, _req, res, _next) => {
|
||||
console.error(err);
|
||||
res.status(500).json({error: err?.message||err})
|
||||
res.status(500).json({ error: err?.message || err })
|
||||
});
|
||||
|
||||
app.listen(Number(process.env.PORT || "3000"), function() {
|
||||
console.log("Server listen on %O", this.address());
|
||||
app.listen(config.port, function () {
|
||||
const addr = this.address();
|
||||
console.log("Dashboard/API listen on %O", typeof addr === "object" ? addr.port : Number(addr));
|
||||
this.on("upgrade", nextPage.nextUpgarde);
|
||||
if (config.sshServer.port >= 0) {
|
||||
sshServer.listen(config.sshServer.port, function listenOn() {
|
||||
const addr = sshServer.address();
|
||||
console.log("SSH listen on %O", typeof addr === "object" ? addr.port : Number(addr));
|
||||
});
|
||||
}
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import express from "express";
|
||||
import rateLimit from "express-rate-limit";
|
||||
// import rateLimit from "express-rate-limit";
|
||||
import crypto from "node:crypto";
|
||||
import { createToken, passwordCheck, passworldSc, userCollection } from "./db.js";
|
||||
import { createToken, passwordCheck, passworldSc, createSSHKey, userCollection } from "./db.js";
|
||||
import { pageRender } from "./reactServer.js";
|
||||
import path from "node:path";
|
||||
|
||||
@ -16,13 +16,6 @@ app.get("/register", (req, res) => {
|
||||
return pageRender(req, res, "/register");
|
||||
});
|
||||
|
||||
// Rate limit
|
||||
app.use(rateLimit({
|
||||
max: 500,
|
||||
windowMs: 1000 * 60 * 60 * 2,
|
||||
message: "Try again more later, you have many requests!"
|
||||
}));
|
||||
|
||||
app.post("/api/login", async (req, res) => {
|
||||
if (typeof req.body !== "object") return res.status(400).json({ error: "Require body to login" });
|
||||
const existsUser = await userCollection.findOne({ $or: [{ username: req.body.username }, { email: req.body.username }] });
|
||||
@ -48,23 +41,27 @@ app.post("/api/register", async (req, res) => {
|
||||
const { username, email, password } = req.body;
|
||||
if (!(typeof username === "string" && typeof email === "string")) return res.status(400).json({ error: "Invalid username and email body" });
|
||||
else if (!(typeof password === "string" && (password.length >= 8))) return res.status(400).json({ error: "Require password with 8 characters" });
|
||||
else if (await userCollection.findOne({ $or: [{ username }, { email }] })) return res.status(400).json({ error: "Username or Email in use!" });
|
||||
else if (await userCollection.findOne({ email})) return res.status(400).json({ error: "email in use!" });
|
||||
else if (await userCollection.findOne({ username})) return res.status(400).json({ error: "username in use!" });
|
||||
const passEncrypt = await passworldSc(password);
|
||||
let ID: string;
|
||||
while (true) if (!(await userCollection.findOne({ ID: (ID = crypto.randomUUID()) }))) break;
|
||||
const token = await createToken();
|
||||
const sshKey = await createSSHKey();
|
||||
await userCollection.insertOne({
|
||||
ID, createAt: new Date(),
|
||||
email, username, password: passEncrypt,
|
||||
tokens: [token],
|
||||
permissions: [
|
||||
"confirm"
|
||||
]
|
||||
],
|
||||
tokens: [token],
|
||||
sshKeys: [sshKey]
|
||||
});
|
||||
|
||||
return res.json({
|
||||
return res.status(201).json({
|
||||
ID,
|
||||
token
|
||||
token,
|
||||
sshKey
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -46,7 +46,7 @@ app.use(async (req, res, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
const sessionMAP = new Map<string, serverRun>();
|
||||
export const sessionMAP = new Map<string, serverRun>();
|
||||
|
||||
// List auth user server allow access
|
||||
app.get("/", async (req, res) => {
|
||||
@ -54,6 +54,7 @@ app.get("/", async (req, res) => {
|
||||
return res.json(servers.map(info => ({
|
||||
ID: info.ID,
|
||||
platform: info.platform,
|
||||
name: info.name,
|
||||
running: sessionMAP.has(info.ID),
|
||||
})));
|
||||
});
|
||||
@ -63,7 +64,7 @@ app.post("/", async (req, res) => {
|
||||
if (!(req.userInfo.permissions.includes("admin"))) return res.status(401).json({ error: "You no have access to create server" });
|
||||
else if (typeof req.body !== "object") return res.status(400).json({ error: "Require body to setup server" });
|
||||
const { platform } = req.body;
|
||||
if (!(platform === "bedrock" || platform === "java")) res.status(400).json({ error: "Invalid platform" });
|
||||
if (!(platform === "bedrock" || platform === "java")) return res.status(400).json({ error: "Invalid platform", body: req.body });
|
||||
const v1 = await createServerID(platform, [req.session.userID]);
|
||||
if (platform === "bedrock") {
|
||||
await Bedrock.installServer(v1, { version: req.body.version, altServer: req.body.altServer, allowBeta: !!req.body.allowBeta });
|
||||
@ -79,18 +80,18 @@ app.post("/", async (req, res) => {
|
||||
app.delete("/", async (req, res) => {
|
||||
if (!(req.userInfo.permissions.includes("admin"))) return res.status(401).json({ error: "You no have access to create server" });
|
||||
else if (typeof req.body !== "object") return res.status(400).json({ error: "Require body to setup server" });
|
||||
const serverInfo = await serversIDs.findOne({ID: String(req.body.id)});
|
||||
if (!(serverInfo)) return res.status(404).json({error: "Server not exists"});
|
||||
const serverInfo = await serversIDs.findOne({ ID: String(req.body.id) });
|
||||
if (!(serverInfo)) return res.status(404).json({ error: "Server not exists" });
|
||||
if (sessionMAP.has(serverInfo.ID)) await sessionMAP.get(serverInfo.ID).stopServer();
|
||||
const v1 = await getServerPaths(serverInfo.ID);
|
||||
await fs.rm(v1.rootPath, {recursive: true, force: true});
|
||||
return res.json((await serversIDs.findOneAndDelete({ID: serverInfo.ID})).value);
|
||||
await fs.rm(v1.rootPath, { recursive: true, force: true });
|
||||
return res.json((await serversIDs.findOneAndDelete({ ID: serverInfo.ID })).value);
|
||||
});
|
||||
|
||||
app.get("/server/:ID", async (req, res) => {
|
||||
const serverInfo = await serversIDs.findOne({ID: req.params.ID});
|
||||
if (!(serverInfo)) return res.status(404).json({error: "Server not exists"});
|
||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({error: "You do not have permission for this server"});
|
||||
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
||||
if (!(serverInfo)) return res.status(404).json({ error: "Server not exists" });
|
||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({ error: "You do not have permission for this server" });
|
||||
const Running = sessionMAP.get(serverInfo.ID);
|
||||
return res.json({
|
||||
running: !!Running,
|
||||
@ -100,32 +101,32 @@ app.get("/server/:ID", async (req, res) => {
|
||||
});
|
||||
|
||||
app.get("/server/:ID/hotbackup", async (req, res) => {
|
||||
const serverInfo = await serversIDs.findOne({ID: req.params.ID});
|
||||
if (!(serverInfo)) return res.status(404).json({error: "Server not exists"});
|
||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({error: "You do not have permission for this server"});
|
||||
else if (!(sessionMAP.has(req.params.ID))) return res.status(400).json({error: "Server not running"});
|
||||
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
||||
if (!(serverInfo)) return res.status(404).json({ error: "Server not exists" });
|
||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({ error: "You do not have permission for this server" });
|
||||
else if (!(sessionMAP.has(req.params.ID))) return res.status(400).json({ error: "Server not running" });
|
||||
const run = sessionMAP.get(serverInfo.ID);
|
||||
const data = await run.hotBackup();
|
||||
if (!data) return res.status(503).json({error: "Server not support hot backup"});
|
||||
if (!data) return res.status(503).json({ error: "Server not support hot backup" });
|
||||
return data.pipe(res.writeHead(200, {}));
|
||||
});
|
||||
|
||||
app.post("/server/:ID", async (req, res) => {
|
||||
const serverInfo = await serversIDs.findOne({ID: req.params.ID});
|
||||
if (!(serverInfo)) return res.status(404).json({error: "Server not exists"});
|
||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({error: "You do not have permission for this server"});
|
||||
else if (sessionMAP.has(serverInfo.ID)) return res.status(400).json({error: "the server is already running"});
|
||||
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
||||
if (!(serverInfo)) return res.status(404).json({ error: "Server not exists" });
|
||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({ error: "You do not have permission for this server" });
|
||||
else if (sessionMAP.has(serverInfo.ID)) return res.status(400).json({ error: "the server is already running" });
|
||||
const v1 = await getServerPaths(serverInfo.ID);
|
||||
const server = await (serverInfo.platform === "bedrock" ? Bedrock.startServer : Java.startServer)(v1, {});
|
||||
sessionMAP.set(v1.id, server);
|
||||
server.once("exit", () => sessionMAP.delete(v1.id));
|
||||
return res.status(201).json({ID: v1.id, pid: server.pid});
|
||||
return res.status(201).json({ ID: v1.id, pid: server.pid });
|
||||
});
|
||||
|
||||
app.delete("/server/:ID", async (req, res) => {
|
||||
const serverInfo = await serversIDs.findOne({ID: req.params.ID});
|
||||
if (!(serverInfo)) return res.status(404).json({error: "Server not exists"});
|
||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({error: "You do not have permission for this server"});
|
||||
else if (!(sessionMAP.has(req.params.ID))) return res.status(400).json({error: "Server not running"});
|
||||
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
||||
if (!(serverInfo)) return res.status(404).json({ error: "Server not exists" });
|
||||
else if (!(serverInfo.users.includes(req.session.userID))) return res.status(404).json({ error: "You do not have permission for this server" });
|
||||
else if (!(sessionMAP.has(req.params.ID))) return res.status(400).json({ error: "Server not running" });
|
||||
return res.json(await sessionMAP.get(req.params.ID).stopServer());
|
||||
});
|
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/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>;
|
||||
}
|
20
packages/web/src/next/pages/dashboard/[server_id]/index.tsx
Normal file
20
packages/web/src/next/pages/dashboard/[server_id]/index.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { GetServerSidePropsContext, InferGetStaticPropsType } from "next";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function Maneger(props: InferGetStaticPropsType<typeof getServerSideProps>) {
|
||||
const [ stats, updateStats ] = useState(props.server);
|
||||
return <div>
|
||||
<pre>{JSON.stringify(stats, null, 2)}</pre>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export async function getServerSideProps({req, params: { server_id }}: GetServerSidePropsContext) {
|
||||
const serversData = await fetch(`http://localhost:${process.env.SERVER_PORT}/api/mcserver/server/${String(server_id)}`, {headers: req.headers as any});
|
||||
return {
|
||||
props: {
|
||||
server_id: String(server_id),
|
||||
server: await serversData.json(),
|
||||
navbarprops: [{name: "Settings", path: `${server_id}/settings`}]
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { GetServerSidePropsContext, InferGetStaticPropsType } from "next";
|
||||
|
||||
export default function ServerConfig(props: InferGetStaticPropsType<typeof getServerSideProps>) {
|
||||
return <div>
|
||||
<pre>{JSON.stringify(props, null, 2)}</pre>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export async function getServerSideProps({req, params: { server_id }}: GetServerSidePropsContext) {
|
||||
// const serversData = await fetch(`http://localhost:${process.env.SERVER_PORT}/api/mcserver/server/${String(server_id)}`, {headers: req.headers as any});
|
||||
return {
|
||||
props: {
|
||||
server_id: String(server_id),
|
||||
navbarprops: [{name: "Back", path: `./`}]
|
||||
},
|
||||
};
|
||||
}
|
16
packages/web/src/next/pages/dashboard/index.module.css
Normal file
16
packages/web/src/next/pages/dashboard/index.module.css
Normal file
@ -0,0 +1,16 @@
|
||||
.ServersGrid {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-start;
|
||||
justify-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.serverModule {
|
||||
color: grey;
|
||||
background-color: #00000069;
|
||||
padding: 1.5%;
|
||||
cursor: pointer;
|
||||
border-radius: 36px;
|
||||
border-style: dotted;
|
||||
}
|
54
packages/web/src/next/pages/dashboard/index.tsx
Normal file
54
packages/web/src/next/pages/dashboard/index.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { InferGetStaticPropsType } from "next";
|
||||
import { useState } from "react";
|
||||
import homeStyle from "./index.module.css";
|
||||
|
||||
export default function Dashboard(props: InferGetStaticPropsType<typeof getServerSideProps>) {
|
||||
const [ server, updateServer ] = useState(props.servers);
|
||||
const refresh = () => fetch("/api/mcserver").then(res => res.json().then(data => ({data, res}))).then(data => data.res.status < 300 ? updateServer(data.data) : data.res);
|
||||
|
||||
return <div>
|
||||
<button style={{display: "none"}} id="refreshServers" onClick={refresh}>Refresh Servers</button>
|
||||
<div className={homeStyle["ServersGrid"]}>
|
||||
{server.map((value) => {
|
||||
return <div key={JSON.stringify(value)} className={homeStyle["serverModule"]} onClick={() => {
|
||||
const a = document.createElement("a");
|
||||
a.href = `/dashboard/${value.ID}`;
|
||||
a.click();
|
||||
}}>
|
||||
<div style={{textAlign: "center"}}>
|
||||
<a href={`/dashboard/${value.ID}`}>{value.name}</a>
|
||||
</div>
|
||||
<br />
|
||||
<div>Platform: {value.platform.slice(0, 1).toUpperCase()}{value.platform.slice(1)}</div>
|
||||
<div>Status: <span style={{color: value.running ? "green" : "red"}}>{value.running ? "Avaible" : "Stoped"}</span></div>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
Dashboard.navBar = [
|
||||
{
|
||||
name: "New server",
|
||||
path: "/dashboard/new"
|
||||
},
|
||||
{
|
||||
name: "Refresh",
|
||||
action: () => document.querySelector("#refreshServers")["click"](),
|
||||
}
|
||||
]
|
||||
|
||||
export async function getServerSideProps({req}) {
|
||||
const serversData = await fetch(`http://localhost:${process.env.SERVER_PORT}/api/mcserver`, {headers: req.headers as any});
|
||||
return {
|
||||
props: {
|
||||
servers: (await serversData.json()) as {
|
||||
ID: string,
|
||||
platform: "bedrock"|"java",
|
||||
name: string,
|
||||
running: boolean
|
||||
}[],
|
||||
|
||||
},
|
||||
};
|
||||
}
|
6
packages/web/src/next/pages/dashboard/new.module.css
Normal file
6
packages/web/src/next/pages/dashboard/new.module.css
Normal file
@ -0,0 +1,6 @@
|
||||
.install {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-start;
|
||||
}
|
120
packages/web/src/next/pages/dashboard/new.tsx
Normal file
120
packages/web/src/next/pages/dashboard/new.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { FormEvent, useState } from "react";
|
||||
import indexStyle from "./new.module.css";
|
||||
|
||||
|
||||
export default function NewServer() {
|
||||
const [ currentPlatform, setPlatform ] = useState<"bedrock" | "java">("bedrock");
|
||||
const [ __lock__, setLock ] = useState<boolean>(false);
|
||||
const [ status, setStatus ] = useState<undefined|{ID: string}>();
|
||||
async function submit(form: FormEvent<HTMLFormElement>) {
|
||||
setLock(true);
|
||||
form.preventDefault();
|
||||
const platform: "bedrock" | "java" = form.currentTarget.querySelector("input[name=\"platform\"]:checked")["value"];
|
||||
const altserver: string = form.currentTarget.querySelector("select[name=\"altServer\"]")["value"];
|
||||
|
||||
try {
|
||||
const installStatus = await fetch("/api/mcserver", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
platform,
|
||||
altServer: altserver
|
||||
}),
|
||||
});
|
||||
if (installStatus.status < 300) setStatus(await installStatus.json());
|
||||
else {
|
||||
setLock(false);
|
||||
console.error(await installStatus.json());
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return <div>
|
||||
<form onSubmit={submit} className={indexStyle.install}>
|
||||
<div>
|
||||
<span>Select platform:</span>
|
||||
<div>
|
||||
<div>
|
||||
<input type="radio" id="bedrockPlatform" name="platform" value="bedrock" defaultChecked onClick={() => setPlatform("bedrock")} />
|
||||
<label htmlFor="bedrockPlatform"> Bedrock</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="javaPlatform" name="platform" value="java" onClick={() => setPlatform("java")} />
|
||||
<label htmlFor="javaPlatform"> Java</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>Select server software</span>
|
||||
<br />
|
||||
<select disabled={__lock__} name="altServer">
|
||||
<option id="mojang" defaultChecked value="mojang">
|
||||
Mojang
|
||||
</option>
|
||||
{ currentPlatform === "bedrock" ?
|
||||
<option id="pocketmine" value="pocketmine">
|
||||
Pocketmine PMMP
|
||||
</option> : null
|
||||
}
|
||||
{ currentPlatform === "bedrock" ?
|
||||
<option id="cloudbust" value="cloudbust">
|
||||
Cloudbust
|
||||
</option> : null
|
||||
}
|
||||
{ currentPlatform === "bedrock" ?
|
||||
<option id="nukkit" value="nukkit">
|
||||
Nukkit
|
||||
</option> : null
|
||||
}
|
||||
{ currentPlatform === "bedrock" ?
|
||||
<option id="powernukkit" value="powernukkit">
|
||||
Powernukkit
|
||||
</option> : null
|
||||
}
|
||||
{ currentPlatform === "java" ?
|
||||
<option id="spigot" value="spigot">
|
||||
Spigot MC
|
||||
</option> : null
|
||||
}
|
||||
{ currentPlatform === "java" ?
|
||||
<option id="paper" value="paper">
|
||||
Paper MC
|
||||
</option> : null
|
||||
}
|
||||
{ currentPlatform === "java" ?
|
||||
<option id="purpur" value="purpur">
|
||||
Purpur MC
|
||||
</option> : null
|
||||
}
|
||||
{ currentPlatform === "java" ?
|
||||
<option id="glowstone" value="glowstone">
|
||||
Glowstone
|
||||
</option> : null
|
||||
}
|
||||
{ currentPlatform === "java" ?
|
||||
<option id="folia" value="folia">
|
||||
folia
|
||||
</option> : null
|
||||
}
|
||||
{ currentPlatform === "java" ?
|
||||
<option id="cuberite" value="cuberite">
|
||||
Cuberite
|
||||
</option> : null
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input disabled={__lock__} type="submit" value="Install" />
|
||||
</div>
|
||||
</form>
|
||||
<div style={{textAlign: "center"}}>
|
||||
{ !status ? <div></div> : <div>
|
||||
<span>{status.ID}</span>
|
||||
<br />
|
||||
<a href={`/dashboard/${status.ID}`}>Open painel</a>
|
||||
</div> }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
@ -1,6 +1,28 @@
|
||||
import { FormEvent } from "react";
|
||||
|
||||
async function onLogin(elemnt: FormEvent<HTMLFormElement>) {
|
||||
elemnt.preventDefault();
|
||||
const inputs = Array.from(elemnt.currentTarget.querySelectorAll("input"));
|
||||
const username = inputs.find(e => e.name === "username").value;
|
||||
const password = inputs.find(e => e.type === "password").value;
|
||||
|
||||
const onCreate = await fetch("/api/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password
|
||||
})
|
||||
});
|
||||
if (onCreate.status === 200) return location.href = "/";
|
||||
return elemnt.currentTarget.querySelector("errorsMessage").textContent = (await onCreate.json()).error;
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
return <>
|
||||
<form action="/api/login" method="post">
|
||||
<form onSubmit={onLogin}>
|
||||
<div>
|
||||
<label>Username/Email: </label>
|
||||
<input type="text" name="username" />
|
||||
@ -9,6 +31,7 @@ export default function LoginPage() {
|
||||
<label>Password: </label>
|
||||
<input type="password" name="password" />
|
||||
</div>
|
||||
<input type="submit" value="Login" />
|
||||
</form>
|
||||
</>;
|
||||
}
|
@ -1,9 +1,44 @@
|
||||
import { FormEvent } from "react";
|
||||
|
||||
async function create(elemnt: FormEvent<HTMLFormElement>) {
|
||||
elemnt.preventDefault();
|
||||
const inputs = Array.from(elemnt.currentTarget.querySelectorAll("input"));
|
||||
const username = inputs.find(e => e.name === "username").value;
|
||||
const email = inputs.find(e => e.name === "email").value;
|
||||
const password = inputs.find(e => e.type === "password").value;
|
||||
|
||||
const onCreate = await fetch("/api/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
email,
|
||||
password
|
||||
})
|
||||
});
|
||||
if (onCreate.status === 201) return; location.href = "/login";
|
||||
elemnt.currentTarget.querySelector("errorsMessage").textContent = (await onCreate.json()).error;
|
||||
}
|
||||
|
||||
export default function RegisterPage() {
|
||||
return <>
|
||||
<form action="/api/register" method="post">
|
||||
<div id="errorsMessage"></div>
|
||||
<form onSubmit={create}>
|
||||
<div>
|
||||
<label>Username:</label>
|
||||
<input type="text" name="username" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Email:</label>
|
||||
<input type="email" name="email" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" />
|
||||
</div>
|
||||
<input type="submit" value="Create" />
|
||||
</form>
|
||||
</>;
|
||||
}
|
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;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"esModuleInterop": true,
|
||||
@ -7,12 +7,16 @@
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"resolveJsonModule": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"next.config.cjs",
|
||||
"**/next.config.*js",
|
||||
"**/.next/"
|
||||
],
|
||||
@ -21,4 +25,4 @@
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,25 @@
|
||||
import _next from "next";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import _next, { NextConfig } from "next";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { config } from "./config.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const next: typeof _next.default = _next as any;
|
||||
let _require: typeof require;
|
||||
if (typeof require === "function") _require = require; else _require = (await import("module")).default.createRequire(import.meta.url);
|
||||
export const dev = import.meta.url.endsWith(".ts"), { PORT = "3000", HOSTNAME = "localhost" } = process.env;
|
||||
export const dev = import.meta.url.endsWith(".ts"), { HOSTNAME = "localhost" } = process.env;
|
||||
|
||||
const dir = path.join(__dirname, "next");
|
||||
const nextConfig: NextConfig = _require(path.join(dir, "next.config.cjs"));
|
||||
nextConfig.env ||= {};
|
||||
nextConfig.env.SERVER_PORT = String(config.port)
|
||||
|
||||
export const nextApp = next({
|
||||
customServer: true,
|
||||
hostname: HOSTNAME,
|
||||
quiet: true,
|
||||
port: Number(PORT),
|
||||
conf: _require(path.join(dir, "next.config.cjs")),
|
||||
port: config.port,
|
||||
conf: nextConfig,
|
||||
dir,
|
||||
dev,
|
||||
});
|
||||
|
51
packages/web/src/ssh.ts
Normal file
51
packages/web/src/ssh.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import ssh2 from "ssh2";
|
||||
import { config } from "./config.js";
|
||||
import { userCollection, serveCollection } from "./db.js";
|
||||
import { sessionMAP } from "./mcserver.js";
|
||||
|
||||
export const server = new ssh2.Server({
|
||||
hostKeys: config.sshServer.hostKeys,
|
||||
banner: config.sshServer.banner,
|
||||
});
|
||||
|
||||
server.on("error", err => console.error(err));
|
||||
server.on("connection", client => {
|
||||
let username: string;
|
||||
client.on("error", err => console.error(err));
|
||||
client.on("authentication", async ctx => {
|
||||
username = ctx.username;
|
||||
const serverInfo = await serveCollection.findOne({ID: username});
|
||||
if (!serverInfo) return ctx.reject();
|
||||
if (ctx.method === "hostbased" || ctx.method === "keyboard-interactive") return ctx.reject(["password", "publickey"]);
|
||||
else if (ctx.method === "none") return ctx.reject(["password", "publickey"]);
|
||||
else if (ctx.method === "publickey") {
|
||||
const { key } = ctx;
|
||||
const Users = await userCollection.find({ID: serverInfo.users}).toArray();
|
||||
const userInfo = Users.find(user => user.sshKeys.find(userKey => {
|
||||
const ps = ssh2.utils.parseKey(userKey.private);
|
||||
if (ps instanceof Error) return false;
|
||||
return Buffer.compare(ps.getPublicSSH(), key.data) === 0;
|
||||
}));
|
||||
if (!userInfo) return ctx.reject();
|
||||
} else if (ctx.method === "password") {
|
||||
const userInfo = await userCollection.findOne({$or: [serverInfo.users.map(ID => ({ID}))], tokens: [ctx.password]});
|
||||
if (!userInfo) return ctx.reject();
|
||||
}
|
||||
return ctx.accept();
|
||||
});
|
||||
client.on("ready", () => {
|
||||
client.on("session", (accept, reject) => {
|
||||
if (!(sessionMAP.has(username))) return reject();
|
||||
const serverSession = sessionMAP.get(username);
|
||||
const session = accept();
|
||||
serverSession.stdout.pipe(session.stdout, {end: false});
|
||||
serverSession.stderr.pipe(session.stderr, {end: false});
|
||||
serverSession.stdin.pipe(session.stdin, {end: false});
|
||||
session.once("close", () => {
|
||||
serverSession.stdout.unpipe(session.stdout);
|
||||
serverSession.stderr.unpipe(session.stderr);
|
||||
session.stdin.unpipe(serverSession.stdin);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -6,6 +6,7 @@
|
||||
}
|
||||
],
|
||||
"exclude": [
|
||||
"src/next/next.config.cjs"
|
||||
"src/next/next.config.cjs",
|
||||
"src/next/**/*.tsx"
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user