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