From eef410014d8d29e4d9c4b5c4fd0088089ce463bc Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Fri, 21 Aug 2026 18:05:04 +0200 Subject: [PATCH] fix: keep the destination through a first-time signup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Someone who follows a shared hub project without an account signs up, and the OAuth callback sends a first-time user to onboarding — dropping the `rd` it had already read out of localStorage. They finish onboarding in an empty workspace with no sign of what they came to import, and have to go back to the hub and click again. That is the path this feature exists for. The callback now passes `rd` on, and onboarding's two exits honour it instead of hardcoding `/user/workspaces`. Same-origin relative paths only: `//host` is a valid URL that leaves the origin while still starting with `/`, so the guard rejects it rather than bouncing a fresh account off-site. Nothing changes for a signup without `rd`, which is every existing one. Gets the user to the wizard with the project in hand; they still pick a destination on step 1. Having onboarding create the workspace and hand into step 3 is the larger version, not done here. Co-Authored-By: Claude Opus 5 (1M context) --- .../user/(user)/onboarding/+page.svelte | 18 +++++++++++++++--- .../login_callback/[client_name]/+page.svelte | 5 ++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/frontend/src/routes/(root)/(logged)/user/(user)/onboarding/+page.svelte b/frontend/src/routes/(root)/(logged)/user/(user)/onboarding/+page.svelte index b2cbb1685f..0428e61fbb 100644 --- a/frontend/src/routes/(root)/(logged)/user/(user)/onboarding/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/user/(user)/onboarding/+page.svelte @@ -2,6 +2,7 @@ import { ArrowLeft } from 'lucide-svelte' import { UserService } from '$lib/gen/services.gen' import { goto } from '$lib/navigation' + import { page } from '$app/state' import CenteredModal from '$lib/components/CenteredModal.svelte' import { Button } from '$lib/components/common' import Popover from '$lib/components/meltComponents/Popover.svelte' @@ -16,7 +17,7 @@ Building2, Twitter, Youtube, - Bot, + Bot, MessageCircleCode } from 'lucide-svelte' import { sendUserToast } from '$lib/toast' @@ -96,10 +97,21 @@ sendUserToast('Failed to save information: ' + (error?.body || error?.message || error), true) } finally { // do not block users from accessing windmill even if there is an error - goto('/user/workspaces') + goto(onboardingDestination()) } } + /** + * Where to go once onboarding is done. `/user/workspaces` unless the sign-in carried a + * destination — a hub project import, say — in which case that is what the user came for. + * Same-origin relative paths only, so a crafted `?rd=` cannot bounce them off-site. + */ + function onboardingDestination(): string { + const rd = page.url.searchParams.get('rd') + if (rd && rd.startsWith('/') && !rd.startsWith('//')) return rd + return '/user/workspaces' + } + async function skip() { isSubmitting = true try { @@ -110,7 +122,7 @@ console.error('Error skipping onboarding:', error) } finally { // do not block users from accessing windmill even if there is an error - goto('/user/workspaces') + goto(onboardingDestination()) } } diff --git a/frontend/src/routes/user/login_callback/[client_name]/+page.svelte b/frontend/src/routes/user/login_callback/[client_name]/+page.svelte index 1aeb57f223..3d4a4adf7a 100644 --- a/frontend/src/routes/user/login_callback/[client_name]/+page.svelte +++ b/frontend/src/routes/user/login_callback/[client_name]/+page.svelte @@ -64,7 +64,10 @@ try { const globalUserInfo = await UserService.globalWhoami() if (globalUserInfo.first_time_user) { - goto('/user/onboarding') + // `rd` rides along: someone arriving from a shared hub project signs up with a + // destination already in hand, and dropping it here strands them in an empty + // workspace with no sign of what they came to import. + goto(`/user/onboarding${rd ? `?rd=${encodeURIComponent(rd)}` : ''}`) return } } catch (err) {