mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
removed redirectUrl in functions, added newUser attribute to signInWithOAuth, added after sign-in/up/out urls
This commit is contained in:
parent
f13a71ec56
commit
c0151a8e5d
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -7,6 +7,11 @@ export default function CustomOAuthSignIn() {
|
||||
|
||||
return <div>
|
||||
<h1>My Custom Sign In page</h1>
|
||||
<button onClick={async () => await app.signInWithOAuth('google')}>Sign In with Google</button>
|
||||
<button onClick={async () => {
|
||||
// this will redirect to the OAuth provider's login page
|
||||
await app.signInWithOAuth('google');
|
||||
}}>
|
||||
Sign In with Google
|
||||
</button>
|
||||
</div>;
|
||||
}
|
||||
@ -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 (<button onClick={() => runAsynchronously(user?.signOut())}>Sign out</button>);
|
||||
const app = useStackApp();
|
||||
return (<button onClick={() => runAsynchronously(async () => {
|
||||
await user?.signOut();
|
||||
app.redirectToAfterSignOut();
|
||||
})}>Sign out</button>);
|
||||
}
|
||||
|
||||
@ -6,5 +6,7 @@ export const stackServerApp = new StackServerApp({
|
||||
tokenStore: "nextjs-cookie",
|
||||
urls: {
|
||||
signIn: "/signin",
|
||||
afterSignIn: "/after-signin",
|
||||
afterSignUp: "/after-signup",
|
||||
}
|
||||
});
|
||||
|
||||
@ -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 <button onClick={() => user?.signOut()}>
|
||||
return <button onClick={async () => {
|
||||
await user?.signOut()
|
||||
// redirectToXXX will redirect and reload the page so server components can re-render
|
||||
// you can also use router.push if you don't have any server components that uses the user
|
||||
app.redirectToAfterSignOut();
|
||||
}>
|
||||
Sign Out
|
||||
</button>;
|
||||
}
|
||||
|
||||
@ -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 <div>
|
||||
<h1>My Custom Sign In page</h1>
|
||||
<button onClick={async () => await app.signInWithOAuth('google')}>Sign In with Google</button>
|
||||
<button onClick={async () => {
|
||||
// this will redirect to the OAuth provider's login page
|
||||
await app.signInWithOAuth('google');
|
||||
}}>
|
||||
Sign In with Google
|
||||
</button>
|
||||
</div>;
|
||||
}
|
||||
```
|
||||
|
||||
@ -13,7 +13,7 @@ import { useStackApp, SignIn } from "@stackframe/stack";
|
||||
export default function DefaultSignIn() {
|
||||
const app = useStackApp();
|
||||
|
||||
return <SignIn fullPage redirectUrl={app.urls.userHome} />;
|
||||
return <SignIn fullPage redirectUrl={app.urls.signInRedirect} />;
|
||||
}
|
||||
```
|
||||
|
||||
@ -43,10 +43,13 @@ export default function CustomOAuthSignIn() {
|
||||
const app = useStackApp();
|
||||
|
||||
return <div>
|
||||
<button onClick={async () => await app.signInWithOAuth({
|
||||
provider: 'google',
|
||||
redirectUrl: app.urls.userHome
|
||||
})}>Sign In with Google</button>
|
||||
<h1>My Custom Sign In page</h1>
|
||||
<button onClick={async () => {
|
||||
// this will redirect to the OAuth provider's login page
|
||||
await app.signInWithOAuth('google');
|
||||
}}>
|
||||
Sign In with Google
|
||||
</button>
|
||||
</div>;
|
||||
}
|
||||
```
|
||||
@ -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 (
|
||||
|
||||
@ -147,6 +147,7 @@ model ProjectUserAuthorizationCode {
|
||||
|
||||
codeChallenge String
|
||||
codeChallengeMethod String
|
||||
newUser Boolean
|
||||
|
||||
projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade)
|
||||
|
||||
|
||||
@ -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'
|
||||
>
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,4 +104,5 @@ export async function getAuthorizationCallback(
|
||||
|
||||
export const oauthServer = new OAuth2Server({
|
||||
model: new OAuthModel(),
|
||||
allowExtendedTokenAttributes: true,
|
||||
});
|
||||
|
||||
@ -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<Token | Falsey> {
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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<void> {
|
||||
|
||||
@ -14,7 +14,6 @@ export default function EmailVerification({
|
||||
}: {
|
||||
searchParams?: Record<string, string>,
|
||||
fullPage?: boolean,
|
||||
redirectUrl?: string,
|
||||
}) {
|
||||
const stackApp = useStackApp();
|
||||
|
||||
|
||||
@ -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 <MessageCard title='Redirecting...' fullPage />;
|
||||
|
||||
@ -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?:
|
||||
</NextLink>
|
||||
</p>
|
||||
</CardHeader>
|
||||
<OAuthGroup type='signin' redirectUrl={redirectUrl} />
|
||||
<OAuthGroup type='signin'/>
|
||||
{project.credentialEnabled &&
|
||||
<>
|
||||
<DividerWithText text={'OR'} />
|
||||
<CredentialSignIn redirectUrl={redirectUrl} />
|
||||
<CredentialSignIn/>
|
||||
</>}
|
||||
</CardFrame>
|
||||
);
|
||||
|
||||
@ -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 <GoHomeMessageCard type='signedOut' fullPage />;
|
||||
|
||||
@ -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?:
|
||||
</NextLink>
|
||||
</p>
|
||||
</CardHeader>
|
||||
<OAuthGroup type='signup' redirectUrl={redirectUrl} />
|
||||
<OAuthGroup type='signup'/>
|
||||
{project.credentialEnabled && <>
|
||||
<DividerWithText text={'OR'} />
|
||||
<CredentialSignUp redirectUrl={redirectUrl} />
|
||||
<CredentialSignUp/>
|
||||
</>}
|
||||
</CardFrame>
|
||||
);
|
||||
|
||||
@ -42,15 +42,15 @@ export default async function StackHandler<HasTokenStore extends boolean>({
|
||||
switch (path) {
|
||||
case 'signin': {
|
||||
redirectIfNotHandler('signIn');
|
||||
return <SignIn fullPage redirectUrl={app.urls.userHome}/>;
|
||||
return <SignIn fullPage/>;
|
||||
}
|
||||
case 'signup': {
|
||||
redirectIfNotHandler('signUp');
|
||||
return <SignUp fullPage redirectUrl={app.urls.userHome}/>;
|
||||
return <SignUp fullPage/>;
|
||||
}
|
||||
case 'email-verification': {
|
||||
redirectIfNotHandler('emailVerification');
|
||||
return <EmailVerification searchParams={searchParams} fullPage redirectUrl={app.urls.signIn} />;
|
||||
return <EmailVerification searchParams={searchParams} fullPage/>;
|
||||
}
|
||||
case 'password-reset': {
|
||||
redirectIfNotHandler('passwordReset');
|
||||
@ -62,7 +62,7 @@ export default async function StackHandler<HasTokenStore extends boolean>({
|
||||
}
|
||||
case 'signout': {
|
||||
redirectIfNotHandler('signOut');
|
||||
return <Signout redirectUrl={app.urls.home} />;
|
||||
return <Signout/>;
|
||||
}
|
||||
case 'oauth-callback': {
|
||||
redirectIfNotHandler('oauthCallback');
|
||||
|
||||
@ -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 */}
|
||||
<div className="wl_flex wl_items-center wl_justify-between">
|
||||
<NextLink
|
||||
href={stackApp.urls.forgotPassword}
|
||||
href={app.urls.forgotPassword}
|
||||
className="wl_text-sm wl_text-blue-500 wl_no-underline wl_hover:wl_underline">
|
||||
Forgot password?
|
||||
</NextLink>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -10,11 +10,9 @@ const iconSize = 24;
|
||||
export default function OAuthButton({
|
||||
provider,
|
||||
type,
|
||||
redirectUrl
|
||||
}: {
|
||||
provider: string,
|
||||
type: 'signin' | 'signup',
|
||||
redirectUrl?: string,
|
||||
}) {
|
||||
const stackApp = useStackApp();
|
||||
|
||||
|
||||
@ -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 (
|
||||
<div className="wl_space-y-4 wl_flex wl_flex-col wl_items-stretch">
|
||||
{project.oauthProviders.filter(p => p.enabled).map(p => (
|
||||
<OAuthButton key={p.id} provider={p.id} type={type} redirectUrl={redirectUrl} />
|
||||
<OAuthButton key={p.id} provider={p.id} type={type}/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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.");
|
||||
|
||||
@ -28,13 +28,15 @@ export type TokenStoreOptions<HasTokenStore extends boolean = boolean> =
|
||||
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>): 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<HasTokenStore extends boolean, ProjectId extends strin
|
||||
update(update) {
|
||||
return app._updateUser(update, tokenStore);
|
||||
},
|
||||
signOut(redirectUrl?: string) {
|
||||
return app._signOut(tokenStore, redirectUrl);
|
||||
signOut() {
|
||||
return app._signOut(tokenStore);
|
||||
},
|
||||
};
|
||||
Object.freeze(res);
|
||||
@ -414,7 +418,6 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
return neverResolve();
|
||||
}
|
||||
|
||||
async redirectToHandler() { return await this._redirectTo("handler"); }
|
||||
async redirectToSignIn() { return await this._redirectTo("signIn"); }
|
||||
async redirectToSignUp() { return await this._redirectTo("signUp"); }
|
||||
async redirectToSignOut() { return await this._redirectTo("signOut"); }
|
||||
@ -422,8 +425,10 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
async redirectToPasswordReset() { return await this._redirectTo("passwordReset"); }
|
||||
async redirectToForgotPassword() { return await this._redirectTo("forgotPassword"); }
|
||||
async redirectToHome() { return await this._redirectTo("home"); }
|
||||
async redirectToUserHome() { return await this._redirectTo("userHome"); }
|
||||
async redirectToOAuthCallback() { return await this._redirectTo("oauthCallback"); }
|
||||
async redirectToAfterSignIn() { return await this._redirectTo("afterSignIn"); }
|
||||
async redirectToAfterSignUp() { return await this._redirectTo("afterSignUp"); }
|
||||
async redirectToAfterSignOut() { return await this._redirectTo("afterSignOut"); }
|
||||
|
||||
async sendForgotPasswordEmail(email: string) {
|
||||
const redirectUrl = constructRedirectUrl(this.urls.passwordReset);
|
||||
@ -519,11 +524,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
async signInWithCredential(options: {
|
||||
email: string,
|
||||
password: string,
|
||||
redirectUrl?: string,
|
||||
}): Promise<SignInErrorCode | undefined> {
|
||||
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<HasTokenStore extends boolean, ProjectId extends strin
|
||||
async signUpWithCredential(options: {
|
||||
email: string,
|
||||
password: string,
|
||||
redirectUrl?: string,
|
||||
}): Promise<SignUpErrorCode | undefined>{
|
||||
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<HasTokenStore extends boolean, ProjectId extends strin
|
||||
});
|
||||
}
|
||||
|
||||
async callOAuthCallback(options: {
|
||||
redirectUrl?: string,
|
||||
} = {}) {
|
||||
async callOAuthCallback() {
|
||||
this._ensurePersistentTokenStore();
|
||||
const tokenStore = getTokenStore(this._tokenStoreOptions);
|
||||
await callOAuthCallback(this._interface, tokenStore, options.redirectUrl);
|
||||
const result = await callOAuthCallback(this._interface, tokenStore, this.urls.oauthCallback);
|
||||
return {
|
||||
newUser: !!result.newUser,
|
||||
};
|
||||
}
|
||||
|
||||
protected async _signOut(tokenStore: TokenStore, redirectUrl?: string): Promise<never> {
|
||||
redirectUrl = constructRedirectUrl(redirectUrl);
|
||||
protected async _signOut(tokenStore: TokenStore): Promise<void> {
|
||||
await this._interface.signOut(tokenStore);
|
||||
window.location.assign(redirectUrl);
|
||||
return await neverResolve();
|
||||
}
|
||||
|
||||
async signOut(redirectUrl: string): Promise<never> {
|
||||
async signOut(): Promise<void> {
|
||||
const user = await this.getUser();
|
||||
if (user) {
|
||||
await user.signOut(redirectUrl);
|
||||
await user.signOut();
|
||||
}
|
||||
window.location.assign(redirectUrl);
|
||||
return await neverResolve();
|
||||
}
|
||||
|
||||
async getProject(): Promise<ClientProjectJson> {
|
||||
@ -773,8 +766,8 @@ class _StackServerAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
await app._refreshUser(tokenStore);
|
||||
return res;
|
||||
},
|
||||
signOut(redirectUrl?: string) {
|
||||
return app._signOut(tokenStore, redirectUrl);
|
||||
signOut() {
|
||||
return app._signOut(tokenStore);
|
||||
},
|
||||
getClientUser() {
|
||||
return app._currentUserFromJson(json, tokenStore);
|
||||
@ -992,7 +985,7 @@ class _StackAdminAppImpl<HasTokenStore extends boolean, ProjectId extends string
|
||||
type Auth<T, C> = {
|
||||
readonly tokenStore: ReadonlyTokenStore,
|
||||
update(this: T, user: Partial<C>): Promise<void>,
|
||||
signOut(this: T, redirectUrl?: string): Promise<never>,
|
||||
signOut(this: T): Promise<void>,
|
||||
};
|
||||
|
||||
export type User = {
|
||||
@ -1118,9 +1111,9 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
|
||||
readonly urls: Readonly<HandlerUrls>,
|
||||
|
||||
signInWithOAuth(provider: string): Promise<void>,
|
||||
signInWithCredential(options: { email: string, password: string, redirectUrl?: string }): Promise<SignInErrorCode | undefined>,
|
||||
signUpWithCredential(options: { email: string, password: string, redirectUrl?: string }): Promise<SignUpErrorCode | undefined>,
|
||||
callOAuthCallback(options?: { redirectUrl?: string }): Promise<void>,
|
||||
signInWithCredential(options: { email: string, password: string }): Promise<SignInErrorCode | undefined>,
|
||||
signUpWithCredential(options: { email: string, password: string }): Promise<SignUpErrorCode | undefined>,
|
||||
callOAuthCallback(): Promise<{ newUser: boolean }>,
|
||||
sendForgotPasswordEmail(email: string): Promise<void>,
|
||||
resetPassword(options: { code: string, password: string }): Promise<PasswordResetLinkErrorCode | undefined>,
|
||||
verifyPasswordResetCode(code: string): Promise<PasswordResetLinkErrorCode | undefined>,
|
||||
@ -1131,7 +1124,7 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
|
||||
},
|
||||
}
|
||||
& AsyncStoreProperty<"project", ClientProjectJson, false>
|
||||
& { [K in `redirectTo${Capitalize<keyof Omit<HandlerUrls, 'oauthCallback'>>}`]: () => Promise<never> }
|
||||
& { [K in `redirectTo${Capitalize<keyof Omit<HandlerUrls, 'handler' | 'oauthCallback'>>}`]: () => Promise<never> }
|
||||
& (HasTokenStore extends false
|
||||
? {}
|
||||
: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user