fix: harden dashboard RBAC and cookie auth flows
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 2m17s

Restrict Docker/Files writes to admins, move terminal websocket auth to cookie-first with temporary query fallback, and migrate refresh-token handling to httpOnly cookies for safer session persistence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gan, Jimmy
2026-03-09 23:44:23 +08:00
parent e72665c466
commit 4201917e17
11 changed files with 185 additions and 115 deletions
+18 -30
View File
@@ -1,5 +1,5 @@
const BASE = "/api";
let token = localStorage.getItem("token") || "";
let token = "";
// Current user info populated after auth
export let currentUser = { username: "", role: "", pages: [] };
@@ -18,33 +18,24 @@ export function getToken() {
return token;
}
export function setRefreshToken(t) {
if (t) localStorage.setItem("refresh_token", t);
else localStorage.removeItem("refresh_token");
}
function getRefreshToken() {
return localStorage.getItem("refresh_token") || "";
}
// Refresh token is cookie-managed server-side.
export function setRefreshToken() {}
let refreshPromise = null;
async function tryRefresh() {
export async function tryRefreshSession() {
if (refreshPromise) return refreshPromise;
const rt = getRefreshToken();
if (!rt) return false;
refreshPromise = (async () => {
try {
const r = await fetch(BASE + "/auth/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: rt }),
credentials: "same-origin",
});
if (!r.ok) return false;
const data = await r.json();
setToken(data.access_token);
setRefreshToken(data.refresh_token);
return true;
setToken(data.access_token || "");
return !!data.access_token;
} catch {
return false;
} finally {
@@ -54,15 +45,6 @@ async function tryRefresh() {
return refreshPromise;
}
export async function getWebSocketToken(forceRefresh = false) {
if (!forceRefresh && token) return token;
const refreshed = await tryRefresh();
if (refreshed && token) return token;
setToken("");
setRefreshToken("");
return "";
}
async function request(path, opts = {}) {
const headers = opts.headers || {};
if (token) headers["Authorization"] = `Bearer ${token}`;
@@ -73,27 +55,29 @@ async function request(path, opts = {}) {
delete opts.json;
}
const fetchOpts = { ...opts, headers };
if (path.startsWith("/auth/")) fetchOpts.credentials = "same-origin";
try {
let r = await fetch(BASE + path, { ...opts, headers });
let r = await fetch(BASE + path, fetchOpts);
if (r.status === 401 && !path.includes("/auth/refresh")) {
const refreshed = await tryRefresh();
const refreshed = await tryRefreshSession();
if (refreshed) {
headers["Authorization"] = `Bearer ${token}`;
r = await fetch(BASE + path, { ...opts, headers });
r = await fetch(BASE + path, fetchOpts);
}
}
if (r.status === 401) {
setToken("");
setRefreshToken("");
throw new Error("Unauthorized");
}
if (!r.ok) {
const error = new Error(`HTTP ${r.status}`);
error.status = r.status;
try { error.body = await r.json(); } catch { }
try { error.body = await r.json(); } catch {}
throw error;
}
return r.json();
@@ -140,6 +124,10 @@ export function checkAuth() {
return request("/auth/me");
}
export function logout() {
return request("/auth/logout", { method: "POST" });
}
export function getPreferences() {
return get("/auth/preferences");
}