From 7d3f94738f495de98464d23b6fdf18214d59005e Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Fri, 29 Jul 2022 23:36:02 +0800 Subject: [PATCH] feat(console): add Next.js integration guide in admin console --- .../docs/tutorial/integrate-sdk/next.mdx | 222 ++++++++++++++++++ .../tutorial/integrate-sdk/next_zh-cn.mdx | 222 ++++++++++++++++++ .../Applications/components/Guide/index.tsx | 2 + .../components/GuideHeader/index.tsx | 2 + packages/console/src/types/applications.ts | 3 +- .../translation/admin-console/applications.ts | 2 +- .../translation/admin-console/applications.ts | 2 +- packages/shared/package.json | 9 +- packages/shared/src/utilities/index.ts | 1 + packages/shared/src/utilities/string.ts | 6 + pnpm-lock.yaml | 2 + 11 files changed, 466 insertions(+), 7 deletions(-) create mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/next.mdx create mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/next_zh-cn.mdx create mode 100644 packages/shared/src/utilities/string.ts diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/next.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/next.mdx new file mode 100644 index 000000000..9e057d559 --- /dev/null +++ b/packages/console/src/assets/docs/tutorial/integrate-sdk/next.mdx @@ -0,0 +1,222 @@ +import UriInputField from '@mdx/components/UriInputField'; +import Step from '@mdx/components/Step'; +import Tabs from '@mdx/components/Tabs'; +import TabItem from '@mdx/components/TabItem'; +import Alert from '@/components/Alert'; +import { generateRandomString } from '@logto/shared'; + + props.onNext(1)} +> + + + +```bash +npm i @logto/next +``` + + + + +```bash +yarn add @logto/next +``` + + + + +```bash +pnpm add @logto/next +``` + + + + + + props.onNext(2)} +> + + + In the following steps, we assume your app is running on http://localhost:3000. + + +Import and initialize LogtoClient: + +
+
+{`// libraries/logto.js
+import { LogtoProvider, LogtoConfig } from '@logto/next';
+
+export const logtoClient = new LogtoClient({
+  endpoint: '${props.endpoint}',
+  appId: '${props.appId}',
+  baseUrl: 'http://localhost:3000', // Change to your own base URL
+  cookieSecret: '${generateRandomString(32)}', // Auto-generated 32 digit secret
+  cookieSecure: process.env.NODE_ENV === 'production',
+});`}
+
+
+ +
+ + props.onNext(3)} +> + +### Configure Redirect URI + +First, let’s enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`. + + + +### Prepare API routes + +Prepare [API routes](https://nextjs.org/docs/api-routes/introduction) to connect with Logto. + +Go back to your IDE/editor, use the following code to implement the API routes first: + +
+
+{`// pages/api/logto/[action].ts
+import { logtoClient } from '../../../libraries/logto';
+
+export default logtoClient.handleAuthRoutes();`}
+
+
+ +This will create 4 routes automatically: + +1. `/api/logto/sign-in`: Sign in with Logto. +2. `/api/logto/sign-in-callback`: Handle sign-in callback. +3. `/api/logto/sign-out`: Sign out with Logto. +4. `/api/logto/user`: Check if user is authenticated with Logto, if yes, return user info. + +### Implement sign-in button + +We're almost there! In the last step, we will create a sign-in button: + +```tsx +import { useRouter } from 'next/router'; + +const { push } = useRouter(); + + +``` + +Now you will be navigated to Logto sign-in page when you click the button. + +
+ + props.onNext(4)} +> + +### Through API request in the frontend + +You can fetch user info by calling `/api/logto/user`. + +```tsx +import { LogtoUser } from '@logto/next'; +import useSWR from 'swr'; + +const Home = () => { + const { data } = useSWR('/api/logto/user'); + + return
User ID: {data?.claims?.sub}
; +}; + +export default Profile; +``` + +Check [this guide](https://swr.vercel.app/docs/getting-started) to learn more about `useSWR`. + +### Through `getServerSideProps` in the backend + +```tsx +import { LogtoUser } from '@logto/next'; +import { logtoClient } from '../libraries/logto'; + +type Props = { + user: LogtoUser; +}; + +const Profile = ({ user }: Props) => { + return
User ID: {user.claims?.sub}
; +}; + +export default Profile; + +export const getServerSideProps = logtoClient.withLogtoSsr(({ request }) => { + const { user } = request; + + return { + props: { user }, + }; +}); +``` + +Check [Next.js documentation](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) for more details on `getServerSideProps`. + +
+ + props.onNext(5)} +> + +Calling `.signOut()` will clear all the Logto data in memory and cookies if they exist. + +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 before calling `api/logto/sign-out`. + + + +### Implement a sign-out button + +
+
+{``}
+
+
+ +
+ + + +- [Customize sign-in experience](https://docs.logto.io/docs/recipes/customize-sie) +- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/enable-passcode-sign-in) +- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/enable-social-sign-in) +- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api) + + diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/next_zh-cn.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/next_zh-cn.mdx new file mode 100644 index 000000000..192719fae --- /dev/null +++ b/packages/console/src/assets/docs/tutorial/integrate-sdk/next_zh-cn.mdx @@ -0,0 +1,222 @@ +import UriInputField from '@mdx/components/UriInputField'; +import Step from '@mdx/components/Step'; +import Tabs from '@mdx/components/Tabs'; +import TabItem from '@mdx/components/TabItem'; +import Alert from '@/components/Alert'; +import { generateRandomString } from '@logto/shared'; + + props.onNext(1)} +> + + + +```bash +npm i @logto/next +``` + + + + +```bash +yarn add @logto/next +``` + + + + +```bash +pnpm add @logto/next +``` + + + + + + props.onNext(2)} +> + + + 在如下代码示例中, 我们均先假设你的 React 应用运行在 http://localhost:3000 上。 + + +引入并实例化 LogtoClient: + +
+
+{`// libraries/logto.js
+import { LogtoProvider, LogtoConfig } from '@logto/next';
+
+export const logtoClient = new LogtoClient({
+  endpoint: '${props.endpoint}',
+  appId: '${props.appId}',
+  baseUrl: 'http://localhost:3000', // 你可以修改为自己真实的 URL
+  cookieSecret: '${generateRandomString(32)}', // Logto 自动帮你生成的 32 位密钥
+  cookieSecure: process.env.NODE_ENV === 'production',
+});`}
+
+
+ +
+ + props.onNext(3)} +> + +### 配置 Redirect URI + +首先,我们来添加 Redirect URI,如:`http://localhost:3000/api/logto/sign-in-callback`. + + + +### 准备 API 路由 + +实现与 Logto 后台交互的 [API 路由](https://nextjs.org/docs/api-routes/introduction)。 + +返回你的 IDE 或编辑器,首先让我们使用如下代码来实现一组 API 路由: + +
+
+{`// pages/api/logto/[action].ts
+import { logtoClient } from '../../../libraries/logto';
+
+export default logtoClient.handleAuthRoutes();`}
+
+
+ +这将为你自动创建好 4 个路由,分别是: + +1. `/api/logto/sign-in`:登录 +2. `/api/logto/sign-in-callback`:处理登录重定向 +3. `/api/logto/sign-out`:登出 +4. `/api/logto/user`:检查用户是否以登录到 Logto。如果是,则返回用户信息。 + +### 实现登录按钮 + +马上就要大功告成!在这最后一步,我们将用如下代码实现一个登录按钮: + +```tsx +import { useRouter } from 'next/router'; + +const { push } = useRouter(); + + +``` + +现在你可以尝试点击登录按钮了,点击之后页面会跳转到 Logto 的登录界面。 + +
+ + props.onNext(4)} +> + +### 通过前端发送 API 请求获取 + +你可以调用 `/api/logto/user` 接口来获取用户信息,如: + +```tsx +import { LogtoUser } from '@logto/next'; +import useSWR from 'swr'; + +const Home = () => { + const { data } = useSWR('/api/logto/user'); + + return
用户 ID:{data?.claims?.sub}
; +}; + +export default Profile; +``` + +你可以查看 [这篇教程](https://swr.vercel.app/docs/getting-started) 来了解有关 `useSWR` 的更多信息。 + +### 通过后端的 `getServerSideProps` 方法获取 + +```tsx +import { LogtoUser } from '@logto/next'; +import { logtoClient } from '../libraries/logto'; + +type Props = { + user: LogtoUser; +}; + +const Profile = ({ user }: Props) => { + return
用户 ID:{user.claims?.sub}
; +}; + +export default Profile; + +export const getServerSideProps = logtoClient.withLogtoSsr(({ request }) => { + const { user } = request; + + return { + props: { user }, + }; +}); +``` + +查看 [Next.js 官方文档](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) 来了解有关 `getServerSideProps` 的更多信息。 + +
+ + props.onNext(5)} +> + +调用 `.signOut()` 将清理内存与 cookies 中的所有 Logto 数据(如果有)。 + +在退出登录后,让你的用户重新回到你的网站是个不错的选择。在调用 `api/logto/sign-out` 发起退出登录操作之前,让我们先将 `http://localhost:3000` 添加至下面的输入框。 + + + +### 实现退出登录按钮 + +
+
+{``}
+
+
+ +
+ + + +- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie) +- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in) +- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in) +- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api) + + diff --git a/packages/console/src/pages/Applications/components/Guide/index.tsx b/packages/console/src/pages/Applications/components/Guide/index.tsx index 5882b3791..8c64c1529 100644 --- a/packages/console/src/pages/Applications/components/Guide/index.tsx +++ b/packages/console/src/pages/Applications/components/Guide/index.tsx @@ -26,6 +26,7 @@ const Guides: Record JSX.Elemen vue: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/vue.mdx')), vanilla: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/vanilla.mdx')), express: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/express.mdx')), + next: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/next.mdx')), 'ios_zh-cn': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/ios_zh-cn.mdx')), 'android_zh-cn': lazy( async () => import('@/assets/docs/tutorial/integrate-sdk/android_zh-cn.mdx') @@ -38,6 +39,7 @@ const Guides: Record JSX.Elemen 'express_zh-cn': lazy( async () => import('@/assets/docs/tutorial/integrate-sdk/express_zh-cn.mdx') ), + 'next_zh-cn': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/next_zh-cn.mdx')), }; const Guide = ({ app, isCompact, onClose }: Props) => { diff --git a/packages/console/src/pages/Applications/components/GuideHeader/index.tsx b/packages/console/src/pages/Applications/components/GuideHeader/index.tsx index 86bf2c607..2f99c41f3 100644 --- a/packages/console/src/pages/Applications/components/GuideHeader/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideHeader/index.tsx @@ -34,6 +34,8 @@ const getSampleProjectUrl = (sdk: SupportedSdk) => { return `${githubUrlPrefix}/js/tree/master/packages/vue-sample`; case SupportedSdk.Vanilla: return `${githubUrlPrefix}/js/tree/master/packages/browser-sample`; + case SupportedSdk.Next: + return `${githubUrlPrefix}/js/tree/master/packages/next-sample`; case SupportedSdk.Express: return `${githubUrlPrefix}/express-example`; default: diff --git a/packages/console/src/types/applications.ts b/packages/console/src/types/applications.ts index f5b88ee04..966f32ba5 100644 --- a/packages/console/src/types/applications.ts +++ b/packages/console/src/types/applications.ts @@ -13,10 +13,11 @@ export enum SupportedSdk { Vue = 'Vue', Vanilla = 'Vanilla', Express = 'Express', + Next = 'Next', } export const applicationTypeAndSdkTypeMappings = Object.freeze({ [ApplicationType.Native]: [SupportedSdk.iOS, SupportedSdk.Android], [ApplicationType.SPA]: [SupportedSdk.React, SupportedSdk.Vue, SupportedSdk.Vanilla], - [ApplicationType.Traditional]: [SupportedSdk.Express], + [ApplicationType.Traditional]: [SupportedSdk.Next, SupportedSdk.Express], } as const); diff --git a/packages/phrases/src/locales/en/translation/admin-console/applications.ts b/packages/phrases/src/locales/en/translation/admin-console/applications.ts index 033d4adcd..a05c3f864 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/applications.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/applications.ts @@ -26,7 +26,7 @@ const applications = { traditional: { title: 'Traditional Web', subtitle: 'An app that renders and updates pages by the web server alone', - description: 'E.g., JSP, PHP', + description: 'E.g., Next.js, PHP', }, }, guide: { diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/applications.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/applications.ts index 8eea19526..6c1f92cd5 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/applications.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/applications.ts @@ -24,7 +24,7 @@ const applications = { traditional: { title: '传统网页应用', subtitle: '仅由 Web 服务器渲染和更新的应用程序', - description: '例如 JSP, PHP', + description: '例如 Next.js, PHP', }, }, guide: { diff --git a/packages/shared/package.json b/packages/shared/package.json index 05cce42f7..89306cb40 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -21,6 +21,10 @@ "engines": { "node": "^16.0.0" }, + "dependencies": { + "color": "^4.2.3", + "nanoid": "^3.1.23" + }, "devDependencies": { "@silverhand/eslint-config": "^0.17.0", "@silverhand/eslint-config-react": "^0.17.0", @@ -42,8 +46,5 @@ "stylelint": { "extends": "@silverhand/eslint-config-react/.stylelintrc" }, - "prettier": "@silverhand/eslint-config/.prettierrc", - "dependencies": { - "color": "^4.2.3" - } + "prettier": "@silverhand/eslint-config/.prettierrc" } diff --git a/packages/shared/src/utilities/index.ts b/packages/shared/src/utilities/index.ts index 3ea528766..12d75bec7 100644 --- a/packages/shared/src/utilities/index.ts +++ b/packages/shared/src/utilities/index.ts @@ -1,3 +1,4 @@ export * from './file'; export * from './react-router'; export * from './color'; +export * from './string'; diff --git a/packages/shared/src/utilities/string.ts b/packages/shared/src/utilities/string.ts new file mode 100644 index 000000000..7c0a83ae9 --- /dev/null +++ b/packages/shared/src/utilities/string.ts @@ -0,0 +1,6 @@ +import { customAlphabet } from 'nanoid'; + +export const generateRandomString = ( + size: number, + alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' +): string => customAlphabet(alphabet, size)(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 913d743d7..90b03bc08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1273,12 +1273,14 @@ importers: color: ^4.2.3 eslint: ^8.19.0 lint-staged: ^13.0.0 + nanoid: ^3.1.23 postcss: ^8.4.6 prettier: ^2.3.2 stylelint: ^14.8.2 typescript: ^4.6.2 dependencies: color: 4.2.3 + nanoid: 3.3.4 devDependencies: '@silverhand/eslint-config': 0.17.0_udveqfgjtporakzcjlry75cuqi '@silverhand/eslint-config-react': 0.17.0_ooy3j52eylrixuekzgeaykdz54