mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
Aggressively deprecate app.urls.xyz & update docs to avoid it
Closes #1587
This commit is contained in:
parent
c50f1e64ed
commit
ec0008d515
@ -111,7 +111,7 @@ it("adds secure cross-domain handoff parameters when redirecting to hosted sign-
|
||||
});
|
||||
});
|
||||
|
||||
it("returns static app.urls.signIn for hosted flows", async ({ expect }) => {
|
||||
it("throws when app.urls.signIn is read for hosted flows", async ({ expect }) => {
|
||||
await withHostedDomainSuffix(async () => {
|
||||
const projectId = "44444444-4444-4444-8444-444444444444";
|
||||
const currentHref = `${localRedirectUrl}/private-page?foo=bar`;
|
||||
@ -124,17 +124,13 @@ it("returns static app.urls.signIn for hosted flows", async ({ expect }) => {
|
||||
href: currentHref,
|
||||
assign: () => { throw new Error("INTENTIONAL_TEST_ABORT"); },
|
||||
},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
} as any;
|
||||
|
||||
try {
|
||||
const clientApp = createClientApp(projectId);
|
||||
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("after_auth_return_to")).toBeNull();
|
||||
expect(signInUrl.searchParams.get("hexclave_cross_domain_state")).toBeNull();
|
||||
expect(signInUrl.searchParams.get("hexclave_cross_domain_code_challenge")).toBeNull();
|
||||
expect(signInUrl.searchParams.get("hexclave_cross_domain_after_callback_redirect_url")).toBeNull();
|
||||
expect(() => clientApp.urls.signIn).toThrowError(/app\.urls\.signIn cannot be used when this app is configured to use hosted components.*Use app\.redirectToSignIn\(\) instead/s);
|
||||
} finally {
|
||||
globalThis.window = previousWindow;
|
||||
globalThis.document = previousDocument;
|
||||
@ -142,7 +138,7 @@ it("returns static app.urls.signIn for hosted flows", async ({ expect }) => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns static app.urls.signOut for hosted flows", async ({ expect }) => {
|
||||
it("throws when app.urls.signOut is read for hosted flows", async ({ expect }) => {
|
||||
await withHostedDomainSuffix(async () => {
|
||||
const projectId = "55555555-5555-4555-8555-555555555555";
|
||||
const currentHref = `${localRedirectUrl}/signed-in-page?foo=bar`;
|
||||
@ -155,14 +151,13 @@ it("returns static app.urls.signOut for hosted flows", async ({ expect }) => {
|
||||
href: currentHref,
|
||||
assign: () => { throw new Error("INTENTIONAL_TEST_ABORT"); },
|
||||
},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
} as any;
|
||||
|
||||
try {
|
||||
const clientApp = createClientApp(projectId);
|
||||
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")).toBeNull();
|
||||
expect(() => clientApp.urls.signOut).toThrowError(/app\.urls\.signOut cannot be used when this app is configured to use hosted components.*Use app\.redirectToSignOut\(\) instead/s);
|
||||
} finally {
|
||||
globalThis.window = previousWindow;
|
||||
globalThis.document = previousDocument;
|
||||
|
||||
@ -185,7 +185,7 @@ function HostedAuthPageInner(props: {
|
||||
<p className="text-muted-foreground">
|
||||
Don't have an account?{" "}
|
||||
<a
|
||||
href={app.urls.signUp}
|
||||
href="#"
|
||||
className={authFooterLinkClassName}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
@ -200,7 +200,7 @@ function HostedAuthPageInner(props: {
|
||||
<p className="text-muted-foreground">
|
||||
Already have an account?{" "}
|
||||
<a
|
||||
href={app.urls.signIn}
|
||||
href="#"
|
||||
className={authFooterLinkClassName}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@ -107,7 +107,7 @@ export function HostedForgotPassword(props: {
|
||||
<p className="text-muted-foreground">
|
||||
Remembered your password?{" "}
|
||||
<a
|
||||
href={app.urls.signIn}
|
||||
href="#"
|
||||
className={authFooterLinkClassName}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@ -64,7 +64,7 @@ export function CredentialSignIn() {
|
||||
<div className="mb-1.5 mt-4 flex items-center justify-between">
|
||||
<Label htmlFor="password" className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Password</Label>
|
||||
<a
|
||||
href={app.urls.forgotPassword}
|
||||
href="#"
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@ -214,7 +214,7 @@ export function HostedPasswordReset(props: {
|
||||
<p className="text-muted-foreground">
|
||||
Remembered your password?{" "}
|
||||
<a
|
||||
href={app.urls.signIn}
|
||||
href="#"
|
||||
className={authFooterLinkClassName}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -123,7 +123,6 @@ Let's create a sample table and some RLS policies to demonstrate how to integrat
|
||||
|
||||
import { createSupabaseClient } from "@/utils/supabase-client";
|
||||
import { useHexclaveApp, useUser } from "@hexclave/next";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
@ -149,9 +148,9 @@ Let's create a sample table and some RLS policies to demonstrate how to integrat
|
||||
<>
|
||||
<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>
|
||||
|
||||
@ -114,12 +114,11 @@ After setup, open the hosted auth UI (for example `/handler/sign-up`), create a
|
||||
|
||||
### Marketing header: sign in / sign out
|
||||
|
||||
Use `useHexclaveApp()` so you do not hard-code handler URLs (they can be customized in the project):
|
||||
Use `useHexclaveApp()` so navigation goes through Hexclave's redirect helpers:
|
||||
|
||||
```tsx title="components/auth-header.tsx"
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useHexclaveApp, useUser } from "@hexclave/next";
|
||||
|
||||
export function AuthHeader() {
|
||||
@ -131,13 +130,13 @@ export function AuthHeader() {
|
||||
{user ? (
|
||||
<>
|
||||
<span>{user.displayName ?? user.primaryEmail ?? user.id}</span>
|
||||
<Link href={app.urls.accountSettings}>Account</Link>
|
||||
<Link href={app.urls.signOut}>Sign out</Link>
|
||||
<button onClick={async () => await app.redirectToAccountSettings()}>Account</button>
|
||||
<button onClick={async () => await app.redirectToSignOut()}>Sign out</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href={app.urls.signIn}>Sign in</Link>
|
||||
<Link href={app.urls.signUp}>Sign up</Link>
|
||||
<button onClick={async () => await app.redirectToSignIn()}>Sign in</button>
|
||||
<button onClick={async () => await app.redirectToSignUp()}>Sign up</button>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
|
||||
@ -44,7 +44,7 @@ You typically combine **one or more** of:
|
||||
|
||||
Match **only** prefixes that should be gated. Do **not** blanket-match `/` or you can block static assets and Stack’s **`/handler`** routes (sign-in, sign-up, callbacks).
|
||||
|
||||
If your project uses **custom handler base paths**, use the sign-in URL from `hexclaveServerApp.urls.signIn` as the redirect target instead of hard-coding `/handler/sign-in` (it may be an absolute URL depending on configuration—`NextResponse.redirect` accepts that).
|
||||
If your project uses hosted components or cross-domain auth, prefer protecting pages with `hexclaveServerApp.getUser({ or: "redirect" })` in a Server Component. Middleware cannot run the runtime `redirectToSignIn()` helper, so only hard-code `/handler/sign-in` here when your app owns a same-domain handler route.
|
||||
</Tab>
|
||||
<Tab title="Server Component">
|
||||
```tsx title="app/app/dashboard/page.tsx"
|
||||
|
||||
@ -731,7 +731,6 @@ Follow these instructions to integrate Hexclave with Convex.
|
||||
|
||||
import { createSupabaseClient } from "@/utils/supabase-client";
|
||||
import { useHexclaveApp, useUser } from "@hexclave/next";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
@ -756,10 +755,10 @@ Follow these instructions to integrate Hexclave with Convex.
|
||||
<>
|
||||
<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>
|
||||
|
||||
@ -15,7 +15,7 @@ import { useHexclaveApp } from "@hexclave/next"; // replace `next` with the cor
|
||||
|
||||
function MyComponent() {
|
||||
const hexclaveApp = useHexclaveApp();
|
||||
return <div>Sign In URL: {hexclaveApp.urls.signIn}</div>;
|
||||
return <button onClick={async () => await hexclaveApp.redirectToSignIn()}>Sign in</button>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -66,7 +66,7 @@ export default function CustomCredentialSignIn() {
|
||||
setError('Please enter your password');
|
||||
return;
|
||||
}
|
||||
// This will redirect to app.urls.afterSignIn if successful.
|
||||
// This will redirect to the configured afterSignIn URL if successful.
|
||||
// You can customize the redirect URL in the StackServerApp constructor.
|
||||
const result = await app.signInWithCredential({ email, password });
|
||||
// It's better to handle each error code separately, but for simplicity,
|
||||
@ -180,7 +180,7 @@ export default function CustomCredentialSignUp() {
|
||||
setError('Please enter your password');
|
||||
return;
|
||||
}
|
||||
// This will redirect to app.urls.afterSignUp if successful.
|
||||
// This will redirect to the configured afterSignUp URL if successful.
|
||||
// You can customize the redirect URL in the StackServerApp constructor.
|
||||
const result = await app.signUpWithCredential({ email, password });
|
||||
// It's better to handle each error code separately, but for simplicity,
|
||||
|
||||
@ -142,7 +142,7 @@ You can also store custom user data in the `clientMetadata`, `serverMetadata`, o
|
||||
|
||||
## Signing out
|
||||
|
||||
You can sign out the user by redirecting them to `/handler/sign-out` or simply by calling `user.signOut()`. They will be redirected to the URL [configured as `afterSignOut` in the `StackServerApp`](../sdk/objects/stack-app).
|
||||
You can sign out the user by calling `user.signOut()` or by using the app's redirect helper. They will be redirected to the URL [configured as `afterSignOut` in the `StackServerApp`](../sdk/objects/stack-app).
|
||||
|
||||
<Tabs defaultValue="signout">
|
||||
<TabsList>
|
||||
@ -163,12 +163,13 @@ You can sign out the user by redirecting them to `/handler/sign-out` or simply b
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="redirect">
|
||||
```tsx title="sign-out-link.tsx"
|
||||
import { stackServerApp } from "@/stack/server";
|
||||
```tsx title="sign-out-button.tsx"
|
||||
"use client";
|
||||
import { useStackApp } from "@stackframe/stack";
|
||||
|
||||
export default async function SignOutLink() {
|
||||
// stackServerApp.urls.signOut is equal to /handler/sign-out
|
||||
return <a href={stackServerApp.urls.signOut}>Sign Out</a>;
|
||||
export default function SignOutButton() {
|
||||
const app = useStackApp();
|
||||
return <button onClick={async () => await app.redirectToSignOut()}>Sign Out</button>;
|
||||
}
|
||||
```
|
||||
</TabsContent>
|
||||
@ -228,13 +229,12 @@ Stack automatically creates a user profile on sign-up. Let's build a page that d
|
||||
<UserButton />
|
||||
<p>Welcome, {user.displayName ?? "unnamed user"}</p>
|
||||
<p>Your e-mail: {user.primaryEmail}</p>
|
||||
<p><a href={stackServerApp.urls.signOut}>Sign Out</a></p>
|
||||
<UserButton />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p>You are not logged in</p>
|
||||
<p><a href={stackServerApp.urls.signIn}>Sign in</a></p>
|
||||
<p><a href={stackServerApp.urls.signUp}>Sign up</a></p>
|
||||
<p>Render a client component that calls <code>stackApp.redirectToSignIn()</code> or <code>stackApp.redirectToSignUp()</code>.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -120,7 +120,6 @@ Let's create an example page that fetches data from Supabase and displays it.
|
||||
|
||||
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() {
|
||||
@ -146,9 +145,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>
|
||||
|
||||
@ -11,6 +11,6 @@ import { useStackApp } from "@stackframe/stack";
|
||||
|
||||
function MyComponent() {
|
||||
const stackApp = useStackApp();
|
||||
return <div>Sign In URL: {stackApp.urls.signIn}</div>;
|
||||
return <button onClick={async () => await stackApp.redirectToSignIn()}>Sign in</button>;
|
||||
}
|
||||
```
|
||||
|
||||
@ -9,7 +9,7 @@ export async function middleware(request: NextRequest) {
|
||||
const user = await hexclaveServerApp.getUser();
|
||||
if (!user) {
|
||||
console.log('User in middleware is not logged in. Redirecting to sign-in page');
|
||||
return NextResponse.redirect(hexclaveServerApp.urls.signIn);
|
||||
return NextResponse.redirect(new URL('/handler/sign-in', request.url));
|
||||
}
|
||||
|
||||
console.log('User in middleware is logged in. ID: ', user.id);
|
||||
|
||||
@ -216,7 +216,6 @@ export const supabaseSetupPrompt = deindent`
|
||||
|
||||
import { createSupabaseClient } from "@/utils/supabase-client";
|
||||
import { useHexclaveApp, useUser } from "@hexclave/next";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Page() {
|
||||
@ -241,10 +240,10 @@ export const supabaseSetupPrompt = deindent`
|
||||
<>
|
||||
<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>
|
||||
|
||||
@ -218,7 +218,7 @@ function createAuthPagePrompt(type: AuthPagePromptType): CustomPagePrompt {
|
||||
<Typography>
|
||||
{"Don't have an account? "}
|
||||
<a
|
||||
href={hexclaveApp.urls.signUp}
|
||||
href="#"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
await hexclaveApp.redirectToSignUp();
|
||||
@ -231,7 +231,7 @@ function createAuthPagePrompt(type: AuthPagePromptType): CustomPagePrompt {
|
||||
}` : `<Typography>
|
||||
{"Already have an account? "}
|
||||
<a
|
||||
href={hexclaveApp.urls.signIn}
|
||||
href="#"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
await hexclaveApp.redirectToSignIn();
|
||||
@ -552,7 +552,15 @@ export function getCustomPagePrompts(): Record<PageComponentKey, CustomPagePromp
|
||||
<Typography type="h2">Reset Your Password</Typography>
|
||||
<Typography>
|
||||
{"Don't need to reset? "}
|
||||
<a href={hexclaveApp.urls.signIn}>Sign in</a>
|
||||
<a
|
||||
href="#"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
await hexclaveApp.redirectToSignIn();
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={async (e) => {
|
||||
@ -618,7 +626,15 @@ export function getCustomPagePrompts(): Record<PageComponentKey, CustomPagePromp
|
||||
{showRedirectLink ? (
|
||||
<Typography>
|
||||
{"If you are not redirected automatically, "}
|
||||
<a href={hexclaveApp.urls.home}>click here</a>
|
||||
<a
|
||||
href="#"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
await hexclaveApp.redirectToHome();
|
||||
}}
|
||||
>
|
||||
click here
|
||||
</a>
|
||||
</Typography>
|
||||
) : null}
|
||||
{error ? <pre>{JSON.stringify(error, null, 2)}</pre> : null}
|
||||
|
||||
@ -143,7 +143,7 @@ function Inner(props: Props) {
|
||||
project.config.signUpEnabled && (
|
||||
<Typography>
|
||||
{t("Don't have an account?")}{" "}
|
||||
<StyledLink href={hexclaveApp.urls.signUp} onClick={(e) => {
|
||||
<StyledLink href="#" onClick={(e) => {
|
||||
runAsynchronously(hexclaveApp.redirectToSignUp());
|
||||
e.preventDefault();
|
||||
}}>{t("Sign up")}</StyledLink>
|
||||
@ -152,7 +152,7 @@ function Inner(props: Props) {
|
||||
) : (
|
||||
<Typography>
|
||||
{t("Already have an account?")}{" "}
|
||||
<StyledLink href={hexclaveApp.urls.signIn} onClick={(e) => {
|
||||
<StyledLink href="#" onClick={(e) => {
|
||||
runAsynchronously(hexclaveApp.redirectToSignIn());
|
||||
e.preventDefault();
|
||||
}}>{t("Sign in")}</StyledLink>
|
||||
|
||||
@ -85,7 +85,13 @@ export function ForgotPassword(props: { fullPage?: boolean }) {
|
||||
<Typography type='h2'>{t("Reset Your Password")}</Typography>
|
||||
<Typography>
|
||||
{t("Don't need to reset?")}{" "}
|
||||
<StyledLink href={hexclaveApp.urls['signIn']}>
|
||||
<StyledLink
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
runAsynchronouslyWithAlert(hexclaveApp.redirectToSignIn());
|
||||
}}
|
||||
>
|
||||
{t("Sign in")}
|
||||
</StyledLink>
|
||||
</Typography>
|
||||
|
||||
@ -66,7 +66,15 @@ export function OAuthCallback({ fullPage }: { fullPage?: boolean }) {
|
||||
<div className="flex flex-col justify-center items-center gap-4">
|
||||
<Spinner size={20} />
|
||||
</div>
|
||||
{showRedirectLink || redirectUrl != null ? <p>{t('If you are not redirected automatically, ')}<StyledLink className="whitespace-nowrap" href={redirectUrl ?? app.urls.home}>{t("click here")}</StyledLink></p> : null}
|
||||
{showRedirectLink || redirectUrl != null ? <p>{t('If you are not redirected automatically, ')}<StyledLink
|
||||
className="whitespace-nowrap"
|
||||
href={redirectUrl ?? "#"}
|
||||
onClick={(e) => {
|
||||
if (redirectUrl != null) return;
|
||||
e.preventDefault();
|
||||
runAsynchronously(app.redirectToHome());
|
||||
}}
|
||||
>{t("click here")}</StyledLink></p> : null}
|
||||
</div>
|
||||
</MaybeFullPage>
|
||||
);
|
||||
|
||||
@ -26,7 +26,6 @@ export function TeamCreation(props: { fullPage?: boolean }) {
|
||||
const project = app.useProject();
|
||||
const user = useUser({ or: 'redirect' });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = app.useNavigate();
|
||||
|
||||
if (!project.config.clientTeamCreationEnabled) {
|
||||
return <MessageCard title={t('Team creation is not enabled')} />;
|
||||
@ -36,8 +35,8 @@ export function TeamCreation(props: { fullPage?: boolean }) {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const team = await user.createTeam({ displayName: data.displayName });
|
||||
navigate(`${app.urls.handler}/team-settings/${team.id}`);
|
||||
await user.createTeam({ displayName: data.displayName });
|
||||
await app.redirectToAccountSettings();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@ -66,7 +66,14 @@ export function CredentialSignIn() {
|
||||
/>
|
||||
<FormWarningText text={errors.password?.message?.toString()} />
|
||||
|
||||
<StyledLink href={app.urls.forgotPassword} className="mt-1 text-sm">
|
||||
<StyledLink
|
||||
href="#"
|
||||
className="mt-1 text-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
runAsynchronouslyWithAlert(app.redirectToForgotPassword());
|
||||
}}
|
||||
>
|
||||
{t('Forgot password?')}
|
||||
</StyledLink>
|
||||
|
||||
|
||||
@ -65,8 +65,7 @@ function Inner<AllowNull extends boolean>(props: TeamSwitcherProps<AllowNull>) {
|
||||
// Use mock data if provided, otherwise use real data
|
||||
const app = props.mockUser ? {
|
||||
useProject: () => props.mockProject || { config: { clientTeamCreationEnabled: false } },
|
||||
useNavigate: () => () => {}, // Mock navigate function
|
||||
urls: { accountSettings: '/account-settings' },
|
||||
redirectToAccountSettings: async () => {},
|
||||
} : appFromHook;
|
||||
|
||||
const user = props.mockUser ? {
|
||||
@ -75,7 +74,6 @@ function Inner<AllowNull extends boolean>(props: TeamSwitcherProps<AllowNull>) {
|
||||
setSelectedTeam: async () => {}, // Mock function
|
||||
} : userFromHook;
|
||||
|
||||
const navigate = app.useNavigate();
|
||||
const project = app.useProject();
|
||||
const rawTeams = props.teams ?? user?.useTeams();
|
||||
const selectedTeam = props.team || rawTeams?.find(team => team.id === props.teamId);
|
||||
@ -120,7 +118,7 @@ function Inner<AllowNull extends boolean>(props: TeamSwitcherProps<AllowNull>) {
|
||||
className="h-6 w-6"
|
||||
onClick={() => {
|
||||
if (!props.mockUser) {
|
||||
navigate(`${app.urls.accountSettings}#team-${selectedTeam.id}`);
|
||||
runAsynchronouslyWithAlert(app.redirectToAccountSettings());
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -170,7 +168,7 @@ function Inner<AllowNull extends boolean>(props: TeamSwitcherProps<AllowNull>) {
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!props.mockUser) {
|
||||
navigate(`${app.urls.accountSettings}#team-creation`);
|
||||
runAsynchronouslyWithAlert(app.redirectToAccountSettings());
|
||||
}
|
||||
}}
|
||||
className="w-full"
|
||||
|
||||
@ -269,6 +269,8 @@ describe("StackClientApp cross-domain auth", () => {
|
||||
protocol: "https:",
|
||||
hostname: "demo.stack-auth.com",
|
||||
},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
} as any;
|
||||
|
||||
const clientApp = new StackClientApp({
|
||||
@ -456,6 +458,40 @@ describe("StackClientApp cross-domain auth", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("throws when public app.urls reads would return hosted component URLs", () => {
|
||||
const clientApp = new StackClientApp({
|
||||
baseUrl: "http://localhost:12345",
|
||||
projectId: "00000000-0000-4000-8000-000000000003",
|
||||
publishableClientKey: "stack-pk-test",
|
||||
tokenStore: "memory",
|
||||
redirectMethod: "window",
|
||||
urls: {
|
||||
default: { type: "hosted" },
|
||||
},
|
||||
noAutomaticPrefetch: true,
|
||||
});
|
||||
|
||||
expect(() => clientApp.urls.signIn).toThrowError(/app\.urls\.signIn cannot be used when this app is configured to use hosted components.*Use app\.redirectToSignIn\(\) instead/s);
|
||||
expect(() => clientApp.urls.signOut).toThrowError(/app\.urls\.signOut cannot be used when this app is configured to use hosted components.*Use app\.redirectToSignOut\(\) instead/s);
|
||||
expect(clientApp.urls.afterSignIn).toBe("/");
|
||||
});
|
||||
|
||||
it("keeps public app.urls reads available for non-hosted targets", () => {
|
||||
const clientApp = new StackClientApp({
|
||||
baseUrl: "http://localhost:12345",
|
||||
projectId: "00000000-0000-4000-8000-000000000003",
|
||||
publishableClientKey: "stack-pk-test",
|
||||
tokenStore: "memory",
|
||||
redirectMethod: "window",
|
||||
urls: {
|
||||
handler: "/custom-handler",
|
||||
},
|
||||
noAutomaticPrefetch: true,
|
||||
});
|
||||
|
||||
expect(clientApp.urls.signIn).toBe("/custom-handler/sign-in");
|
||||
});
|
||||
|
||||
it("keeps default hosted signOut() on the source domain when afterSignOut is not configured", async () => {
|
||||
const clientApp = new StackClientApp({
|
||||
baseUrl: "http://localhost:12345",
|
||||
|
||||
@ -97,6 +97,38 @@ const nestedCrossDomainAuthQueryParams = {
|
||||
afterCallbackRedirectUrl: "after_callback_redirect_url",
|
||||
} as const;
|
||||
|
||||
function getRedirectHelperInstruction(handlerName: string): string {
|
||||
if (handlerName === "handler") {
|
||||
return "Use a page-specific redirect helper such as app.redirectToSignIn() instead.";
|
||||
}
|
||||
const redirectMethodName = `redirectTo${handlerName.slice(0, 1).toUpperCase()}${handlerName.slice(1)}`;
|
||||
return `Use app.${redirectMethodName}() instead.`;
|
||||
}
|
||||
|
||||
function createUrlsForPublicAccess(options: {
|
||||
urls: ResolvedHandlerUrls,
|
||||
projectId: string,
|
||||
}): Readonly<ResolvedHandlerUrls> {
|
||||
const hostedUrlNames = new Set(
|
||||
Object.entries(options.urls)
|
||||
.filter(([, url]) => isHostedHandlerUrlForProject({ url, projectId: options.projectId }))
|
||||
.map(([handlerName]) => handlerName),
|
||||
);
|
||||
|
||||
return new Proxy(options.urls, {
|
||||
get(target, property, receiver) {
|
||||
if (typeof property === "string" && hostedUrlNames.has(property)) {
|
||||
throw new Error(
|
||||
`app.urls.${property} cannot be used when this app is configured to use hosted components. ` +
|
||||
"`app.urls` is static and does not include the runtime redirect-back, cross-domain auth, or sign-out state required by hosted components. " +
|
||||
getRedirectHelperInstruction(property),
|
||||
);
|
||||
}
|
||||
return Reflect.get(target, property, receiver);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const oauthCallbackResponseQueryParams = ["code", "state", "error", "error_description", "errorCode", "message", "details"] as const;
|
||||
|
||||
const allClientApps = new Map<string, [checkString: string | undefined, app: StackClientApp<any, any>]>();
|
||||
@ -1734,7 +1766,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
teamId: crud.id,
|
||||
email: options.email,
|
||||
session,
|
||||
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation, "callbackUrl"),
|
||||
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app._getUrls().teamInvitation, "callbackUrl"),
|
||||
});
|
||||
await app._teamInvitationsCache.refresh([session, crud.id]);
|
||||
},
|
||||
@ -1804,7 +1836,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
async sendVerificationEmail(options?: { callbackUrl?: string }) {
|
||||
await app._interface.sendCurrentUserContactChannelVerificationEmail(
|
||||
crud.id,
|
||||
options?.callbackUrl || constructRedirectUrl(app.urls.emailVerification, "callbackUrl"),
|
||||
options?.callbackUrl || constructRedirectUrl(app._getUrls().emailVerification, "callbackUrl"),
|
||||
session
|
||||
);
|
||||
},
|
||||
@ -2174,7 +2206,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
{
|
||||
provider,
|
||||
redirectUrl: app._getOAuthCallbackRedirectUri(),
|
||||
errorRedirectUrl: app.urls.error,
|
||||
errorRedirectUrl: app._getUrls().error,
|
||||
providerScope: mergeScopeStrings(scopeString, (app._oauthScopesOnSignIn[provider as ProviderType] ?? []).join(" ")),
|
||||
},
|
||||
session,
|
||||
@ -2315,7 +2347,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
}
|
||||
return await app._interface.sendVerificationEmail(
|
||||
crud.primary_email,
|
||||
options?.callbackUrl ?? constructRedirectUrl(app.urls.emailVerification, "callbackUrl"),
|
||||
options?.callbackUrl ?? constructRedirectUrl(app._getUrls().emailVerification, "callbackUrl"),
|
||||
session
|
||||
);
|
||||
},
|
||||
@ -2798,6 +2830,13 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
}
|
||||
|
||||
get urls(): Readonly<ResolvedHandlerUrls> {
|
||||
return createUrlsForPublicAccess({
|
||||
urls: this._getUrls(),
|
||||
projectId: this.projectId,
|
||||
});
|
||||
}
|
||||
|
||||
protected _getUrls(): Readonly<ResolvedHandlerUrls> {
|
||||
return getUrls(this._urlOptions, { projectId: this.projectId });
|
||||
}
|
||||
|
||||
|
||||
@ -362,7 +362,7 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
isPrimary: crud.is_primary,
|
||||
usedForAuth: crud.used_for_auth,
|
||||
async sendVerificationEmail(options?: { callbackUrl?: string }) {
|
||||
await app._interface.sendServerContactChannelVerificationEmail(userId, crud.id, options?.callbackUrl ?? constructRedirectUrl(app.urls.emailVerification, "callbackUrl"));
|
||||
await app._interface.sendServerContactChannelVerificationEmail(userId, crud.id, options?.callbackUrl ?? constructRedirectUrl(app._getUrls().emailVerification, "callbackUrl"));
|
||||
},
|
||||
async update(data: ServerContactChannelUpdateOptions) {
|
||||
await app._interface.updateServerContactChannel(userId, crud.id, serverContactChannelUpdateOptionsToCrud(data));
|
||||
@ -1074,7 +1074,7 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
await app._interface.sendServerTeamInvitation({
|
||||
teamId: crud.id,
|
||||
email: options.email,
|
||||
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation, "callbackUrl"),
|
||||
callbackUrl: options.callbackUrl ?? constructRedirectUrl(app._getUrls().teamInvitation, "callbackUrl"),
|
||||
});
|
||||
await app._serverTeamInvitationsCache.refresh([crud.id]);
|
||||
},
|
||||
|
||||
@ -64,8 +64,9 @@ 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()`).
|
||||
* @deprecated Do not use `app.urls` for navigation. It is static and does not include runtime redirect-back,
|
||||
* cross-domain auth, or sign-out state. Use the matching `redirectToXyz()` method instead, for example
|
||||
* `redirectToSignIn()`, `redirectToSignUp()`, `redirectToSignOut()`, or `redirectToAccountSettings()`.
|
||||
*/
|
||||
readonly urls: Readonly<ResolvedHandlerUrls>,
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user