Fix: Persistent Security State

This commit is contained in:
Youwes09
2026-04-05 00:59:27 -05:00
parent 8005c82654
commit ee708d85d0
7 changed files with 173 additions and 207 deletions
+12 -15
View File
@@ -16,27 +16,25 @@ function basicHeader(user: string, pass: string): Record<string, string> {
return { Authorization: `Basic ${btoa(`${user}:${pass}`)}` };
}
function buildRequestInit(init: RequestInit, extraHeaders: Record<string, string> = {}): RequestInit {
return {
...init,
credentials: "include",
headers: { ...(init.headers as Record<string, string> ?? {}), ...extraHeaders },
};
}
export async function fetchAuthenticated(
export function fetchAuthenticated(
url: string,
init: RequestInit,
signal?: AbortSignal,
): Promise<Response> {
const mode = store.settings.serverAuthMode ?? "NONE";
const s = store.settings;
if (mode === "BASIC_AUTH") {
const user = s.serverAuthUser?.trim() ?? "";
const pass = s.serverAuthPass?.trim() ?? "";
const headers = user && pass ? basicHeader(user, pass) : {};
return fetch(url, buildRequestInit({ ...init, signal }, headers));
const user = store.settings.serverAuthUser?.trim() ?? "";
const pass = store.settings.serverAuthPass?.trim() ?? "";
return fetch(url, {
...init,
signal,
credentials: "include",
headers: {
...(init.headers as Record<string, string> ?? {}),
...(user && pass ? basicHeader(user, pass) : {}),
},
});
}
return fetch(url, { ...init, signal });
@@ -80,7 +78,6 @@ export async function probeServer(): Promise<"ok" | "auth_required" | "unsupport
});
if (res.ok) {
if (mode !== "NONE") updateSettings({ serverAuthMode: "NONE" });
return "ok";
}
-3
View File
@@ -10,15 +10,12 @@ function getServerUrl(): string {
function gqlUrl(): string { return `${getServerUrl()}/api/graphql`; }
// Returns a clean absolute URL with no embedded credentials.
export function plainThumbUrl(path: string): string {
if (!path) return "";
if (path.startsWith("http")) return path;
return `${getServerUrl()}${path}`;
}
// Same as plainThumbUrl — credentials are never embedded in URLs.
// Auth users load images via getBlobUrl (imageCache.ts) instead.
export function thumbUrl(path: string): string {
return plainThumbUrl(path);
}
+77 -35
View File
@@ -1,49 +1,91 @@
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
import { store } from "../store/state.svelte";
const cache = new Map<string, string>();
const cache = new Map<string, string>();
const inflight = new Map<string, Promise<string>>();
function getAuthHeaders(): Record<string, string> {
const mode = store.settings.serverAuthMode;
if (mode === "BASIC_AUTH") {
const user = store.settings.serverAuthUser?.trim() ?? "";
const pass = store.settings.serverAuthPass?.trim() ?? "";
if (user && pass) {
return { Authorization: `Basic ${btoa(`${user}:${pass}`)}` };
}
}
return {};
const MAX_CONCURRENT = 6;
let active = 0;
interface QueueEntry {
url: string;
priority: number;
resolve: (v: string) => void;
reject: (e: unknown) => void;
}
export async function getBlobUrl(url: string): Promise<string> {
if (!url) return "";
const queue: QueueEntry[] = [];
const cached = cache.get(url);
if (cached) return cached;
function getAuthHeaders(): Record<string, string> {
const user = store.settings.serverAuthUser?.trim() ?? "";
const pass = store.settings.serverAuthPass?.trim() ?? "";
return user && pass ? { Authorization: `Basic ${btoa(`${user}:${pass}`)}` } : {};
}
const existing = inflight.get(url);
if (existing) return existing;
async function doFetch(url: string): Promise<string> {
const res = await tauriFetch(url, { method: "GET", headers: getAuthHeaders() });
if (!res.ok) throw new Error(`${res.status}`);
const blobUrl = URL.createObjectURL(await res.blob());
cache.set(url, blobUrl);
return blobUrl;
}
const promise = tauriFetch(url, {
method: "GET",
headers: getAuthHeaders(),
})
.then(res => {
if (!res.ok) throw new Error(`${res.status}`);
return res.blob();
})
.then(blob => {
const blobUrl = URL.createObjectURL(blob);
cache.set(url, blobUrl);
inflight.delete(url);
return blobUrl;
})
.catch(err => {
inflight.delete(url);
throw err;
});
function drain() {
while (active < MAX_CONCURRENT && queue.length > 0) {
queue.sort((a, b) => b.priority - a.priority);
const entry = queue.shift()!;
active++;
doFetch(entry.url)
.then(entry.resolve, entry.reject)
.finally(() => {
inflight.delete(entry.url);
active--;
drain();
});
}
}
function enqueue(url: string, priority: number): Promise<string> {
const promise = new Promise<string>((resolve, reject) => {
queue.push({ url, priority, resolve, reject });
});
inflight.set(url, promise);
drain();
return promise;
}
export function getBlobUrl(url: string, priority = 0): Promise<string> {
if (!url) return Promise.resolve("");
const cached = cache.get(url);
if (cached) return Promise.resolve(cached);
const existing = inflight.get(url);
if (existing) {
const entry = queue.find(e => e.url === url);
if (entry && priority > entry.priority) entry.priority = priority;
return existing;
}
return enqueue(url, priority);
}
export function preloadBlobUrls(urls: string[], basePriority = 0): void {
urls.forEach((url, i) => {
if (!url || cache.has(url) || inflight.has(url)) return;
enqueue(url, basePriority - i);
});
}
export function revokeBlobUrl(url: string): void {
const blob = cache.get(url);
if (blob) {
URL.revokeObjectURL(blob);
cache.delete(url);
}
}
export function clearBlobCache(): void {
cache.forEach(blob => URL.revokeObjectURL(blob));
cache.clear();
}