From a4a857925048b9b06822a60be7ebbf76ae895d9a Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 28 Nov 2022 20:58:42 +0800 Subject: [PATCH] docs: add more content to authentication integration doc --- docs/building-your-app.md | 4 +- docs/integrating-authentication.md | 144 ++++++++++++++++++++++++++++- docs/quick-start.md | 86 ++++++++++++++++- 3 files changed, 226 insertions(+), 8 deletions(-) diff --git a/docs/building-your-app.md b/docs/building-your-app.md index 98d2af6e9..41eafdfdf 100644 --- a/docs/building-your-app.md +++ b/docs/building-your-app.md @@ -32,7 +32,7 @@ export default requestHandler(service, options); Please note that the services need to be configured with a callback `getServerUser` for getting the current login user. The example above uses NextAuth to do it, but you can also hand-code it based on the authentication approach you use, as long as it returns a user object that represents the current session's user. -_TBD_ In the future we'll provide more samples for showing how to integrate with other libraries, like IronSession. +_NOTE_ Check out [this guide](integrating-authentication.md) for more details about integrating with authentication. Make sure the services are mounted at route `/api/zenstack/` with a catch all parameter named `path`, as this is required by the generate React hooks. @@ -68,8 +68,6 @@ export const authOptions: NextAuthOptions = { export default NextAuth(authOptions); ``` -_TBD_ In the future we'll provide more samples for showing how to integrate with other libraries, like IronSession. - ## Using React hooks React hooks are generated for CRUD'ing each data model you defined. They save your time writing explicit HTTP requests to call the generated services. Internally the hooks use [SWR](https://swr.vercel.app/) for data fetching, so you'll also enjoy its built-in features, like caching, revalidation on interval, etc. diff --git a/docs/integrating-authentication.md b/docs/integrating-authentication.md index 44fd4b49c..035d153c2 100644 --- a/docs/integrating-authentication.md +++ b/docs/integrating-authentication.md @@ -6,6 +6,8 @@ This documentation explains how to integrate ZenStack with popular authenticatio [NextAuth](https://next-auth.js.org/) is a comprehensive framework for implementating authentication. It offers a pluggable mechanism for configuring how user data is persisted. You can find a full example using ZenStack with NextAuth [here](https://github.com/zenstackhq/zenstack/tree/main/samples/todo ':target=blank'). +### Using generated adapter + When `zenstack generate` runs, it generates an adapter for NextAuth if it finds the `next-auth` npm package is installed. The generated adapter can be configured to NextAuth as follows: ```ts @@ -24,6 +26,8 @@ export const authOptions: NextAuthOptions = { export default NextAuth(authOptions); ``` +### Using generated `authorize` + If you use [`CredentialsProvider`](https://next-auth.js.org/providers/credentials ':target=blank'), i.e. username/password based auth, you can also use the generated `authorize` function to implement how username/password is verified against the database: ```ts @@ -56,6 +60,35 @@ export const authOptions: NextAuthOptions = { export default NextAuth(authOptions); ``` +### Configuring ZenStack services + +ZenStack's CRUD services need to be configured with a `getServerUser` callback for fetching current login user from the backend. This can be easily done when using Next-Auth's `unstable_getServerSession` API: + +```ts +// pages/api/zenstack/[...path].ts + +... +import service, { + type RequestHandlerOptions, + requestHandler, +} from '@zenstackhq/runtime/server'; +import { authOptions } from '../auth/[...nextauth]'; +import { unstable_getServerSession } from 'next-auth'; + +const options: RequestHandlerOptions = { + async getServerUser(req: NextApiRequest, res: NextApiResponse) { + const session = await unstable_getServerSession(req, res, authOptions); + return session?.user; + }, +}; +export default requestHandler(service, options); + +``` + +_NOTE_ Although the name `unstable_getServerSession` looks suspicious, it's officially recommended by Next-Auth and is production-ready. + +### Data model requirement + NextAuth is agnostic about the type of underlying database, but it requires certain table structures, depending on how you configure it. Your ZModel definitions should reflect these requirements. A sample `User` model is shown here (to be used with `CredentialsProvider`): ```zmodel @@ -77,9 +110,116 @@ model User { You can find the detailed database model requirements [here](https://next-auth.js.org/adapters/models ':target=blank'). -## IronSession +## Iron-session -[TBD] +[Iron-session](https://www.npmjs.com/package/iron-session ':target=blank') is a lightweighted authentication toolkit. + +### Authentication endpoints + +Iron-session requires you to implement auth related API endpoints by yourself. Usually you need to at least have these three endpoints: **api/auth/login**, **/api/auth/logout**, and **/api/auth/user**. The following code shows how to use ZenStack backend service to implement them. + +- **/api/auth/login** + +```ts +... +import service from '@zenstackhq/runtime/server'; +import * as bcrypt from 'bcryptjs'; + +const loginRoute: NextApiHandler = async (req, res) => { + const { email, password } = req.body; + + const user = await service.db.user.findUnique({ where: { email } }); + if (!user || !bcrypt.compareSync(password, user.password)) { + res.status(401).json({ + message: 'invalid email and password combination', + }); + return; + } + + delete (user as any).password; + req.session.user = user; + await req.session.save(); + + res.json(user); +}; + +export default withIronSessionApiRoute(loginRoute, sessionOptions); +``` + +- **/api/auth/logout** + +```ts +... + +const logoutRoute: NextApiHandler = async (req, res) => { + req.session.destroy(); + res.json({}); +}; + +export default withIronSessionApiRoute(logoutRoute, sessionOptions); + +``` + +- **/api/auth/user** + +```ts +... +import service from '@zenstackhq/runtime/server'; + +const userRoute: NextApiHandler = async (req, res) => { + if (req.session?.user) { + // fetch user from db for fresh data + const user = await service.db.user.findUnique({ + where: { email: req.session.user.email }, + }); + if (!user) { + res.status(401).json({ message: 'invalid login status' }); + return; + } + + delete (user as any).password; + res.json(user); + } else { + res.status(401).json({ message: 'invalid login status' }); + } +}; + +export default withIronSessionApiRoute(userRoute, sessionOptions); +``` + +### Configuring ZenStack services + +ZenStack's CRUD services need to be configured with a `getServerUser` callback for fetching current login user from the backend. This can be easily done when using iron-session: + +```ts +// pages/api/zenstack/[...path].ts + +... +import service, { + requestHandler, + type RequestHandlerOptions, +} from '@zenstackhq/runtime/server'; + +const options: RequestHandlerOptions = { + async getServerUser(req: NextApiRequest, res: NextApiResponse) { + const user = req.session?.user; + if (!user) { + return undefined; + } + + const dbUser = await service.db.user.findUnique({ + where: { email: user.email }, + }); + + return dbUser ?? undefined; + }, +}; + +export default withIronSessionApiRoute( + requestHandler(service, options), + sessionOptions +); +``` ## Custom-built authentication diff --git a/docs/quick-start.md b/docs/quick-start.md index 91ee9d499..3e71dc6fc 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -4,7 +4,15 @@ Please check out the corresponding guide for [creating a new project](#creating- ## Creating a new project -Follow these steps to create a new project from a preconfigured template: +You can choose from these preconfigured starter to create a new project: + +- [Using Next-Auth for authentication](#with-next-auth) +- [Using iron-session for authentication](#with-iron-session) +- [Without integrating with authentication](#without-integrating-authentication) + +### With Next-Auth + +Follow these steps to create a new project from a preconfigured template using [Next-Auth](https://next-auth.js.org/ ':target=blank') for authentication: 1. Clone from starter template @@ -36,13 +44,81 @@ npm run db:push npm run dev ``` +### With iron-session + +Follow these steps to create a new project from a preconfigured template using [iron-session](https://www.npmjs.com/package/iron-session ':target=blank') for authentication: + +1. Clone from starter template + +```bash +npx create-next-app --use-npm -e https://github.com/zenstackhq/nextjs-iron-session-starter +``` + +2. Install dependencies + +```bash +npm install +``` + +3. Generate CRUD services and hooks code from the starter model + +```bash +npm run generate +``` + +4. push database schema to the local sqlite db + +```bash +npm run db:push +``` + +5. start dev server + +``` +npm run dev +``` + +### Without integrating authentication + +If you would rather not use a template preconfigured with authentication, you can use the barebone starter instead. You can add an authentication solution later or hand-code it by yourself. + +1. Clone from starter template + +```bash +npx create-next-app --use-npm -e https://github.com/zenstackhq/nextjs-barebone-starter +``` + +2. Install dependencies + +```bash +npm install +``` + +3. Generate CRUD services and hooks code from the starter model + +```bash +npm run generate +``` + +4. push database schema to the local sqlite db + +```bash +npm run db:push +``` + +5. start dev server + +``` +npm run dev +``` + +### Check result + If everything worked, you should see a simple blog app like this: ![starter screen shot](_media/starter-shot.png 'Starter project screenshot') No worries if a blogger app doesn't suit you. The created project contains a starter model at `/zenstack/schema.zmodel`. You can modify it and build up your application's own model following [this guide](modeling-your-app.md). -It's good idea to install the [VSCode extension](https://marketplace.visualstudio.com/items?itemName=zenstack.zenstack ':target=_blank') so you get syntax highlighting and error checking when authoring model files. - ## Adding to an existing project To add ZenStack to an existing Next.js + Typescript project, run command below: @@ -52,3 +128,7 @@ npx zenstack init ``` You should find a `/zenstack/schema.model` file created, containing a simple blogger model in it. No worries if a blogger app doesn't suit you. You can modify it and build up your application's own model following [this guide](modeling-your-app.md). + +## Installing VSCode extension + +It's good idea to install the [VSCode extension](https://marketplace.visualstudio.com/items?itemName=zenstack.zenstack ':target=_blank') so you get syntax highlighting and error checking when authoring model files.