【Laravel】パスワードリセット機能の実装（有効期限・署名付きURLをメール送信）

# はじめに
今回は、Laravelでパスワードリセット機能の実装についてまとめました。

※おことわり※
基本的に学習内容のアウトプットです。
初学者であるため、間違い等あればご指摘いただけますと嬉しいです。

# この記事の目的
以下内容のアウトプット
- カラムの追加　・・・マイグレーション機能
- メール送信機能の実装　・・・Mailableクラス
- パスワードリセット機能を実装　・・・リポジトリパターン
- パスワードリセット機能を実装　・・・ファサード
- トークンの有効期限の判定　・・・オリジナルバリデーション

# この記事の内容
1. 前提
2. 実装内容と大まかな手順
3. 既存テーブルにカラムの追加
4. ルーティング・コントローラの設定
5. ビューの作成
6. プロバイダの作成・登録
7. バリデーションの実装_part1
8. リポジトリパターンの実装_part1
9. Mailableクラスの実装
10. コントローラにメール送信処理を追加
11. バリデーションの実装_part2
12. リポジトリパターンの実装_part2
13. コントローラにパスワード更新処理を追加
14. おまけ：ファサードで実装 
15. 参考 


# 1. 前提
**テーブルについて**
- userテーブルが存在している
- userテーブルには下記カラムが存在している
    - user_id（ユーザーID）
    - mail（メールアドレス）
    - password（パスワード）

**機能について**
- ログイン機能実装済み（UsersController）

# 2. 実装内容と大まかな手順
以下にて、パスワードリセット機能の内容と、実装の大まかな手順について説明します。

### 実装内容
- パスワード再設定を希望するユーザーに、パスワード再設定フォームのURLをメールにて送付します。
- URLには、有効期限と署名が付いていて、不正なアクセスを制御します。
- 有効期限内かつ署名検証をパスした場合、ユーザーはパスワードを更新することができます。

### 大まかな手順
以下の順番で実装します。
実装完了を100%としたら、①20% ②50% ③30% といったボリューム感です。

① パスワードリセット機能実装のための準備
② メール送信の実装
③ パスワード更新の実装

### 目次

**＊①パスワードリセット機能実装のための準備**
[3. 既存テーブルにカラムの追加](https://qiita.com/rikako_hira/items/4626090769f4023d4107#3-%E6%97%A2%E5%AD%98%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AB%E3%82%AB%E3%83%A9%E3%83%A0%E3%81%AE%E8%BF%BD%E5%8A%A0)
[4. ルーティング・コントローラの設定](https://qiita.com/rikako_hira/items/4626090769f4023d4107#4-%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%81%AE%E8%A8%AD%E5%AE%9A)
[5. ビューの作成](https://qiita.com/rikako_hira/items/4626090769f4023d4107#5-%E3%83%93%E3%83%A5%E3%83%BC%E3%81%AE%E4%BD%9C%E6%88%90)
[6. プロバイダの作成・登録](https://qiita.com/rikako_hira/items/4626090769f4023d4107#6-%E3%83%97%E3%83%AD%E3%83%90%E3%82%A4%E3%83%80%E3%81%AE%E4%BD%9C%E6%88%90%E7%99%BB%E9%8C%B2)

**＊②メール送信処理**
[7. バリデーションの実装_part1](https://qiita.com/rikako_hira/items/4626090769f4023d4107#7-%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E8%A3%85_part1)
[8. リポジトリパターンの実装_part1](https://qiita.com/rikako_hira/items/4626090769f4023d4107#8-%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%AE%E5%AE%9F%E8%A3%85_part1)
[9. Mailableクラスの実装](https://qiita.com/rikako_hira/items/4626090769f4023d4107#9-mailable%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E5%AE%9F%E8%A3%85)
[10. コントローラにメール送信処理を追加](https://qiita.com/rikako_hira/items/4626090769f4023d4107#10-%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%81%AB%E3%83%A1%E3%83%BC%E3%83%AB%E9%80%81%E4%BF%A1%E5%87%A6%E7%90%86%E3%82%92%E8%BF%BD%E5%8A%A0)

**＊③パスワード更新処理**
[11. バリデーションの実装_part2](https://qiita.com/rikako_hira/items/4626090769f4023d4107#11-%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%AE%9F%E8%A3%85_part2)
[12. リポジトリパターンの実装_part2](https://qiita.com/rikako_hira/items/4626090769f4023d4107#12-%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%AE%E5%AE%9F%E8%A3%85_part2)
[13. コントローラにパスワード更新処理を追加](https://qiita.com/rikako_hira/items/4626090769f4023d4107#13-%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%81%AB%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E6%9B%B4%E6%96%B0%E5%87%A6%E7%90%86%E3%82%92%E8%BF%BD%E5%8A%A0)
<br/>

:::note
ここから実装に入っていきます。
はじめに、「①パスワードリセット機能実装のための準備」を行います。
:::

## 3. 既存テーブルにカラムの追加
userテーブルに、2つのカラムを追加します。
追加の手順については、[【Laravel】既存テーブルに好きな数のカラムを追加する](https://qiita.com/rikako_hira/items/6a0a2b972ddbf638fefd) を参照ください。


### 追加するカラムの詳細

**Tips：**
- `reset_password_access_key`
  - `nullable` ユーザー新規登録の際、パスワード再設定キーはnullになるためnullを許容にしています。
  - `unique` トークンが重複しないよう、ユニーク制約を設定しています。
  パスワード再設定キーは、トークンのことです。

| カラム名 | データ型 | 制約 | コメント
| ------ | ------- | --- | ----
| reset_password_access_key | VARCHAR(64) | nullable, unique| パスワード再設定キー
| reset_password_expire_at | TIMESTAMP | nullable | パスワード再設定キーの有効期限

### 参照

https://qiita.com/rikako_hira/items/6a0a2b972ddbf638fefd

# 4. ルーティング・コントローラの設定
ルーティングとコントローラの設定を行います。

### ルーティングの設定
パスワードリセット関連のルーティングを設定ます。

```PHP:web.php

// 省略

Route::prefix('reset')->group(function () {
    // パスワード再設定用のメール送信フォーム
    Route::get('/', 'UsersController@requestResetPassword')->name('reset.form');
    // メール送信処理
    Route::post('/send', 'UsersController@sendResetPasswordMail')->name('reset.send');
    // メール送信完了
    Route::get('/send/complete', 'UsersController@sendCompleteResetPasswordMail')->name('reset.send.complete');
    // パスワード再設定
    Route::get('/password/edit', 'UsersController@resetPassword')->name('reset.password.edit');
    // パスワード更新
    Route::post('/password/update', 'UsersController@updatePassword')->name('reset.password.update');
});

```

### コントローラの設定
コントローラに各ファンクションの定義をします。

>新規でコントローラを作成する場合は、Tipsを参照ください。
>ビューは次章で作成します。
>引数のバリデーション（ResetInputMailRequest, ResetPasswordRequest）は後ほど実装します。

**Tips：**
- コントローラを新規で作成する場合
`php artisan make:controller ディレクトリ/コントローラ名` を実行し、コントローラを作成します。

```php:UsersController
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class UsersController extends Controller
{
	/**
     * パスワードリセット
     */

    // パスワード再設定用のメール送信フォーム
    public function requestResetPassword()
    {
        return view('users.reset_input_mail');
    }

    //  メール送信 
    public function sendResetPasswordMail(ResetInputMailRequest $request)
    {
        return redirect()->route('reset.send.complete');
    }

    // メール送信完了
    public function sendCompleteResetPasswordMail()
    {
        return view('users.reset_input_mail_complete');
    }

    // パスワード再設定
    public function resetPassword(Request $request)
    {
        return view('users.reset_input_password');
    }

    // パスワード更新
    public function updatePassword(ResetPasswordRequest $request)
    {
        return view('users.reset_input_password_complete');
    }
}

```

# 5. ビューの作成
以下の通りビューを作成します。

>いずれのビューファイルも `resources/views/users` 配下に作成しています。
>最低限のコードになりますので、アレンジして使ってください。

### ログインページ（既存）
パスワード再設定用のメール送信フォームへのリンクを設置します。

```html
<a href="{{ route('reset.form') }}">パスワードをお忘れの方</a>
```

### メール送信フォーム

```html:reset_input_mail.blade.php
@extends('xxxx')
@section('xxxx')
<main>
    <h2>パスワード再設定</h2>
    <p>ご利用中のメールアドレスを入力してください</p>
    <p>パスワード再設定のためのURLをお送りします</p>
    <form method="POST" action="{{ route('reset.send') }}">
        @csrf
        <div>
            <label>メールアドレス</label>
            <input type="text" name="mail" value="{{ old('mail') }}">
            <span>{{ $errors->first('mail') }}</span>
        </div>
        <div>
            <a href="{{ route('xxxx') }}">戻る</a>
            <button type="submit">再設定メールを送信</button>
        </div>
    </form>
</main>
@endsection

```

### メール送信完了ページ

```html:reset_input_mail_complete.blade.php
@extends('xxxx')
@section('xxxx')
<main>
    <h2>メール送信完了</h2>
    <div>
        <p>パスワード再設定用のメールを送信しました</p>
        <p>メールに記載されているリンクからパスワードの再設定を行ってください</p>
    </div>
    <div>
      <a href="{{ route('xxxx') }}">ログイン画面へ</a>
    </div>
</main>
@endsection

```

### パスワード再設定フォーム

```html:reset_input_password.blade.php
@extends('xxxx')
@section('xxxx')
<main>
    <h2>パスワード再設定</h2>
    <form method="POST" action="{{ route('reset.password.update') }}">
        @csrf
        <input type="hidden" name="reset_token" value="{{ $userToken->rest_password_access_key }}">
        <div>
            <label>新パスワード</label>
            <input type="password" name="password" value="">
            <span>{{ $errors->first('password') }}</span>
            <span>{{ $errors->first('reset_token') }}</span>
        </div>
        <div>
            <label>新パスワード<span>確認</span></label>
            <input type="password" name="password_confirmation" value="">
        </div>
        <div>
            <button type="submit">パスワードを再設定する</button>
        </div>
    </form>
</main>
@endsection
```

### パスワード再設定完了ページ

```html:reset_input_password_complete.blade.php
@extends('xxxx')
@section('xxxx')
<main>
    <h2>パスワード変更完了</h2>
    <div>
        <p>パスワードの変更が完了しました</p>
        <p>新しいパスワードにて再ログインしてください</p>
    </div>
    <div>
      <a href="{{ route('xxxx') }}">ログイン画面へ</a>
    </div>
</main>
@endsection

```

# 6. プロバイダの作成・登録
インタフェースとリポジトリを紐づけるためのプロバイダを作成し、設定用ファイルに追加します。

### プロバイダの作成
以下コマンドを実行し、プロバイダファイルを作成します。

```terminal:terminal
php artisan make:provider RepositoryServiceProvider
```

### プロバイダの設定
上記で作成したファイルに、以下コードを記述します。

>UserRepositoryInterface, UserRepositoryは後ほど実装します。

**Tips：**
- `$models 配列`　
今後インタフェースとリポジトリが増えた場合は、配列に追記するだけで bindが行えるように配列を使用しています。

```PHP:RepositoryServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Binding of models
     *
     * @var array
     */
    private $models = [
        'User'
    ];

    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        foreach ($this->models as $model) {
            $this->app->bind(
                "App\Repositories\Interfaces\\{$model}RepositoryInterface",
                "App\Repositories\Eloquents\\{$model}Repository"
            );
        }
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}
```

### プロバイダの登録
上記で作成したプロバイダを、`config/app.php` の `providers配列` に追加します。

**Tips：**
- サービスプロバイダクラスとして、提供するサービスが必要な場合にのみ読み込まれます。


```php:config/app.php
'providers' => [
        // 中略
        /* 下記１行追加 */
        App\Providers\RepositoryServiceProvider::class,

    ],
```
<br/>

:::note
「①パスワードリセット機能実装のための準備」は以上になります。
続いて「②メール送信の実装」を行います。
:::

# 7. バリデーションの実装_part1
「パスワード再設定用のメールアドレス入力フォーム用」のバリデーションを実装します。

### リクエストファイルの作成
以下コマンドを実行し、リクエストファイルを作成します。

```terminal:terminal
php artisan make:request ResetInputMailRequest
```

### バリデーションの設定
上記で作成したファイルに、バリデーションを定義します。

**Tips：**
- `email:rfc,dns,filter`
  - `rfc` RFC準拠になっているか判定します。
  - `dnc` メールアドレスのドメインが有効か判定します。
  - `filter` 平仮名, カタカナ, 漢字の入力を防ぎ、@の後にドットが最低1つ存在するか判定します。

- `exists:user,mail`
フォームに入力されたメールアドレスが、userテーブルに登録されているか判定します。

```php:ResetInputMailRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ResetInputMailRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules()
    {
        return [
            'mail' => ['required', 'email:rfc,dns,filter',  'exists:user,mail']
        ];
    }

    /**
     * エラーメッセージ
     * @return array
     */
    public function messages()
    {
    return [
        'mail.required' =>  "メールアドレスを入力してください",,
        'mail.email' => "メールアドレスの形式ではありません",
        'mail.exists' => "登録しているメールアドレスを入力してください"
        ];
    }
}
```

# 8. リポジトリパターンの実装_part1
インタフェースと、リポジトリの作成を行います。

>インタフェースとリポジトリは、プロバイダによってbindされます。
>ここで定義する処理は、次章で実装するコントローラで呼び出して使います。

### インタフェースの作成
以下の通りインタフェースファイルを作成します。
- `App\Repositories` 配下に `Interfaces` ディレクトリを作成
- 上記で作成したディレクトリ配下に `UserRepositoryInterface.php` を作成

作成したファイルに、以下コードを記述します。


```PHP:UserRepositoryInterface.php
<?php

namespace App\Repositories\Interfaces;

use App\Models\User;

interface UserRepositoryInterface
{
    /**
     * メールアドレスからユーザー情報を取得
     *
     * @param string $mail
     * @return User
     */
    public function findFromMail(string $mail): User;

    /**
     * パスワードリセット用トークンを発行
     *
     * @param int $userId
     * @return User
     */
    public function updateOrCreateUser(int $userId): User;
}
```

### リポジトリの作成
以下の通りリポジトリファイルを作成します。
- `App\Repositories` 配下に `Eloquents` ディレクトリを作成
- 上記で作成したディレクトリ配下に `UserRepository.php` を作成

作成したファイルに、以下コードを記述します。

**Tips：**
- `updateOrCreateUser(int $userId)`
引数の $userIdが userテーブルに存在しない場合、以下2つのカラを新規で作成し、DBに登録します。
存在する場合は、以下2つのカラムを更新します。
  - rest_password_access_key
  - rest_password_expire_data

- `$hashedToken = hash('sha256', $userId);`
$userIdをハッシュ化し、変数に代入します。

- `'rest_password_access_key' => uniqid(rand(), $hashedToken)`
  - `uniqid()` ユニークIDを生成する関数
  - `rand()` ランダムな数値を生成する関数
ユニークIDにハッシュ化した値を含ませることで、より一意性を確保することができます。

- `'rest_password_expire_data' => $now->addHours(24)->toDateTimeString()`
トークンの有効期限を、現在から24時間後に設定します。 
つまり、現在から２４時間後の時間がカラムに登録されます。

```PHP:UserRepository.php
<?php

namespace App\Repositories\Eloquents;

use App\Models\User;
use App\Repositories\Interfaces\UserRepositoryInterface;
use Carbon\Carbon;
use Illuminate\Support\Facades\Hash;


class UserRepository implements UserRepositoryInterface
{
    private $user;
    private $userToken;

    /**
     * constructor
     *
     * @param User $user
     */
    public function __construct(User $user, User $userToken)
    {
        $this->user = $user;
        $this->userToken = $userToken;
    }

    // メールアドレスからユーザー情報取得
    public function findFromMail(string $mail): User
    {
        return $this->user->where('mail', $mail)->firstOrFail();
    }

    // パスワードリセット用トークンを発行
    public function updateOrCreateUser(int $userId): User
    {
        $now = Carbon::now();
        // $userIdをハッシュ化
        $hashedToken = hash('sha256', $userId);
        return $this->userToken->updateOrCreate(
            [
                'id' => $userId,
            ],
            [
            // $hashedTokenを含むトークンを作成
            'rest_password_access_key' => uniqid(rand(), $hashedToken),
            // トークンの有効期限を現在から24時間後に設定
            'rest_password_expire_data' => $now->addHours(24)->toDateTimeString()
            ]);
    }
}
```

# 9. Mailableクラスの実装
Mailableクラスを作成し、パスワード再設定用メールの送信処理を実装します。

>Mailableクラスは、コントローラ（メール送信）で呼び出して使用します。


### Mailableクラスを作成
以下コマンドを実装し、Mailableクラスを継承したファイルを作成します。

```terminal:terminal
php artisan make:mail ResetPasswordMail
```

### ResetPasswordMailクラスの実装
上記で作成したファイルに、以下コードを記述します。

**Tips：**
- `URL::temporarySignedRoute`
署名付き、有効期限24時間のURLを生成します。

- `URL::temporarySignedRoute`
署名付きURLの検証するメソッドです。

- `from(config('mail.from.address'), config('mail.from.name'))`
config/mail.phpに以下のように定義します。
送信元の名前はご自身の環境のものを設定してください。

```php:config/mail.php
‘from’ => [
    ‘address’ => ‘hogefuga@example.com’,  // 送信元のメールアドレスと
    ‘name’ => ‘山田太郎’                   // 送信元の名前
    ],
```

```php:ResetPasswordMail.php
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
use Carbon\Carbon;

class ResetPasswordMail extends Mailable
{
    use Queueable, SerializesModels;

    private $user;
    private $userToken;

    /**
     * construct
     *
     * @param User $user
     * @param User $userToken
     */
    public function __construct(User $user, User $userToken)
    {
        $this->user = $user;
        $this->userToken = $userToken;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        // トークン取得 
        $tokenParam = ['reset_token' => $this->userToken->rest_password_access_key];
        $now = Carbon::now();

        // 署名付き有効期限24時間のURLを生成
        $url = URL::temporarySignedRoute('reset.password.edit' , $now->addHours(24), $tokenParam);

        // HTML形式でメール作成
        return $this->view('users.password_reset_mail')
                    ->subject('パスワード再設定用URLのご案内')
                    ->from(config('mail.from.address'), config('mail.from.name'))
                    ->to($this->user->mail)
                    ->with([
                        'user' => $this->user,
                        'url' => $url,
                        ]);
    }
}
```

### パスワード再設定用メールの本文を作成
以下の通り、ビューファイルを用意します。
- `resources/views` 配下に `mails` ディレクトリを作成
- 上記ディレクトリ配下に `password_reset_mail.blade.php` を作成

上記で作成したファイルに、メールの本文を記述します。

>最低限のコードになりますので、アレンジして使ってください。

```php:password_reset_mail.blade.php
<p>※このメールはパスワード再設定をご希望された方にお送りしております。</p>
<br/>
<p>「xxxx」をご利用いただき誠にありがとうございます。</p>
<p>パスワード再設定用のURLをお送りします。</p>
<br/>
<a href="{{ $url }}">{{ $url }}</a><br>
<br/>
<p>24時間以内に上記のURLにアクセスし、パスワード再設定の手続きをお願いいたします。</p>
<br/>
<p>このメールは「xxxx」より自動送信されております。</p>
<p>ご返信できませんのでご了承ください。</p>
<br/>
<br/>
<br/>
<p>
    ----------------------------
    署名など
    
    
    ----------------------------
</p>
```

# 10. コントローラにメール送信処理を追加
コントローラに、①〜④のコード（参照：`/* ここから追加 */` ）を記述します。

**Tips：**
- `Log::info` メール送信に成功した場合／`Log::errror` メール送信に失敗した場合
storage/logs/laravel.logにログが出力されます。特にエラーになった際、原因を特定しやすくなります。

- `Mail::send(new ResetPasswordMail($user, $userToken));`
ResetPasswordMailクラスを呼び出し、メール送信の処理を行います。


```php:UsersController
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
/* ①ここから追加 */
use App\Repositories\Interfaces\UserRepositoryInterface;
use App\Http\Requests\ResetInputMailRequest;
use App\Mail\ResetPasswordMail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Exception;
/* ①ここまで追加 */

class UsersController extends Controller
{
    /* ②ここから追加 */
    private $userRepository;
    private const MAIL_SENDED_SESSION_KEY = 'user_reset_password_mail_sended_action';

    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }
    /* ②ここまで追加 */

	/**
     * パスワードリセット
     */

    // パスワード再設定用のメール送信フォーム
    public function requestResetPassword()
    {
        return view('users.reset_input_mail');
    }

    //  メール送信
    public function sendResetPasswordMail(ResetInputMailRequest $request)
    {
        /* ③ここから追加 */
        try {
            // ユーザー情報取得
            $user = $this->userRepository->findFromMail($request->mail);
            $userToken = $this->userRepository->updateOrCreateUser($user->id);

            // メール送信
            Log::info(__METHOD__ . '...ID:' . $user->id . 'のユーザーにパスワード再設定用メールを送信します。');
            Mail::send(new ResetPasswordMail($user, $userToken));
            Log::info(__METHOD__ . '...ID:' . $user->id . 'のユーザーにパスワード再設定用メールを送信しました。');
        } catch(Exception $e) {
            Log::error(__METHOD__ . '...ユーザーへのパスワード再設定用メール送信に失敗しました。 request_email = ' . $request->mail . ' error_message = ' . $e);
            return redirect()->route('reset.form')
                ->with('flash_message', '処理に失敗しました。時間をおいて再度お試しください。');
        }
        // 不正アクセス防止セッションキー
        session()->put(self::MAIL_SENDED_SESSION_KEY, 'user_reset_password_send_email');
        /* ③ここまで追加 */

        return redirect()->route('reset.send.complete');
    }

    // メール送信完了
    public function sendCompleteResetPasswordMail()
    {
        /* ④ここから追加 */
        // 不正アクセス防止セッションキーを持っていない場合
        if (session()->pull(self::MAIL_SENDED_SESSION_KEY) !== 'user_reset_password_send_email') {
            return redirect()->route('reset.form')
                ->with('flash_message', '不正なリクエストです。');
        }
        /* ④ここまで追加 */
        
        return view('users.reset_input_mail_complete');
    }

// 省略

```
<br/>

:::note
「②メール送信の実装」は以上になります。
ここまでの実装で、**フォームに入力したメールアドレスに、パスワード再設定用のURLを記載したメールを送信する** 実装ができました。

ここからは、**パスワード再設定用のURLにアクセスし、DBのパスワードを更新する** に箇所あたる、「③パスワード更新の実装」を行います。
:::

# 11. バリデーションの実装_part2
以下2つのバリデーションを実装します。
- パスワード再設定フォーム用
- トークンの有効期限確認用（オリジナルバリデーション）


### パスワード再設定フォーム用バリデーションの実装
以下コマンドを実行し、リクエストファイルを作成します。

```terminal:terminal
php artisan make:request ResetPasswordRequest
```

上記で作成したファイルに、以下コードを記述します。

**Tips：**
- `regex:/^[0-9a-zA-z-_]{8,32}$/`
半角英数字, -, _ のみ入力を許可し、8文字〜32文字で入力されているか判定します。
こちらの正規表現はあくまで一例ですので、アレンジして使ってください。

- `confirmed` 
再入力欄と一致しているか判定します。

- `new \App\Rules\TokenExpirationTimeCheck()` 
トークンの有効期限をチェックするオリジナルバリデーションを呼び出しています。

```php:ResetInputPasswordRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ResetPasswordRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules()
    {
        $rules = [
            'password' => ['required', `regex:/^[0-9a-zA-z-_]{8,32}$/`, 'confirmed'],
            'password_confirmation' => ['required', 'same:password'],
            'reset_token' => ['required', new \App\Rules\TokenExpirationTimeCheck()],
        ];

        return $rules;
    }

    /**
     * エラーメッセージ
     * @return array
     */
    public function messages()
    {
        return [
            'password.required' => 'パスワードを入力してください',
            'password.regex' => 'パスワードは半角英数字とハイフンとアンダーバーのみで8文字以上32文字以内で入力してください',
            'password.confirmed' => 'パスワードが再入力欄と一致していません',
        ];
    }

}
```

### トークンの有効期限確認用バリデーションの実装
以下コマンドを実行し、オリジナルバリデーションファイルを作成します。

```terminal:terminal
php artisan make:rule TokenExpirationTimeRule
```

上記で作成したファイルに、以下コードを記述します。

**Tips：**
- URLの有効期限が切れている場合
メール記載のリンクにアクセスすることは可能ですが、パスワードを更新しようとした際に、こちらのバリデーションに引っかかりエラーメッセージが出力されます。
```php:TokenExpirationTimeCheck.php
<?php

namespace App\Rules;

use App\Repositories\Interfaces\UserRepositoryInterface;
use Illuminate\Contracts\Validation\Rule;
use Carbon\Carbon;


class TokenExpirationTimeCheck implements Rule
{
    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * トークンの有効期限をチェック
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $now = Carbon::now();
        $userRepository = app()->make(UserRepositoryInterface::class);
        $userToken = $userRepository->getUserTokenFromUser($value);
        $expireTime = new Carbon($userToken->rest_password_expire_data);

        return $now->lte($expireTime);
    }

    /**
     * エラーメッセージ
     *
     * @return string
     */
    public function message()
    {
        return '有効期限が過ぎています。パスワード再設定用のメールを再発行してください。';
    }
}

```

# 12. リポジトリパターンの実装_part2
インタフェースとリポジトリに、パスワード更新の処理を実装します。

### インタフェースに追加実装

`パスワードリセット用トークンを発行 updateOrCreateUser()`の下に以下2つのファンクションを追加します。

```PHP:UserRepositoryInterface.php
// 省略
      
    /**
     * トークンからユーザー情報を取得
     * @param string $token
     * @return User
     */
    public function getUserTokenFromUser(string $token): User;

    /**
     * パスワード更新
     *
     * @param string $password
     * @param int $id
     * @return void
     */
    public function updateUserPassword(string $password, int $id): void;
}
```

### リポジトリに追加実装
インタフェースに追加した2つのファンクションについて、具体的な処理（参照：`/* ここから追加 */`）を追加します。

```PHP:UserRepository.php
// 省略

    // userテーブルに登録
    public function updateOrCreateUser(int $userId): User
    {

        $now = Carbon::now();
        // $userIdをハッシュ化
        $hashedToken = hash('sha256', $userId);
        return $this->userToken->updateOrCreate(
            [
                'id' => $userId,
            ],
            [
            // $hashedTokenを含むトークンを作成
            'rest_password_access_key' => uniqid(rand(), $hashedToken),
            // トークンの有効期限を現在から24時間後に設定
            'rest_password_expire_data' => $now->addHours(24)->toDateTimeString()
            ]);
    }


    /* ここから追加 */
    // トークンからユーザー情報を取得
    public function getUserTokenFromUser(string $token): User
    {
        return $this->userToken->where('rest_password_access_key', $token)->firstOrFail();
    }

    // パスワード更新
    public function updateUserPassword(string $password, int $id): void
    {
        $this->user->where('id', $id)->update(['password' => $password]);
    }
    /* ここまで追加 */
}
```

# 13. コントローラにパスワード更新処理を追加
コントローラに、①②のコード（参照：`/* ここから追加 */` ）を記述します。

```php:UsersController.php
// 省略

/* ①ここから追加 */
use App\Http\Requests\ResetPasswordRequest;
/* ①ここまで追加 */

class UsersController extends Controller
{

// 中略
    
    /* ②ここから追加 */
    // パスワード再設定
    public function resetPassword(Request $request)
    {
        // 署名付きURLではない場合
    	if (!$request->hasValidSignature()) {
            abort(403, 'URLの有効期限が過ぎたためエラーが発生しました。パスワード再設定メールを再発行してください。');
        }

        $resetToken = $request->reset_token;

        try {
            // ユーザー情報取得
            $userToken = $this->userRepository->getUserTokenFromUser($resetToken);
        } catch (Exception $e) {
            Log::error(__METHOD__ . ' UserTokenの取得に失敗しました。 error_message = ' . $e);
            return redirect()->route('reset.form')
                ->with('flash_message', __('パスワード再設定メールに添付されたURLから遷移してください。'));
        }

        return view('users.reset_input_password', compact('userToken', 'userMail'));
    }

    // パスワード更新
    public function updatePassword(ResetPasswordRequest $request)
    {
        try {
            // ユーザー情報取得
            $userToken = $this->userRepository->getUserTokenFromUser($request->reset_token);
            // パスワード暗号化
            $password = encrypt($request->password);
            $this->userRepository->updateUserPassword($password, $userToken->id);
            Log::info(__METHOD__ . '...ID:' . $userToken->user_id . 'のユーザーのパスワードを更新しました。');
        } catch (Exception $e) {
            Log::error(__METHOD__ . '...ユーザーのパスワードの更新に失敗しました。...error_message = ' . $e);
            return redirect()->route('reset.form')
                ->with('flash_message', __('処理に失敗しました。時間をおいて再度お試しください。'));
        }

        return view('users.reset_input_password_complete');
    }
    /* ②ここまで追加 */

}
```

:::note info
以上でパスワードリセット機能の実装が全て完了しました！
次章にて、ファサードでの実装をご紹介しています。
:::

# 14. おまけ：ファサードで実装 
処理の手順に関しては、リポジトリパターンと同様です。

### 変更点
リポジトリパターンで使用した、以下コードは使用しません。
- プロバイダー
- インタフェース
- リポジトリ
 
ファサードで実装にあたり、以下ファイルの新規作成・修正を行います。
- app/Fasades/UsersService（新規）
- app/Services/UsersService（新規）
- UsersController（修正）

### ファサードクラスの作成
Facadeを継承したUsersServiceクラスを作成します。

```php:app/Fasades/UsersService
<?php

namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class UsersService extends Facade
{
    protected static function getFacadeAccessor()
    {
        return \App\Services\UsersService::class;
    }
}
```

### サービスクラスの作成
上記で作成したファサードを使用して以下サービスクラスを呼び出し、その中に実際の処理を記述します。
サービスクラスに記述にした処理は、コントローラで呼び出して使います。

```php:app/Services/UsersService
<?php

namespace App\Services;

use Illuminate\Http\Request;
use Carbon\Carbon;

class UsersService
{

	/**
     * パスワードリセット
     */

    // メールアドレスからユーザー情報取得
    public function findFromMail(string $mail)
    {
        $user = \App\Models\User::where('mail', $mail)->first();
        return $user;
    }

    // トークン, 有効期限を登録
    public function updateOrCreateUser(int $userId)
    {
        $now = Carbon::now();
        // $userIdをハッシュ化
        $hashedToken = hash('sha256', $userId);
        $userToken = \App\Models\User::updateOrCreate(
                        [
                            'id' => $userId,
                        ],
                        [
                        // $hashedTokenを含むトークンを作成
                        'rest_password_access_key' => uniqid(rand(), $hashedToken),
                        // トークンの有効期限を現在から24時間後に設定
                        'rest_password_expire_data' => $now->addHours(24)->toDateTimeString()
                        ]);
        return $userToken;
    }

    // トークンからユーザー情報を取得
    public function getUserTokenFromUser(string $token)
    {
        $userToken = \App\Models\User::where('rest_password_access_key', $token)->first();
        return $userToken;
    }

    // トークンからメールアドレスを取得
    public function getUserMailByResetToken(string $resetToken)
    {
        $userMail = \App\Models\User::select('mail')
        ->where('rest_password_access_key', $resetToken)->first();
        return $userMail;
    }

    // パスワード更新
    public function updateUserPassword(string $password, int $userId)
    {
        \App\Models\User::where('id', $userId)->update(['password' => $password]);
    }
}
```
 
### コントローラの修正
UsersControllerを以下の通り修正します。

```php:UsersController
// 省略

/**
 * パスワードリセット
 */

public function requestResetPassword()
{
    return view('users.reset_input_mail');
}

//  パスワード再設定用のメール送信
public function sendResetPasswordMail(ResetInputMailRequest $request)
{
    try {
        // ユーザー情報取得
        $user = UsersService::findFromMail($request->mail);
        $userToken = UsersService::updateOrCreateUser($user->id);

        // メール送信
        Log::info(__METHOD__ . '...ID:' . $user->id . 'のユーザーにパスワード再設定用メールを送信します。');
        Mail::send(new ResetPasswordMail($user, $userToken));
        Log::info(__METHOD__ . '...ID:' . $user->id . 'のユーザーにパスワード再設定用メールを送信しました。');
    } catch(Exception $e) {
        Log::error(__METHOD__ . '...ユーザーへのパスワード再設定用メール送信に失敗しました。 request_email = ' . $request->mail . ' error_message = ' . $e);
        return redirect()->route('reset.form')
            ->with('flash_message', '処理に失敗しました。時間をおいて再度お試しください。');
    }
    // 不正アクセス防止セッションキー
    session()->put(self::MAIL_SENDED_SESSION_KEY, 'user_reset_password_send_email');

    return redirect()->route('reset.send.complete');
}

// メール送信完了
public function sendCompleteResetPasswordMail()
{
   // 不正アクセス防止セッションキーを持っていない場合
    if (session()->pull(self::MAIL_SENDED_SESSION_KEY) !== 'user_reset_password_send_email') {
        return redirect()->route('reset.form')
            ->with('flash_message', '不正なリクエストです。');
    }
    return view('users.reset_input_mail_complete');
}

// パスワード再設定
public function resetPassword(Request $request)
{
    // 署名付きURLではない場合
    if (!$request->hasValidSignature()) {
        abort(403, 'URLの有効期限が過ぎたためエラーが発生しました。パスワード再設定メールを再発行してください。');
    }

    $resetToken = $request->reset_token;
    $userMail = UsersService::getUserMailByResetToken($resetToken);

    try {
        // ユーザー情報取得
        $userToken = UsersService::getUserTokenFromUser($resetToken);
    } catch (Exception $e) {
        Log::error(__METHOD__ . ' UserTokenの取得に失敗しました。 error_message = ' . $e);
        return redirect()->route('reset.form')
            ->with('flash_message', __('パスワード再設定メールに添付されたURLから遷移してください。'));
    }

    return view('users.reset_input_password', compact('userToken', 'userMail'));
}

// パスワード更新
public function updatePassword(ResetPasswordRequest $request)
{
    try {
        // ユーザー情報取得
        $userToken = UsersService::getUserTokenFromUser($request->reset_token);
        // パスワード暗号化
        $password = encrypt($request->password);
        UsersService::updateUserPassword($password, $userToken->id);
        Log::info(__METHOD__ . '...ID:' . $userToken->user_id . 'のユーザーのパスワードを更新しました。');
    } catch (Exception $e) {
        Log::error(__METHOD__ . '...ユーザーのパスワードの更新に失敗しました。...error_message = ' . $e);
        return redirect()->route('reset.form')
            ->with('flash_message', __('処理に失敗しました。時間をおいて再度お試しください。'));
    }
    
    return view('users.reset_input_password_complete');
}

// 省略
```
# 15. 参考 
実装内容は以下サイトを参考にさせていただきました。

https://takuma-it.com/laravel-password-reset/