Laravel PassportでOAuth2を用いた認証機能を作成しよう！②

# はじめに

本記事は Laravel Passport で OAuth2 を用いた認証機能を作成する手順の一部です。
事前にこちらの記事からご覧ください。

https://qiita.com/falya128/items/652a0d150faaaeb58b2b

※ 太字の部分が本記事で説明している内容です。

【リソースサーバ 兼 認証サーバ】
    [1. Laravel Passport のインストール](https://qiita.com/falya128/items/3b306194a5bad14ab582)
    [2. Laravel Passport の初期設定](https://qiita.com/falya128/items/3b306194a5bad14ab582)
    [3. ログイン画面の作成](https://qiita.com/falya128/items/3b306194a5bad14ab582)
    [4. マイページ画面の作成](https://qiita.com/falya128/items/3b306194a5bad14ab582)
    **5. ユーザ情報確認／変更画面の作成**
    **6. ログアウト処理の追加**
 
【クライアントアプリ】
    [7. トークンのリクエスト処理の追加](https://qiita.com/falya128/items/651daf3f4dd91ac70dcc)
    [8. Laravel Sanctum の初期設定](https://qiita.com/falya128/items/651daf3f4dd91ac70dcc)
    [9. ログインユーザ情報表示画面の作成](https://qiita.com/falya128/items/651daf3f4dd91ac70dcc)
    [10. ログアウト処理の追加](https://qiita.com/falya128/items/651daf3f4dd91ac70dcc)

【リソースサーバ 兼 認証サーバ】
    [11. Laravel Passport のルート登録](https://qiita.com/falya128/items/b73d91b0bde1fa65ad98)
    [12. トークン取消 API の作成](https://qiita.com/falya128/items/b73d91b0bde1fa65ad98)

# 作成手順

## 5. ユーザ情報確認／変更画面の作成

### View の作成

ユーザ情報確認画面として表示する `user\Index.vue` を新たに作成します。
初期表示時にユーザ情報を取得する API を実行しています。未認証であればログイン画面にリダイレクトします。

```vue:resources\js\views\user\Index.vue
<script setup>
import axios from 'axios'
import { useRouter } from 'vue-router'

const router = useRouter()

let user = null
await axios
  .get('/api/user')
  .then((response) => {
    user = response?.data
  })
  .catch((reason) => {
    if (reason?.response?.status === 401) {
      window.location.href = '/login'
    }
  })
</script>

<template>
  <div class="flex justify-center">
    <div class="max-w-md w-full rounded overflow-hidden shadow-lg my-6 mx-4 md:mx-auto">
      <div class="font-bold text-xl px-4 pt-6 pb-2">ユーザ情報確認</div>
      <div class="flex justify-end mx-4 mb-4">
        <RouterLink
          class="text-sm underline underline-offset-4 text-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-300"
          to="/user/edit"
        >
          ユーザ情報変更
        </RouterLink>
      </div>
      <div class="border-y border-gray-100">
        <dl v-if="user !== null" class="divide-y divide-gray-100">
          <div class="px-4 py-6 sm:grid sm:grid-cols-3">
            <dt class="text-sm font-medium leading-6 text-gray-900">ユーザ名</dt>
            <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
              {{ user.name }}
            </dd>
          </div>
          <div class="px-4 py-6 sm:grid sm:grid-cols-3">
            <dt class="text-sm font-medium leading-6 text-gray-900">メールアドレス</dt>
            <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
              {{ user.email }}
            </dd>
          </div>
          <div class="px-4 py-6 sm:grid sm:grid-cols-3">
            <dt class="text-sm font-medium leading-6 text-gray-900">最終ログイン日時</dt>
            <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
              {{ user.last_login_at }}
            </dd>
          </div>
        </dl>
      </div>
      <div class="flex justify-center my-4">
        <button
          type="button"
          class="rounded-md bg-white border-2 px-5 py-2 text-sm shadow-sm hover:bg-gray-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-300"
          @click="router.push('/')"
        >
          戻る
        </button>
      </div>
    </div>
  </div>
</template>
```


ユーザ情報変更画面として表示する `user\Edit.vue` を新たに作成します。
初期表示時にユーザ情報を取得する API を実行しています。未認証であればログイン画面にリダイレクトします。

```vue:resources\js\views\user\Edit.vue
<script setup>
import axios from 'axios'
import { ref } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()

const form = ref({
  name: '',
  email: ''
})

await axios
  .get('/api/user')
  .then((response) => {
    form.value = {
      name: response?.data?.name ?? '',
      email: response?.data?.email ?? ''
    }
  })
  .catch((reason) => {
    if (reason?.response?.status === 401) {
      window.location.href = '/login'
    }
  })

const errors = ref({})
const submit = async () => {
  await axios
    .post('/user', form.value)
    .then((response) => {
      router.push('/user')
    })
    .catch((reason) => {
      if (reason?.response?.status === 422) {
        errors.value = reason.response.data?.errors ?? {}
        console.log(errors.value)
      }
    })
}
</script>

<template>
  <div class="flex justify-center">
    <div class="max-w-md w-full rounded overflow-hidden shadow-lg my-6 mx-4 md:mx-auto">
      <div class="font-bold text-xl px-4 pt-6 pb-2">ユーザ情報確認</div>
      <form @submit.prevent="submit()">
        <div class="px-4 py-2">
          <label for="name" class="text-sm font-medium">ユーザ名</label>
          <div class="mt-2">
            <input
              v-model="form.name"
              id="name"
              name="name"
              type="name"
              placeholder="Test User"
              class="w-full rounded-md border-0 p-2 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 sm:leading-6"
            />
            <div
              v-if="errors.name"
              class="bg-red-100 border border-red-400 text-red-700 px-2 py-1 mt-1 text-sm rounded relative"
              role="alert"
            >
              <div v-for="error in errors.name">{{ error }}</div>
            </div>
          </div>
        </div>
        <div class="px-4 py-2">
          <label for="email" class="text-sm font-medium">メールアドレス</label>
          <div class="mt-2">
            <input
              v-model="form.email"
              id="email"
              name="email"
              type="email"
              placeholder="test@example.com"
              class="w-full rounded-md border-0 p-2 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 sm:leading-6"
            />
            <div
              v-if="errors.email"
              class="bg-red-100 border border-red-400 text-red-700 px-2 py-1 mt-1 text-sm rounded relative"
              role="alert"
            >
              <div v-for="error in errors.email">{{ error }}</div>
            </div>
          </div>
        </div>
        <div class="flex justify-center my-4 space-x-4">
          <button
            type="button"
            class="rounded-md bg-white border-2 px-5 py-2 text-sm shadow-sm hover:bg-gray-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-300"
            @click="router.push('/user')"
          >
            キャンセル
          </button>
          <button
            type="submit"
            class="rounded-md bg-indigo-600 px-5 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
          >
            変更
          </button>
        </div>
      </form>
    </div>
  </div>
</template>
```

ログイン画面からユーザ情報確認画面へ遷移する処理を `Index.vue` に追加します。

```diff_vue:resources\js\views\Index.vue
<script setup>
import axios from 'axios'
+ import { useRouter } from 'vue-router'

+ const router = useRouter()
```

```diff_vue:resources\js\views\Index.vue
  <button
    type="button"
    class="rounded-md bg-gray-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
+   @click="router.push('/user')"
  >
    ユーザ情報確認
  </button>
```

### Controller の作成

ユーザ情報更新処理を実行する `UserController` を作成します。ユーザ情報更新処理の内容は以下の通りです。

1. `name` と `email` の入力値をチェックする。
2. ログインユーザの `name` と `email` を更新する。

```php:app\Http\Controllers\UserController.php
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class UserController extends Controller
{
    public function update(Request $request): JsonResponse
    {
        $inputs = $request->validate([
            'name' => ['required'],
            'email' => ['required', 'email'],
        ]);

        /** @var User ログインユーザ */
        $user = Auth::user();

        $user->fill($inputs)->save();

        return response()->json();
    }
}
```

### ルートの登録

追加した View を表示する ルートを登録します。

```diff_php:routes\web.php
Route::middleware(['auth:web'])->group(function () {
    Route::get('/', fn () => view('app'));
+   Route::get('/user/{path?}', fn () => view('app'))->where('path', 'edit');
});
```

```diff_javascript:resources\js\router\index.js
  routes: [
    {
      path: '/',
      name: 'index',
      component: () => import('../views/Index.vue')
+   },
+   {
+     path: '/user',
+     name: 'user.index',
+     component: () => import('../views/user/Index.vue')
+   },
+   {
+     path: '/user/edit',
+     name: 'user.edit',
+     component: () => import('../views/user/Edit.vue')
    }
  ]
```

追加した Controller にアクセスするルートを登録します。

```diff_php:routes\web.php
Route::middleware(['auth:web'])->group(function () {
    Route::get('/', fn () => view('app'));
    Route::get('/user/{path?}', fn () => view('app'))->where('path', 'edit');

+   Route::post('/user', [App\Http\Controllers\UserController::class, 'update']);
});
```

## 6. ログアウト処理の追加

### Controller の作成

ログアウト処理を `AuthController` に追加します。
処理後にリダイレクトを行い、デフォルトでログイン画面に遷移します。もし、`redirect_uri` パラメータの指定があれば、その指定されたルートへリダイレクトします。（後ほど作成するクライアントアプリからリダイレクトしてきた際にパラメータを指定する想定）

```diff_php:app\Http\Controllers\AuthController.php
+   /**
+    * ログアウト
+    *
+    * @param Request $request
+    * @return RedirectResponse
+    */
+   public function logout(Request $request): RedirectResponse
+   {
+       Auth::logout();
+
+       $request->session()->invalidate();
+       $request->session()->regenerateToken();
+
+       return redirect($request->input('redirect_uri', '/login'));
+   }
}
```

### ルートの登録

追加したログアウト処理を実行するルートを登録します。

```diff_php:routes\web.php
Route::middleware(['auth:web'])->group(function () {
    Route::get('/', fn () => view('app'));
    Route::get('/user/{path?}', fn () => view('app'))->where('path', 'edit');

    Route::post('/user', [App\Http\Controllers\UserController::class, 'update']);
});

+ Route::get('/logout', [App\Http\Controllers\AuthController::class, 'logout']);
```


### 動作確認

リソースサーバ 兼 認証サーバの画面の作成が一通り完了しました。ここまで正しく作成できているか確認のため、実際に開発用サーバを起動して動作させてみます。以下のコマンドを実行して、`http://localhost:8000` にアクセスします。

```bash 
npm run build
php artisan serve
```

ログイン後、「ユーザ情報確認」をクリックするとユーザ情報確認画面が表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339770/2940e9c5-ac95-1e93-eb70-6ec61462293b.png" width="500" />

また、「ユーザ情報変更」のリンクをクリックするとユーザ情報変更画面が表示されます。実際に値を変更して、変更が反映されれば OK です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339770/2bdbb78f-ae01-301c-70d8-7918a8cef4a8.png" width="450" />

最後に、マイページ画面まで戻って「ログアウト」をクリックします。認証が解除されてログイン画面が表示されれば OK です。

# おわりに

続きはこちらの記事をご覧ください。

https://qiita.com/falya128/items/651daf3f4dd91ac70dcc