mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-27 07:21:58 -06:00
Add GitHub API token management
This commit is contained in:
278
Data/Server/WebUI/src/Access_Management/Github_API_Token.jsx
Normal file
278
Data/Server/WebUI/src/Access_Management/Github_API_Token.jsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
InputAdornment,
|
||||
Link,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
|
||||
const paperSx = {
|
||||
m: 2,
|
||||
p: 0,
|
||||
bgcolor: "#1e1e1e",
|
||||
color: "#f5f7fa",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 320
|
||||
};
|
||||
|
||||
const fieldSx = {
|
||||
mt: 2,
|
||||
"& .MuiOutlinedInput-root": {
|
||||
bgcolor: "#181818",
|
||||
color: "#f5f7fa",
|
||||
"& fieldset": { borderColor: "#2a2a2a" },
|
||||
"&:hover fieldset": { borderColor: "#58a6ff" },
|
||||
"&.Mui-focused fieldset": { borderColor: "#58a6ff" }
|
||||
},
|
||||
"& .MuiInputLabel-root": { color: "#bbb" },
|
||||
"& .MuiInputLabel-root.Mui-focused": { color: "#7db7ff" }
|
||||
};
|
||||
|
||||
export default function GithubAPIToken({ isAdmin = false }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [token, setToken] = useState("");
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [fetchError, setFetchError] = useState("");
|
||||
const [verification, setVerification] = useState({
|
||||
message: "",
|
||||
valid: null,
|
||||
status: "",
|
||||
rateLimit: null,
|
||||
error: ""
|
||||
});
|
||||
|
||||
const hydrate = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setFetchError("");
|
||||
try {
|
||||
const resp = await fetch("/api/github/token");
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
const storedToken = typeof data?.token === "string" ? data.token : "";
|
||||
setToken(storedToken);
|
||||
setInputValue(storedToken);
|
||||
setVerification({
|
||||
message: typeof data?.message === "string" ? data.message : "",
|
||||
valid: data?.valid === true,
|
||||
status: typeof data?.status === "string" ? data.status : "",
|
||||
rateLimit: typeof data?.rate_limit === "number" ? data.rate_limit : null,
|
||||
error: typeof data?.error === "string" ? data.error : ""
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err && typeof err.message === "string" ? err.message : String(err);
|
||||
setFetchError(message);
|
||||
setToken("");
|
||||
setInputValue("");
|
||||
setVerification({ message: "", valid: null, status: "", rateLimit: null, error: "" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdmin) return;
|
||||
hydrate();
|
||||
}, [hydrate, isAdmin]);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
setSaving(true);
|
||||
setFetchError("");
|
||||
try {
|
||||
const resp = await fetch("/api/github/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token: inputValue })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
const storedToken = typeof data?.token === "string" ? data.token : "";
|
||||
setToken(storedToken);
|
||||
setInputValue(storedToken);
|
||||
setVerification({
|
||||
message: typeof data?.message === "string" ? data.message : "",
|
||||
valid: data?.valid === true,
|
||||
status: typeof data?.status === "string" ? data.status : "",
|
||||
rateLimit: typeof data?.rate_limit === "number" ? data.rate_limit : null,
|
||||
error: typeof data?.error === "string" ? data.error : ""
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err && typeof err.message === "string" ? err.message : String(err);
|
||||
setFetchError(message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [inputValue]);
|
||||
|
||||
const dirty = useMemo(() => inputValue !== token, [inputValue, token]);
|
||||
|
||||
const verificationMessage = useMemo(() => {
|
||||
if (dirty) {
|
||||
return { text: "Token has unsaved changes — save to verify.", color: "#f0c36d" };
|
||||
}
|
||||
const message = verification.message || "";
|
||||
if (!message) {
|
||||
return { text: "", color: "#bbb" };
|
||||
}
|
||||
if (verification.valid) {
|
||||
return { text: message, color: "#7dffac" };
|
||||
}
|
||||
if ((verification.status || "").toLowerCase() === "missing") {
|
||||
return { text: message, color: "#bbb" };
|
||||
}
|
||||
return { text: message, color: "#ff8080" };
|
||||
}, [dirty, verification]);
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<Paper sx={{ m: 2, p: 3, bgcolor: "#1e1e1e" }}>
|
||||
<Typography variant="h6" sx={{ color: "#ff8080" }}>
|
||||
Access denied
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "#bbb" }}>
|
||||
You do not have permission to manage the GitHub API token.
|
||||
</Typography>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper sx={paperSx} elevation={2}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
p: 2,
|
||||
borderBottom: "1px solid #2a2a2a"
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0.3 }}>
|
||||
Github API Token
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "#aaa" }}>
|
||||
Using a Github Personal Access Token increased the Github API rate limits from 60/hr to 5000/hr.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<RefreshIcon />}
|
||||
sx={{ borderColor: "#58a6ff", color: "#58a6ff" }}
|
||||
onClick={hydrate}
|
||||
disabled={loading || saving}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{loading && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
color: "#7db7ff",
|
||||
px: 2,
|
||||
py: 1.5,
|
||||
borderBottom: "1px solid #2a2a2a"
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={18} sx={{ color: "#58a6ff" }} />
|
||||
<Typography variant="body2">Loading token…</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{fetchError && (
|
||||
<Box sx={{ px: 2, py: 1.5, color: "#ff8080", borderBottom: "1px solid #2a2a2a" }}>
|
||||
<Typography variant="body2">{fetchError}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ px: 2, py: 2, display: "flex", flexDirection: "column", gap: 1.5 }}>
|
||||
<Typography variant="body2" sx={{ color: "#ccc" }}>
|
||||
Navigate to{' '}
|
||||
<Link
|
||||
href="https://github.com/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{ color: "#7db7ff" }}
|
||||
>
|
||||
https://github.com/settings/tokens
|
||||
</Link>{' '}
|
||||
> Personal Access Tokens > Tokens (Classic) > Generate New Token > New Personal Access Token (Classic)
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "#ccc" }}>
|
||||
<Box component="span" sx={{ fontWeight: 600 }}>Note:</Box> Borealis Automation Platform
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "#ccc" }}>
|
||||
<Box component="span" sx={{ fontWeight: 600 }}>Scope:</Box>{' '}
|
||||
<Box component="code" sx={{ bgcolor: "#222", px: 0.75, py: 0.25, borderRadius: 1, fontSize: "0.85rem" }}>
|
||||
public_repo
|
||||
</Box>
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "#ccc" }}>
|
||||
<Box component="span" sx={{ fontWeight: 600 }}>Expiration:</Box>{' '}
|
||||
<Box component="code" sx={{ bgcolor: "#222", px: 0.75, py: 0.25, borderRadius: 1, fontSize: "0.85rem" }}>
|
||||
No Expiration
|
||||
</Box>
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label="Personal Access Token"
|
||||
value={inputValue}
|
||||
onChange={(event) => setInputValue(event.target.value)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
sx={fieldSx}
|
||||
disabled={saving || loading}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end" sx={{ mr: -1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleSave}
|
||||
disabled={saving || loading}
|
||||
sx={{
|
||||
bgcolor: "#58a6ff",
|
||||
color: "#0b0f19",
|
||||
minWidth: 88,
|
||||
"&:hover": { bgcolor: "#7db7ff" }
|
||||
}}
|
||||
>
|
||||
{saving ? <CircularProgress size={16} sx={{ color: "#0b0f19" }} /> : "Save"}
|
||||
</Button>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
{verificationMessage.text && (
|
||||
<Typography variant="body2" sx={{ color: verificationMessage.color }}>
|
||||
{verificationMessage.text}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{!dirty && verification.rateLimit && (
|
||||
<Typography variant="caption" sx={{ color: "#7db7ff" }}>
|
||||
Authenticated GitHub rate limit: {verification.rateLimit.toLocaleString()} requests / hour
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import ScheduledJobsList from "./Scheduling/Scheduled_Jobs_List";
|
||||
import CreateJob from "./Scheduling/Create_Job.jsx";
|
||||
import CredentialList from "./Access_Management/Credential_List.jsx";
|
||||
import UserManagement from "./Access_Management/Users.jsx";
|
||||
import GithubAPIToken from "./Access_Management/Github_API_Token.jsx";
|
||||
import ServerInfo from "./Admin/Server_Info.jsx";
|
||||
|
||||
// Networking Imports
|
||||
@@ -215,6 +216,8 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
||||
}
|
||||
case "access_credentials":
|
||||
return "/access_management/credentials";
|
||||
case "access_github_token":
|
||||
return "/access_management/github_token";
|
||||
case "access_users":
|
||||
return "/access_management/users";
|
||||
case "server_info":
|
||||
@@ -267,6 +270,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
||||
};
|
||||
}
|
||||
if (path === "/access_management/users") return { page: "access_users", options: {} };
|
||||
if (path === "/access_management/github_token") return { page: "access_github_token", options: {} };
|
||||
if (path === "/access_management/credentials") return { page: "access_credentials", options: {} };
|
||||
if (path === "/admin/server_info") return { page: "server_info", options: {} };
|
||||
return { page: "devices", options: {} };
|
||||
@@ -434,6 +438,10 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
||||
items.push({ label: "Access Management", page: "access_credentials" });
|
||||
items.push({ label: "Credentials", page: "access_credentials" });
|
||||
break;
|
||||
case "access_github_token":
|
||||
items.push({ label: "Access Management", page: "access_credentials" });
|
||||
items.push({ label: "GitHub API Token", page: "access_github_token" });
|
||||
break;
|
||||
case "access_users":
|
||||
items.push({ label: "Access Management", page: "access_credentials" });
|
||||
items.push({ label: "Users", page: "access_users" });
|
||||
@@ -869,6 +877,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
||||
useEffect(() => {
|
||||
const requiresAdmin = currentPage === 'server_info'
|
||||
|| currentPage === 'access_credentials'
|
||||
|| currentPage === 'access_github_token'
|
||||
|| currentPage === 'access_users'
|
||||
|| currentPage === 'ssh_devices'
|
||||
|| currentPage === 'winrm_devices'
|
||||
@@ -1055,6 +1064,9 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
||||
case "access_credentials":
|
||||
return <CredentialList isAdmin={isAdmin} />;
|
||||
|
||||
case "access_github_token":
|
||||
return <GithubAPIToken isAdmin={isAdmin} />;
|
||||
|
||||
case "access_users":
|
||||
return <UserManagement isAdmin={isAdmin} />;
|
||||
|
||||
|
||||
@@ -22,7 +22,12 @@ import {
|
||||
Apps as AssembliesIcon
|
||||
} from "@mui/icons-material";
|
||||
import { LocationCity as SitesIcon } from "@mui/icons-material";
|
||||
import { Dns as ServerInfoIcon, VpnKey as CredentialIcon, PersonOutline as UserIcon } from "@mui/icons-material";
|
||||
import {
|
||||
Dns as ServerInfoIcon,
|
||||
VpnKey as CredentialIcon,
|
||||
PersonOutline as UserIcon,
|
||||
GitHub as GitHubIcon
|
||||
} from "@mui/icons-material";
|
||||
|
||||
function NavigationSidebar({ currentPage, onNavigate, isAdmin = false }) {
|
||||
const [expandedNav, setExpandedNav] = useState({
|
||||
@@ -296,7 +301,10 @@ function NavigationSidebar({ currentPage, onNavigate, isAdmin = false }) {
|
||||
{/* Access Management */}
|
||||
{(() => {
|
||||
if (!isAdmin) return null;
|
||||
const groupActive = currentPage === "access_credentials" || currentPage === "access_users";
|
||||
const groupActive =
|
||||
currentPage === "access_credentials" ||
|
||||
currentPage === "access_users" ||
|
||||
currentPage === "access_github_token";
|
||||
return (
|
||||
<Accordion
|
||||
expanded={expandedNav.access}
|
||||
@@ -334,6 +342,7 @@ function NavigationSidebar({ currentPage, onNavigate, isAdmin = false }) {
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 0, bgcolor: "#232323" }}>
|
||||
<NavItem icon={<CredentialIcon fontSize="small" />} label="Credentials" pageKey="access_credentials" />
|
||||
<NavItem icon={<GitHubIcon fontSize="small" />} label="GitHub API Token" pageKey="access_github_token" />
|
||||
<NavItem icon={<UserIcon fontSize="small" />} label="Users" pageKey="access_users" />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
Reference in New Issue
Block a user