2022年4月26日

Remix、Prisma & MongoDB でフルスタックアプリを構築する:認証

15分で読めます

MongoDB、Prisma、Remix を使用して、ゼロからフルスタックアプリケーションを構築する方法を学ぶシリーズの第2回へようこそ! 今回は、Remixアプリケーションのセッションベース認証を設定します。

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

目次

はじめに

このシリーズの前回のパートでは、Remixプロジェクトをセットアップし、MongoDBデータベースを起動して実行しました。 また、TailwindCSSとPrismaを設定し、schema.prisma ファイルで User コレクションのモデル化を開始しました。

今回のパートでは、アプリケーションに認証を実装し、ユーザーがアカウントを作成し、サインインフォームとサインアップフォームを介してサインインできるようにします。

: このプロジェクトの開始点は、GitHubリポジトリの part-1 ブランチで入手できます。 このパートの最終結果を確認したい場合は、part-2 ブランチにアクセスしてください。

開発環境

提供されている例に従うには、以下が必要です...

  • ... Node.js がインストールされていること。
  • ... Git がインストールされていること。
  • ... TailwindCSS VSCode Extension がインストールされていること。 (オプション)
  • ... Prisma VSCode Extension がインストールされていること。 (オプション)

: オプションの拡張機能は、TailwindとPrismaに非常に優れたインテリセンスと構文の強調表示を追加します。

ログインルートの設定

最初に始める必要があるのは、サインインフォームとサインアップフォームが配置される /login ルートを設定することです。

Remixフレームワーク内にルートを作成するには、app/routes フォルダにファイルを追加します。 そのファイルの名前がルートの名前として使用されます。 Remixでのルーティングの仕組みの詳細については、ドキュメントを参照してください。

app/routeslogin.tsx という名前の新しいファイルを作成し、次の内容を記述します。

ルートファイルのデフォルトエクスポートは、Remixがブラウザにレンダリングするコンポーネントです。

npm run dev を使用して開発サーバーを起動し、http://localhost: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> タグを現在存在する <div> の代わりに新しい Layout コンポーネント内にラップできます。

フォームの構築

次に、email 入力と password 入力を受け取り、送信ボタンを表示するサインインフォームを追加します。 サイトにアクセスしたときにユーザーを迎えるための素敵なウェルカムメッセージを上部に追加し、Tailwindのflexクラスを使用してフォーム全体を画面の中央に配置します。

現時点では、<form> の action がどこを指しているかを気にする必要はありません。 method 値が "post" であることだけを確認してください。 後で、アクションを設定するクールなRemixマジックをいくつか確認します!

フォームフィールドコンポーネントの作成

入力フィールドとそのラベルは、フォームを追加するときにこのアプリケーション全体でかなり書き換えられるため、コードの重複を避けるために、それらを 制御されたコンポーネント である FormField に分割します。

app/componentsform-field.tsx という名前の新しいファイルを作成し、そこに FormField コンポーネントを構築します。 次に、次のコードを追加して開始します。

これにより、以前にサインインフォームに存在していたラベルと入力の組み合わせがまったく同じように定義およびエクスポートされます。ただし、このコンポーネントには構成可能なオプションがあります。

  • htmlFor: 入力フィールドの id および name 属性と、ラベルの htmlFor 属性に使用する値。
  • label: ラベルに表示するテキスト。
  • value: 入力フィールドの現在の制御された値。
  • type: オプション 入力フィールドの type 属性を設定できますが、デフォルト値は 'text' です。
  • onChange: オプション 入力フィールドの値が変更されたときに実行する関数を提供できます。 デフォルトは空の関数呼び出しです。

これで、既存のラベルと入力をこのコンポーネントに置き換えることができます。

これにより、新しい FormField コンポーネントがインポートされます。このコンポーネントの状態は、親(この場合はサインインフォーム)によって管理されます。 値への更新は、handleInputChange 関数を使用して追跡されます。

エラーメッセージ処理を追加するために、FormField コンポーネントには後で戻ってきますが、これで今のところ必要なことは実行できます。

サインアップフォームの追加

ユーザーがアカウントにサインアップする方法も必要になります。つまり、別のフォームが必要になります。 このフォームは4つの値を受け取ります。

  • メール
  • パスワード

/signup ルートを新しく作成する必要がないように、/login ルートとほぼ同じように見えるように、サインインフォームの目的を変更して、ログインと登録の2つの異なるアクションを切り替えられるようにします。

フォームアクションを state に保存する

まず、ユーザーがフォームを切り替えることができるようにし、コードがフォームを区別できるようにする方法が必要です。

Login コンポーネントの上部で、state に別の変数を作成して action を保持します。

: デフォルトの状態はログイン画面になります。

次に、表示する状態を切り替える方法が必要です。 「Kudosへようこそ」メッセージの上に、次のボタンを追加します。

このボタンのテキストは、action state に基づいて変更されます。 onClick メソッドは、state を 'login' 値と 'register' 値の間で前後に切り替えます。

このページの静的テキストのいくつかの場所で、表示しているフォームに応じて調整する必要があります。 具体的には、「賞賛を与えるためにログインしてください!」サブヘッダーと、フォーム自体の「サインイン」ボタンです。

フォームのサブヘッダーを変更して、各フォームに異なるメッセージを表示します。

サインイン ボタンを完全に取り除き、代わりに次の <button> に置き換えます。

この新しいボタンには、name 属性と value 属性があります。 値は、state の action が何であれ設定されます。 フォームが送信されると、この値はフォームデータとともに _action として渡されます。

: このトリックは、<button>name 属性がアンダースコアで始まる場合にのみ機能します。

選択したフォームに応じて、更新されたメッセージが表示されるはずです。 「サインアップ」ボタンと「サインイン」ボタンを数回クリックして試してみてください。

切り替え可能なフィールドの追加

このページのテキストは見栄えが良いですが、両方のフォームで同じ入力フィールドが表示されています。 最後に必要なのは、サインアップフォームが表示されているときにもう少しフィールドを追加することです。

password フィールドの後に次のフィールドを追加し、新しいフィールドを formData オブジェクトに追加してください。

ここでは2つの変更が行われました。

  1. formData state に2つの新しいキーを追加しました。
  2. サインインフォームまたはサインアップフォームを表示しているかどうかに応じて条件付きでレンダリングされる2つのフィールドを追加しました。

これで、サインインフォームとサインアップフォームが視覚的に完成しました! 次のステップに進む時が来ました。フォームを機能させることです。

認証フロー

このセクションは、設計および構築してきたすべてを実際に機能させる楽しい部分です!

ただし、先に進む前に、プロジェクトに新しい依存関係が必要です。 次のコマンドを実行します。

これにより、bcryptjs ライブラリとその型定義がインストールされます。 これは、後でパスワードをハッシュ化および比較するために使用します。

認証はセッションベースになり、Remixの認証と同じパターンに従います。Jokes App チュートリアル。

アプリの認証フローがどのように見えるかをよりよく視覚化するために、下の図を見てください。

ユーザーを認証するために実行する一連のステップがあり、2つの潜在的な経路(サインインサインアップ)があります。

  1. ユーザーはサインインまたはサインアップを試みます。
  2. フォームが検証されます。
  3. login 関数または register 関数が呼び出されます。
  4. ログインしている場合、サーバー側のコードは、提供されたログイン詳細情報を持つユーザーが存在することを確認します。 アカウントに登録している場合、提供されたメールアドレスでアカウントがまだ存在しないことを確認します。
  5. 上記の手順が成功した場合、新しいクッキーセッションが作成され、ユーザーはホームページにリダイレクトされます。
  6. ステップが成功せず、問題が発生した場合、ユーザーはログイン画面または登録画面に戻され、エラーが表示されます。

これを開始するには、app ディレクトリ内に utils という名前のフォルダを作成します。 ここには、ヘルパー、サービス、構成ファイルを保存します。

その新しいフォルダ内に、auth.server.ts という名前のファイルを作成します。ここに、認証とセッション関連のメソッドを記述します。

: Remixは、ファイルタイプ前の .server を持つファイルを、ブラウザに送信されるコードでバンドルしません。

register 関数の構築

最初に構築する関数は register 関数です。これにより、ユーザーは新しいアカウントを作成できます。

app/utils/auth.server.ts から register という名前の非同期関数をエクスポートします。

登録フォームが提供するフィールドを定義する type を作成し、app/utils 内の別の新しいファイルに types.server.ts という名前で保存してエクスポートします。

その typeapp/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 関数に次を追加します。

register 関数は、提供されたメールアドレスを持つデータベース内のユーザーをクエリするようになりました。

ここでは count 関数が使用されました。これは、数値が返されるためです。 クエリに一致するレコードがない場合、0 が返され、これは false と評価されます。 それ以外の場合は、0 より大きい値が返され、これは true と評価されます。

ユーザーが見つかった場合、関数は 400 ステータスコードで json 応答を返します。

データモデルの更新

これで、ユーザーがサインアップしようとしたときに、提供されたメールアドレスを持つ別のユーザーがすでに存在しないことを確認できます。 次に、register 関数は新しいユーザーを作成する必要があります。 ただし、Prismaスキーマ(firstNamelastName)にまだ存在しないフィールドがいくつかあります。

このデータは、埋め込みドキュメント を含む User モデルの profile という名前のフィールドに保存します。

prisma/schema.prisma ファイルを開き、次の type ブロックを追加します。

type キーワードは、コンポジットタイプを定義するために使用されます。これにより、ドキュメント内にドキュメントを定義できます。 JSONタイプではなくコンポジットタイプを使用する利点は、ドキュメントをクエリするときにタイプセーフティが得られることです。

これは非常に役立ちます。これにより、MongoDBの柔軟な性質により、流動的で何でも含めることができるはずだったデータの形状を明示的に定義する機能が提供されるためです。

この新しいコンポジットタイプ(埋め込みドキュメントの別の名前)を使用してフィールドを記述したことはまだありません。 User モデルで、新しい profile フィールドを追加し、Profile タイプをデータ型として使用します。

素晴らしいですね。User モデルに profile 埋め込みドキュメントが含まれるようになりました。 これらの新しい変更を考慮して、Prisma Client を再生成します。

: 新しいコレクションまたはインデックスを追加していないため、prisma db push を実行する必要はありません。

ユーザーサービスの追加

app/utilsuser.server.ts という名前の別のファイルを作成します。ここに、ユーザー固有の関数を記述します。 そのファイルに、次の関数とインポートを追加します。

この createUser 関数は、いくつかのことを行います。

  1. 登録フォームで提供されたパスワードをハッシュ化します。プレーンテキストとして保存しないでください。
  2. Prisma を使用して新しい User ドキュメントを保存します。
  3. 新しいユーザーの idemail を返します。

: JSONオブジェクトを渡すことで、このクエリで profile 埋め込みドキュメントの詳細を直接入力できました。Prismaが生成する型定義のおかげで、優れたオートコンプリートが表示されます。

この関数は、ユーザーの実際の作成を処理するために register 関数で使用されます。 app/utils/auth.server.ts で、新しい createUser 関数をインポートし、register 関数内で呼び出します。

これで、ユーザーが登録するときに、提供されたメールアドレスを持つ別のユーザーがまだ存在しない場合、新しいユーザーが作成されます。 ユーザーの作成中に問題が発生した場合、emailpassword に渡された値とともにエラーがクライアントに返されます。

login 関数の構築

login 関数は、emailpassword を受け取ります。この関数を開始するために、app/utils/types.server.ts にそのデータを記述する新しい LoginForm タイプを作成します。

次に、login 関数を作成します。app/utils/auth.server.ts に次を追加します。

上記のコード...

  1. ... 新しい typebcryptjs ライブラリをインポートします。
  2. ... 一致するメールアドレスを持つユーザーをクエリします。
  3. ... ユーザーが見つからない場合、または提供されたパスワードがデータベース内のハッシュ化された値と一致しない場合は、null 値を返します。
  4. ... すべてがうまくいった場合は、ユーザーの idemail を返します。

これにより、正しい資格情報が提供されたことが保証され、新しいクッキーセッションを作成するために必要なデータが返されます。

セッション管理の追加

サインインまたはアカウント登録時にユーザーのクッキーセッションを生成する方法が必要になりました。 Remixは、createCookieSessionStorage 関数を使用して、これらのクッキーセッションを簡単に保存する方法を提供します。

その関数を app/utils/auth.server.ts にインポートし、インポートの直後に新しいクッキーセッションストレージを追加します。

上記のコードは、いくつかの設定でセッションストレージを作成します。

  • name: クッキーの名前。
  • secure: trueの場合、CookieはHTTPS経由でのみ送信できます。
  • secrets: セッションの秘密鍵です。
  • sameSite: Cookieをクロスサイトリクエストで送信できるかどうかを指定します。
  • path: Cookieを送信するためにURLに存在する必要があるパスです。
  • maxAge: Cookieが自動的に削除されるまでに有効である期間を定義します。
  • httpOnly: trueの場合、JavaScriptがCookieにアクセスすることを許可しません。

Note: Cookieのさまざまなオプションの詳細についてはこちらをご覧ください。

また、.envファイルにセッションシークレットを設定する必要があります。SESSION_SECRETという名前の変数を秘密の値と共に追加してください。例:

これでセッションストレージの設定は完了です。Cookieセッションを実際に作成する関数をもう1つapp/utils/auth.server.tsに作成します。

この関数は...

  • ... 新しいセッションを作成します。
  • ... そのセッションのuserIdをログインしているユーザーのidに設定します。
  • ... ユーザーをこの関数を呼び出すときに指定できるルートにリダイレクトします。
  • ... Cookieヘッダーを設定するときにセッションをコミットします。

createUserSession関数は、ユーザーが正常に登録またはサインインしたときに、register関数とlogin関数で使用できるようになりました。

ログインおよび登録フォームの送信を処理する

新しいユーザーを作成してログインするために必要なすべての関数を作成しました。今度は、作成したフォームでそれらを活用します。

app/routes/login.tsxで、action関数をエクスポートします。

Note: Remixは、定義しているルートへのPOSTリクエストを設定するために、actionという名前のエクスポートされた関数を探します。

次に、フォーム入力を検証するために使用されるバリデーター関数を2つ、app/utils内の新しいファイルvalidators.server.tsに作成します。

app/routes/login.tsxaction関数内で、リクエストからフォームデータを取得し、正しい形式であることを検証します。

上記のコードは少し怖いかもしれませんが、要するに...

  • ... リクエストオブジェクトからフォームデータを取得します。
  • ... emailpasswordが提供されていることを確認します。
  • ... _actionの値が"register"の場合、firstNamelastNameが提供されていることを確認します。
  • ... 問題が発生した場合、フォームフィールドの値とともにエラーを返します。これにより、フィールドのいずれかが無効な場合に、ユーザーの入力とエラーメッセージでフォームを再度入力できます。

最後に行う必要があるのは、入力が適切に見える場合に、実際にregister関数とlogin関数を実行することです。

switchステートメントを使用すると、フォームからの_action値に含まれる内容に応じて、login関数とregister関数を条件付きで実行できます。

実際にこのアクションをトリガーするには、フォームはこのルートにPOSTする必要があります。幸いなことに、Remixはこれを処理します。Remixは、エクスポートされたaction関数を認識すると、/loginルートへのPOSTリクエストを自動的に構成します。

ログインまたはアカウントの作成を試みると、その後メイン画面に送信されるはずです。成功!🎉

プライベートルートでユーザーを認証する

ユーザーエクスペリエンスをより良くするために次に行うことは、ユーザーが有効なセッションを持っているかどうかに応じて、ユーザーをホームまたはログインページに自動的にリダイレクトすることです。

app/utils/auth.server.tsに、いくつかのヘルパー関数を追加する必要があります。

これは多くの新しい機能です。上記の関数が何をするかを示します

  • requireUserIdは、ユーザーのセッションを確認します。存在する場合、成功となり、userIdを返すだけです。ただし、失敗した場合は、ユーザーをログイン画面にリダイレクトします。
  • getUserSessionは、リクエストのCookieに基づいて現在のユーザーのセッションを取得します。
  • getUserIdは、セッションストレージから現在のユーザーのidを返します。
  • getUserは、現在のセッションに関連付けられたuserドキュメント全体を返します。見つからない場合、ユーザーはログアウトされます。
  • logoutは、現在のセッションを破棄し、ユーザーをログイン画面にリダイレクトします。

これらを配置することで、プライベートルートで適切な認証を実装できます。

app/routes/index.tsxで、ログインしていない場合は、以下を追加してユーザーをログイン画面に戻します。

Note: Remixは、ページを提供する前にloader関数を実行します。これは、ローダー内のリダイレクトは、ページが提供される前にトリガーされることを意味します。

ログインしていない状態でアプリケーションのベースルート(/)に移動しようとすると、URLにredirectToパラメーターが付いたログイン画面にリダイレクトされるはずです。

Note: すでにログインしている場合は、Cookieをクリアする必要があるかもしれません。

次に、基本的に反対のことを行います。ログインしているユーザーがログインページにアクセスしようとすると、すでにログインしているため、ホームページにリダイレクトする必要があります。次のコードをapp/routes/login.tsxに追加します。

フォームの検証を追加する

素晴らしい!サインインフォームとサインアップフォームが機能し、プライベートルートに認証とリダイレクトを設定しました。ゴールは目前です!

最後に行うことは、フォームの検証を追加し、action関数から返されるエラーメッセージを表示することです。

FormFieldコンポーネントを更新して、エラーメッセージを処理できるようにします。

このコンポーネントは、エラーメッセージを受け取るようになります。ユーザーがそのフィールドに入力を開始すると、エラーメッセージが表示されていた場合はクリアされます。

ログインフォームでは、エラーメッセージを取り出すために、RemixのuseActionDataフックを使用して、アクションから返されたデータにアクセスする必要があります。

このコードは以下を追加します

  1. action関数から返されたデータにフックします。
  2. 「無効なメールアドレス」などのフィールド固有のエラーをオブジェクトに保持するerrors変数を設定します。また、「ログインに失敗しました」などのフォームメッセージを表示するためのエラーメッセージを保持するformError変数も設定します。
  3. 利用可能な場合は、formData状態変数を、action関数から返された任意の値にデフォルト設定するように更新します。

ユーザーにエラーが表示され、フォームを切り替えた場合、フォームと表示されているエラーをクリアする必要があります。これを実現するために、これらのeffectsを使用します。

これらを配置することで、フォームとフィールドに表示するエラーを最終的に知らせることができます。

これで、サインアップフォームとサインインフォームでエラーメッセージとフォームのリセットが正常に機能するはずです!

まとめと今後のステップ

Kudos _(😉)_ このセクションの最後までお付き合いいただき、**お疲れ様でした** _(😉)_! カバーすべきことがたくさんありましたが、うまくいけば、次のことを理解できたはずです。

  • Remixでルートを設定する方法。
  • 検証付きのサインインフォームとサインアップフォームを作成する方法。
  • セッションベースの認証の仕組み。
  • 認証を実装してプライベートルートを保護する方法。
  • ユーザーの作成と認証時にPrismaを使用してデータを保存およびクエリする方法。

このシリーズの次のセクションでは、Kudosのホームページとkudos共有機能を構築します。また、kudosフィードに検索およびフィルタリング機能を追加します。

次の記事をお見逃しなく!

Prismaニュースレターに登録する