In part 4 of developing the Vuetiful blog we created a basic layout for the blog’s front page, we’ll no doubt change this later, but for now the main site is set up and ready to be developed. In part 5 we are going to be setting up the admin area so we can actually add some blog posts, we will also be setting up our database and implementing Axios which we will be using with Laravel’s authentication controllers to create a login form. There’s quite a lot to get through, so let’s get started.

Creating an Admin Area

SPA maybe isn’t the correct word when it comes to developing an App like this, because essentially we will be creating two SPA’s; one for the main site and one for the admin area. There is no reason to try to jam two sites into one, both of which do not really have anything to do with each other, except that they are sharing the same API, so for our Admin area we’re essentially going to repeat what we have previously done to set up the main site.

First let’s create a new folder called “admin” in resources/assets/js then create a new file called “admin.js” – I’m calling it “admin” because I find it get’s confusing having files with the same name in the same project, but feel free to stick with naming the file “app.js” if you prefer. Now let’s create another folder called “components” in resources/assets/js/admin and create a new file called Admin.vue.

Let’s start with setting up our admin.js file by adding vue router and our main Vue instance, like so:

import Vue from 'vue';
import Admin from './components/Admin.vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const routes = [];

const router = new VueRouter({
    mode: 'history',
    routes
});

const app = new Vue({
    base: '/admin',
    router,
    render: h => h(Admin)
}).$mount('#app');

That shouldn’t need any explanation because we built a similar file for the main site, however, we have now added a “base” for our admin area called “/admin” so we don’t have to keep repeating it for every route.

Now let’s sort out our “Admin.vue” file in resource/assets/js/admin/components, once again we’re just creating a base component, the same as for the main site but with an admin specific navigation menu. Rather than bore you by duplicating the last section you can find the completed code on the GitHub page and check out part 4 if anything is unclear.

Finally we need a new .html file to serve up our Admin area, so let’s add a new file called “admin.blade.php” in resources/assets/views with the same code as before but just referencing the admin’s bundled js file, which of course we haven’t created yet, but will be output to: public/js/admin/admin.js, so make sure your admin.blade.php includes the following script tag:

<script src="/js/admin/admin.js"></script>

Bundling the Admin Area JS Code

Now we have two separate SPA’s in our project we need to make sure our admin code can be bundled, which means we want to make some adjustments to our gulpfile. I don’t want to repeat the hmr task we wrote previously, instead I would rather set the file to bundle from the command line, so lets install yargs which will allow us to pass some arguments to our gulp task:

npm install yargs --save-dev

Now in our gulpfile, let’s import that by adding the following to the top of the file:

import { argv } from 'yargs';

We’ll use an “-f” flag to set our file name and a “-d” flag to set our output destination, so let’s add the following to our gulpfile:

var entry = (argv.f !== undefined) ? argv.f : "resources/assets/js/app.js";
var dest = (argv.d !== undefined) ? argv.d : "public/js";

While that’s great, gulp likes to copy the folder structure when it outputs the file, meaning our file will get output to: public/js/admin/resources/assets/js/admin. Yuck! Let’s install gulp-flatten which will sort that out for us:

npm install gulp-flatten --save-dev

And once again, import it in our gulpfile by adding:

import flatten from 'gulp-flatten';

Now we can update update our hmr gulp task so that it can bundle any file we want:

gulp.task('hmr', () => {
    const b = browserify({
        entries: entry,
        plugin: [hmr, watchify],
        debug: true
    })

    b.on('update', bundle);
    bundle();

    function bundle() {
        b.bundle()
            .on('error', err => {
                util.log("Browserify Error", util.colors.red(err.message))
            })
            .pipe(source(entry))
            .pipe(flatten())
            .pipe(gulp.dest(dest));
    }
});

You can see the completed gulpfile on the GitHub page:

Finally let’s finish up by adding some new scripts to package json, like so:

"hmr-admin": "gulp hmr -f=resources/assets/js/admin/admin.js -d=public/js/admin",
"dev-admin": "npm-run-all --parallel hmr-admin serve"

Now when we want to develop our admin area we just need to run npm run dev-admin to fire up the server and watch for changes in our files which will automatically reload in our browser.

Adding our Admin Route

We’re now setup, but our admin area isn’t accessible yet, so we need to add our admin route to routes/web.php. To do that we just need to add our admin route before our main website route like so:

Route::get('/admin/{vue?}', function(){
    return view('admin');
});

At this stage everything should be setup, so let’s run our dev-admin script:

npm run dev-admin

And navigate to http://localhost:8000/admin and you should now see the admin page.

Setting up the Database

Laravel already has databases configured for MySQL, however, I’m going to use SQLite which is more appropriate for a small blog (feel free to use MySQL if you prefer). Luckily Laravel comes with SQLite support built in, all we have to do is create a file called “database.sqlite” in the “database” folder and change the database driver config in config/database.php to:

'default' => env('DB_CONNECTION', 'sqlite'),

I’m also not going to use env to get the website database path for the time being (I will in production), mainly because I’m writing for this blog and if you’re following along you will have to set an absolute path; what that path is, is dependent on what server and OS you are using, and I don’t really want to get bogged down with configs. If you’re familiar with setting up SQLite in Laravel then ignore this step and do what you usually do, if you just want to get it working then simply change the SQLite connection in config/database.php to:

'database' => database_path('database.sqlite'),

Now in .env we just need to setup sqlite for our migrations like so (note this DB_DATABASE path is relative so will only work for migrations):

DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=./database/database.sqlite
DB_USERNAME=root
DB_PASSWORD=null

Now all we need to do is run the default migration which will create our “users” table:

php artisan migrate

Setting Up Authentication

Now we have an admin section , we need to make sure that only authorised persons can access it. To do this we are first going to setup Laravel’s authentication for use in our SPA.

I don’t want to create a registration page because users cannot register, so I’m going to create a seeder with just me as a user, we can sort out adding additional users later. We already have our “user” table setup so to create a seeder we can do:

php artisan make:seeder UsersTableSeeder

That will crete our seeder at database/seeds/UsersTableSeeder.php, so let’s open that up and add the following to the run method:

 DB::table('users')->insert([
  'name' => "Craig",
  'email' => 'craig@example.com',
  'password' =>  bcrypt('password')
]);

Now let’s go to database/seeds/DatabaseSeeder.php and uncomment the line of code in the run() method, which is calling our seeder:

$this->call(UsersTableSeeder::class);

Now let’s run the seeder:

php artisan db:seed

Authenticating Users

Now we have a user that we can log in with, but we don’t have a login page. Let’s start by adding a login route in routes/web.php. Laravel comes with a LoginController in app/Http/Controllers/Auth so we just need to add the route to that before the other routes in routes/web.php and while we’re here we may as well add the logout route aswell:

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

That’s really all we need to do from the Laravel side, but we are going to need to send a post request, so let’s add Axois to our project which will allow us to do just that.

Setting Up Axios

In order to use Axios we just need to add the following to resources/assets/js/admin/admin.js:

import Axios from 'axios';

However, Laravel uses a csrf_token, which needs to be sent with post, put and delete requests when using “web” routes, in our case our login and logout pages, so lets add the following to make sure the csrf_token is added to each request like so:

window.axios = Axios;
axios.defaults.headers.common = {
    'X-CSRF-TOKEN': window.Laravel.csrfToken,
    'X-Requested-With': 'XMLHttpRequest'
};

That will attach the token to each request we make using axios, however, the csrfToken variable doesn’t exist yet. Laravel’s boilerplate “layout” (which we are not using) automatically sets that variable, but we haven’t added it yet, so in resources/views/admin.blade.php add the following in the header:

<script>
  window.Laravel = <?php echo json_encode([
    'csrfToken' => csrf_token(),
  ]); ?>
</script>

One final thing, the TokenMismatchException is frustrating in SPA’s because it just returns a generic “500 Internal Status Error”, so let’s fix that by catching it in Laravel’s Exception handler and returning a user friendly message we can catch and display, by adding the following to the render method in app/Exceptions/Handler.php:

if($exception instanceof \Illuminate\Session\TokenMismatchException){
    return  response()->json(['header' => 'Your session has expired.', 'message' => 'Please refresh the page and try again.'], 500);   
        }

That’s it, all our web requests will now work with Laravel’s csrf protection.

Creating the Login Component

Finally we are going to create a login component called Login.vue for our admin area in resources/assets/js/admin/components/views/:

<template>
    <div class="ui center aligned grid">
        <message :show="failed">
          <span slot="header">Authentication Failed!</span>
          <p slot="message">Invalid email or password.</p>
        </message>

        <div class="ui ten wide left aligned column">
            <h1 class="ui header">
              <i class="sign in icon"></i>
              <div class="content">
                Login
              </div>
            </h1>

            <div class="ui segment">
                <div :class="['ui', {active: loading}, 'inverted', 'dimmer']">
                    <div class="ui loader"></div>
                </div>

                <form class="ui form">
                    <div class="field">
                        <label>Email</label>
                        <input type="text" placeholder="Email" v-model="email">
                    </div>
                    <div class="field">
                        <label>Password</label>
                        <input type="password" name="last-name" placeholder="Password" v-model="password">
                    </div>
                    <button class="ui primary button" @click.prevent="login">Login</button>
                </form>
            </div>
        </div>
    </div>
</template>

<script type="text/javascript">
<template>
    <div class="ui center aligned grid">
        <message :show="error.show">
          <span slot="header">{{error.header}}</span>
          <p slot="message">{{error.message}}</p>
        </message>

        <div class="ui ten wide left aligned column">
            <h1 class="ui header">
              <i class="sign in icon"></i>
              <div class="content">
                Login
              </div>
            </h1>

            <div class="ui segment">
                <div :class="['ui', {active: loading}, 'inverted', 'dimmer']">
                    <div class="ui loader"></div>
                </div>

                <form class="ui form">
                    <div class="field">
                        <label>Email</label>
                        <input type="text" placeholder="Email" v-model="email">
                    </div>
                    <div class="field">
                        <label>Password</label>
                        <input type="password" name="last-name" placeholder="Password" v-model="password">
                    </div>
                    <button class="ui primary button" @click.prevent="login">Login</button>
                </form>
            </div>
        </div>
    </div>
</template>

<script type="text/javascript">
import Message from "@/components/core/Message.vue";
export default {
    components: {
        Message
    },
    methods: {
        login() {
            this.error.show = false;
            this.loading = true;
            axios.post('/login', {
                    email: this.email,
                    password: this.password
                })
                .then(response => {
                    this.$router.push('/');
                }).catch(error => {
                    this.error.show = true;
                    if (error.response.status === 422) {  
                        this.error.header = "Authentication Failed!",
                        this.error.message = "Invalid email or password."
                    }else{
                        this.error.header = error.response.data.header
                        this.error.message = error.response.data.message
                    }
                }).then(() => {
                    this.loading = false;
                });
        }
    },
    data() {
        return {
            email: "",
            password: "",
            loading: false,
            error: {
              show: false,
              header: '',
              message: ''
            }
        }
    }
}
</script>

Here we are simply binding the “email” and “password” data properties to our login form using v-model and using axios to send them to the “login” route via a post request. If the authentication fails, Laravel will return a 422 status which we are catching in our error callback and then showing an error message. If the authentication passes, Laravel will return a success status which will redirect us to our (not yet created) dashboard using router.push("/").

You may also notice that I have created a new component called “message”, which allows me to show the error message, but this can be used for messages site wide. I’ve also pulled in Velocity which allows me to creates a smooth transition for the error message. If you’re interested in this component then you can check it out on the projects GitHub page.

In addition I’ve pulled in babel-plugin-root-import which allows me to define the root of my project and import from there. You may notice that I’m importing with an “@”, this is something I have defined so that the babel-plugin-root-import plugin knows that I want it to apply the correct path for me, which is much easier than me trying to deal with all the relative paths. If you’re unterested in how I’ve set that up take a look at the .babelrc file on the project’s GitHub page.

Let’s now add our Login route to our router in admin.js:

import Login from './components/views/Login.vue'

const routes = [
    { path: '/login', component: Login },
]

Once that’s all set up correctly you can navigate to http://localhost:8000/admin/login and you should see the functioning login page.

We now have a login page that authenticates the user but we we don’t yet have any pages to restrict access to, so in Part 6 we will be setting up our first API Controller and implementing Laravel Passport, allowing us to restrict access to the admin area.

Advertisements