From 714c4574e6a80cf874c56d941523fffad520de5e Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 13 Apr 2026 16:27:21 -0700 Subject: [PATCH 1/5] Prevent [cohort_name] dynamic route from capturing /admin and /login Add a SvelteKit param matcher that rejects reserved route names ("admin", "login") so direct navigation to /admin works correctly instead of being captured by the dynamic [cohort_name] route and redirected to /market. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/params/cohort.ts | 5 +++++ .../{[cohort_name] => [cohort_name=cohort]}/+layout.svelte | 0 .../routes/{[cohort_name] => [cohort_name=cohort]}/+page.ts | 0 .../accounts/+page.svelte | 0 .../auction/+page.svelte | 0 .../docs/+page.svelte | 0 .../docs/[slug]/+page.svelte | 0 .../home/+page.svelte | 0 .../market/+layout.svelte | 0 .../market/+page.svelte | 0 .../market/[id]/+page.svelte | 0 .../options/+page.svelte | 0 .../performance/+page.svelte | 0 .../transfers/+page.svelte | 0 14 files changed, 5 insertions(+) create mode 100644 frontend/src/params/cohort.ts rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/+layout.svelte (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/+page.ts (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/accounts/+page.svelte (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/auction/+page.svelte (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/docs/+page.svelte (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/docs/[slug]/+page.svelte (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/home/+page.svelte (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/market/+layout.svelte (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/market/+page.svelte (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/market/[id]/+page.svelte (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/options/+page.svelte (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/performance/+page.svelte (100%) rename frontend/src/routes/{[cohort_name] => [cohort_name=cohort]}/transfers/+page.svelte (100%) diff --git a/frontend/src/params/cohort.ts b/frontend/src/params/cohort.ts new file mode 100644 index 00000000..cb3e275e --- /dev/null +++ b/frontend/src/params/cohort.ts @@ -0,0 +1,5 @@ +import type { ParamMatcher } from '@sveltejs/kit'; + +export const match: ParamMatcher = (param) => { + return param !== 'admin' && param !== 'login'; +}; diff --git a/frontend/src/routes/[cohort_name]/+layout.svelte b/frontend/src/routes/[cohort_name=cohort]/+layout.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/+layout.svelte rename to frontend/src/routes/[cohort_name=cohort]/+layout.svelte diff --git a/frontend/src/routes/[cohort_name]/+page.ts b/frontend/src/routes/[cohort_name=cohort]/+page.ts similarity index 100% rename from frontend/src/routes/[cohort_name]/+page.ts rename to frontend/src/routes/[cohort_name=cohort]/+page.ts diff --git a/frontend/src/routes/[cohort_name]/accounts/+page.svelte b/frontend/src/routes/[cohort_name=cohort]/accounts/+page.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/accounts/+page.svelte rename to frontend/src/routes/[cohort_name=cohort]/accounts/+page.svelte diff --git a/frontend/src/routes/[cohort_name]/auction/+page.svelte b/frontend/src/routes/[cohort_name=cohort]/auction/+page.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/auction/+page.svelte rename to frontend/src/routes/[cohort_name=cohort]/auction/+page.svelte diff --git a/frontend/src/routes/[cohort_name]/docs/+page.svelte b/frontend/src/routes/[cohort_name=cohort]/docs/+page.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/docs/+page.svelte rename to frontend/src/routes/[cohort_name=cohort]/docs/+page.svelte diff --git a/frontend/src/routes/[cohort_name]/docs/[slug]/+page.svelte b/frontend/src/routes/[cohort_name=cohort]/docs/[slug]/+page.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/docs/[slug]/+page.svelte rename to frontend/src/routes/[cohort_name=cohort]/docs/[slug]/+page.svelte diff --git a/frontend/src/routes/[cohort_name]/home/+page.svelte b/frontend/src/routes/[cohort_name=cohort]/home/+page.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/home/+page.svelte rename to frontend/src/routes/[cohort_name=cohort]/home/+page.svelte diff --git a/frontend/src/routes/[cohort_name]/market/+layout.svelte b/frontend/src/routes/[cohort_name=cohort]/market/+layout.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/market/+layout.svelte rename to frontend/src/routes/[cohort_name=cohort]/market/+layout.svelte diff --git a/frontend/src/routes/[cohort_name]/market/+page.svelte b/frontend/src/routes/[cohort_name=cohort]/market/+page.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/market/+page.svelte rename to frontend/src/routes/[cohort_name=cohort]/market/+page.svelte diff --git a/frontend/src/routes/[cohort_name]/market/[id]/+page.svelte b/frontend/src/routes/[cohort_name=cohort]/market/[id]/+page.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/market/[id]/+page.svelte rename to frontend/src/routes/[cohort_name=cohort]/market/[id]/+page.svelte diff --git a/frontend/src/routes/[cohort_name]/options/+page.svelte b/frontend/src/routes/[cohort_name=cohort]/options/+page.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/options/+page.svelte rename to frontend/src/routes/[cohort_name=cohort]/options/+page.svelte diff --git a/frontend/src/routes/[cohort_name]/performance/+page.svelte b/frontend/src/routes/[cohort_name=cohort]/performance/+page.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/performance/+page.svelte rename to frontend/src/routes/[cohort_name=cohort]/performance/+page.svelte diff --git a/frontend/src/routes/[cohort_name]/transfers/+page.svelte b/frontend/src/routes/[cohort_name=cohort]/transfers/+page.svelte similarity index 100% rename from frontend/src/routes/[cohort_name]/transfers/+page.svelte rename to frontend/src/routes/[cohort_name=cohort]/transfers/+page.svelte From 0a066dfe1b06040f3cc6efe9e99b75ad55e1d098 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 13 Apr 2026 16:42:35 -0700 Subject: [PATCH 2/5] Preserve return URL across Kinde auth redirect on page refresh On full page refresh, kinde.isAuthenticated() returns false, triggering a login redirect through Kinde. After auth completes, Kinde returns to / which auto-redirects to /{cohort}/market, losing the original URL. Save the intended path in sessionStorage before the login redirect and restore it when landing back on /. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/routes/+layout.svelte | 5 +++++ frontend/src/routes/+page.svelte | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index cfc94533..52218b63 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -25,6 +25,11 @@ isAuthenticated = await kinde.isAuthenticated(); isCheckingAuth = false; if (!isAuthenticated) { + // Save the current path so we can return here after login + const currentPath = $page.url.pathname; + if (currentPath !== '/') { + sessionStorage.setItem('postLoginRedirect', currentPath); + } kinde.login(); } }); diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index ce6291e7..3e51873c 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -10,6 +10,16 @@ let error = $state(null); onMount(async () => { + // Check if we need to redirect back to a page after login + if (browser) { + const postLoginRedirect = sessionStorage.getItem('postLoginRedirect'); + if (postLoginRedirect) { + sessionStorage.removeItem('postLoginRedirect'); + goto(postLoginRedirect, { replaceState: true }); + return; + } + } + try { const response = await fetchCohorts(); cohorts = response.cohorts; From 94eb2bae72136f29cc63217a4c6d411436c7d8fc Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 13 Apr 2026 16:46:11 -0700 Subject: [PATCH 3/5] Use Kinde's on_redirect_callback to restore URL after auth round-trip Replace the manual sessionStorage workaround with Kinde's built-in mechanism. The PKCE library already saves window.location.href as appState.kindeOriginUrl when login() is called. By providing an on_redirect_callback, we can redirect back to the original page (e.g. /admin) after the OAuth round-trip instead of staying on / and getting auto-redirected to /{cohort}/market. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/lib/auth.svelte.ts | 16 +++++++++++++++- frontend/src/routes/+layout.svelte | 5 ----- frontend/src/routes/+page.svelte | 10 ---------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/auth.svelte.ts b/frontend/src/lib/auth.svelte.ts index 556d1dde..fa87aac3 100644 --- a/frontend/src/lib/auth.svelte.ts +++ b/frontend/src/lib/auth.svelte.ts @@ -26,7 +26,21 @@ const kindePromise = isTestAuth client_id: PUBLIC_KINDE_CLIENT_ID, domain: PUBLIC_KINDE_DOMAIN, redirect_uri: - PUBLIC_KINDE_REDIRECT_URI || `${window.location.protocol}//${window.location.host}` + PUBLIC_KINDE_REDIRECT_URI || `${window.location.protocol}//${window.location.host}`, + on_redirect_callback: (_user: unknown, appState: Record) => { + // After OAuth callback, navigate to the page the user was on before login. + // Kinde saves window.location.href as appState.kindeOriginUrl when login() is called. + // If it differs from the site root, restore it so refreshing e.g. /admin doesn't + // lose the original URL after the Kinde round-trip. + const origin = appState?.kindeOriginUrl; + if (origin) { + const url = new URL(origin); + if (url.pathname !== '/' && url.pathname !== '') { + window.location.replace(origin); + return; + } + } + } }); })(); diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 52218b63..cfc94533 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -25,11 +25,6 @@ isAuthenticated = await kinde.isAuthenticated(); isCheckingAuth = false; if (!isAuthenticated) { - // Save the current path so we can return here after login - const currentPath = $page.url.pathname; - if (currentPath !== '/') { - sessionStorage.setItem('postLoginRedirect', currentPath); - } kinde.login(); } }); diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 3e51873c..ce6291e7 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -10,16 +10,6 @@ let error = $state(null); onMount(async () => { - // Check if we need to redirect back to a page after login - if (browser) { - const postLoginRedirect = sessionStorage.getItem('postLoginRedirect'); - if (postLoginRedirect) { - sessionStorage.removeItem('postLoginRedirect'); - goto(postLoginRedirect, { replaceState: true }); - return; - } - } - try { const response = await fetchCohorts(); cohorts = response.cohorts; From d50372f86d1ee0b4b7b0f537b787e97bc3e6824e Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 13 Apr 2026 17:01:10 -0700 Subject: [PATCH 4/5] =?UTF-8?q?Fix=20infinite=20redirect=20loop=20?= =?UTF-8?q?=E2=80=94=20use=20sessionStorage=20+=20client-side=20goto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit window.location.replace() causes a full page reload which wipes the Kinde PKCE in-memory token store, restarting the auth flow in a loop. Instead, save the pre-login path to sessionStorage in the Kinde on_redirect_callback and let the root +page.svelte restore it via goto() (client-side navigation that preserves the token store). Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/lib/auth.svelte.ts | 11 +++++------ frontend/src/routes/+page.svelte | 12 ++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/auth.svelte.ts b/frontend/src/lib/auth.svelte.ts index fa87aac3..954dafc7 100644 --- a/frontend/src/lib/auth.svelte.ts +++ b/frontend/src/lib/auth.svelte.ts @@ -28,16 +28,15 @@ const kindePromise = isTestAuth redirect_uri: PUBLIC_KINDE_REDIRECT_URI || `${window.location.protocol}//${window.location.host}`, on_redirect_callback: (_user: unknown, appState: Record) => { - // After OAuth callback, navigate to the page the user was on before login. - // Kinde saves window.location.href as appState.kindeOriginUrl when login() is called. - // If it differs from the site root, restore it so refreshing e.g. /admin doesn't - // lose the original URL after the Kinde round-trip. + // After OAuth callback, save the pre-login URL so the root page can + // restore it via client-side navigation (goto). We can't use + // window.location.replace() here because that would do a full page + // reload, wiping the in-memory token store and causing an infinite loop. const origin = appState?.kindeOriginUrl; if (origin) { const url = new URL(origin); if (url.pathname !== '/' && url.pathname !== '') { - window.location.replace(origin); - return; + sessionStorage.setItem('postLoginRedirect', url.pathname); } } } diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index ce6291e7..ff5b2847 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -10,6 +10,18 @@ let error = $state(null); onMount(async () => { + // After a Kinde auth round-trip, the on_redirect_callback in auth.svelte.ts + // saves the pre-login path here. Use client-side goto() (not window.location) + // so the in-memory token store isn't wiped by a full page reload. + if (browser) { + const postLoginRedirect = sessionStorage.getItem('postLoginRedirect'); + if (postLoginRedirect) { + sessionStorage.removeItem('postLoginRedirect'); + goto(postLoginRedirect, { replaceState: true }); + return; + } + } + try { const response = await fetchCohorts(); cohorts = response.cohorts; From d12cac564a4e13a576b4d5e6acadbd512d814488 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 13 Apr 2026 17:17:10 -0700 Subject: [PATCH 5/5] Persist Kinde refresh token in localStorage to survive page reloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable is_dangerously_use_local_storage so the refresh token persists across page reloads. Without this, the Kinde PKCE library stores tokens only in memory, forcing a full OAuth redirect round-trip on every refresh. With persistent storage, the library silently refreshes tokens via a background API call — no redirect, no URL loss. This replaces the on_redirect_callback + sessionStorage workaround. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/lib/auth.svelte.ts | 14 +------------- frontend/src/routes/+page.svelte | 12 ------------ 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/frontend/src/lib/auth.svelte.ts b/frontend/src/lib/auth.svelte.ts index 954dafc7..d3c809d6 100644 --- a/frontend/src/lib/auth.svelte.ts +++ b/frontend/src/lib/auth.svelte.ts @@ -27,19 +27,7 @@ const kindePromise = isTestAuth domain: PUBLIC_KINDE_DOMAIN, redirect_uri: PUBLIC_KINDE_REDIRECT_URI || `${window.location.protocol}//${window.location.host}`, - on_redirect_callback: (_user: unknown, appState: Record) => { - // After OAuth callback, save the pre-login URL so the root page can - // restore it via client-side navigation (goto). We can't use - // window.location.replace() here because that would do a full page - // reload, wiping the in-memory token store and causing an infinite loop. - const origin = appState?.kindeOriginUrl; - if (origin) { - const url = new URL(origin); - if (url.pathname !== '/' && url.pathname !== '') { - sessionStorage.setItem('postLoginRedirect', url.pathname); - } - } - } + is_dangerously_use_local_storage: true }); })(); diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index ff5b2847..ce6291e7 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -10,18 +10,6 @@ let error = $state(null); onMount(async () => { - // After a Kinde auth round-trip, the on_redirect_callback in auth.svelte.ts - // saves the pre-login path here. Use client-side goto() (not window.location) - // so the in-memory token store isn't wiped by a full page reload. - if (browser) { - const postLoginRedirect = sessionStorage.getItem('postLoginRedirect'); - if (postLoginRedirect) { - sessionStorage.removeItem('postLoginRedirect'); - goto(postLoginRedirect, { replaceState: true }); - return; - } - } - try { const response = await fetchCohorts(); cohorts = response.cohorts;