本シリーズの第2回へようこそ。このシリーズでは、MongoDB、Prisma、Remixを使用してゼロからフルスタックアプリケーションを構築する方法を学んでいます!このパートでは、Remixアプリケーション向けにセッションベースの認証を設定します。
目次
- はじめに
- ログインルートを設定する
- 再利用可能なレイアウトコンポーネントを作成する
- サインインフォームを作成する
- サインアップフォームを追加する
- 認証フロー
- 登録関数を構築する
- ログイン関数を構築する
- セッション管理を追加する
- ログインおよび登録フォームの送信を処理する
- プライベートなルートでユーザーを認証する
- フォームの検証を追加する
- まとめと今後の展望
はじめに
本シリーズの最後のパートでは、Remixプロジェクトをセットアップし、MongoDBデータベースを稼働させました。また、TailwindCSSとPrismaを設定し、schema.prisma
ファイルでUser
コレクションのモデル化を開始しました。
このパートでは、アプリケーションに認証を実装し、ユーザーがアカウントを作成したり、サインインフォームとサインアップフォームを通じてサインインできるようにします。
注意: このプロジェクトの開始点は、GitHubリポジトリのpart-1ブランチで利用できます。このパートの最終結果を確認したい場合は、part-2ブランチを参照してください。
開発環境
提供された例に沿って進めるためには、以下の条件を満たしている必要があります...
- ... Node.jsがインストールされていること。
- ... Gitがインストールされていること。
- ... TailwindCSS VSCode拡張機能がインストールされていること。(任意)
- ... Prisma VSCode拡張機能がインストールされていること。(任意)
注意: オプションの拡張機能は、TailwindとPrismaに対して非常に優れたIntelliSenseとシンタックスハイライトを追加します。
ログインルートを設定する
最初にやるべきことは、サインインおよびサインアップフォームが配置される/login
ルートを設定することです。
Remixフレームワーク内でルートを作成するには、app/routes
フォルダにファイルを追加します。そのファイル名がルート名として使用されます。Remixでのルーティングの仕組みに関する詳細については、ドキュメントを参照してください。
app/routes
内にlogin.tsx
という新しいファイルを以下の内容で作成します
ルートファイルのデフォルトエクスポートは、Remixがブラウザにレンダリングするコンポーネントです。
npm run dev
を使用して開発サーバーを起動し、https://:3000/login
にアクセスすると、ルートがレンダリングされているのを確認できるはずです。
これは機能しますが、まだ見た目が良くありません... 次に、実際のサインインフォームを追加して、少し見栄えを良くします。
再利用可能なレイアウトコンポーネントを作成する
まず、ルートをラップして共通のフォーマットとスタイルを提供するコンポーネントを作成します。このLayout
コンポーネントを作成するために、コンポジションパターンを使用します。
これを実際に見てみるには、app
フォルダ内にcomponents
という新しいフォルダを作成します。そのフォルダの中に、layout.tsx
という新しいファイルを作成します。
そのファイルで、以下の関数コンポーネントをエクスポートします
このコンポーネントはTailwindのクラスを使用して、コンポーネントでラップされたものが画面の全幅と高さを占め、モノフォントを使用し、背景として適度な濃い青を表示するように指定しています。
children
propが<div>
内にレンダリングされていることに注目してください。これが実際に使用されるとどのようにレンダリングされるかを見るには、以下のスニペットを確認してください
サインインフォームを作成する
これで、そのコンポーネントをapp/routes/login.tsx
ファイルにインポートし、現在の<div>
の代わりに、新しいLayout
コンポーネント内に<h2>
タグをラップすることができます
フォームを構築する
次に、email
とpassword
の入力を受け取り、送信ボタンを表示するサインインフォームを追加します。サイトに入ったときにユーザーに挨拶するための素敵なウェルカムメッセージを上部に追加し、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つの変更が行われました
formData
の状態に2つの新しいキーを追加しました。- サインインフォームまたはサインアップフォームのどちらを表示しているかに応じて条件付きでレンダリングされる2つのフィールドを追加しました。
サインインフォームとサインアップフォームが視覚的に完成しました!次のステップ、フォームの機能化に進みましょう。
認証フロー
このセクションは楽しい部分で、これまで設計し構築してきたすべてを実際に機能させることができます!
しかし、次に進む前に、プロジェクトに新しい依存関係が必要です。次のコマンドを実行してください
これはbcryptjs
ライブラリとその型定義をインストールします。これは後でパスワードのハッシュ化と比較に使用します。
認証はセッションベースで行われ、RemixのJokes Appチュートリアルの認証で使用されているパターンと同じものを使用します。
アプリケーションの認証フローがどのように見えるかをよりよく視覚化するために、以下のグラフィックをご覧ください。
ユーザーを認証するために一連のステップがあり、2つの潜在的なパス (サインインとサインアップ) があります
- ユーザーはサインインまたはサインアップを試みます。
- フォームは検証されます。
login
またはregister
関数が呼び出されます。- ログインの場合、サーバーサイドコードは提供されたログイン詳細を持つユーザーが存在することを確認します。アカウント登録の場合、提供されたメールアドレスでアカウントがすでに存在しないことを確認します。
- 上記のステップがパスした場合、新しいクッキーセッションが作成され、ユーザーはホームページにリダイレクトされます。
- ステップがパスせず問題が発生した場合、ユーザーはログインまたは登録画面に戻され、エラーが表示されます。
これを始めるために、app
ディレクトリ内にutils
というフォルダを作成します。ここにヘルパー、サービス、設定ファイルを保存します。
その新しいフォルダの中に、認証およびセッション関連のメソッドを記述するauth.server.ts
というファイルを作成します。
注意: Remixは、ファイルタイプが
.server
で終わるファイルを、ブラウザに送信されるコードと共にバンドルしません。
登録関数を構築する
最初に構築する関数は登録関数で、ユーザーが新しいアカウントを作成できるようにします。
app/utils/auth.server.ts
からregister
という名前の非同期関数をエクスポートします
app/utils
内の別の新しいファイルtypes.server.ts
で、登録フォームが提供するフィールドを定義するtype
を作成し、エクスポートします。
そのtype
をapp/utils/auth.server.ts
にインポートし、register
関数でサインアップフォームのデータを含むuser
パラメータを記述するために使用します
このregister
関数が呼び出され、user
が提供されたとき、最初に確認する必要があるのは、提供されたメールアドレスでユーザーがすでに存在するかどうかです。
注意:
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スキーマにまだ存在しないいくつかのフィールド (firstName
とlastName
) を保存します。
このデータは、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
関数はいくつかのことを行います
- 登録フォームで提供されたパスワードをハッシュ化します。プレーンテキストで保存すべきではないためです。
- Prismaを使用して新しい
User
ドキュメントを保存します。 - 新しいユーザーの
id
とemail
を返します。
注意: JSONオブジェクトを渡すことで、このクエリで
profile
埋め込みドキュメントの詳細を直接入力できました。Prismaが生成する型定義のおかげで、優れたオートコンプリートが利用できます。
この関数は、register
関数で使用され、実際のユーザー作成を処理します。app/utils/auth.server.ts
で新しいcreateUser
関数をインポートし、register
関数内でそれを呼び出します。
これで、ユーザーが登録する際、提供されたメールアドレスを持つ別のユーザーがすでに存在しない場合、新しいユーザーが作成されます。ユーザーの作成中に何か問題が発生した場合、email
とpassword
に渡された値と共にエラーがクライアントに返されます。
ログイン関数を構築する
login
関数はemail
とpassword
を受け取るため、この関数を開始するために、app/utils/types.server.ts
にそのデータを記述する新しいLoginForm
型を作成します
次に、app/utils/auth.server.ts
に以下を追加してlogin
関数を作成します
上記のコードは...
- ... 新しい
type
とbcryptjs
ライブラリをインポートします。 - ... 一致するメールアドレスを持つユーザーをクエリします。
- ... ユーザーが見つからない場合、または提供されたパスワードがデータベース内のハッシュ化された値と一致しない場合、
null
を返します。 - ... すべてがうまくいけば、ユーザーの
id
とemail
を返します。
これにより、正しい認証情報が提供されたことが確認され、新しいクッキーセッションを作成するために必要なデータが返されます。
セッション管理を追加する
サインインまたはアカウント登録時に、ユーザーのためのクッキーセッションを生成する方法が必要になりました。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.tsx
のaction
関数内で、リクエストからフォームデータを取得し、それが正しい形式であることを検証します。
上記のコードは少し恐ろしく見えるかもしれませんが、要するに...
- ... リクエストオブジェクトからフォームデータを抽出します。
- ...
email
とpassword
が提供されていることを確認します。 - ...
_action
の値が"register"
の場合、firstName
とlastName
が提供されていることを確認します。 - ... 問題が発生した場合、フォームフィールドの値とともにエラーを返すため、後でそれらのフィールドが無効な場合にユーザーの入力とエラーメッセージでフォームを再入力できます。
最後に必要なのは、入力が正しそうであれば、実際に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
フックを使用してアクションから返されたデータにアクセスする必要があります。
このコードは以下を追加します
action
関数から返されたデータにフックします。- オブジェクト内に「無効なメールアドレス」などのフィールド固有のエラーを保持する
errors
変数を設定します。また、「ログイン情報が正しくありません」などのフォームメッセージを表示するためのエラーメッセージを保持するformError
変数を設定します。 - 利用可能な場合、
action
関数から返された値にデフォルト設定されるようにformData
状態変数を更新します。
ユーザーにエラーが表示され、フォームを切り替えた場合、フォームと表示されているエラーをクリアする必要があります。これを実現するには、これらのeffects
を使用します
これらが整ったので、最後にフォームとフィールドに表示するエラーを知らせることができます。
これで、サインアップフォームとサインインフォームでエラーメッセージとフォームのリセットが適切に機能しているはずです!
まとめと今後の展望
お疲れ様でした (😉)、このセクションの最後までお付き合いいただきありがとうございます!多くのことをカバーしましたが、以下の理解を深めることができたことを願っています
- Remixでルートを設定する方法。
- 検証機能付きのサインインおよびサインアップフォームを構築する方法。
- セッションベース認証の仕組み。
- 認証を実装してプライベートなルートを保護する方法。
- ユーザーの作成と認証の際に、Prismaを使用してデータを保存およびクエリする方法。
このシリーズの次のセクションでは、Kudosのホームページとクドス共有機能を作成します。また、クドスフィードに検索およびフィルタリング機能を追加します。
次の投稿をお見逃しなく!
Prismaニュースレターに登録