fix: redirect to /library after login instead of /portal (#631)

most users are listeners first, uploaders second - landing on the library
(liked tracks, playlists) is a better default than the portal (artist management).

- change backend OAuth callback to redirect to /library
- add exchange_token handling to library page (same pattern as portal)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

authored by zzstoatzz.io Claude Opus 4.5 and committed by GitHub 6aa3a300 19e24744

Changed files
+41 -3
backend
src
backend
api
frontend
src
routes
library
+2 -2
backend/src/backend/api/auth.py
··· 166 166 # schedule ATProto sync (via docket if enabled, else asyncio) 167 167 await schedule_atproto_sync(session_id, did) 168 168 169 - # redirect to profile setup if needed, otherwise to portal 170 - redirect_path = "/portal" if has_profile else "/profile/setup" 169 + # redirect to profile setup if needed, otherwise to library 170 + redirect_path = "/library" if has_profile else "/profile/setup" 171 171 172 172 return RedirectResponse( 173 173 url=f"{settings.frontend.url}{redirect_path}?exchange_token={exchange_token}",
+39 -1
frontend/src/routes/library/+page.svelte
··· 1 1 <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { replaceState, invalidateAll, goto } from '$app/navigation'; 2 4 import Header from '$lib/components/Header.svelte'; 3 5 import { auth } from '$lib/auth.svelte'; 4 - import { goto } from '$app/navigation'; 6 + import { preferences } from '$lib/preferences.svelte'; 5 7 import { API_URL } from '$lib/config'; 6 8 import type { PageData } from './$types'; 7 9 import type { Playlist } from '$lib/types'; ··· 13 15 let newPlaylistName = $state(''); 14 16 let creating = $state(false); 15 17 let error = $state(''); 18 + 19 + onMount(async () => { 20 + // check if exchange_token is in URL (from OAuth callback) 21 + const params = new URLSearchParams(window.location.search); 22 + const exchangeToken = params.get('exchange_token'); 23 + const isDevToken = params.get('dev_token') === 'true'; 24 + 25 + // redirect dev token callbacks to settings page 26 + if (exchangeToken && isDevToken) { 27 + window.location.href = `/settings?exchange_token=${exchangeToken}&dev_token=true`; 28 + return; 29 + } 30 + 31 + if (exchangeToken) { 32 + // regular login - exchange token for session 33 + try { 34 + const exchangeResponse = await fetch(`${API_URL}/auth/exchange`, { 35 + method: 'POST', 36 + headers: { 'Content-Type': 'application/json' }, 37 + credentials: 'include', 38 + body: JSON.stringify({ exchange_token: exchangeToken }) 39 + }); 40 + 41 + if (exchangeResponse.ok) { 42 + // invalidate all load functions so they rerun with the new session cookie 43 + await invalidateAll(); 44 + await auth.initialize(); 45 + await preferences.fetch(); 46 + } 47 + } catch (_e) { 48 + console.error('failed to exchange token:', _e); 49 + } 50 + 51 + replaceState('/library', {}); 52 + } 53 + }); 16 54 17 55 async function handleLogout() { 18 56 await auth.logout();