2022年4月26日

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

15分の読書

本シリーズの第2回へようこそ。このシリーズでは、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に対して非常に優れたIntelliSenseとシンタックスハイライトを追加します。

ログインルートを設定する

最初にやるべきことは、サインインおよびサインアップフォームが配置される/loginルートを設定することです。

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

app/routes内にlogin.tsxという新しいファイルを以下の内容で作成します

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

npm run devを使用して開発サーバーを起動し、https://:3000/loginにアクセスすると、ルートがレンダリングされているのを確認できるはずです。

これは機能しますが、まだ見た目が良くありません... 次に、実際のサインインフォームを追加して、少し見栄えを良くします。

再利用可能なレイアウトコンポーネントを作成する

まず、ルートをラップして共通のフォーマットとスタイルを提供するコンポーネントを作成します。このLayoutコンポーネントを作成するために、コンポジションパターンを使用します。

コンポジション

コンポジション は、コンポーネントにそのpropsを介して一連の子要素を提供するパターンです。children propは、親コンポーネントの開始タグと終了タグの間に定義された要素を表します。例えば、Parentという名前のコンポーネントのこの使用例を考えてみましょう

この場合、<p>タグはParentコンポーネントの子であり、children propの値をレンダリングすることを決定した場所ならどこでもParentコンポーネント内にレンダリングされます。

これを実際に見てみるには、appフォルダ内にcomponentsという新しいフォルダを作成します。そのフォルダの中に、layout.tsxという新しいファイルを作成します。

そのファイルで、以下の関数コンポーネントをエクスポートします

このコンポーネントはTailwindのクラスを使用して、コンポーネントでラップされたものが画面の全幅と高さを占め、モノフォントを使用し、背景として適度な濃い青を表示するように指定しています。

children propが<div>内にレンダリングされていることに注目してください。これが実際に使用されるとどのようにレンダリングされるかを見るには、以下のスニペットを確認してください

サインインフォームを作成する

これで、そのコンポーネントをapp/routes/login.tsxファイルにインポートし、現在の<div>の代わりに、新しいLayoutコンポーネント内に<h2>タグをラップすることができます

フォームを構築する

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

この時点では、<form>のアクションがどこを指しているかを気にする必要はありません。methodの値が"post"であることを確認するだけで十分です。後で、アクションを設定してくれるRemixの素晴らしい魔法を見ていきます!

フォームフィールドコンポーネントを作成する

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

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

これにより、以前サインインフォームにあったものと全く同じラベルと入力の組み合わせが定義され、エクスポートされますが、このコンポーネントには設定可能なオプションが追加されます

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

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

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

後でFormFieldコンポーネントに戻ってエラーメッセージ処理を追加しますが、今のところはこれで十分です!

サインアップフォームを追加する

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

  • メールアドレス
  • パスワード

/loginルートとほとんど同じ見た目の新しい/signupルートを作成する手間を省くために、サインインフォームを再利用して、ログインと登録の2つの異なるアクションを切り替えられるようにします。

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

まず、ユーザーがフォームを切り替えることができ、コードがフォームを区別できる何らかの方法が必要です。

Loginコンポーネントの最上部で、actionを保持するための別の状態変数を定義します。

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

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

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

このページには静的なテキストがいくつかあり、表示しているフォームに応じて調整したくなるでしょう。特に、「Log In To Give Some Praise!」というサブヘッダーと、フォーム内の「Sign In」ボタンです。

フォームのサブヘッダーを、それぞれのフォームで異なるメッセージが表示されるように変更します

サインインボタンを完全に削除し、以下の<button>に置き換えます

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

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

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

切り替え可能なフィールドを追加する

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

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

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

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

サインインフォームとサインアップフォームが視覚的に完成しました!次のステップ、フォームの機能化に進みましょう。

認証フロー

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

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

これはbcryptjsライブラリとその型定義をインストールします。これは後でパスワードのハッシュ化と比較に使用します。

認証はセッションベースで行われ、RemixのJokes Appチュートリアルの認証で使用されているパターンと同じものを使用します。

アプリケーションの認証フローがどのように見えるかをよりよく視覚化するために、以下のグラフィックをご覧ください。

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

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

これを始めるために、appディレクトリ内にutilsというフォルダを作成します。ここにヘルパー、サービス、設定ファイルを保存します。

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

注意: Remixは、ファイルタイプが.serverで終わるファイルを、ブラウザに送信されるコードと共にバンドルしません。

登録関数を構築する

最初に構築する関数は登録関数で、ユーザーが新しいアカウントを作成できるようにします。

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

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

その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関数に以下を追加します

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

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関数はemailpasswordを受け取るため、この関数を開始するために、app/utils/types.server.tsにそのデータを記述する新しいLoginForm型を作成します

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

上記のコードは...

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

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

セッション管理を追加する

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

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

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

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

注意: さまざまなクッキーオプションについては、こちらで詳しく学ぶことができます。

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

セッションストレージの設定が完了しました。app/utils/auth.server.tsにもう1つ、実際にクッキーセッションを作成する関数を作成します

この関数は...

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

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

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

新しいユーザーを作成し、ログインさせるために必要なすべての関数を作成しました。次に、構築したフォームでそれらを使用します。

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

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

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

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

上記のコードは少し恐ろしく見えるかもしれませんが、要するに...

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

最後に必要なのは、入力が正しそうであれば、実際にregister関数とlogin関数を実行することです。

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

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

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

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

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

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

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

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

これらが整備されたので、プライベートなルートに素敵な認証を実装できます。

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

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

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

注意: すでにログインしている場合、クッキーをクリアする必要があるかもしれません。

次に、基本的にその逆を行います。ログインしているユーザーがログインページにアクセスしようとした場合、すでにログインしているためホームページにリダイレクトされるべきです。app/routes/login.tsxに以下のコードを追加します

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

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

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

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

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

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

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

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

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

これらが整ったので、最後にフォームとフィールドに表示するエラーを知らせることができます。

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

まとめと今後の展望

お疲れ様でした (😉)、このセクションの最後までお付き合いいただきありがとうございます!多くのことをカバーしましたが、以下の理解を深めることができたことを願っています

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

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

次の投稿をお見逃しなく!

Prismaニュースレターに登録

© . All rights reserved.