Web interface #525
12
.github/workflows/phpBuild.yaml
vendored
12
.github/workflows/phpBuild.yaml
vendored
@ -215,6 +215,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: ./phpOutput
|
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
|
- name: Upload to bucket
|
||||||
run: node .github/uploadToBucket.mjs phpOutput:php_bin
|
run: node .github/uploadToBucket.mjs phpOutput:php_bin
|
||||||
timeout-minutes: 25
|
timeout-minutes: 25
|
||||||
|
16
.github/workflows/spigotBuild.yaml
vendored
16
.github/workflows/spigotBuild.yaml
vendored
@ -51,10 +51,22 @@ jobs:
|
|||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
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
|
- name: Upload to actifial
|
||||||
run: node .github/uploadToBucket.mjs artifacts:SpigotBuild
|
run: node .github/uploadToBucket.mjs SpigotBuild/latest:SpigotBuild
|
||||||
env:
|
env:
|
||||||
ociauth: "${{ secrets.OCI_AUTHKEY }}"
|
ociauth: "${{ secrets.OCI_AUTHKEY }}"
|
||||||
OCI_AUTHKEY: "${{ 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": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"ssh2": "^1.13.0",
|
"ssh2": "^1.13.0",
|
||||||
|
"unique-names-generator": "^4.7.1",
|
||||||
"xterm": "^5.1.0",
|
"xterm": "^5.1.0",
|
||||||
"yaml": "^2.3.1"
|
"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 cookie, { Store } from "express-session";
|
||||||
import { database } from "./db.js";
|
import { database } from "./db.js";
|
||||||
|
import { config } from "./config.js";
|
||||||
|
|
||||||
declare module "express-session" {
|
declare module "express-session" {
|
||||||
interface SessionData {
|
interface SessionData {
|
||||||
@ -17,14 +18,27 @@ class bdsSession extends Store {
|
|||||||
nMap = new Map<string, cookie.SessionData>();
|
nMap = new Map<string, cookie.SessionData>();
|
||||||
destroy(sid: string, callback?: (err?: any) => void): void {
|
destroy(sid: string, callback?: (err?: any) => void): void {
|
||||||
if (this.nMap.has(sid)) this.nMap.delete(sid);
|
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) {
|
get(sid: string, callback: (err?: any, session?: cookie.SessionData) => void) {
|
||||||
if (this.nMap.has(sid)) return callback(null, this.nMap.get(sid));
|
if (this.nMap.has(sid)) return callback(null, this.nMap.get(sid));
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
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);
|
if (inDb) return callback(null, inDb.session);
|
||||||
return callback();
|
return callback();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -38,8 +52,8 @@ class bdsSession extends Store {
|
|||||||
try {
|
try {
|
||||||
if (this.nMap.has(sid)) return callback();
|
if (this.nMap.has(sid)) return callback();
|
||||||
this.nMap.set(sid, session);
|
this.nMap.set(sid, session);
|
||||||
const existsInDb = await cookieCollection.findOne({sid});
|
const existsInDb = await cookieCollection.findOne({ sid });
|
||||||
if (existsInDb) await cookieCollection.deleteOne({sid});
|
if (existsInDb) await cookieCollection.deleteOne({ sid });
|
||||||
await cookieCollection.insertOne({
|
await cookieCollection.insertOne({
|
||||||
sid,
|
sid,
|
||||||
session: typeof session["toJSON"] === "function" ? session["toJSON"]() : session,
|
session: typeof session["toJSON"] === "function" ? session["toJSON"]() : session,
|
||||||
@ -73,7 +87,7 @@ class bdsSession extends Store {
|
|||||||
|
|
||||||
export default cookie({
|
export default cookie({
|
||||||
name: "bdsLogin",
|
name: "bdsLogin",
|
||||||
secret: process.env.COOKIE_SECRET,
|
secret: config.cookieSecret,
|
||||||
resave: true,
|
resave: true,
|
||||||
saveUninitialized: true,
|
saveUninitialized: true,
|
||||||
cookie: {
|
cookie: {
|
||||||
@ -82,4 +96,4 @@ export default cookie({
|
|||||||
secure: "auto"
|
secure: "auto"
|
||||||
},
|
},
|
||||||
store: new bdsSession(),
|
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 { 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 crypto from "node:crypto";
|
||||||
import path from "node:path";
|
|
||||||
import fs from "node:fs/promises";
|
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();
|
const config = await getConfig();
|
||||||
export const database = client.db(DB_NAME);
|
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 = {
|
export type userCollection = {
|
||||||
ID: string;
|
ID: string;
|
||||||
createAt: Date;
|
createAt: Date;
|
||||||
@ -22,6 +24,10 @@ export type userCollection = {
|
|||||||
username: string;
|
username: string;
|
||||||
permissions: userPermission[];
|
permissions: userPermission[];
|
||||||
tokens: string[];
|
tokens: string[];
|
||||||
|
sshKeys: {
|
||||||
|
private: string;
|
||||||
|
public: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userCollection = database.collection<userCollection>("user");
|
export const userCollection = database.collection<userCollection>("user");
|
||||||
@ -32,18 +38,33 @@ export async function createToken() {
|
|||||||
let str: string = "";
|
let str: string = "";
|
||||||
for (let i = 0; buf.length > i; i++) {
|
for (let i = 0; buf.length > i; i++) {
|
||||||
if ((/[a-zA-Z0-9]/).test(String.fromCharCode(buf[i]))) str += String.fromCharCode(buf[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;
|
return str;
|
||||||
}
|
}
|
||||||
while (true) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
return token;
|
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 iv = crypto.randomBytes(16);
|
||||||
const secret = crypto.randomBytes(24);
|
const secret = crypto.randomBytes(24);
|
||||||
return new Promise((done, reject) => {
|
return new Promise((done, reject) => {
|
||||||
@ -81,13 +102,15 @@ export async function passwordCheck(info: userCollection, password: string) {
|
|||||||
export type serverDB = {
|
export type serverDB = {
|
||||||
ID: string;
|
ID: string;
|
||||||
platform: serverManegerV1["platform"];
|
platform: serverManegerV1["platform"];
|
||||||
|
name: string;
|
||||||
users: 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> {
|
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!");
|
if (!(info)) throw new Error("Server not exists!");
|
||||||
|
|
||||||
const rootPath = path.join(bdsManegerRoot, info.platform, ID);
|
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");
|
const log = path.join(rootPath, "logs");
|
||||||
|
|
||||||
// Create folders
|
// 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 {
|
return {
|
||||||
id: ID,
|
id: ID,
|
||||||
@ -120,16 +143,21 @@ export async function createServerID(platform: serverManegerV1["platform"], user
|
|||||||
// Create Server ID
|
// Create Server ID
|
||||||
let ID: string;
|
let ID: string;
|
||||||
while (true) {
|
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;
|
else if (await extendsFS.exists(path.join(bdsManegerRoot, platform, ID))) continue;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert
|
// 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 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);
|
return getServerPaths(ID);
|
||||||
}
|
}
|
@ -1,11 +1,13 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import "dotenv/config.js";
|
import "dotenv/config.js";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import cookie from "./cookie.js";
|
|
||||||
import expressLayer from "express/lib/router/layer.js";
|
import expressLayer from "express/lib/router/layer.js";
|
||||||
import * as nextPage from "./reactServer.js";
|
import { config } from "./config.js";
|
||||||
import mcserverAPI from "./mcserver.js";
|
import cookie from "./cookie.js";
|
||||||
import loginRegisterRoute from "./login.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
|
// Patch express promise catch's
|
||||||
expressLayer.prototype.handle_request = async function handle_request_promised(...args) {
|
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();
|
const app = express();
|
||||||
|
|
||||||
app.disable("etag").disable("x-powered-by");
|
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
|
// API
|
||||||
app.use("/api/mcserver", mcserverAPI);
|
app.use("/api/mcserver", mcserverAPI);
|
||||||
@ -30,10 +32,17 @@ app.all("*", (req, res) => nextPage.nextHandler(req, res));
|
|||||||
// 500 error
|
// 500 error
|
||||||
app.use((err, _req, res, _next) => {
|
app.use((err, _req, res, _next) => {
|
||||||
console.error(err);
|
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() {
|
app.listen(config.port, function () {
|
||||||
console.log("Server listen on %O", this.address());
|
const addr = this.address();
|
||||||
|
console.log("Dashboard/API listen on %O", typeof addr === "object" ? addr.port : Number(addr));
|
||||||
this.on("upgrade", nextPage.nextUpgarde);
|
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 express from "express";
|
||||||
import rateLimit from "express-rate-limit";
|
// import rateLimit from "express-rate-limit";
|
||||||
import crypto from "node:crypto";
|
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 { pageRender } from "./reactServer.js";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
@ -16,13 +16,6 @@ app.get("/register", (req, res) => {
|
|||||||
return pageRender(req, res, "/register");
|
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) => {
|
app.post("/api/login", async (req, res) => {
|
||||||
if (typeof req.body !== "object") return res.status(400).json({ error: "Require body to login" });
|
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 }] });
|
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;
|
const { username, email, password } = req.body;
|
||||||
if (!(typeof username === "string" && typeof email === "string")) return res.status(400).json({ error: "Invalid username and email 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 (!(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);
|
const passEncrypt = await passworldSc(password);
|
||||||
let ID: string;
|
let ID: string;
|
||||||
while (true) if (!(await userCollection.findOne({ ID: (ID = crypto.randomUUID()) }))) break;
|
while (true) if (!(await userCollection.findOne({ ID: (ID = crypto.randomUUID()) }))) break;
|
||||||
const token = await createToken();
|
const token = await createToken();
|
||||||
|
const sshKey = await createSSHKey();
|
||||||
await userCollection.insertOne({
|
await userCollection.insertOne({
|
||||||
ID, createAt: new Date(),
|
ID, createAt: new Date(),
|
||||||
email, username, password: passEncrypt,
|
email, username, password: passEncrypt,
|
||||||
tokens: [token],
|
|
||||||
permissions: [
|
permissions: [
|
||||||
"confirm"
|
"confirm"
|
||||||
]
|
],
|
||||||
|
tokens: [token],
|
||||||
|
sshKeys: [sshKey]
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.json({
|
return res.status(201).json({
|
||||||
ID,
|
ID,
|
||||||
token
|
token,
|
||||||
|
sshKey
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ app.use(async (req, res, next) => {
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionMAP = new Map<string, serverRun>();
|
export const sessionMAP = new Map<string, serverRun>();
|
||||||
|
|
||||||
// List auth user server allow access
|
// List auth user server allow access
|
||||||
app.get("/", async (req, res) => {
|
app.get("/", async (req, res) => {
|
||||||
@ -54,6 +54,7 @@ app.get("/", async (req, res) => {
|
|||||||
return res.json(servers.map(info => ({
|
return res.json(servers.map(info => ({
|
||||||
ID: info.ID,
|
ID: info.ID,
|
||||||
platform: info.platform,
|
platform: info.platform,
|
||||||
|
name: info.name,
|
||||||
running: sessionMAP.has(info.ID),
|
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" });
|
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" });
|
else if (typeof req.body !== "object") return res.status(400).json({ error: "Require body to setup server" });
|
||||||
const { platform } = req.body;
|
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]);
|
const v1 = await createServerID(platform, [req.session.userID]);
|
||||||
if (platform === "bedrock") {
|
if (platform === "bedrock") {
|
||||||
await Bedrock.installServer(v1, { version: req.body.version, altServer: req.body.altServer, allowBeta: !!req.body.allowBeta });
|
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) => {
|
app.delete("/", async (req, res) => {
|
||||||
if (!(req.userInfo.permissions.includes("admin"))) return res.status(401).json({ error: "You no have access to create server" });
|
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" });
|
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)});
|
const serverInfo = await serversIDs.findOne({ ID: String(req.body.id) });
|
||||||
if (!(serverInfo)) return res.status(404).json({error: "Server not exists"});
|
if (!(serverInfo)) return res.status(404).json({ error: "Server not exists" });
|
||||||
if (sessionMAP.has(serverInfo.ID)) await sessionMAP.get(serverInfo.ID).stopServer();
|
if (sessionMAP.has(serverInfo.ID)) await sessionMAP.get(serverInfo.ID).stopServer();
|
||||||
const v1 = await getServerPaths(serverInfo.ID);
|
const v1 = await getServerPaths(serverInfo.ID);
|
||||||
await fs.rm(v1.rootPath, {recursive: true, force: true});
|
await fs.rm(v1.rootPath, { recursive: true, force: true });
|
||||||
return res.json((await serversIDs.findOneAndDelete({ID: serverInfo.ID})).value);
|
return res.json((await serversIDs.findOneAndDelete({ ID: serverInfo.ID })).value);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/server/:ID", async (req, res) => {
|
app.get("/server/:ID", async (req, res) => {
|
||||||
const serverInfo = await serversIDs.findOne({ID: req.params.ID});
|
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
||||||
if (!(serverInfo)) return res.status(404).json({error: "Server not exists"});
|
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 (!(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);
|
const Running = sessionMAP.get(serverInfo.ID);
|
||||||
return res.json({
|
return res.json({
|
||||||
running: !!Running,
|
running: !!Running,
|
||||||
@ -100,32 +101,32 @@ app.get("/server/:ID", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get("/server/:ID/hotbackup", async (req, res) => {
|
app.get("/server/:ID/hotbackup", async (req, res) => {
|
||||||
const serverInfo = await serversIDs.findOne({ID: req.params.ID});
|
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
||||||
if (!(serverInfo)) return res.status(404).json({error: "Server not exists"});
|
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 (!(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"});
|
else if (!(sessionMAP.has(req.params.ID))) return res.status(400).json({ error: "Server not running" });
|
||||||
const run = sessionMAP.get(serverInfo.ID);
|
const run = sessionMAP.get(serverInfo.ID);
|
||||||
const data = await run.hotBackup();
|
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, {}));
|
return data.pipe(res.writeHead(200, {}));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/server/:ID", async (req, res) => {
|
app.post("/server/:ID", async (req, res) => {
|
||||||
const serverInfo = await serversIDs.findOne({ID: req.params.ID});
|
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
||||||
if (!(serverInfo)) return res.status(404).json({error: "Server not exists"});
|
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 (!(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"});
|
else if (sessionMAP.has(serverInfo.ID)) return res.status(400).json({ error: "the server is already running" });
|
||||||
const v1 = await getServerPaths(serverInfo.ID);
|
const v1 = await getServerPaths(serverInfo.ID);
|
||||||
const server = await (serverInfo.platform === "bedrock" ? Bedrock.startServer : Java.startServer)(v1, {});
|
const server = await (serverInfo.platform === "bedrock" ? Bedrock.startServer : Java.startServer)(v1, {});
|
||||||
sessionMAP.set(v1.id, server);
|
sessionMAP.set(v1.id, server);
|
||||||
server.once("exit", () => sessionMAP.delete(v1.id));
|
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) => {
|
app.delete("/server/:ID", async (req, res) => {
|
||||||
const serverInfo = await serversIDs.findOne({ID: req.params.ID});
|
const serverInfo = await serversIDs.findOne({ ID: req.params.ID });
|
||||||
if (!(serverInfo)) return res.status(404).json({error: "Server not exists"});
|
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 (!(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"});
|
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());
|
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() {
|
export default function LoginPage() {
|
||||||
return <>
|
return <>
|
||||||
<form action="/api/login" method="post">
|
<form onSubmit={onLogin}>
|
||||||
<div>
|
<div>
|
||||||
<label>Username/Email: </label>
|
<label>Username/Email: </label>
|
||||||
<input type="text" name="username" />
|
<input type="text" name="username" />
|
||||||
@ -9,6 +31,7 @@ export default function LoginPage() {
|
|||||||
<label>Password: </label>
|
<label>Password: </label>
|
||||||
<input type="password" name="password" />
|
<input type="password" name="password" />
|
||||||
</div>
|
</div>
|
||||||
|
<input type="submit" value="Login" />
|
||||||
</form>
|
</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() {
|
export default function RegisterPage() {
|
||||||
return <>
|
return <>
|
||||||
<form action="/api/register" method="post">
|
<div id="errorsMessage"></div>
|
||||||
|
<form onSubmit={create}>
|
||||||
<div>
|
<div>
|
||||||
|
<label>Username:</label>
|
||||||
|
<input type="text" name="username" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Email:</label>
|
||||||
<input type="email" name="email" />
|
<input type="email" name="email" />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Password:</label>
|
||||||
|
<input type="password" name="password" />
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Create" />
|
||||||
</form>
|
</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": {
|
"compilerOptions": {
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
@ -7,12 +7,16 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"target": "ESNext",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"DOM.Iterable"
|
"DOM.Iterable"
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
"next.config.cjs",
|
||||||
"**/next.config.*js",
|
"**/next.config.*js",
|
||||||
"**/.next/"
|
"**/.next/"
|
||||||
],
|
],
|
||||||
@ -21,4 +25,4 @@
|
|||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx"
|
"**/*.tsx"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import _next from "next";
|
import _next, { NextConfig } from "next";
|
||||||
import path from "path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { config } from "./config.js";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const next: typeof _next.default = _next as any;
|
const next: typeof _next.default = _next as any;
|
||||||
let _require: typeof require;
|
let _require: typeof require;
|
||||||
if (typeof require === "function") _require = require; else _require = (await import("module")).default.createRequire(import.meta.url);
|
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 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({
|
export const nextApp = next({
|
||||||
customServer: true,
|
customServer: true,
|
||||||
hostname: HOSTNAME,
|
hostname: HOSTNAME,
|
||||||
quiet: true,
|
quiet: true,
|
||||||
port: Number(PORT),
|
port: config.port,
|
||||||
conf: _require(path.join(dir, "next.config.cjs")),
|
conf: nextConfig,
|
||||||
dir,
|
dir,
|
||||||
dev,
|
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": [
|
"exclude": [
|
||||||
"src/next/next.config.cjs"
|
"src/next/next.config.cjs",
|
||||||
|
"src/next/**/*.tsx"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user