import { z } from'zod'exportconstSignupFormSchema=z.object({ name: z.string().min(2, { message:'Name must be at least 2 characters long.' }).trim(), email:z.string().email({ message:'Please enter a valid email.' }).trim(), password: z.string().min(8, { message:'Be at least 8 characters long' }).regex(/[a-zA-Z]/, { message:'Contain at least one letter.' }).regex(/[0-9]/, { message:'Contain at least one number.' }).regex(/[^a-zA-Z0-9]/, { message:'Contain at least one special character.', }).trim(),})exporttypeFormState=| { errors?: { name?:string[] email?:string[] password?:string[] } message?:string }|undefined
為了避免對您的驗證提供者 API 或資料庫進行不必要的呼叫,如果任何表單欄位不符合定義的結構,您可以在伺服器動作中提前 return。
app/actions/auth.ts
TypeScript
import { SignupFormSchema, FormState } from'@/app/lib/definitions'exportasyncfunctionsignup(state:FormState, formData:FormData) {// Validate form fieldsconstvalidatedFields=SignupFormSchema.safeParse({ name:formData.get('name'), email:formData.get('email'), password:formData.get('password'), })// If any form fields are invalid, return earlyif (!validatedFields.success) {return { errors:validatedFields.error.flatten().fieldErrors, } }// Call the provider or db to create a user...}
驗證表單欄位後,您可以透過呼叫您的驗證提供者 API 或資料庫來建立新的使用者帳戶或檢查使用者是否存在。
接續前面的範例
app/actions/auth.tsx
TypeScript
exportasyncfunctionsignup(state:FormState, formData:FormData) {// 1. Validate form fields// ...// 2. Prepare data for insertion into databaseconst { name,email,password } =validatedFields.data// e.g. Hash the user's password before storing itconsthashedPassword=awaitbcrypt.hash(password,10)// 3. Insert the user into the database or call an Auth Library's APIconstdata=await db.insert(users).values({ name, email, password: hashedPassword, }).returning({ id:users.id })constuser= data[0]if (!user) {return { message:'An error occurred while creating your account.', } }// TODO:// 4. Create user session// 5. Redirect user}
回到您的伺服器動作中,您可以呼叫 createSession() 函式,並使用 redirect() API 將使用者重新導向到適當的頁面。
app/actions/auth.ts
TypeScript
import { createSession } from'@/app/lib/session'exportasyncfunctionsignup(state:FormState, formData:FormData) {// Previous steps:// 1. Validate form fields// 2. Prepare data for insertion into database// 3. Insert the user into the database or call an Library API// Current steps:// 4. Create user sessionawaitcreateSession(user.id)// 5. Redirect userredirect('/profile')}
在將工作階段 ID 儲存到使用者的瀏覽器之前將其加密,並確保資料庫和 Cookie 保持同步(這是可選的,但建議在中介軟體中進行樂觀的驗證檢查)。
例如:
app/lib/session.ts
TypeScript
import cookies from'next/headers'import { db } from'@/app/lib/db'import { encrypt } from'@/app/lib/session'exportasyncfunctioncreateSession(id:number) {constexpiresAt=newDate(Date.now() +7*24*60*60*1000)// 1. Create a session in the databaseconstdata=await db.insert(sessions).values({ userId: id, expiresAt, })// Return the session ID.returning({ id:sessions.id })constsessionId= data[0].id// 2. Encrypt the session IDconstsession=awaitencrypt({ sessionId, expiresAt })// 3. Store the session in cookies for optimistic auth checksconstcookieStore=awaitcookies()cookieStore().set('session', session, { httpOnly:true, secure:true, expires: expiresAt, sameSite:'lax', path:'/', })}
import { NextRequest, NextResponse } from'next/server'import { decrypt } from'@/app/lib/session'import { cookies } from'next/headers'// 1. Specify protected and public routesconstprotectedRoutes= ['/dashboard']constpublicRoutes= ['/login','/signup','/']exportdefaultasyncfunctionmiddleware(req:NextRequest) {// 2. Check if the current route is protected or publicconstpath=req.nextUrl.pathnameconstisProtectedRoute=protectedRoutes.includes(path)constisPublicRoute=publicRoutes.includes(path)// 3. Decrypt the session from the cookieconstcookie= (awaitcookies()).get('session')?.valueconstsession=awaitdecrypt(cookie)// 4. Redirect to /login if the user is not authenticatedif (isProtectedRoute &&!session?.userId) {returnNextResponse.redirect(newURL('/login',req.nextUrl)) }// 5. Redirect to /dashboard if the user is authenticatedif ( isPublicRoute &&session?.userId &&!req.nextUrl.pathname.startsWith('/dashboard') ) {returnNextResponse.redirect(newURL('/dashboard',req.nextUrl)) }returnNextResponse.next()}// Routes Middleware should not run onexportconstconfig= { matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],}
exportconstgetUser=cache(async () => {constsession=awaitverifySession()if (!session) returnnulltry {constdata=awaitdb.query.users.findMany({ where:eq(users.id,session.userId),// Explicitly return the columns you need rather than the whole user object columns: { id:true, name:true, email:true, }, })constuser= data[0]return user } catch (error) {console.log('Failed to fetch user')returnnull }})
提示:
DAL 可用於保護請求時擷取的資料。但是,對於使用者之間共用資料的靜態路由,資料將在建置時而不是請求時擷取。請使用中間件來保護靜態路由。
為了進行安全檢查,您可以透過將工作階段 ID 與您的資料庫進行比較來檢查工作階段是否有效。使用 React 的 cache 函式,避免在渲染過程中向資料庫發送不必要的重複請求。
import'server-only'import { getUser } from'@/app/lib/dal'functioncanSeeUsername(viewer:User) {returntrue}functioncanSeePhoneNumber(viewer:User, team:string) {returnviewer.isAdmin || team ===viewer.team}exportasyncfunctiongetProfileDTO(slug:string) {constdata=awaitdb.query.users.findMany({ where:eq(users.slug, slug),// Return specific columns here })constuser= data[0]constcurrentUser=awaitgetUser(user.id)// Or return only what's specific to the query herereturn { username:canSeeUsername(currentUser) ?user.username :null, phonenumber:canSeePhoneNumber(currentUser,user.team)?user.phonenumber:null, }}
透過在 DAL 中集中資料請求和授權邏輯,並使用 DTO,您可以確保所有資料請求都是安全且一致的,從而更容易在應用程式擴展時進行維護、審核和除錯。
處理 伺服器動作 時,應採取與面向公眾的 API 端點相同的安全考量,並驗證使用者是否被允許執行變更。
在下面的範例中,我們會在允許動作繼續進行之前檢查使用者的角色
app/lib/actions.ts
TypeScript
'use server'import { verifySession } from'@/app/lib/dal'exportasyncfunctionserverAction(formData:FormData) {constsession=awaitverifySession()constuserRole=session?.user?.role// Return early if user is not authorized to perform the actionif (userRole !=='admin') {returnnull }// Proceed with the action for authorized users}
處理 路由處理程式 時,應採取與面向公眾的 API 端點相同的安全考量,並驗證使用者是否被允許存取路由處理程式。
例如:
app/api/route.ts
TypeScript
import { verifySession } from'@/app/lib/dal'exportasyncfunctionGET() {// User authentication and role verificationconstsession=awaitverifySession()// Check if the user is authenticatedif (!session) {// User is not authenticatedreturnnewResponse(null, { status:401 }) }// Check if the user has the 'admin' roleif (session.user.role !=='admin') {// User is authenticated but does not have the right permissionsreturnnewResponse(null, { status:403 }) }// Continue for authorized users}