Phase 1 — multi-user auth: - server/auth.py: bcrypt passwords, itsdangerous signed-cookie sessions, sha256 bearer tokens, FastAPI current_user/require_owner deps, login rate limit - users + api_tokens tables + jobs/assets.user_id (additive migrations) - HARD RULE enforced server-side: guests are local-only — /api/operators filters out requires_env operators, POST /api/jobs 403s cloud ops for non-owners (proven via direct POST in smoke.sh, not just UI). Settings owner-only. auth_secret hidden from the settings API. Per-user active-job cap (owner exempt). Own-asset/ own-job checks. WS auth via cookie or ?token=. Owner bootstrap prints pw once. - mb-ready: bearer MB_TOKEN; scripts/users.py for out-of-band management - Frontend: Login gate, header user chip + logout, guest note, username on jobs, Users panel in Settings (owner) Phase 2 — dashboard: - server/sysinfo.py: psutil CPU/RAM/disk + macmon Apple GPU (util/power/temp, no sudo), computed lane occupancy, 24h job summary, recent jobs w/ output thumbs; all cached (5s stats, 5min du). /api/system + /api/jobs/recent. - Dashboard.jsx: snapshot-on-refresh (no polling) — stat cards, per-core strip, lane strip, running/queued, recent grid. tests/smoke.sh rewritten for auth: 28 checks passing incl. all guest-security rules. Browser-verified owner + guest + dashboard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
3.2 KiB
JavaScript
78 lines
3.2 KiB
JavaScript
const BASE = "";
|
|
|
|
export async function api(path, opts = {}) {
|
|
const res = await fetch(BASE + path, { credentials: "same-origin", ...opts });
|
|
if (res.status === 401) {
|
|
// session gone — tell App to show the login screen
|
|
window.dispatchEvent(new CustomEvent("mb-unauth"));
|
|
throw new Error("401 unauthorized");
|
|
}
|
|
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
|
|
const ct = res.headers.get("content-type") || "";
|
|
return ct.includes("json") ? res.json() : res;
|
|
}
|
|
|
|
// -- auth --------------------------------------------------------------------
|
|
export const getMe = () => api("/api/me");
|
|
export const login = (username, password) =>
|
|
api("/api/login", { method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username, password }) });
|
|
export const logout = () => api("/api/logout", { method: "POST" });
|
|
|
|
export const listUsers = () => api("/api/users");
|
|
export const addUser = (u) =>
|
|
api("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(u) });
|
|
export const patchUser = (username, u) =>
|
|
api(`/api/users/${username}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(u) });
|
|
export const delUser = (username) => api(`/api/users/${username}`, { method: "DELETE" });
|
|
export const createToken = (body) =>
|
|
api("/api/tokens", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body || {}) });
|
|
|
|
export const getSystem = () => api("/api/system");
|
|
export const getRecentJobs = () => api("/api/jobs/recent");
|
|
|
|
export const listAssets = () => api("/api/assets");
|
|
export const listOperators = () => api("/api/operators");
|
|
export const listJobs = () => api("/api/jobs");
|
|
export const deleteAsset = (id) => api(`/api/assets/${id}`, { method: "DELETE" });
|
|
|
|
export async function uploadFile(file) {
|
|
const form = new FormData();
|
|
form.append("file", file, file.name || "pasted.png");
|
|
return api("/api/assets", { method: "POST", body: form });
|
|
}
|
|
|
|
export const runJob = (operator, asset_id, params) =>
|
|
api("/api/jobs", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ operator, asset_id, params }),
|
|
});
|
|
|
|
export const cancelJob = (id) => api(`/api/jobs/${id}/cancel`, { method: "POST" });
|
|
export const retryJob = (id) => api(`/api/jobs/${id}/retry`, { method: "POST" });
|
|
export const deleteJob = (id) => api(`/api/jobs/${id}`, { method: "DELETE" });
|
|
|
|
export const getSettings = () => api("/api/settings");
|
|
export const putSettings = (updates) =>
|
|
api("/api/settings", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(updates),
|
|
});
|
|
|
|
export function connectWS(onMessage) {
|
|
const proto = location.protocol === "https:" ? "wss" : "ws";
|
|
const ws = new WebSocket(`${proto}://${location.host}/ws`);
|
|
ws.onmessage = (e) => onMessage(JSON.parse(e.data));
|
|
const ping = setInterval(() => ws.readyState === 1 && ws.send("ping"), 20000);
|
|
ws.onclose = () => {
|
|
clearInterval(ping);
|
|
setTimeout(() => connectWS(onMessage), 2000);
|
|
};
|
|
return ws;
|
|
}
|
|
|
|
export const assetFileURL = (id, member) =>
|
|
`/api/assets/${id}/file` + (member ? `?member=${encodeURIComponent(member)}` : "");
|