mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Deprecate app.urls
This commit is contained in:
parent
766fd02a47
commit
9197d4f32b
@ -82,7 +82,7 @@ it("adds secure cross-domain handoff parameters when redirecting to hosted sign-
|
||||
});
|
||||
});
|
||||
|
||||
it("includes redirect-back params in app.urls.signIn for hosted flows", async ({ expect }) => {
|
||||
it("returns static app.urls.signIn for hosted flows", async ({ expect }) => {
|
||||
await withHostedDomainSuffix(async () => {
|
||||
const projectId = "44444444-4444-4444-8444-444444444444";
|
||||
const currentHref = `${localRedirectUrl}/private-page?foo=bar`;
|
||||
@ -102,19 +102,10 @@ it("includes redirect-back params in app.urls.signIn for hosted flows", async ({
|
||||
const signInUrl = new URL(clientApp.urls.signIn);
|
||||
expect(signInUrl.origin).toBe(`https://${projectId}.example-stack-hosted.test`);
|
||||
expect(signInUrl.pathname).toBe("/handler/sign-in");
|
||||
expect(signInUrl.searchParams.get("stack_cross_domain_after_callback_redirect_url")).toBe(currentHref);
|
||||
|
||||
const callbackUrl = new URL(signInUrl.searchParams.get("after_auth_return_to") ?? "");
|
||||
expect(callbackUrl.origin).toBe(new URL(localRedirectUrl).origin);
|
||||
expect(callbackUrl.pathname).toBe("/handler/oauth-callback");
|
||||
expect(callbackUrl.searchParams.get("stack_cross_domain_auth")).toBe("1");
|
||||
|
||||
const maybeState = signInUrl.searchParams.get("stack_cross_domain_state");
|
||||
const maybeChallenge = signInUrl.searchParams.get("stack_cross_domain_code_challenge");
|
||||
if (maybeState != null || maybeChallenge != null) {
|
||||
expect(maybeState).toEqual(expect.any(String));
|
||||
expect(maybeChallenge).toEqual(expect.any(String));
|
||||
}
|
||||
expect(signInUrl.searchParams.get("after_auth_return_to")).toBeNull();
|
||||
expect(signInUrl.searchParams.get("stack_cross_domain_state")).toBeNull();
|
||||
expect(signInUrl.searchParams.get("stack_cross_domain_code_challenge")).toBeNull();
|
||||
expect(signInUrl.searchParams.get("stack_cross_domain_after_callback_redirect_url")).toBeNull();
|
||||
} finally {
|
||||
globalThis.window = previousWindow;
|
||||
globalThis.document = previousDocument;
|
||||
@ -122,7 +113,7 @@ it("includes redirect-back params in app.urls.signIn for hosted flows", async ({
|
||||
});
|
||||
});
|
||||
|
||||
it("includes after_auth_return_to in app.urls.signOut for hosted flows", async ({ expect }) => {
|
||||
it("returns static app.urls.signOut for hosted flows", async ({ expect }) => {
|
||||
await withHostedDomainSuffix(async () => {
|
||||
const projectId = "55555555-5555-4555-8555-555555555555";
|
||||
const currentHref = `${localRedirectUrl}/signed-in-page?foo=bar`;
|
||||
@ -142,7 +133,7 @@ it("includes after_auth_return_to in app.urls.signOut for hosted flows", async (
|
||||
const signOutUrl = new URL(clientApp.urls.signOut);
|
||||
expect(signOutUrl.origin).toBe(`https://${projectId}.example-stack-hosted.test`);
|
||||
expect(signOutUrl.pathname).toBe("/handler/sign-out");
|
||||
expect(signOutUrl.searchParams.get("after_auth_return_to")).toBe(currentHref);
|
||||
expect(signOutUrl.searchParams.get("after_auth_return_to")).toBeNull();
|
||||
} finally {
|
||||
globalThis.window = previousWindow;
|
||||
globalThis.document = previousDocument;
|
||||
|
||||
@ -149,7 +149,10 @@ Q: Why did EventTracker throw `Reflect.get called on non-object` in JS cookie te
|
||||
A: Partial browser mocks can expose `window` without a real `history` object. Calling `Reflect.get(historyObject, "pushState")` throws before type checks. Use normal guarded access (`Object.getOwnPropertyDescriptor(window, "history")?.value`) plus type guards for `pushState`/`replaceState`, and patch/restore methods directly without `Reflect`.
|
||||
|
||||
Q: How are custom handler URL target versions validated?
|
||||
A: In `packages/template/src/lib/stack-app/url-targets.ts`, `{ type: "custom", url, version }` always allows `version: 0`. Any non-zero version is only allowed when that version exists in `customPagePrompts[handlerName].versions`; otherwise resolution throws `StackAssertionError` including `supportedVersions`.
|
||||
A: In `packages/template/src/lib/stack-app/url-targets.ts`, custom targets are only allowed for handler names listed in `customPagePrompts` (not for `handler`). For allowed pages, `version: 0` is always accepted and non-zero versions must exist in `customPagePrompts[handlerName].versions`; otherwise an error is thrown.
|
||||
|
||||
Q: What ordering matters for custom handler URL target version checks?
|
||||
A: In `resolveCustomTargetUrl` (`packages/template/src/lib/stack-app/url-targets.ts`), check `version === 0` before handler-name eligibility checks. Otherwise `{ type: "custom", version: 0 }` can be incorrectly rejected for `handler`, breaking legacy string-alias behavior.
|
||||
Q: How should `StackHandlerClient.redirectIfNotHandler` avoid SSR `window` crashes?
|
||||
A: In `packages/template/src/components-page/stack-handler-client.tsx`, parse handler URLs with a placeholder origin (`http://example.com`) and avoid reading `window` on the server path. For SSR, compare only handler path shape; for browser, keep origin+path checks using `window.location.origin`.
|
||||
|
||||
Q: What is the current `app.urls` contract after deprecating runtime URL mutation?
|
||||
A: `app.urls` is now static (`getUrls(...)` only) and no longer injects runtime `after_auth_return_to` / `stack_cross_domain_*` params from `window.location`. For navigation flows, examples and consumers should use `redirectToXyz()` methods instead (for example `redirectToSignIn()` / `redirectToSignOut()`), while tests for hosted flows should assert dynamic params on actual redirect methods, not on `app.urls`.
|
||||
|
||||
@ -10,7 +10,6 @@ export default function CrossDomainHandoffPage() {
|
||||
const user = useUser();
|
||||
|
||||
const currentUrl = typeof window === "undefined" ? "unknown" : window.location.href;
|
||||
const isHostedSignIn = app.urls.signIn.startsWith("https://");
|
||||
const extraRedirectActions: Array<{ label: string, run: () => Promise<void> }> = [
|
||||
{ label: "redirectToAccountSettings()", run: async () => await app.redirectToAccountSettings() },
|
||||
{ label: "redirectToHome()", run: async () => await app.redirectToHome() },
|
||||
@ -26,11 +25,11 @@ export default function CrossDomainHandoffPage() {
|
||||
{ label: "redirectToError()", run: async () => await app.redirectToError() },
|
||||
];
|
||||
const rawUrlActions: Array<{ label: string, href: string }> = [
|
||||
{ label: "Account Settings URL", href: app.urls.accountSettings },
|
||||
{ label: "OAuth Callback URL", href: app.urls.oauthCallback },
|
||||
{ label: "Team Invitation URL", href: app.urls.teamInvitation },
|
||||
{ label: "MFA URL", href: app.urls.mfa },
|
||||
{ label: "Error URL", href: app.urls.error },
|
||||
{ label: "Account Settings URL", href: "/handler/account-settings" },
|
||||
{ label: "OAuth Callback URL", href: "/handler/oauth-callback" },
|
||||
{ label: "Team Invitation URL", href: "/handler/team-invitation" },
|
||||
{ label: "MFA URL", href: "/handler/mfa" },
|
||||
{ label: "Error URL", href: "/handler/error" },
|
||||
];
|
||||
|
||||
return (
|
||||
@ -49,11 +48,10 @@ export default function CrossDomainHandoffPage() {
|
||||
<div className="space-y-2 text-sm">
|
||||
<div><span className="font-semibold">Signed in:</span> {user ? "yes" : "no"}</div>
|
||||
<div><span className="font-semibold">Current URL:</span> <code>{currentUrl}</code></div>
|
||||
<div><span className="font-semibold">Sign-in URL:</span> <code>{app.urls.signIn}</code></div>
|
||||
<div><span className="font-semibold">OAuth callback URL:</span> <code>{app.urls.oauthCallback}</code></div>
|
||||
<div><span className="font-semibold">Sign-in route:</span> <code>/handler/sign-in</code></div>
|
||||
<div><span className="font-semibold">OAuth callback route:</span> <code>/handler/oauth-callback</code></div>
|
||||
<div>
|
||||
<span className="font-semibold">Cross-domain mode:</span>{" "}
|
||||
{isHostedSignIn ? "active (hosted sign-in URL is absolute)" : "inactive (sign-in URL is local/relative)"}
|
||||
<span className="font-semibold">Cross-domain mode:</span> driven by redirect methods and current URL state
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@ -4,11 +4,9 @@ import { UserAvatar, useStackApp, useUser } from '@stackframe/stack';
|
||||
import { Button, buttonVariants, Card, CardContent, CardFooter, CardHeader, Typography } from '@stackframe/stack-ui';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function PageClient() {
|
||||
const user = useUser({ includeRestricted: true });
|
||||
const router = useRouter();
|
||||
const app = useStackApp();
|
||||
|
||||
const authButtons = (
|
||||
@ -18,8 +16,8 @@ export default function PageClient() {
|
||||
<Typography>Try signing in/up with the buttons below!</Typography>
|
||||
<Typography>Also feel free to check out the things on the top right corner.</Typography>
|
||||
<div className='flex gap-2'>
|
||||
<Button onClick={() => router.push(app.urls.signIn)}>Sign In</Button>
|
||||
<Button onClick={() => router.push(app.urls.signUp)}>Sign Up</Button>
|
||||
<Button onClick={async () => await app.redirectToSignIn()}>Sign In</Button>
|
||||
<Button onClick={async () => await app.redirectToSignUp()}>Sign Up</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -72,9 +70,9 @@ export default function PageClient() {
|
||||
<Link href="https://app.stack-auth.com" className={buttonVariants()}>
|
||||
Visit Stack Auth
|
||||
</Link>
|
||||
<Link href={app.urls.signOut} className={buttonVariants({ variant: 'destructive' })}>
|
||||
<Button variant='destructive' onClick={async () => await app.redirectToSignOut()}>
|
||||
Sign Out
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -21,6 +21,11 @@ const testKeys = {
|
||||
};
|
||||
|
||||
const authReturnStorageKey = "turnstile-auth-demo-last-redirect";
|
||||
const handlerRoutes = {
|
||||
oauthCallback: "/handler/oauth-callback",
|
||||
magicLinkCallback: "/handler/magic-link-callback",
|
||||
error: "/handler/error",
|
||||
};
|
||||
|
||||
type FlowResult = {
|
||||
status: "success" | "error" | "info",
|
||||
@ -317,7 +322,7 @@ export default function TurnstileSignupPageClient() {
|
||||
}
|
||||
|
||||
function getOAuthCallbackUrlForTurnstileLab() {
|
||||
const callbackUrl = new URL(getAppAbsoluteUrl(app.urls.oauthCallback));
|
||||
const callbackUrl = new URL(getAppAbsoluteUrl(handlerRoutes.oauthCallback));
|
||||
callbackUrl.searchParams.set("after_auth_return_to", getCurrentRelativeUrl());
|
||||
return callbackUrl.toString();
|
||||
}
|
||||
@ -409,7 +414,7 @@ export default function TurnstileSignupPageClient() {
|
||||
|
||||
async function handleMagicLinkVisibleDrill(): Promise<FlowResult> {
|
||||
const drillEmail = freshEmail();
|
||||
const callbackUrl = getAppAbsoluteUrl(app.urls.magicLinkCallback);
|
||||
const callbackUrl = getAppAbsoluteUrl(handlerRoutes.magicLinkCallback);
|
||||
|
||||
const firstRes = await debugMagicLinkSend(sendRequest, {
|
||||
email: drillEmail,
|
||||
@ -451,7 +456,7 @@ export default function TurnstileSignupPageClient() {
|
||||
codeChallenge: oauthDebugState.codeChallenge,
|
||||
state: oauthDebugState.state,
|
||||
redirectUrl: getOAuthCallbackUrlForTurnstileLab(),
|
||||
errorRedirectUrl: getAppAbsoluteUrl(app.urls.error),
|
||||
errorRedirectUrl: getAppAbsoluteUrl(handlerRoutes.error),
|
||||
turnstileToken: "mock-turnstile-invalid",
|
||||
turnstilePhase: "invisible",
|
||||
});
|
||||
@ -469,7 +474,7 @@ export default function TurnstileSignupPageClient() {
|
||||
codeChallenge: oauthDebugState.codeChallenge,
|
||||
state: oauthDebugState.state,
|
||||
redirectUrl: getOAuthCallbackUrlForTurnstileLab(),
|
||||
errorRedirectUrl: getAppAbsoluteUrl(app.urls.error),
|
||||
errorRedirectUrl: getAppAbsoluteUrl(handlerRoutes.error),
|
||||
turnstileToken: visibleToken,
|
||||
turnstilePhase: "visible",
|
||||
});
|
||||
@ -820,12 +825,12 @@ export default function TurnstileSignupPageClient() {
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardFooter className="flex gap-2">
|
||||
<Link href={app.urls.signUp} className="inline-flex items-center rounded-md border px-4 py-2 text-sm font-medium">
|
||||
<Button variant="secondary" onClick={async () => await app.redirectToSignUp()}>
|
||||
Hosted sign-up
|
||||
</Link>
|
||||
<Link href={app.urls.signIn} className="inline-flex items-center rounded-md border px-4 py-2 text-sm font-medium">
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={async () => await app.redirectToSignIn()}>
|
||||
Hosted sign-in
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -1052,12 +1057,12 @@ export default function TurnstileSignupPageClient() {
|
||||
<Link href="/" className="inline-flex items-center rounded-md border px-4 py-2 text-sm font-medium">
|
||||
Back to home
|
||||
</Link>
|
||||
<Link href={app.urls.signUp} className="inline-flex items-center rounded-md border px-4 py-2 text-sm font-medium">
|
||||
<Button variant="secondary" onClick={async () => await app.redirectToSignUp()}>
|
||||
Open hosted sign-up
|
||||
</Link>
|
||||
<Link href={app.urls.signIn} className="inline-flex items-center rounded-md border px-4 py-2 text-sm font-medium">
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={async () => await app.redirectToSignIn()}>
|
||||
Open hosted sign-in
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -2,19 +2,16 @@
|
||||
|
||||
import { useStackApp, useUser } from '@stackframe/stack';
|
||||
import { Button } from '@stackframe/stack-ui';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function PageClient() {
|
||||
const user = useUser();
|
||||
const router = useRouter();
|
||||
const app = useStackApp();
|
||||
|
||||
const authButtons = (
|
||||
<div className='flex flex-col gap-5 justify-center items-center'>
|
||||
<div className='flex gap-5'>
|
||||
<Button onClick={() => router.push(app.urls.signIn)}>Sign In</Button>
|
||||
<Button onClick={() => router.push('/handler/signup')}>Sign Up</Button>
|
||||
<Button onClick={async () => await app.redirectToSignIn()}>Sign In</Button>
|
||||
<Button onClick={async () => await app.redirectToSignUp()}>Sign Up</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -23,9 +20,9 @@ export default function PageClient() {
|
||||
<div className='flex flex-col items-center justify-center h-full w-full gap-10'>
|
||||
{user ? (
|
||||
<div className='flex flex-col gap-5 justify-center items-center'>
|
||||
<Link href={app.urls.signOut}>
|
||||
<Button variant="secondary" onClick={async () => await app.redirectToSignOut()}>
|
||||
Sign Out
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : authButtons}
|
||||
</div>
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
// setError('Please enter your password');
|
||||
// return;
|
||||
// }
|
||||
// // this will redirect to app.urls.afterSignIn if successful, you can customize it in the StackServerApp constructor
|
||||
// // this redirects to your configured post-sign-in destination on success
|
||||
// const result = await app.signInWithCredential({ email, password });
|
||||
// // It is better to handle each error code separately, but we will just show the error code directly for simplicity here
|
||||
// if (result.status === 'error') {
|
||||
@ -73,7 +73,7 @@ export default function CustomCredentialSignIn() {
|
||||
const app = useStackApp();
|
||||
|
||||
const onSubmit = async () => {
|
||||
// this will redirect to app.urls.afterSignIn if successful, you can customize it in the StackServerApp constructor
|
||||
// this redirects to your configured post-sign-in destination on success
|
||||
const result = await app.sendMagicLinkEmail(email);
|
||||
// It is better to handle each error code separately, but we will just show the error code directly for simplicity here
|
||||
if (result.status === 'error') {
|
||||
|
||||
@ -24,7 +24,7 @@ export default function CustomCredentialSignUp() {
|
||||
setError('Please enter your password');
|
||||
return;
|
||||
}
|
||||
// this will redirect to app.urls.afterSignUp if successful, you can customize it in the StackServerApp constructor
|
||||
// this redirects to your configured post-sign-up destination on success
|
||||
const result = await app.signUpWithCredential({ email, password });
|
||||
// It is better to handle each error code separately, but we will just show the error code directly for simplicity here
|
||||
if (result.status === 'error') {
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import { createSupabaseClient } from "@/utils/supabase-client";
|
||||
import { useStackApp, useUser } from "@stackframe/stack";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
@ -28,9 +27,9 @@ export default function Page() {
|
||||
<>
|
||||
<p>You are signed in</p>
|
||||
<p>User ID: {user.id}</p>
|
||||
<Link href={app.urls.signOut}>Sign Out</Link>
|
||||
<button onClick={async () => await app.redirectToSignOut()}>Sign Out</button>
|
||||
</> :
|
||||
<Link href={app.urls.signIn}>Sign In</Link>
|
||||
<button onClick={async () => await app.redirectToSignIn()}>Sign In</button>
|
||||
}
|
||||
<h3>Supabase data</h3>
|
||||
<ul>{listContent}</ul>
|
||||
|
||||
@ -65,6 +65,8 @@ const availablePaths = {
|
||||
onboarding: 'onboarding',
|
||||
} as const;
|
||||
|
||||
const placeholderOrigin = "http://example.com";
|
||||
|
||||
const pathAliases = {
|
||||
// also includes the uppercase and non-dashed versions
|
||||
...Object.fromEntries(Object.entries(availablePaths).map(([key, value]) => [value, value])),
|
||||
@ -258,14 +260,16 @@ export function StackHandlerClient(props: BaseHandlerProps & Partial<RouteProps>
|
||||
if (isCrossDomainLocalOauthCallback) {
|
||||
return;
|
||||
}
|
||||
const urlObject = new URL(url, window.location.origin);
|
||||
const isLocalHandlerTarget = urlObject.origin === window.location.origin
|
||||
&& (urlObject.pathname === handlerPath || urlObject.pathname.startsWith(`${handlerPath}/`));
|
||||
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;
|
||||
if (isLocalHandlerTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const urlObj = new URL(url, "http://example.com");
|
||||
const urlObj = new URL(url, placeholderOrigin);
|
||||
for (const [key, value] of Object.entries(searchParams)) {
|
||||
urlObj.searchParams.set(key, value);
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ import { StackClientApp, StackClientAppConstructorOptions, StackClientAppJson }
|
||||
import { _StackAdminAppImplIncomplete } from "./admin-app-impl";
|
||||
import { TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getAnalyticsBaseUrl, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls, resolveConstructorOptions } from "./common";
|
||||
import { EventTracker } from "./event-tracker";
|
||||
import { crossDomainAuthQueryParams, getCrossDomainHandoffParamsFromCurrentUrl, planRedirectToHandler, resolveAppUrlsForCurrentPage } from "./redirect-page-urls";
|
||||
import { crossDomainAuthQueryParams, getCrossDomainHandoffParamsFromCurrentUrl, planRedirectToHandler } from "./redirect-page-urls";
|
||||
import type { CrossDomainHandoffParams } from "./redirect-page-urls";
|
||||
import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson } from "./session-replay";
|
||||
|
||||
@ -2244,20 +2244,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
}
|
||||
|
||||
get urls(): Readonly<ResolvedHandlerUrls> {
|
||||
const resolved = getUrls(this._urlOptions, { projectId: this.projectId });
|
||||
if (isReactServer || typeof window === "undefined") {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const localOAuthCallbackUrl = this._getLocalOAuthCallbackHandlerUrl();
|
||||
const crossDomainHandoffParams = this._getCrossDomainHandoffParamsForUrlsGetter(currentUrl);
|
||||
return resolveAppUrlsForCurrentPage({
|
||||
resolvedUrls: resolved,
|
||||
currentUrl,
|
||||
crossDomainHandoffParams,
|
||||
localOAuthCallbackUrl,
|
||||
});
|
||||
return getUrls(this._urlOptions, { projectId: this.projectId });
|
||||
}
|
||||
|
||||
protected _prefetchCrossDomainHandoffParamsIfNeeded() {
|
||||
|
||||
@ -52,6 +52,10 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
|
||||
*/
|
||||
readonly version: string,
|
||||
|
||||
/**
|
||||
* @deprecated `app.urls` is static and does not include runtime redirect-back parameters.
|
||||
* For navigation, prefer `redirectToXyz()` methods (for example `redirectToSignIn()`).
|
||||
*/
|
||||
readonly urls: Readonly<ResolvedHandlerUrls>,
|
||||
|
||||
signInWithOAuth(provider: string, options?: { returnTo?: string }): Promise<void>,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user