From ea62e70f44c1339cd383c0f50e6c0306f343003c Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Sun, 29 Mar 2026 12:49:40 -0700 Subject: [PATCH] Fix dashboard loading bug --- .../components-page/stack-handler-client.tsx | 12 +++---- .../src/lib/stack-app/url-targets.test.ts | 36 ++++++++++++++++++- .../template/src/lib/stack-app/url-targets.ts | 29 +++++++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/packages/template/src/components-page/stack-handler-client.tsx b/packages/template/src/components-page/stack-handler-client.tsx index e0558df41..d7af8fad2 100644 --- a/packages/template/src/components-page/stack-handler-client.tsx +++ b/packages/template/src/components-page/stack-handler-client.tsx @@ -8,7 +8,7 @@ import { useMemo } from 'react'; import { SignIn, SignUp, StackServerApp } from ".."; import { useStackApp } from "../lib/hooks"; import { HandlerUrls, StackClientApp, stackAppInternalsSymbol } from "../lib/stack-app"; -import { resolveUnknownHandlerPathFallbackUrl } from "../lib/stack-app/url-targets"; +import { isLocalHandlerUrlTarget, resolveUnknownHandlerPathFallbackUrl } from "../lib/stack-app/url-targets"; import { AccountSettings } from "./account-settings"; import { CliAuthConfirmation } from "./cli-auth-confirm"; import { EmailVerification } from "./email-verification"; @@ -260,11 +260,11 @@ export function StackHandlerClient(props: BaseHandlerProps & Partial if (isCrossDomainLocalOauthCallback) { return; } - const urlObject = new URL(url, placeholderOrigin); - const isHandlerPathTarget = urlObject.pathname === handlerPath || urlObject.pathname.startsWith(`${handlerPath}/`); - const isLocalHandlerTarget = typeof window === "undefined" - ? isHandlerPathTarget - : urlObject.origin === window.location.origin && isHandlerPathTarget; + const isLocalHandlerTarget = isLocalHandlerUrlTarget({ + targetUrl: url, + handlerPath, + currentOrigin: typeof window === "undefined" ? undefined : window.location.origin, + }); if (isLocalHandlerTarget) { return; } diff --git a/packages/template/src/lib/stack-app/url-targets.test.ts b/packages/template/src/lib/stack-app/url-targets.test.ts index e9333c69a..b327da3bc 100644 --- a/packages/template/src/lib/stack-app/url-targets.test.ts +++ b/packages/template/src/lib/stack-app/url-targets.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { resolveHandlerUrls, resolveUnknownHandlerPathFallbackUrl } from "./url-targets"; +import { isLocalHandlerUrlTarget, resolveHandlerUrls, resolveUnknownHandlerPathFallbackUrl } from "./url-targets"; describe("handler URL targets", () => { afterEach(() => { @@ -112,3 +112,37 @@ describe("handler URL targets", () => { })).toThrowError(/\{projectId\} and \{hostedPath\}/); }); }); + +describe("isLocalHandlerUrlTarget", () => { + it("treats relative handler URLs as local targets", () => { + expect(isLocalHandlerUrlTarget({ + targetUrl: "/handler/sign-in", + handlerPath: "/handler", + currentOrigin: "http://p91.localhost:9101", + })).toBe(true); + }); + + it("treats same-origin absolute handler URLs as local targets", () => { + expect(isLocalHandlerUrlTarget({ + targetUrl: "http://p91.localhost:9101/handler/sign-in", + handlerPath: "/handler", + currentOrigin: "http://p91.localhost:9101", + })).toBe(true); + }); + + it("treats cross-origin absolute handler URLs as non-local targets", () => { + expect(isLocalHandlerUrlTarget({ + targetUrl: "https://project-id.built-with-stack-auth.com/handler/sign-in", + handlerPath: "/handler", + currentOrigin: "http://p91.localhost:9101", + })).toBe(false); + }); + + it("treats non-handler paths as non-local targets", () => { + expect(isLocalHandlerUrlTarget({ + targetUrl: "/projects", + handlerPath: "/handler", + currentOrigin: "http://p91.localhost:9101", + })).toBe(false); + }); +}); diff --git a/packages/template/src/lib/stack-app/url-targets.ts b/packages/template/src/lib/stack-app/url-targets.ts index 8e5394fcc..a758fa7cf 100644 --- a/packages/template/src/lib/stack-app/url-targets.ts +++ b/packages/template/src/lib/stack-app/url-targets.ts @@ -6,6 +6,8 @@ import { DefaultHandlerUrlTarget, HandlerPageUrls, HandlerUrlOptions, HandlerUrl const defaultHostedHandlerDomainSuffix = ".built-with-stack-auth.com"; const hostedHandlerProjectIdPlaceholder = "{projectId}"; const hostedHandlerPathPlaceholder = "{hostedPath}"; +const localUrlPlaceholderOrigin = "http://example.com"; +const schemePrefixRegex = /^[a-zA-Z][a-zA-Z\d+\-.]*:/; type CustomPagePrompt = { title: string, @@ -216,6 +218,33 @@ export const getHostedHandlerUrl = (options: { projectId: string, pagePath: stri return new URL(templateFilled).toString(); }; +const isRelativeUrlString = (url: string): boolean => { + if (url.startsWith("//")) { + return false; + } + return !schemePrefixRegex.test(url); +}; + +export const isLocalHandlerUrlTarget = (options: { + targetUrl: string, + handlerPath: string, + currentOrigin?: string, +}): boolean => { + const urlObject = new URL(options.targetUrl, localUrlPlaceholderOrigin); + const isHandlerPathTarget = urlObject.pathname === options.handlerPath + || urlObject.pathname.startsWith(`${options.handlerPath}/`); + if (!isHandlerPathTarget) { + return false; + } + + // On server we only have path information, so treat matching handler paths as local. + if (options.currentOrigin == null) { + return true; + } + + return isRelativeUrlString(options.targetUrl) || urlObject.origin === options.currentOrigin; +}; + const resolveUrlTarget = (options: { target: HandlerUrlTarget, fallbackPath: string,