removed redirectUrl in functions, added newUser attribute to signInWithOAuth, added after sign-in/up/out urls

This commit is contained in:
Zai Shi 2024-03-12 17:31:28 +08:00
parent f13a71ec56
commit c0151a8e5d
26 changed files with 144 additions and 119 deletions

View File

@ -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();
}
};

View File

@ -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>;
}

View File

@ -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>);
}

View File

@ -6,5 +6,7 @@ export const stackServerApp = new StackServerApp({
tokenStore: "nextjs-cookie",
urls: {
signIn: "/signin",
afterSignIn: "/after-signin",
afterSignUp: "/after-signup",
}
});

View File

@ -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>;
}

View File

@ -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>;
}
```

View File

@ -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 (

View File

@ -147,6 +147,7 @@ model ProjectUserAuthorizationCode {
codeChallenge String
codeChallengeMethod String
newUser Boolean
projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade)

View File

@ -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'
>

View File

@ -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
};
}
}

View File

@ -104,4 +104,5 @@ export async function getAuthorizationCallback(
export const oauthServer = new OAuth2Server({
model: new OAuthModel(),
allowExtendedTokenAttributes: true,
});

View File

@ -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,
},
};
}

View File

@ -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> {

View File

@ -14,7 +14,6 @@ export default function EmailVerification({
}: {
searchParams?: Record<string, string>,
fullPage?: boolean,
redirectUrl?: string,
}) {
const stackApp = useStackApp();

View File

@ -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 />;

View File

@ -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>
);

View File

@ -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 />;

View File

@ -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>
);

View File

@ -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');

View File

@ -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>

View File

@ -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();
}
}
};

View File

@ -10,11 +10,9 @@ const iconSize = 24;
export default function OAuthButton({
provider,
type,
redirectUrl
}: {
provider: string,
type: 'signin' | 'signup',
redirectUrl?: string,
}) {
const stackApp = useStackApp();

View File

@ -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>
);

View File

@ -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";

View File

@ -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.");

View File

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