0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

feat(console): add next server actions guide (#5161)

feat(console): add next server actions guilde
This commit is contained in:
wangsijie 2023-12-26 13:29:52 +08:00 committed by GitHub
parent 3fb0fbf628
commit 2c26d2a825
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 389 additions and 1 deletions

View file

@ -22,6 +22,7 @@ import webGo from './web-go/index';
import webGptPlugin from './web-gpt-plugin/index';
import webNext from './web-next/index';
import webNextAppRouter from './web-next-app-router/index';
import webNextServerActions from './web-next-server-actions/index';
import webOutline from './web-outline/index';
import webPhp from './web-php/index';
import webPython from './web-python/index';
@ -42,6 +43,13 @@ const guides: Readonly<Guide[]> = Object.freeze([
Component: lazy(async () => import('./spa-react/README.mdx')),
metadata: spaReact,
},
{
order: 1.1,
id: 'web-next-server-actions',
Logo: lazy(async () => import('./web-next-server-actions/logo.svg')),
Component: lazy(async () => import('./web-next-server-actions/README.mdx')),
metadata: webNextServerActions,
},
{
order: 1.1,
id: 'web-next-app-router',

View file

@ -0,0 +1,351 @@
import UriInputField from '@/mdx-components/UriInputField';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
import InlineNotification from '@/ds-components/InlineNotification';
import { generateStandardSecret } from '@logto/shared/universal';
import Steps from '@/mdx-components/Steps';
import Step from '@/mdx-components/Step';
<Steps>
<Step
title="Add Logto SDK as a dependency"
subtitle="Please select your favorite package manager"
>
<Tabs>
<TabItem value="npm" label="npm">
```bash
npm i @logto/next
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn add @logto/next
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @logto/next
```
</TabItem>
</Tabs>
</Step>
<Step
title="Init LogtoClient"
>
<InlineNotification>
In the following steps, we assume your app is running on <code>http://localhost:3000</code>.
</InlineNotification>
Import and initialize LogtoClient:
<pre>
<code className="language-ts">
{`// libraries/logto.js
'use server';
import LogtoClient from '@logto/next/server-actions';
const config = {
endpoint: '${props.endpoint}',
appId: '${props.app.id}',
appSecret: '${props.app.secret}',
baseUrl: 'http://localhost:3000', // Change to your own base URL
cookieSecret: '${generateStandardSecret()}', // Auto-generated 32 digit secret
cookieSecure: process.env.NODE_ENV === 'production',
};
export const logtoClient = new LogtoClient(config);`}
</code>
</pre>
Then use `next/headers` to add utils to manage cookies.
Add an import line:
```ts
import { cookies } from 'next/headers';
```
Add the following code:
```ts
const cookieName = `logto:${config.appId}`;
const setCookies = (value?: string) => {
if (value === undefined) {
return;
}
cookies().set(cookieName, value, {
maxAge: 14 * 3600 * 24,
secure: config.cookieSecure,
});
};
const getCookie = () => {
return cookies().get(cookieName)?.value ?? '';
};
```
</Step>
<Step
title="Sign in"
>
### Configure Redirect URI
First, lets enter your redirect URI. E.g. `http://localhost:3000/callback`.
<UriInputField name="redirectUris" />
### Prepare sign in and callback functions
Continue to add the following code to `libraries/logto.js`:
```ts
export const signIn = async () => {
const { url, newCookie } = await logtoClient.handleSignIn(
getCookie(),
`${config.baseUrl}/callback`
);
setCookies(newCookie);
return url;
};
export const handleSignIn = async (searchParams: Record<string, string>) => {
// Convert searchParams object into a query string.
const search = new URLSearchParams(searchParams).toString();
const newCookie = await logtoClient.handleSignInCallback(
getCookie(),
`${config.baseUrl}/callback?${search}`
);
setCookies(newCookie);
};
```
### Implement sign-in button
The sign-in button will call the method we just created, it is a client component:
```tsx
// app/sign-in.tsx
'use client';
import { useRouter } from 'next/navigation';
import { signIn } from '../libraries/logto';
const SignIn = () => {
const router = useRouter();
const handleClick = async () => {
const redirectUrl = await signIn();
router.push(redirectUrl);
};
return <button onClick={handleClick}>Sign in</button>;
};
export default SignIn;
```
### Implement callback page
Add a callback page to your app:
```tsx
// pages/callback/page.tsx
'use client';
import { useRouter } from 'next/navigation';
import { handleSignIn } from '../../libraries/logto';
import { useEffect } from 'react';
type Props = {
searchParams: Record<string, string>;
};
export default function Callback({ searchParams }: Props) {
const router = useRouter();
useEffect(() => {
handleSignIn(searchParams).then(() => {
router.push('/');
});
}, [router, searchParams]);
return <div>Signing in...</div>;
}
```
### Add sign in button to home page
We're almost there! In the last step, we will add the sign-in button to home page:
```tsx
import SignIn from './sign-in';
export default async function Home() {
return (
<main>
<h1>Hello Logto.</h1>
<div><SignIn /></div>
</main>
);
}
```
Now you will be navigated to Logto sign-in page when you click the button.
</Step>
<Step
title="Get user profile"
>
We'll use "async component" to get user profile, check the [Data Fetching](https://nextjs.org/docs/app/building-your-application/data-fetching) doc to learn more.
### Create `getLogtoContext` helper
Add the following code to `libraries/logto.js`:
```ts
export const getLogtoContext = async () => {
return await logtoClient.getLogtoContext(getCookie());
};
```
### Fetch authentication data
The home page we created in the prev step is an async component, we can fetch user auth context in it, the new code is:
```tsx
import { getLogtoContext } from '../libraries/logto';
import SignIn from './sign-in';
export default async function Home() {
const { isAuthenticated, claims } = await getLogtoContext();
return (
<main className={styles.main}>
<h1>Hello Logto.</h1>
<div>{!isAuthenticated && <SignIn />}</div>
{claims && (
<div>
<h2>Claims:</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{Object.entries(claims).map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
);
}
```
</Step>
<Step
title="Protect pages"
>
Protect pages is easy, just use the value `isAuthenticated` we got from `getLogtoContext`.
</Step>
<Step
title="Sign out"
>
### Configure URI
After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below.
<UriInputField name="postLogoutRedirectUris" />
### Prepare sign out function
Continue to add the following code to `libraries/logto.js`:
```ts
export const signOut = async () => {
const url = await logtoClient.handleSignOut(getCookie());
setCookies('');
return url;
};
```
### Implement a sign-out button
```tsx
// app/sign-out.tsx
'use client';
import { useRouter } from 'next/navigation';
import { signOut } from '../libraries/logto';
const SignOut = () => {
const router = useRouter();
const handleClick = async () => {
const redirectUrl = await signOut();
router.push(redirectUrl);
};
return <button onClick={handleClick}>Sign out</button>;
};
export default SignOut;
```
### Add sign out button to home page
```tsx
// ...
import SignOut from './sign-out';
// ...
export default async function Home() {
// ...
return (
<main>
<h1>Hello Logto.</h1>
<div>{isAuthenticated ? <SignOut /> : <SignIn />}</div>
{/* ... */}
</main>
);
}
```
</Step>
</Steps>

View file

@ -0,0 +1,3 @@
{
"order": 1.1
}

View file

@ -0,0 +1,16 @@
import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'Next.js (Server Actions)',
description:
'Next.js with Server Actions, leverages async component from the latest features of React.',
target: ApplicationType.Traditional,
sample: {
repo: 'js',
path: 'packages/next-server-actions-sample',
},
});
export default metadata;

View file

@ -0,0 +1,10 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_134_42589)">
<path d="M31.7494 38.005C31.6304 38.068 31.6404 38.088 31.7544 38.03C31.7914 38.015 31.8224 37.994 31.8494 37.969C31.8494 37.948 31.8494 37.948 31.7494 38.005ZM31.9894 37.875C31.9324 37.922 31.9324 37.922 32.0004 37.891C32.0364 37.87 32.0684 37.85 32.0684 37.844C32.0684 37.817 32.0524 37.823 31.9894 37.875ZM32.1454 37.781C32.0884 37.828 32.0884 37.828 32.1564 37.797C32.1934 37.776 32.2244 37.754 32.2244 37.749C32.2244 37.724 32.2084 37.729 32.1454 37.781ZM32.3034 37.688C32.2464 37.735 32.2464 37.735 32.3124 37.703C32.3494 37.683 32.3804 37.662 32.3804 37.656C32.3804 37.631 32.3644 37.636 32.3034 37.688ZM32.5164 37.547C32.4074 37.62 32.3694 37.667 32.4694 37.615C32.5364 37.574 32.6504 37.484 32.6304 37.484C32.5874 37.5 32.5514 37.527 32.5154 37.547H32.5164ZM22.9534 8.01101C22.8804 8.01601 22.6614 8.03601 22.4694 8.05201C17.9213 8.46401 13.6663 10.912 10.9693 14.683C9.47826 16.75 8.51025 19.151 8.14525 21.672C8.01624 22.552 8.00024 22.812 8.00024 24.005C8.00024 25.197 8.01624 25.453 8.14525 26.333C9.01625 32.344 13.2923 37.39 19.0883 39.26C20.1313 39.593 21.2243 39.823 22.4694 39.964C22.9534 40.016 25.0464 40.016 25.5304 39.964C27.6824 39.724 29.4994 39.193 31.2974 38.276C31.5734 38.136 31.6254 38.099 31.5884 38.068C30.7084 36.907 29.8444 35.745 28.9794 34.573L26.4224 31.12L23.2194 26.375C22.1514 24.787 21.0793 23.203 19.9903 21.631C19.9793 21.631 19.9653 23.74 19.9593 26.312C19.9483 30.817 19.9483 31 19.8913 31.104C19.8343 31.229 19.7403 31.333 19.6153 31.391C19.5163 31.438 19.4273 31.448 18.9543 31.448H18.4133L18.2723 31.36C18.1843 31.303 18.1113 31.224 18.0643 31.131L17.9963 30.99L18.0013 24.719L18.0123 18.448L18.1113 18.323C18.1743 18.246 18.2523 18.183 18.3403 18.136C18.4713 18.073 18.5233 18.063 19.0643 18.063C19.6993 18.063 19.8043 18.088 19.9713 18.271C21.2673 20.203 22.5594 22.14 23.8304 24.083C25.9094 27.235 28.7474 31.536 30.1424 33.646L32.6794 37.485L32.8044 37.402C34.0234 36.589 35.1325 35.621 36.0895 34.517C38.1055 32.209 39.4135 29.37 39.8565 26.34C39.9855 25.46 40.0015 25.199 40.0015 24.007C40.0015 22.814 39.9855 22.559 39.8565 21.679C38.9855 15.668 34.7095 10.622 28.9134 8.75101C27.8294 8.40801 26.7144 8.17401 25.5854 8.05401C25.2824 8.02301 23.2144 7.98601 22.9544 8.01301L22.9534 8.01101ZM29.5004 17.688C29.6514 17.76 29.7654 17.896 29.8174 18.052C29.8444 18.136 29.8494 19.875 29.8444 23.792L29.8334 29.416L28.8444 27.896L27.8494 26.375V22.292C27.8494 19.645 27.8604 18.161 27.8744 18.088C27.9214 17.921 28.0354 17.781 28.1874 17.693C28.3114 17.63 28.3594 17.625 28.8544 17.625C29.3174 17.625 29.3954 17.63 29.4994 17.688H29.5004Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_134_42589">
<rect width="32.0003" height="32" fill="white" transform="translate(8.00024 8)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,3 +1,3 @@
{
"order": 1
"order": 1.2
}