Laravel mix vue No.6 - Api Resetting Passwords パスワードリセット

こんにちは、あすかのkoheiです。

今回はパスワードを忘れた場合にメールでパスワードリセットできるようにします。
こちらも最善の方法がわからないので自分なりに作っていきます。
もっと良い方法を知ってる方はコメントで教えてくれたら嬉しいです!

連載記事

Api Resetting Passwords - パスワードリセット

パスワードを忘れた場合にメールでパスワードリセット

サンプル


フォルダ構成


└─ server
   ├─ app
   |  ├─ Http
   |  |  └─ Controllers
   |  |     └─ Auth
   |  |        ├─ ForgotPasswordController.php
   |  |        └─ ResetPasswordController.php
   |  ├─ Mail
+  |  |  └─ ResetPasswordMail.php
   |  └─ Models
+  |     └─ ResetPassword.php
   ├─ resources
   |  ├─ views
+  |  |  └─ mails
+  |  |     └─ reset_password_mail.blade.php
   |  └─ js
   |     └─ pages
   |        ├─ Login.vue
+  |        └─ Reset.vue
   └─ routes
      ├─ api.php
      └─ web.php

dockerスタートとnpmモジュール追加

gitからクローンした場合は.envの作成と設定を忘れないように!

# コンテナスタート
docker-compose start

# コンテナに入る
docker-compose exec php bash

# composerをインストール(前回からの続きで行う場合はいらない)
composer install

# npmをインストール(前回からの続きで行う場合はいらない)
npm i

# encryption keyを作成(前回からの続きで行う場合はいらない)
php artisan key:generate

# ホットリリード開始
npm run watch

ResetPasswordモデル

laravelのartisanでmodelを作成

テーブル名はReset_passwordなのでクラス名はResetPasswordとしてModelsフォルダに作成


php artisan make:model Models/ResetPassword

server\app\Models\ResetPassword.phpが作られるので以下のように編集


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ResetPassword extends Model
{
    // テーブル名を指定
+   protected $table = 'password_resets';

    // プライマリキーを「email」に変更
    // デフォルトは「id」
+   protected $primaryKey = 'email';
    // プライマリキーのタイプを指定
+   protected $keyType = 'string';
    // タイプがストリングの場合はインクリメントを「false」にしないといけない
+   public $incrementing = false;

    // モデルが以下のフィールド以外を持たないようにする
+   protected $fillable = [
+       'email',
+       'token',
+   ];

    // タイムスタンプは「created_at」のフィールドだけにしたいので、「false」を指定
+   public $timestamps = false;
    // 自前で用意する
+   public static function boot()
+   {
+       parent::boot();
+       static::creating(function ($model) {
+           $model->created_at = $model->freshTimestamp();
+       });
+   }
}

パスワードを忘れた場合の処理をしてパスワード変更メールを送る

ForgotPasswordControllerの修正

パスワードを忘れた場合用にまるっと修正 server\app\Http\Controllers\Auth\ForgotPasswordController.php


<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Mail;
use App\Models\ResetPassword;
use App\Mail\ResetPasswordMail;

class ForgotPasswordController extends Controller
{
    /**
     * send mail
     * 送られてきた内容をテーブルに保存してパスワード変更メールを送信
     * 
     * @param  \Illuminate\Http\Request  $request
     * @return ResetPassword
     */
    public function forgot(Request $request)
    {
        // validation
        // 送られてきた内容のバリデーション
        $this->validator($request->all())->validate();

        // create token
        // トークンを作成
        $token = $this->createToken();

        // delete old data
        // 古いデータが有れば削除
        ResetPassword::destroy($request->email);

        // insert
        // 送られてきた内容をテーブルに保存
        $resetPassword = new ResetPassword($request->all());
        $resetPassword->token = $token;
        $resetPassword->save();

        // send email
        // メールクラスでメールを送信
        $this->sendResetPasswordMail($resetPassword->email, $token);

        return $resetPassword;
    }

    /**
     * Get a validator
     * バリデーション
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'email' => [
                'required',
                'email',
                'exists:users,email',
            ],
        ]);
    }

    /**
     * create token
     * トークンを作成する
     * @return stirng
     */
    private function createToken()
    {
        return hash_hmac('sha256', Str::random(40), config('app.key'));
    }

    /**
     * send reset password mail
     * メールクラスでメールを送信
     * 
     * @param string $email
     * @param string $token
     * @return void
     */
    private function sendResetPasswordMail($email, $token)
    {
        Mail::to($email)
            ->send(new ResetPasswordMail($token));
    }
}

パスワードを忘れた場合で送信するメールクラスを作成

laravelのartisanでクラス名をMailとしてメールクラスを作成


php artisan make:mail ResetPasswordMail

server\app\Mail\ResetPasswordMail.phpが作成されるので編集


<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class ResetPasswordMail extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($token)
    {
        // 引数でトークンを受け取る
        $this->token = $token;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        // 件名
        $subject = 'Reset password mail';

        // コールバックURLをルート名で取得
        // TODO: これだとホットリロードでホストがおかしくなる
        // $url = route('reset-password', ['token' => $this->token]);

        // TODO: とりあえずこれで対応
        // .envの「APP_URL」に設定したurlを取得
        $baseUrl = config('app.url');
        $token = $this->token;
        $url = "{$baseUrl}/reset-password/{$token}";

        // 送信元のアドレス
        // .envの「MAIL_FROM_ADDRESS」に設定したアドレスを取得
        $from = config('mail.from.address');

        return $this->from($from)
            ->subject($subject)
            // 送信メールのビュー
            ->view('mails.reset_password_mail')
            // ビューで使う変数を渡す
            ->with('url', $url);
    }
}

パスワードを忘れた場合で使用するメールテンプレートの作成

server\resources\views\mails\reset_password_mail.blade.phpを作成


    @extends('layouts.mail')

    @section('title', 'パスワードリセット')

    @section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">パスワードリセット</div>

                    <div class="card-body">
                        <a href='{{$url}}'>こちらのリンク</a>をクリックして、パスワードリセットしてください。
                    </div>
                </div>
            </div>
        </div>
    </div>
    @endsection

パスワードを忘れた場合で使用するルートの追加

server\routes\api.phpを修正する


    ...
    // logout
    Route::post('/logout', 'Auth\LoginController@logout')->name('logout');

    // forgot
+   Route::post('/forgot', 'Auth\ForgotPasswordController@forgot')->name('forgot');

    // reset
+   Route::post('/reset', 'Auth\ResetPasswordController@reset')->name('reset');
    ...

ストアの修正

AUTHストアにパスワードを忘れた場合の処理をいれるのでserver\resources\js\store\auth.jsを修正


    ...
    /*
    * ステート( データの入れ物)
    */
    const state = {
        // ログイン済みユーザーを保持
        user: null,
        // Api通信の成功、失敗の保持
        apiStatus: null,
        // ログインのエラーメッセージの保持
        loginErrorMessages: null,
        // 登録のエラーメッセージの保持
        registerErrorMessages: null,
        // リセットパスワードエラーメッセージの保持
+       resetErrorMessages: null,
        // パスワード変更エラーメッセージの保持
+       forgotErrorMessages: null,
    };

    ...

    /*
     * ミューテーション(同期処理)
     */
    const mutations = {
        ...
        // パスワード変更エラーメッセージの更新
+       setForgotErrorMessages(state, messages) {
+           state.forgotErrorMessages = messages;
+       },
        // リセットパスワードエラーメッセージの更新
+       setResetErrorMessages(state, messages) {
+           state.resetErrorMessages = messages;
+       }
    };

    /*
    * アクション(非同期処理)
    */
    const actions = {
      ...
          /*
          * registerのアクション
          */
          async register(context, data) {
            ...
          }
          /*
           * logoutのアクション
           */
          async logout(context) {
            ...
          },
          /*
           * forgotのアクション
           */
+         async forgot(context, data) {
+             // apiStatusのクリア
+             context.commit("setApiStatus", null);
+
+             // Apiリクエスト
+             const response = await axios.post("/api/forgot", data);
+
+             // 通信成功の場合 201
+             if (response.status === CREATED) {
+               // apiStatus を true に更新
+                 context.commit("setApiStatus", true);
+                 // ここで終了
+                 return false;
+             }
+
+             // 通信失敗のステータスが 422(バリデーションエラー)の場合
+             // apiStatus を false に更新
+             context.commit("setApiStatus", false);
+
+             // 通信失敗のステータスが 422(バリデーションエラー)の場合
+             if (response.status === UNPROCESSABLE_ENTITY) {
+                 // registerErrorMessages にエラーメッセージを登録
+                 context.commit("setForgotErrorMessages", response.data.errors);
+             }
+             // 通信失敗のステータスがその他の場合
+             else {
+                 // エラーストアの code にステータスコードを登録
+                 // 別ストアのミューテーションする場合は第三引数に { root: true } を追加
+                 context.commit("error/setCode", response.status, { root: true });
+             }
+         },
          /*
           * resetのアクション
           */
+          async reset(context, data) {
+              // apiStatusのクリア
+              context.commit("setApiStatus", null);
+
+             // Apiリクエスト
+             const response = await axios.post("/api/reset", data);
+
+             // 通信成功の場合 200
+             if (response.status === OK) {
+                 // apiStatus を true に更新
+                 context.commit("setApiStatus", true);
+                 // user にデータを登録
+                 context.commit("setUser", response.data);
+                 // ここで終了
+                 return false;
+             }
+
+             // apiStatus を false に更新
+             context.commit("setApiStatus", false);
+
+             // 通信失敗のステータスが 422(バリデーションエラー)の場合
+             if (response.status === UNPROCESSABLE_ENTITY) {
+                 // validation error then set message
+                 context.commit("setResetErrorMessages", response.data.errors);
+             }
+             // 通信失敗のステータスがその他の場合
+             else {
+                 // エラーストアの code にステータスコードを登録
+                 // 別ストアのミューテーションする場合は第三引数に { root: true } を追加
+                 context.commit("error/setCode", response.status, { root: true });
+             }
+         },
          /*
           * カレントユーザのアクション
           */
          async currentUser(context) {
            ...
          }
    };
...

ログインページを修正

server\resources\js\pages\Login.vue内のforgotメソッドがまだ実装されていないので修正


    <template>
        <div class="container">
            <!-- tabs -->
            ...
            <!-- /tabs -->

            <!-- login -->
            ...
            <!-- /login -->

            <!-- register -->
            ...
            <!-- /register -->

            <!-- forgot -->
            <section class="forgot" v-show="tab === 3">
                <h2>forgot</h2>
+               <!-- errors -->
+               <div v-if="forgotErrors" class="errors">
+                   <ul v-if="forgotErrors.email">
+                       <li v-for="msg in forgotErrors.email" :key="msg">{{ msg }}</li>
+                   </ul>
+               </div>
+               <!--/ errors -->
                <form @submit.prevent="forgot">
                    <div>Email</div>
                    <div>
                        <input type="email" v-model="forgotForm.email" />
                    </div>
                    <div>
                        <button type="submit">send</button>
                    </div>
                </form>
            </section>
            <!-- /forgot -->
        </div>
    </template>

    <script>
    export default {
        // vueで使うデータ
        data() {
            return {
                tab: 1,
                loginForm: {
                    email: "",
                    password: "",
                    remember: true
                },
                registerForm: {
                    name: "",
                    email: "",
                    password: "",
                    password_confirmation: ""
                },
                forgotForm: {
                    email: ""
                }
            };
        },
        // 算出プロパティでストアのステートを参照
        computed: {
            // authストアのapiStatus
            apiStatus() {
                return this.$store.state.auth.apiStatus;
            },
            // authストアのloginErrorMessages
            loginErrors() {
                return this.$store.state.auth.loginErrorMessages;
            },
            // authストアのregisterErrorMessages
            registerErrors() {
                return this.$store.state.auth.registerErrorMessages;
            },
            // authストアのforgotErrorMessages
+           forgotErrors(){
+             return this.$store.state.auth.forgotErrorMessages;
+           }
        },
        methods: {
            /*
             * login
             */
            async login() {
                ...
            },
            /*
             * register
             */
            async register() {
                ...
            },
            /*
             * forgot
             */
            async forgot() {
-               alert("forgot");
-               this.clearForm();
                // authストアのforgotアクションを呼び出す
+               await this.$store.dispatch("auth/forgot", this.forgotForm);
+               if (this.apiStatus) {
                    // show message
+                   this.$store.commit("message/setContent", {
+                       content: "パスワードリセットメールを送りました。",
+                       timeout: 10000
+                   });
+                   // AUTHストアのエラーメッセージをクリア
+                   this.clearError();
                    // フォームをクリア
+                   this.clearForm();
+               }
            },
            /*
             * clear error messages
             */
            clearError() {
                // AUTHストアのすべてのエラーメッセージをクリア
                this.$store.commit("auth/setLoginErrorMessages", null);
                this.$store.commit("auth/setRegisterErrorMessages", null);
+               this.$store.commit("auth/setForgotErrorMessages", null);
            },
            /*
            * clear form
            */
            clearForm() {
                ...
            }
        }
    };
    </script>
    ...

リセットページを作成

新しいパスワードを入力するページをserver\resources\js\pages\Reset.vueとして作成する


    <template>
        <div class="container--small">
            <h2>password reset</h2>

            <div class="panel">
                <!-- @submitで login method を呼び出し -->
                <!-- @submitイベントリスナに reset をつけるとsubmitイベントによってページがリロードさない -->
                <form class="form" @submit.prevent="reset">
                    <!-- errors -->
                    <div v-if="resetErrors" class="errors">
                        <ul v-if="resetErrors.password">
                            <li v-for="msg in resetErrors.password" :key="msg">{{ msg }}</li>
                        </ul>
                        <ul v-if="resetErrors.token">
                            <li v-for="msg in resetErrors.token" :key="msg">{{ msg }}</li>
                        </ul>
                    </div>
                    <!--/ errors -->

                    <div>
                        <input type="password" v-model="resetForm.password" />
                    </div>
                    <div>
                        <input type="password" v-model="resetForm.password_confirmation" />
                    </div>
                    <div>
                        <button type="submit">reset</button>
                    </div>
                </form>
            </div>
        </div>
    </template>

    <script>
    import Cookies from "js-cookie";

    export default {
        // vueで使うデータ
        data() {
            return {
                resetForm: {
                    password: "",
                    password_confirmation: "",
                    token: ""
                }
            };
        },
        computed: {
            // authストアのapiStatus
            apiStatus() {
                return this.$store.state.auth.apiStatus;
            },
            // authストアのresetErrorMessages
            resetErrors() {
                return this.$store.state.auth.resetErrorMessages;
            }
        },
        methods: {
            /*
            * reset
            */
            async reset() {
                // authストアのresetアクションを呼び出す
                await this.$store.dispatch("auth/reset", this.resetForm);
                // 通信成功
                if (this.apiStatus) {
                    // メッセージストアで表示
                    this.$store.commit("message/setContent", {
                        content: "パスワードをリセットしました。",
                        timeout: 10000
                    });
                    // AUTHストアのエラーメッセージをクリア
                    this.clearError();
                    // フォームをクリア
                    this.clearForm();
                    // トップページに移動
                    this.$router.push("/");
                }
            },

            /*
            * clear error messages
            */
            clearError() {
                // AUTHストアのエラーメッセージをクリア
                this.$store.commit("auth/setResetErrorMessages", null);
            },

            /*
            * clear reset Form
            */
            clearForm() {
                // reset
                this.resetForm.password = "";
                this.resetForm.password_confirmation = "";
                this.resetForm.token = "";
            }
        },
        created() {
            // クッキーからリセットトークンを取得
            const token = Cookies.get("RESETTOKEN");

            // リセットトークンがない場合はルートページへ移動させる
            if (this.resetForm.token == null) {
                // move to home
                this.$router.push("/");
            }

            // フォームにリセットトークンをセット
            this.resetForm.token = token;

            // リセットトークンをクッキーから削除
            if (token) {
                Cookies.remove("RESETTOKEN");
            }
        }
    };
    </script>

Vueルーターにあたらしいルートを追加

server\resources\js\router.jsを編集


    ...
    // ページをインポート
    import Home from "./pages/Home.vue";
    import Login from "./pages/Login.vue";
+   import Reset from "./pages/Reset.vue";
    import SystemError from "./pages/errors/SystemError.vue";
    import NotFound from "./pages/errors/NotFound.vue";

    Vue.use(VueRouter);

    // パスとページの設定
    const routes = [
        // home
        {
            ...
        },
        // login
        {
            ...
        },
        // password reset
+       {
+           // urlのパス
+           path: "/reset",
+           // インポートしたページ
+           component: Reset,
+           // ページコンポーネントが切り替わる直前に呼び出される関数
+           // to はアクセスされようとしているルートのルートオブジェクト
+           // from はアクセス元のルート
+           // next はページの移動先
+           beforeEnter(to, from, next) {
+               if (store.getters["auth/check"]) {
+                   next("/");
+               } else {
+                   next();
+               }
+           }
+       },
        // システムエラー
        {
            ...
        },
        // not found
        {
            ...
        }
    ];
    ...

パスワード変更メールからのコールバックを処理してパスワード変更を完了する

ResetPasswordControllerの修正

server\app\Http\Controllers\Auth\ResetPasswordController.phpをパスワード変更用にまるっと修正


    <?php

    namespace App\Http\Controllers\Auth;

    use App\Http\Controllers\Controller;
    use Illuminate\Foundation\Auth\ResetsPasswords;
    use Illuminate\Http\Request;
    use Illuminate\Support\Carbon;
    use Illuminate\Support\Str;
    use Illuminate\Support\Facades\Lang;
    use Illuminate\Support\Facades\Redirect;
    use Illuminate\Support\Facades\Validator;
    use Illuminate\Support\Facades\Crypt;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\Facades\DB;
    use Illuminate\Support\Facades\Hash;
    use Illuminate\Auth\Events\PasswordReset;
    use App\User;
    use App\Models\ResetPassword;

    class ResetPasswordController extends Controller
    {
        use ResetsPasswords;

        // vueでアクセスするログインへのルート
        protected $vueRouteLogin = 'login';
        // vueでアクセスするリセットへのルート
        protected $vueRouteReset = 'reset';
        // server\config\auth.phpで設定していない場合のデフォルト
        protected $expires = 600 * 5;

        /**
         * Create a new controller instance.
         *
         * @return void
         */
        public function __construct()
        {
            // guestミドルウェアはRedirectIfAuthenticatedクラスを指定しているので
            // 認証済み(ログイン済み)の状態でログインページにアクセスすると、ログイン後のトップページにリダイレクトする
            $this->middleware('guest');

            // server\config\auth.phpで設定した値を取得、ない場合はもとの値
            $this->expires = config('auth.reset_password_expires', $this->expires);
        }

        /**
         * reset password
         * パスワード変更メールからのコールバック
         *
         * @param string $token
         * @return Redirect
         */
        public function resetPassword($token = null)
        {
            // トークンがあるかチェック
            $isNotFoundResetPassword = ResetPassword::where('token', $token)
                ->doesntExist();

            // なかったとき
            if ($isNotFoundResetPassword) {
                // メッセージをクッキーにつけてリダイレクト
                $message = Lang::get('password reset email has not been sent.');
                return $this->redirectWithMessage($this->vueRouteLogin, $message);
            }

            // トークンをクッキーにつけてリセットページにリダイレクト
            return $this->redirectWithToken($this->vueRouteReset, $token);
        }

        /**
         * reset
         * パスワードリセットApi
         *
         * @param Request $request
         * @return void
         */
        public function reset(Request $request)
        {
            // バリデーション
            $validator = $this->validator($request->all());

            // 送られてきたトークンを復号
            $token = Crypt::decryptString($request->token);

            // リセットパスワードモデルを取得
            $resetPassword = ResetPassword::where('token', $token)->first();

            // ユーザの宣言
            $user = null;

            // 追加のバリデーション
            $validator->after(function ($validator) use ($resetPassword, &$user) {

                // リセットパスワードがない場合
                if (!$resetPassword) {

                    $validator->errors()->add('token', __('invalid token.'));
                }

                // トークン期限切れチェック
                $isExpired = $this->tokenExpired($resetPassword->created_at);
                if ($isExpired) {

                    $validator->errors()->add('token', __('Expired token.'));
                }

                // ユーザの取得
                $user = User::where('email', $resetPassword->email)->first();

                // ユーザの存在チェック
                if (!$user) {

                    $validator->errors()->add('token', __('is not user.'));
                }
            });

            // これで、バリデーションがある場合に、jsonレスポンスを返す
            $validator->validate();

            // トランザクション、アップデート後のユーザを返す
            $user = DB::transaction(function () use ($request, $resetPassword, $user) {

                // リセットパスワードテーブルからデータを削除
                ResetPassword::destroy($resetPassword->email);

                // パスワードを変更
                $user->password = Hash::make($request->password);

                // リメンバートークンを変更
                $user->setRememberToken(Str::random(60));

                // データを保存
                $user->save();

                // ユーザを返却
                return $user;
            });

            // イベントを発行
            event(new PasswordReset($user));

            // ユーザをログインさせる
            Auth::login($user, true);

            // ユーザを返却
            return $user;
        }

        /**
         * validator
         *
         * @param  array  $data
         * @return \Illuminate\Support\Facades\Validator;
         */
        protected function validator(array $data)
        {
            return Validator::make($data, [
                'token' => ['required'],
                'password' => ['required', 'min:8', 'confirmed'],
            ]);
        }

        /**
         * Determine if the token has expired.
         *
         * @param  string  $createdAt
         * @return bool
         */
        protected function tokenExpired($createdAt)
        {
            return Carbon::parse($createdAt)->addSeconds($this->expires)->isPast();
        }

        /**
         * redirect with message
         *
         * @param  string  $route
         * @param  string  $message
         * @return Redirect
         */
        protected function redirectWithMessage($vueRoute, $message)
        {
            // vueでアクセスするルートを作る
            // コールバックURLをルート名で取得
            // TODO: これだとホットリロードでポートがとれない
            // $route = url($vueRoute);

            // TODO: とりあえずこれで対応
            // .envの「APP_URL」に設定したurlを取得
            $baseUrl = config('app.url');
            $route = "{$baseUrl}/{$vueRoute}";

            return redirect($route)
                // PHPネイティブのsetcookieメソッドに指定する引数同じ
                // ->cookie($name, $value, $minutes, $path, $domain, $secure, $httpOnly)
                ->cookie('MESSAGE', $message, 0, '', '', false, false);
        }

        /**
         * redirect with token
         *
         * @param  string  $route
         * @param  string  $message
         * @return Redirect
         */
        protected function redirectWithToken($vueRoute, $token)
        {
            // vueでアクセスするルートを作る
            // コールバックURLをルート名で取得
            // TODO: これだとホットリロードでポートがとれない
            // $route = url($vueRoute);

            // TODO: とりあえずこれで対応
            // .envの「APP_URL」に設定したurlを取得
            $baseUrl = config('app.url');
            $route = "{$baseUrl}/{$vueRoute}";
            return redirect($route)->cookie('RESETTOKEN', $token, 0, '', '', false, false);
        }
    }

パスワード変更で使用するルートの追加

パスワード変更メールからのコールバックを受けるルートをserver\routes\web.phpに追加する


    ...
    // verification callback
    Route::get('/verification/{token}', 'Auth\VerificationController@register')
        ->name('verification');

    // reset password callback
+   Route::get('/reset-password/{token}', 'Auth\ResetPasswordController@resetPassword')
+       ->name('reset-password');
    ...

パスワードを忘れた場合とパスワード変更メールで変更完了をテスト

  1. パスワード変更メールを送信

キャプチャ1

データベース
キャプチャ2

  1. メールの確認

キャプチャ3

  1. 移動後

キャプチャ4

  1. 変更後 キャプチャ5

次はソーシャルログインを追加します。

Laravel mix vue No.7 - Socialite - ソーシャルログイン

okuda

Webdeveloper

Laravel mix vue No.6 - Api Resetting Passwords パスワードリセット

お気軽に
お問い合わせください。

お問い合わせ