0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-06-03

はじめに

「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

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?