[FEATURE] Integrated Minecluster with Cairo & Established Gitea workflows (#12)

Co-authored-by: Dunemask <dunemask@gmail.com>
Reviewed-on: https://gitea.dunemask.dev/elysium/minecluster/pulls/12
This commit is contained in:
dunemask 2024-02-05 02:13:32 +00:00
parent edbfc2348a
commit 78c5b72482
30 changed files with 391 additions and 53 deletions

View file

@ -7,8 +7,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";
const textFileTypes = ["properties", "txt", "yaml", "yml", "json", "env"];
const imageFileTypes = ["png", "jpeg", "jpg"];
@ -52,6 +52,7 @@ export default function FilePreview(props) {
await fetch("/api/files/upload", {
method: "POST",
body: formData,
headers: cairoAuthHeader(),
});
dialogToggle();
}

View file

@ -18,6 +18,7 @@ import {
getServerItem,
} from "@mcl/queries";
import { previewServerItem } from "../../util/queries";
import { cairoAuthHeader } from "@mcl/util/auth.js";
import { supportedFileTypes } from "./FilePreview.jsx";
@ -109,6 +110,7 @@ export default function MineclusterFiles(props) {
await fetch("/api/files/upload", {
method: "POST",
body: formData,
headers: cairoAuthHeader(),
});
}

View file

@ -8,6 +8,7 @@ export default class RconSocket {
this.sk.on("rcon-error", this.onRconError.bind(this));
this.sk.on("error", () => console.log("WHOOSPSIE I GUESS?"));
this.rconLive = false;
this.rconError = false;
}
onPush(p) {
@ -22,6 +23,7 @@ export default class RconSocket {
onRconError(v) {
this.rconLive = false;
this.rconError = true;
console.log("Server sent: ", v);
}

View file

@ -55,10 +55,12 @@ export default function RconView(props) {
variant="outlined"
value={cmd}
onChange={updateCmd}
disabled={!(rcon && rcon.rconLive)}
disabled={!(rcon && rcon.rconLive && !rcon.rconError)}
/>
{rcon && rcon.rconLive && <Button onClick={sendCommand}>Send</Button>}
{!(rcon && rcon.rconLive) && (
{rcon && rcon.rconLive && !rcon.rconError && (
<Button onClick={sendCommand}>Send</Button>
)}
{!(rcon && rcon.rconLive && !rcon.rconError) && (
<Button color="secondary">Not Connected</Button>
)}
</Box>

View file

@ -10,6 +10,7 @@ const defaultSettings = {
simplifiedControls: false,
logAppDetails: true,
defaultPage: "home",
cairoAuth: null,
};
const settings = localSettings ? JSON.parse(localSettings) : defaultSettings;
@ -27,6 +28,7 @@ const settingsUpdater = (oldState, settingsUpdate) => {
if (settingsUpdate[k] === undefined) continue;
settingsToUpdate[k] = settingsUpdate[k];
}
console.log("SAVING", settingsToUpdate);
localStorage.setItem("settings", JSON.stringify(settingsToUpdate));
};

View file

@ -1,13 +1,14 @@
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import MCLPortal from "./MCLPortal.jsx";
import Button from "@mui/material/Button";
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
// 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";
export default function Views() {
const auth = useCairoAuth();
if (!auth) return <Auth />;
return (
<div className="view">
<MCLMenu />

63
src/pages/Auth.jsx Normal file
View file

@ -0,0 +1,63 @@
import { useState, useEffect } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
export default function Auth() {
const [searchParams] = useSearchParams();
const currentServer = searchParams.get("token");
const nav = useNavigate();
const cairoLogin = () =>
(window.location.href = `/api/auth/redirect?redirectUri=${window.location.href}`);
return (
<Box
className="auth"
sx={{
height: "100%",
backgroundColor: (theme) => theme.palette.primary.main,
}}
>
<Box className="auth-display" sx={{ display: "flex", height: "95vh" }}>
<Box
sx={{
height: "50%",
width: "50%",
m: "auto",
display: "flex",
flexWrap: "wrap",
}}
>
<Box
sx={{
backgroundColor: "white",
display: "inline-flex",
m: "auto",
borderRadius: "8px",
height: "5rem",
}}
>
<Button
color="secondary"
variant="outlined"
onClick={cairoLogin}
sx={{ p: "1.5rem" }}
endIcon={
<img
src="https://cairo.dunemask.net/cairo/icons/apple-touch-icon-120x120.png"
width="48px"
style={{ borderRadius: "4px" }}
/>
}
>
Login with Cairo
</Button>
</Box>
</Box>
</Box>
</Box>
);
}

40
src/util/auth.js Normal file
View file

@ -0,0 +1,40 @@
import { useState, useContext, useEffect } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import SettingsContext from "@mcl/settings";
const verifyAuth = (authToken) =>
fetch("/api/auth/verify", {
headers: { Authorization: `Bearer ${authToken}` },
})
.then((res) => res.status === 200)
.catch(() => false);
export function useCairoAuth() {
const { state: settings, updateSettings } = useContext(SettingsContext);
const [auth, setAuth] = useState(!!settings.cairoAuth);
const [searchParams] = useSearchParams();
const nav = useNavigate();
useEffect(() => {
const webToken = searchParams.get("cairoAuthToken");
if (!webToken) return;
verifyAuth(webToken).then(setAuth);
updateSettings({ cairoAuth: webToken });
nav("/");
}, [searchParams]);
useEffect(() => {
verifyAuth(settings.cairoAuth).then(setAuth);
nav("/");
}, [settings.cairoAuth]);
return auth;
}
export function getAuthTokenFromStorage() {
return JSON.parse(localStorage.getItem("settings")).cairoAuth;
}
export function cairoAuthHeader() {
return { Authorization: `Bearer ${getAuthTokenFromStorage()}` };
}

View file

@ -1,12 +1,17 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { cairoAuthHeader } from "@mcl/util/auth.js";
const fetchApi = (subPath) => async () =>
fetch(`/api${subPath}`).then((res) => res.json());
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));
@ -16,6 +21,7 @@ const fetchApiPost = (subPath, json) => async () =>
method: "POST",
headers: {
"Content-Type": "application/json",
...cairoAuthHeader(),
},
body: JSON.stringify(json),
}).then((res) => res.json());
@ -117,6 +123,7 @@ const postJsonApi = (subPath, body, invalidate, method = "POST") => {
method,
headers: {
"Content-Type": "application/json",
...cairoAuthHeader(),
},
body: JSON.stringify(body),
});