audio streaming app plyr.fm
38
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #781 from zzstoatzz/fix/auth-check-on-navigation

fix: check auth once on mount instead of every navigation

authored by

nate nowack and committed by
GitHub
45357828 b8cced50

+39 -102
+8
frontend/src/lib/auth.svelte.ts
··· 16 16 isAuthenticated = $state(false); 17 17 loading = $state(true); 18 18 scopeUpgradeRequired = $state(false); 19 + private initialized = false; 19 20 20 21 async initialize(): Promise<void> { 21 22 if (!browser) { 22 23 this.loading = false; 23 24 return; 24 25 } 26 + 27 + // only fetch once - subsequent calls are no-ops 28 + if (this.initialized) { 29 + return; 30 + } 31 + this.initialized = true; 25 32 26 33 try { 27 34 const response = await fetch(`${API_URL}/auth/me`, { ··· 61 68 if (!browser) return; 62 69 this.user = null; 63 70 this.isAuthenticated = false; 71 + this.initialized = false; 64 72 } 65 73 66 74 async logout(): Promise<void> {
+12
frontend/src/lib/moderation.svelte.ts
··· 64 64 return checkImageSensitive(url, this.data); 65 65 } 66 66 67 + /** 68 + * initialize from pre-fetched SSR data (avoids redundant API call) 69 + */ 70 + initializeFromData(ssrData: SensitiveImagesData): void { 71 + if (this.initialized) return; 72 + this.initialized = true; 73 + this.data = { 74 + image_ids: new Set(ssrData.image_ids || []), 75 + urls: new Set(ssrData.urls || []) 76 + }; 77 + } 78 + 67 79 async initialize(): Promise<void> { 68 80 if (!browser || this.initialized || this.loading) return; 69 81 this.initialized = true;
+14 -11
frontend/src/routes/+layout.svelte
··· 43 43 // exclude legal pages so users can read full terms/privacy/cookies 44 44 // uses preferences.data (client state) since acceptTerms() updates it 45 45 let showTermsOverlay = $derived( 46 - data.isAuthenticated && 46 + auth.isAuthenticated && 47 47 preferences.data && 48 48 !preferences.data.terms_accepted_at && 49 49 !$page.url.pathname.startsWith('/terms') && ··· 51 51 !$page.url.pathname.startsWith('/cookies') 52 52 ); 53 53 54 - // sync auth and preferences state from layout data (fetched by +layout.ts) 55 - $effect(() => { 56 - if (browser) { 57 - auth.user = data.user; 58 - auth.isAuthenticated = data.isAuthenticated; 59 - auth.loading = false; 60 - preferences.data = data.preferences; 61 - // fetch explicit images list (public, no auth needed) 62 - moderation.initialize(); 63 - if (data.isAuthenticated && queue.revision === null) { 54 + // initialize auth and preferences once on mount (not on every navigation) 55 + // this prevents repeated /auth/me calls for unauthenticated users 56 + onMount(async () => { 57 + // use sensitive images from SSR (avoids redundant API call) 58 + moderation.initializeFromData(data.sensitiveImages); 59 + 60 + // check auth status once 61 + await auth.initialize(); 62 + 63 + // if authenticated, fetch preferences and queue 64 + if (auth.isAuthenticated) { 65 + await preferences.initialize(); 66 + if (queue.revision === null) { 64 67 void queue.fetchQueue(); 65 68 } 66 69 }
+4 -90
frontend/src/routes/+layout.ts
··· 1 - import { browser } from '$app/environment'; 2 - import { API_URL } from '$lib/config'; 3 - import type { User } from '$lib/types'; 4 - import type { Preferences } from '$lib/preferences.svelte'; 5 1 import type { SensitiveImagesData } from '$lib/moderation.svelte'; 6 2 import type { LoadEvent } from '@sveltejs/kit'; 7 3 8 4 export interface LayoutData { 9 - user: User | null; 10 - isAuthenticated: boolean; 11 - preferences: Preferences | null; 12 5 sensitiveImages: SensitiveImagesData; 13 6 } 14 7 15 - const DEFAULT_PREFERENCES: Preferences = { 16 - accent_color: null, 17 - auto_advance: true, 18 - allow_comments: true, 19 - hidden_tags: ['ai'], 20 - theme: 'dark', 21 - enable_teal_scrobbling: false, 22 - teal_needs_reauth: false, 23 - show_sensitive_artwork: false, 24 - show_liked_on_profile: false, 25 - support_url: null, 26 - ui_settings: {}, 27 - auto_download_liked: false, 28 - terms_accepted_at: null 29 - }; 30 - 31 - export async function load({ fetch, data }: LoadEvent): Promise<LayoutData> { 32 - const sensitiveImages = data?.sensitiveImages ?? { image_ids: [], urls: [] }; 33 - 34 - if (!browser) { 35 - return { 36 - user: null, 37 - isAuthenticated: false, 38 - preferences: null, 39 - sensitiveImages 40 - }; 41 - } 42 - 43 - try { 44 - const response = await fetch(`${API_URL}/auth/me`, { 45 - credentials: 'include' 46 - }); 47 - 48 - if (response.ok) { 49 - const user = await response.json(); 50 - 51 - // fetch preferences in parallel once we know user is authenticated 52 - let preferences: Preferences = { ...DEFAULT_PREFERENCES }; 53 - try { 54 - const prefsResponse = await fetch(`${API_URL}/preferences/`, { 55 - credentials: 'include' 56 - }); 57 - if (prefsResponse.ok) { 58 - const prefsData = await prefsResponse.json(); 59 - // auto_download_liked is stored locally, not on server 60 - const storedAutoDownload = typeof localStorage !== 'undefined' 61 - ? localStorage.getItem('autoDownloadLiked') === '1' 62 - : false; 63 - preferences = { 64 - accent_color: prefsData.accent_color ?? null, 65 - auto_advance: prefsData.auto_advance ?? true, 66 - allow_comments: prefsData.allow_comments ?? true, 67 - hidden_tags: prefsData.hidden_tags ?? ['ai'], 68 - theme: prefsData.theme ?? 'dark', 69 - enable_teal_scrobbling: prefsData.enable_teal_scrobbling ?? false, 70 - teal_needs_reauth: prefsData.teal_needs_reauth ?? false, 71 - show_sensitive_artwork: prefsData.show_sensitive_artwork ?? false, 72 - show_liked_on_profile: prefsData.show_liked_on_profile ?? false, 73 - support_url: prefsData.support_url ?? null, 74 - ui_settings: prefsData.ui_settings ?? {}, 75 - auto_download_liked: storedAutoDownload, 76 - terms_accepted_at: prefsData.terms_accepted_at ?? null 77 - }; 78 - } 79 - } catch (e) { 80 - console.error('preferences fetch failed:', e); 81 - } 82 - 83 - return { 84 - user, 85 - isAuthenticated: true, 86 - preferences, 87 - sensitiveImages 88 - }; 89 - } 90 - } catch (e) { 91 - console.error('auth check failed:', e); 92 - } 93 - 8 + // auth and preferences are handled client-side via singletons 9 + // this load function just passes through server data (sensitive images) 10 + export async function load({ data }: LoadEvent): Promise<LayoutData> { 94 11 return { 95 - user: null, 96 - isAuthenticated: false, 97 - preferences: null, 98 - sensitiveImages 12 + sensitiveImages: data?.sensitiveImages ?? { image_ids: [], urls: [] } 99 13 }; 100 14 }
+1 -1
loq.toml
··· 126 126 127 127 [[rules]] 128 128 path = "frontend/src/routes/+layout.svelte" 129 - max_lines = 676 129 + max_lines = 680 130 130 131 131 [[rules]] 132 132 path = "frontend/src/routes/costs/+page.svelte"