From ceca85b6dfc1fbeeacb933d2ab33f7994e892b94 Mon Sep 17 00:00:00 2001 From: JG Date: Tue, 9 Jul 2024 19:48:29 +0800 Subject: [PATCH 1/9] doc: add Lucia Auth doc --- docs/guides/authentication/lucia.md | 151 +++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 3 deletions(-) diff --git a/docs/guides/authentication/lucia.md b/docs/guides/authentication/lucia.md index 58e809a6..9233f696 100644 --- a/docs/guides/authentication/lucia.md +++ b/docs/guides/authentication/lucia.md @@ -1,9 +1,154 @@ --- description: Integrating with Lucia. sidebar_position: 5 -sidebar_label: 🚧 Lucia +sidebar_label: Lucia --- -# 🚧 Integrating With Lucia +# Integrating With Lucia -Coming soon. +[Lucia](https://lucia-auth.com/) is an auth library for your server that abstracts away the complexity of handling sessions. It is a good choice if you need to add custom logic to the auth flow or use email and password authentication. + +To get access policies to work, ZenStack needs to be connected to the authentication system to get the user's identity. This guide introduces tasks required for integrating ZenStack with Lucia Auth. You can find a complete example [here](https://github.com/zenstackhq/sample-luciaAuth-nextjs). + +## Data Model Requirement + +Lucia needs to store your users and sessions in the database. So, your ZModel definition needs to include these two models. Here is the sample schema: + +```zmodel title='/schema.zmodel' +model User { + id String @id @default(cuid()) + userName String @unique + password String @omit + sessions Session[] + + @@allow('read', true) + @@allow('all', auth() == this) +} + +model Session { + id String @id + userId String + expiresAt DateTime + + user User @relation(references: [id], fields: [userId], onDelete: Cascade) +} +``` + +The data field names and types in `session` model must exactly match the ones in the above. While you can change the model names, the relation name in the session model (`Session.user`) must be the camel-case version of the user model name. For example, if the user model was named `AuthUser`, the relation must be named `Session.authUser`. + +## Prisma Adapter + +Lucia connects to your database via an adapter, which provides a set of basic, standardized querying methods that Lucia can use. Since ZenStack is based on Prisma, you can + +Lucia has its own Prisma adapter `@lucia-auth/adapter-prisma`, which provides a set of basic, standardized querying methods. Since ZenStack is based on Prisma, you can use PrismaAdapter for the job: + +```tsx title='/lib/auth.ts' +import { PrismaAdapter } from "@lucia-auth/adapter-prisma"; +import { PrismaClient } from "@prisma/client"; + +const client = new PrismaClient(); + +const adapter = new PrismaAdapter(client.session, client.user); +``` + +## Validate requests + +Create `validateRequest()`. This will check for the session cookie, validate it, and set a new cookie if necessary. Make sure to catch errors when setting cookies and wrap the function with `cache()` to prevent unnecessary database calls.  To learn more, see Lucia’s  [Validating requests](https://lucia-auth.com/guides/validate-session-cookies/nextjs-app) page. + +```tsx title='/lib/auth.ts' +export const validateRequest = cache( + async (): Promise<{ user: User; session: Session } | { user: null; session: null }> => { + const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null; + if (!sessionId) { + return { + user: null, + session: null + }; + } + + const result = await lucia.validateSession(sessionId); + // next.js throws when you attempt to set cookie when rendering page + try { + if (result.session && result.session.fresh) { + const sessionCookie = lucia.createSessionCookie(result.session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + if (!result.session) { + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + } catch {} + return result; + } +); +``` + +This function can be used in server components and form actions to get the current session and user. + +```tsx title='/app/page.tsx' + const { user } = await validateRequest(); + + if (!user) { + return redirect("/login"); + } +``` + +## Create an enhanced Prisma client + +You can create an enhanced Prisma client which automatically validates access policies, field validation rules etc., during CRUD operations. For more details, please refer to [ZModel Language](https://zenstack.dev/docs/reference/zmodel-language) reference. + +To create such a client, simply call the `validateRequest` function to get the current user id and then call the `enhance` API to pass the user identity. + +```tsx title='/lib/db.ts' +import { PrismaClient } from '@prisma/client'; +import { validateRequest } from './auth'; +import { enhance } from '@zenstackhq/runtime'; + +export const prisma = new PrismaClient(); + +export async function getEnhancedPrisma(): Promise { + const { user } = await validateRequest(); + // create a wrapper of Prisma client that enforces access policy, + // data validation, and @password, @omit behaviors + return enhance(prisma, { user: {id: user?.id!}}); +} +``` + +## Expose more user data + +By default, Lucia will not expose any database columns to the `User` type. To add a `userName` field to it, use the `getUserAttributes()` option. + +```tsx title='/lib/auth.ts' +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes + } +} + +interface DatabaseUserAttributes { + userName: string; +} + +export const lucia = new Lucia(adapter, { + getUserAttributes: (attributes) => { + return { + userName: attributes.userName + }; + } +}); +``` + +Then, you can directly access it from the result returned by `validateRequest` function: + +```tsx title='/app/page.tsx' +export default async function Page() { + const { user } = await validateRequest(); + return ( + <> +

Hi, {user.userName}!

+

Your user ID is {user.id}.

+ + ); +} +``` From b110bf5c8bf6213ccbd72208f2ffff1791dab0a5 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Tue, 9 Jul 2024 19:58:30 +0800 Subject: [PATCH 2/9] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- docs/guides/authentication/lucia.md | 80 ++++++++++++++--------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/docs/guides/authentication/lucia.md b/docs/guides/authentication/lucia.md index 9233f696..1697229c 100644 --- a/docs/guides/authentication/lucia.md +++ b/docs/guides/authentication/lucia.md @@ -57,29 +57,29 @@ Create `validateRequest()`. This will check for the session cookie, validate it ```tsx title='/lib/auth.ts' export const validateRequest = cache( - async (): Promise<{ user: User; session: Session } | { user: null; session: null }> => { - const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null; - if (!sessionId) { - return { - user: null, - session: null - }; - } - - const result = await lucia.validateSession(sessionId); - // next.js throws when you attempt to set cookie when rendering page - try { - if (result.session && result.session.fresh) { - const sessionCookie = lucia.createSessionCookie(result.session.id); - cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); - } - if (!result.session) { - const sessionCookie = lucia.createBlankSessionCookie(); - cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); - } - } catch {} - return result; - } + async (): Promise<{ user: User; session: Session } | { user: null; session: null }> => { + const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null; + if (!sessionId) { + return { + user: null, + session: null + }; + } + + const result = await lucia.validateSession(sessionId); + // next.js throws when you attempt to set cookie when rendering page + try { + if (result.session && result.session.fresh) { + const sessionCookie = lucia.createSessionCookie(result.session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + if (!result.session) { + const sessionCookie = lucia.createBlankSessionCookie(); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + } + } catch {} + return result; + } ); ``` @@ -120,22 +120,22 @@ By default, Lucia will not expose any database columns to the `User` type. To ```tsx title='/lib/auth.ts' declare module "lucia" { - interface Register { - Lucia: typeof lucia; - DatabaseUserAttributes: DatabaseUserAttributes - } + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: DatabaseUserAttributes + } } interface DatabaseUserAttributes { - userName: string; + userName: string; } export const lucia = new Lucia(adapter, { - getUserAttributes: (attributes) => { - return { - userName: attributes.userName - }; - } + getUserAttributes: (attributes) => { + return { + userName: attributes.userName + }; + } }); ``` @@ -143,12 +143,12 @@ Then, you can directly access it from the result returned by `validateRequest` f ```tsx title='/app/page.tsx' export default async function Page() { - const { user } = await validateRequest(); - return ( - <> -

Hi, {user.userName}!

-

Your user ID is {user.id}.

- - ); + const { user } = await validateRequest(); + return ( + <> +

Hi, {user.userName}!

+

Your user ID is {user.id}.

+ + ); } ``` From 84b88dd7b9b4571c019eae98ed67c2616191554a Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Fri, 12 Jul 2024 12:37:54 +0800 Subject: [PATCH 3/9] Update lucia.md --- docs/guides/authentication/lucia.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/authentication/lucia.md b/docs/guides/authentication/lucia.md index 1697229c..097b4887 100644 --- a/docs/guides/authentication/lucia.md +++ b/docs/guides/authentication/lucia.md @@ -10,7 +10,7 @@ sidebar_label: Lucia To get access policies to work, ZenStack needs to be connected to the authentication system to get the user's identity. This guide introduces tasks required for integrating ZenStack with Lucia Auth. You can find a complete example [here](https://github.com/zenstackhq/sample-luciaAuth-nextjs). -## Data Model Requirement +## Data model requirement Lucia needs to store your users and sessions in the database. So, your ZModel definition needs to include these two models. Here is the sample schema: @@ -36,7 +36,7 @@ model Session { The data field names and types in `session` model must exactly match the ones in the above. While you can change the model names, the relation name in the session model (`Session.user`) must be the camel-case version of the user model name. For example, if the user model was named `AuthUser`, the relation must be named `Session.authUser`. -## Prisma Adapter +## Prisma adapter Lucia connects to your database via an adapter, which provides a set of basic, standardized querying methods that Lucia can use. Since ZenStack is based on Prisma, you can From 6e5d138b022bdad5074cbeb2991fc5ca1664179b Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Fri, 12 Jul 2024 15:28:01 +0800 Subject: [PATCH 4/9] Update docs/guides/authentication/lucia.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- docs/guides/authentication/lucia.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/authentication/lucia.md b/docs/guides/authentication/lucia.md index 097b4887..5f6e917e 100644 --- a/docs/guides/authentication/lucia.md +++ b/docs/guides/authentication/lucia.md @@ -34,7 +34,7 @@ model Session { } ``` -The data field names and types in `session` model must exactly match the ones in the above. While you can change the model names, the relation name in the session model (`Session.user`) must be the camel-case version of the user model name. For example, if the user model was named `AuthUser`, the relation must be named `Session.authUser`. +The data field names and types in the `session` model must exactly match the ones in the above. While you can change the model names, the relation name in the session model (`Session.user`) must be the camel-case version of the user model name. For example, if the user model was named `AuthUser`, the relation must be named `Session.authUser`. ## Prisma adapter From 61c7d1c0e87e974f69a43f443cd98b5a78e650d3 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Fri, 12 Jul 2024 15:28:35 +0800 Subject: [PATCH 5/9] Update docs/guides/authentication/lucia.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- docs/guides/authentication/lucia.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/guides/authentication/lucia.md b/docs/guides/authentication/lucia.md index 5f6e917e..447f66ac 100644 --- a/docs/guides/authentication/lucia.md +++ b/docs/guides/authentication/lucia.md @@ -16,7 +16,7 @@ Lucia needs to store your users and sessions in the database. So, your ZModel de ```zmodel title='/schema.zmodel' model User { - id String @id @default(cuid()) + id String @id @default(uuid()) userName String @unique password String @omit sessions Session[] @@ -32,7 +32,6 @@ model Session { user User @relation(references: [id], fields: [userId], onDelete: Cascade) } -``` The data field names and types in the `session` model must exactly match the ones in the above. While you can change the model names, the relation name in the session model (`Session.user`) must be the camel-case version of the user model name. For example, if the user model was named `AuthUser`, the relation must be named `Session.authUser`. From 66ef8cf61ba6bcdfdfe76ca9edd117f342b32c58 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:21:37 -0700 Subject: [PATCH 6/9] use consistent indentation --- docs/guides/authentication/lucia.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/guides/authentication/lucia.md b/docs/guides/authentication/lucia.md index 447f66ac..baa814f2 100644 --- a/docs/guides/authentication/lucia.md +++ b/docs/guides/authentication/lucia.md @@ -16,21 +16,21 @@ Lucia needs to store your users and sessions in the database. So, your ZModel de ```zmodel title='/schema.zmodel' model User { - id String @id @default(uuid()) - userName String @unique - password String @omit - sessions Session[] + id String @id @default(uuid()) + userName String @unique + password String @omit + sessions Session[] - @@allow('read', true) - @@allow('all', auth() == this) + @@allow('read', true) + @@allow('all', auth() == this) } model Session { - id String @id - userId String - expiresAt DateTime + id String @id + userId String + expiresAt DateTime - user User @relation(references: [id], fields: [userId], onDelete: Cascade) + user User @relation(references: [id], fields: [userId], onDelete: Cascade) } The data field names and types in the `session` model must exactly match the ones in the above. While you can change the model names, the relation name in the session model (`Session.user`) must be the camel-case version of the user model name. For example, if the user model was named `AuthUser`, the relation must be named `Session.authUser`. @@ -106,10 +106,10 @@ import { enhance } from '@zenstackhq/runtime'; export const prisma = new PrismaClient(); export async function getEnhancedPrisma(): Promise { - const { user } = await validateRequest(); - // create a wrapper of Prisma client that enforces access policy, - // data validation, and @password, @omit behaviors - return enhance(prisma, { user: {id: user?.id!}}); + const { user } = await validateRequest(); + // create a wrapper of Prisma client that enforces access policy, + // data validation, and @password, @omit behaviors + return enhance(prisma, { user: {id: user?.id!}}); } ``` From 965d88b9528049c89156a77b4b3ad18e8c87c478 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:23:59 -0700 Subject: [PATCH 7/9] fix missing end enclosure --- docs/guides/authentication/lucia.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guides/authentication/lucia.md b/docs/guides/authentication/lucia.md index baa814f2..b3dab292 100644 --- a/docs/guides/authentication/lucia.md +++ b/docs/guides/authentication/lucia.md @@ -32,6 +32,7 @@ model Session { user User @relation(references: [id], fields: [userId], onDelete: Cascade) } +``` The data field names and types in the `session` model must exactly match the ones in the above. While you can change the model names, the relation name in the session model (`Session.user`) must be the camel-case version of the user model name. For example, if the user model was named `AuthUser`, the relation must be named `Session.authUser`. From 32a8cbbade9fcddc5bd6a4b02bbb6e212639a576 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:24:35 -0700 Subject: [PATCH 8/9] consistent tab --- docs/guides/authentication/lucia.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guides/authentication/lucia.md b/docs/guides/authentication/lucia.md index b3dab292..78f1d4a6 100644 --- a/docs/guides/authentication/lucia.md +++ b/docs/guides/authentication/lucia.md @@ -86,11 +86,11 @@ export const validateRequest = cache( This function can be used in server components and form actions to get the current session and user. ```tsx title='/app/page.tsx' - const { user } = await validateRequest(); + const { user } = await validateRequest(); - if (!user) { - return redirect("/login"); - } + if (!user) { + return redirect("/login"); + } ``` ## Create an enhanced Prisma client From 454753cdcc2efb17915ae0c596b863f08f8753bb Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:26:33 -0700 Subject: [PATCH 9/9] tab --- docs/guides/authentication/lucia.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guides/authentication/lucia.md b/docs/guides/authentication/lucia.md index 78f1d4a6..881362b3 100644 --- a/docs/guides/authentication/lucia.md +++ b/docs/guides/authentication/lucia.md @@ -86,11 +86,11 @@ export const validateRequest = cache( This function can be used in server components and form actions to get the current session and user. ```tsx title='/app/page.tsx' - const { user } = await validateRequest(); +const { user } = await validateRequest(); - if (!user) { - return redirect("/login"); - } +if (!user) { + return redirect("/login"); +} ``` ## Create an enhanced Prisma client