はじめに
「Laravel+SPA+JTWAuthで認証ありの投稿アプリをつくる ~パート1~」のチートシート作成しました。
こちらはコードのみの紹介で一切説明文を入れていません。
説明をみながら作りたい方はこちらからお願いします。
https://qiita.com/ProgramingDai/items/403ee4fbc0971827f160
チートシート
1.JTWAuthログイン
Laravel認証の設定
$ php artisan make:auth
$ php artisan migrate
JWTAuthのインストールと設定
$ composer require tymon/jwt-auth 1.0.0-rc3
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
$ php artisan jwt:secret
(注)Laravelのバージョンは5.7まで
app/User.php
namespace App;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements JWTSubject
{
use Notifiable;
protected $fillable = [
'name', 'email', 'password',
];
protected $hidden = [
'password', 'remember_token',
];
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
}
config/auth.php
// 中略
'defaults' => [
'guard' => 'api', // 変更
'passwords' => 'users',
],
// 中略
'guards' => [
// コメントアウト or トル
// 'web' => [
// 'driver' => 'session',
// 'provider' => 'users',
// ],
'api' => [
'driver' => 'jwt', // 変更
'provider' => 'users',
],
],
// 中略
AuthController作成
$ php artisan make:controller AuthController
app/Http/Controllers/AuthController.php
namespace App\Http\Controllers;
class AuthController extends Controller
{
function login() {
$credentials = request(['email', 'password']);
if (!$token = auth('api')->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
public function logout()
{
auth()->logout();
return response()->json(['message' => 'ログアウトしました。']);
}
public function me()
{
return response()->json(auth()->user());
}
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth("api")->factory()->getTTL() * 60
]);
}
}
routes/api.php
Route::post('/login', 'AuthController@login');
Route::group(['middleware' => 'auth:api'], function () {
Route::get('/me', 'AuthController@me');
Route::post('/logout', 'AuthController@logout');
});
resources/views/app.blade.php
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>LaravelSPA</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link href="{{ mix('/css/app.css') }}" rel="stylesheet">
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body>
<div id="app">
<app></app>
<router-view></router-view>
</div>
<script src="{{ mix('/js/app.js') }}"></script>
</body>
</html>
routes/web.php
// コメントアウト or トル
// Route::get('/', function () {
// return view('welcome');
// });
// Auth::routes();
Route::any('{all}', function () {
return view('app');
})->where(['all' => '.*']);
VueRouterインストール
$ npm install
$ npm install vue-router
$ npm run dev
もしくは
$ npm run watch-poll
resources/js/store.js
export default {
state: {
isLogin: false
}
}
resources/js/app.js
require('./bootstrap');
import Vue from 'vue';
import store from './store';
import router from './router';
window.state = store.state;
Vue.component('app', require('./components/App.vue'));
Vue.component('navbar', require('./components/Navbar.vue'));
const app = new Vue({
router
}).$mount('#app');
resources/js/router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from './components/pages/Home'
import Login from './components/pages/Login'
import User from './components/pages/User'
Vue.use(VueRouter);
const routes = [
{ path: '/', component: Home, meta: { requiresAuth: true } },
{ path: '/login', component: Login },
{ path: '/user', component: User, meta: { requiresAuth: true } }
];
const router = new VueRouter({
mode: 'history',
routes
});
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (state.isLogin === false) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next();
}
});
export default router;
resources/js/components/App.vue
<template>
<div>
<ul>
<li><router-link to="/">ホーム</router-link></li>
<li><router-link to="/login">ログイン</router-link></li>
<li><router-link to="/user">ユーザー情報</router-link></li>
<li @click="logout">ログアウト</li>
</ul>
<hr>
</div>
</template>
<script>
export default {
methods: {
logout() {
axios.post('/api/logout').then(res => {
axios.defaults.headers.common['Authorization'] = '';
state.isLogin = false;
this.$router.push({path: '/login'}); // ログアウト後のルーティング
});
}
}
}
</script>
resources/js/components/pages/Login.vue
<template>
<div>
<p v-show="isError">認証に失敗しました。</p>
<form @submit.prevent="login">
<h1>ログイン</h1>
メールアドレス: <input type="email" v-model="email">
パスワード: <input type="password" v-model="password">
<button type="submit" class="btn btn-primary">ログイン</button>
</form>
</div>
</template>
<script>
export default {
data () {
return {
isError: false,
email: '',
password: '',
}
},
methods: {
login() {
axios.post('/api/login', {
email: this.email,
password: this.password
}).then(res => {
const token = res.data.access_token;
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
state.isLogin = true;
this.$router.push({path: '/'}); // ログイン後のルーティング
}).catch(error => {
this.isError = true;
});
}
}
}
</script>
resources/js/components/pages/User.vue
<template>
<div>
<p v-show="isError">情報の取得に失敗しました。</p>
<h1>ユーザー情報</h1>
<table>
<tr>
<th>ID</th>
<td>{{ user.id }}</td>
</tr>
<tr>
<th>ユーザー名</th>
<td>{{ user.name }}</td>
</tr>
<tr>
<th>メール</th>
<td>{{ user.email }}</td>
</tr>
<tr>
<th>登録日</th>
<td>{{ user.created_at }}</td>
</tr>
</table>
</div>
</template>
<script>
export default {
data () {
return {
isError: false,
user: {}
}
},
created() {
axios.get('/api/me').then(res => {
this.user = res.data;
}).catch(error => {
this.isError = true;
});
}
}
</script>
resources/js/components/pages/Home.vue
<template>
<div>
<h1>Home</h1>
</div>
</template>
<script>
export default {
}
</script>
2.記事投稿
Topicsテーブルに必要なファイル生成
$ php artisan make:model Topic -mr
$ php artisan make:resource Topic
database/migrations/****_**_**_******_create_topics_table.php
public function up()
{
Schema::create('topics', function (Blueprint $table) {
$table->increments('id');
$table->text('user_id'); // 外部キーカラム追加
$table->text('title');
$table->text('content');
$table->timestamps();
});
}
マイグレート
$ php artisan migrate
app/Http/Controllers/TopicController.php
namespace App\Http\Controllers;
use App\Topic;
use Illuminate\Http\Request;
use App\Http\Resources\Topic AS TopicResource;
class TopicController extends Controller
{
// 一覧表示
public function index() {
return TopicResource::collection(Topic::all());
}
// 保存
public function store(Request $request) {
$topic = new Topic;
$topic->user_id = $request->input('user_id');
$topic->title = $request->input('title');
$topic->content = $request->input('content');
$topic->save();
}
// 1データ表示
public function show(Topic $topic) {
return new TopicResource($topic);
}
// 更新
public function update(Request $request, Topic $topic) {
$topic->user_id = $request->input('user_id');
$topic->title = $request->input('title');
$topic->content = $request->input('content');
$topic->save();
}
// 削除
public function destroy(Topic $topic) {
$topic->delete();
}
}
routes/api.php
Route::resource('topics', 'TopicController');
resources/views/app.blade.php
<!--中略-->
<div id="app">
<app></app>
<navbar></navbar><!--追加-->
<router-view></router-view>
</div>
<!--中略-->
resources/js/app.js
// 中略
Vue.component('navbar', require('./components/Navbar.vue')); // 追加
// 中略
resources/js/router.js
// 中略
import List from './components/pages/List'
import Form from './components/pages/Form'
import Detail from './components/pages/Detail'
Vue.use(VueRouter);
const routes = [
// 中略
{ path: '/list', component: List, meta: { requiresAuth: true }, name: 'list' },
{ path: '/create', component: Form, meta: { requiresAuth: true }, name: 'create' },
{ path: '/:id', component: Detail, meta: { requiresAuth: true }, name: 'detail' },
];
resources/assets/js/components/pages/List.vue
<template lang="html">
<div class="container">
<div class="list-group">
<router-link v-for="( item, key, index ) in items" :key="key" :to="{ name: 'detail', params: { id: item.id } }" class="list-group-item">
{{item.title}}
<button class="btn" @click.stop.prevent="onDelete(item.id, key)">削除</button>
</router-link>
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
items: null
}
},
mounted: function() {
this.getItems();
},
methods: {
getItems: function() {
axios.get('/api/topics')
.then( (res) => {
this.items = res.data.data;
});
},
onDelete: function(id, key) {
axios.delete('/api/topics/' + id)
.then( () => {
this.$delete(this.items, key);
})
}
}
}
</script>
resources/assets/js/components/pages/Detail.vue
<template lang="html">
<div class="container">
<div class="card" v-if="item">
<div v-if="updated" class="alert alert-primary" role="alert">
更新しました
</div>
<div class="card-body">
<div v-if="!editFlg">
<h1 class="card-title">{{item.title}}</h1>
<div class="card-text">{{item.content}}</div>
</div>
<form v-else>
<div class="form-group">
<input type="text" name="title" id="TopicTitle" class="form-control" v-model="item.title">
</div>
<div class="form-group">
<textarea name="content" id="TopicContent" class="form-control" v-model="item.content"></textarea>
</div>
</form>
</div>
<div class="card-footer">
<time>{{item.date}}</time>
<button class="btn btn-light text-right" v-if="!editFlg" @click="(editFlg = true)">編集</button>
<button class="btn btn-light text-right" v-else @click="onUpdate">更新</button>
</div>
</div>
</div>
</template>
<script>
export default {
data: function( ) {
return {
item: null,
editFlg: false,
updated: false,
}
},
mounted: function() {
this.getItem();
},
methods: {
getItem: function() {
axios.get('/api/topics/' + this.$route.params.id)
.then( ( res ) => {
this.item = res.data.data;
});
},
onUpdate: function() {
axios.put('/api/topics/' + this.item.id, {
title: this.item.title,
content: this.item.content
})
.then( (res) => {
this.editFlg = false;
this.updated = true;
console.log('update')
});
}
}
}
</script>
<style lang="css">
.card-text {
white-space: pre-wrap;
}
</style>
resources/assets/js/components/pages/Form.vue
<template lang="html">
<div class="container">
<div v-if="saved" class="alert alert-primary" role="alert">
保存しました
</div>
<form>
<div class="form-group">
<label for="TopicTitle">タイトル</label>
<input type="text" class="form-control" id="TopicTitle" v-model="title">
</div>
<div class="form-group">
<label for="TopicContent">内容</label>
<textarea class="form-control" id="TopicContent" rows="3" v-model="content"></textarea>
</div>
<button type="submit" class="btn btn-primary" @click.prevent="create">登録</button>
</form>
</div>
</template>
<script>
export default {
data: function() {
return {
saved: false,
title: '',
content: '',
}
},
methods: {
create : function() {
axios.post('/api/topics', {
title: this.title,
content: this.content,
})
.then((res) => {
this.title = '';
this.content = '';
this.saved = true;
console.log('created');
});
}
}
}
</script>
resources/assets/js/components/Navbar.vue
<template lang="html">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<ul class="navbar-nav mr-auto">
<li class="nav-item"><router-link class="nav-link active" :to="{ name: 'list' }">一覧</router-link></li>
<li class="nav-item"><router-link class="nav-link active" :to="{ name: 'create' }">新規作成</router-link></li>
</ul>
</nav>
</template>
コンパイル
$ npm run dev
ここまでで、ログイン認証と記事投稿のベースデータが完成できたかと思います。ここで記事を投稿しようとしてもuser_idをpostしていないので投稿できません。
次回は、認証ユーザーで記事投稿できるようにしたいと思います。
パート2へ続く
https://qiita.com/ProgramingDai/items/dd14e02b804be1ed5516#_reference-5e1833ee9b9b59f7f546