mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 21:21:57 -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 CreateJob from "./Scheduling/Create_Job.jsx";
|
||||||
import CredentialList from "./Access_Management/Credential_List.jsx";
|
import CredentialList from "./Access_Management/Credential_List.jsx";
|
||||||
import UserManagement from "./Access_Management/Users.jsx";
|
import UserManagement from "./Access_Management/Users.jsx";
|
||||||
|
import GithubAPIToken from "./Access_Management/Github_API_Token.jsx";
|
||||||
import ServerInfo from "./Admin/Server_Info.jsx";
|
import ServerInfo from "./Admin/Server_Info.jsx";
|
||||||
|
|
||||||
// Networking Imports
|
// Networking Imports
|
||||||
@@ -215,6 +216,8 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
|||||||
}
|
}
|
||||||
case "access_credentials":
|
case "access_credentials":
|
||||||
return "/access_management/credentials";
|
return "/access_management/credentials";
|
||||||
|
case "access_github_token":
|
||||||
|
return "/access_management/github_token";
|
||||||
case "access_users":
|
case "access_users":
|
||||||
return "/access_management/users";
|
return "/access_management/users";
|
||||||
case "server_info":
|
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/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 === "/access_management/credentials") return { page: "access_credentials", options: {} };
|
||||||
if (path === "/admin/server_info") return { page: "server_info", options: {} };
|
if (path === "/admin/server_info") return { page: "server_info", options: {} };
|
||||||
return { page: "devices", 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: "Access Management", page: "access_credentials" });
|
||||||
items.push({ label: "Credentials", page: "access_credentials" });
|
items.push({ label: "Credentials", page: "access_credentials" });
|
||||||
break;
|
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":
|
case "access_users":
|
||||||
items.push({ label: "Access Management", page: "access_credentials" });
|
items.push({ label: "Access Management", page: "access_credentials" });
|
||||||
items.push({ label: "Users", page: "access_users" });
|
items.push({ label: "Users", page: "access_users" });
|
||||||
@@ -869,6 +877,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const requiresAdmin = currentPage === 'server_info'
|
const requiresAdmin = currentPage === 'server_info'
|
||||||
|| currentPage === 'access_credentials'
|
|| currentPage === 'access_credentials'
|
||||||
|
|| currentPage === 'access_github_token'
|
||||||
|| currentPage === 'access_users'
|
|| currentPage === 'access_users'
|
||||||
|| currentPage === 'ssh_devices'
|
|| currentPage === 'ssh_devices'
|
||||||
|| currentPage === 'winrm_devices'
|
|| currentPage === 'winrm_devices'
|
||||||
@@ -1055,6 +1064,9 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
|||||||
case "access_credentials":
|
case "access_credentials":
|
||||||
return <CredentialList isAdmin={isAdmin} />;
|
return <CredentialList isAdmin={isAdmin} />;
|
||||||
|
|
||||||
|
case "access_github_token":
|
||||||
|
return <GithubAPIToken isAdmin={isAdmin} />;
|
||||||
|
|
||||||
case "access_users":
|
case "access_users":
|
||||||
return <UserManagement isAdmin={isAdmin} />;
|
return <UserManagement isAdmin={isAdmin} />;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ import {
|
|||||||
Apps as AssembliesIcon
|
Apps as AssembliesIcon
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { LocationCity as SitesIcon } 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 }) {
|
function NavigationSidebar({ currentPage, onNavigate, isAdmin = false }) {
|
||||||
const [expandedNav, setExpandedNav] = useState({
|
const [expandedNav, setExpandedNav] = useState({
|
||||||
@@ -296,7 +301,10 @@ function NavigationSidebar({ currentPage, onNavigate, isAdmin = false }) {
|
|||||||
{/* Access Management */}
|
{/* Access Management */}
|
||||||
{(() => {
|
{(() => {
|
||||||
if (!isAdmin) return null;
|
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 (
|
return (
|
||||||
<Accordion
|
<Accordion
|
||||||
expanded={expandedNav.access}
|
expanded={expandedNav.access}
|
||||||
@@ -334,6 +342,7 @@ function NavigationSidebar({ currentPage, onNavigate, isAdmin = false }) {
|
|||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails sx={{ p: 0, bgcolor: "#232323" }}>
|
<AccordionDetails sx={{ p: 0, bgcolor: "#232323" }}>
|
||||||
<NavItem icon={<CredentialIcon fontSize="small" />} label="Credentials" pageKey="access_credentials" />
|
<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" />
|
<NavItem icon={<UserIcon fontSize="small" />} label="Users" pageKey="access_users" />
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|||||||
@@ -130,6 +130,134 @@ _REPO_HASH_INTERVAL = max(30, min(_REPO_HASH_INTERVAL, 3600))
|
|||||||
_REPO_HASH_WORKER_STARTED = False
|
_REPO_HASH_WORKER_STARTED = False
|
||||||
_REPO_HASH_WORKER_LOCK = Lock()
|
_REPO_HASH_WORKER_LOCK = Lock()
|
||||||
|
|
||||||
|
DB_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "database.db"))
|
||||||
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||||
|
|
||||||
|
_GITHUB_TOKEN_CACHE: Dict[str, Any] = {"token": None, "loaded_at": 0.0, "known": False}
|
||||||
|
_GITHUB_TOKEN_LOCK = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _set_cached_github_token(token: Optional[str]) -> None:
|
||||||
|
with _GITHUB_TOKEN_LOCK:
|
||||||
|
_GITHUB_TOKEN_CACHE["token"] = token if token else None
|
||||||
|
_GITHUB_TOKEN_CACHE["loaded_at"] = time.time()
|
||||||
|
_GITHUB_TOKEN_CACHE["known"] = True
|
||||||
|
|
||||||
|
|
||||||
|
def _load_github_token_from_db(*, force_refresh: bool = False) -> Optional[str]:
|
||||||
|
now = time.time()
|
||||||
|
with _GITHUB_TOKEN_LOCK:
|
||||||
|
if (
|
||||||
|
not force_refresh
|
||||||
|
and _GITHUB_TOKEN_CACHE.get("known")
|
||||||
|
and now - (_GITHUB_TOKEN_CACHE.get("loaded_at") or 0.0) < 15.0
|
||||||
|
):
|
||||||
|
return _GITHUB_TOKEN_CACHE.get("token") # type: ignore[return-value]
|
||||||
|
|
||||||
|
conn: Optional[sqlite3.Connection] = None
|
||||||
|
token: Optional[str] = None
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH, timeout=5)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT token FROM github_token LIMIT 1")
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row and row[0]:
|
||||||
|
candidate = str(row[0]).strip()
|
||||||
|
token = candidate or None
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
token = None
|
||||||
|
except Exception as exc:
|
||||||
|
_write_service_log("server", f"github token lookup failed: {exc}")
|
||||||
|
token = None
|
||||||
|
finally:
|
||||||
|
if conn is not None:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
_set_cached_github_token(token)
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def _github_api_token(*, force_refresh: bool = False) -> Optional[str]:
|
||||||
|
token = _load_github_token_from_db(force_refresh=force_refresh)
|
||||||
|
if token:
|
||||||
|
return token
|
||||||
|
env_token = os.environ.get("BOREALIS_GITHUB_TOKEN") or os.environ.get("GITHUB_TOKEN")
|
||||||
|
if env_token:
|
||||||
|
env_token = env_token.strip()
|
||||||
|
return env_token or None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_github_token(token: Optional[str]) -> Dict[str, Any]:
|
||||||
|
if not token:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"message": "API Token Not Configured",
|
||||||
|
"status": "missing",
|
||||||
|
"rate_limit": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"User-Agent": "Borealis-Server",
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
resp = requests.get(
|
||||||
|
f"https://api.github.com/repos/{_DEFAULT_REPO}/branches/{_DEFAULT_BRANCH}",
|
||||||
|
headers=headers,
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
limit_header = resp.headers.get("X-RateLimit-Limit")
|
||||||
|
try:
|
||||||
|
limit_value = int(limit_header) if limit_header is not None else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
limit_value = None
|
||||||
|
|
||||||
|
if resp.status_code == 200:
|
||||||
|
if limit_value is not None and limit_value >= 5000:
|
||||||
|
return {
|
||||||
|
"valid": True,
|
||||||
|
"message": "API Authentication Successful",
|
||||||
|
"status": "ok",
|
||||||
|
"rate_limit": limit_value,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"message": "API Token Invalid",
|
||||||
|
"status": "insufficient",
|
||||||
|
"rate_limit": limit_value,
|
||||||
|
"error": "Authenticated request did not elevate GitHub rate limits",
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.status_code == 401:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"message": "API Token Invalid",
|
||||||
|
"status": "invalid",
|
||||||
|
"rate_limit": limit_value,
|
||||||
|
"error": resp.text[:200],
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"message": f"GitHub API error (HTTP {resp.status_code})",
|
||||||
|
"status": "error",
|
||||||
|
"rate_limit": limit_value,
|
||||||
|
"error": resp.text[:200],
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"message": f"API Token validation error: {exc}",
|
||||||
|
"status": "error",
|
||||||
|
"rate_limit": None,
|
||||||
|
"error": str(exc),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _hydrate_repo_hash_cache_from_disk() -> None:
|
def _hydrate_repo_hash_cache_from_disk() -> None:
|
||||||
try:
|
try:
|
||||||
@@ -226,7 +354,7 @@ def _fetch_repo_head(owner_repo: str, branch: str = 'main', *, ttl_seconds: int
|
|||||||
'Accept': 'application/vnd.github+json',
|
'Accept': 'application/vnd.github+json',
|
||||||
'User-Agent': 'Borealis-Server'
|
'User-Agent': 'Borealis-Server'
|
||||||
}
|
}
|
||||||
token = os.environ.get('BOREALIS_GITHUB_TOKEN') or os.environ.get('GITHUB_TOKEN')
|
token = _github_api_token(force_refresh=force_refresh)
|
||||||
if token:
|
if token:
|
||||||
headers['Authorization'] = f'Bearer {token}'
|
headers['Authorization'] = f'Bearer {token}'
|
||||||
|
|
||||||
@@ -1891,6 +2019,74 @@ def api_credentials_detail(credential_id: int):
|
|||||||
return jsonify({"credential": record})
|
return jsonify({"credential": record})
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Section: Access Management - GitHub Token
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/github/token", methods=["GET", "POST"])
|
||||||
|
def api_github_token():
|
||||||
|
chk = _require_admin()
|
||||||
|
if chk:
|
||||||
|
return chk
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
token = _load_github_token_from_db(force_refresh=True)
|
||||||
|
verify = _verify_github_token(token)
|
||||||
|
message = verify.get("message") or ("API Token Invalid" if token else "API Token Not Configured")
|
||||||
|
return jsonify({
|
||||||
|
"token": token or "",
|
||||||
|
"has_token": bool(token),
|
||||||
|
"valid": bool(verify.get("valid")),
|
||||||
|
"message": message,
|
||||||
|
"status": verify.get("status") or ("missing" if not token else "unknown"),
|
||||||
|
"rate_limit": verify.get("rate_limit"),
|
||||||
|
"error": verify.get("error"),
|
||||||
|
"checked_at": _now_ts(),
|
||||||
|
})
|
||||||
|
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
token = str(data.get("token") or "").strip()
|
||||||
|
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = _db_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("DELETE FROM github_token")
|
||||||
|
if token:
|
||||||
|
cur.execute("INSERT INTO github_token (token) VALUES (?)", (token,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
except Exception as exc:
|
||||||
|
if conn:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return jsonify({"error": f"Failed to store token: {exc}"}), 500
|
||||||
|
|
||||||
|
_set_cached_github_token(token or None)
|
||||||
|
|
||||||
|
verify = _verify_github_token(token or None)
|
||||||
|
message = verify.get("message") or ("API Token Invalid" if token else "API Token Not Configured")
|
||||||
|
|
||||||
|
try:
|
||||||
|
eventlet.spawn_n(_refresh_default_repo_hash, True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"token": token,
|
||||||
|
"has_token": bool(token),
|
||||||
|
"valid": bool(verify.get("valid")),
|
||||||
|
"message": message,
|
||||||
|
"status": verify.get("status") or ("missing" if not token else "unknown"),
|
||||||
|
"rate_limit": verify.get("rate_limit"),
|
||||||
|
"error": verify.get("error"),
|
||||||
|
"checked_at": _now_ts(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Section: Server-Side Ansible Execution
|
# Section: Server-Side Ansible Execution
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -3564,10 +3760,6 @@ registered_agents: Dict[str, Dict] = {}
|
|||||||
agent_configurations: Dict[str, Dict] = {}
|
agent_configurations: Dict[str, Dict] = {}
|
||||||
latest_images: Dict[str, Dict] = {}
|
latest_images: Dict[str, Dict] = {}
|
||||||
|
|
||||||
# Database initialization (merged into a single SQLite database)
|
|
||||||
DB_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "database.db"))
|
|
||||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
||||||
|
|
||||||
DEVICE_TABLE = "devices"
|
DEVICE_TABLE = "devices"
|
||||||
_DEVICE_JSON_LIST_FIELDS = {
|
_DEVICE_JSON_LIST_FIELDS = {
|
||||||
"memory": [],
|
"memory": [],
|
||||||
@@ -4565,6 +4757,15 @@ def init_db():
|
|||||||
pass
|
pass
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS github_token (
|
||||||
|
token TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
# Scheduled jobs table
|
# Scheduled jobs table
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user