[Laravel] Single Page Application sử dụng Vue, JWTAuth (P2)
Bài đăng này đã không được cập nhật trong 3 năm
Ở bài trước tôi đã đi đến bước tạo unit test, và phần còn lại như root component, child component, sử dụng router, axios, sử dụng JWTAuth ... sẽ được trính bày nốt trong bài này.
Vue Instances
Vue.js là hướng component nên tôi sẽ ra những component có đuôi .vue dưới đây :
- Template
- Script
- Style
Kết hợp với chúng sẽ thực hiện được việc di chuyển giữa các trang dưới client side.
Root Component
Đầu tiên sẽ phải tạo ra app.vue
làm gốc.
resources/assets/js/app.vue
<template>
<div id="app">
<div class="container">
<router-view></router-view>
</div>
<hr>
<div class="container-fluid">
<a href="https://github.com/acro5piano/laravel-vue-jwtauth-spa-todo-app" target="_blank">
<img src="https://image.flaticon.com/icons/svg/25/25231.svg" width="30" height="20">
</a>
</div>
</div>
</template>
Rồi cần tạo ra app.js
để đọc component này. Sau khi browser nhận được reponse từ server thì nó sẽ là entry point được thực thi.
resources/assets/js/app.js
import Vue from 'vue'
require('bootstrap-sass')
const app = new Vue({
el: '#app',
render: h => h(require('./app.vue')),
})
Child component + Routing
Kế đến là đi tạo những component con của root bên trên. Đầu tiên sẽ tạo từ component tĩnh là trang about us
.
resources/assets/js/components/About.vue
<template>
<div>
This page describes who we are.
</div>
</template>
Rồi thực hiện đăng kí component này với vue-router
:
resources/assets/js/router.js
import VueRouter from 'vue-router'
import Vue from 'vue'
Vue.use(VueRouter)
export default new VueRouter({
mode: 'history',
routes: [
{ path: '/about', component: require('./components/About.vue') },
],
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
},
})
Khi mà truy cập vào URL /about
thì component About.vue
sẽ được mount vào <router-view></router-view>
của app.vue
.
Ở phần mode: 'history'
tôi đang dùng push state
của HTML5
, còn mặc định của mode sẽ là hash
.
Còn phần scrollBehavior
sẽ giúp bảo lưu vị trí scroll trình duyệt.
Tôi sẽ đọc vào file router.js
từ app.js
.
resources/assets/js/app.js
import Vue from 'vue'
// Thêm vào
import router from './router'
require('bootstrap-sass')
const app = new Vue({
// Thêm vào
router,
el: '#app',
render: h => h(require('./app.vue')),
})
Nếu bạn access vào localhost:8000/about
thì sẽ được routing như bên dưới :
Layout
Tôi sẽ tạo ra trước tất cả component và những routing tương ứng cho chúng. Những routing cần thiết sẽ là :
/
/about
/login
Ở /
sẽ hiện thị task list nên sẽ cần những component con sau :
- components/Tasks.vue
- components/About.vue
- components/Login.vue
Ngoài những cái đó ra thì cũng cần navigation bar lúc nào cũng được hiển thị :
components/Navbar.vue
resources/assets/js/components/Tasks.vue
<template>
<div>
please <router-link to="/login">Login.</router-link>
<div>
<strong>Hello, HuongNV!</strong>
<p>Your tasks here.</p>
<ul>
<li>
Learn Vue.js
</li>
<button class="btn btn-sm btn-success">Done</button>
<button class="btn btn-sm btn-danger">Remove</button>
</ul>
<div class="form-group">
<div class="alert alert-danger" role="alert">
Task name should not be blank.
</div>
<input type="text" class="form-control" placeholder="new task...">
<button class="btn btn-primary">
Add task
</button>
</div>
</div>
</div>
</template>
resources/assets/js/components/About.vue
<template>
<div>
This page describes who we are.
</div>
</template>
resources/assets/js/components/Login.vue
<template>
<div>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Login</div>
<div class="panel-body">
<div class="alert alert-danger" role="alert">
Wrong email or password.
</div>
<div class="form-group">
<label for="email" class="col-md-4 control-label">E-Mail Address</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control" required autofocus>
</div>
</div>
<div class="form-group">
<label for="password" class="col-md-4 control-label">Password</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control" required autofocus>
</div>
</div>
<div class="form-group">
<div class="col-md-8 col-md-offset-4">
<button type="submit" class="btn btn-primary">
Login
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
resources/assets/js/components/Navbar.vue
<template>
<nav class="navbar navbar-default">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"
aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<router-link to="/" class="navbar-brand">Vue TODO</router-link>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li><router-link to="/about">About</router-link></li>
<li>
<router-link to="/login">Log in</router-link>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
</template>
Việc tiếp theo sẽ cần phải đăng kí những component đã tạo vào Router
:
resources/assets/js/router.js
import VueRouter from 'vue-router'
import Vue from 'vue'
Vue.use(VueRouter)
export default new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: require('./components/Tasks.vue') }, // Thêm
{ path: '/about', component: require('./components/About.vue') },
{ path: '/login', component: require('./components/Login.vue') }, // Thêm
],
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
},
})
Tiến hành đặt Navbar
. Do ko phải biến đổi trên Router
nên tôi sẽ viết ngay vào app.vue
:
resources/assets/js/app.vue
<template>
<div id="app">
<!-- Thêm -->
<navbar></navbar>
<div class="container">
<router-view></router-view>
</div>
<hr>
<div class="container-fluid">
<a href="https://github.com/acro5piano/laravel-vue-jwtauth-spa-todo-app" target="_blank">
<img src="https://image.flaticon.com/icons/svg/25/25231.svg" width="30" height="20">
</a>
</div>
</div>
</template>
<script>
// Thêm
export default {
components: {
navbar: require('./components/Navbar.vue'),
},
}
</script>
Đến đây thì việc di chuyển màn hình trong SPA đã chắc chắn hoàn thiện. Bạn có thể access vào localhost:8000/login
để thử.
Note : sẽ có alert nhưng bạn cứ bỏ qua nó.
axios
Tôi sẽ dùng axios
để gửi request và viết xử lý lấy tasks
về từ API server.
Việc dùng axios
đẩy request sẽ có một vài cách nhưng mà để không cho một vài component nào đó gọi được thì tôi sẽ tạo ra services/http.js
thực hiện việc đó.
resources/assets/js/services/http.js
import axios from 'axios'
/**
* Responsible cho tất cả HTTP requests.
*/
export default {
request (method, url, data, successCb = null, errorCb = null) {
axios.request({
url,
data,
method: method.toLowerCase()
}).then(successCb).catch(errorCb)
},
get (url, successCb = null, errorCb = null) {
return this.request('get', url, {}, successCb, errorCb)
},
post (url, data, successCb = null, errorCb = null) {
return this.request('post', url, data, successCb, errorCb)
},
put (url, data, successCb = null, errorCb = null) {
return this.request('put', url, data, successCb, errorCb)
},
delete (url, data = {}, successCb = null, errorCb = null) {
return this.request('delete', url, data, successCb, errorCb)
},
/**
* Khởi tạo service.
*/
init () {
axios.defaults.baseURL = '/api'
// Intercept the request to make sure the token is injected into the header.
axios.interceptors.request.use(config => {
config.headers['X-CSRF-TOKEN'] = window.Laravel.csrfToken
config.headers['X-Requested-With'] = 'XMLHttpRequest'
return config
})
}
}
resources/assets/js/app.js
import Vue from 'vue'
import router from './router'
import http from './services/http.js' // Thêm
require('bootstrap-sass')
const app = new Vue({
router,
el: '#app',
// Thêm
created () {
http.init()
},
render: h => h(require('./app.vue')),
}).$mount('#app')
Và ở Task
component tôi sẽ sử dụng http service này để đẩy request.
resources/assets/js/components/Tasks.vue
<template>
<div>
please <router-link to="/login">Login.</router-link>
<div>
<strong>Hello, HuongNV!</strong>
<p>Your tasks here.</p>
<ul v-for="task in tasks">
<li v-if="task.is_done">
<strike> {{ task.name }} </strike>
</li>
<li v-else>
{{ task.name }}
</li>
<button @click="completeTask(task)" class="btn btn-sm btn-success" v-if="task.is_done">Undo</button>
<button @click="completeTask(task)" class="btn btn-sm btn-success" v-else>Done</button>
<button @click="removeTask(task)" class="btn btn-sm btn-danger">Remove</button>
</ul>
<div class="form-group">
<div class="alert alert-danger" role="alert" v-if="showAlert">
{{ alertMessage }}
</div>
<input type="text" class="form-control"
v-model="name" @keyup.enter="addTask" placeholder="new task...">
<button class="btn btn-primary" disabled="disabled" v-if="name === ''">
Add task
</button>
<button class="btn btn-primary" @click='addTask' v-else>
Add task
</button>
</div>
</div>
</div>
</template>
<script>
import http from '../services/http'
export default {
mounted() {
this.fetchTasks()
},
data() {
return {
tasks: [],
name: '',
showAlert: false,
alertMessage: '',
}
},
methods: {
fetchTasks () {
// TODO: not to send request when the user is not authenticated
http.get('tasks', res => {
this.tasks = res.data
})
},
addTask () {
if (this.name === '') {
this.showAlert = true
this.alertMessage = 'Task name should not be blank.'
return false
}
http.post('tasks', {name: this.name}, res => {
this.tasks[res.data.id] = res.data
this.name = ''
this.showAlert = false
this.alertMessage = ''
})
},
completeTask (task) {
http.put('tasks/' + task.id, {is_done: !task.is_done}, res => {
this.tasks[task.id] = res.data
this.$forceUpdate()
})
},
removeTask (task) {
http.delete('tasks/' + task.id, {}, () => {
delete this.tasks[task.id]
this.$forceUpdate()
})
},
}
}
</script>
JWTAuth
Cuối cùng tôi sẽ dùng JWTAuth để cho phép user có thể login được (không có chức năng đăng kí user).
Cài đặt JWTAuth
Trước tiên là những package cần cho JWTAuth. Với Laravel thì việc này rất đơn giản, gói mà tôi sẽ dùng là jwt-auth
ở đây. Đầu tiên sẽ là chạy composer.
composer require tymon/jwt-auth
Sau đó là đăng kí từ các file setting :
config/app.php
// ...
'providers' => [
// ...
Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class,
],
// ...
'aliases' => [
// ...
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
],
];
Tạo ra file setting của JWTAuth :
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"
php artisan jwt:generate
Cuối cùng là làm cho sử dụng được trên JWTAuth Routing là kết thúc cài đặt :
app/Http/Kernel.php
protected $routeMiddleware = [
// ...
'jwt.auth' => \Tymon\JWTAuth\Middleware\GetUserFromToken::class,
'jwt.refresh' => \Tymon\JWTAuth\Middleware\RefreshToken::class,
];
}
Định nghĩa Routing dùng cho chứng thực
Tôi sẽ tạo mới một vài Routing dùng cho API mới dùng cho chứng thực sau :
/api/authenticate Dùng cho login
/api/logout Dùng cho logout
/api/tasks Thay đổi để chỉ trả về tasks của user đang login
/api/me Trả về thông tin của user đang login
routes/api.php
Route::group(['middleware' => 'api'], function () {
Route::post('authenticate', 'AuthenticateController@authenticate');
Route::group(['middleware' => 'jwt.auth'], function () {
Route::resource('tasks', 'TaskController');
Route::get('me', 'AuthenticateController@getCurrentUser');
});
});
Bằng việc thêm vào middleware jwt.auth
như trên thì ta có thể control được việc access vào SPA.
Đến thời điểm này thì khi mà truy cập vào /api/tasks
thì sẽ được trả về cho các tasks một cách ngẫu nhiên nhưng tôi sẽ thay đổi lại để chỉ có thể nhìn được những task của user đã được chứng thực.
Tạo controller dùng cho chứng thực
Controller này sẽ quản lý user login.
php artisan make:controller AuthenticateController
app/Http/Controllers/AuthenticateController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use App\User;
class AuthenticateController extends Controller
{
public function authenticate(Request $request)
{
// lấy credentials từ the request
$credentials = $request->only('email', 'password');
try {
// verify cái credential lấy được và tạo token cho user
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
// khi có ngoại lệ xảy ra
return response()->json(['error' => 'could_not_create_token'], 500);
}
$user = User::where('email', $request->email)->first();
// khi tất cả ok sẽ trả về token
return response()->json(compact('user', 'token'));
}
public function getCurrentUser()
{
$user = JWTAuth::parseToken()->authenticate();
return response()->json(compact('user'));
}
}
Chỉnh sửa Model
Tôi sẽ tạo mối quan hệ 1-n : user sẽ có nhiều tasks và task sẽ được gắn liền với user.
Đầu tiên sẽ thêm vào trường user_id
trong bảng bảng tasks
.
php artisan make:migration add_user_id_to_tasks
Tiến hành chỉnh sửa :
2017_03_18_084344_add_user_id_to_tasks.php
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddUserIdToTasks extends Migration
{
/**
* chạy migrations.
*
* @return void
*/
public function up()
{
Schema::table('tasks', function (Blueprint $table) {
$table->integer('user_id')->references('id')->on('users')->unsigned()->index()->nullable();
});
}
/**
* loại bỏ migrations.
*
* @return void
*/
public function down()
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropColumn('user_id');
});
}
}
Chạy mirgration
php artisan migrate
Với User Model tôi sẽ thêm hasMany
vào :
app/User.php
// Thêm
public function tasks()
{
return $this->hasMany(Task::class);
}
Kế tiếp là tôi đi tạo ra User Factory để sinh ra user một cách tự động
database/factories/ModelFactory.php
// ...
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'password' => bcrypt('secret'),
'remember_token' => str_random(10),
];
});
// ...
Do là một User sẽ có nhiều Tasks nên cần chỉnh lại Seeder để mapping user với task :
database/seeds/DatabaseSeeder.php
public function run()
{
factory(App\User::class, 50)->create()->each(function ($user) {
$user->tasks()->save(
factory(App\Task::class)->make()
);
});
}
Tạo dữ liệu
php artisan db:seed
Task Controller
Tôi sẽ trả về task mà thuộc về user đã chứng thực :
app/Http/Controllers/TaskController.php
public function index()
{
$user = \JWTAuth::parseToken()->authenticate();
return $user->tasks()->get()->keyBy('id');
}
public function store(Request $request)
{
$user = \JWTAuth::parseToken()->authenticate();
return $user->tasks()->create($request->only('name'))->fresh();
}
Tôi sẽ thử bằng lệnh Curl để xác nhận việc user có thể đăng nhập được. Chọn tuỳ ý một user nào đó :
>>> App\User::first()
=> App\User {#701
id: "1",
name: "Margarette Kshlerin",
email: "laura.cartwright@example.com",
created_at: "2017-10-17 13:28:22",
updated_at: "2017-10-17 13:28:22",
}
Thử login bằng user vừa chọn :
curl -XPOST localhost:8000/api/authenticate -d 'email=laura.cartwright@example.com' -d 'password=secret'
{
"user": {
"id": 1,
"name": "Margarette Kshlerin",
"email": "laura.cartwright@example.com",
"created_at": "2017-10-17 13:28:22",
"updated_at": "2017-10-17 13:28:22"
},
"token": "eyJ0eXAiOiJ******W2gvafWitgza_2H5A-g_1xS5SBkZPHde8tE"
}
Vậy là Token đã được sinh ra ok. Sau này thì sẽ verify user đã chứng thực bằng cái Token này trên các request được gửi từ Vue.js. Nhưng trước đó hãy thử xem thực sự là đã chứng thực chuẩn chỉ hay chưa ?
Tôi sẽ cho Token vào header và gửi request đến /api/tasks
.
curl -XGET localhost:8000/api/tasks -H 'Authorization: Bearer eyJ0eXAiOiJ******W2gvafWitgza_2H5A-g_1xS5SBkZPHde8tE'
{
"1": {
"id": 1,
"name": "Thora Strosin",
"is_done": false,
"created_at": "2017-10-16 22:39:49",
"updated_at": "2017-10-16 22:39:49",
"user_id": "1"
},
"2": {
# ...
},
"5": {
"id": 5,
"name": "August Denesik",
"is_done": true,
"created_at": "2017-10-16 22:39:49",
"updated_at": "2017-10-16 22:39:49"
"user_id": "5",
}
}
Vậy là chuẩn rồi nhưng hãy thử thêm trường hợp chưa được chứng thực xem sao :
curl -XGET localhost:8000/api/tasks
{"error":"token_not_provided"}
Đó là khi không có token, vậy thử tiếp token giả :
curl -XGET localhost:8000/api/tasks -H 'Authorization: Bearer hoge.fuga.piyo'
{"error":"token_invalid"}
`
Vậy là tôi đã xác nhận được chức năng login hoạt động tốt. Giờ tôi sẽ setting `axios` thêm Authorization header của chứng thực.
```PHP
resources/assets/js/services/http.js
// ...
delete (url, data = {}, successCb = null, errorCb = null) {
return this.request('delete', url, data, successCb, errorCb)
},
/**
* Khởi tạo service.
*/
init () {
axios.defaults.baseURL = '/api'
// Chặn request để chắc chắn rằng token bị injected tới header.
axios.interceptors.request.use(config => {
config.headers['X-CSRF-TOKEN'] = window.Laravel.csrfToken
config.headers['X-Requested-With'] = 'XMLHttpRequest'
config.headers['Authorization'] = `Bearer ${localStorage.getItem('jwt-token')}` // Thêm cái này
return config
})
// Thêm từ đây
// Chặn response và ...
axios.interceptors.response.use(response => {
// ... lấy token từ header hoặc dữ liệu response data nếu như tồn tại, rồi lưu nó.
const token = response.headers['Authorization'] || response.data['token']
if (token) {
localStorage.setItem('jwt-token', token)
}
return response
}, error => {
// Nếu như nhận được một Bad Request hay lỗi Unauthorized
console.log(error)
return Promise.reject(error)
})
}
// ...
Bằng việc làm như vậy thì khi mà khởi tạo services/http.js
thì sẽ lấy token
từ local storage đưa vào header, axios
nó sẽ xem response header khi mà login thành công rồi lưu token
vào local storage cho ta. Nhưng mà khi tham khảo blog dưới thì có vẻ như là về mặt an toàn thì dùng cookie sẽ tốt hơn :
https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage
Dùng Store pattern để duy trì trạng thái login
Tôi sẽ nghĩ đến việc quản lý trạng thái và data binding thông qua component trên Vue.js. Nếu mà phân chia thành component thì sẽ có rất nhiều thông tin có thể tham chiếu từ component. Ví dụ như là dựa vào trạng thái user đang login hay không mà nội dung hiển thị các component có thể thay đổi sẽ rất thường gặp. Để mà làm điều này thì có nhiều cách khác nhau.
- Mang toàn bộ thông tin trong component cha, rồi từ các component con sẽ tham chiếu bằng
this.$parent
- Đưa module chứa trạng thái ra thành file riêng là Store để dùng chung giữa các components.
- Sử dụng
vuex
Lần này tôi dùng cách thứ 2 là Store pattern sẽ hợp với quy mô như này.
Tôi sẽ tạo User Store
:
resources/assets/js/stores/userStore.js
import http from '../services/http'
export default {
debug: true,
state: {
user: {},
authenticated: false,
},
login (email, password, successCb = null, errorCb = null) {
var login_param = {email: email, password: password}
http.post('authenticate', login_param, res => {
this.state.user = res.data.user
this.state.authenticated = true
successCb()
}, error => {
errorCb()
})
},
setCurrentUser () {
http.get('me', res => {
this.state.user = res.data.user
this.state.authenticated = true
})
},
/**
* Khởi tạo store
*/
init () {
this.setCurrentUser()
}
}
Phần state:
chính là trạng thái. Các component sẽ đọc cái này, nên trạng thái được chia sẻ giữa chúng, và data binding cũng sẽ được thực hiện nên thật là tiện. Còn ở phương thức init()
sẽ xem /api/me
để lấy thông tin chính bản thân. Sau đó là sẽ đọc vào cái stores/userStore.js
này và khởi tạo nó ở component cha :
resources/assets/js/app.js
// ...
import userStore from './stores/userStore'
// ...
const app = new Vue({
router,
el: '#app',
created () {
http.init()
userStore.init()
},
render: h => h(require('./app.vue')),
})
Login
Trên Login
component tôi sẽ viết xử lý login :
resources/assets/js/components/Login.vue
<template>
<div>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Login</div>
<div class="panel-body">
<label for="email" class="col-md-4 control-label">E-Mail Address</label>
<div class="alert alert-danger" role="alert" v-if="showAlert">
{{ alertMessage }}
</div>
<div class="form-group">
<div class="col-md-6">
<input id="email" type="email" class="form-control"
v-model="email" @keyup.enter="login" required autofocus>
</div>
</div>
<label for="password" class="col-md-4 control-label">Password</label>
<div class="form-group">
<div class="col-md-6">
<input id="password" type="password" class="form-control"
v-model="password" @keyup.enter="login" required autofocus>
</div>
</div>
<div class="form-group">
<div class="col-md-8 col-md-offset-4">
<button @click="login" type="submit" class="btn btn-primary">
Login
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import userStore from '../stores/userStore'
import http from '../services/http'
export default {
mounted () {
this.fetchUsers()
},
data() {
return {
email: '',
password: '',
showAlert: false,
alertMessage: '',
}
},
methods: {
login () {
userStore.login(this.email, this.password, res => {
this.$router.push('/')
}, error => {
this.showAlert = true
this.alertMessage = 'Wrong email or password.'
})
},
}
}
</script>
Tôi đang xử lý nêú mà Enter hoặc click nút login thì sẽ gửi request đi chứng thực, nếu mà thành công thì di chuyển đến top page. Còn nêú có thất bại thì sẽ hiển thị alert. Sau đó là dựa theo có hay không login để thay đổi hiển thị của navigation. Khi mà đã login thì sẽ hiển thị tên của user đag login, còn không thì sẽ hiển thị link login.
resources/assets/js/components/Navbar.vue
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li><router-link to="/about">About</router-link></li>
<!-- Thêm từ đây -->
<li class="dropdown" v-if="userState.authenticated">
<a href="#" class="dropdown-toggle"
data-toggle="dropdown"
role="button" aria-haspopup="true" aria-expanded="false">
{{ userState.user.name }}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="#">Log out</a></li>
</ul>
</li>
<li v-else>
<router-link to="/login">Log in</router-link>
</li>
<!-- ... -->
<script>
import userStore from '../stores/userStore'
export default {
data (){
return {
userState: userStore.state
}
},
}
</script>
Xử lý logout tôi sẽ viết sau. Còn trên task list khi mà user đang logn sẽ không cho hiển thị please login
.
resources/assets/js/components/Tasks.vue
<template>
<div>
<div v-if="userState.authenticated">
<strong>Hello, {{ userState.user.name }}!</strong>
<!-- ... -->
<p v-else>
please <router-link to="/login">Login.</router-link>
</p>
</div>
</template>
<script>
import http from '../services/http'
import userStore from '../stores/userStore' // Thêm
export default {
mounted() {
this.fetchTasks()
},
data() {
return {
tasks: [],
name: '',
showAlert: false,
alertMessage: '',
userState: userStore.state, // Thêm
}
},
}
</script>
Vậy là đến đây nó sẽ như thế này, và tôi cũng đã sắp hoàn thành SPA :
Logout
Việc này chỉ là xử lý xoá đi jwt-token
đã lưu ở local storage. Và ở trên phía server thì chỉ cần access vào Routing đã được setting Middlewảe có tên là jwt.refresh
là token sẽ bị huỷ. Việc logout này sẽ thực hiện từ Navigation bar :
routes/api.php
Route::group(['middleware' => 'api'], function () {
// ...
Route::get('logout', 'AuthenticateController@logout')->middleware('jwt.refresh');
// ...
});
Thêm phương thức vào `AuthenticateController` :
```PHP
app/Http/Controllers/AuthenticateController.php
public function logout()
{
}
Kế đến là thêm xử lý logout đã viết ở trên vào userStore
:
resources/assets/js/stores/userStore.js
// Để logout thì ta chỉ cần bỏ đi token
logout (successCb = null, errorCb = null) {
http.get('logout', () => {
localStorage.removeItem('jwt-token')
this.state.authenticated = false
successCb()
}, errorCb)
},
Rồi ok, nếu mà access vào URL trên thì jwt-token
sẽ bị xoá khỏi local storage. Cuối cùng thì tôi cần viết xử lý logout vào link Log out
trên Navbar
:
resources/assets/js/components/Navbar.vue
<!-- ... -->
<ul class="dropdown-menu">
<li><a @click="logout()">Log out</a></li>
</ul>
<!-- ... -->
<script>
import userStore from '../stores/userStore'
export default {
data (){
return {
userState: userStore.state
}
},
methods: {
logout() {
userStore.logout( () => {
this.$router.push('/login')
})
}
}
}
</script>
Hoàn thành !
Tôi xin kết thúc demo một SPA sử dụng Vue, JWTAuth tại đây, xin cảm ơn đã đọc bài viết này !
Nguồn tài liệu : qiita.com
All rights reserved