fix: harden dashboard RBAC and cookie auth flows
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 2m17s
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:
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user