背景
Nuxtを使用しないプロジェクトに参画するので、vue-routerやvuexの設定等を復習したい。
前提
手順
- APIルートにはwebミドルウェアを使用し、トークンを使った認証にする
- 認証機能はLaravelのデフォルトのものを使用する
-
login_id
とpassword
で新規登録、ログイン、ログアウトできるところまでを実装 - 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>