Laravel Breeze + React 認証機能をカスタマイズする

Laravel BreezeはLaravel公式が管理している認証機能スターターキットの一つです。
このLaravel Breezeと React + TypeScript を使って認証機能を作成する機会があったため、この記事を書いています。Laravel Breezeはインストールするとコードが自動で生成され、DBのテーブルまで用意してくれて、とてもお手軽に認証機能を作れてしまう優れものです。

現時点でLaravel BreezeはReact + TypeScriptのスタックに標準で対応しており、コマンドにオプションを加えるだけで叩き台は完成します。この状態からシステム要件に合わせてカスタマイズした手順を書きたいと思います。設定自体は難しくないのですが、挙動を理解するためにBreeze のコードを少し読む必要がありましたので、その辺りも残したいと思います。

目次

カスタマイズ前後の動作

はじめに、Laravel BreezeのReact + TypeScriptテンプレートの標準状態からどのようにスタマイズしたいのか、について記載します。大きくは以下の点を変更していきます。

変更前
  • /register でアカウント登録フォームの送信でユーザー登録が完了し、ログイン状態で/dashboard に遷移する
  • /を参照するとウェルカムページが開く
変更後
  • register でユーザー登録が完了したら、確認メールを送信し、/verify-email に遷移させる
  • 確認メールから認証リンクを踏むとメールアドレス認証が完了し、 /register-complete 登録完了ページに遷移させる。このときログアウト状態にしておく。
  • / で未認証であればログイン画面、認証済みであればダッシュボードを開く

要するにメールアドレス認証が完了するまで、ログインさせないという、という動きを実装したいと思います。
webサービスでよく見かける動作なのですが、Breeze(とInertia.js)が比較的に新しいパッケージで情報が少ないように感じます。

前提のプロジェクト構成

以下の手順でLaravelプロジェクトが用意されていることを前提としています。

1. Laravelプロジェクトの作成

ご自身の環境に合わせてLaravelプロジェクトを作成してください。

2. Laravel Breezeで認証機能をセットアップ

以下のコマンドでBreezeをインストールします。

php artisan breeze:install react --typescript

php artisan migrate
npm install
npm run dev

後述の認証機能カスタマイズは別の技術スタックでもはできます。その場合はこちらを参照してコマンドを調整してください。ちなみにTypeScriptが対応されたのは1年ほど前に対応されたようです。

補足:重要パッケージのバージョン
パッケージ名バージョン
php8.2
laravel/framework10
laravel/breeze1.28
composerのパッケージ

カスタマイズ

ここから、本題である実装方法について順に説明していきます。

メールアドレス認証の追加

register でユーザー登録が完了したら、認証メールを送信し、/verify-email に遷移させる、について実装していきます。これは以下のページにそのままの説明がありますので、一部抜粋だけして次に進みます。

このインターフェースがモデルに追加されると、新規登録ユーザーには、電子メール検証リンクを含む電子メールが自動的に送信されます。

Email Verification – Model Preparation

「このインターフェース」というのは Illuminate\Contracts\Auth\MustVerifyEmail のことです。ユーザーモデルが MustVerifyEmail の実装を検知すると、メールアドレス検証のためのリンクが含まれるメールが送信されることになります。

RouteServiceProvider に確認メールの送信完了ページとメール認証後の登録完了ページのパスを定義しておきます。リダイレクト先を指定するときなどに利用しますので、実装前にこれらパスに遷移させると言うことが分かっていると理解がスムーズに進むかと思います。

diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php
index 52dabc1..7de5095 100644
--- a/app/Providers/RouteServiceProvider.php
+++ b/app/Providers/RouteServiceProvider.php
@@ -17,7 +17,21 @@ class RouteServiceProvider extends ServiceProvider
+    /**
+     * 登録完了ページ
+     *
+     * @var string
+     */
+    public const REGISTER_COMPLETE = '/register-complete';
+
+    /**
+     * 確認メールの送信完了ページ
+     *
+     * @var string
+     */
+    public const VERIFY_EMAIL = '/verify-email';

ユーザー登録後に確認メールの送信完了ページに遷移

ユーザー登録後に確認メールを送信する実装を行っていきます。以下のようにコードを変更します。

ポイント

  • ユーザー登録イベントが発火時にMustVerifyEmail が実装されているので、確認メールが送信されます。
  • ログイン処理を行い、verification.verify ルート(メールアドレスの認証リンク先)に遷移できるようにします。
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Inertia\Inertia;
use Inertia\Response;

class RegisteredUserController extends Controller
{
    /**
     * Handle an incoming registration request.
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function store(Request $request): Response
    {
        /*** 省略 ***/

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

     // ユーザー登録イベントを発火
        event(new Registered($user));

        // ユーザーログインを実施
        Auth::login($user);

        // 確認メールの送信完了ページに遷移
        return redirect(RouteServiceProvider::VERIFY_EMAIL);
    }
}

以下のようにユーザー登録後に、送信完了ページにリダイレクトするように変更します。送信完了ページのルートがまだ実装されていないため、これだけでは表示されません。

@@ -31,3 +29,3 @@ public function create(): Response
      */
-    public function store(Request $request): RedirectResponse
+    public function store(Request $request): Response
     {
@@ -49,3 +47,3 @@ public function store(Request $request): RedirectResponse

-        return redirect(RouteServiceProvider::HOME);
+        return redirect(RouteServiceProvider::VERIFY_EMAIL);
     }

確認メールの送信完了ページの表示

以下のように送信完了ページを表示させます。

--- a/app/Http/Controllers/Auth/EmailVerificationPromptController.php
+++ b/app/Http/Controllers/Auth/EmailVerificationPromptController.php
@@ -18,5 +18,7 @@ public function __invoke(Request $request): RedirectResponse|Response
     {
-        return $request->user()->hasVerifiedEmail()
-                    ? redirect()->intended(RouteServiceProvider::HOME)
-                    : Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
+        $user = $request->user();
+        return $user->hasVerifiedEmail()
+            ? redirect()->intended(RouteServiceProvider::HOME)
+            : Inertia::render('Auth/VerifyEmail', ['email' => $user->email, 'status' => session('status')]);
+
     }

ここで、アカウント登録をしてみます。以下のような画面が表示されましたでしょうか?
説明していなかったのですが、私は日本語に変えてメールアドレスを表示させています。

送信完了ページ

認証リンクの遷移先を変更する

確認メールに含まれる認証リンクの遷移先を変更します。
以下のようにコードを変更します。

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;

class VerifyEmailController extends Controller
{
    /**
     * Mark the authenticated user's email address as verified.
     */
    public function __invoke(EmailVerificationRequest $request): RedirectResponse
    {
        // 認証済みのメールアドレスであれば、セッションに含まれるパスに遷移する
        if ($request->user()->hasVerifiedEmail()) {
            return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
        }

        // メールアドレスを認証済みにする
        if ($request->user()->markEmailAsVerified()) {
            event(new Verified($request->user()));
        }

     // 登録完了ページにリダイレクト
        return redirect(RouteServiceProvider::REGISTER_COMPLETE);
    }
}

以下のように登録完了ページにリダイレクトするように変更しています。

@@ -25,3 +26,3 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse

-        return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
+        return redirect(RouteServiceProvider::REGISTER_COMPLETE);
     }

認証完了ページを作成

以下のようにメソッドを追加して、認証確認ページを開けるようにします。

ポイントとしてはログイン状態であればログアウトさせているところです。セッションが維持されていればログイン状態でアクセスが来ます。アカウント作成の流れとしては、改めてログインしてもらいたいので、認証完了ページを開くときには必ずログアウトさせます。

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Inertia\Response;

class RegisteredUserController extends Controller
{
    /*** 省略 ***/

    public function complete(): Response
    {
        if (Auth::check()) {
            Auth::logout();
        }
        return Inertia::render('Auth/RegisterComplete');
    }
}

アクセスができるようにルーティングを追加します。

diff --git a/routes/auth.php b/routes/auth.php
index 1040b51..7a6da99 100644
--- a/routes/auth.php
+++ b/routes/auth.php
@@ -45,2 +45,5 @@

+    Route::get('register-complete', [RegisteredUserController::class, 'complete'])
+        ->name('register.complete');
+

最後に、登録完了ページを作成します。

import GuestLayout from "@/Layouts/GuestLayout";
import { Head, useForm } from "@inertiajs/react";
import PrimaryButton from "@/Components/PrimaryButton";

export default function RegisterComplete() {
    const { get } = useForm({});

    const submit = (e: any) => {
        e.preventDefault();
        get(route("login"));
    };

    return (
        <GuestLayout>
            <Head title="Email Verification" />

            <div className="text-sm text-gray-600">
                入力頂いた情報で登録いたしました。
            </div>
            <div className="mb-4 text-sm text-gray-600">
                ログイン画面よりログイン処理を行なってください。
            </div>

            <form onSubmit={submit}>
                <PrimaryButton>ログイン画面へ</PrimaryButton>
            </form>
        </GuestLayout>
    );
}

うまく行けば、以下のように表示されるはずです。

登録完了ページ

動作確認

最後に、カウント登録から順に操作をしていき、ダッシュボードページの表示まで一連の操作をしてみましょう。途中の登録完了ページができており、登録したユーザーでログインができればカスタマイズは成功です!

  1. アカウント登録ページからフォームを送信
  2. 確認メールの送信完了ページが表示される
  3. 確認メールから認証リンクを踏む
  4. 登録完了ページが表示される
  5. ログインページに遷移して、ログインフォームを送信する
  6. ダッシュボードページが表示される

終わりに

お疲れ様でした。区切りが見つからず、思った以上に長い説明になりました。ただ最後まで進めることでメールアドレス認証つきのログイン機能が実装できるようになったかと思います。

現時点でLaravel Breezeのバージョン更新が早く、これから大きな変更が入る可能性があります。そういったことを念頭におきながら、作業を進められると詰まった時にも解決が早いように思います。もし動かなかったり、不明な点がありましたら、答えますのでよろしくお願いします。

以上、ここまでお読みいただき、ありがとうございました。

目次