[FEATURE] Initial Cairo Auth Integration

This commit is contained in:
Dunemask 2024-02-04 01:24:36 -07:00
parent edbfc2348a
commit 184f1fa631
10 changed files with 234 additions and 14 deletions

16
lib/routes/auth-route.js Normal file
View file

@ -0,0 +1,16 @@
import { Router } from "express";
import cairoAuthMiddleware from "./middlewares/auth-middleware.js";
const router = Router();
const ok = (_r, res) => res.sendStatus(200);
function cairoRedirect(req, res) {
res.redirect(
`${process.env.MCL_CAIRO_URL}/cairo/auth?redirectUri=${req.query.redirectUri}`,
);
}
router.get("/verify", cairoAuthMiddleware, ok);
router.get("/redirect", cairoRedirect);
export default router;

View file

@ -0,0 +1,32 @@
// Imports
import { Router } from "express";
import bearerTokenMiddleware from "express-bearer-token";
import { ERR, VERB } from "../../util/logging.js";
// Constants
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/user/info`, config).then((res) =>
res.json(),
);
};
// Middleware
const cairoAuthHandler = (req, res, next) => {
if (!req.token) return res.status(401).send("Cairo auth required!");
VERB("AUTH", `${MCL_CAIRO_URL}/api/user/info`);
cairoAuthenticate(req.token)
.then(() => next())
.catch((err) => {
ERR("AUTH", err.response ? err.response.data : err.message);
if (!err.response) return res.status(500).send(`Auth failure ${err}`);
return res.status(err.response.status).send(err.response.data);
});
};
cairoAuthMiddleware.use([bearerTokenMiddleware(), cairoAuthHandler]);
export default cairoAuthMiddleware;

View file

@ -11,6 +11,9 @@ import {
serverInstances,
serverList,
} from "../controllers/status-controller.js";
import cairoAuthMiddleware from "./middlewares/auth-middleware.js";
const router = Router();
router.use(jsonMiddleware());
// Routes
@ -19,7 +22,7 @@ router.delete("/delete", deleteServer);
router.post("/start", startServer);
router.post("/stop", stopServer);
router.get("/list", serverList);
router.get("/instances", serverInstances);
router.get("/instances", cairoAuthMiddleware, serverInstances);
router.post("/blueprint", getServer);
router.post("/modify", modifyServer);
export default router;

View file

@ -3,6 +3,7 @@ import express from "express";
// Routes
import vitals from "../routes/vitals-route.js";
import authRoute from "../routes/auth-route.js";
import systemRoute from "../routes/system-route.js";
import serverRoute from "../routes/server-route.js";
import filesRoute from "../routes/files-route.js";
@ -22,6 +23,7 @@ export default function buildRoutes(pg, skio) {
// Middlewares
// Routes
router.use("/api/auth", authRoute);
router.use("/api/system", systemRoute);
router.use("/api/server", serverRoute);
router.use("/api/files", filesRoute);

41
package-lock.json generated
View file

@ -15,6 +15,7 @@
"bcrypt": "^5.1.1",
"chalk": "^5.3.0",
"express": "^4.18.2",
"express-bearer-token": "^2.4.0",
"figlet": "^1.7.0",
"js-yaml": "^4.1.0",
"moment": "^2.29.4",
@ -4603,6 +4604,26 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
"dependencies": {
"cookie": "0.4.1",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@ -5073,6 +5094,26 @@
"node": ">= 0.10.0"
}
},
"node_modules/express-bearer-token": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/express-bearer-token/-/express-bearer-token-2.4.0.tgz",
"integrity": "sha512-2+kRZT2xo+pmmvSY7Ma5FzxTJpO3kGaPCEXPbAm3GaoZ/z6FE4K6L7cvs1AUZwY2xkk15PcQw7t4dWjsl5rdJw==",
"dependencies": {
"cookie": "^0.3.1",
"cookie-parser": "^1.4.4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/express-bearer-token/node_modules/cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",

View file

@ -22,41 +22,42 @@
"author": "Dunemask",
"license": "LGPL-2.1",
"devDependencies": {
"@emotion/react": "^11.11.1",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.19",
"@mui/material": "^5.14.20",
"@tanstack/react-query": "^5.12.2",
"@mui/icons-material": "^5.15.7",
"@mui/material": "^5.15.7",
"@tanstack/react-query": "^5.18.1",
"@vitejs/plugin-react": "^4.2.1",
"chonky": "^2.3.2",
"chonky-icon-fontawesome": "^2.3.2",
"concurrently": "^8.2.2",
"nodemon": "^3.0.2",
"prettier": "^3.1.0",
"nodemon": "^3.0.3",
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-quill": "^2.0.0",
"react-router-dom": "^6.20.1",
"react-toastify": "^9.1.3",
"socket.io-client": "^4.7.2",
"vite": "^5.0.7"
"react-router-dom": "^6.22.0",
"react-toastify": "^10.0.4",
"socket.io-client": "^4.7.4",
"vite": "^5.0.12"
},
"dependencies": {
"@kubernetes/client-node": "^0.20.0",
"aws-sdk": "^2.1514.0",
"aws-sdk": "^2.1550.0",
"basic-ftp": "^5.0.4",
"bcrypt": "^5.1.1",
"chalk": "^5.3.0",
"express": "^4.18.2",
"express-bearer-token": "^2.4.0",
"figlet": "^1.7.0",
"js-yaml": "^4.1.0",
"moment": "^2.29.4",
"moment": "^2.30.1",
"multer": "^1.4.5-lts.1",
"multer-s3": "^3.0.1",
"pg-promise": "^11.5.4",
"postgres-migrations": "^5.3.0",
"rcon-client": "^4.2.4",
"socket.io": "^4.7.2",
"socket.io": "^4.7.4",
"uuid": "^9.0.1"
}
}

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

@ -5,9 +5,13 @@ 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>
);
}

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

@ -0,0 +1,56 @@
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 useAuth() {
const { state: settings } = useContext(SettingsContext);
const [auth, setAuth] = useState(!!!settings.cairoAuth);
if (!settings.cairoAuth) return auth;
fetch("/api/auth/verify", {
headers: { Authorization: `Bearer ${settings.cairoAuth}` },
})
.then(() => setAuth(true))
.catch(() => setAuth(false));
return auth;
}
export function useUpdateAuth() {
const { updateSettings } = useContext(SettingsContext);
const [searchParams] = useSearchParams();
const webToken = searchParams.get("cairoAuthToken");
if (webToken) {
updateSettings({ cairoAuth: webToken });
searchParams.delete("cairoAuthToken");
}
}