【Laravel8】メールアドレスの変更APIを作ってみる。email verify

今回はメールアドレスの変更APIを作成してみます。

普通にユーザーに対してupdateしたらよいんじゃないか?って声は聞こえてきそうですが、そうはいかないのです。

なぜなら…?

伝えたいこと: メールアドレス変更API作成の方法

まず、なぜメールアドレスの変更を行うのか。基本的にwebサービスではユーザー登録を行った際にemail verifyが行われますよね?

email verifyとは、「登録したメールアドレスってちゃんとあなたが使っていますよね?」って確認するやつです。

ユーザー登録を行うと、メールが届いてurlにアクセスしてください。っていうのが基本的ですがそれです。

そのemail verifyが行われたメールアドレスから別のアドレスに変更する際に、普通にユーザー情報の更新と一緒にemail verify無しで更新しちゃったら…

ダミーメールアドレスかもしれないですよね?

それを防ぐためにも、メールアドレスを変更するときにはそれ専用のAPIが必要となってくるわけです。

そして今回メールアドレスを変更する流れは以下です。

  1. ユーザーがメールアドレス変更の申請を行う。(新メールアドレスを送信)
  2. 新メールアドレスに対してtokenを発行して、新メールアドレスにメールを送る。(今回メールを送る機能は作りません。)
  3. ユーザーがメールから届いたurlにアクセスするとメールアドレスの更新は完了。

この流れをAPI的に作ってみましょう。

前提条件

以下を前提として進めていきますね。

  • Laravel8
  • APIをつかった方法
  • メール送信の機能は作成しない
  • すでにuserテーブルは作成されていると仮定
  • 認証機能は作成済み(sanctum)

メールアドレス変更の申請API

今回はこちらの記事を参考にさせていいただいています。

新メールアドレスとtokenを保存するテーブルを作成

ほい、それでは作っていきましょうか。

まず作りたいのは、まずは新メールアドレスを保存するテーブルを作らないといけませんね。

最終的にここで作成するテーブルを見てユーザー情報を更新します。

user_email_resetsテーブルを作成しましょう。

php artisan make:migration create_user_email_resets_tableCode language: CSS (css)

ではテーブルの中身を書いていきます。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUserEmailResetsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        //userがメールアドレスを変更する時に利用するテーブル
        Schema::create('user_email_resets', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id')->comment('users.id');
            $table->string('new_email')->comment('ユーザーが新規に設定するメールアドレス');
            $table->string('token')->unique()->comment('email address変更用token');
            $table->dateTime('expired_at')->nullable()->comment('tokenの有効期限');
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users')->onUpdate('cascade')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('user_email_resets');
    }
}Code language: HTML, XML (xml)

こんな感じで作ります。

正直tokenの有効期限はcreated_atを見てそこから何時間か計算したりしてもよいんですが、今回は有効期限用のカラムも準備しました。そっちのほうが個人的にシンプルに見える!

そしたらテーブルを作成っと

php artisan migrate

モデルを作成

先ほど作成したテーブルに付随するモデルを作成しましょう。

php artisan make:model UserEmailResetCode language: CSS (css)

モデルには今回大したことは書いていません。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class UserEmailReset extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'new_email',
        'token',
        'expired_at',
    ];
}
Code language: HTML, XML (xml)

メールアドレス変更を申請するControllerの作成

php artisan make:controller ChangeCode language: CSS (css)
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

//追加
use App\Http\Requests\EmailReset\CreateTokenUserEmailResetRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use App\Models\UserEmailReset;
use App\Models\User;

class ChangeUserEmailController extends Controller
{
    public function createToken(CreateTokenUserEmailResetRequest $request)
    {
        //token生成
        $token = hash_hmac(
            'sha256',
            Str::random(40),
            config('app.key')
        );

        //メール変更したいユーザー取得
        $user_id = Auth::id();

        //createメソッドを使ってレコードを追加する。
       $reset_info = UserEmailReset::create([
            'user_id' => $user_id,
            'new_email' => $request->email,
            'token' => $token,
            'expired_at' => date("Y-m-d H:i:s", strtotime("+1 day", time())), //tokenの有効期限は作成日から24時間
        ]);

        //成功をレスポンスで返す。
        return response()->json([
            'success' => true,
            'message' => 'Create email reset token success!',
            'details' => $reset_info->token
        ]);

    }
}
Code language: HTML, XML (xml)

とりあえずtokenの有効期限は1日にしておきました。1時間とかのほうが良いんかなぁ…?

hash_hmac()の使い方

FormRequest

一応バリデーションはformrequestに書いていきます。

php artisan make:request EmailReset/CreateTokenUserEmailResetRequest

ほんでバリデーション

<?php

namespace App\Http\Requests\UserEmailReset;

use Illuminate\Foundation\Http\FormRequest;

//追加
use Illuminate\Validation\Rule;

class CreateTokenUserEmailResetRequest 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
     */
    public function rules()
    {
        return [
            'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')],
        ];
    }
}
Code language: HTML, XML (xml)

今回の場合は変更後のメールアドレスをリクエストパラメータに入れて送られてくるので、emailというパラメータをバリデーションしましょう。

これでOK後はroutingに追加。

api.php(routing)に追加

use App\Http\Controllers\ChangeUserEmailController;

//sanctum認証
Route::middleware('auth:sanctum')->group(function(){

 Route::post('user/email-reset', [ChangeUserEmailController::class, 'createToken']);

});Code language: PHP (php)

なんで認証入れてるのかというのは、controllerのところでAuth::id()を利用しているからですね~

認証済みのユーザーからユーザー情報を取得しています。

リクエストパラメータ

今作ったAPIで送るリクエストは、新しくしたいメールアドレスをリクエストパラメータに入れて送ります。

POST api/user/email-reset

{
  email: new_email@email.com
}Code language: CSS (css)

レスポンスには、アクティベート用のtokenが返ってきます。

その返ってきたtokenを使って次のAPIでアクティベートをかけます。

メールアドレス変更を完了するAPI

ChangeUserEmailControllerに追記

そしたら次はemailを変更する機能を作りましょう。

    public function resetEmail($token)
    {
        //送られてきたtokenが存在しない場合エラーを返す
        if(!UserEmailReset::where('token', $token)->exists()){
            return response()->json([
                'success' => false,
                'message' => 'token does not exist',
                'details' => $token,
            ]);
        }
        $email_reset = UserEmailReset::where('token', $token)->first();

        //tokenが存在&tokenの期限が切れていない場合はuser_email_resetsテーブルをみて
        //new_emailを対象のユーザーのメールアドレスとして更新する。
        if($email_reset && $email_reset->expired_at >= now()) {
            $user = User::where('id', $email_reset->user_id)->first();
            $user->email = $email_reset->new_email;
            $user->save();

            //updateできたらレコードを削除する
            UserEmailReset::where('token', $token)->delete();

            return response()->json([
                'success' => true,
                'message' => 'Update email address success!',
                'details' => $user
            ]);
        }else{
            //対象のレコードのtokenの期限が切れているとレコードを削除する。ユーザーは再設定が必要。
            UserEmailReset::where('token', $token)->delete();
            
            return response()->json([
                'success' => false,
                'message' => 'your token has expired',
                'details' => $email_reset,
            ]);
        }
    }
Code language: PHP (php)

今回はGETパラメーターに先ほど作成したtokenが送られてくるのでそのtokenから情報を取得してユーザーのメールアドレスをupdateさせます。

まず最初に以下の動作を行います。

  1. リクエストで送られてきたtokenがuser_email_resetsテーブルに存在するか確認
  2. tokenの有効期限は切れていないか確認

問題がなさそうだったら、user_email_resetsテーブルにuser_idカラムがあるのでusersテーブルのidカラムと一致するデータを取得します。

そして最後にメールアドレスをuser_email_resets.new_emailからメールアドレスを取得して、users.emailにupdateします!

そうするとアクティベートの完成!

アクティベートができたらuser_email_resetsの対象レコードを削除しておきます!

routing (api.php)

routingに以下を追加します。

Route::get('user/email-reset/{token}', [ChangeUserEmailController::class, 'resetEmail']);//新規メールアドレスに更新
Code language: PHP (php)

リクエストはGETリクエストでGETパラメータに先ほどのresetのAPIで作成したtokenを入れて下さい。

まとめ

意外とシンプルにできましたね。

laravelってデフォルトでメールアドレス変更する用の機能ないんかなぁって思いました。

あったらすみません。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA