laravel9で仮登録・本登録を挟むユーザログインの作り方

Userテーブルにverifiedフラグを追加

今回作成するログインでは、デフォルトで作成されるuserテーブルには仮登録に必要なカラムが入っていないため、それを追加する。
また、仮登録から本登録へ移行する際、登録時のメールアドレス宛にURLを送付し、クリックすることで本登録が完了とするものとする。
そのため本登録対象を把握するため、トークン用のカラムを追加する。
ただし、URLの有効期限は1時間としたい。そのためのカラムはデフォルトではdatetime型の「email_verified_at」カラムが予め設定されているため、これを使用するものとする。

・ユーザテーブルの追加

artisanにて「modify_users_table」編集ファイルの雛形を追加。

php artisan make:migration modify_users_table --table=users

追加した「database/migrations/xxxx_modify_users_table.php」にて、
「updated_at」カラムの後に「email_token」カラムを追加、
「email_token」カラムの後に「verified」カラムを追加する。

    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('email_token')->after('updated_at')->nullable();
            $table->tinyInteger('verified')->after('email_token')->default(0);
        });
    }

編集後、artisanにてテーブルに反映させる。

php artisan migrate

Modelの編集

追加した「email_token」「verified」と既存の「email_verified_at」カラムにユーザが登録変更を行えるようにするため、Modelの「app/Models/User.php」に追加する。

    protected $fillable = [
        'name',
        'email',
        'password',
        'email_token',
        'verified',
        'email_verified_at',
    ];

メール認証用クラスを作成

artisaにてメール認証クラスを作成する。

php artisan make:mail EmailVerification

ここより「app/Mail/EmailVerification.php」ファイルを編集し、メール認証を作成する。

メール送信時の変数を受け取るため、必要な変数を追加する。

    public $user;
    public $text = 'email.verification';
    public $htmlString;
    public $with = ['text' => ''];
    public $subject;

constructにてuser情報と件名(subject)を設定する。subjectは日本語ファイル(ja.json)を活用し、賢明はここで設定することとする。

    public function __construct($user)
    {
        $this->user = $user;

        $path = resource_path() . '/lang/ja.json';
        if(file_exists($path)){
            $json = file_get_contents($path);
            $json = json_decode($json);
            $this->subject = $json->VerificationSubject;
        }
    }

取得した件名をセットする。

    public function envelope()
    {
        return new Envelope(
            subject: $this->subject
        );
    }

メール本文はviewから取得するため、contentメソッドへview(email.verification)の設定をすると共に、認証用のURLは登録者ごとのトークンが発行されるため、本文内のURLに対しトークン(email_token)をセットする。

    public function content()
    {
        return new Content(
            view: 'email.verification',
            with: [
                'email_token' => $this->user->email_token,
            ]
            );
    }

本文用のviewを作成するため、「resources/views/」へ「email」フォルダを作成し、その中に「resources/views/email/email.blade.php」ファイルを作成する。

メール本文は認証用のURLを含めた内容とするが、ここで日本語セットを活用したいため英語本文とした。
トークン用のURLは「/auth/verifyemail/」とし、「email_token」

<h1>{{ __('Notification of email address verification.') }}</h1>

<p>{{ __('Please click on the following link to verify your email address.') }}<br>
{{url('/auth/verifyemail/'.$email_token)}} </p>

<p>{{ __('If you have no idea what it is, please ignore it.') }}</p>

メール送信用のキュージョブを作成

artisaでqueue job作成する。

php artisan make:job SendVerificationEmail

「app/Jobs/SendVerificationEmail.php」ファイルを編集する。

メール送信用クラスと、先ほど作成したEmailVerificationクラスを追加する。

use Illuminate\Support\Facades\Mail;
use App\Mail\EmailVerification;

ユーザ情報用の変数を追加する。

protected $user;

constructメソッドにユーザ情報をセットする。

    public function __construct($user)
    {
        $this->user = $user;
    }

ジョブに送信処理を追加する。

    public function handle()
    {
        $email = new EmailVerification($this->user);
        Mail::to($this->user->email)->send($email);
    }

会員登録用のコントローラを編集

「app/Http/Controllers/Auth/RegisterController.php」ファイルへクラスを追加する。

use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use App\Jobs\SendVerificationEmail;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;

RegisterControllerクラスに「RegistersUsers」クラスを追加する。

class RegisterController extends Controller
{
    use RegistersUsers;

createメソッドへトークン用の「email_token」と登録時の日時保存する「email_verified_at」を追加する。トークンはemailをbase64でエンコードしたものを設定するが、複雑化するために余分にランダム英数字を追加したほうが無難。

日時はCarbonクラスにて追加する。

    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
            'email_token' => base64_encode($data['email']),
            'email_verified_at' => Carbon::now(),
        ]);
    }

registerメソッドに先ほど作成した「SendVerificationEmail」クラスのジョブを追加する。

    public function register(Request $request)
    {
        $this->validator($request->all())->validate();
        event(new Registered($user = $this->create($request->all())));
        dispatch(new SendVerificationEmail($user));
        return view('auth.verification');
    }

verifyメソッドへ本登録処理を追加する。

「verified」カラムが0であれば仮登録ユーザとし、「verified」カラムを1にアップデートし、本登録処理を行う。ただし、URLの有効期限を1時間とし、1時間以上の場合はデータを削除する。

有効期限切れの場合のviewは「auth.warning」とし、本登録が完了した場合のviewは「auth.emailconfirm」とする。

登録データがない場合、トップページに遷移する。

public function verify($token)
    {
        $user = User::where('email_token', $token)->where('verified', 0)->first();
        
        // 登録があれば処理
        if(isset($user->id) && $user->id <> NULL){
            // 現在の時刻を取得
            $date_now = new Carbon(Carbon::now());

            // メールの送信時刻を取得し,1時間加えた時刻を有効期限とする
            $date_expire = Carbon::createFromFormat('Y-m-d H:i:s', $user->email_verified_at);
            $date_expire->addHour(1);

            // リンクの有効期限のチェック
            if ($date_now->gt($date_expire)) {
                DB::table('users')->where('email_token', $token)->delete();
                return view('auth.warning');
            }

            // 仮登録を本登録にする
            else if(DB::table('users')->where('email_token', $token)->update(['verified' => 1])){
                return view('auth.emailconfirm', ['user' => $user]);
            }
        }
        // 登録がない場合はトップページに遷移
        else{
            return redirect(url('/'));
        }
    }

ログイン用のコントローラを編集

ログイン時、仮登録であればログインを拒否したいため、「attemptLogin」メソッドを追加し、「verified」カラムが1の場合のみ認証するようにする。

    function attemptLogin(\Illuminate\Http\Request $request)
    {
        $email = $request->input('email');
        $password = $request->input('password');
        $credentials = ['email' => $email, 'password' => $password, 'verified' => 1];

        return $this->guard()->attempt($credentials, $request->filled('remember'));
    }

loginメソッドにて、先ほど作成した「attemptLogin」メソッドで認証した場合、ログインページへ遷移し、それ以外はログイン画面に遷移するようにする。

    public function login(Request $request)
    {
        $this->validateLogin($request);

        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);
        }else{
            redirect()->route('login');
        }
    }

Viewの作成

会員仮登録後のページを作成するため、「resources/views/auth/verification.blade.php」ファイルを作成し、英語表記にてページを作成する。

@extends('layouts.app')
 
@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
              <h1>{{ __('User Registration') }}</h1>
              <p>{{ __('User Registration.') }}</p>
              <p>{{ __('An email has been sent to you for confirmation, please click on the link in the email.') }}</a></p>
            </div>
        </div>
    </div>
</div>
@endsection

メール送信後、リンクをクリックした際の認証用ページを作成するため、「resources/views/auth/emailconfirm.blade.php」ファイルを作成し、英語表記にてページを作成する。

@extends('layouts.app')
 
@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
              <h1>{{ __('Confirmation complete') }}</h1>
              <p>{{ __('Your email address has been verified.') }}</p>
              <p><a href="{{url('/login')}}">{{ __('Please log in here.') }}</a></p>
            </div>
        </div>
    </div>
</div>
@endsection

ルートを定義

認証用リンククリック時のルートを定義するため、「routes/web.php」の最後に定義追加する。

Route::get('/auth/verifyemail/{token}', [RegisterController::class, 'verify']);

日本語ファイルを追加する

「resources/lang/ja.json」ファイルへ日本語ファイルを追加する。これまで英語表記にしていたため、それらを日本語に変更する。

{
    "User Registration": "ユーザ登録",
    "User Registration.": "ユーザ登録を行いました。",
    "An email has been sent to you for confirmation, please click on the link in the email.": "確認のためにEメールを送信しましたので、メールのリンクをクリックしてください。",
    "Confirmation complete": "確認完了",
    "Your email address has been verified.": "あなたのメールアドレスが確認できました。",
    "Please log in here.": "ここからログインしてください。",
    "VerificationSubject": "メール認証",
    "Verify error": "有効期限エラー",
    "Email link expiration date (1 hour) has passed.": "メールリンクの有効期限(1時間)を過ぎました。",
    "Please register again.": "再度会員登録を行なってください。",
    "Click here for new registration.": "新規登録はこちら。",
    "Notification of email address verification.": "メールアドレスの認証のお知らせ。",
    "Please click on the following link to verify your email address.": "以下のリンクをクリックして、メールアドレスを認証をしてください。",
    "If you have no idea what it is, please ignore it.": "心当たりが無い場合は、無視してください。"
}