【備忘録】NuxtとLaravelでログイン機能を実装する　

# 前提

### ローカル環境
* macOS Mojave10.14.6
* Docker for mac 2.2.0.5

### バージョン
* Laravel...7.30.4
* @nuxt/types...2.14.7
* alpine

### やりたいこと
* ```login_id```, `password`でログインする
* cookieによる認証機能を使う

### その他

* Dockerを使って環境構築済み
* [自分が作った環境](https://qiita.com/kaba_farm/items/8d8b8faab808a55d3232)
* 厳密なエラーハンドリングやバリデーションチェック等は省略


# Laravel側の実装

## Userモデルの移動関連
```terminal
/var/www/laravel # mkdir app/Models && \
> mv app/User.php app/Models/
```

```php:app/Models/User.php
<?php

// App\Modelsに修正
namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{

  // 以下略
```

```php:database/factories/UserFactory.php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Models\User;　　// ここを修正
use Faker\Generator as Faker;
use Illuminate\Support\Str;

// 以下略
```

```php:config/auth.php

<?php

return [

  // 中略
  'providers' => [
    'users' => [
      'driver' => 'eloquent',
      'model' => App\Models\User::class,  // ここを修正
    ],
  ],

  // 以下略
```

```terminal:Terminal
$ docker-compose exec app ash
$ php artisan -V // 7.30.4
$ composer require laravel/ui:2
$ php artisan ui vue --auth
```

### APIでセッション管理を使った認証機能を有効にする
```diff:app/Http/Kernel.php
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{

  (中略)

  protected $middlewareGroups = [
    'web' => [
      \App\Http\Middleware\EncryptCookies::class,
      \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
      \Illuminate\Session\Middleware\StartSession::class,
      // \Illuminate\Session\Middleware\AuthenticateSession::class,
      \Illuminate\View\Middleware\ShareErrorsFromSession::class,
      \App\Http\Middleware\VerifyCsrfToken::class,
      \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],

    'api' => [
+     \App\Http\Middleware\EncryptCookies::class,
+     \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
+     \Illuminate\Session\Middleware\StartSession::class,
+     \Illuminate\View\Middleware\ShareErrorsFromSession::class,
     'throttle:60,1',
      \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
  ];
}
```

### authミドルウェアの設定
```diff:config/auth.php
<?php

return [

  (中略)

  'defaults' => [
    'guard' => 'api',  // 'web'から変更
    'passwords' => 'users',
  ],

  (中略)

  'guards' => [
    'web' => [
      'driver' => 'session',
      'provider' => 'users',
    ],

    'api' => [
      'driver' => 'session', // tokenからsessionに変更
      'provider' => 'users',
-      'hash' => false,
    ],
  ],

  (以下略)

];
```

### マイグレーション
```php:database/migrations/2014_10_12_000000_create_users_table.php
<?php

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

class CreateUsersTable extends Migration
{
  /**
   * Run the migrations.
   *
   * @return void
   */
  public function up()
  {
    Schema::create('users', function (Blueprint $table) {
      $table->id();
      $table->string('name');
      $table->string('login_id')->unique();　// 追加
      $table->string('email')->unique();
      $table->timestamp('email_verified_at')->nullable();
      $table->string('password');
      $table->rememberToken();
      $table->timestamps();
    });
  }

  // 以下略
}
```

```terminal
$ php artisan migrate
```


### シーディング
```terminal
$php artisan make:seeder UsersTableSeeder
```

```php:database/factories/UserFactory.php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Models\User;
use Faker\Generator as Faker;
use Illuminate\Support\Str;

$factory->define(User::class, function (Faker $faker) {
  return [
    'name' => $faker->name,
    'login_id' => $faker->unique()->userName(), // 追加
    'email' => $faker->unique()->safeEmail,
    'email_verified_at' => now(),
    'password' => \Hash::make('nininopassword'), // passwordから変更
    'remember_token' => Str::random(10),
  ];
});
```

```php:database/seeds/UsersTableSeeder.php
<?php

use Illuminate\Database\Seeder;
use App\Models\User;

class UsersTableSeeder extends Seeder
{
  /**
   * Run the database seeds.
   *
   * @return void
   */
  public function run()
  {
    \DB::table('users')->truncate();
    factory(User::class, 1)->create([
      'login_id' => 'testuser',
    ]);
  }
}
```

```php:database/seeds/DatabaseSeeder.php

public function run()
{
  $this->call(UserSeeder::class);
  }
```



```terminal
$ php artisan db:seed
```

### ルーティング
```php:routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth;  // 追加

// 追加
Route::group(["middleware" => "api"], function () {

  Route::get('/current_user', function () {
    return Auth::user();
  })->name('current_user');

  Route::namespace('Auth')->group(function() {
    Route::post('/login', 'LoginController@login')->name('login');
  });
});
```

### コントローラー
```php:app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use App\Models\User;  // 追加

class LoginController extends Controller
{

  //中略

  public function username()
  {
    return 'login_id';
  }

  /**
   * @param Request $request
   * @param User $user
   * @return mixed
   */
  protected function authenticated(Request $request, User $user)
  {
    return $user;
  }
}
```

### ミドルウェアの修正
```php:app/Http/Middleware/RedirectIfAuthenticated.php
<?php

namespace App\Http\Middleware;

use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Support\Facades\Auth;

class RedirectIfAuthenticated
{
  /**
   * Handle an incoming request.
   *
   * @param \Illuminate\Http\Request $request
   * @param \Closure $next
   * @param string|null $guard
   * @return mixed
   */
  public function handle($request, Closure $next, $guard = null)
  {
    if (Auth::guard($guard)->check()) {
      return redirect()->route('current_user');  // 変更
    }

    return $next($request);
  }
}
```

### corsの設定
```terminal:/var/www/laravel
/var/www/laravel # composer require fruitcake/laravel-cors
```

```php:app/Http/Kernel.php
protected $middleware = [
  // \App\Http\Middleware\TrustHosts::class,
  \App\Http\Middleware\TrustProxies::class,
  \Fruitcake\Cors\HandleCors::class,
  \App\Http\Middleware\CheckForMaintenanceMode::class,
  \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
  \App\Http\Middleware\TrimStrings::class,
  \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
  \Fruitcake\Cors\HandleCors::class,  // 追加
];
```

```env:.env
CORS_ALLOWED_ORIGIN=http://localhost:3000
```

```terminal:/var/www/laravel
/var/www/laravel # php artisan vendor:publish --tag="cors"
```

```php:config/cors.php
<?php

return [

  /*
    |--------------------------------------------------------------------------
    | Cross-Origin Resource Sharing (CORS) Configuration
    |--------------------------------------------------------------------------
    |
    | Here you may configure your settings for cross-origin resource sharing
    | or "CORS". This determines what cross-origin operations may execute
    | in web browsers. You are free to adjust these settings as needed.
    |
    | To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
    |
    */


  'paths' => ['*'],

  'allowed_methods' => ['*'],

  'allowed_origins' => ['*'],

  'allowed_origins_patterns' => [],

  'allowed_headers' => ['*'],

  'exposed_headers' => [],

  'max_age' => 0,

  'supports_credentials' => true,

];
```

```Terminal:/var/www/laravel
/var/www/laravel # php artisan config:cache 
```

# Nuxt側の実装

### nuxt-property-decoratorのインストール
```terminal:~/workspace/myapp
$ docker-compose exec front_app ash
/var/www/nuxt # yarn add -D nuxt-property-decorator
```

### ログインに最低限必要なファイル作成
```terminal:~/workspace/myapp/nuxt
$ mkdir pages/mypage
$ touch pages/{login,mypage/index}.vue \
> store/{index,auth}.ts \
> middleware/{guestCheck,loginCheck}.ts
```

### ストアにログイン処理
```ts:store/auth.ts
import { GetterTree, ActionTree, MutationTree } from 'vuex'

export const state = () => ({
  user: null,
  statusCode: null,
  messages: null,
})

export type RootState = ReturnType<typeof state>

export const mutations: MutationTree<RootState> = {
  setUser(state, user) {
    state.user = user
  },
  setStatus(state, error) {
    state.statusCode = error.status
    state.messages = error.messages
  },
  clearStatus(state) {
    state.statusCode = null
    state.messages = null
  },
}

export const getters: GetterTree<RootState, RootState> = {
  currentUser(state) {
    return state.user
  },
  isLogin(state): boolean {
    return !!state.user
  },
  isError(state): boolean {
    return state.messages === null
  },
}

export const actions: ActionTree<RootState, RootState> = {
  async login({ commit, dispatch }, submitData): Promise<void> {
    await this.$axios
      .$post('/login', submitData)
      .then((response): void => {
        commit('status/clearStatus')
        commit('setUser', response)
      })
      .catch((err): void => {
        dispatch('errorHandler', err)
      })
  },
  errorHandler({ commit, dispatch }, err): void {
    commit('setStatus', {
      status: err.response.status,
      messages: err.response.data.errors,
    })
  },
}
```

### 状態維持
```ts:store/index.ts
export const actions = {
  async nuxtServerInit({ commit }, { app }): Promise<void> {
    await app.$axios
      .$get('/current_user')
      .then((user): void => commit('auth/setUser', user))
      .catch(() => commit('auth/setUser', null))
  },
}
```

### ミドルウェア

認証済の場合はへマイページ画面にリダイレクトさせる

```ts:middleware/loginCheck.ts
import { Middleware } from '@nuxt/types'

const loginCheck: Middleware = ({ store, redirect }): void => {
  if (store.state.auth.user) {
    redirect('/mypage')
  }
}

export default loginCheck
```

認証済でない場合はへログイン画面にリダイレクトさせる

```ts:middleware/guestCheck.ts
import { Middleware } from '@nuxt/types'
const guestCheck: Middleware = ({ store, redirect, route }): void => {　
  if (!store.state.auth.user) {
    redirect({
      name: 'login',
    })
  }
}

export default guestCheck
```
### ログイン画面へのリンク〜ログイン後のマイページへの画面遷移
```vue:pages/index.vue
<template>
  <div>
    <router-link to="/login" tag="a">ログイン</router-link>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator';

@Component
export default class Index extends Vue {}
</script>
```

```vue:pages/login.vue
<template>
  <div class="login">
    <h1>ログイン画面</h1>
    <form @submit.prevent="login">
      <div class="row">
        <input type="text" v-model="loginData.login_id">
      </div>
      <div class="row">
        <input type="password" v-model="loginData.password">
      </div>
      <div class="row">
        <button>ログイン</button>
      </div>
    </form>
  </div>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
@Component({
  middleware: 'loginCheck',
})
export default class Login extends Vue {
  loginData: object = {
    login_id: '',
    password: '',
  }

  async login(): Promise<void> {
    const submitData = new FormData()
    submitData.append('login_id', this.loginData.login_id)
    submitData.append('password', this.loginData.password)
    await this.$store.dispatch('auth/login', submitData)
    if (this.$store.getters['auth/isLogin']) {
      this.$router.push('/mypage')
    }
  }
}
</script>
```

```vue:pages/mypage/index.vue
<template>
  <div>
    <h1>{{ user.name }}さん、こんにちは</h1>
  </div>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import { mapGetters } from 'vuex'

@Component({
  middleware: 'guestCheck',
  computed: {
    ...mapGetters({
      user: 'auth/currentUser',
    })
  }
})
export default class Index extends Vue {}
</script>
```

### 画面確認

認証後にログイン画面に行こうとするとマイページにリダイレクトされるところまで

![login.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553584/ab008d9b-4e9e-2c59-d7e2-8fc51f5dc194.gif)


# ログアウト

### Laravel側の実装
```php:routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth;

Route::group(["middleware" => "api"], function () {
  Route::post('/login', 'Auth\LoginController@login')->name('login');
  Route::get('/current_user', function () {
    return Auth::user();
  })->name('current_user');
  // 追加
  Route::middleware('auth:api')->group(function() {
    Route::post('/logout', 'Auth\LoginController@logout')->name('logout');
  });
});
```

```php:app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{

  //中略

  public function username()
  {
    return 'login_id';
  }

  /**
   * @param Request $request
   * @param $user
   * @return mixed
   */
  protected function authenticated(Request $request, $user)
  {
    return $user;
  }

  // 追加
  /**
   * @param Request $request
   * @return \Illuminate\Http\JsonResponse
   */
  protected function loggedOut(Request $request)
  {
    Auth::logout();
    return response()->json();
  }

}
```

### Nuxt側の実装
```ts:store/auth.ts
import { GetterTree, ActionTree, MutationTree } from 'vuex'

// 中略

export const actions: ActionTree<RootState, RootState> = {
  async login(
    { commit, dispatch },
    submitData
  ): Promise<void> {
    await this.$axios
      .$post('/login', submitData)
      .then((response): void => {
        commit('setUser', response)
      })
      .catch((err): void => {
        dispatch('errorHandler', err)
      })
  },
  // 追加
  async logout({ commit, dispatch }): Promise<void> {
    await this.$axios
      .$post('/logout')
      .then(() => {
        commit('setUser', null)
      })
      .catch((err): void => {
        dispatch('status/errorHandler', err)
      })
  },
  errorHandler({ commit, dispatch }, err): void {
    commit('setStatus', {
      status: err.response.status,
      messages: err.response.data.errors,
    })
  },
}

```
```vue:pages/mypage/index.vue
<template>
  <div>
    <h1>{{ user.name }}さん、こんにちは</h1>
    <!-- ↓追加 -->
    <button @click="logout">ログアウト</button>
  </div>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import { mapGetters } from 'vuex'

@Component({
  middleware: 'guestCheck',
  computed: {
    ...mapGetters({
      user: 'auth/currentUser',
    })
  }
})
export default class Index extends Vue {
  // ↓追加
  async logout(): Promise<void> {
    await this.$store.dispatch('auth/logout')
    if(!this.user) {
      this.$router.push('/login')
    }
  }
}
</script>
```

### 画面確認

認証されていない状態でマイページに行こうとするとログイン画面に戻されるところまで

![logout.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553584/9184f903-05d7-fddb-d4f4-51dd10599ff9.gif)