Add security improvements: refresh tokens, passkeys, CSP, audit logging, TOTP encryption
- Shorten access token to 30min with 7-day refresh token flow - Add WebAuthn passkey registration and login with TOTP fallback - Add Content-Security-Policy header - Add audit logging middleware to /volume1/docker/nas-dashboard/audit.log - Block /volume1/docker/ in files endpoint - Encrypt TOTP secret at rest with Fernet (derived from SECRET_KEY) - New deps: py_webauthn, cryptography
This commit is contained in:
@@ -11,6 +11,42 @@ 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") || "";
|
||||
}
|
||||
|
||||
let refreshPromise = null;
|
||||
|
||||
async function tryRefresh() {
|
||||
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 }),
|
||||
});
|
||||
if (!r.ok) return false;
|
||||
const data = await r.json();
|
||||
setToken(data.access_token);
|
||||
setRefreshToken(data.refresh_token);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
refreshPromise = null;
|
||||
}
|
||||
})();
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
async function request(path, opts = {}) {
|
||||
const headers = opts.headers || {};
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
@@ -22,15 +58,19 @@ async function request(path, opts = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await fetch(BASE + path, { ...opts, headers });
|
||||
let r = await fetch(BASE + path, { ...opts, headers });
|
||||
|
||||
if (r.status === 401 && !path.includes("/auth/refresh")) {
|
||||
const refreshed = await tryRefresh();
|
||||
if (refreshed) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
r = await fetch(BASE + path, { ...opts, headers });
|
||||
}
|
||||
}
|
||||
|
||||
if (r.status === 401) {
|
||||
// Session expired or invalid
|
||||
setToken("");
|
||||
// Only reload if not already on login page (simple check)
|
||||
if (!location.pathname.includes("login") && !token) {
|
||||
// Let the app handle redirect, but clearer is to throw
|
||||
}
|
||||
setRefreshToken("");
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user