Rework auth to allow smooth switching

This commit is contained in:
Zerebos
2026-05-15 23:50:19 -04:00
parent 062662781a
commit 0bea9c22cb
3 changed files with 86 additions and 22 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use crate::server::resolve::strip_unc; use crate::server::resolve::strip_unc;
use tauri::Manager; use tauri::Manager;
use std::path::PathBuf;
#[tauri::command] #[tauri::command]
pub fn get_platform_ui_scale(window: tauri::Window) -> f64 { pub fn get_platform_ui_scale(window: tauri::Window) -> f64 {
@@ -97,4 +98,4 @@ pub fn reset_suwayomi_data(app: tauri::AppHandle) -> Result<(), String> {
} }
} }
Ok(()) Ok(())
} }
+60 -10
View File
@@ -9,20 +9,53 @@ export class AuthRequiredError extends Error {
} }
} }
const TOKEN_KEY = "moku_access_token"; const TOKEN_KEY = "moku_access_token_v2";
let _accessToken: string | null = sessionStorage.getItem(TOKEN_KEY); const LEGACY_TOKEN_KEY = "moku_access_token";
interface StoredAccessToken {
base: string;
token: string;
}
let _accessToken: string | null = null;
let _accessTokenBase: string | null = null;
export const uiAuth = { export const uiAuth = {
getToken: () => _accessToken, getToken: () => {
setToken: (t: string) => { _accessToken = t; sessionStorage.setItem(TOKEN_KEY, t); }, const base = getServerBase();
clearToken: () => { _accessToken = null; sessionStorage.removeItem(TOKEN_KEY); }, if (_accessToken && _accessTokenBase === base) return _accessToken;
const stored = readStoredToken();
if (!stored) return null;
if (stored.base !== base) {
sessionStorage.removeItem(TOKEN_KEY);
_accessToken = null;
_accessTokenBase = null;
return null;
}
_accessToken = stored.token;
_accessTokenBase = stored.base;
return _accessToken;
},
setToken: (t: string) => {
const base = getServerBase();
_accessToken = t;
_accessTokenBase = base;
sessionStorage.setItem(TOKEN_KEY, JSON.stringify({ base, token: t }));
sessionStorage.removeItem(LEGACY_TOKEN_KEY);
},
clearToken: () => {
_accessToken = null;
_accessTokenBase = null;
sessionStorage.removeItem(TOKEN_KEY);
sessionStorage.removeItem(LEGACY_TOKEN_KEY);
},
}; };
export const authSession = { export const authSession = {
clearTokens() { uiAuth.clearToken(); }, clearTokens() { uiAuth.clearToken(); },
hasSession(): boolean { hasSession(): boolean {
const mode = store.settings.serverAuthMode ?? "NONE"; const mode = store.settings.serverAuthMode ?? "NONE";
if (mode === "UI_LOGIN") return _accessToken !== null; if (mode === "UI_LOGIN") return uiAuth.getToken() !== null;
return true; return true;
}, },
}; };
@@ -32,6 +65,22 @@ function getServerBase(): string {
return typeof url === "string" && url.trim() ? url.replace(/\/$/, "") : "http://127.0.0.1:4567"; return typeof url === "string" && url.trim() ? url.replace(/\/$/, "") : "http://127.0.0.1:4567";
} }
function readStoredToken(): StoredAccessToken | null {
const raw = sessionStorage.getItem(TOKEN_KEY);
if (raw) {
try {
const parsed = JSON.parse(raw);
if (typeof parsed?.base === "string" && typeof parsed?.token === "string")
return { base: parsed.base, token: parsed.token };
} catch {}
}
const legacy = sessionStorage.getItem(LEGACY_TOKEN_KEY);
if (legacy && legacy.trim()) {
return { base: getServerBase(), token: legacy.trim() };
}
return null;
}
function timeoutSignal(ms: number): AbortSignal { function timeoutSignal(ms: number): AbortSignal {
const controller = new AbortController(); const controller = new AbortController();
setTimeout(() => controller.abort(), ms); setTimeout(() => controller.abort(), ms);
@@ -100,7 +149,7 @@ export async function loginUI(user: string, pass: string): Promise<void> {
const token: string | undefined = json?.data?.login?.accessToken; const token: string | undefined = json?.data?.login?.accessToken;
if (!token) throw new Error(json?.errors?.[0]?.message ?? "Login failed"); if (!token) throw new Error(json?.errors?.[0]?.message ?? "Login failed");
uiAuth.setToken(token); uiAuth.setToken(token);
updateSettings({ serverAuthMode: "UI_LOGIN" }); updateSettings({ serverAuthMode: "UI_LOGIN", serverAuthUser: user, serverAuthPass: "" });
} }
export async function loginBasic(user: string, pass: string): Promise<void> { export async function loginBasic(user: string, pass: string): Promise<void> {
@@ -123,8 +172,9 @@ export async function probeServer(): Promise<"ok" | "auth_required" | "unreachab
const base = getServerBase(); const base = getServerBase();
const mode = store.settings.serverAuthMode ?? "NONE"; const mode = store.settings.serverAuthMode ?? "NONE";
const s = store.settings; const s = store.settings;
const token = uiAuth.getToken();
if (mode === "UI_LOGIN" && !_accessToken) return "auth_required"; if (mode === "UI_LOGIN" && !token) return "auth_required";
try { try {
const headers: Record<string, string> = { "Content-Type": "application/json" }; const headers: Record<string, string> = { "Content-Type": "application/json" };
@@ -132,8 +182,8 @@ export async function probeServer(): Promise<"ok" | "auth_required" | "unreachab
const user = s.serverAuthUser?.trim() ?? ""; const user = s.serverAuthUser?.trim() ?? "";
const pass = s.serverAuthPass?.trim() ?? ""; const pass = s.serverAuthPass?.trim() ?? "";
if (user && pass) Object.assign(headers, basicHeader(user, pass)); if (user && pass) Object.assign(headers, basicHeader(user, pass));
} else if (mode === "UI_LOGIN" && _accessToken) { } else if (mode === "UI_LOGIN" && token) {
Object.assign(headers, bearerHeader(_accessToken)); Object.assign(headers, bearerHeader(token));
} }
const res = await fetch(`${base}/api/graphql`, { const res = await fetch(`${base}/api/graphql`, {
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { store, updateSettings } from "@store/state.svelte"; import { store, updateSettings } from "@store/state.svelte";
import { gql } from "@api/client"; import { gql } from "@api/client";
import { authSession } from "@core/auth"; import { authSession, loginUI } from "@core/auth";
import { GET_SERVER_SECURITY } from "@api/queries/extensions"; import { GET_SERVER_SECURITY } from "@api/queries/extensions";
import { SET_SERVER_AUTH, SET_SOCKS_PROXY, SET_FLARESOLVERR } from "@api/mutations/extensions"; import { SET_SERVER_AUTH, SET_SOCKS_PROXY, SET_FLARESOLVERR } from "@api/mutations/extensions";
@@ -33,6 +33,11 @@
let flareTtl = $state(store.settings.flareSolverrSessionTtl ?? 15); let flareTtl = $state(store.settings.flareSolverrSessionTtl ?? 15);
let flareFallback = $state(store.settings.flareSolverrAsResponseFallback ?? false); let flareFallback = $state(store.settings.flareSolverrAsResponseFallback ?? false);
function normalizeAuthMode(mode: string): "NONE" | "BASIC_AUTH" | "UI_LOGIN" {
if (mode === "BASIC_AUTH" || mode === "UI_LOGIN" || mode === "NONE") return mode;
return "NONE";
}
function showSaved(key: string) { function showSaved(key: string) {
secSaved = key; secError = null; secSaved = key; secError = null;
setTimeout(() => { if (secSaved === key) secSaved = null; }, 2000); setTimeout(() => { if (secSaved === key) secSaved = null; }, 2000);
@@ -53,9 +58,10 @@
flareSolverrAsResponseFallback: boolean; flareSolverrAsResponseFallback: boolean;
}}>(GET_SERVER_SECURITY); }}>(GET_SERVER_SECURITY);
const s = res.settings; const s = res.settings;
authMode = store.settings.serverAuthMode ?? "NONE"; const serverMode = normalizeAuthMode(s.authMode);
authUsername = s.authUsername || store.settings.serverAuthUser || ""; authMode = serverMode;
updateSettings({ serverAuthUser: authUsername }); authUsername = s.authUsername || "";
updateSettings({ serverAuthMode: serverMode, serverAuthUser: authUsername });
socksEnabled = s.socksProxyEnabled; socksHost = s.socksProxyHost; socksEnabled = s.socksProxyEnabled; socksHost = s.socksProxyHost;
socksPort = s.socksProxyPort; socksVersion = s.socksProxyVersion; socksPort = s.socksProxyPort; socksVersion = s.socksProxyVersion;
socksUsername = s.socksProxyUsername; socksUsername = s.socksProxyUsername;
@@ -82,23 +88,30 @@
try { try {
const newUser = authMode !== "NONE" ? authUsername.trim() : ""; const newUser = authMode !== "NONE" ? authUsername.trim() : "";
const newPass = authMode !== "NONE" ? authPassword.trim() : ""; const newPass = authMode !== "NONE" ? authPassword.trim() : "";
await gql(SET_SERVER_AUTH, { authMode, authUsername: newUser, authPassword: newPass }); authSession.clearTokens();
if (authMode === "UI_LOGIN") { if (authMode === "UI_LOGIN") {
authSession.clearTokens(); await loginUI(newUser, newPass);
updateSettings({ serverAuthMode: "UI_LOGIN", serverAuthUser: newUser, serverAuthPass: "" }); updateSettings({ serverAuthMode: "UI_LOGIN", serverAuthUser: newUser, serverAuthPass: "" });
} else if (authMode === "BASIC_AUTH") { } else if (authMode === "BASIC_AUTH") {
updateSettings({ serverAuthMode: "BASIC_AUTH", serverAuthUser: newUser, serverAuthPass: newPass }); updateSettings({ serverAuthMode: "BASIC_AUTH", serverAuthUser: newUser, serverAuthPass: newPass });
} else { } else {
authSession.clearTokens();
updateSettings({ serverAuthMode: "NONE", serverAuthUser: "", serverAuthPass: "" }); updateSettings({ serverAuthMode: "NONE", serverAuthUser: "", serverAuthPass: "" });
} }
await gql(SET_SERVER_AUTH, { authMode, authUsername: newUser, authPassword: newPass });
authPassword = ""; authPassword = "";
showSaved("auth"); showSaved("auth");
} catch (e: any) { } catch (e: any) {
updateSettings({ serverAuthMode: prev.mode, serverAuthUser: prev.user, serverAuthPass: prev.pass }); const msg = e?.message ?? "Failed to save authentication settings";
secError = e?.message ?? "Failed to save authentication settings"; const authMismatch = /unauthorized|unauthenticated|authentication|401/i.test(msg);
if (!authMismatch) {
authSession.clearTokens();
updateSettings({ serverAuthMode: prev.mode, serverAuthUser: prev.user, serverAuthPass: prev.pass });
}
secError = authMismatch
? "Saved local auth settings, but the server rejected the update. Verify your new credentials with the current server configuration."
: msg;
} finally { secLoading = false; } } finally { secLoading = false; }
} }
@@ -223,7 +236,7 @@
</button> </button>
{/if} {/if}
<button class="s-btn s-btn-accent" onclick={saveAuth} <button class="s-btn s-btn-accent" onclick={saveAuth}
disabled={secLoading || (authMode === "BASIC_AUTH" && (!authUsername.trim() || !authPassword.trim()))}> disabled={secLoading || ((authMode === "BASIC_AUTH" || authMode === "UI_LOGIN") && (!authUsername.trim() || !authPassword.trim()))}>
{secLoading ? "Saving…" : secSaved === "auth" ? "Saved ✓" : store.settings.serverAuthMode === "BASIC_AUTH" ? "Update" : authMode === "NONE" ? "Save" : "Enable"} {secLoading ? "Saving…" : secSaved === "auth" ? "Saved ✓" : store.settings.serverAuthMode === "BASIC_AUTH" ? "Update" : authMode === "NONE" ? "Save" : "Enable"}
</button> </button>
</div> </div>