[FEATURE] Initial Cairo Auth Integration
This commit is contained in:
parent
edbfc2348a
commit
184f1fa631
10 changed files with 234 additions and 14 deletions
16
lib/routes/auth-route.js
Normal file
16
lib/routes/auth-route.js
Normal 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;
|
32
lib/routes/middlewares/auth-middleware.js
Normal file
32
lib/routes/middlewares/auth-middleware.js
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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
41
package-lock.json
generated
|
@ -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",
|
||||
|
|
27
package.json
27
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
||||
|
|
|
@ -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
63
src/pages/Auth.jsx
Normal 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
56
src/util/auth.js
Normal 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");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue