diff --git a/lib/controllers/lifecycle-controller.js b/lib/controllers/lifecycle-controller.js index 57b8385..5aa271d 100644 --- a/lib/controllers/lifecycle-controller.js +++ b/lib/controllers/lifecycle-controller.js @@ -4,34 +4,19 @@ import { createServerEntry, deleteServerEntry, getServerEntry, + modifyServerEntry, } from "../database/queries/server-queries.js"; -import { sendError } from "../util/ExpressClientError.js"; +import ExpressClientError, { sendError } from "../util/ExpressClientError.js"; import { toggleServer } from "../k8s/k8s-server-control.js"; const dnsRegex = new RegExp( `^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$`, ); -function payloadFilter(req, res) { +function backupPayloadFilter(req, res) { const serverSpec = req.body; - if (!serverSpec) return res.sendStatus(400); - const { name, host, version, serverType, memory, extraPorts } = serverSpec; const { backupHost, backupBucket, backupId, backupKey, backupInterval } = serverSpec; - if (!name) return res.status(400).send("Server name is required!"); - if (!host) return res.status(400).send("Server host is required!"); - if (!dnsRegex.test(host)) return res.status(400).send("Hostname invalid!"); - if (!version) return res.status(400).send("Server version is required!"); - if (!serverType) return res.status(400).send("Server type is required!"); - if (!memory) return res.status(400).send("Memory is required!"); - if ( - !!extraPorts && - (!Array.isArray(extraPorts) || - extraPorts.find((e) => typeof e !== "string" || e.length > 5)) - ) - return res - .status(400) - .send("Extra ports must be a list of strings with length of 5!"); // TODO: Impliment non creation time backups if ( !!backupHost || @@ -57,6 +42,27 @@ function payloadFilter(req, res) { return "filtered"; } +function payloadFilter(req, res) { + const serverSpec = req.body; + if (!serverSpec) return res.sendStatus(400); + const { name, host, version, serverType, memory, extraPorts } = serverSpec; + if (!name) return res.status(400).send("Server name is required!"); + if (!host) return res.status(400).send("Server host is required!"); + if (!dnsRegex.test(host)) return res.status(400).send("Hostname invalid!"); + if (!version) return res.status(400).send("Server version is required!"); + if (!serverType) return res.status(400).send("Server type is required!"); + if (!memory) return res.status(400).send("Memory is required!"); + if ( + !!extraPorts && + (!Array.isArray(extraPorts) || + extraPorts.find((e) => typeof e !== "string" || e.length > 5)) + ) + return res + .status(400) + .send("Extra ports must be a list of strings with length of 5!"); + return "filtered"; +} + function checkServerId(serverSpec) { if (!serverSpec) throw new ExpressClientError({ c: 400 }); if (!serverSpec.id) @@ -65,6 +71,7 @@ function checkServerId(serverSpec) { export async function createServer(req, res) { if (payloadFilter(req, res) !== "filtered") return; + if (backupPayloadFilter(req, res) !== "filtered") return; const serverSpec = req.body; try { const serverEntry = await createServerEntry(serverSpec); @@ -117,3 +124,34 @@ export async function stopServer(req, res) { .then(() => res.sendStatus(200)) .catch(sendError(res)); } + +export async function getServer(req, res) { + // Ensure spec is safe + const serverSpec = req.body; + try { + checkServerId(serverSpec); + } catch (e) { + return sendError(res)(e); + } + const { id } = serverSpec; + getServerEntry(id).then((s) => { + delete s.backupKey; // Do not let this ever get to an API client + s.backupBucket = s.backupPath; + delete s.backupPath; + delete s.backupId; // Do not let this ever get to an API client + res.json(s); + }); +} + +export async function modifyServer(req, res) { + if (payloadFilter(req, res) !== "filtered") return; + const serverSpec = req.body; + try { + checkServerId(serverSpec); + const serverEntry = await modifyServerEntry(serverSpec); + // await createServerResources(serverEntry); + res.sendStatus(200); + } catch (e) { + sendError(res)(e); + } +} diff --git a/lib/database/queries/server-queries.js b/lib/database/queries/server-queries.js index 93e84bd..66300bc 100644 --- a/lib/database/queries/server-queries.js +++ b/lib/database/queries/server-queries.js @@ -1,5 +1,10 @@ import pg from "../postgres.js"; -import { deleteQuery, insertQuery, selectWhereQuery } from "../pg-query.js"; +import { + deleteQuery, + insertQuery, + selectWhereQuery, + updateWhereAllQuery, +} from "../pg-query.js"; import ExpressClientError from "../../util/ExpressClientError.js"; const table = "servers"; @@ -30,7 +35,7 @@ export async function createServerEntry(serverSpec) { server_type, memory, extra_ports, - backup_enabled: !!backup_interval, // We already verified the payload, so any backup key will work + backup_enabled: !!backup_interval ? true : null, // We already verified the payload, so any backup key will work backup_host, backup_bucket_path, backup_id, @@ -128,6 +133,45 @@ export async function getServerEntry(serverId) { } } +export async function modifyServerEntry(serverSpec) { + const { + id, + name, + host, + version, + serverType: server_type, + memory, + extraPorts: extra_ports, + backupEnabled: backup_enabled, + backupHost: backup_host, + backupBucket: backup_bucket_path, + backupId: backup_id, + backupKey: backup_key, + backupInterval: backup_interval, + } = serverSpec; + + const q = updateWhereAllQuery( + table, + { + name, + host, + version, + server_type, + memory, + extra_ports, + backup_enabled, + backup_host, + backup_bucket_path, + backup_id, + backup_key, + backup_interval, + }, + { id }, + ); + + return pg.query(q); +} + export async function getServerEntries() { const q = `SELECT * FROM ${table}`; return pg.query(q); diff --git a/lib/routes/server-route.js b/lib/routes/server-route.js index 7361abd..d6eb922 100644 --- a/lib/routes/server-route.js +++ b/lib/routes/server-route.js @@ -4,6 +4,8 @@ import { deleteServer, startServer, stopServer, + getServer, + modifyServer, } from "../controllers/lifecycle-controller.js"; import { serverInstances, @@ -18,4 +20,6 @@ router.post("/start", startServer); router.post("/stop", stopServer); router.get("/list", serverList); router.get("/instances", serverInstances); +router.post("/blueprint", getServer); +router.post("/modify", modifyServer); export default router; diff --git a/src/components/server-options/BackupBucketOption.jsx b/src/components/server-options/BackupBucketOption.jsx index bc7139d..7650187 100644 --- a/src/components/server-options/BackupBucketOption.jsx +++ b/src/components/server-options/BackupBucketOption.jsx @@ -6,7 +6,7 @@ export default function BackupBucketOption(props) { p !== "25565" && p !== "25575" && p.length < 6; export default function ExtraPortsOption(props) { - const [extraPorts, setExtraPorts] = useState([]); + const { extraPorts: initExtraPorts } = props; + const [extraPorts, setExtraPorts] = useState(initExtraPorts ?? []); const { onChange } = props; function portChange(e, val, optionType, changedValue) { diff --git a/src/components/server-options/HostOption.jsx b/src/components/server-options/HostOption.jsx index 7efe6a9..d03d1db 100644 --- a/src/components/server-options/HostOption.jsx +++ b/src/components/server-options/HostOption.jsx @@ -1,10 +1,11 @@ import TextField from "@mui/material/TextField"; export default function HostOption(props) { - const { onChange } = props; + const { value, onChange } = props; return ( diff --git a/src/nav/MCLPages.jsx b/src/nav/MCLPages.jsx index 56ab52e..c2518c7 100644 --- a/src/nav/MCLPages.jsx +++ b/src/nav/MCLPages.jsx @@ -1,6 +1,7 @@ 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"; // 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"; @@ -21,10 +22,17 @@ export default [ visible: true, }, { - name: "Edit", + name: "Files", path: "/mcl/files", icon: , component: , visible: false, }, + { + name: "Edit", + path: "/mcl/edit", + icon: , + component: , + visible: false, + }, ]; diff --git a/src/pages/CreateCoreOptions.jsx b/src/pages/CreateCoreOptions.jsx index afe2f0d..4d68121 100644 --- a/src/pages/CreateCoreOptions.jsx +++ b/src/pages/CreateCoreOptions.jsx @@ -55,8 +55,8 @@ export default function CreateCoreOptions() { async function upsertSpec() { if (validateSpec() !== "validated") return; - createServer(spec) - // .then(() => nav("/")) + createServer() + .then(() => nav("/")) .catch(alert); } @@ -90,8 +90,8 @@ export default function CreateCoreOptions() { sx={{ width: "100%", maxWidth: "600px", margin: "auto" }} > - - + + + + + + + ); +} diff --git a/src/pages/EditCoreOptions.jsx b/src/pages/EditCoreOptions.jsx new file mode 100644 index 0000000..163a6a1 --- /dev/null +++ b/src/pages/EditCoreOptions.jsx @@ -0,0 +1,134 @@ +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +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"; + +// 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 ServerTypeOption, { + serverTypeOptions, +} from "@mcl/components/server-options/ServerTypeOption.jsx"; +import CpuOption, { + cpuOptions, +} from "@mcl/components/server-options/CpuOption.jsx"; +import MemoryOption, { + memoryOptions, +} from "@mcl/components/server-options/MemoryOption.jsx"; +import ExtraPortsOption from "@mcl/components/server-options/ExtraPortsOption.jsx"; + +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 BackupIntervalOption, { + backupIntervalDefault, +} from "@mcl/components/server-options/BackupIntervalOption.jsx"; + +export default function EditCoreOptions(props) { + const { serverId } = props; + const [spec, setSpec] = useState(); + const modifyServer = useModifyServer(spec); + const { isLoading, data: serverBlueprint } = useGetServer(serverId); + + useEffect(() => setSpec(serverBlueprint), [serverBlueprint]); + + const updateSpec = (attr, val) => { + const s = { ...spec }; + s[attr] = val; + setSpec(s); + }; + + const coreUpdate = (attr) => (e) => updateSpec(attr, e.target.value); + + const upsertSpec = () => { + modifyServer(spec); + }; + + const toggleBackupEnabled = () => + updateSpec("backupEnabled", !spec.backupEnabled); + + function validateSpec() { + console.log("TODO CREATE VALIDATION"); + if (!spec.host) return alertValidationError("Host cannot be blank"); + if (!spec.name) return alertValidationError("Name not included"); + if (!spec.version) return alertValidationError("Version cannot be blank"); + return "validated"; + } + + function alertValidationError(reason) { + alert(`Could not validate spec because: ${reason}`); + } + + if (!spec) return; // TODO: Add loading for spec + return ( + + + + + + + + + + + {spec.backupEnabled !== null && ( + + } + label="Enable Backups?" + labelPlacement="start" + sx={{ mr: "auto" }} + /> + )} + + {/*spec.backupEnabled && ( // TODO: Disabled while secrets are insecure + + Backups + + + + + + + )*/} + + + + + ); +} diff --git a/src/util/queries.js b/src/util/queries.js index 2e02366..c80be01 100644 --- a/src/util/queries.js +++ b/src/util/queries.js @@ -39,6 +39,14 @@ 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 getServerFiles = async (serverId, path) => fetchApiCore("/files/list", { id: serverId, path }, "POST", true);