Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #2: Templating & Authentication

Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #2: Templating & Authentication

Pendahuluan

Sebagai sebuah aplikasi internal antar pemilik laundry dan pengelola usaha, maka hal pertama yang akan diselesaikan adalah halaman untuk melakukan proses otentikasi agar data yang dimiliki dapat dilindungi dan aplikasi hanya dapat digunakan oleh user yang memiliki akses ke dalamnya. Seluruh rangkaian kerja akan menggunakan Vue.js untuk men-delivery content ke user melalui ajax request, sedangkan Laravel bertindak sebagai backend untuk mengolah data dan berinteraksi dengan database.

Sebelum memulai rangkaian development, pastikan pada perangkat kamu telah ter-install Node.js dimana versi yang saya gunakan adalah v11.2.0 atau gunakan yang terbaru. Instalasi Node.js biasanya sudah melibatkan npm dimana versi yang saya gunakan adalah 6.9.0. Untuk mengeceknya gunakan command node -v dan npm -v.

Baca Juga: Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #1: Schema Database

Pra Development

Development menggunakan Vue.js akan melibatkan beberapa library untuk memudahkan dan mempercepat proses development itu sendiri, tentu saja library ini belum ada di dalam Vue.js fresh install sehingga dibutuhkan beberapa langkah untuk instalasi library tersebut. Pada command line di dalam folder project Laravel yang telah kita install pada chapter #1, jalankan command:

npm install

Untuk state management, Vuex menjadi pilihan yang menarik, install library tersebut dengan command:

npm install vuex --save

Perpindahan page dari satu page ke page lainnya akan di-handle oleh Vue Router, install library-nya dengan command:

npm install vue-router

Sedangkan untuk meng-handle style kita dapat menggunakan Bootstrap Vue, dimana pada library ini telah tersedia banyak component yang siap digunakan, pada command line, install library-nya:

npm i vue bootstrap-vue bootstrap

Adapun library lainnya yang dibutuhkan adalah Axios yang berfungsi untuk melakukan HTTP request ke back-end. Install library tersebut dengan command:

npm install axios

Authentication Module

Agar lebih terstruktur, maka module pertama yang akan dikerjakan adalah modul otentikasi agar setiap user yang menggunakan aplikasi ini harus melalui halaman login terlebih dahulu. Aplikasi SPA hanya memanfaatkan 1 file view yang di-load oleh Laravel, sedangkan view selanjutnya akan di-render secara otomatis oleh Vue.js karena sesuai definisinya bahwa SPA adalah single page application. Buat sebuah controller dengan command:

php artisan make:controller FrontController

Kemudian buka file FrontController.php dan modifikasi menjadi:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FrontController extends Controller
{
    public function index()
    {
        return view('index');
    }
}

Adapun view yang di-load bernama index.blade.php, buat file tersebut di dalam folder resources/views dan tambahkan code berikut:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>DW Laundry</title>
    <!-- Tell the browser to be responsive to screen width -->
    <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
    <!-- Bootstrap 3.3.7 -->
    <link rel="stylesheet" href="{{ asset('bower_components/bootstrap/dist/css/bootstrap.min.css') }}">
    <!-- Font Awesome -->
    <link rel="stylesheet" href="{{ asset('bower_components/font-awesome/css/font-awesome.min.css') }}">
    <!-- Ionicons -->
    <link rel="stylesheet" href="{{ asset('bower_components/Ionicons/css/ionicons.min.css') }}">
    <!-- Theme style -->
    <link rel="stylesheet" href="{{ asset('assets/css/AdminLTE.min.css') }}">
    <!-- AdminLTE Skins. Choose a skin from the css/skins
        folder instead of downloading all of them to reduce the load. -->
    <link rel="stylesheet" href="{{ asset('assets/css/skins/_all-skins.min.css') }}">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
</head>
<body class="hold-transition skin-blue layout-top-nav">
  
    <div id="dw">
        <app></app>
    </div>
  
    <!-- jQuery 3 -->
    <script src="{{ asset('bower_components/jquery/dist/jquery.min.js') }}"></script>
    <!-- Bootstrap 3.3.7 -->
    <script src="{{ asset('bower_components/bootstrap/dist/js/bootstrap.min.js') }}"></script>
    <!-- SlimScroll -->
    <script src="{{ asset('bower_components/jquery-slimscroll/jquery.slimscroll.min.js') }}"></script>
    <!-- FastClick -->
    <script src="{{ asset('bower_components/fastclick/lib/fastclick.js') }}"></script>
    <!-- AdminLTE App -->
    <script src="{{ asset('assets/js/adminlte.min.js') }}"></script>
    <!-- AdminLTE for demo purposes -->
    <script src="{{ asset('assets/js/demo.js') }}"></script>
    <script src="{{ asset('js/app.js') }}"></script>
</body>
</html>

Penjelasan: Perhatikan tag <app></app>, dimana tag tersebut merupakan custom tag yang dibuat dari component Vue.js dan pastikan tag div yang mengapitnya memiliki id dw. Adapun tag lainnya hanya me-load file js dan css dari template adminLTE.

Download seluruh file css dan js (folder assets & bower_components) pada repository dw-laundry dan tempatkan file-nya ke dalam folder public.

Definisikan route-nya agar view yang telah dibuat dapat dilihat secara publik, buka file routes/web.php dan tambahkan code berikut:

Route::get('/{any}', 'FrontController@index')->where('any', '.*');

Sebelum diakses melalui browser, kita harus melengkapi component Vue.js nya agar menampilkan hasil sesuai yang diinginkan. Buka file app.js yang berada di dalam folder resources/js dan modifikasi menjadi:

import Vue from 'vue'
import router from './router.js'
import store from './store.js'
import App from './App.vue'

new Vue({
    el: '#dw',
    router,
    store,
    components: {
        App
    }
})

Ada 3 bagian yang selanjutnya harus dilengkapi sesuai dengan apa yang telah di-import pada app.js, dimana bagian tersebut adalah router.js, store.js dan App.vue. Langkah pertama, buat file router.js di dalam folder resources/js dan tambahkan code berikut:

//IMPORT SECTION
import Vue from 'vue'
import Router from 'vue-router'
import Home from './pages/Home.vue'
import Login from './pages/Login.vue'
import store from './store.js'

Vue.use(Router)

//DEFINE ROUTE
const router = new Router({
    mode: 'history',
    routes: [
        {
            path: '/',
            name: 'home',
            component: Home,
            meta: { requiresAuth: true }
        },
        {
            path: '/login',
            name: 'login',
            component: Login
        }
    ]
});

//Navigation Guards
router.beforeEach((to, from, next) => {
    if (to.matched.some(record => record.meta.requiresAuth)) {
        let auth = store.getters.isAuth
        if (!auth) {
            next({ name: 'login' })
        } else {
            next()
        }
    } else {
        next()
    }
})

export default router

Penjelasan:

  1. Tahapan pertama adalah import section dimana yang perlu diperhatikan adalah dua component lainnya yakni Home.vue yang berisi tampilan untuk dashboard dan Login.vue adalah sebuah form login, adapun kedua component ini akan dibuat selanjutnya.
  2. Tahap kedua adalah define route, dimana pada bagian ini berfungsi untuk mengontrol alur aplikasi atau bisa dikatakan cara kerjanya sama dengan fungsi routing di Laravel, perlu diperhatikan pada route dengan name home kita menambahkan meta requiresAuth yang menandakan bahwa untuk mengakses route ini dibutuhkan proses otentikasi.
  3. Tahap terakhir adalah navigation guards dimana pada bagian ini berfungsi untuk mengecek jika route tersebut membutuhkan proses otentikasi untuk mengakses page-nya, maka dibutuhkan pengecekan lebih lanjut apakah user sudah login atau belum, jika belum secara otomatis akan di-direct ke route dengan name login, apabila sebaliknya maka akan diteruskan kehalaman yang diinginkan.

Sebelum melanjutkan apa yang dibutuhkan oleh app.js, terlebih dahulu kita lengkapi component yang di-import oleh router.js, buat folder resources/js/pages, kemudian buat file Home.vue di didalam folder pages tersebut dan tambahkan code berikut.

<template>
    <div class="container">
        <section class="content-header">
            <h1>
                Top Navigation
                <small>Example 2.0</small>
            </h1>
            <ol class="breadcrumb">
                <li><a href="#"><i class="fa fa-dashboard"></i> Home</a></li>
                <li><a href="#">Layout</a></li>
                <li class="active">Top Navigation</li>
            </ol>
        </section>

        <section class="content">
            <div class="callout callout-info">
                <h4>Tip!</h4>
                <p>Add the layout-top-nav class to the body tag to get this layout. This feature can also be used with a
                    sidebar! So use this class if you want to remove the custom dropdown menus from the navbar and use regular
                    links instead.</p>
            </div>
            <div class="callout callout-danger">
                <h4>Warning!</h4>

                <p>The construction of this layout differs from the normal one. In other words, the HTML markup of the navbar
                    and the content will slightly differ than that of the normal layout.</p>
            </div>
            <div class="box box-default">
                <div class="box-header with-border">
                    <h3 class="box-title">Blank Box</h3>
                </div>
                <div class="box-body">
                    The great content goes here
                </div>
            </div>
        </section>
    </div>
</template>
<script>
    export default {
        
    }
</script>

Note: Potongan code diatas bersifat sementara, karena fitur dashboard akan dikerjakan pada artikel yang berbeda.

Selanjutnya buat file Login.vue di dalam folder resources/js/pages dan tambahkan code:

<!-- HTML SECTION -->
<template>
    <div class="container">
        <div class="login-box">
            <div class="login-logo">
                <router-link :to="{ name: 'home' }"><b>DW</b>Laundry</router-link>
            </div>
            <div class="login-box-body">
                <p class="login-box-msg">Sign in to start your session</p>

                <div class="form-group has-feedback" :class="{'has-error': errors.email}">
                    <input type="email" class="form-control" placeholder="Email" v-model="data.email">
                    <span class="glyphicon glyphicon-envelope form-control-feedback"></span>
                    <p class="text-danger" v-if="errors.email">{{ errors.email[0] }}</p>
                </div>
                <div class="form-group has-feedback" :class="{'has-error': errors.password}">
                    <input type="password" class="form-control" placeholder="Password" v-model="data.password">
                    <span class="glyphicon glyphicon-lock form-control-feedback"></span>
                    <p class="text-danger" v-if="errors.password">{{ errors.password[0] }}</p>
                </div>
                <div class="alert alert-danger" v-if="errors.invalid">{{ errors.invalid }}</div>
                <div class="row">
                    <div class="col-xs-8">
                        <div class="checkbox">
                            <label>
                                <input type="checkbox" v-model="data.remember_me"> Remember Me
                            </label>
                        </div>
                    </div>
                    <div class="col-xs-4">
                        <button type="submit" class="btn btn-primary btn-block btn-flat" @click.prevent="postLogin">Login</button>
                    </div>
                </div>

                <a href="#">I forgot my password</a><br>
            </div>
        </div>
    </div>
</template>

<!-- JAVASCRIPT SECTION -->
<script>
import { mapActions, mapMutations, mapGetters, mapState } from 'vuex';
export default {
    data() {
        return {
            data: {
                email: '',
                password: '',
                remember_me: false
            }
        }
    },
    //SEBELUM COMPONENT DI-RENDER
    created() {
        //KITA MELAKUKAN PENGECEKAN JIKA SUDAH LOGIN DIMANA VALUE isAuth BERNILAI TRUE
        if (this.isAuth) {
            //MAKA DI-DIRECT KE ROUTE DENGAN NAME home
            this.$router.push({ name: 'home' })
        }
    },
    computed: {
        ...mapGetters(['isAuth']), //MENGAMBIL GETTERS isAuth DARI VUEX
      	...mapState(['errors'])
    },
    methods: {
        ...mapActions('auth', ['submit']), //MENGISIASI FUNGSI submit() DARI VUEX AGAR DAPAT DIGUNAKAN PADA COMPONENT TERKAIT. submit() BERASAL DARI ACTION PADA FOLDER STORES/auth.js
        ...mapMutations(['CLEAR_ERRORS']),
      
      	//KETIKA TOMBOL LOGIN DITEKAN, MAKA AKAN MEMINCU METHODS postLogin()
        postLogin() {
            //DIMANA TOMBOL INI AKAN MENJALANKAN FUNGSI submit() DENGAN MENGIRIMKAN DATA YANG DIBUTUHKAN
            this.submit(this.data).then(() => {
                //KEMUDIAN DI CEK VALUE DARI isAuth
                //APABILA BERNILAI TRUE
                if (this.isAuth) {
                    this.CLEAR_ERRORS()
                    //MAKA AKAN DI-DIRECT KE ROUTE DENGAN NAME home
                    this.$router.push({ name: 'home' })
                }
            })
        }
    }
}
</script>

Penjelasan: Pada bagian HTML section hanya berisi tag html dimana terdapat dua inputan yakni email dan password dimana masing-masing form input-an tersebut memiliki v-model yang merujuk ke property data() dari Vue.js. Pada bagian Javascript section, berisi beberapa logic yang bisa kamu baca pada komentar code diatas.

Berpindah pada bagian selanjutnya, dimana router.js membutuhkan sebuah file bernama store.js, hal ini serupa dengan yang dibutuhkan oleh app.js. Buat file store.js di dalam folder resources/js dan tambahkan potongan code berikut:

import Vue from 'vue'
import Vuex from 'vuex'

//IMPORT MODULE SECTION
import auth from './stores/auth.js'

Vue.use(Vuex)

//DEFINE ROOT STORE VUEX
const store = new Vuex.Store({
    //SEMUA MODULE YANG DIBUAT AKAN DITEPATKAN DIDALAM BAGIAN INI DAN DIPISAHKAN DENGAN KOMA UNTUK SETIAP MODULE-NYA
    modules: {
        auth
    },
  	//STATE HAMPIR SERUPA DENGAN PROPERTY DATA DARI COMPONENT HANYA SAJA DAPAT DIGUNAKAN SECARA GLOBAL
    state: {
        //VARIABLE TOKEN MENGAMBIL VALUE DARI LOCAL STORAGE token
        token: localStorage.getItem('token'),
        errors: []
    },
    getters: {
        //KITA MEMBUAT SEBUAH GETTERS DENGAN NAMA isAuth
        //DIMANA GETTERS INI AKAN BERNILAI TRUE/FALSE DENGAN KONDISI BERDASARKAN
        //STATE token.
        isAuth: state => {
            return state.token != "null" && state.token != null
        }
    },
    mutations: {
        //SEBUAH MUTATIONS YANG BERFUNGSI UNTUK MEMANIPULASI VALUE DARI STATE token
        SET_TOKEN(state, payload) {
            state.token = payload
        },
        SET_ERRORS(state, payload) {
            state.errors = payload
        },
        CLEAR_ERRORS(state) {
            state.errors = []
        }
    }
})

export default store

Penjelasan: Ada dua bagian yang perlu diperhatikan, bagian pertama ada import module section dimana metode ini kita lakukan untuk meminimalisir menumpuknya sebuah code didalam file yang sama sehingga kita akan menggunakan sistem modules dari Vuex. Management state dari proses otentikasi akan kita tempatkan didalam file auth.js dan adapun file-nya akan dibuat setelah penjelasan ini. Bagian selanjutnya adalah define root store vuex, dimana pada bagian ini kita akan mendefinisikan root state yang kategorinya bersifat umum, dalam hal ini adalah token dimana token akan digunakan pada semua bagian yang membutuhkan otentikasi. Adapun penjelasan dari codingannya, silahkan baca pada komentar yang telah disematkan.

Tugas selanjutnya adalah memenuhi kebutuhan dari store.js dimana file tersebut memerlukan sebuah file bernama auth.js, buat file auth.js di dalam folder resources/js/stores dan tambahkan code berikut:

import $axios from '../api.js'

const state = () => ({

})

const mutations = {
    
}

const actions = {
    submit({ commit }, payload) {
        localStorage.setItem('token', null) //RESET LOCAL STORAGE MENJADI NULL
        commit('SET_TOKEN', null, { root: true }) //RESET STATE TOKEN MENJADI NULL
        //KARENA MUTATIONS SET_TOKEN BERADA PADA ROOT STORES, MAKA DITAMBAHKAN PARAMETER
        //{ root: true }
      
        //KITA MENGGUNAKAN PROMISE AGAR FUNGSI SELANJUTNYA BERJALAN KETIKA FUNGSI INI SELESAI
        return new Promise((resolve, reject) => {
            //MENGIRIM REQUEST KE SERVER DENGAN URI /login 
            //DAN PAYLOAD ADALAH DATA YANG DIKIRIMKAN DARI COMPONENT LOGIN.VUE
            $axios.post('/login', payload)
            .then((response) => {
                //KEMUDIAN JIKA RESPONNYA SUKSES
                if (response.data.status == 'success') {
                    //MAKA LOCAL STORAGE DAN STATE TOKEN AKAN DISET MENGGUNAKAN
                    //API DARI SERVER RESPONSE
                    localStorage.setItem('token', response.data.data)
                    commit('SET_TOKEN', response.data.data, { root: true })
                } else {
                    commit('SET_ERRORS', { invalid: 'Email/Password Salah' }, { root: true })
                }
                //JANGAN LUPA UNTUK MELAKUKAN RESOLVE AGAR DIANGGAP SELESAI
                resolve(response.data)
            })
            .catch((error) => {
                if (error.response.status == 422) {
                    commit('SET_ERRORS', error.response.data.errors, { root: true })
                }
            })
        })
    }
}

export default {
    namespaced: true,
    state,
    actions,
    mutations
}

Penjelasan: Pada bagian ini kita hanya membutuhkan sebuah actions dimana didalamnya terdapat fungsi bernama submit() yang digunakan pada component Login.vue.

File diatas dan semua file Vuex selanjutnya akan membutuhkan file lainnya yang bernama api.js dimana file ini berisi konfigurasi global dari axios. Buat file tersebut di dalam folder resources/js dan tambahkan code:

import axios from 'axios';

const $axios = axios.create({
    baseURL: '/api',
    headers: {
        Authorization: localStorage.getItem('token') != 'null' ? 'Bearer ' + JSON.stringify(localStorage.getItem('token')):'',
        'Content-Type': 'application/json'
    }
});

export default $axios;

Penjelasan: Konfigurasi global-nya adalah dimana sebuah url yang digunakan untuk melakukan request memiliki prefix /api, contohnya /api/login. Selanjutnya pada bagian headers akan mengirimkan Authorization berdasarkan token yang didapatkan dari local storage dan dengan content type json.

Hal terakhir yang dibutuhkan oleh app.js adalah file dengan nama App.vue dimana pada file ini menjadi master layout. Buat file tersebut di dalam folder resources/js dan tambahkan code:

<template>
    <div class="wrapper">
        <app-header v-if="isAuth" />

        <div class="content-wrapper">
            <router-view></router-view>
        </div>
        <app-footer v-if="isAuth" />
    </div>
</template>
<script>
    import { mapState, mapGetters } from 'vuex'
    import Header from './components/Header.vue'
    import Footer from './components/Footer.vue'
    export default {
        computed: {
            ...mapState(['token']),
            ...mapGetters(['isAuth'])
        },
        components: {
            'app-header': Header,
            'app-footer': Footer
        }
    }
</script>

Penjelasan: app-header dan app-footer adalah custom tag dari component Header.vue dan Footer.vue, dimana kedua tag ini akan di-render apabila isAuth bernilai true. Adapun tag router-view adalah tag yang berasal dari vue router dimana html yang akan di-render berdasarkan route yang sedang diakses.

Selanjutnya buat file Header.vue didalam folder resources/js/components dan tambahkan code:

<template>
    <header class="main-header">
        <nav class="navbar navbar-static-top">
            <div class="container">
                <div class="navbar-header">
                    <router-link to="/" class="navbar-brand"><b>DW</b>Laundry</router-link>
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse">
                        <i class="fa fa-bars"></i>
                    </button>
                </div>

                <div class="collapse navbar-collapse pull-left" id="navbar-collapse">
                    <ul class="nav navbar-nav">
                        <li class="active"><router-link to="/">Home <span class="sr-only">(current)</span></router-link></li>
                        <li><a href="#">Link</a></li>
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">Dropdown <span class="caret"></span></a>
                            <ul class="dropdown-menu" role="menu">
                                <li><a href="#">Action</a></li>
                                <li><a href="#">Another action</a></li>
                                <li><a href="#">Something else here</a></li>
                                <li class="divider"></li>
                                <li><a href="#">Separated link</a></li>
                                <li class="divider"></li>
                                <li><a href="#">One more separated link</a></li>
                            </ul>
                        </li>
                    </ul>
                    <form class="navbar-form navbar-left" role="search">
                        <div class="form-group">
                            <input type="text" class="form-control" id="navbar-search-input" placeholder="Search">
                        </div>
                    </form>
                </div>
                <div class="navbar-custom-menu">
                    <ul class="nav navbar-nav">
                        <li class="dropdown messages-menu">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                                <i class="fa fa-envelope-o"></i>
                                <span class="label label-success">4</span>
                            </a>
                            <ul class="dropdown-menu">
                                <li class="header">You have 4 messages</li>
                                <li>
                                    <ul class="menu">
                                        <li>
                                            <a href="#">
                                                <div class="pull-left">
                                                    <img src="https://via.placeholder.com/160" class="img-circle" alt="User Image">
                                                </div>
                                                <h4>
                                                    Support Team
                                                    <small><i class="fa fa-clock-o"></i> 5 mins</small>
                                                </h4>
                                                <p>Why not buy a new awesome theme?</p>
                                            </a>
                                        </li>
                                    </ul>
                                </li>
                                <li class="footer"><a href="#">See All Messages</a></li>
                            </ul>
                        </li>
                        <li class="dropdown notifications-menu">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                                <i class="fa fa-bell-o"></i>
                                <span class="label label-warning">10</span>
                            </a>
                            <ul class="dropdown-menu">
                                <li class="header">You have 10 notifications</li>
                                <li>
                                    <ul class="menu">
                                        <li>
                                            <a href="#">
                                                <i class="fa fa-users text-aqua"></i> 5 new members joined today
                                            </a>
                                        </li>
                                    </ul>
                                </li>
                                <li class="footer"><a href="#">View all</a></li>
                            </ul>
                        </li>
                        <li class="dropdown tasks-menu">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                                <i class="fa fa-flag-o"></i>
                                <span class="label label-danger">9</span>
                            </a>
                            <ul class="dropdown-menu">
                                <li class="header">You have 9 tasks</li>
                                <li>
                                    <ul class="menu">
                                        <li>
                                            <a href="#">
                                                <h3>
                                                    Design some buttons
                                                    <small class="pull-right">20%</small>
                                                </h3>
                                                <div class="progress xs">
                                                    <div class="progress-bar progress-bar-aqua" style="width: 20%" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100">
                                                        <span class="sr-only">20% Complete</span>
                                                    </div>
                                                </div>
                                            </a>
                                        </li>
                                    </ul>
                                </li>
                                <li class="footer">
                                    <a href="#">View all tasks</a>
                                </li>
                            </ul>
                        </li>
                        <li class="dropdown user user-menu">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                                <img src="https://via.placeholder.com/160" class="user-image" alt="User Image">
                                <span class="hidden-xs">Alexander Pierce</span>
                            </a>
                            <ul class="dropdown-menu">
                                <li class="user-header">
                                    <img src="https://via.placeholder.com/160" class="img-circle" alt="User Image">
                                    <p>Alexander Pierce - Web Developer <small>Member since Nov. 2012</small></p>
                                </li>
                                <li class="user-body">
                                    <div class="row">
                                        <div class="col-xs-4 text-center">
                                            <a href="#">Followers</a>
                                        </div>
                                        <div class="col-xs-4 text-center">
                                            <a href="#">Sales</a>
                                        </div>
                                        <div class="col-xs-4 text-center">
                                            <a href="#">Friends</a>
                                        </div>
                                    </div>
                                </li>
                                <li class="user-footer">
                                    <div class="pull-left">
                                        <a href="#" class="btn btn-default btn-flat">Profile</a>
                                    </div>
                                    <div class="pull-right">
                                        <a href="#" class="btn btn-default btn-flat">Sign out</a>
                                    </div>
                                </li>
                            </ul>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
</template>

<script>
export default {
    
}
</script>

Note: Hanya HTML tag header dari template adminLTE yang akan dimodifikasi kemudian sesuai dengan kebutuhan.

Kemudian buat file Footer.vue didalam folder yang sama dan tambahkan code:

<template>
    <footer class="main-footer">
        <div class="container">
            <div class="pull-right hidden-xs">
                <b>Version</b> 2.4.0
            </div>
            <strong>Copyright &copy; 2014-2016 <a href="https://adminlte.io">Almsaeed Studio</a>.</strong> All rights
            reserved.
        </div>
    </footer>
</template>

<script>
export default {
    
}
</script>

API Login

Tentu saja API menjadi bagian penting bagi sebuah aplikasi SPA karena dari sisi client hanya mengirimkan request yang kemudian akan diproses dari sisi backend (API - Laravel). Pada serial kali ini kita membutuhkan sebuah API untuk memproses permintaan user ketika melakukan login. Buka file LoginController.php yang berada di dalam folder /Controllers/Auth, kemudian tambahkan method berikut:

public function login(Request $request)
{
    //VALIDASI EMAIL DAN PASSWORD YANG DIKIRIMKAN
    $this->validate($request, [
        'email' => 'required|email|exists:users,email',
        'password' => 'required'
    ]);

    //ADA 3 VALUE YANG DIKIRIMKAN, YAKNI: EMAIL, PASSWORD DAN REMEMBER_ME
    //AMBIL SEMUA REQUEST TERSEBUT KECUALI REMEMBER ME KARENA YANG DIBUTUHKAN 
    //UNTUK OTENTIKASI ADALAH EMAIL DAN PASSWORD
    $auth = $request->except(['remember_me']);
    
    //MELAKUKAN PROSES OTENTIKASI
    if (auth()->attempt($auth, $request->remember_me)) {
        //APABILA BERHASIL, GENERATE API_TOKEN MENGGUNAKAN STRING RANDOM
        auth()->user()->update(['api_token' => Str::random(40)]);
        //KEMUDIAN KIRIM RESPONSENYA KE CLIENT UNTUK DIPROSES LEBIH LANJUT
        return response()->json(['status' => 'success', 'data' => auth()->user()->api_token], 200);
    }
    //APABILA GAGAL, KIRIM RESPONSE LAGI KE BAHWA PERMINTAAN GAGAL
    return response()->json(['status' => 'failed']);
}

Jangan lupa untuk menambahkan use statement pada file yang sama:

use Illuminate\Http\Request;
use Illuminate\Support\Str;

Definisikan route API dari method diatas, buka file routes/api.php kemudian tambahkan baris code:

Route::post('/login', 'Auth\LoginController@login');

Untuk melakukan ujian coba, buat file seeder untuk user dummy. Pada command line, jalankan command:

php artisan make:seeder UsersTableSeeder

Buka file seeder tersebut yang terletak didalam folder database/seeds dan modifikasi menjadi:

<?php

use Illuminate\Database\Seeder;
use App\User;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        User::create([
            'name' => 'Anugrah Sandi',
            'email' => '[email protected]',
            'email_verified_at' => now(),
            'password' => bcrypt('secret'),
            'role' => 0
        ]);
    }
}

Terakhir, buka file DatabaseSeeder.php kemudian modifikasi menjadi:

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(UsersTableSeeder::class); // TAMBAHKAN LINE INI
    }
}

Sebelum mengeksusi seeder diatas, izinkan terlebih dahulu field yang ada agar dapat menyimpan data. Buka file model app/User.php dan modifikasi menjadi:

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $guarded = [];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token', 'api_token'
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

Eksekusi seeder yang telah dibuat pada command line dengan command:

php artisan db:seed

Testing Application

Sebelum menjalankan aplikasi yang telah dibuat, pada command line jalankan command:

npm run dev

Tunggu hingga proses selesai dan jalankan command:

php artisan serve

Pada browser buka url http://localhost:8000 maka kita akan mendapatkan hasil sebagai berikut

spa login page vue laravel

Tekan tombol login tanpa mengisi kolom isian, maka hasil yang akan diproleh seperti berikut

spa login validation vue laravel

Dan yang terakhir adalah isi kolom isian sesuai email dan password yang ada di-seeder, maka kita akan di-direct ke halaman home

spa laravel vue js dashboard laundry

Baca Juga: Google Authentication With Laravel 5.8

Kesimpulan

Single page application adalah seni memanipulasi halaman agar dapat menampilkan sesuai permintaan user tanpa harus berpindah uri. Sepanjang artikel diatas kita telah banyak belajar diantaranya bagaimana templating di Vue.js, bagaimana membuat aplikasi menggunakan Laravel dan Vue.js, bagaimana menggunakan Vue router, bagaimana menggunakan Vuex sebagai management state, bagaimana berkomunikasi antara Laravel dan Vue.js dan masih banyak lagi. Apa yang kita pelajar diatas bisa menjadi modal dasar untuk mengembangkan aplikasi ini kedepannya.

Adapun bagi kamu yang ingin melihat dokumentasi code-nya dapat diakses di Github.

Update Error

Beberapa teman teman pembaca mengalami kendala tidak bisa login, termasuk mereka yang melakukan clone secara langsung di-repository paling update. Hal ini di sebabkan karena pada model user kita membuat Mutators untuk men-encrypt password dan pada seeder juga terdapat fungsi encrypt. Untuk mengatasi error tersebut cukup menghapus fungsi encrypt pada seeder. Perubahannya bisa kamu lihat disini.

Category:
Share:

Comments