Skip to content

Commit

Permalink
Merge pull request #733 from supertokens/next13-custom-ui
Browse files Browse the repository at this point in the history
Add custom ui snippets for next integration docs with app directory
  • Loading branch information
rishabhpoddar authored Oct 23, 2023
2 parents 7e6cb7a + 5e82689 commit 59d0b35
Show file tree
Hide file tree
Showing 10 changed files with 1,274 additions and 2 deletions.
153 changes: 153 additions & 0 deletions v2/emailpassword/nextjs/app-directory/protecting-route.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@ hide_title: true
<!-- COPY DOCS -->
<!-- ./thirdpartyemailpassword/nextjs/app-directory/protecting-route.mdx -->

import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher"

# 5. Checking for sessions in frontend routes

Protecting a website route means that it cannot be accessed unless a user is signed in. If a non signed in user tries to access it, they will be redirected to the login page.

<PreBuiltOrCustomUISwitcher>

<PreBuiltUIContent>

## Sessions with Client Components

Lets create a client component for the `/` route of our website.
Expand Down Expand Up @@ -253,3 +259,150 @@ You should be redirected to the login page. After that, sign in, and then visit
:::important
An example of this can be seen [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/page.tsx).
:::

</PreBuiltUIContent>

<CustomUIContent>

## Sessions with Client Components

Checking for sessions in client components involves:

- Using the `Session` recipe to manually check if a session exists, rendering some default UI while you check.
- Render your UI if a session exists.

To learn more about how to do this refer to [this page](../../common-customizations/sessions/protecting-frontend-routes.mdx).

## Sessions with Server Components

### Creating a helper component for session refreshing

Lets start by creating a component that will refresh the session if it exists and has expired.

```tsx title="app/components/tryRefreshClientComponent.tsx"
"use client";

import { useEffect, useState } from "react";
import { useRouter, redirect } from "next/navigation";
import Session from "supertokens-web-js/recipe/session";

export const TryRefreshComponent = () => {
const router = useRouter();
const [didError, setDidError] = useState(false);

useEffect(() => {
void Session.attemptRefreshingSession()
.then((hasSession) => {
if (hasSession) {
router.refresh();
} else {
/**
* This means that the session is expired and cannot be refreshed.
* In this example we redirect the user back to the login page.
*/
redirect("/auth");
}
})
.catch(() => {
setDidError(true);
});
}, []);

if (didError) {
return <div>Something went wrong, please reload the page</div>;
}

return <div>Loading...</div>;
};
```

`Session.attemptRefreshingSession` will call the refresh endpoint. `hasSession` will be:
- `true` if the session was refreshed
- `false` if the session could not be refreshed

### Modify home page to check for sessions

Lets modify the Home page server component we created earlier:

```tsx title="app/components/home.tsx"
import { NextRequest, NextResponse } from "next/server";// typecheck-only, removed from output
import { SessionContainer, VerifySessionOptions } from "supertokens-node/recipe/session";// typecheck-only, removed from output
import { CollectingResponse } from "supertokens-node/framework/custom";// typecheck-only, removed from output
declare let getSSRSession: (req?: NextRequest, options?: VerifySessionOptions) => Promise<{session: SessionContainer | undefined;hasToken: boolean;hasInvalidClaims: boolean;baseResponse: CollectingResponse;nextResponse?: NextResponse;}>; // typecheck-only, removed from output
declare let TryRefreshComponent: any; // typecheck-only, removed from output
import { redirect } from "next/navigation";
// @ts-ignore
import { getSSRSession } from "../sessionUtils";
// @ts-ignore
import { TryRefreshComponent } from "./tryRefreshClientComponent";

export async function HomePage() {
const { session, hasToken, hasInvalidClaims } = await getSSRSession();

// `session` will be undefined if it does not exist or has expired
if (!session) {
if (!hasToken) {
/**
* This means that the user is not logged in. If you want to display some other UI in this
* case, you can do so here.
*/
return redirect("/auth");
}

/**
* `hasInvalidClaims` indicates that session claims did not pass validation. For example if email
* verification is required but the user's email has not been verified.
*/
if (hasInvalidClaims) {
/**
* This means that one of the session claims is invalid. You should redirect the user to
* the appropriate page depending on which claim is invalid.
*/
return <div>Invalid Session Claims</div>
} else {
/**
* This means that the session does not exist but we have session tokens for the user. In this case
* the `TryRefreshComponent` will try to refresh the session.
*/
return <TryRefreshComponent />;
}
}

return (
<div>
Your user id is: {session.getUserId()}
</div>
);
}
```

`getSSRSession` is a utility function we created in the [previous step](./session-helpers.mdx). The `TryRefreshComponent` is a client component that checks if a session exists and tries to refresh the session if it is expired.

`hasInvalidClaims` indicates that one or more of the session claims failed their validation. In this case you should check which session claim failed and redirect the user accordingly. For example to check for the email verification claim you can refer to [this page](../../common-customizations/email-verification/protecting-routes.mdx#protecting-frontend-routes--cust).

And then we can modify the `/app/page.tsx` file to use our server component

```tsx title="app/page.tsx"
declare let HomePage: any; // typecheck-only, removed from output
import styles from './page.module.css'
// @ts-ignore
import { HomePage } from "./components/home";

export default function Home() {
return (
<main className={styles.main}>
<HomePage />
</main>
)
}
```

:::tip Test by navigating to `/`
You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection.

For custom UI SuperTokens provides no login UI, the code above will redirect the user to the `/auth` route but you will have to build some UI that is served on that route.
:::

</CustomUIContent>

</PreBuiltOrCustomUISwitcher>
101 changes: 101 additions & 0 deletions v2/emailpassword/nextjs/app-directory/server-components-requests.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@ hide_title: true
<!-- COPY DOCS -->
<!-- ./thirdpartyemailpassword/nextjs/app-directory/server-components-requests.mdx -->

import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher"

# 7. Making requests from Server Components

Lets modify the Home page we made in a [previous step](../protecting-route.mdx) to make a call to this API

<PreBuiltOrCustomUISwitcher>

<PreBuiltUIContent>

```tsx title="app/components/home.tsx"
import { NextRequest, NextResponse } from "next/server";// typecheck-only, removed from output
import { SessionContainer, VerifySessionOptions } from "supertokens-node/recipe/session";// typecheck-only, removed from output
Expand Down Expand Up @@ -81,3 +87,98 @@ export async function HomePage() {
```

We use `session` returned by `getSSRSession` to get the access token of the user. We can then send the access token as a header to the API. When the API calls `withSession` it will try to read the access token from the headers and if a session exists it will return the information. You can use the `session` object to fetch other information such as `session.getUserId()`.

</PreBuiltUIContent>

<CustomUIContent>

```tsx title="app/components/home.tsx"
import { NextRequest, NextResponse } from "next/server";// typecheck-only, removed from output
import { SessionContainer, VerifySessionOptions } from "supertokens-node/recipe/session";// typecheck-only, removed from output
import { CollectingResponse } from "supertokens-node/framework/custom";// typecheck-only, removed from output
declare let getSSRSession: (req?: NextRequest, options?: VerifySessionOptions) => Promise<{session: SessionContainer | undefined;hasToken: boolean;hasInvalidClaims: boolean;baseResponse: CollectingResponse;nextResponse?: NextResponse;}>; // typecheck-only, removed from output
declare let TryRefreshComponent: any; // typecheck-only, removed from output
import { redirect } from "next/navigation";
// @ts-ignore
import { getSSRSession } from "../sessionUtils";
// @ts-ignore
import { TryRefreshComponent } from "./tryRefreshClientComponent";

export async function HomePage() {
const { session, hasToken, hasInvalidClaims } = await getSSRSession();

// `session` will be undefined if it does not exist or has expired
if (!session) {
if (!hasToken) {
/**
* This means that the user is not logged in. If you want to display some other UI in this
* case, you can do so here.
*/
return redirect("/auth");
}

/**
* `hasInvalidClaims` indicates that session claims did not pass validation. For example if email
* verification is required but the user's email has not been verified.
*/
if (hasInvalidClaims) {
/**
* This means that one of the session claims is invalid. You should redirect the user to
* the appropriate page depending on which claim is invalid.
*/
return <div>Invalid Session Claims</div>
} else {
/**
* This means that the session does not exist but we have session tokens for the user. In this case
* the `TryRefreshComponent` will try to refresh the session.
*/
return <TryRefreshComponent />;
}
}

// highlight-start
const userInfoResponse = await fetch('http://localhost:3000/api/user', {
headers: {
/**
* We read the access token from the session and use that as a Bearer token when
* making network requests.
*/
Authorization: 'Bearer ' + session.getAccessToken(),
},
});

let message = "";

if (userInfoResponse.status === 200) {
message = `Your user id is: ${session.getUserId()}`
} else if (userInfoResponse.status === 500) {
message = "Something went wrong"
} else if (userInfoResponse.status === 401) {
// The TryRefreshComponent will try to refresh the session
return <TryRefreshComponent />
} else if (userInfoResponse.status === 403) {
/**
* This means that one of the session claims is invalid. You should redirect the user to
* the appropriate page depending on which claim is invalid.
*/
return <div>Invalid Session Claims</div>;
}

// You can use `userInfoResponse` to read the users session information
// highlight-end

return (
<div>
{message}
</div>
);
}
```

APIs that require sessions will return status:
- `401` if there is no valid session or if the session has expired. In this case we return the `TryRefreshComponent` component which will try to refresh the session or redirect to the login page if the session cant be refreshed.
- `403` if one or more of the session claims fail their validation. In this case you should check which session claim failed and redirect the user accordingly. For example to check for the email verification claim you can refer to [this page](../../common-customizations/email-verification/protecting-routes.mdx#protecting-frontend-routes--cust).

</CustomUIContent>

</PreBuiltOrCustomUISwitcher>
Loading

0 comments on commit 59d0b35

Please sign in to comment.