+1

[Laravel] Single Page Application sử dụng Vue, JWTAuth (P2)

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.

  1. 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
  2. Đưa module chứa trạng thái ra thành file riêng là Store để dùng chung giữa các components.
  3. 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí