LoginSignup
2
3

More than 3 years have passed since last update.

【備忘録】Laravel + Vueだけの環境でログイン機能を実装する

Posted at

背景

Nuxtを使用しないプロジェクトに参画するので、vue-routerやvuexの設定等を復習したい。

前提

Dockerを使ってローカル環境構築済み

手順

  • APIルートにはwebミドルウェアを使用し、トークンを使った認証にする
  • 認証機能はLaravelのデフォルトのものを使用する
  • login_idpasswordで新規登録、ログイン、ログアウトできるところまでを実装
  • cookieから取り出したCSRF-TOKENをajaxリクエストのヘッダーに付与する
  • app.tsに、常に認証状態をvuexで保持するよう記述する
  • コンポーネント化とcssによるスタイリングは割愛

認証機能はLaravelのデフォルトのものを使用する

usersテーブルにlogin_idカラムを追加

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');  // 追加
      $table->string('email');  // unique()を削除
      $table->timestamp('email_verified_at')->nullable();
      $table->string('password');
      $table->rememberToken();
      $table->timestamps();
      $table->softDeletes();  // 追加
      $table->unique(['email', 'deleted_at'], 'users_email_deleted_at_unique');  // 追加
      $table->unique(['login_id', 'deleted_at'], 'users_login_id_deleted_at_unique');  // 追加
    });
  }
app/Models/User.php

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;  // 追加

class User extends Authenticatable
{
  use Notifiable;
  use SoftDeletes;  // 追加

  /**
   * The attributes that are mass assignable.
   *
   * @var array
   */
  protected $fillable = [
    'name', 'email', 'login_id', 'password',
  ];

  protected $fillable = [
    'name', 'email', 'login_id', 'password',  // login_idを追加
  ];

// (中略)
/var/www/laravel
$ php artisan migrate:rollback
$ php artisan migrate

APIルートにはwebミドルウェアを使用し、トークンを使った認証にする

app/Providers/RouteServiceProvider.php
<?php


use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{

  (中略)

  protected function mapApiRoutes()
  {
    Route::prefix('api')
         ->middleware('web') // apiからwebに変更
         ->namespace($this->namespace)
         ->group(base_path('routes/api.php'));
  }

  (以下略)

ルーティング

routes/api.php
Route::group(['middleware' => 'api'], function () {

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

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

    Route::middleware('auth')->group(function() {
      Route::post('/logout', 'LoginController@logout')->name('logout');
    });
  });
});

新規登録

app/Http/Controllers/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\Models\User;  // 追加
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\Request;  // 追加
use Illuminate\Validation\Rule;  // 追加

class RegisterController extends Controller
{
  // 中略

  protected function validator(array $data)
  {
    return Validator::make($data, [
      'name' => ['required', 'string', 'max:255'],
      'login_id' => ['required', 'string', 'max:255', Rule::unique('users', 'login_id')->whereNull('deleted_at')],  // 追加
      'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'login_id')->whereNull('deleted_at')],
      'password' => ['required', 'string', 'min:8', 'confirmed'],
    ]);
  }

  // (中略)

  protected function create(array $data)
  {
    return User::create([
      'name' => $data['name'],
      'login_id' => $data['login_id'],  // 追加
      'email' => $data['email'],
      'password' => Hash::make($data['password']),
    ]);
  }

  // (中略)

  // ↓追加
  /**
   * @param Request $request
   * @param User $user
   * @return User
   */
  protected function registered(Request $request, $user)
  {
    return $user;
  }

ログイン/ログアウト

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;  // 追加
use Illuminate\Support\Facades\Auth;  // 追加

class LoginController extends Controller
{
  // 中略

  /**
   * @return string
   */
  public function username()
  {
    return 'login_id';
  }

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

  /**
   * @param Request $request
   */
  protected function loggedOut(Request $request)
  {
    Auth::logout();
    $request->session()->regenerate();

    return response()->json();
  }

// (以下略)

認証時は/homeではなく、ユーザーを返す処理にリダイレクト

app/Http/Middleware/RedirectIfAuthenticated.php
public function handle($request, Closure $next, $guard = null)
{
  if (Auth::guard($guard)->check()) {
    return redirect()->route('currentUser');  // 修正
  }

  return $next($request);
}

フロント側の実装

~/workspace/myapp/laravel
$ touch resources/ts/util.ts
resources/ts/util.ts
export function getCookieValue (searchKey: string): string {
  if (typeof searchKey === 'undefined') {
    return ''
  }

  let val: string = ''

  document.cookie.split(';').forEach((cookie: string): void | string => {
    const [key, value] = cookie.split('=')
    if (key === searchKey) {
      return (val = value)
    }
  })

  return val
}

export const OK: number = 200
export const CREATED: number = 201
export const DELETED: number = 204
export const INTERNAL_SERVER_ERROR: number = 500
export const UNPROCESSABLE_ENTITY: number = 422
export const UNAUTHORIZED: number = 401
export const NOT_FOUND: number = 404
resources/ts/bootstrap.ts
import Axios, { AxiosStatic } from 'axios'
import { getCookieValue } from '@/util'

declare global {
  interface Window {
    axios: AxiosStatic
  }
  interface Element {
    content: string
  }
}

export default function bootstrap(): void {
  window.axios = Axios

  window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'

  window.axios.interceptors.request.use(config => {
    // クッキーからトークンを取り出してヘッダーに添付する
    config.headers['X-XSRF-TOKEN'] = getCookieValue('XSRF-TOKEN')

    return config
  })
}
resources/ts/app.ts
import Vue from 'vue'
import router from './router'
import App from './App.vue'
import store from './store'

// 追加
import bootstrap from './bootstrap'
bootstrap()

new Vue({
  el: '#app',
  router,
  store,
  template: '<App />'
})
~/workspace/myapp/laravel
$ mkdir -p resources/ts/types/Models
$ touch resources/ts/types/{Models/User,Auth,Error}.ts \
resources/ts/store/{auth,error}.ts
resources/ts/types/Models/User.ts
export type User = {
  id: number
  name: string
  [k: string]: any
} | null
resources/ts/types/Auth.ts
export type RegisterRequest = {
  name: string
  login_id: string
  email: string
  password: string
  password_confirmation: string
}

export type LoginRequest = {
  login_id: string
  password: string
}
resources/ts/types/Error.ts
export type ErrorMessages = null | {
  [k: string]: string[]
}
resources/ts/store/error.ts
import { ErrorMessages } from '@/types/Error'

export type State = {
  messages: ErrorMessages
  status: null | number
}

const state = {
  messages: null,
  status: null,
}

const getters = {
  messages: (state: State) => state.messages || [],
  status: (state: State) => state.status,
}

const mutations = {
  setErrors(state: State, data: State) {
    state.messages = data.messages
    state.status = data.status
  },
}

const actions = {}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions,
}
resources/ts/store/auth.ts
import { User } from '@/types/Models/User'
import { RegisterRequest, LoginRequest } from '@/types/Auth'
import { UNPROCESSABLE_ENTITY } from '@/util'
import router from '@/router'

export type State = {
  user: User
}

const state = {
  user: null,
}

const getters = {
  isLogin: (state: State): boolean => !!state.user,
  userId: (state: State): number | null => (state.user ? state.user.id : null),
  userName: (state: State): string => (state.user ? state.user.name : ''),
}

const mutations = {
  setUser(state: State, user: User): void {
    state.user = user
  },
}

const actions = {
  async register(context, data: RegisterRequest): Promise<void> {
    await window.axios
      .post('/api/register', data)
      .then((response) => {
        context.commit('setUser', response.data)
        context.commit(
          'error/setErrors',
          {
            messages: null,
            status: null,
          },
          { root: true }
        )
        router.push('/mypage')
      })
      .catch((err) => {
        if (err.response.status === UNPROCESSABLE_ENTITY) {
          context.commit(
            'error/setErrors',
            {
              messages: err.response.data.errors,
              status: err.response.status,
            },
            { root: true }
          )
        }
      })
  },

  async login(context, data: LoginRequest): Promise<void> {
    await window.axios
      .post('/api/login', data)
      .then((response) => {
        context.commit('setUser', response.data)
        context.commit(
          'error/setErrors',
          {
            messages: null,
            status: null,
          },
          { root: true }
        )
        router.push('/mypage')
      })
      .catch((err) => {
        if (err.response.status === UNPROCESSABLE_ENTITY) {
          context.commit(
            'error/setErrors',
            {
              messages: err.response.data.errors,
              status: err.response.status,
            },
            { root: true }
          )
        } else {
          context.commit('error/setErrors',
          {
            messages: err.response,
            status: err.response.status,
          },
          { root: true }
          )
        }
      })
  },

  async logout(context): Promise<void> {
    await window.axios
      .post('/api/logout')
      .then((response) => {
        context.commit('setUser', null)
        router.push('/login')
      })
      .catch((err) => {
        context.commit(
          'error/setErrors',
          {
            messages: err.response,
            status: err.response.status,
          },
          { root: true }
        )
      })
  },

  async currentUser(context): Promise<void> {
    await window.axios
      .get('/api/current_user')
      .then((response) => {
        const user = response.data || null
        context.commit('setUser', user)
      })
      .catch((err) => {
        context.commit(
          'error/setErrors',
          {
            messages: err.response.data.errors,
            status: err.response.status,
          },
          { root: true }
        )
      })
  },
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions,
}
resources/ts/store/index.ts
import Vue from 'vue'
import Vuex from 'vuex'

import auth from './auth'
import error from './error'
Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    auth,
    error
  },
})

export default store
resources/ts/app.ts
import Vue from 'vue'
import router from './router'
import App from './App.vue'
import store from './store'

import bootstrap from './bootstrap'

bootstrap()

const createApp = async () => {
  await store.dispatch('auth/currentUser')
  new Vue({
    el: '#app',
    router,
    store,
    render: (h) => h(App),
  })
}

createApp()
~/workspace/myapp/laravel
$ mkdir resources/ts/layouts resources/ts/pages/mypage
$ touch resources/ts/layouts/{Default,MyPageLayout}.vue \
> resources/ts/pages/Register.vue \
> resources/ts/pages/mypage/Index.vue 
resources/ts/router.ts

// ↓レイアウト追加
import DefaultLayout from './layouts/Default.vue'
import MyPageLayout from './layouts/MyPageLayout.vue'

import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from './pages/Index.vue'
import Login from './pages/Login.vue'

// ↓追加
import Register from './pages/Register.vue'
import MyPageIndex from './pages/mypage/Index.vue'

import store from './store'
Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    component: DefaultLayout,
    children: [
      {
        path: '/',
        component: Index,
      },
      {
        path: '/register',
        component: Register,
        beforeEnter (to, from, next) {
          if (store.getters['auth/isLogin']) {
            next('/')
          } else {
            next()
          }
        }
      },
      {
        path: '/login',
        component: Login,
        beforeEnter (to, from, next) {
          if (store.getters['auth/isLogin']) {
            next('/')
          } else {
            next()
          }
        }
      },
    ],
  },
  {
    path: '/mypage',
    component: MyPageLayout,
    beforeEnter (to, from, next) {
      if (!store.getters['auth/isLogin']) {
        next('/login')
      } else {
        next()
      }
    },
    children: [
      {
        path: '/',
        component: MyPageIndex,
      }
    ],
  },
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router
resources/ts/App.vue
<template>
  <div>
    <router-view />
  </div>
</template>
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'

@Component

export default class App extends Vue {}
</script>
resources/ts/layouts/Default.vue
<template>
  <div>
    <header>
      <nav>
        <ul v-if="!isLogin">
          <li><router-link tag="a" to="/login">ログイン</router-link></li>
          <li><router-link tag="a" to="/register">会員登録</router-link></li>
        </ul>
        <ul v-else>
          <li @click="logout">ログアウト</li>
        </ul>
      </nav>
    </header>
    <main>
      <div class="container">
        <router-view />
      </div>
    </main>
  </div>
</template>
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
import { mapGetters } from 'vuex'
@Component({
  computed: {
    ...mapGetters({
      isLogin: 'admin/isLogin'
    })
  }
})

export default class Default extends Vue {
  async logout(): Promise<void> {
    await this.$store.dispatch('admin/logout')
  }
}
</script>
resources/ts/layouts/MyPageLayout.vue
<template>
  <div>
    <header>
      <nav>
        <ul v-if="!isLogin">
          <li><router-link tag="a" to="/login">ログイン</router-link></li>
          <li><router-link tag="a" to="/register">会員登録</router-link></li>
        </ul>
        <ul v-else>
          <li @click="logout">ログアウト</li>
        </ul>
      </nav>
    </header>
    <main>
      <div class="container">
        <router-view :user="user" />
      </div>
    </main>
  </div>
</template>
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
import { mapGetters } from 'vuex'
@Component({
  computed: {
    ...mapGetters({
      isLogin: 'auth/isLogin',
      user: 'auth/userInfo',
    })
  }
})

export default class MyPageLayout extends Vue {
  async logout(): Promise<void> {
    await this.$store.dispatch('auth/logout')
  }
}
</script>
resources/ts/pages/Login.vue
<template>
<div>
  <h1>ログイン</h1>
  <form @submit.prevent="login">
    <div>
      <input type="text" v-model="loginForm.login_id" placeholder="login_id" required>
    </div>
    <div>
      <input type="password" v-model="loginForm.password" placeholder="password" required>
    </div>
    <div>
      <button>ログイン</button>
    </div>
  </form>
</div>
</template>

<script lang="ts">
import { Vue, Component} from 'vue-property-decorator'
import { LoginRequest } from '@/types/Auth'

@Component
export default class Login extends Vue {

  loginForm: LoginRequest = {
    login_id: '',
    password: '',
  }

  async login(): Promise<void> {
    const submitData = new FormData()
    submitData.append('login_id', this.loginForm.login_id)
    submitData.append('password', this.loginForm.password)
    await this.$store.dispatch('auth/login', submitData)
  }

}
</script>
resources/ts/pages/Register.vue
<template>
<div>
  <h1>会員登録</h1>
  <form @submit.prevent="register">
    <div>
      <input type="text" v-model="registerForm.name" placeholder="name" required>
    </div>
    <div>
      <input type="email" v-model="registerForm.email" placeholder="mail" required>
    </div>
    <div>
      <input type="text" v-model="registerForm.login_id" placeholder="login_id" required>
    </div>
    <div>
      <input type="password" v-model="registerForm.password" placeholder="password" required>
    </div>
    <div>
      <input type="password" v-model="registerForm.password_confirmation" placeholder="password_confirmation" required>
    </div>
    <div>
      <button>会員登録</button>
    </div>
  </form>
</div>
</template>

<script lang="ts">
import { Vue, Component} from 'vue-property-decorator'
import { RegisterRequest } from '@/types/Auth'

@Component
export default class Register extends Vue {

  registerForm: RegisterRequest = {
    name: '',
    email: '',
    login_id: '',
    password: '',
    password_confirmation: '',
  }

  async register(): Promise<void> {
    const submitData = new FormData()
    submitData.append('name', this.registerForm.name)
    submitData.append('email', this.registerForm.email)
    submitData.append('login_id', this.registerForm.login_id)
    submitData.append('password', this.registerForm.password)
    submitData.append('password_confirmation', this.registerForm.password_confirmation)
    await this.$store.dispatch('auth/register', submitData)
  }

}
</script>
resources/ts/pages/mypage/Index.vue
<template>
<div>
  <p>{{ user.name }}さん、マイページへようこそ</p>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator'
import { User } from '@/types/Models/User'
@Component
export default class Index extends Vue {
  @Prop({ type: Object, required: true })
  user!: User
}
</script>
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3