From 1493ed23e9d4f7b6b4f4f7f01baa242c919fdbd3 Mon Sep 17 00:00:00 2001 From: JG Date: Thu, 25 Jul 2024 13:22:35 +0800 Subject: [PATCH 1/3] doc: update nextjs page router tutorial code for the latest create-t3 --- docs/quick-start/nextjs-app-router.mdx | 5 + docs/quick-start/nextjs.mdx | 599 +++++++++++++------------ 2 files changed, 319 insertions(+), 285 deletions(-) diff --git a/docs/quick-start/nextjs-app-router.mdx b/docs/quick-start/nextjs-app-router.mdx index 7e6a4106..3439e69b 100644 --- a/docs/quick-start/nextjs-app-router.mdx +++ b/docs/quick-start/nextjs-app-router.mdx @@ -101,6 +101,11 @@ npx zenstack generate && npx prisma db push ### 4. Configure NextAuth to use credential-based auth +First we need to install type definitions for bcryptjs +```bash +npm install --save @types/bcryptjs +``` + Now let's update `/src/server/auth.ts` to the content below to use credentials auth and JWT-based session: diff --git a/docs/quick-start/nextjs.mdx b/docs/quick-start/nextjs.mdx index 53a77857..8ab1396b 100644 --- a/docs/quick-start/nextjs.mdx +++ b/docs/quick-start/nextjs.mdx @@ -101,17 +101,27 @@ npx zenstack generate && npx prisma db push ### 4. Configure NextAuth to use credential-based auth +First we need to install type definitions for bcryptjs +```bash +npm install --save @types/bcryptjs +``` + Now let's update `/src/server/auth.ts` to the content below to use credentials auth and JWT-based session: ```ts title='/src/server/auth.ts' -import { PrismaAdapter } from '@next-auth/prisma-adapter'; -import type { PrismaClient } from '@prisma/client'; -import { compare } from 'bcryptjs'; -import type { GetServerSidePropsContext } from 'next'; -import NextAuth, { getServerSession, type DefaultSession, type NextAuthOptions } from 'next-auth'; -import CredentialsProvider from 'next-auth/providers/credentials'; -import { db } from './db'; +import { PrismaAdapter } from "@auth/prisma-adapter"; +import type { PrismaClient } from "@prisma/client"; +import { compare } from "bcryptjs"; +import type { GetServerSidePropsContext } from "next"; +import NextAuth, { + getServerSession, + type DefaultSession, + type NextAuthOptions, +} from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import { db } from "./db"; +import { Adapter } from "next-auth/adapters"; /** * Module augmentation for `next-auth` types. * Allows us to add custom properties to the `session` object and keep type @@ -119,12 +129,12 @@ import { db } from './db'; * * @see https://next-auth.js.org/getting-started/typescript#module-augmentation **/ -declare module 'next-auth' { - interface Session extends DefaultSession { - user: { - id: string; - } & DefaultSession['user']; - } +declare module "next-auth" { + interface Session extends DefaultSession { + user: { + id: string; + } & DefaultSession["user"]; + } } /** @@ -134,46 +144,50 @@ declare module 'next-auth' { * @see https://next-auth.js.org/configuration/options **/ export const authOptions: NextAuthOptions = { - session: { - strategy: 'jwt', + session: { + strategy: "jwt", + }, + // Include user.id on session + callbacks: { + session({ session, token }) { + if (session.user) { + session.user.id = token.sub!; + } + return session; }, - // Include user.id on session - callbacks: { - session({ session, token }) { - if (session.user) { - session.user.id = token.sub!; - } - return session; - }, - }, - // Configure one or more authentication providers - adapter: PrismaAdapter(db), - providers: [ - CredentialsProvider({ - credentials: { - email: { type: 'email' }, - password: { type: 'password' }, - }, - authorize: authorize(db), - }), - ], + }, + // Configure one or more authentication providers + adapter: PrismaAdapter(db) as Adapter, + providers: [ + CredentialsProvider({ + credentials: { + email: { type: "email" }, + password: { type: "password" }, + }, + authorize: authorize(db), + }), + ], }; function authorize(prisma: PrismaClient) { - return async (credentials: Record<'email' | 'password', string> | undefined) => { - if (!credentials) throw new Error('Missing credentials'); - if (!credentials.email) throw new Error('"email" is required in credentials'); - if (!credentials.password) throw new Error('"password" is required in credentials'); - const maybeUser = await prisma.user.findFirst({ - where: { email: credentials.email }, - select: { id: true, email: true, password: true }, - }); - if (!maybeUser || !maybeUser.password) return null; - // verify the input password with stored hash - const isValid = await compare(credentials.password, maybeUser.password); - if (!isValid) return null; - return { id: maybeUser.id, email: maybeUser.email }; - }; + return async ( + credentials: Record<"email" | "password", string> | undefined, + ) => { + if (!credentials) throw new Error("Missing credentials"); + if (!credentials.email) + throw new Error('"email" is required in credentials'); + if (!credentials.password) + throw new Error('"password" is required in credentials'); + const maybeUser = await prisma.user.findFirst({ + where: { email: credentials.email }, + select: { id: true, email: true, password: true }, + }); + if (!maybeUser?.password) return null; + // verify the input password with stored hash + const isValid = await compare(credentials.password, maybeUser.password); + if (!isValid) return null; + return { id: maybeUser.id, email: maybeUser.email }; + }; } /** @@ -183,10 +197,10 @@ function authorize(prisma: PrismaClient) { * @see https://next-auth.js.org/configuration/nextjs **/ export const getServerAuthSession = (ctx: { - req: GetServerSidePropsContext['req']; - res: GetServerSidePropsContext['res']; + req: GetServerSidePropsContext["req"]; + res: GetServerSidePropsContext["res"]; }) => { - return getServerSession(ctx.req, ctx.res, authOptions); + return getServerSession(ctx.req, ctx.res, authOptions); }; export default NextAuth(authOptions); @@ -215,17 +229,17 @@ Let's mount it to the `/api/model/[...path]` endpoint. Create a `/src/pages/api/ file and fill in the content below: ```ts title='/src/pages/api/model/[...path].ts' -import { NextRequestHandler } from '@zenstackhq/server/next'; -import { enhance } from '@zenstackhq/runtime'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import { getServerAuthSession } from '../../../server/auth'; -import { prisma } from '../../../server/db'; +import { NextRequestHandler } from "@zenstackhq/server/next"; +import { enhance } from "@zenstackhq/runtime"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { getServerAuthSession } from "../../../server/auth"; +import { db } from "../../../server/db"; async function getPrisma(req: NextApiRequest, res: NextApiResponse) { - const session = await getServerAuthSession({ req, res }); - // create a wrapper of Prisma client that enforces access policy, - // data validation, and @password, @omit behaviors - return enhance(prisma, { user: session?.user }); + const session = await getServerAuthSession({ req, res }); + // create a wrapper of Prisma client that enforces access policy, + // data validation, and @password, @omit behaviors + return enhance(db, { user: session?.user }); } export default NextRequestHandler({ getPrisma }); @@ -259,73 +273,76 @@ Now let's implement the signup/signin pages. First, create a new page `/src/page ```tsx title='/src/pages/signup.tsx' /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import type { NextPage } from 'next'; -import { signIn } from 'next-auth/react'; -import Router from 'next/router'; -import { useState, type FormEvent } from 'react'; +import type { NextPage } from "next"; +import { signIn } from "next-auth/react"; +import Router from "next/router"; +import { useState, type FormEvent } from "react"; import { useCreateUser } from "../lib/hooks"; const Signup: NextPage = () => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const { trigger: signup } = useCreateUser(); - - async function onSignup(e: FormEvent) { - e.preventDefault(); - try { - await signup({ data: { email, password } }); - } catch (err: any) { - console.error(err); - if (err.info?.prisma && err.info?.code === 'P2002') { - // P2002 is Prisma's error code for unique constraint violations - alert('User alread exists'); - } else { - alert('An unknown error occurred'); - } - return; - } - - // signin to create a session - await signIn('credentials', { redirect: false, email, password }); - await Router.push('/'); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const { trigger: signup } = useCreateUser(); + + async function onSignup(e: FormEvent) { + e.preventDefault(); + try { + await signup({ data: { email, password } }); + } catch (err: any) { + console.error(err); + if (err.info?.prisma && err.info?.code === "P2002") { + // P2002 is Prisma's error code for unique constraint violations + alert("User alread exists"); + } else { + alert("An unknown error occurred"); + } + return; } - return ( -
-

Sign up

-
void onSignup(e)}> -
- - setEmail(e.currentTarget.value)} - className="ml-4 w-72 rounded border p-2" - /> -
-
- - setPassword(e.currentTarget.value)} - className="ml-4 w-72 rounded border p-2" - /> -
- -
+ // signin to create a session + await signIn("credentials", { redirect: false, email, password }); + await Router.push("/"); + } + + return ( +
+

Sign up

+
void onSignup(e)} + > +
+ + setEmail(e.currentTarget.value)} + className="ml-4 w-72 rounded border p-2" + /> +
+
+ + setPassword(e.currentTarget.value)} + className="ml-4 w-72 rounded border p-2" + />
- ); + +
+
+ ); }; export default Signup; @@ -353,67 +370,70 @@ It should look like this: Similarly, create the signin page `/src/pages/signin.tsx`: ```tsx title='/src/pages/signin.tsx' -import type { NextPage } from 'next'; -import { signIn } from 'next-auth/react'; -import Router from 'next/router'; -import { useState, type FormEvent } from 'react'; +import type { NextPage } from "next"; +import { signIn } from "next-auth/react"; +import Router from "next/router"; +import { useState, type FormEvent } from "react"; const Signin: NextPage = () => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - - async function onSignin(e: FormEvent) { - e.preventDefault(); - - const result = await signIn('credentials', { - redirect: false, - email, - password, - }); - - if (result?.ok) { - await Router.push('/'); - } else { - alert('Signin failed'); - } - } + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + async function onSignin(e: FormEvent) { + e.preventDefault(); + + const result = await signIn("credentials", { + redirect: false, + email, + password, + }); - return ( -
-

Login

-
void onSignin(e)}> -
- - setEmail(e.currentTarget.value)} - className="ml-4 w-72 rounded border p-2" - /> -
-
- - setPassword(e.currentTarget.value)} - className="ml-4 w-72 rounded border p-2" - /> -
- -
+ if (result?.ok) { + await Router.push("/"); + } else { + alert("Signin failed"); + } + } + + return ( +
+

Login

+
void onSignin(e)} + > +
+ + setEmail(e.currentTarget.value)} + className="ml-4 w-72 rounded border p-2" + /> +
+
+ + setPassword(e.currentTarget.value)} + className="ml-4 w-72 rounded border p-2" + />
- ); + +
+
+ ); }; export default Signin; @@ -421,7 +441,7 @@ export default Signin; ### 7. Prepare the Post model -Now let's create a `Post` model. We'll use it to store blog posts. +Now let's create a `Post` model(if it already exists, simply replace it). We'll use it to store blog posts. ```zmodel title='/schema.zmodel' model Post { @@ -479,11 +499,11 @@ npx zenstack generate && npx prisma db push Now let's replace `/src/pages/index.tsx` with the content below and use it for viewing and managing posts. ```tsx title='/src/pages/index.tsx' -import type { Post } from '@prisma/client'; -import { type NextPage } from 'next'; -import { signOut, useSession } from 'next-auth/react'; -import Link from 'next/link'; -import Router from 'next/router'; +import type { Post } from "@prisma/client"; +import { type NextPage } from "next"; +import { signOut, useSession } from "next-auth/react"; +import Link from "next/link"; +import Router from "next/router"; import { useCreatePost, useDeletePost, @@ -494,116 +514,125 @@ import { type AuthUser = { id: string; email?: string | null }; const Welcome = ({ user }: { user: AuthUser }) => { - async function onSignout() { - await signOut({ redirect: false }); - await Router.push('/signin'); - } - return ( -
-

Welcome back, {user?.email}

- -
- ); + async function onSignout() { + await signOut({ redirect: false }); + await Router.push("/signin"); + } + return ( +
+

Welcome back, {user?.email}

+ +
+ ); }; const SigninSignup = () => { - return ( -
- - Signin - - - Signup - -
- ); + return ( +
+ + Signin + + + Signup + +
+ ); }; -const Posts = ({ user }: { user: AuthUser }) => { - // Post crud hooks - const { trigger: createPost } = useCreatePost(); - const { trigger: updatePost } = useUpdatePost(); - const { trigger: deletePost } = useDeletePost(); - - // list all posts that're visible to the current user, together with their authors - const { data: posts } = useFindManyPost({ - include: { author: true }, - orderBy: { createdAt: 'desc' }, - }); - - async function onCreatePost() { - const title = prompt('Enter post title'); - if (title) { - await createPost({ data: { title } }); - } - } - - async function onTogglePublished(post: Post) { - await updatePost({ - where: { id: post.id }, - data: { published: !post.published }, - }); +const Posts = () => { + // Post crud hooks + const { trigger: createPost } = useCreatePost(); + const { trigger: updatePost } = useUpdatePost(); + const { trigger: deletePost } = useDeletePost(); + + // list all posts that're visible to the current user, together with their authors + const { data: posts } = useFindManyPost({ + include: { author: true }, + orderBy: { createdAt: "desc" }, + }); + + async function onCreatePost() { + const title = prompt("Enter post title"); + if (title) { + await createPost({ data: { title } }); } + } - async function onDelete(post: Post) { - await deletePost({ where: { id: post.id } }); - } - - return ( -
- - -
    - {posts?.map((post) => ( -
  • -

    - {post.title} - by {post.author.email} -

    -
    - - -
    -
  • - ))} -
-
- ); + async function onTogglePublished(post: Post) { + await updatePost({ + where: { id: post.id }, + data: { published: !post.published }, + }); + } + + async function onDelete(post: Post) { + await deletePost({ where: { id: post.id } }); + } + + return ( +
+ + +
    + {posts?.map((post) => ( +
  • +

    + {post.title} + by {post.author.email} +

    +
    + + +
    +
  • + ))} +
+
+ ); }; const Home: NextPage = () => { - const { data: session, status } = useSession(); - - if (status === 'loading') return

Loading ...

; - - return ( -
-
-

My Awesome Blog

- - {session?.user ? ( - // welcome & blog posts -
- -
- -
-
- ) : ( - // if not logged in - - )} -
-
- ); + const { data: session, status } = useSession(); + + if (status === "loading") return

Loading ...

; + + return ( +
+
+

My Awesome Blog

+ + {session?.user ? ( + // welcome & blog posts +
+ +
+ +
+
+ ) : ( + // if not logged in + + )} +
+
+ ); }; export default Home; From 82c976b877356fe8cd3b33554130eaa594b6700c Mon Sep 17 00:00:00 2001 From: JG Date: Thu, 25 Jul 2024 13:31:50 +0800 Subject: [PATCH 2/3] add session provider setup --- docs/quick-start/nextjs.mdx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/quick-start/nextjs.mdx b/docs/quick-start/nextjs.mdx index 8ab1396b..d0dad870 100644 --- a/docs/quick-start/nextjs.mdx +++ b/docs/quick-start/nextjs.mdx @@ -268,6 +268,30 @@ Now we're ready to implement the signup/signin flow. ### 6. Implement Signup/Signin +Firstly, NextAuth and React Query require context providers to be set up. Let's add it to `/src/pages/_app.tsx`: + +```tsx title='/src/pages/_app.tsx' +import { type AppType } from "next/app"; +import { type Session } from "next-auth"; +import { SessionProvider } from "next-auth/react"; + +import "../styles/globals.css"; + +const MyApp: AppType<{ session: Session | null }> = ({ + Component, + pageProps: { session, ...pageProps }, +}) => { + return ( + + + + ); +}; + +export default MyApp; +``` + + Now let's implement the signup/signin pages. First, create a new page `/src/pages/signup.tsx`: ```tsx title='/src/pages/signup.tsx' From e50de8b8b87e74fb0a2da9c5273b115394aa3306 Mon Sep 17 00:00:00 2001 From: JG Date: Thu, 25 Jul 2024 13:35:15 +0800 Subject: [PATCH 3/3] grammar change --- docs/quick-start/nextjs-app-router.mdx | 2 +- docs/quick-start/nextjs.mdx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/quick-start/nextjs-app-router.mdx b/docs/quick-start/nextjs-app-router.mdx index 3439e69b..c6296b84 100644 --- a/docs/quick-start/nextjs-app-router.mdx +++ b/docs/quick-start/nextjs-app-router.mdx @@ -101,7 +101,7 @@ npx zenstack generate && npx prisma db push ### 4. Configure NextAuth to use credential-based auth -First we need to install type definitions for bcryptjs +First, we need to install type definitions for bcryptjs ```bash npm install --save @types/bcryptjs ``` diff --git a/docs/quick-start/nextjs.mdx b/docs/quick-start/nextjs.mdx index d0dad870..11267bd1 100644 --- a/docs/quick-start/nextjs.mdx +++ b/docs/quick-start/nextjs.mdx @@ -101,7 +101,7 @@ npx zenstack generate && npx prisma db push ### 4. Configure NextAuth to use credential-based auth -First we need to install type definitions for bcryptjs +First, we need to install type definitions for bcryptjs ```bash npm install --save @types/bcryptjs ``` @@ -218,7 +218,7 @@ value (use a complex secret in production and don't check it into git): ZenStack has built-in support for Next.js and can provide database CRUD services automagically, so you don't need to write it yourself. -First install the `@zenstackhq/server` and `@zenstackhq/swr` packages: +First, install the `@zenstackhq/server` and `@zenstackhq/swr` packages: ```bash npm install @zenstackhq/server swr @@ -268,7 +268,7 @@ Now we're ready to implement the signup/signin flow. ### 6. Implement Signup/Signin -Firstly, NextAuth and React Query require context providers to be set up. Let's add it to `/src/pages/_app.tsx`: +First, NextAuth and React Query require context providers to be set up. Let's add it to `/src/pages/_app.tsx`: ```tsx title='/src/pages/_app.tsx' import { type AppType } from "next/app";