import { createToken, detectExtensions, getSourceApi, isAuthenticationError, isNetworkError, } from "./client.svelte.js"; import type { Credentials } from "./credentials.js"; import { deploymentConfig } from "./deploymentConfig.js"; import { loadLib } from "./library.svelte.js"; import { offlineManager } from "./offline/offlineManager.svelte.js"; import { updateBackgroundSync } from "./pwa.svelte.js"; import { removeSourceTracks, syncQueue } from "./queue.svelte.js"; import { effectiveAutoSyncMode } from "./settings.svelte.js"; import { addSource, getSource, removeSource, setActiveSource, sources, updateSource, } from "./sources.svelte.js"; export const auth = $state({ sourceId: null as string | null, loggedIn: false, busy: false, err: null as string | null, user: "", admin: false, authFailed: false, autoLogin: false, }); const setAuthSource = (sourceId: string | null, loggedIn = !!sourceId) => { const source = getSource(sourceId); Object.assign(auth, { sourceId, loggedIn: loggedIn && !!source, user: source?.credentials.username || "", admin: false, err: null, authFailed: false, }); }; const isCurrentSource = (sourceId: string) => sourceId === sources.activeId && auth.sourceId === sourceId; export const markAuthFailed = () => { Object.assign(auth, { loggedIn: false, admin: false, autoLogin: false, authFailed: true, }); offlineManager.setOffline(); }; const loadSessionData = async (sourceId: string, includeQueue = false) => { const source = getSource(sourceId); if (!source) return; let admin = false; const tasks: Promise[] = [loadLib(sourceId)]; if (includeQueue) tasks.push(syncQueue(sourceId)); if (source.credentials.username) { tasks.push( getSourceApi(sourceId) .getUser(source.credentials.username) .then((user) => { admin = user.adminRole === true || user.adminRole === "true"; }) .catch((err) => { console.error("failed to fetch user info:", err); }), ); } await Promise.all(tasks); if (isCurrentSource(sourceId)) auth.admin = admin; }; const activateLoadedSource = async ( sourceId: string, includeQueue = false, checkConnection = false, ) => { if (!getSource(sourceId) || !setActiveSource(sourceId)) return false; setAuthSource(sourceId); await offlineManager.setSource(sourceId); if (checkConnection) { if (!isCurrentSource(sourceId)) return false; await getSourceApi(sourceId).forcePing(); if (!isCurrentSource(sourceId)) return false; offlineManager.setConnected(sourceId); } await detectExtensions(sourceId); if (checkConnection && !isCurrentSource(sourceId)) return false; await loadSessionData(sourceId, includeQueue); return true; }; export const activateSource = async (sourceId: string) => { if (sourceId === sources.activeId && auth.loggedIn) return true; auth.autoLogin = false; try { return await activateLoadedSource( sourceId, false, offlineManager.isNetworkAvailable, ); } catch (err) { if (!isCurrentSource(sourceId)) return false; if (isAuthenticationError(err)) markAuthFailed(); else console.error("failed to activate library:", err); try { await loadSessionData(sourceId); } catch (offlineErr) { console.error("failed to load cached library:", offlineErr); } return false; } }; export const login = async ( serverUrl: string, username: string, secret: string, authMode: "token" | "password" | "apikey" | "auto" = "auto", serverName = "", ) => { if (deploymentConfig.serverLock && deploymentConfig.server) serverUrl = deploymentConfig.server; if (authMode === "apikey") username = ""; serverUrl = serverUrl.trim().replace(/\/+$/, ""); username = username.trim(); const previousSourceId = sources.activeId; const previousAuthState = { sourceId: auth.sourceId, loggedIn: auth.loggedIn, user: auth.user, admin: auth.admin, authFailed: auth.authFailed, }; const wasOffline = offlineManager.isOfflineMode; auth.busy = true; auth.err = null; auth.authFailed = false; const source = addSource(serverName, { server: serverUrl, authMode: authMode === "auto" ? "token" : authMode, username, }); try { const tryAuth = async (mode: "token" | "password" | "apikey") => { const credentials: Credentials = { server: serverUrl, authMode: mode, ...(mode === "apikey" ? { apiKey: secret } : mode === "password" ? { username, password: secret } : { username, ...(await createToken(secret)) }), }; updateSource(source.id, { credentials }); await getSourceApi(source.id).forcePing(); }; if (authMode === "auto") { try { await tryAuth("token"); } catch (err) { if (isNetworkError(err)) throw err; console.warn("token auth failed, trying cleartext:", err); await tryAuth("password"); } } else { await tryAuth(authMode); } offlineManager.setConnected(source.id); await detectExtensions(source.id); await offlineManager.autoSyncQueuedRequests(); if (authMode === "apikey") { try { const info = await getSourceApi(source.id).tokenInfo(); const user = info?.tokenInfo?.username; if (user) { username = user; const current = getSource(source.id); if (current) updateSource(source.id, { credentials: { ...current.credentials, username }, }); } } catch (err) { console.warn("failed to fetch token info:", err); } } await activateLoadedSource(source.id); return true; } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); auth.err = errorMessage; removeSource(source.id); updateBackgroundSync(effectiveAutoSyncMode()); if (previousSourceId) { setActiveSource(previousSourceId); if (wasOffline) offlineManager.setOffline(); await offlineManager.setSource(previousSourceId); } else { offlineManager.setOffline(); await loadLib(null); } Object.assign(auth, previousAuthState, { err: errorMessage }); return false; } finally { auth.busy = false; auth.autoLogin = false; } }; export const removeLibrarySource = async (sourceId: string) => { if (!getSource(sourceId)) return false; const wasActive = sources.activeId === sourceId; try { await offlineManager.removeSourceData(sourceId); } catch (err) { console.error("failed to clear library data:", err); return false; } removeSourceTracks(sourceId); removeSource(sourceId); updateBackgroundSync(effectiveAutoSyncMode()); if (wasActive) { if (sources.activeId) await tryStoredSession(sources.activeId); else { setAuthSource(null, false); offlineManager.setOffline(); await loadLib(null); } } return true; }; export const isOwner = (owner?: string | null) => !owner || (!!auth.user && owner.toLowerCase() === auth.user.toLowerCase()); const tryStoredSession = async (sourceId: string) => { if (!getSource(sourceId) || !setActiveSource(sourceId)) return false; setAuthSource(sourceId); auth.autoLogin = true; offlineManager.setOffline(); try { await offlineManager.setSource(sourceId); if (!isCurrentSource(sourceId)) return false; await getSourceApi(sourceId).forcePing(); if (!isCurrentSource(sourceId)) return false; offlineManager.setConnected(); await detectExtensions(sourceId); if (!isCurrentSource(sourceId)) return false; await offlineManager.autoSyncQueuedRequests(); if (!isCurrentSource(sourceId)) return false; await loadSessionData(sourceId, true); if (!isCurrentSource(sourceId)) return false; return true; } catch (err) { if (!isCurrentSource(sourceId)) return false; if (isAuthenticationError(err)) markAuthFailed(); else if (isNetworkError(err)) auth.err = null; else console.error("failed to restore stored library:", err); try { if (isCurrentSource(sourceId)) await loadSessionData(sourceId, true); } catch (offlineErr) { console.error("failed to load cached library:", offlineErr); } return false; } finally { if (isCurrentSource(sourceId)) auth.autoLogin = false; } }; export const initializeAuth = async () => { if (sources.primaryId) await tryStoredSession(sources.primaryId); }; export const reconnectSession = async (sourceId = auth.sourceId) => { if (!sourceId || !getSource(sourceId) || !isCurrentSource(sourceId)) return; try { await getSourceApi(sourceId).forcePing(); if (!isCurrentSource(sourceId)) return; offlineManager.setConnected(); await detectExtensions(sourceId); if (!isCurrentSource(sourceId)) return; await offlineManager.autoSyncQueuedRequests(); if (!isCurrentSource(sourceId)) return; await loadSessionData(sourceId); if (isCurrentSource(sourceId)) setAuthSource(sourceId); } catch (err) { if (isAuthenticationError(err) && isCurrentSource(sourceId)) markAuthFailed(); else console.error("failed to reconnect library:", err); } }; export const retrySession = (sourceId = auth.sourceId) => sourceId ? tryStoredSession(sourceId) : Promise.resolve(false);