Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/little-pets-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@stackframe/stack-shared": major
"@stackframe/stack": major
---

removed redirect URL in function options, redirect automatically to default URL
2 changes: 1 addition & 1 deletion apps/dev/src/app/signin/custom-credential.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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);
Expand Down
7 changes: 6 additions & 1 deletion apps/dev/src/app/signin/custom-oauth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
5 changes: 1 addition & 4 deletions apps/dev/src/app/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { SignIn } from "@stackframe/stack";
import { stackServerApp } from "src/stack";
import CustomCredentialSignIn from "./custom-credential";
import CustomOAuthSignIn from "./custom-oauth";

export default function Page() {
return <SignIn fullPage redirectUrl={stackServerApp.urls.home} />;
return <SignIn fullPage />;
}
2 changes: 1 addition & 1 deletion apps/dev/src/components/SignOutButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useUser, useStackApp } from "@stackframe/stack";
import { useUser } from "@stackframe/stack";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";

export default function SignOutButton() {
Expand Down
2 changes: 2 additions & 0 deletions apps/dev/src/stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ export const stackServerApp = new StackServerApp({
tokenStore: "nextjs-cookie",
urls: {
signIn: "/signin",
// afterSignIn: "/after-signin",
// afterSignUp: "/after-signup",
}
});
4 changes: 2 additions & 2 deletions docs/docs/01-getting-started/02-users.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Call `useUser` (or `getUser`) with the `{ or: 'redirect' }` option to protect th

## Signing out

You can sign out the user by redirecting them to `/handler/signout` or simply by calling `user.signOut()`.
You can sign out the user by redirecting them to `/handler/signout` or simply by calling `user.signOut()`. The user will be redirected to `afterSignOut` URL. you can customize it in the `StackServerApp` constructor (see [here](/docs/api-documentation/app)).


<Tabs>
Expand All @@ -93,7 +93,7 @@ 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={user?.signOut}>
Sign Out
</button>;
}
Expand Down
11 changes: 8 additions & 3 deletions docs/docs/02-advanced-guides/01-customization/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 />;
}
```

Expand Down Expand Up @@ -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>;
}
```
Expand All @@ -69,8 +72,9 @@ export default function CustomCredentialSignIn() {
setError('Please enter your password');
return;
}
const errorCode = await app.signInWithCredential({ email, password, redirectUrl: app.urls.userHome });
// It is better to handle each error code separately, but for simplicity in this example, we will just show the error code directly
// this will rediret to app.urls.afterSignIn if successful, you can customize it in the StackServerApp constructor
const errorCode = await app.signInWithCredential({ email, password });
// It is better to handle each error code separately, but we will just show the error code directly for simplicity here
if (errorCode) {
setError(errorCode);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ProjectUserAuthorizationCode" ADD COLUMN "newUser" BOOLEAN NOT NULL DEFAULT false;
1 change: 1 addition & 0 deletions packages/stack-server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ model ProjectUserAuthorizationCode {

codeChallenge String
codeChallengeMethod String
newUser Boolean

projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -114,16 +111,25 @@ 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,
oauthProviderConfigId: provider.id,
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: {
Expand All @@ -147,7 +153,8 @@ export const GET = smartRouteHandler(async (req: NextRequest, options: { params:
});

return {
id: account.projectUserId,
id: newAccount.projectUserId,
newUser: true
};
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/stack-server/src/oauth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,5 @@ export async function getAuthorizationCallback(

export const oauthServer = new OAuth2Server({
model: new OAuthModel(),
allowExtendedTokenAttributes: true,
});
11 changes: 7 additions & 4 deletions packages/stack-server/src/oauth/model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -164,6 +167,7 @@ export class OAuthModel implements AuthorizationCodeModel {
redirectUri: code.redirectUri,
expiresAt: code.expiresAt,
projectUserId: user.id,
newUser: user.newUser,
projectId: client.id,
},
});
Expand All @@ -177,9 +181,7 @@ export class OAuthModel implements AuthorizationCodeModel {
id: client.id,
grants: ["authorization_code", "refresh_token"],
},
user: {
id: user.id,
},
user,
};
}

Expand All @@ -205,6 +207,7 @@ export class OAuthModel implements AuthorizationCodeModel {
},
user: {
id: code.projectUserId,
newUser: code.newUser,
},
};
}
Expand Down
4 changes: 3 additions & 1 deletion packages/stack-server/src/stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { StackServerApp } from '@stackframe/stack';
export const stackServerApp = new StackServerApp({
tokenStore: "nextjs-cookie",
urls: {
userHome: "/projects",
afterSignIn: "/projects",
afterSignUp: "/projects",
afterSignOut: "/",
}
});
2 changes: 2 additions & 0 deletions packages/stack-shared/src/interface/clientInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
1 change: 0 additions & 1 deletion packages/stack/src/components/EmailVerification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export default function EmailVerification({
}: {
searchParams?: Record<string, string>,
fullPage?: boolean,
redirectUrl?: string,
}) {
const stackApp = useStackApp();

Expand Down
3 changes: 0 additions & 3 deletions packages/stack/src/components/OAuthCallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@ 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);
}), []);

return <MessageCard title='Redirecting...' fullPage />;
Expand Down
6 changes: 3 additions & 3 deletions packages/stack/src/components/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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>
);
Expand Down
7 changes: 4 additions & 3 deletions packages/stack/src/components/SignOut.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'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(user.signOut());
}

return <GoHomeMessageCard type='signedOut' fullPage />;
Expand Down
7 changes: 3 additions & 4 deletions packages/stack/src/components/SignUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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>
);
Expand Down
8 changes: 4 additions & 4 deletions packages/stack/src/components/StackHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down
Loading