Compare commits

..

6 commits

95 changed files with 4515 additions and 2096 deletions

View file

@ -1,31 +0,0 @@
# name: Deploy Edge Proxy
# run-name: ${{ gitea.actor }} Deploy Edge Proxy
# on:
# push:
# branches: [ master ]
# env:
# GARDEN_DEPLOY_ACTION: minecluster-proxy
# jobs:
# deploy-edge:
# steps:
# # Setup Oasis
# - name: Oasis Setup
# uses: https://gitea.dunemask.dev/elysium/elysium-actions@oasis-setup-auto
# with:
# deploy-env: edge
# infisical-token: ${{ secrets.INFISICAL_ELYSIUM_EDGE_READ_TOKEN }}
# # Deploy to Edge Cluster
# - name: Deploy to Edge Cluster
# run: garden deploy $GARDEN_DEPLOY_ACTION --force --force-build --env usw-edge
# working-directory: ${{ env.OASIS_WORKSPACE }}
# # Alert via Discord
# - name: Discord Alert
# if: always()
# uses: https://gitea.dunemask.dev/elysium/elysium-actions@discord-status
# with:
# status: ${{ job.status }}
# channel: deployments
# header: DEPLOY EDGE
# additional-content: "Minecluster Proxy"

View file

@ -1,42 +0,0 @@
# name: QA API Tests
# run-name: ${{ gitea.actor }} QA API Test
# on:
# pull_request:
# branches: [ master ]
# env:
# REPO_DIR: ${{ gitea.workspace }}/minecluster
# GARDEN_LINK_ACTION: build.minecluster-image
# jobs:
# qa-api-tests:
# steps:
# # Setup Oasis
# - name: Oasis Setup
# uses: https://gitea.dunemask.dev/elysium/elysium-actions@oasis-setup-auto
# with:
# deploy-env: ci
# infisical-token: ${{ secrets.INFISICAL_ELYSIUM_CI_READ_TOKEN }}
# # Test Code
# - name: Checkout repository
# uses: actions/checkout@v3
# with:
# path: ${{ env.REPO_DIR }}
# # Garden link
# - name: Link Repo code to Garden
# run: garden link action $GARDEN_LINK_ACTION $REPO_DIR --env usw-ci --var cubit-projects=cairo,minecluster
# working-directory: ${{ env.OASIS_WORKSPACE }}
# # Cubit CI Tests
# - name: Run Cubit tests in CI env
# run: garden workflow qa-api-tests --env usw-ci --var ci-ttl=25m
# working-directory: ${{ env.OASIS_WORKSPACE }}
# # Discord Alert
# - name: Discord Alert
# if: always()
# uses: https://gitea.dunemask.dev/elysium/elysium-actions@discord-status
# with:
# status: ${{ job.status }}
# channel: ci
# header: QA API Tests
# additional-content: "CI Namespace: `${{env.CI_NAMESPACE}}`"

View file

@ -1,17 +0,0 @@
name: S3 Repo Backup
run-name: ${{ forgejo.actor }} S3 Repo Backup
on:
push:
branches: [ master ]
jobs:
s3-repo-backup:
steps:
- name: S3 Backup
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@s3-backup
with:
infisical-token: ${{ secrets.INFISICAL_ELYSIUM_EDGE_READ_TOKEN }}
infisical-project: ${{ vars.INFISICAL_DEPLOYMENTS_PROJECT_ID }}
- name: Status Alert
if: always()
run: echo "The Job ended with status ${{ job.status }}."

View file

@ -0,0 +1,31 @@
name: Deploy Edge Proxy
run-name: ${{ gitea.actor }} Deploy Edge Proxy
on:
push:
branches: [ master ]
env:
GARDEN_DEPLOY_ACTION: minecluster-proxy
jobs:
deploy-edge:
steps:
# Setup Oasis
- name: Oasis Setup
uses: https://gitea.dunemask.dev/elysium/elysium-actions@oasis-setup-auto
with:
deploy-env: edge
infisical-token: ${{ secrets.INFISICAL_ELYSIUM_EDGE_READ_TOKEN }}
# Deploy to Edge Cluster
- name: Deploy to Edge Cluster
run: garden deploy $GARDEN_DEPLOY_ACTION --force --force-build --env usw-edge
working-directory: ${{ env.OASIS_WORKSPACE }}
# Alert via Discord
- name: Discord Alert
if: always()
uses: https://gitea.dunemask.dev/elysium/elysium-actions@discord-status
with:
status: ${{ job.status }}
channel: deployments
header: DEPLOY EDGE
additional-content: "Minecluster Proxy"

View file

@ -1,8 +1,8 @@
name: Deploy USW-MC
run-name: ${{ forgejo.actor }} Deploy USW-MC
run-name: ${{ gitea.actor }} Deploy USW-MC
on:
push:
branches: [master]
push:
branches: [ master ]
env:
GARDEN_DEPLOY_ACTION: minecluster
@ -10,35 +10,34 @@ env:
jobs:
deploy-edge:
steps:
# Configure proper kubeconfig (Used when cluster does not match the edge environment)
# Configure proper kubeconfig
- name: Get usw-mc deployment kubeconfig
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@infisical-env
uses: https://gitea.dunemask.dev/elysium/elysium-actions@infisical-env
with:
infisical-token: ${{ secrets.INFISICAL_ELYSIUM_EDGE_READ_TOKEN }}
project-id: ${{ vars.INFISICAL_DEPLOYMENTS_PROJECT_ID }}
secret-envs: edge
secret-paths: /kubernetes/usw-mc
secret-paths: /kubernetes
# Setup Oasis
- name: Oasis Setup
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@oasis-setup-auto
uses: https://gitea.dunemask.dev/elysium/elysium-actions@oasis-setup-auto
with:
deploy-env: edge
infisical-token: ${{ secrets.INFISICAL_ELYSIUM_EDGE_READ_TOKEN }}
infisical-project: ${{ vars.INFISICAL_DEPLOYMENTS_PROJECT_ID }}
extra-secret-paths: /dashboard
extra-secret-paths: /alexandria
extra-secret-envs: edge
kubeconfig: ${{ env.KUBERNETES_CONFIG_USW_MC }}
# Deploy to Edge
- name: Deploy to Edge env
run: garden deploy $GARDEN_DEPLOY_ACTION --force --force-build --env usw-edge
run: garden deploy $GARDEN_DEPLOY_ACTION --force --force-build --env usw-mc
working-directory: ${{ env.OASIS_WORKSPACE }}
env: # (Used when cluster does not match the edge environment)
MCL_KUBECONFIG: ${{ env.KUBERNETES_CONFIG_USW_MC }}
env:
MCL_KUBECONFIG: ${{ env.KUBERNETES_CONFIG_USW_MC }}
# Alert via Discord
- name: Discord Alert
if: always()
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@discord-status
uses: https://gitea.dunemask.dev/elysium/elysium-actions@discord-status
with:
status: ${{ job.status }}
channel: deployments
header: DEPLOY MC
additional-content: "Minecluster Server Manager Deployment"
additional-content: "Minecluster Server Manager Deployment"

View file

@ -0,0 +1,42 @@
name: QA API Tests
run-name: ${{ gitea.actor }} QA API Test
on:
pull_request:
branches: [ master ]
env:
REPO_DIR: ${{ gitea.workspace }}/minecluster
GARDEN_LINK_ACTION: build.minecluster-image
jobs:
qa-api-tests:
steps:
# Setup Oasis
- name: Oasis Setup
uses: https://gitea.dunemask.dev/elysium/elysium-actions@oasis-setup-auto
with:
deploy-env: ci
infisical-token: ${{ secrets.INFISICAL_ELYSIUM_CI_READ_TOKEN }}
# Test Code
- name: Checkout repository
uses: actions/checkout@v3
with:
path: ${{ env.REPO_DIR }}
# Garden link
- name: Link Repo code to Garden
run: garden link action $GARDEN_LINK_ACTION $REPO_DIR --env usw-ci --var cubit-projects=cairo,minecluster
working-directory: ${{ env.OASIS_WORKSPACE }}
# Cubit CI Tests
- name: Run Cubit tests in CI env
run: garden workflow qa-api-tests --env usw-ci --var ci-ttl=25m
working-directory: ${{ env.OASIS_WORKSPACE }}
# Discord Alert
- name: Discord Alert
if: always()
uses: https://gitea.dunemask.dev/elysium/elysium-actions@discord-status
with:
status: ${{ job.status }}
channel: ci
header: QA API Tests
additional-content: "CI Namespace: `${{env.CI_NAMESPACE}}`"

View file

@ -0,0 +1,31 @@
name: S3 Repo Backup
run-name: ${{ gitea.actor }} S3 Repo Backup
on:
push:
branches: [ master ]
env:
S3_BACKUP_ENDPOINT: https://s3.dunemask.dev
S3_BACKUP_KEY_ID: gitea-repo-backup
S3_BACKUP_KEY: ${{ secrets.S3_REPO_BACKUP_KEY }}
REPO_DIR: ${{ gitea.workspace }}/${{ gitea.respository }}
jobs:
s3-repo-backup:
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
path: ${{ env.REPO_DIR }}
- name: S3 Backup
uses: peter-evans/s3-backup@v1
env:
ACCESS_KEY_ID: ${{ env.S3_BACKUP_KEY_ID }}
SECRET_ACCESS_KEY: ${{ env.S3_BACKUP_KEY }}
MIRROR_SOURCE: ${{ env.REPO_DIR }}
MIRROR_TARGET: backups/gitea-repositories/${{ gitea.repository }}
STORAGE_SERVICE_URL: ${{env.S3_BACKUP_ENDPOINT}}
with:
args: --overwrite --remove
- name: Status Alert
if: always()
run: echo "The Job ended with status ${{ job.status }}."

3
.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules/
.env
build
dist

View file

@ -2,3 +2,5 @@ node_modules/
.git/
lib/
src/
dist/
build/

View file

@ -4,13 +4,15 @@ WORKDIR /dunemask/net/minecluster
COPY package.json .
COPY package-lock.json .
RUN npm i
# Copy react build resources over
# Copy build resources over
COPY public public
COPY dist dist
COPY src src
COPY lib lib
# Copy TSConfigs over
COPY index.html .
COPY vite.config.js .
RUN npm run build:react
# Copy Backend resources over
COPY lib lib
COPY vite.config.ts .
COPY tsconfig.json .
COPY tsconfig.server.json .
RUN npm run package:full
CMD ["npm","start"]

11
dist/app.js vendored
View file

@ -1,11 +0,0 @@
import k8s from "@kubernetes/client-node";
import Minecluster from "../lib/Minecluster.js";
const mcl = new Minecluster();
mcl.start();
async function main(){
const kc = new k8s.KubeConfig();
kc.loadFromDefault();
}
main().catch((e)=>{console.error(e)});

View file

@ -18,6 +18,6 @@
<body>
<noscript>You need to enable JavaScript to run Minecluster</noscript>
<div id="mcl"></div>
<script type="module" src="/src/App.jsx"></script>
<script type="module" src="/src/App.tsx"></script>
</body>
</html>

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// Imports
import fig from "figlet";
import http from "http";

14
lib/app.ts Normal file
View file

@ -0,0 +1,14 @@
// @ts-nocheck
import k8s from "@kubernetes/client-node";
import Minecluster from "./Minecluster.js";
const mcl = new Minecluster();
mcl.start();
async function main() {
const kc = new k8s.KubeConfig();
kc.loadFromDefault();
}
main().catch((e) => {
console.error(e);
});

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import {
createServerFolder,
getServerItem,

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import createServerResources from "../k8s/server-create.js";
import deleteServerResources from "../k8s/server-delete.js";
import {

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { S3, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { basename } from "node:path";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { getUserDeployments } from "../k8s/k8s-server-control.js";
import { getInstances } from "../k8s/server-status.js";
import { sendError } from "../util/ExpressClientError.js";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// Imports
import k8s from "@kubernetes/client-node";
import { Rcon as RconClient } from "rcon-client";

View file

@ -1,7 +1,7 @@
CREATE SEQUENCE servers_id_seq;
CREATE TABLE servers (
id bigint NOT NULL DEFAULT nextval('servers_id_seq') PRIMARY KEY,
owner_cairo_id varchar(63),
owner_cairo_id bigint,
host varchar(255) DEFAULT NULL,
name varchar(255) DEFAULT NULL,
version varchar(63) DEFAULT 'latest',

View file

@ -1,3 +1,4 @@
// @ts-nocheck
const buildPostgresEntry = (entry) => {
const pgEntry = { ...entry };
Object.keys(pgEntry).forEach((col) => {

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// Imports
import path from "node:path";
import { URL } from "node:url";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import pg from "../postgres.js";
import {
deleteQuery,
@ -6,6 +7,7 @@ import {
updateWhereAllQuery,
} from "../pg-query.js";
import ExpressClientError from "../../util/ExpressClientError.js";
import { VERB } from "@mcl/logging";
const table = "servers";
const asExpressClientError = (e) => {
@ -16,9 +18,8 @@ const getMclName = (host, id) =>
`${host.toLowerCase().replaceAll(".", "-")}-${id}`;
export async function checkAuthorization(serverId, cairoId) {
console.log(
`Checking Authorization for user ${cairoId} for serverId ${serverId}`,
);
const msgLog = `Checking Authorization for user ${cairoId} for serverId ${serverId}`;
VERB("DEBUG", msgLog);
if (!cairoId) return false;
const q = selectWhereAllQuery(table, {
id: serverId,

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import k8s from "@kubernetes/client-node";
const MCL_KUBECONFIG = process.env.MCL_KUBECONFIG;
const envConfig = MCL_KUBECONFIG ? MCL_KUBECONFIG : null;
@ -9,6 +10,4 @@ try {
} catch (e) {
kc.loadFromDefault();
}
if(kc.contexts.length === 1) kc.setCurrentContext(kc.contexts[0].name);
if(!kc.currentContext) throw new Error("Could not infer current context! Please set it manually in the Kubeconfig!");
export default kc;

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import k8s from "@kubernetes/client-node";
import yaml from "js-yaml";
import { VERB, ERR } from "../util/logging.js";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import fs from "node:fs";
import path from "node:path";
import yaml from "js-yaml";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { v4 as uuidv4 } from "uuid";
import bcrypt from "bcrypt";
import k8s from "@kubernetes/client-node";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import k8s from "@kubernetes/client-node";
import { ERR } from "../util/logging.js";
import { getServerAssets } from "./k8s-server-control.js";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import ftp from "basic-ftp";
import { ERR } from "../util/logging.js";
import { getServerAssets } from "./k8s-server-control.js";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import k8s from "@kubernetes/client-node";
import {
createExtraService,

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import k8s from "@kubernetes/client-node";
import { getUserDeployments } from "./k8s-server-control.js";
import { getServerEntries } from "../database/queries/server-queries.js";

View file

@ -1,15 +1,13 @@
// @ts-nocheck
import { Router } from "express";
import cairoAuthMiddleware from "./middlewares/auth-middleware.js";
const router = Router();
const cairoProjectId = process.env.MCL_CAIRO_PROJECT;
if(!cairoProjectId) throw Error("Cairo Project Required!");
const ok = (_r, res) => res.sendStatus(200);
function cairoRedirect(req, res) {
res.redirect(
`${process.env.MCL_CAIRO_URL}/cairo/authenticate?redirectUri=${req.query.redirectUri}&projectId=${cairoProjectId}`,
`${process.env.MCL_CAIRO_URL}/cairo/auth?redirectUri=${req.query.redirectUri}`,
);
}

View file

@ -1,3 +1,4 @@
// @ts-nocheck
export function logErrors(err, req, res, next) {
console.error(err.stack);
next(err);

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { Router, json as jsonMiddleware } from "express";
import multer from "multer";
import {

View file

@ -1,15 +1,16 @@
// @ts-nocheck
// Imports
import { Router } from "express";
import bearerTokenMiddleware from "express-bearer-token";
import { ERR, VERB } from "../../util/logging.js";
// Constants
const { MCL_CAIRO_URL, MCL_CAIRO_PROJECT } = process.env;
const { MCL_CAIRO_URL } = process.env;
const cairoAuthMiddleware = Router();
const cairoAuthenticate = async (token) => {
const config = { headers: { Authorization: `Bearer ${token}` } };
return fetch(`${MCL_CAIRO_URL}/api/${MCL_CAIRO_PROJECT}/auth/credentials`, config).then(async (res) => {
return fetch(`${MCL_CAIRO_URL}/api/user/info`, config).then(async (res) => {
if (res.status >= 300) {
const errorMessage = await res
.json()
@ -30,9 +31,9 @@ const cairoAuthHandler = (req, res, next) => {
cairoAuthenticate(req.token)
.then((authData) => {
console.log(authData);
if (!authData?.user?.id)
throw Error(`Cairo didn't return the expected data! ${authData?.user?.id}`);
req.cairoId = authData?.user?.id;
if (!authData.id)
throw Error(`Cairo didn't return the expected data! ${authData.id}`);
req.cairoId = authData.id;
})
.then(() => next())
.catch((err) => {

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import express, { Router } from "express";
import path from "path";
const router = Router();

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { Router, json as jsonMiddleware } from "express";
import { getS3BackupUrl, listS3Backups } from "../controllers/s3-controller.js";
import cairoAuthMiddleware from "./middlewares/auth-middleware.js";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { Router, json as jsonMiddleware } from "express";
import {
createServer,

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { Router } from "express";
import k8s from "@kubernetes/client-node";
import { WARN } from "../util/logging.js";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { Router } from "express";
const router = Router();
// Get Routes

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// Imports
import express from "express";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { Server as Skio } from "socket.io";
import { VERB, WARN, ERR } from "../util/logging.js";
import {

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { VERB } from "./logging.js";
export default class ExpressClientError extends Error {

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// Imports
import { Chalk } from "chalk";
const { redBright, greenBright, yellowBright, cyanBright, magentaBright } =

5495
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,13 +4,18 @@
"description": "Minecraft Server management using Kubernetes",
"type": "module",
"scripts": {
"build:react": "vite build",
"start": "node dist/app.js",
"dev:server": "nodemon dist/app.js",
"dev:react": "vite",
"lint": "npx prettier -w src lib vite.config.js",
"start:dev": "concurrently -k \"MCL_DEV_PORT=52025 npm run dev:server\" \" MCL_VITE_DEV_PORT=52000 MCL_VITE_BACKEND_URL=http://localhost:52025 npm run dev:react\" -n s,v -p -c green,yellow",
"start:dev:garden": "concurrently -k \"npm run dev:server\" \"npm run dev:react\" -n s,v -p -c green,yellow"
"start:dev:garden": "concurrently -k \"npm run dev:server\" \"npm run dev:react\" -n s,v -p -c green,yellow",
"build:react": "vite build",
"build:server": "esbuild `find lib \\( -name '*.ts' \\)` --tsconfig=tsconfig.server.json --outdir=build/server && tsc-alias -p tsconfig.server.json",
"build:all": "rm -Rf build && concurrently --kill-others-on-fail \"npm run build:react\" \"npm run build:server\" -n s,v -c cyan,yellow",
"package:dist": "mkdir -p dist && mv build/server/* dist/ && mv build/vite dist/static && rm -Rf build && cp -R lib/database/migrations dist/database/migrations",
"package:full": "rm -Rf dist && npm run build:all && npm run package:dist",
"dev:server": "nodemon lib/app.ts",
"dev:react": "vite",
"format": "npx prettier -w src lib vite.config.ts tsconfig*.json",
"tsc": "concurrently --kill-others-on-fail \"tsc --noEmit\" \"tsc -p tsconfig.server.json --noEmit\" -n s,v -c cyan,yellow"
},
"keywords": [
"Minecraft",
@ -22,44 +27,65 @@
"author": "Dunemask",
"license": "LGPL-2.1",
"devDependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.9",
"@mui/material": "^5.15.9",
"@tanstack/react-query": "^5.20.1",
"@aperturerobotics/chonky": "^0.2.8",
"@aperturerobotics/chonky-icon-fontawesome": "^0.2.8",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.15",
"@mui/material": "^5.15.15",
"@tanstack/react-query": "^5.29.0",
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.12.5",
"@types/pg": "^8.11.4",
"@vitejs/plugin-react": "^4.2.1",
"chonky": "^2.3.2",
"chonky-icon-fontawesome": "^2.3.2",
"concurrently": "^8.2.2",
"nodemon": "^3.0.3",
"esbuild": "^0.20.2",
"nodemon": "^3.1.0",
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-quill": "^2.0.0",
"react-router-dom": "^6.22.0",
"react-toastify": "^10.0.4",
"socket.io-client": "^4.7.4",
"vite": "^5.1.1"
"react-router-dom": "^6.22.3",
"react-toastify": "^10.0.5",
"socket.io-client": "^4.7.5",
"tsc-alias": "^1.8.8",
"tsx": "^4.7.2",
"typescript": "^5.4.4",
"vite": "^5.2.8",
"vite-tsconfig-paths": "^4.3.2"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.529.1",
"@aws-sdk/s3-request-presigner": "^3.529.1",
"@aws-sdk/client-s3": "^3.550.0",
"@aws-sdk/s3-request-presigner": "^3.550.0",
"@kubernetes/client-node": "^0.20.0",
"basic-ftp": "^5.0.4",
"basic-ftp": "^5.0.5",
"bcrypt": "^5.1.1",
"chalk": "^5.3.0",
"express": "^4.18.2",
"express": "^4.19.2",
"express-bearer-token": "^2.4.0",
"figlet": "^1.7.0",
"js-yaml": "^4.1.0",
"moment": "^2.30.1",
"multer": "^1.4.5-lts.1",
"multer-s3": "^3.0.1",
"pg-promise": "^11.5.4",
"pg-promise": "^11.6.0",
"postgres-migrations": "^5.3.0",
"rcon-client": "^4.2.4",
"react-dropzone": "^14.2.3",
"socket.io": "^4.7.4",
"socket.io": "^4.7.5",
"uuid": "^9.0.1"
},
"nodemonConfig": {
"watch": [
"lib"
],
"ext": "ts",
"execMap": {
"ts": "tsx --tsconfig tsconfig.server.json"
}
}
}

View file

@ -1,5 +1,6 @@
// @ts-nocheck
import { createRoot } from "react-dom/client";
import MCL from "./MCL.jsx";
import MCL from "./MCL.tsx";
const appRoot = document.getElementById("mcl");
const root = createRoot(appRoot);

View file

@ -1,9 +1,10 @@
// @ts-nocheck
// Imports
import { ThemeProvider } from "@mui/material/styles";
import mclTheme from "./util/theme.js";
import mclTheme from "./util/theme.ts";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { SettingsProvider } from "@mcl/settings";
import Viewport from "./nav/Viewport.jsx";
import Viewport from "./nav/Viewport.tsx";
import { BrowserRouter } from "react-router-dom";
// Create a query client for the app
const queryClient = new QueryClient();

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { useState, useEffect } from "react";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
@ -7,8 +8,8 @@ import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";
import Dialog from "@mui/material/Dialog";
import Toolbar from "@mui/material/Toolbar";
import TextEditor from "./TextEditor.jsx";
import { cairoAuthHeader } from "@mcl/util/auth.js";
import TextEditor from "./TextEditor.tsx";
import { uploadServerItem } from "@mcl/api/clients/files.ts";
const textFileTypes = [
"properties",
@ -60,11 +61,7 @@ export default function FilePreview(props) {
formData.append("file", blob, name);
formData.append("id", serverId);
formData.append("path", filePath);
await fetch("/api/files/upload", {
method: "POST",
body: formData,
headers: cairoAuthHeader(),
});
await uploadServerItem(formData);
dialogToggle();
}

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { useState, useEffect, useMemo, useRef } from "react";
import Box from "@mui/material/Box";
import Dropzone from "react-dropzone";
@ -10,8 +11,8 @@ import {
FileToolbar,
setChonkyDefaults,
ChonkyActions,
} from "chonky";
import { ChonkyIconFA } from "chonky-icon-fontawesome";
} from "@aperturerobotics/chonky";
import { ChonkyIconFA } from "@aperturerobotics/chonky-icon-fontawesome";
import {
getServerFiles,
@ -20,10 +21,10 @@ import {
getServerItem,
moveServerItems,
previewServerItem,
} from "@mcl/queries";
import { cairoAuthHeader } from "@mcl/util/auth.js";
uploadServerItem,
} from "@mcl/api/clients/files";
import { supportedFileTypes } from "./FilePreview.jsx";
import { supportedFileTypes } from "./FilePreview.tsx";
export default function MineclusterFiles(props) {
// Chonky configuration
@ -117,11 +118,7 @@ export default function MineclusterFiles(props) {
formData.append("id", serverId);
const path = `${[...dirStack].join("/")}${filePath}`;
formData.append("path", path);
await fetch("/api/files/upload", {
method: "POST",
body: formData,
headers: cairoAuthHeader(),
});
await uploadServerItem(formData);
}
async function downloadFiles(files) {

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import ReactQuill from "react-quill";
import { useState, useEffect, useMemo, memo } from "react";
import "react-quill/dist/quill.snow.css";

View file

@ -1,7 +1,8 @@
// @ts-nocheck
import { useState, useEffect } from "react";
import { useSystemAvailable } from "@mcl/queries";
import { useSystemAvailable } from "@mcl/api/clients/misc";
import Box from "@mui/material/Box";
import OverviewVisual from "./OverviewVisual.jsx";
import OverviewVisual from "./OverviewVisual.tsx";
export default function Overview(props) {
const [memory, setMemory] = useState(100);
const [cpu, setCpu] = useState(100);
@ -10,6 +11,7 @@ export default function Overview(props) {
useEffect(() => {
if (systemLoading || !props.clusterMetrics) return;
if (!systemAvailable) return;
setCpu((props.clusterMetrics.cpu / systemAvailable.cpu) * 100);
setMemory((props.clusterMetrics.memory / systemAvailable.memory) * 100);
}, [systemAvailable, props.clusterMetrics]);

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import * as React from "react";
import CircularProgress from "@mui/material/CircularProgress";
import Typography from "@mui/material/Typography";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import TextField from "@mui/material/TextField";
export default function BackupBucketOption(props) {
const { value, onChange } = props;

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import TextField from "@mui/material/TextField";
export default function BackupHostOption(props) {
const { value, onChange } = props;

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import TextField from "@mui/material/TextField";
export default function BackupIdOption(props) {
const { value, onChange } = props;

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { useState } from "react";
import Box from "@mui/material/Box";
import MenuItem from "@mui/material/MenuItem";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import TextField from "@mui/material/TextField";
export default function BackupKeyOption(props) {
const { value, onChange } = props;

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { useState } from "react";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import TextField from "@mui/material/TextField";
export default function HostOption(props) {
const { value, onChange, disabled } = props;

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
const maxMemSupported = 10;

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import TextField from "@mui/material/TextField";
export default function NameOption(props) {
const { value, onChange } = props;

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";

View file

@ -1,7 +1,8 @@
// @ts-nocheck
import { useState, useEffect } from "react";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import { useVersionList } from "@mcl/queries";
import { useVersionList } from "@mcl/api/clients/misc";
export default function VersionOption(props) {
const { value, onChange } = props;

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { useEffect, useState } from "react";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
@ -11,7 +12,7 @@ import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import Stack from "@mui/material/Stack";
import DownloadIcon from "@mui/icons-material/Download";
import { getBackupUrl, getServerBackups } from "../../util/queries";
import { getBackupUrl, getServerBackups } from "@mcl/api/clients/backups";
export function useBackupDialog(isOpen = false) {
const [open, setOpen] = useState(isOpen);
@ -35,7 +36,7 @@ export default function BackupDialog(props) {
function normalizeLastModified(lastModified) {
const d = new Date(Date.parse(lastModified));
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}`;
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}`;
}
const downloadBackup = (backup) =>

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { useState } from "react";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
@ -7,7 +8,7 @@ import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";
import Dialog from "@mui/material/Dialog";
import Toolbar from "@mui/material/Toolbar";
import RconView from "./RconView.jsx";
import RconView from "./RconView.tsx";
export function useRconDialog(isOpen = false) {
const [open, setOpen] = useState(isOpen);

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { io } from "socket.io-client";
export default class RconSocket {
constructor(logUpdate, serverId) {

View file

@ -1,10 +1,11 @@
// @ts-nocheck
import { useState, useEffect, useRef } from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Skeleton from "@mui/material/Skeleton";
import Typography from "@mui/material/Typography";
import RconSocket from "./RconSocket.js";
import RconSocket from "./RconSocket.ts";
import "@mcl/css/rcon.css";
function RconLogSkeleton() {

View file

@ -1,5 +1,5 @@
// @ts-nocheck
import React from "react";
import { useStartServer, useStopServer, useDeleteServer } from "@mcl/queries";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardActions from "@mui/material/CardActions";
@ -16,13 +16,18 @@ import EditIcon from "@mui/icons-material/Edit";
import FolderIcon from "@mui/icons-material/Folder";
import BackupIcon from "@mui/icons-material/Backup";
import { Link } from "react-router-dom";
import {
useServerDelete,
useServerStart,
useServerStop,
} from "@mcl/api/clients/server";
export default function ServerCard(props) {
const { server, openRcon, openBackups } = props;
const { name, id, metrics, ftpAvailable, serverAvailable, services } = server;
const startServer = useStartServer(id);
const stopServer = useStopServer(id);
const deleteServer = useDeleteServer(id);
const startServer = useServerStart(id);
const stopServer = useServerStop(id);
const deleteServer = useServerDelete(id);
function toggleRcon() {
if (!services.includes("server")) return;
openRcon();

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import React, { useReducer, createContext, useMemo } from "react";
const SettingsContext = createContext();

View file

@ -1,9 +1,10 @@
// @ts-nocheck
// React imports
import { useState } from "react";
import { Link, useLocation } from "react-router-dom";
// Internal Imports
import pages from "./MCLPages.jsx";
import pages from "./MCLPages.tsx";
// Materialui
import AppBar from "@mui/material/AppBar";

View file

@ -1,7 +1,8 @@
import Home from "@mcl/pages/Home.jsx";
import Create from "@mcl/pages/Create.jsx";
import Files from "@mcl/pages/Files.jsx";
import Edit from "@mcl/pages/Edit.jsx";
// @ts-nocheck
import Home from "@mcl/pages/Home.tsx";
import Create from "@mcl/pages/Create.tsx";
import Files from "@mcl/pages/Files.tsx";
import Edit from "@mcl/pages/Edit.tsx";
// Go To https://mui.com/material-ui/material-icons/ for more!
import HomeIcon from "@mui/icons-material/Home";
import AddIcon from "@mui/icons-material/Add";

View file

@ -1,6 +1,7 @@
// @ts-nocheck
// Import React
import { Routes, Route, Navigate } from "react-router-dom";
import pages from "./MCLPages.jsx";
import pages from "./MCLPages.tsx";
const defaultPage = pages[0].path;

View file

@ -1,10 +1,11 @@
// @ts-nocheck
import Toolbar from "@mui/material/Toolbar";
import MCLPortal from "./MCLPortal.jsx";
import MCLPortal from "./MCLPortal.tsx";
// Import Navbar
/*import Navbar from "./Navbar.jsx";*/
import { useCairoAuth } from "@mcl/util/auth.js";
import MCLMenu from "./MCLMenu.jsx";
import Auth from "@mcl/pages/Auth.jsx";
/*import Navbar from "./Navbar.tsx";*/
import { useCairoAuth } from "@mcl/api/auth";
import MCLMenu from "./MCLMenu.tsx";
import Auth from "@mcl/pages/Auth.tsx";
export default function Views() {
const auth = useCairoAuth();

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { useState, useEffect } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import Box from "@mui/material/Box";

View file

@ -1,5 +1,6 @@
// @ts-nocheck
import Box from "@mui/material/Box";
import CreateCoreOptions from "./CreateCoreOptions.jsx";
import CreateCoreOptions from "./CreateCoreOptions.tsx";
export default function Create() {
return (
<Box className="create">

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import Box from "@mui/material/Box";
@ -6,31 +7,31 @@ import FormControl from "@mui/material/FormControl";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Typography from "@mui/material/Typography";
import { useCreateServer } from "@mcl/queries";
import { useServerCreate } from "@mcl/api/clients/server";
// Core Options
import NameOption from "@mcl/components/server-options/NameOption.jsx";
import HostOption from "@mcl/components/server-options/HostOption.jsx";
import VersionOption from "@mcl/components/server-options/VersionOption.jsx";
import NameOption from "@mcl/components/server-options/NameOption.tsx";
import HostOption from "@mcl/components/server-options/HostOption.tsx";
import VersionOption from "@mcl/components/server-options/VersionOption.tsx";
import ServerTypeOption, {
serverTypeOptions,
} from "@mcl/components/server-options/ServerTypeOption.jsx";
} from "@mcl/components/server-options/ServerTypeOption.tsx";
import CpuOption, {
cpuOptions,
} from "@mcl/components/server-options/CpuOption.jsx";
} from "@mcl/components/server-options/CpuOption.tsx";
import MemoryOption, {
memoryOptions,
} from "@mcl/components/server-options/MemoryOption.jsx";
import ExtraPortsOption from "@mcl/components/server-options/ExtraPortsOption.jsx";
import StorageOption from "@mcl/components/server-options/StorageOption.jsx";
} from "@mcl/components/server-options/MemoryOption.tsx";
import ExtraPortsOption from "@mcl/components/server-options/ExtraPortsOption.tsx";
import StorageOption from "@mcl/components/server-options/StorageOption.tsx";
import BackupHostOption from "@mcl/components/server-options/BackupHostOption.jsx";
import BackupBucketOption from "@mcl/components/server-options/BackupBucketOption.jsx";
import BackupIdOption from "@mcl/components/server-options/BackupIdOption.jsx";
import BackupKeyOption from "@mcl/components/server-options/BackupKeyOption.jsx";
import BackupHostOption from "@mcl/components/server-options/BackupHostOption.tsx";
import BackupBucketOption from "@mcl/components/server-options/BackupBucketOption.tsx";
import BackupIdOption from "@mcl/components/server-options/BackupIdOption.tsx";
import BackupKeyOption from "@mcl/components/server-options/BackupKeyOption.tsx";
import BackupIntervalOption, {
backupIntervalDefault,
} from "@mcl/components/server-options/BackupIntervalOption.jsx";
} from "@mcl/components/server-options/BackupIntervalOption.tsx";
const defaultServer = {
version: "latest",
@ -45,7 +46,7 @@ export default function CreateCoreOptions() {
const [backupEnabled, setBackupEnabled] = useState(false);
const [spec, setSpec] = useState(defaultServer);
const nav = useNavigate();
const createServer = useCreateServer(spec);
const createServer = useServerCreate(spec);
const updateSpec = (attr, val) => {
const s = { ...spec };
@ -57,9 +58,8 @@ export default function CreateCoreOptions() {
async function upsertSpec() {
if (validateSpec() !== "validated") return;
createServer()
.then(() => nav("/"))
.catch(alert);
createServer();
nav("/");
}
function validateSpec() {

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import Autocomplete from "@mui/material/Autocomplete";
@ -5,11 +6,10 @@ import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Chip from "@mui/material/Chip";
import Select from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import InputLabel from "@mui/material/InputLabel";
import FormControl from "@mui/material/FormControl";
import { useCreateServer, useVersionList } from "@mcl/queries";
import { useServerCreate } from "@mcl/api/clients/server";
import { useVersionList } from "@mcl/api/clients/misc";
const defaultServer = {
version: "latest",
@ -28,7 +28,7 @@ export default function Create() {
const nav = useNavigate();
const versionList = useVersionList();
const [versions, setVersions] = useState(["latest"]);
const createServer = useCreateServer(spec);
const createServer = useServerCreate(spec);
const updateSpec = (attr, val) => {
const s = { ...spec };
s[attr] = val;
@ -90,9 +90,8 @@ export default function Create() {
async function upsertSpec() {
if (validateSpec() !== "validated") return;
createServer(spec)
.then(() => nav("/"))
.catch(alert);
createServer(spec);
nav("/");
}
function validateSpec() {

View file

@ -1,6 +1,7 @@
// @ts-nocheck
import { useSearchParams, useNavigate } from "react-router-dom";
import Box from "@mui/material/Box";
import EditCoreOptions from "./EditCoreOptions.jsx";
import EditCoreOptions from "./EditCoreOptions.tsx";
export default function Edit() {
const [searchParams] = useSearchParams();
const currentServer = searchParams.get("server");

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import Box from "@mui/material/Box";
@ -6,37 +7,37 @@ import FormControl from "@mui/material/FormControl";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Typography from "@mui/material/Typography";
import { useGetServer, useModifyServer } from "@mcl/queries";
import { useBlueprint, useServerModify } from "@mcl/api/clients/server";
// Core Options
import NameOption from "@mcl/components/server-options/NameOption.jsx";
import HostOption from "@mcl/components/server-options/HostOption.jsx";
import VersionOption from "@mcl/components/server-options/VersionOption.jsx";
import NameOption from "@mcl/components/server-options/NameOption.tsx";
import HostOption from "@mcl/components/server-options/HostOption.tsx";
import VersionOption from "@mcl/components/server-options/VersionOption.tsx";
import ServerTypeOption, {
serverTypeOptions,
} from "@mcl/components/server-options/ServerTypeOption.jsx";
} from "@mcl/components/server-options/ServerTypeOption.tsx";
import CpuOption, {
cpuOptions,
} from "@mcl/components/server-options/CpuOption.jsx";
} from "@mcl/components/server-options/CpuOption.tsx";
import MemoryOption, {
memoryOptions,
} from "@mcl/components/server-options/MemoryOption.jsx";
import ExtraPortsOption from "@mcl/components/server-options/ExtraPortsOption.jsx";
} from "@mcl/components/server-options/MemoryOption.tsx";
import ExtraPortsOption from "@mcl/components/server-options/ExtraPortsOption.tsx";
import BackupHostOption from "@mcl/components/server-options/BackupHostOption.jsx";
import BackupBucketOption from "@mcl/components/server-options/BackupBucketOption.jsx";
import BackupIdOption from "@mcl/components/server-options/BackupIdOption.jsx";
import BackupKeyOption from "@mcl/components/server-options/BackupKeyOption.jsx";
import BackupHostOption from "@mcl/components/server-options/BackupHostOption.tsx";
import BackupBucketOption from "@mcl/components/server-options/BackupBucketOption.tsx";
import BackupIdOption from "@mcl/components/server-options/BackupIdOption.tsx";
import BackupKeyOption from "@mcl/components/server-options/BackupKeyOption.tsx";
import BackupIntervalOption, {
backupIntervalDefault,
} from "@mcl/components/server-options/BackupIntervalOption.jsx";
} from "@mcl/components/server-options/BackupIntervalOption.tsx";
export default function EditCoreOptions(props) {
const { serverId } = props;
const [spec, setSpec] = useState();
const modifyServer = useModifyServer(spec);
const modifyServer = useServerModify(spec);
const nav = useNavigate();
const { isLoading, data: serverBlueprint } = useGetServer(serverId);
const { isLoading, data: serverBlueprint } = useBlueprint(serverId);
useEffect(() => setSpec(serverBlueprint), [serverBlueprint]);

View file

@ -1,10 +1,11 @@
// @ts-nocheck
import { useState, useEffect } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import Box from "@mui/material/Box";
import FilePreview, {
useFilePreview,
} from "@mcl/components/files/FilePreview.jsx";
import MineclusterFiles from "@mcl/components/files/MineclusterFiles.jsx";
} from "@mcl/components/files/FilePreview.tsx";
import MineclusterFiles from "@mcl/components/files/MineclusterFiles.tsx";
export default function Files() {
const [open, dialogToggle] = useFilePreview();

View file

@ -1,17 +1,18 @@
// @ts-nocheck
import { Link } from "react-router-dom";
import { useState, useEffect } from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import ServerCard from "@mcl/components/servers/ServerCard.jsx";
import ServerCard from "@mcl/components/servers/ServerCard.tsx";
import RconDialog, {
useRconDialog,
} from "@mcl/components/servers/RconDialog.jsx";
import Overview from "@mcl/components/overview/Overview.jsx";
} from "@mcl/components/servers/RconDialog.tsx";
import Overview from "@mcl/components/overview/Overview.tsx";
import Button from "@mui/material/Button";
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
import "@mcl/css/server-card.css";
import "@mcl/css/overview.css";
import { useServerInstances } from "@mcl/queries";
import { useServerInstances } from "@mcl/api/clients/server";
import BackupDialog, {
useBackupDialog,
} from "../components/servers/BackupsDialog";

View file

@ -1,8 +1,9 @@
// @ts-nocheck
import { useState, useEffect } from "react";
import { useSearchParams } from "react-router-dom";
const tokenStorageName = "cairoUserToken";
const tokenQuery = "cairoUserToken";
const tokenStorageName = "cairoAuthToken";
const tokenQuery = "cairoAuthToken";
const verifyAuth = (authToken) =>
fetch("/api/auth/verify", {

View file

@ -0,0 +1,15 @@
import { mclAuthenticatedApiRequest } from "@mcl/api/requests";
export const getServerBackups = (serverId: string) =>
mclAuthenticatedApiRequest({
subPath: "/s3/backups",
json: { id: serverId },
jsonify: true,
});
export const getBackupUrl = (serverId: string, backupPath: string) =>
mclAuthenticatedApiRequest({
subPath: "/s3/backup-url",
json: { id: serverId, backupPath },
jsonify: true,
});

View file

@ -0,0 +1,68 @@
import { mclAuthenticatedApiRequest } from "@mcl/api/requests";
// ===== API Requests =====
export const getServerFiles = (serverId: string, path: string) =>
mclAuthenticatedApiRequest({
subPath: "/files/list",
json: { id: serverId, path },
});
export const createServerFolder = (serverId: string, path: string) =>
mclAuthenticatedApiRequest({
subPath: "/files/folder",
json: { id: serverId, path },
});
export const uploadServerItem = (body: FormData) =>
mclAuthenticatedApiRequest({
subPath: "/files/upload",
body,
});
export const deleteServerItem = (
serverId: string,
path: string,
isDir: boolean,
) =>
mclAuthenticatedApiRequest({
subPath: "/files/item",
json: { id: serverId, path, isDir },
method: "DELETE",
});
export const moveServerItems = (
serverId: string,
files: string,
destination: string,
origin: string,
) =>
mclAuthenticatedApiRequest({
subPath: "/files/move",
json: { id: serverId, files, destination, origin },
});
export const previewServerItem = (serverId: string, path: string) =>
mclAuthenticatedApiRequest({
subPath: "/files/item",
json: { id: serverId, path },
}).then((res) => res.blob());
export const getServerItem = (serverId: string, name: string, path: string) =>
mclAuthenticatedApiRequest({
subPath: "/files/item",
json: { id: serverId, path },
})
.then((res) => res.blob())
.then((blob) => downloadBlob(blob, name));
async function downloadBlob(blob: Blob, name: string) {
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = name;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}

View file

@ -0,0 +1,27 @@
import { useQuery } from "@tanstack/react-query";
import { mclAuthenticatedApiRequest } from "../requests";
// ===== API Requests =====
export const getSystemAvailable = () =>
mclAuthenticatedApiRequest({
subPath: "/system/available",
jsonify: true,
});
// ===== API Queries =====
export const useSystemAvailable = () =>
useQuery({
queryKey: ["system-available"],
queryFn: getSystemAvailable,
});
export const useVersionList = () =>
useQuery({
queryKey: ["minecraft-versions"],
queryFn: () =>
fetch(
"https://piston-meta.mojang.com/mc/game/version_manifest.json",
).then((r) => r.json()),
});

View file

@ -0,0 +1,123 @@
import { useQuery } from "@tanstack/react-query";
import { mclAuthenticatedApiRequest, useMutator } from "../requests";
// ===== API Utils =====
const useInstanceControl = (apiRequest: (args: any) => Promise<any>) =>
useMutator(apiRequest, ["server-instances"]);
const useListControl = (apiRequest: (args: any) => Promise<any>) =>
useMutator(apiRequest, ["server-list"]);
// ===== API Requests =====
export const getServerList = () =>
mclAuthenticatedApiRequest({
subPath: "/server/path",
jsonify: true,
});
export const getServerInstances = () =>
mclAuthenticatedApiRequest({
subPath: "/server/instances",
jsonify: true,
});
export const getServerMetrics = (serverId: string) =>
mclAuthenticatedApiRequest({
subPath: "/server/metrics",
json: { id: serverId },
jsonify: true,
});
export const getServerStatus = (serverId: string) =>
mclAuthenticatedApiRequest({
subPath: "/server/status",
json: { id: serverId },
jsonify: true,
});
export const requestServerStart = (serverId: string) =>
mclAuthenticatedApiRequest({
subPath: "/server/start",
json: { id: serverId },
});
export const requestServerStop = (serverId: string) =>
mclAuthenticatedApiRequest({
subPath: "/server/stop",
json: { id: serverId },
});
export const requestServerDelete = (serverId: string) =>
mclAuthenticatedApiRequest({
subPath: "/server/delete",
json: { id: serverId },
method: "DELETE",
});
export const requestServerCreate = (serverSpec: any) => {
console.log("requestServerCreate being called");
return mclAuthenticatedApiRequest({
subPath: "/server/create",
json: serverSpec,
});
};
export const requestServerModify = (serverSpec: any) =>
mclAuthenticatedApiRequest({
subPath: "/server/modify",
json: serverSpec,
});
export const getServerBlueprint = (serverId: string) =>
mclAuthenticatedApiRequest({
subPath: "/server/blueprint",
json: { id: serverId },
jsonify: true,
});
// ===== API Queries =====
export const useServerList = () =>
useQuery({ queryKey: ["server-list"], queryFn: getServerList });
export const useServerInstances = () =>
useQuery({
queryKey: ["server-instances"],
queryFn: getServerInstances,
refetchInterval: 5000,
});
export const useServerMetrics = (serverId: string) =>
useQuery({
queryKey: [`server-metrics-${serverId}`],
queryFn: () => getServerMetrics(serverId),
refetchInterval: 10000,
});
export const useServerStatus = (serverId: string) =>
useQuery({
queryKey: [`server-status-${serverId}`],
queryFn: () => getServerStatus(serverId),
});
export const useServerStart = (serverId: string) =>
useInstanceControl(() => requestServerStart(serverId));
export const useServerStop = (serverId: string) =>
useInstanceControl(() => requestServerStop(serverId));
export const useServerDelete = (serverId: string) =>
useInstanceControl(() => requestServerDelete(serverId));
export const useServerCreate = (spec: any) =>
useListControl(() => requestServerCreate(spec));
export const useServerModify = (spec: any) =>
useListControl(() => requestServerModify(spec));
export const useBlueprint = (serverId: string) =>
useQuery({
queryKey: [`server-blueprint-${serverId}`],
queryFn: () => getServerBlueprint(serverId),
});

55
src/util/api/requests.ts Normal file
View file

@ -0,0 +1,55 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { cairoAuthHeader } from "./auth";
declare interface ApiRequestArgs {
subPath: string;
body?: any | undefined;
json?: object | undefined;
method?: string;
jsonify?: boolean;
extraHeaders?: HeadersInit;
handleErrors?: boolean;
}
async function apiRequest(apiRequestArgs: ApiRequestArgs) {
const { subPath, json, body, method, jsonify, extraHeaders, handleErrors } =
apiRequestArgs;
const headers: HeadersInit = extraHeaders ?? {};
const requestMethod = method ?? !!json ? "POST" : "GET";
const requestBody = body ?? !!json ? JSON.stringify(json) : undefined;
if (!!json) headers["Content-Type"] = "application/json";
return fetch(`/api${subPath}`, {
method: requestMethod,
body: requestBody,
headers,
}).then((res) => {
if (res.status >= 300 && handleErrors) throw Error(res.statusText);
if (jsonify) return res.json();
return res;
});
}
export async function mclAuthenticatedApiRequest(
apiRequestArgs: ApiRequestArgs,
) {
const extraHeaders = apiRequestArgs.extraHeaders ?? {};
const authHeaders = cairoAuthHeader();
apiRequestArgs.extraHeaders = { ...extraHeaders, ...authHeaders };
return apiRequest(apiRequestArgs);
}
export async function mclApiRequest(apiRequestArgs: ApiRequestArgs) {
return apiRequest(apiRequestArgs);
}
export const useMutator = (
apiRequest: (args?: any) => Promise<any>,
invalidate: string[],
) => {
const qc = useQueryClient();
const mutate = async (...args: any) =>
apiRequest(...args).then(() =>
qc.invalidateQueries({ queryKey: invalidate }),
);
return mutate;
};

View file

@ -1,144 +0,0 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { cairoAuthHeader } from "@mcl/util/auth.js";
const fetchApi = (subPath) => async () =>
fetch(`/api${subPath}`, { headers: cairoAuthHeader() }).then((res) =>
res.json(),
);
const fetchApiCore = async (subPath, json, method = "POST", jsonify = false) =>
fetch(`/api${subPath}`, {
method,
headers: {
"Content-Type": "application/json",
...cairoAuthHeader(),
},
body: JSON.stringify(json),
}).then((res) => (jsonify ? res.json() : res));
const fetchApiPost = (subPath, json) => async () =>
fetch(`/api${subPath}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...cairoAuthHeader(),
},
body: JSON.stringify(json),
}).then((res) => res.json());
export const useServerStatus = (serverId) =>
useQuery({
queryKey: [`server-status-${serverId}`],
queryFn: fetchApiPost("/server/status", { id: serverId }),
});
export const useServerMetrics = (serverId) =>
useQuery({
queryKey: [`server-metrics-${serverId}`],
queryFn: fetchApiPost("/server/metrics", { id: serverId }),
refetchInterval: 10000,
});
export const useStartServer = (serverId) =>
postJsonApi("/server/start", { id: serverId }, "server-instances");
export const useStopServer = (serverId) =>
postJsonApi("/server/stop", { id: serverId }, "server-instances");
export const useDeleteServer = (serverId) =>
postJsonApi("/server/delete", { id: serverId }, "server-instances", "DELETE");
export const useCreateServer = (spec) =>
postJsonApi("/server/create", spec, "server-list");
export const useModifyServer = (spec) =>
postJsonApi("/server/modify", spec, "server-list");
export const useGetServer = (serverId) =>
useQuery({
queryKey: [`server-blueprint-${serverId}`],
queryFn: fetchApiPost("/server/blueprint", { id: serverId }),
});
export const getServerBackups = (serverId) =>
fetchApiCore("/s3/backups", { id: serverId }, "POST", true);
export const getBackupUrl = (serverId, backupPath) =>
fetchApiCore("/s3/backup-url", { id: serverId, backupPath }, "POST", true);
export const getServerFiles = async (serverId, path) =>
fetchApiCore("/files/list", { id: serverId, path }, "POST", true);
export const createServerFolder = async (serverId, path) =>
fetchApiCore("/files/folder", {
id: serverId,
path,
});
export const deleteServerItem = async (serverId, path, isDir) =>
fetchApiCore("/files/item", { id: serverId, path, isDir }, "DELETE");
export const moveServerItems = async (serverId, files, destination, origin) =>
fetchApiCore(
"/files/move",
{ id: serverId, files, destination, origin },
"POST",
);
export async function previewServerItem(serverId, path) {
const resp = await fetchApiCore("/files/item", { id: serverId, path });
if (resp.status !== 200) return console.log("AHHHH");
const blob = await resp.blob();
return blob;
}
export const getServerItem = async (serverId, name, path) =>
fetchApiCore("/files/item", { id: serverId, path })
.then((resp) =>
resp.status === 200
? resp.blob()
: Promise.reject("something went wrong"),
)
.then((blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = name;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
});
export const useInvalidator = () => {
const qc = useQueryClient();
return (q) => qc.invalidateQueries([q]);
};
export const useServerList = () =>
useQuery({ queryKey: ["server-list"], queryFn: fetchApi("/server/list") });
export const useServerInstances = () =>
useQuery({
queryKey: ["server-instances"],
queryFn: fetchApi("/server/instances"),
refetchInterval: 5000,
});
export const useSystemAvailable = () =>
useQuery({
queryKey: ["system-available"],
queryFn: fetchApi("/system/available"),
});
export const useVersionList = () =>
useQuery({
queryKey: ["minecraft-versions"],
queryFn: () =>
fetch(
"https://piston-meta.mojang.com/mc/game/version_manifest.json",
).then((r) => r.json()),
});
const postJsonApi = (subPath, body, invalidate, method = "POST") => {
const qc = useQueryClient();
return async () => {
const res = await fetch(`/api${subPath}`, {
method,
headers: {
"Content-Type": "application/json",
...cairoAuthHeader(),
},
body: JSON.stringify(body),
});
if (invalidate) qc.invalidateQueries([invalidate]);
};
};

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// Generated using https://zenoo.github.io/mui-theme-creator/
import { createTheme } from "@mui/material/styles";
import { unstable_ClassNameGenerator as ClassNameGenerator } from "@mui/material/className";

39
tsconfig.json Normal file
View file

@ -0,0 +1,39 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"target": "ESNext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
/* Vite Compile Options */
"allowImportingTsExtensions": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"paths": {
/* Common */
"@mcl/enums/*": ["./lib/types/enums/*"],
"@mcl/types/*": ["./lib/types/*"],
"@mcl/logging": ["./lib/util/logging.ts"],
/* Vite */
"@mcl/settings": ["./src/ctx/SettingsContext.tsx"],
"@mcl/api/*": ["./src/util/api/*"],
"@mcl/src/util/*": ["./src/util/*"],
"@mcl/pages/*": ["./src/pages/*"],
"@mcl/components/*": ["./src/components/*"],
"@mcl/css/*": ["./src/css/*"],
/* Server */
"@mcl/db/*": ["./lib/database/*"],
"@mcl/controllers/*": ["./lib/controllers/*"],
"@mcl/svc/*": ["./lib/services/*"],
"@mcl/ClientErrors": ["./lib/router/ClientErrors.ts"]
}
},
"exclude": ["node_modules"],
"tsc-alias": {
"resolveFullPaths": true,
"verbose": false
}
}

19
tsconfig.server.json Normal file
View file

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "bundler",
"allowJs": true,
"esModuleInterop": true,
"allowImportingTsExtensions": false,
"isolatedModules": true,
"noEmit": false,
"resolveJsonModule": true,
"outDir": "./build/server",
"strict": true,
"strictPropertyInitialization": false
},
"include": ["lib", "lib/**/*.ts"],
"exclude": ["src", "node_modules", "old-updater"]
}

View file

@ -1,6 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "node:path";
import tsconfigPaths from "vite-tsconfig-paths";
const { MCL_VITE_BACKEND_URL, MCL_VITE_DEV_PORT } = process.env;
const backendUrl = MCL_VITE_BACKEND_URL ?? "http://localhost:52000";
@ -8,10 +8,10 @@ const vitePort = MCL_VITE_DEV_PORT ?? 52025;
export default () => {
return defineConfig({
plugins: [react()],
plugins: [react(), tsconfigPaths()],
server: {
host: "0.0.0.0",
port: vitePort,
port: Number(vitePort),
proxy: {
"/api": backendUrl,
"/socket.io": backendUrl,
@ -22,18 +22,8 @@ export default () => {
},
},
build: {
outDir: "./build",
outDir: "./build/vite",
},
base: "/mcl/",
resolve: {
alias: {
"@mcl/css": path.resolve("./src/css"),
"@mcl/settings": path.resolve("./src/ctx/SettingsContext.jsx"),
"@mcl/pages": path.resolve("./src/pages"),
"@mcl/queries": path.resolve("./src/util/queries.js"),
"@mcl/components": path.resolve("./src/components"),
"@mcl": path.resolve("./src"),
},
},
});
};