【チートシート版】Laravel+SPA+JTWAuthで認証ありの投稿アプリをつくる ~パート1~

# はじめに
「Laravel+SPA+JTWAuthで認証ありの投稿アプリをつくる ~パート1~」のチートシート作成しました。
こちらはコードのみの紹介で一切説明文を入れていません。
説明をみながら作りたい方はこちらからお願いします。
https://qiita.com/ProgramingDai/items/403ee4fbc0971827f160

# チートシート
1.JTWAuthログイン

```console:Laravel認証の設定
$ php artisan make:auth
$ php artisan migrate
```

```console: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まで

```php: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 [];
    }
}
```

```php:config/auth.php
// 中略

'defaults' => [
    'guard' => 'api', // 変更
    'passwords' => 'users',
],
 
// 中略
 
'guards' => [
    // コメントアウト or トル
    // 'web' => [
    //     'driver' => 'session',
    //     'provider' => 'users',
    // ],
    'api' => [
        'driver' => 'jwt', // 変更
        'provider' => 'users',
    ],
],

// 中略
```

```console:AuthController作成
$ php artisan make:controller AuthController
```

```php: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
        ]);
    }
}
```

```php:routes/api.php
Route::post('/login', 'AuthController@login');
 
Route::group(['middleware' => 'auth:api'], function () {
    Route::get('/me', 'AuthController@me');
    Route::post('/logout', 'AuthController@logout');
});
```

```html: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>
```

```php:routes/web.php
// コメントアウト or トル
// Route::get('/', function () {
//     return view('welcome');
// });

// Auth::routes();

Route::any('{all}', function () {
    return view('app');
})->where(['all' => '.*']);
```

```console:VueRouterインストール
$ npm install
$ npm install vue-router
$ npm run dev
もしくは
$ npm run watch-poll
```

```js:resources/js/store.js
export default {
    state: {
        isLogin: false
    }
}
```

```js: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');
```

```js: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;
```

```js: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>
```

```js: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>
```

```js: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>
```

```js:resources/js/components/pages/Home.vue
<template>
    <div>
        <h1>Home</h1>
    </div>
</template>
 
<script>
    export default {

    }
</script>
```


2.記事投稿

```console:Topicsテーブルに必要なファイル生成
$ php artisan make:model Topic -mr
$ php artisan make:resource Topic
```

```php: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();
    });
}
```

```console:マイグレート
$ 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();
    }

    // １データ表示
    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();
    }
}
```

```php:routes/api.php
Route::resource('topics', 'TopicController');
```

```html:resources/views/app.blade.php
<!--中略-->
<div id="app">
   <app></app>
   <navbar></navbar><!--追加-->
   <router-view></router-view>
</div>
<!--中略-->
```

```js:resources/js/app.js
// 中略
Vue.component('navbar', require('./components/Navbar.vue')); // 追加
// 中略
```

```js: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' },
];
```

```js: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>
```

```js: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>
```

```js: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>
```

```js: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>
```

```console:コンパイル
$ npm run dev
```

ここまでで、ログイン認証と記事投稿のベースデータが完成できたかと思います。ここで記事を投稿しようとしてもuser_idをpostしていないので投稿できません。
次回は、認証ユーザーで記事投稿できるようにしたいと思います。

パート2へ続く
https://qiita.com/ProgramingDai/items/dd14e02b804be1ed5516#_reference-5e1833ee9b9b59f7f546