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
-
+
{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
? {}
: {