mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Merge branch 'fix/auth'
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import ThreeDCard from "@shared/manga/ThreeDCard.svelte";
|
||||
import { store, addToast } from "@store/state.svelte";
|
||||
import { cache } from "@core/cache/index";
|
||||
import { getUiAuthDebugStatus, refreshUiAccessToken, type UiAuthDebugStatus } from "@core/auth";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
interface PerfSnapshot { cacheEntries: number; cacheKeys: string[]; oldestEntryMs: number | null; newestEntryMs: number | null; }
|
||||
@@ -12,13 +13,69 @@
|
||||
let appVersion = $state("…");
|
||||
let helloAvailable = $state<boolean | null>(null);
|
||||
let helloBusy = $state(false);
|
||||
let authStatus = $state<UiAuthDebugStatus | null>(null);
|
||||
let authRefreshBusy = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
import("@tauri-apps/api/app").then(m => m.getVersion()).then(v => appVersion = v).catch(() => {});
|
||||
refreshPerfMetrics();
|
||||
refreshAuthStatus();
|
||||
invoke<boolean>("windows_hello_available").then(v => helloAvailable = v).catch(() => helloAvailable = false);
|
||||
|
||||
const timer = setInterval(() => refreshAuthStatus(), 1000);
|
||||
return () => clearInterval(timer);
|
||||
});
|
||||
|
||||
function refreshAuthStatus() {
|
||||
authStatus = getUiAuthDebugStatus();
|
||||
}
|
||||
|
||||
function fmtCountdown(ms: number | null): string {
|
||||
if (ms === null) return "—";
|
||||
if (ms <= 0) return "expired";
|
||||
|
||||
const total = Math.floor(ms / 1000);
|
||||
const month = 30 * 24 * 60 * 60;
|
||||
const day = 24 * 60 * 60;
|
||||
const hour = 60 * 60;
|
||||
const minute = 60;
|
||||
|
||||
const months = Math.floor(total / month);
|
||||
const days = Math.floor((total % month) / day);
|
||||
const hours = Math.floor(total / 3600);
|
||||
const remainingHours = Math.floor((total % day) / hour);
|
||||
const mins = Math.floor((total % hour) / minute);
|
||||
const secs = total % 60;
|
||||
|
||||
if (months > 0) return days > 0 ? `${months}mo ${days}d` : `${months}mo`;
|
||||
if (days > 0) return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
|
||||
if (hours > 0) return `${hours}h ${mins}m ${secs}s`;
|
||||
if (mins > 0) return `${mins}m ${secs}s`;
|
||||
return `${secs}s`;
|
||||
}
|
||||
|
||||
function fmtTime(ts: number | null): string {
|
||||
if (ts === null) return "—";
|
||||
return new Date(ts).toLocaleString([], { dateStyle: "medium", timeStyle: "medium" });
|
||||
}
|
||||
|
||||
async function forceTokenRefresh() {
|
||||
authRefreshBusy = true;
|
||||
try {
|
||||
const token = await refreshUiAccessToken(true);
|
||||
addToast({
|
||||
kind: token ? "success" : "info",
|
||||
title: "UI auth refresh",
|
||||
body: token ? "Refresh succeeded" : "No refreshed token available",
|
||||
});
|
||||
} catch (e: any) {
|
||||
addToast({ kind: "error", title: "UI auth refresh", body: String(e?.message ?? e) });
|
||||
} finally {
|
||||
authRefreshBusy = false;
|
||||
refreshAuthStatus();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPerfMetrics() {
|
||||
let entries = 0, oldest: number | null = null, newest: number | null = null;
|
||||
const foundKeys: string[] = [];
|
||||
@@ -75,7 +132,7 @@
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><span class="s-label">Fire test toast</span><span class="s-desc">Triggers each kind with realistic content</span></div>
|
||||
<div class="s-dev-pill-group">
|
||||
{#each ([["success","S"],["error","E"],["info","I"],["download","D"]] as const) as [kind, label]}
|
||||
{#each ([["success","S"],["error","E"],["info","I"],["download","D"]] as const) as [kind, label] (kind)}
|
||||
<button class="s-dev-pill {kind}" onclick={() => addToast({
|
||||
kind,
|
||||
title: kind === "success" ? "Library updated" : kind === "error" ? "Could not reach server" : kind === "info" ? "Already up to date" : "Download complete",
|
||||
@@ -122,7 +179,7 @@
|
||||
<div class="s-row" style="flex-direction:column;align-items:flex-start;gap:var(--sp-3)">
|
||||
<span class="s-desc">3D tilt cards — hover to preview</span>
|
||||
<div style="display:flex;gap:var(--sp-3)">
|
||||
{#each [{ title: "Berserk", sub: "Ch. 372", hue: "265" },{ title: "Vinland Saga", sub: "Ch. 208", hue: "200" },{ title: "Dungeon Meshi", sub: "Ch. 97", hue: "140" }] as card}
|
||||
{#each [{ title: "Berserk", sub: "Ch. 372", hue: "265" },{ title: "Vinland Saga", sub: "Ch. 208", hue: "200" },{ title: "Dungeon Meshi", sub: "Ch. 97", hue: "140" }] as card (card.title)}
|
||||
<ThreeDCard>
|
||||
<div style="width:72px;height:100px;border-radius:var(--radius-md);background:hsl({card.hue},40%,18%);display:flex;flex-direction:column;align-items:center;justify-content:flex-end;padding:var(--sp-2)">
|
||||
<span style="font-size:var(--text-2xs);color:var(--text-secondary);text-align:center;line-height:1.2">{card.title}</span>
|
||||
@@ -159,4 +216,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s-section">
|
||||
<p class="s-section-title">Auth (UI Login)</p>
|
||||
<div class="s-section-body">
|
||||
<div class="s-dev-grid">
|
||||
<span class="s-dev-key">Mode</span> <span class="s-dev-val">{authStatus?.mode ?? "—"}</span>
|
||||
<span class="s-dev-key">Session</span> <span class="s-dev-val">{authStatus?.hasSession ? "present" : "none"}</span>
|
||||
<span class="s-dev-key">Refresh token</span> <span class="s-dev-val">{authStatus?.hasRefreshToken ? "present" : "none"}</span>
|
||||
<span class="s-dev-key">Access expires in</span> <span class="s-dev-val">{fmtCountdown(authStatus?.accessExpiresInMs ?? null)}</span>
|
||||
<span class="s-dev-key">Refresh expires in</span> <span class="s-dev-val">{fmtCountdown(authStatus?.refreshExpiresInMs ?? null)}</span>
|
||||
<span class="s-dev-key">Refresh window</span> <span class="s-dev-val">{authStatus?.shouldRefreshSoon ? "open" : "not yet"}</span>
|
||||
<span class="s-dev-key">Refresh in-flight</span> <span class="s-dev-val">{authStatus?.refreshInFlight ? "yes" : "no"}</span>
|
||||
</div>
|
||||
<div class="s-row">
|
||||
<div class="s-row-info">
|
||||
<span class="s-desc">Access expiry at: {fmtTime(authStatus?.accessExpiresAt ?? null)}</span>
|
||||
<span class="s-desc">Refresh expiry at: {fmtTime(authStatus?.refreshExpiresAt ?? null)}</span>
|
||||
<span class="s-desc">Skew window: {Math.round((authStatus?.skewMs ?? 0) / 1000)}s before expiry</span>
|
||||
</div>
|
||||
<div class="s-btn-row">
|
||||
<button class="s-btn" onclick={refreshAuthStatus}>Refresh</button>
|
||||
<button class="s-btn s-btn-accent" onclick={forceTokenRefresh} disabled={authRefreshBusy || authStatus?.mode !== "UI_LOGIN" || !authStatus?.hasRefreshToken}>
|
||||
{authRefreshBusy ? "Refreshing…" : "Force refresh"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { store, updateSettings } from "@store/state.svelte";
|
||||
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 { SET_SERVER_AUTH, SET_SOCKS_PROXY, SET_FLARESOLVERR } from "@api/mutations/extensions";
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
let flareTtl = $state(store.settings.flareSolverrSessionTtl ?? 15);
|
||||
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) {
|
||||
secSaved = key; secError = null;
|
||||
setTimeout(() => { if (secSaved === key) secSaved = null; }, 2000);
|
||||
@@ -53,9 +58,10 @@
|
||||
flareSolverrAsResponseFallback: boolean;
|
||||
}}>(GET_SERVER_SECURITY);
|
||||
const s = res.settings;
|
||||
authMode = store.settings.serverAuthMode ?? "NONE";
|
||||
authUsername = s.authUsername || store.settings.serverAuthUser || "";
|
||||
updateSettings({ serverAuthUser: authUsername });
|
||||
const serverMode = normalizeAuthMode(s.authMode);
|
||||
authMode = serverMode;
|
||||
authUsername = s.authUsername || "";
|
||||
updateSettings({ serverAuthMode: serverMode, serverAuthUser: authUsername });
|
||||
socksEnabled = s.socksProxyEnabled; socksHost = s.socksProxyHost;
|
||||
socksPort = s.socksProxyPort; socksVersion = s.socksProxyVersion;
|
||||
socksUsername = s.socksProxyUsername;
|
||||
@@ -82,23 +88,30 @@
|
||||
try {
|
||||
const newUser = authMode !== "NONE" ? authUsername.trim() : "";
|
||||
const newPass = authMode !== "NONE" ? authPassword.trim() : "";
|
||||
await gql(SET_SERVER_AUTH, { authMode, authUsername: newUser, authPassword: newPass });
|
||||
|
||||
authSession.clearTokens();
|
||||
if (authMode === "UI_LOGIN") {
|
||||
authSession.clearTokens();
|
||||
await loginUI(newUser, newPass);
|
||||
updateSettings({ serverAuthMode: "UI_LOGIN", serverAuthUser: newUser, serverAuthPass: "" });
|
||||
} else if (authMode === "BASIC_AUTH") {
|
||||
updateSettings({ serverAuthMode: "BASIC_AUTH", serverAuthUser: newUser, serverAuthPass: newPass });
|
||||
} else {
|
||||
authSession.clearTokens();
|
||||
updateSettings({ serverAuthMode: "NONE", serverAuthUser: "", serverAuthPass: "" });
|
||||
}
|
||||
|
||||
await gql(SET_SERVER_AUTH, { authMode, authUsername: newUser, authPassword: newPass });
|
||||
|
||||
authPassword = "";
|
||||
showSaved("auth");
|
||||
} catch (e: any) {
|
||||
updateSettings({ serverAuthMode: prev.mode, serverAuthUser: prev.user, serverAuthPass: prev.pass });
|
||||
secError = e?.message ?? "Failed to save authentication settings";
|
||||
const msg = 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; }
|
||||
}
|
||||
|
||||
@@ -223,7 +236,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
<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"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user