2022 年 4 月 26 日

使用 Remix、Prisma 和 MongoDB 建構全端應用程式:身份驗證

15 分鐘閱讀

歡迎來到本系列的第二篇文章,您將在此學習如何從頭開始使用 MongoDB、Prisma 和 Remix 建構全端應用程式!在這一部分中,您將為您的 Remix 應用程式設定基於會話的身份驗證。

Build A Fullstack App with Remix, Prisma & MongoDB: Authentication

目錄

簡介

在本系列的上一部分中,您設定了您的 Remix 專案,並啟動並執行了 MongoDB 資料庫。您還設定了 TailwindCSS 和 Prisma,並開始在您的 schema.prisma 檔案中建立 User 集合的模型。

在這一部分中,您將在您的應用程式中實作身份驗證,允許使用者建立帳戶並透過登入和註冊表單登入。

注意:此專案的起點可在 GitHub 儲存庫的 part-1 分支中找到。如果您想查看此部分的最終結果,請前往 part-2 分支。

開發環境

為了跟隨提供的範例,您應該...

注意:可選的擴充功能為 Tailwind 和 Prisma 增加了一些非常好的智能感知和語法突顯。

設定登入路由

您需要做的第一件事是設定一個 /login 路由,您的登入和註冊表單將位於其中。

若要在 Remix 框架中建立路由,請將檔案新增至 app/routes 資料夾。該檔案的名稱將用作路由的名稱。有關 Remix 中路由如何運作的更多資訊,請查看他們的文件

app/routes 中建立一個名為 login.tsx 的新檔案,其中包含以下內容

路由檔案的預設匯出是 Remix 呈現到瀏覽器中的組件。

使用 npm run dev 啟動開發伺服器,並導航至 https://127.0.0.1:3000/login,您應該會看到呈現的路由。

這可行,但看起來還不太好... 接下來您將透過新增實際的登入表單來改善它。

建立可重複使用的版面配置組件

首先,建立一個組件,您將在其中包裝您的路由,以提供一些共用的格式和樣式。您將使用組合模式來建立此 Layout 組件。

組合

組合是一種模式,您透過組件的 props 為組件提供一組子元素。children prop 代表在父組件的開始和結束標籤之間定義的元素。例如,考慮組件名為 Parent 的用法

在這種情況下,<p> 標籤是 Parent 組件的子項,並且將在您決定呈現 children prop 值的位置呈現到 Parent 組件中。

若要查看實際運作情況,請在 app 資料夾內建立一個名為 components 的新資料夾。在該資料夾內建立一個名為 layout.tsx 的新檔案。

在該檔案中,匯出以下函式組件

此組件使用 Tailwind 類別來指定您希望包裝在組件中的任何內容佔據螢幕的完整寬度和高度,使用單一字體,並顯示適度深藍色作為背景。

請注意,children prop 呈現在 <div> 內部。若要查看實際使用時的呈現方式,請查看以下程式碼片段

建立登入表單

現在您可以將該組件匯入到 app/routes/login.tsx 檔案中,並將您的 <h2> 標籤包裝在新的 Layout 組件內,而不是目前所在的 <div>

建構表單

接下來新增一個登入表單,該表單接受 emailpassword 輸入,並顯示提交按鈕。在頂部新增一條友善的歡迎訊息,以便在使用者進入您的網站時問候他們,並使用Tailwind 的 flex 類別將整個表單置於螢幕中央。

目前,您無需擔心 <form> 的 action 指向何處,只需確保它具有 method"post" 即可。稍後您將查看一些很酷的 Remix 魔術,為我們設定 action!

建立表單欄位組件

當您新增更多表單時,輸入欄位及其標籤將在本應用程式中被大量重寫,因此將這些欄位分解為名為 FormField受控組件,以避免程式碼重複。

app/components 中建立一個名為 form-field.tsx 的新檔案,您將在其中建構 FormField 組件。然後新增以下程式碼以開始

這將定義和匯出與您之前在登入表單中相同的標籤和輸入組合,只是此組件將具有可配置的選項

  • htmlFor:用於輸入欄位的 idname 屬性的值,以及標籤的 htmlFor 屬性。
  • label:標籤中顯示的文字。
  • value:輸入欄位目前的受控值。
  • type可選 允許您設定輸入欄位的 type 屬性,但預設值為 'text'
  • onChange可選 允許您提供一個函數,以便在輸入欄位的值變更時執行。預設為空函數呼叫。

您現在可以使用此組件取代現有的標籤和輸入

這將匯入新的 FormField 組件,其狀態將由父項(在本例中為登入表單)管理。任何對值的更新都將使用 handleInputChange 函數進行追蹤。

稍後您將回到 FormField 組件以新增錯誤訊息處理,但目前這樣就足以滿足您的需求!

新增註冊表單

您還需要一種讓使用者註冊帳戶的方法,這表示您需要另一個表單。此表單將接受四個值

  • 電子郵件
  • 密碼
  • 名字
  • 姓氏

為了避免建立看起來與 /login 路由幾乎相同的新的 /signup 路由,請重新調整登入表單的用途,使其可以在兩種不同的動作之間切換:登入和註冊。

將表單動作儲存在狀態中

首先,您需要某種方法讓使用者能夠在表單之間切換,並讓您的程式碼能夠區分表單。

Login 組件的頂部,在狀態中建立另一個變數來保存您的 action

注意:預設狀態將是登入畫面。

接下來,您需要某種方法來切換您想要檢視的狀態。在 "Welcome to Kudos" 訊息上方,新增以下按鈕

此按鈕的文字將根據 action 狀態而變更。onClick 方法將在 'login' 和 'register' 值之間來回切換狀態。

此頁面上有一些靜態文字,您將需要根據您正在檢視的表單進行調整。具體而言,是 "Log In To Give Some Praise!" 子標題和表單本身的 "Sign In" 按鈕。

變更表單的子標題以在每個表單上顯示不同的訊息

完全刪除登入按鈕,並將其替換為以下 <button>

這個新按鈕具有 namevalue 屬性。值設定為狀態 action 的任何值。當您的表單提交時,此值將與表單資料一起作為 _action 傳遞。

注意:此技巧僅在 <button> 上有效,如果 name 屬性以下底線開頭。

根據您選取的表單,您現在應該看到更新的訊息。點擊 "Sign Up" 和 "Sign In" 按鈕幾次來試試看。

新增可切換欄位

此頁面上的文字看起來很棒,但在兩個表單上都顯示相同的輸入欄位。您需要的最後一件事是在顯示註冊表單時新增一些欄位。

password 欄位之後新增以下欄位,並確保將新欄位新增至 formData 物件。

這裡做了兩個變更

  1. 您在 formData 狀態中新增了兩個新鍵。
  2. 您新增了兩個欄位,這些欄位是有條件地呈現的,具體取決於您是檢視登入表單還是註冊表單。

您的登入和註冊表單現在在視覺上已完成!現在是時候繼續進行下一個部分了:使表單具備功能。

身份驗證流程

本節是有趣的部分,您將在此讓您一直在設計和建構的所有內容實際運作!

但是,在繼續之前,您需要在專案中新增一個新的依賴項。執行以下命令

這會安裝 bcryptjs 程式庫及其類型定義。稍後您將使用它來雜湊和比較密碼。

身份驗證將是基於會話的,遵循 Remix 的 身份驗證 中使用的相同模式,用於 Remix 的 Jokes App 教學課程。

為了更好地視覺化您的應用程式身份驗證流程的外觀,請查看下圖。

將有一系列步驟來驗證使用者身份,其中有兩個潛在路徑(登入註冊

  1. 使用者將嘗試登入或註冊。
  2. 表單將被驗證。
  3. 將呼叫 loginregister 函數。
  4. 如果登入,伺服器端程式碼將確保存在具有所提供登入詳細資訊的使用者。如果註冊帳戶,它將確保具有所提供電子郵件的帳戶尚不存在。
  5. 如果上述步驟通過,將建立新的 Cookie 會話,並且使用者將被重新導向到首頁。
  6. 如果某個步驟未通過並且出現問題,使用者將被送回登入或註冊畫面,並且將顯示錯誤訊息。

首先,在 app 目錄中建立一個名為 utils 的資料夾。您將在此處儲存任何輔助程式、服務和組態檔案。

在該新資料夾內,建立一個名為 auth.server.ts 的檔案,您將在其中編寫您的身份驗證和工作階段相關方法。

注意:Remix 不會將檔案類型前帶有 .server 的檔案與傳送到瀏覽器的程式碼捆綁在一起。

建構註冊函式

您將建構的第一個函數是註冊函數,它將允許使用者建立新帳戶。

app/utils/auth.server.ts 匯出一個名為 register 的異步函數

建立並匯出一個 type,用於定義註冊表單將在 app/utils 內名為 types.server.ts 的另一個新檔案中提供的欄位。

將該 type 匯入到 app/utils/auth.server.ts 中,並在 register 函數中使用它來描述 user 參數,其中將包含註冊表單的資料

當呼叫此 register 函數並提供 user 時,您需要檢查的第一件事是是否已存在具有所提供電子郵件的使用者。

注意:請記住,email 欄位在您的架構中定義為唯一的。

建立 PrismaClient 的實例

您將使用 PrismaClient 來執行資料庫查詢,但是您的應用程式尚無法使用其實例。

app/utils 資料夾中建立一個名為 prisma.server.ts 的新檔案,您將在其中建立和匯出 Prisma Client 的實例

注意:上面已採取預防措施,以防止即時重新載入在開發時使用連線使您的資料庫飽和。

您現在有一種存取資料庫的方法。在 app/utils/auth.server.ts 中,匯入實例化的 PrismaClient,並將以下內容新增至 register 函數

現在,註冊函數將查詢資料庫中是否有任何使用者具有提供的電子郵件。

此處使用了 count 函數,因為它會傳回數值。如果沒有符合查詢的記錄,它將傳回 0,其評估結果為 false。否則,將傳回大於 0 的值,其評估結果為 true

如果找到使用者,該函數將傳回 json 回應,其中包含 400 狀態碼。

更新您的資料模型

現在您可以確定,當使用者嘗試註冊時,另一個使用者不會使用提供的電子郵件已經存在。接下來,register 函數應該建立新使用者。但是,我們將儲存一些欄位,這些欄位在 Prisma 架構 (firstNamelastName) 中尚不存在。

您將把此資料儲存在包含 嵌入式文件User 模型中名為 profile 的欄位中。

開啟您的 prisma/schema.prisma 檔案並新增以下 type 區塊

type 關鍵字用於定義複合類型 – 允許您在文件內部定義文件。與 JSON 類型相比,使用複合類型的優點是,在查詢文件時,您可以獲得類型安全。

超級有幫助,因為它讓您能夠明確定義資料的形狀,否則由於 MongoDB 的彈性性質,資料的形狀將是流動的並且能夠包含任何內容。

您尚未將這個新的複合類型(嵌入式文件的另一個名稱)用於描述欄位。在您的 User 模型中,新增一個新的 profile 欄位,並使用 Profile 類型作為其資料類型

太棒了,您的 User 模型現在將包含一個 profile 嵌入式文件。重新產生 Prisma Client 以考慮這些新變更

注意:您不需要執行 prisma db push,因為您沒有新增任何新的集合或索引。

新增使用者服務

app/utils 中建立另一個名為 user.server.ts 的檔案,其中將編寫任何使用者特定的函數。在該檔案中,新增以下函數和匯入

createUser 函數執行幾項操作

  1. 它會雜湊註冊表單中提供的密碼,因為您不應將其儲存為純文字。
  2. 它使用 Prisma 儲存新的 User 文件。
  3. 它會傳回新使用者的 idemail

注意:您可以透過傳入 JSON 物件直接填寫 profile 嵌入式文件的詳細資訊,在此查詢中,您將看到一些不錯的自動完成功能,因為 Prisma 產生了類型定義。

此函數將在您的 register 函數中使用,以處理使用者的實際建立。在 app/utils/auth.server.ts 中,匯入新的 createUser 函數,並在 register 函數中調用它。

現在,當使用者註冊時,如果另一個使用者尚未使用提供的電子郵件存在,則將建立一個新使用者。如果在建立使用者期間發生錯誤,則會將錯誤以及傳入 emailpassword 的值傳回給用戶端。

建構登入函式

login 函數將接受 emailpassword,因此若要開始此函數,請建立一個新的 LoginForm 類型,以在 app/utils/types.server.ts 中描述該資料

然後透過將以下內容新增至 app/utils/auth.server.ts 來建立 login 函數

上面的程式碼...

  1. ... 匯入新的 typebcryptjs 程式庫。
  2. ... 查詢具有相符電子郵件的使用者。
  3. ... 如果找不到使用者或提供的密碼與資料庫中的雜湊值不符,則傳回 null 值。
  4. ... 如果一切順利,則傳回使用者的 idemail

這將確保提供了正確的憑證,並將回傳建立新的 Cookie 會話所需的資料。

新增工作階段管理

您現在需要一種方法,在使用者登入或註冊帳號時,為他們產生 Cookie Session。Remix 提供了簡單的方式來儲存這些 Cookie Session,透過他們的 createCookieSessionStorage 函式。

將該函式匯入到 app/utils/auth.server.ts 中,並在您的 imports 之後直接新增一個 Cookie Session Storage

上面的程式碼建立了一個包含幾個設定的 Session Storage

  • name:Cookie 的名稱。
  • secure:如果 true,則僅允許透過 HTTPS 發送 Cookie。
  • secrets:Session 的密鑰。
  • sameSite:指定是否允許跨站請求發送 Cookie。
  • path:URL 中必須存在的路徑,才能發送 Cookie。
  • maxAge:定義 Cookie 在自動刪除之前允許存活的時間長度。
  • httpOnly:如果 true,則不允許 JavaScript 存取 Cookie。

注意:若要瞭解更多關於 Cookie 選項的資訊,請參閱這裡

您還需要在 .env 檔案中設定 Session 密鑰。新增一個名為 SESSION_SECRET 的變數,並設定一個密鑰值。例如

Session Storage 現在已設定完成。在 app/utils/auth.server.ts 中再建立一個函式,該函式將實際建立 Cookie Session

此函式...

  • ... 建立一個新的 Session。
  • ... 將該 Session 的 userId 設定為已登入使用者的 id
  • ... 將使用者重新導向至您在呼叫此函式時可以指定的路由。
  • ... 在設定 Cookie 標頭時提交 Session。

現在可以在使用者成功註冊或登入時,在 registerlogin 函式中使用 createUserSession 函式。

處理登入和註冊表單提交

您已建立所有建立新使用者和讓他們登入所需的函式。現在您將在您建立的表單中使用它們。

app/routes/login.tsx 中,匯出一個 action 函式。

注意:Remix 會尋找名為 action 的匯出函式,以便在您定義的路由上設定 POST 請求。

現在在 app/utils 內的新檔案中建立幾個驗證器函式,名為 validators.server.ts,這些函式將用於驗證表單輸入。

app/routes/login.tsx 中的 action 函式中,從請求中抓取表單資料並驗證其格式是否正確。

上面的程式碼可能看起來有點嚇人,但簡而言之,它...

  • ... 從請求物件中取出表單資料。
  • ... 確保提供了 emailpassword
  • ... 如果 _action 值為 "register",則確保提供了 firstNamelastName
  • ... 如果發生任何問題,則傳回錯誤以及表單欄位值,以便您稍後在任何欄位無效時,可以使用使用者的輸入和錯誤訊息重新填入表單。

您最後需要做的是,如果輸入看起來沒問題,則實際執行您的 registerlogin 函式。

switch 語句將允許您根據表單中的 _action 值有條件地執行 loginregister 函式。

為了實際觸發此 action,表單需要 POST 到此路由。幸運的是,Remix 會處理此問題,因為當它識別出匯出的 action 函式時,它會自動將 POST 請求配置到 /login 路由。

如果您嘗試登入或建立帳號,您應該會看到您之後被傳送到主畫面。成功!🎉

授權私有路由上的使用者

接下來您將要做的事情是讓使用者的體驗更好,根據使用者是否擁有有效的 Session,自動將使用者重新導向到首頁或登入頁面。

app/utils/auth.server.ts 中,您需要新增幾個輔助函式。

這是很多新功能。以下是上述函式將執行的操作

  • requireUserId 檢查使用者的 Session。如果 Session 存在,則表示成功,並僅傳回 userId。但是,如果失敗,它將將使用者重新導向到登入畫面。
  • getUserSession 根據請求的 Cookie 抓取目前使用者的 Session。
  • getUserId 從 Session Storage 傳回目前使用者的 id
  • getUser 傳回與目前 Session 相關聯的整個 user 文件。如果找不到,則使用者會登出。
  • logout 銷毀目前的 Session,並將使用者重新導向到登入畫面。

有了這些功能,您就可以在您的私有路由上實作一些良好的授權。

app/routes/index.tsx 中,如果使用者未登入,則透過新增以下內容將使用者傳回登入畫面

注意:Remix 會在提供您的頁面之前執行 loader 函式。這表示 loader 中的任何重新導向都會在您的頁面可以提供之前觸發。

如果您嘗試導覽至應用程式的基本路由/,但未登入,您應該會被重新導向到登入畫面,URL 中會帶有 redirectTo 參數。

注意:如果您已登入,您可能需要清除 Cookie。

接下來,基本上做相反的事情。如果已登入的使用者嘗試進入登入頁面,他們應該被重新導向到首頁,因為他們已經登入。將以下程式碼新增至 app/routes/login.tsx

新增表單驗證

太棒了!您的登入和註冊表單運作正常,並且您已在您的私有路由上設定授權和重新導向。您幾乎到達終點線了!

最後要做的事情是新增表單驗證,並顯示從 action 函式傳回的錯誤訊息。

更新 FormField 元件,使其能夠處理錯誤訊息。

此元件現在將接收錯誤訊息。當使用者開始在該欄位中輸入時,如果正在顯示任何錯誤訊息,它將被清除。

在登入表單中,您將需要使用 Remix 的 useActionData Hook 來存取從 action 傳回的資料,以便取出錯誤訊息。

此程式碼新增了以下內容

  1. Hook 進入從 action 函式傳回的資料。
  2. 設定一個 errors 變數,它將在物件中保存特定欄位的錯誤,例如「無效的電子郵件」。它還設定了一個 formError 變數,它將保存要顯示表單訊息的錯誤訊息,例如「登入不正確」。
  3. 更新 formData 狀態變數,以預設為 action 函式傳回的任何值(如果可用)。

如果向使用者顯示錯誤並且切換表單,您將需要清除表單和任何正在顯示的錯誤。使用這些 effects 來達成此目的

有了這些功能,您終於可以讓您的表單和欄位知道要顯示哪些錯誤。

現在您應該會看到錯誤訊息和表單重設在您的註冊和登入表單上正常運作!

摘要 & 接下來是什麼

讚賞(😉) 您堅持到了本節的結尾!有很多內容要涵蓋,但希望您能夠從中瞭解

  • 如何在 Remix 中設定路由。
  • 如何使用驗證建立登入和註冊表單。
  • 基於 Session 的身份驗證如何運作。
  • 如何透過實作授權來保護私有路由。
  • 在建立和驗證使用者時,如何使用 Prisma 儲存和查詢您的資料。

在本系列的下一節中,您將建立 Kudos 的首頁和 kudos 分享功能。您還將在 kudos Feed 中新增搜尋和篩選功能。

不要錯過下一篇文章!

訂閱 Prisma 電子報