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:
Gan, Jimmy
2026-02-22 10:54:23 +08:00
parent a10cb50ca0
commit be1f3b3ad6
10 changed files with 484 additions and 46 deletions
+46 -6
View File
@@ -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");
}