MongoDB、Prisma、Remix を使用して、ゼロからフルスタックアプリケーションを構築する方法を学ぶシリーズの第2回へようこそ! 今回は、Remixアプリケーションのセッションベース認証を設定します。
目次
- はじめに
- ログインルートの設定
- 再利用可能なレイアウトコンポーネントの作成
- サインインフォームの作成
- サインアップフォームの追加
- 認証フロー
- register 関数の構築
- login 関数の構築
- セッション管理の追加
- ログインフォームと登録フォームの送信処理
- プライベートルートでのユーザー認証
- フォームのバリデーションの追加
- まとめと今後のステップ
はじめに
このシリーズの前回のパートでは、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/routes
に login.tsx
という名前の新しいファイルを作成し、次の内容を記述します。
ルートファイルのデフォルトエクスポートは、Remixがブラウザにレンダリングするコンポーネントです。
npm run dev
を使用して開発サーバーを起動し、http://localhost:3000/login
にアクセスすると、ルートがレンダリングされるはずです。
これは機能しますが、まだ見栄えが良くありません... 次に、実際のサインインフォームを追加して、少しだけ見栄えを良くします。
再利用可能なレイアウトコンポーネントの作成
まず、ルートをラップして、いくつかの共有フォーマットとスタイルを提供するコンポーネントを作成します。 コンポジション パターンを使用して、この Layout
コンポーネントを作成します。
これを実際に確認するには、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/components
に form-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つの変更が行われました。
formData
state に2つの新しいキーを追加しました。- サインインフォームまたはサインアップフォームを表示しているかどうかに応じて条件付きでレンダリングされる2つのフィールドを追加しました。
これで、サインインフォームとサインアップフォームが視覚的に完成しました! 次のステップに進む時が来ました。フォームを機能させることです。
認証フロー
このセクションは、設計および構築してきたすべてを実際に機能させる楽しい部分です!
ただし、先に進む前に、プロジェクトに新しい依存関係が必要です。 次のコマンドを実行します。
これにより、bcryptjs
ライブラリとその型定義がインストールされます。 これは、後でパスワードをハッシュ化および比較するために使用します。
認証はセッションベースになり、Remixの認証と同じパターンに従います。Jokes App チュートリアル。
アプリの認証フローがどのように見えるかをよりよく視覚化するために、下の図を見てください。
ユーザーを認証するために実行する一連のステップがあり、2つの潜在的な経路(サインインとサインアップ)があります。
- ユーザーはサインインまたはサインアップを試みます。
- フォームが検証されます。
login
関数またはregister
関数が呼び出されます。- ログインしている場合、サーバー側のコードは、提供されたログイン詳細情報を持つユーザーが存在することを確認します。 アカウントに登録している場合、提供されたメールアドレスでアカウントがまだ存在しないことを確認します。
- 上記の手順が成功した場合、新しいクッキーセッションが作成され、ユーザーはホームページにリダイレクトされます。
- ステップが成功せず、問題が発生した場合、ユーザーはログイン画面または登録画面に戻され、エラーが表示されます。
これを開始するには、app
ディレクトリ内に utils
という名前のフォルダを作成します。 ここには、ヘルパー、サービス、構成ファイルを保存します。
その新しいフォルダ内に、auth.server.ts
という名前のファイルを作成します。ここに、認証とセッション関連のメソッドを記述します。
注: Remixは、ファイルタイプ前の
.server
を持つファイルを、ブラウザに送信されるコードでバンドルしません。
register 関数の構築
最初に構築する関数は register 関数です。これにより、ユーザーは新しいアカウントを作成できます。
app/utils/auth.server.ts
から register
という名前の非同期関数をエクスポートします。
登録フォームが提供するフィールドを定義する type
を作成し、app/utils
内の別の新しいファイルに types.server.ts
という名前で保存してエクスポートします。
その 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
関数に次を追加します。
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 関数の構築
login
関数は、email
と password
を受け取ります。この関数を開始するために、app/utils/types.server.ts
にそのデータを記述する新しい LoginForm
タイプを作成します。
次に、login
関数を作成します。app/utils/auth.server.ts
に次を追加します。
上記のコード...
- ... 新しい
type
とbcryptjs
ライブラリをインポートします。 - ... 一致するメールアドレスを持つユーザーをクエリします。
- ... ユーザーが見つからない場合、または提供されたパスワードがデータベース内のハッシュ化された値と一致しない場合は、
null
値を返します。 - ... すべてがうまくいった場合は、ユーザーの
id
とemail
を返します。
これにより、正しい資格情報が提供されたことが保証され、新しいクッキーセッションを作成するために必要なデータが返されます。
セッション管理の追加
サインインまたはアカウント登録時にユーザーのクッキーセッションを生成する方法が必要になりました。 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.tsx
のaction
関数内で、リクエストからフォームデータを取得し、正しい形式であることを検証します。
上記のコードは少し怖いかもしれませんが、要するに...
- ... リクエストオブジェクトからフォームデータを取得します。
- ...
email
とpassword
が提供されていることを確認します。 - ...
_action
の値が"register"
の場合、firstName
とlastName
が提供されていることを確認します。 - ... 問題が発生した場合、フォームフィールドの値とともにエラーを返します。これにより、フィールドのいずれかが無効な場合に、ユーザーの入力とエラーメッセージでフォームを再度入力できます。
最後に行う必要があるのは、入力が適切に見える場合に、実際に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
フックを使用して、アクションから返されたデータにアクセスする必要があります。
このコードは以下を追加します
action
関数から返されたデータにフックします。- 「無効なメールアドレス」などのフィールド固有のエラーをオブジェクトに保持する
errors
変数を設定します。また、「ログインに失敗しました」などのフォームメッセージを表示するためのエラーメッセージを保持するformError
変数も設定します。 - 利用可能な場合は、
formData
状態変数を、action
関数から返された任意の値にデフォルト設定するように更新します。
ユーザーにエラーが表示され、フォームを切り替えた場合、フォームと表示されているエラーをクリアする必要があります。これを実現するために、これらのeffects
を使用します。
これらを配置することで、フォームとフィールドに表示するエラーを最終的に知らせることができます。
これで、サインアップフォームとサインインフォームでエラーメッセージとフォームのリセットが正常に機能するはずです!
まとめと今後のステップ
Kudos _(😉)_ このセクションの最後までお付き合いいただき、**お疲れ様でした** _(😉)_! カバーすべきことがたくさんありましたが、うまくいけば、次のことを理解できたはずです。
- Remixでルートを設定する方法。
- 検証付きのサインインフォームとサインアップフォームを作成する方法。
- セッションベースの認証の仕組み。
- 認証を実装してプライベートルートを保護する方法。
- ユーザーの作成と認証時にPrismaを使用してデータを保存およびクエリする方法。
このシリーズの次のセクションでは、Kudosのホームページとkudos共有機能を構築します。また、kudosフィードに検索およびフィルタリング機能を追加します。
次の記事をお見逃しなく!
Prismaニュースレターに登録する