From c0151a8e5d6913c9381a46e13e851364684fbfb7 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Tue, 12 Mar 2024 17:31:28 +0800 Subject: [PATCH] removed redirectUrl in functions, added newUser attribute to signInWithOAuth, added after sign-in/up/out urls --- apps/dev/src/app/signin/custom-credential.tsx | 6 +- apps/dev/src/app/signin/custom-oauth.tsx | 7 ++- apps/dev/src/components/SignOutButton.tsx | 8 ++- apps/dev/src/stack.tsx | 2 + docs/docs/01-getting-started/02-users.md | 7 ++- .../01-customization/01-overview.md | 11 +++- .../01-customization/02-examples/01-signin.md | 16 +++-- packages/stack-server/prisma/schema.prisma | 1 + .../projects/[projectId]/sidebar.tsx | 6 +- .../api/v1/auth/callback/[provider]/route.tsx | 25 +++++--- packages/stack-server/src/oauth/index.tsx | 1 + packages/stack-server/src/oauth/model.tsx | 11 ++-- .../src/interface/clientInterface.ts | 2 + .../src/components/EmailVerification.tsx | 1 - .../stack/src/components/OAuthCallback.tsx | 10 ++-- packages/stack/src/components/SignIn.tsx | 6 +- packages/stack/src/components/SignOut.tsx | 10 +++- packages/stack/src/components/SignUp.tsx | 7 +-- .../stack/src/components/StackHandler.tsx | 8 +-- .../stack/src/elements/CredentialSignIn.tsx | 10 ++-- .../stack/src/elements/CredentialSignUp.tsx | 8 +-- packages/stack/src/elements/OAuthButton.tsx | 2 - packages/stack/src/elements/OAuthGroup.tsx | 4 +- .../src/elements/RedirectMessageCard.tsx | 2 +- packages/stack/src/lib/auth.ts | 33 +++-------- packages/stack/src/lib/stack-app.ts | 59 ++++++++----------- 26 files changed, 144 insertions(+), 119 deletions(-) diff --git a/apps/dev/src/app/signin/custom-credential.tsx b/apps/dev/src/app/signin/custom-credential.tsx index a24d1622f..bacabc1ff 100644 --- a/apps/dev/src/app/signin/custom-credential.tsx +++ b/apps/dev/src/app/signin/custom-credential.tsx @@ -14,10 +14,14 @@ export default function CustomCredentialSignIn() { setError('Please enter your password'); return; } - const errorCode = await app.signInWithCredential({ email, password, redirectUrl: app.urls.userHome }); + const errorCode = await app.signInWithCredential({ email, password }); // It is better to handle each error code separately, but for simplicity, we will just show the error code directly if (errorCode) { setError(errorCode); + } else { + // redirectToXXX will refresh the page so server components can be updated + // you can also router.push if you don't have any server components using the user info + app.redirectToAfterSignIn(); } }; diff --git a/apps/dev/src/app/signin/custom-oauth.tsx b/apps/dev/src/app/signin/custom-oauth.tsx index dc0781d3e..7ee7bdfee 100644 --- a/apps/dev/src/app/signin/custom-oauth.tsx +++ b/apps/dev/src/app/signin/custom-oauth.tsx @@ -7,6 +7,11 @@ export default function CustomOAuthSignIn() { return

My Custom Sign In page

- +
; } \ No newline at end of file diff --git a/apps/dev/src/components/SignOutButton.tsx b/apps/dev/src/components/SignOutButton.tsx index faa057a15..0e3ce8962 100644 --- a/apps/dev/src/components/SignOutButton.tsx +++ b/apps/dev/src/components/SignOutButton.tsx @@ -1,9 +1,13 @@ 'use client'; -import { useUser, useStackApp } from "@stackframe/stack"; +import { useStackApp, useUser } from "@stackframe/stack"; import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; export default function SignOutButton() { const user = useUser(); - return (); + const app = useStackApp(); + return (); } diff --git a/apps/dev/src/stack.tsx b/apps/dev/src/stack.tsx index 361548451..3c618da71 100644 --- a/apps/dev/src/stack.tsx +++ b/apps/dev/src/stack.tsx @@ -6,5 +6,7 @@ export const stackServerApp = new StackServerApp({ tokenStore: "nextjs-cookie", urls: { signIn: "/signin", + afterSignIn: "/after-signin", + afterSignUp: "/after-signup", } }); diff --git a/docs/docs/01-getting-started/02-users.md b/docs/docs/01-getting-started/02-users.md index 025a5202f..27587cb51 100644 --- a/docs/docs/01-getting-started/02-users.md +++ b/docs/docs/01-getting-started/02-users.md @@ -93,7 +93,12 @@ You can sign out the user by redirecting them to `/handler/signout` or simply by export default function SignOutButton() { const user = useUser(); - return ; } diff --git a/docs/docs/02-advanced-guides/01-customization/01-overview.md b/docs/docs/02-advanced-guides/01-customization/01-overview.md index 49ac8d6e6..b7fca7789 100644 --- a/docs/docs/02-advanced-guides/01-customization/01-overview.md +++ b/docs/docs/02-advanced-guides/01-customization/01-overview.md @@ -52,15 +52,20 @@ For more examples, please refer to the [Examples](/docs/category/examples). We also provide the low-level functions powering our components, so that you can build your own logic. For example, to build a custom OAuth sign-in button, create a file at `app/signin/page.tsx`: ```tsx -"use client"; +'use client'; import { useStackApp } from "@stackframe/stack"; -export default function CustomOAuthSignInPage() { +export default function CustomOAuthSignIn() { const app = useStackApp(); return

My Custom Sign In page

- +
; } ``` diff --git a/docs/docs/02-advanced-guides/01-customization/02-examples/01-signin.md b/docs/docs/02-advanced-guides/01-customization/02-examples/01-signin.md index cedc0fcb0..5d1c45d5f 100644 --- a/docs/docs/02-advanced-guides/01-customization/02-examples/01-signin.md +++ b/docs/docs/02-advanced-guides/01-customization/02-examples/01-signin.md @@ -13,7 +13,7 @@ import { useStackApp, SignIn } from "@stackframe/stack"; export default function DefaultSignIn() { const app = useStackApp(); - return ; + return ; } ``` @@ -43,10 +43,13 @@ export default function CustomOAuthSignIn() { const app = useStackApp(); return
- +

My Custom Sign In page

+
; } ``` @@ -69,11 +72,12 @@ export default function CustomCredentialSignIn() { setError('Please enter your password'); return; } - const errorCode = await app.signInWithCredential({ email, password, redirectUrl: app.urls.userHome }); + const errorCode = await app.signInWithCredential({ email, password }); // It is better to handle each error code separately, but for simplicity in this example, we will just show the error code directly if (errorCode) { setError(errorCode); } + app.redirectToSignInRedirect(); }; return ( diff --git a/packages/stack-server/prisma/schema.prisma b/packages/stack-server/prisma/schema.prisma index 178074a70..b59c9ae52 100644 --- a/packages/stack-server/prisma/schema.prisma +++ b/packages/stack-server/prisma/schema.prisma @@ -147,6 +147,7 @@ model ProjectUserAuthorizationCode { codeChallenge String codeChallengeMethod String + newUser Boolean projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) diff --git a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/sidebar.tsx b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/sidebar.tsx index 6a45996b4..cb5508b4e 100644 --- a/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/sidebar.tsx +++ b/packages/stack-server/src/app/(main)/(protected)/projects/[projectId]/sidebar.tsx @@ -64,6 +64,7 @@ function SidebarItem({ function AvatarSection() { const { mode, setMode } = useColorScheme(); const user = useUser({ or: 'redirect' }); + const app = useAdminApp(); const nameStyle = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', @@ -98,7 +99,10 @@ function AvatarSection() { }} onClick={isSigningOut ? undefined : () => { setIsSigningOut(true); - runAsynchronously(user.signOut().finally(() => setIsSigningOut(false))); + runAsynchronously((async () => { + await user.signOut(); + app.redirectToAfterSignOut(); + })().finally(() => setIsSigningOut(false))); }} variant='plain' > diff --git a/packages/stack-server/src/app/api/v1/auth/callback/[provider]/route.tsx b/packages/stack-server/src/app/api/v1/auth/callback/[provider]/route.tsx index 6f8d57813..8c58ca1df 100644 --- a/packages/stack-server/src/app/api/v1/auth/callback/[provider]/route.tsx +++ b/packages/stack-server/src/app/api/v1/auth/callback/[provider]/route.tsx @@ -73,11 +73,8 @@ export const GET = smartRouteHandler(async (req: NextRequest, options: { params: } const provider = project.evaluatedConfig.oauthProviders.find((p) => p.id === providerId); - if (!provider) { - throw new StatusError(StatusError.NotFound, "Provider not found"); - } - if (!provider.enabled) { - throw new StatusError(StatusError.NotFound, "Provider not enabled"); + if (!provider || !provider.enabled) { + throw new StatusError(StatusError.NotFound, "Provider not found or not enabled"); } const userInfo = await getAuthorizationCallback( @@ -114,7 +111,7 @@ export const GET = smartRouteHandler(async (req: NextRequest, options: { params: { authenticateHandler: { handle: async () => { - const account = await prismaClient.projectUserOAuthAccount.upsert({ + const oldAccount = await prismaClient.projectUserOAuthAccount.findUnique({ where: { projectId_oauthProviderConfigId_providerAccountId: { projectId: decoded.projectId, @@ -122,8 +119,17 @@ export const GET = smartRouteHandler(async (req: NextRequest, options: { params: providerAccountId: userInfo.accountId, }, }, - update: {}, - create: { + }); + + if (oldAccount) { + return { + id: oldAccount.projectUserId, + newUser: false + }; + } + + const newAccount = await prismaClient.projectUserOAuthAccount.create({ + data: { providerAccountId: userInfo.accountId, email: userInfo.email, providerConfig: { @@ -147,7 +153,8 @@ export const GET = smartRouteHandler(async (req: NextRequest, options: { params: }); return { - id: account.projectUserId, + id: newAccount.projectUserId, + newUser: true }; } } diff --git a/packages/stack-server/src/oauth/index.tsx b/packages/stack-server/src/oauth/index.tsx index abbaf9193..142b6e89c 100644 --- a/packages/stack-server/src/oauth/index.tsx +++ b/packages/stack-server/src/oauth/index.tsx @@ -104,4 +104,5 @@ export async function getAuthorizationCallback( export const oauthServer = new OAuth2Server({ model: new OAuthModel(), + allowExtendedTokenAttributes: true, }); diff --git a/packages/stack-server/src/oauth/model.tsx b/packages/stack-server/src/oauth/model.tsx index 275ceae5d..3d3501768 100644 --- a/packages/stack-server/src/oauth/model.tsx +++ b/packages/stack-server/src/oauth/model.tsx @@ -91,7 +91,10 @@ export class OAuthModel implements AuthorizationCodeModel { token.client = client; token.user = user; - return token; + return { + ...token, + newUser: user.newUser, + }; } async getAccessToken(accessToken: string): Promise { @@ -164,6 +167,7 @@ export class OAuthModel implements AuthorizationCodeModel { redirectUri: code.redirectUri, expiresAt: code.expiresAt, projectUserId: user.id, + newUser: user.newUser, projectId: client.id, }, }); @@ -177,9 +181,7 @@ export class OAuthModel implements AuthorizationCodeModel { id: client.id, grants: ["authorization_code", "refresh_token"], }, - user: { - id: user.id, - }, + user, }; } @@ -205,6 +207,7 @@ export class OAuthModel implements AuthorizationCodeModel { }, user: { id: code.projectUserId, + newUser: code.newUser, }, }; } diff --git a/packages/stack-shared/src/interface/clientInterface.ts b/packages/stack-shared/src/interface/clientInterface.ts index 73ffb7ce8..dc3349dfe 100644 --- a/packages/stack-shared/src/interface/clientInterface.ts +++ b/packages/stack-shared/src/interface/clientInterface.ts @@ -578,6 +578,8 @@ export class StackClientInterface { accessToken: result.access_token ?? null, refreshToken: result.refresh_token ?? old?.refreshToken ?? null, })); + + return result; } async signOut(tokenStore: TokenStore): Promise { diff --git a/packages/stack/src/components/EmailVerification.tsx b/packages/stack/src/components/EmailVerification.tsx index 00eb3f098..62f213b64 100644 --- a/packages/stack/src/components/EmailVerification.tsx +++ b/packages/stack/src/components/EmailVerification.tsx @@ -14,7 +14,6 @@ export default function EmailVerification({ }: { searchParams?: Record, fullPage?: boolean, - redirectUrl?: string, }) { const stackApp = useStackApp(); diff --git a/packages/stack/src/components/OAuthCallback.tsx b/packages/stack/src/components/OAuthCallback.tsx index 50302e6ac..1b61563c1 100644 --- a/packages/stack/src/components/OAuthCallback.tsx +++ b/packages/stack/src/components/OAuthCallback.tsx @@ -3,18 +3,20 @@ import { useRef, useEffect } from "react"; import { useStackApp } from ".."; import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; import MessageCard from "../elements/MessageCard"; -import { useRouter } from "next/navigation"; export default function OAuthCallback () { const app = useStackApp(); - const router = useRouter(); const called = useRef(false); useEffect(() => runAsynchronously(async () => { if (called.current) return; called.current = true; - await app.callOAuthCallback(); - router.push(app.urls.userHome); + const { newUser } = await app.callOAuthCallback(); + if (newUser) { + await app.redirectToAfterSignUp(); + } else { + await app.redirectToAfterSignIn(); + } }), []); return ; diff --git a/packages/stack/src/components/SignIn.tsx b/packages/stack/src/components/SignIn.tsx index 353907364..6230d6c8f 100644 --- a/packages/stack/src/components/SignIn.tsx +++ b/packages/stack/src/components/SignIn.tsx @@ -9,7 +9,7 @@ import CardHeader from '../elements/CardHeader'; import { useUser, useStackApp } from '..'; import RedirectMessageCard from '../elements/RedirectMessageCard'; -export default function SignIn({ redirectUrl, fullPage=false }: { redirectUrl?: string, fullPage?: boolean }) { +export default function SignIn({ fullPage=false }: { fullPage?: boolean }) { const stackApp = useStackApp(); const user = useUser(); const project = stackApp.useProject(); @@ -28,11 +28,11 @@ export default function SignIn({ redirectUrl, fullPage=false }: { redirectUrl?:

- + {project.credentialEnabled && <> - + } ); diff --git a/packages/stack/src/components/SignOut.tsx b/packages/stack/src/components/SignOut.tsx index 03535ee91..58fc5c5e6 100644 --- a/packages/stack/src/components/SignOut.tsx +++ b/packages/stack/src/components/SignOut.tsx @@ -1,14 +1,18 @@ 'use client'; import { use } from "react"; -import { useUser } from ".."; +import { useStackApp, useUser } from ".."; import GoHomeMessageCard from "../elements/RedirectMessageCard"; -export default function Signout({ redirectUrl }: { redirectUrl?: string }) { +export default function Signout() { const user = useUser(); + const app = useStackApp(); if (user) { - use(user.signOut(redirectUrl)); + use((async () => { + await user.signOut(); + await app.redirectToAfterSignOut(); + })()); } return ; diff --git a/packages/stack/src/components/SignUp.tsx b/packages/stack/src/components/SignUp.tsx index b78b16538..4ab856efb 100644 --- a/packages/stack/src/components/SignUp.tsx +++ b/packages/stack/src/components/SignUp.tsx @@ -6,10 +6,9 @@ import CardFrame from '../elements/CardFrame'; import CredentialSignUp from '../elements/CredentialSignUp'; import CardHeader from '../elements/CardHeader'; import { useUser, useStackApp } from '..'; -import AlreadySignedInMessageCard from '../elements/RedirectMessageCard'; import RedirectMessageCard from '../elements/RedirectMessageCard'; -export default function SignUp({ redirectUrl, fullPage=false }: { redirectUrl?: string, fullPage?: boolean }) { +export default function SignUp({ fullPage=false }: { fullPage?: boolean }) { const stackApp = useStackApp(); const user = useUser(); const project = stackApp.useProject(); @@ -28,10 +27,10 @@ export default function SignUp({ redirectUrl, fullPage=false }: { redirectUrl?:

- + {project.credentialEnabled && <> - + } ); diff --git a/packages/stack/src/components/StackHandler.tsx b/packages/stack/src/components/StackHandler.tsx index 212f1b142..a008b020f 100644 --- a/packages/stack/src/components/StackHandler.tsx +++ b/packages/stack/src/components/StackHandler.tsx @@ -42,15 +42,15 @@ export default async function StackHandler({ switch (path) { case 'signin': { redirectIfNotHandler('signIn'); - return ; + return ; } case 'signup': { redirectIfNotHandler('signUp'); - return ; + return ; } case 'email-verification': { redirectIfNotHandler('emailVerification'); - return ; + return ; } case 'password-reset': { redirectIfNotHandler('passwordReset'); @@ -62,7 +62,7 @@ export default async function StackHandler({ } case 'signout': { redirectIfNotHandler('signOut'); - return ; + return ; } case 'oauth-callback': { redirectIfNotHandler('oauthCallback'); diff --git a/packages/stack/src/elements/CredentialSignIn.tsx b/packages/stack/src/elements/CredentialSignIn.tsx index e6cb23fa5..a44b1b25e 100644 --- a/packages/stack/src/elements/CredentialSignIn.tsx +++ b/packages/stack/src/elements/CredentialSignIn.tsx @@ -9,13 +9,13 @@ import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises" import { EmailPasswordMissMatchErrorCode, UserNotExistErrorCode } from "@stackframe/stack-shared/dist/utils/types"; // Import or define the PasswordField, FormWarningText, and validateEmail utilities if they're custom components or functions. -export default function CredentialSignIn({ redirectUrl }: { redirectUrl?: string }) { +export default function CredentialSignIn() { const [email, setEmail] = useState(''); const [emailError, setEmailError] = useState(''); const [password, setPassword] = useState(''); const [passwordError, setPasswordError] = useState(''); const [loading, setLoading] = useState(false); - const stackApp = useStackApp(); + const app = useStackApp(); const onSubmit = async () => { if (!email) { @@ -32,7 +32,7 @@ export default function CredentialSignIn({ redirectUrl }: { redirectUrl?: string } setLoading(true); - const errorCode = await stackApp.signInWithCredential({ email, password, redirectUrl }); + const errorCode = await app.signInWithCredential({ email, password }); setLoading(false); switch (errorCode) { @@ -46,7 +46,7 @@ export default function CredentialSignIn({ redirectUrl }: { redirectUrl?: string } case undefined: { // success - break; + await app.redirectToAfterSignIn(); } } }; @@ -90,7 +90,7 @@ export default function CredentialSignIn({ redirectUrl }: { redirectUrl?: string {/* forgot password */}
Forgot password? diff --git a/packages/stack/src/elements/CredentialSignUp.tsx b/packages/stack/src/elements/CredentialSignUp.tsx index ffe335e71..00dab8079 100644 --- a/packages/stack/src/elements/CredentialSignUp.tsx +++ b/packages/stack/src/elements/CredentialSignUp.tsx @@ -10,7 +10,7 @@ import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises" import Button from "./Button"; import { UserAlreadyExistErrorCode } from "@stackframe/stack-shared/dist/utils/types"; -export default function CredentialSignUp({ redirectUrl }: { redirectUrl?: string }) { +export default function CredentialSignUp() { const [email, setEmail] = useState(''); const [emailError, setEmailError] = useState(''); const [password, setPassword] = useState(''); @@ -18,7 +18,7 @@ export default function CredentialSignUp({ redirectUrl }: { redirectUrl?: string const [passwordRepeat, setPasswordRepeat] = useState(''); const [passwordRepeatError, setPasswordRepeatError] = useState(''); const [loading, setLoading] = useState(false); - const stackApp = useStackApp(); + const app = useStackApp(); const onSubmit = async () => { if (!email) { @@ -49,7 +49,7 @@ export default function CredentialSignUp({ redirectUrl }: { redirectUrl?: string } setLoading(true); - const errorCode = await stackApp.signUpWithCredential({ email, password, redirectUrl }); + const errorCode = await app.signUpWithCredential({ email, password }); setLoading(false); switch (errorCode) { @@ -59,7 +59,7 @@ export default function CredentialSignUp({ redirectUrl }: { redirectUrl?: string } case undefined: { // success - break; + await app.redirectToAfterSignIn(); } } }; diff --git a/packages/stack/src/elements/OAuthButton.tsx b/packages/stack/src/elements/OAuthButton.tsx index 2e2a95ca3..07c9acdf5 100644 --- a/packages/stack/src/elements/OAuthButton.tsx +++ b/packages/stack/src/elements/OAuthButton.tsx @@ -10,11 +10,9 @@ const iconSize = 24; export default function OAuthButton({ provider, type, - redirectUrl }: { provider: string, type: 'signin' | 'signup', - redirectUrl?: string, }) { const stackApp = useStackApp(); diff --git a/packages/stack/src/elements/OAuthGroup.tsx b/packages/stack/src/elements/OAuthGroup.tsx index f919a0f9e..4b9dd4679 100644 --- a/packages/stack/src/elements/OAuthGroup.tsx +++ b/packages/stack/src/elements/OAuthGroup.tsx @@ -3,10 +3,8 @@ import OAuthButton from "./OAuthButton"; export default function OAuthGroup({ type, - redirectUrl }: { type: 'signin' | 'signup', - redirectUrl?: string, }) { const stackApp = useStackApp(); const project = stackApp.useProject(); @@ -14,7 +12,7 @@ export default function OAuthGroup({ return (
{project.oauthProviders.filter(p => p.enabled).map(p => ( - + ))}
); diff --git a/packages/stack/src/elements/RedirectMessageCard.tsx b/packages/stack/src/elements/RedirectMessageCard.tsx index 0a2d267de..10837d4cb 100644 --- a/packages/stack/src/elements/RedirectMessageCard.tsx +++ b/packages/stack/src/elements/RedirectMessageCard.tsx @@ -25,7 +25,7 @@ export default function RedirectMessageCard({ case 'signedIn': { title = "You are already signed in"; message = 'You are already signed in.'; - primaryUrl = stackApp.urls.userHome; + primaryUrl = stackApp.urls.home; secondaryUrl = stackApp.urls.signOut; primaryButton = "Go to Home"; secondaryButton = "Sign Out"; diff --git a/packages/stack/src/lib/auth.ts b/packages/stack/src/lib/auth.ts index a91f58f49..26235c3dd 100644 --- a/packages/stack/src/lib/auth.ts +++ b/packages/stack/src/lib/auth.ts @@ -30,10 +30,7 @@ export async function signInWithOAuth( * * Must be synchronous for the logic in callOAuthCallback to work without race conditions. */ -function consumeOAuthCallbackQueryParams(expectedState: string | null): null | { - newUrl: URL, - originalUrl: URL, -} { +function consumeOAuthCallbackQueryParams(expectedState: string | null): null | URL { const requiredParams = ["code", "state"]; const originalUrl = new URL(window.location.href); for (const param of requiredParams) { @@ -62,49 +59,37 @@ function consumeOAuthCallbackQueryParams(expectedState: string | null): null | { // prevent an unnecessary reload window.history.replaceState({}, "", newUrl.toString()); - return { newUrl, originalUrl }; + return originalUrl; } export async function callOAuthCallback( iface: StackClientInterface, tokenStore: TokenStore, - redirectUrl?: string, + redirectUrl: string, ) { // note: this part of the function (until the return) needs // to be synchronous, to prevent race conditions when // callOAuthCallback is called multiple times in parallel const { codeVerifier, state } = getVerifierAndState(); - const consumeResult = consumeOAuthCallbackQueryParams(state); - if (!consumeResult) { - return; + const originalUrl = consumeOAuthCallbackQueryParams(state); + if (!originalUrl) { + throw new Error("Invalid OAuth callback URL"); } if (!codeVerifier || !state) { - return; + throw new Error("Invalid OAuth callback URL"); } // the rest can be asynchronous (we now know that we are the // intended recipient of the callback) - - const { newUrl, originalUrl } = consumeResult; - - if (!redirectUrl) { - redirectUrl = newUrl.toString(); - } - - redirectUrl = redirectUrl.split("#")[0]; // remove hash - try { - await iface.callOAuthCallback( + return await iface.callOAuthCallback( originalUrl.searchParams, - redirectUrl, + constructRedirectUrl(redirectUrl), codeVerifier, state, tokenStore, ); - - // reload/redirect so the server can update now that the user is signed in - window.location.assign(redirectUrl); } catch (e) { console.error("Error signing in during OAuth callback", e); throw new Error("Error signing in. Please try again."); diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 5a7885eb4..29f8b5a5e 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -28,13 +28,15 @@ export type TokenStoreOptions = export type HandlerUrls = { handler: string, signIn: string, + afterSignIn: string, signUp: string, + afterSignUp: string, signOut: string, + afterSignOut: string, emailVerification: string, passwordReset: string, forgotPassword: string, home: string, - userHome: string, oauthCallback: string, } @@ -43,14 +45,16 @@ function getUrls(partial: Partial): HandlerUrls { return { handler, signIn: `${handler}/signin`, + afterSignIn: "/", signUp: `${handler}/signup`, + afterSignUp: "/", signOut: `${handler}/signout`, + afterSignOut: "/", emailVerification: `${handler}/email-verification`, passwordReset: `${handler}/password-reset`, forgotPassword: `${handler}/forgot-password`, oauthCallback: `${handler}/oauth-callback`, home: "/", - userHome: "/", ...filterUndefined(partial), }; } @@ -333,8 +337,8 @@ class _StackClientAppImpl { - if (!options.redirectUrl) { - options.redirectUrl = constructRedirectUrl(options.redirectUrl); - } this._ensurePersistentTokenStore(); const tokenStore = getTokenStore(this._tokenStoreOptions); return await signInWithCredential(this._interface, tokenStore, options); @@ -532,11 +533,7 @@ class _StackClientAppImpl{ - if (!options.redirectUrl) { - options.redirectUrl = constructRedirectUrl(options.redirectUrl); - } this._ensurePersistentTokenStore(); const tokenStore = getTokenStore(this._tokenStoreOptions); return await signUpWithCredential(this._interface, tokenStore, { @@ -545,28 +542,24 @@ class _StackClientAppImpl { - redirectUrl = constructRedirectUrl(redirectUrl); + protected async _signOut(tokenStore: TokenStore): Promise { await this._interface.signOut(tokenStore); - window.location.assign(redirectUrl); - return await neverResolve(); } - async signOut(redirectUrl: string): Promise { + async signOut(): Promise { const user = await this.getUser(); if (user) { - await user.signOut(redirectUrl); + await user.signOut(); } - window.location.assign(redirectUrl); - return await neverResolve(); } async getProject(): Promise { @@ -773,8 +766,8 @@ class _StackServerAppImpl = { readonly tokenStore: ReadonlyTokenStore, update(this: T, user: Partial): Promise, - signOut(this: T, redirectUrl?: string): Promise, + signOut(this: T): Promise, }; export type User = { @@ -1118,9 +1111,9 @@ export type StackClientApp, signInWithOAuth(provider: string): Promise, - signInWithCredential(options: { email: string, password: string, redirectUrl?: string }): Promise, - signUpWithCredential(options: { email: string, password: string, redirectUrl?: string }): Promise, - callOAuthCallback(options?: { redirectUrl?: string }): Promise, + signInWithCredential(options: { email: string, password: string }): Promise, + signUpWithCredential(options: { email: string, password: string }): Promise, + callOAuthCallback(): Promise<{ newUser: boolean }>, sendForgotPasswordEmail(email: string): Promise, resetPassword(options: { code: string, password: string }): Promise, verifyPasswordResetCode(code: string): Promise, @@ -1131,7 +1124,7 @@ export type StackClientApp - & { [K in `redirectTo${Capitalize>}`]: () => Promise } + & { [K in `redirectTo${Capitalize>}`]: () => Promise } & (HasTokenStore extends false ? {} : {