Laravel User Authentication with Ajax Validation

This post documents how to add Ajax form validation to Laravel User Authentication that is generated by the Artisan console.

Requirements

An instance of Laravel 5.2 setup for local development is required. Head over to the Laravel 5.2 docs to get setup as needed. For my setup on Windows 10, I am using XAMPP 5.6.12 which meets all of the requirements. Note that your document root will need to be the laravel/public directory. Here is the virtual hosts configuration I am using for Laravel.

httpd-vhosts.conf
<VirtualHost *:8080>
    DocumentRoot "C:/xampp/htdocs/laravel/public"
    ServerName laravel.dev
    ServerAlias www.laravel.dev
    SetEnv APPLICATION_ENV development
    <Directory "C:/xampp/htdocs/laravel">
        Options Indexes MultiViews FollowSymLinks
        AllowOverride All
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

More info on setting up XAMPP for Windows

New Laravel Site

Using Composer, install Laravel 5.2 into htdocs/laravel

cd c:/xampp/htdocs

composer create-project laravel/laravel laravel "5.2.*"
Generate Authentication Scaffolding
cd laravel

php artisan make:auth

Now the Laravel site has routes and views to allow a user to register, login, logout, and reset their password. A HomeController has also been added to allow authenticated users to enter its view.

User Model

I prefer to organize models within their own directory. Therfore, let’s create a folder named Models in the app directory and move the User model into it.

mkdir app/Models

mv app/User.php app/Models

Edit app/Models/User.php, update the App namespace to App\Models.

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    ...

Throughout this tutorial you will encounter an ellipsis ... in the code examples. These are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, examine the source code.

Edit app/Http/Controllers/Auth/AuthController.php, update use App\User to use App\Models\User.

AuthController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Models\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

class AuthController extends Controller
{
    ...

Edit, config/auth.php, update the authentication drivers user provider.

'users' => [
    'driver' => 'eloquent',
    'model' => App\User::class,
],

REPLACE

'model' => App\User::class,

WITH

'model' => App\Models\User::class,

Database

Edit the .env configuration file to access the database. I have already created a database named laravel on the MySQL Server that is part of XAMPP.

.env
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

The settings above are the only ones I needed to update for my setup, I do not have a password set for MySQL Server and will just use the root account for local development. Now run php artisan migrate to create the tables for users.

Run Migration
php artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table

Validation Rules

The validation rules are currently located in the AuthController, I would like to be able to share them with the new controller we will create later for ajax validation. Since these rules apply to the User, and that model is already being used by the AuthController, it seeems to be the best place for them.

Edit app/Models/User.php, adding the public function rules() to the class.

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    /**
     * Get the validation rules that apply to the user.
     *
     * @return array
        */
    public function rules()
    {
        return [
            'name' => 'required|max:255',
            'email' => 'required|email|max:255|unique:users',
            'password' => 'required|min:6|confirmed',
        ];
    }

    ...

Edit app/Http/Controllers/Auth/AuthController.php. Locate the validator function and replace the array of rules being passed to the Validator make method with a call to the newly created rules method in our User model.

REPLACE

return Validator::make($data, [
    'name' => 'required|max:255',
    'email' => 'required|email|max:255|unique:users',
    'password' => 'required|min:6|confirmed',
]);

WITH

return Validator::make($data, (new User)->rules());

Before going further, now would be a good time to test registering a new user, login, logout etc.. to make sure everyting works. All we have done so far aside from the code that artisan generated is move the User model and move the validation rules from the AuthController into it.

User Validation Controller

Create a new Validation folder in Controllers

mkdir app/Http/Controllers/Validation

Create a new controller: app/Http/Controllers/Validation/UserController.php in the new Validation folder. It has a single method that applies the rules from the User model for validation.

UserController.php
<?php

namespace App\Http\Controllers\Validation;

use App\Models\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

use App\Http\Requests;

class UserController extends Controller
{
    public function user(Request $request)
    {
        $this->validate($request, (new User)->rules());
    }
}

Routes

Edit the app/Http/routes.php file to register a route group using api middleware. Inside this group add a validate/user route that accepts a POST method. The request is then sent to the new Validation\UserController user method for validation.

routes.php
...

Route::get('/', function () {
    return view('welcome');
});

Route::group(['middleware' => ['api']], function () {
    Route::post('/validate/user', [
        'uses' => 'Validation\UserController@user',
    ]);
});

Route::auth();

Route::get('/home', 'HomeController@index');

Register Form Template

Edit the user registration form template, resources/views/auth/register.blade.php. Remove all the blade if statements that surround the span help blocks. Each of these blocks need to be rendered by the server and made available to the javascript to display any errors. For example:

REPLACE

@if ($errors->has('name'))
    <span class="help-block">
        <strong>{{ $errors->first('name') }}</strong>
    </span>
@endif

WITH

<span class="help-block">
    <strong>{{ $errors->first('name') }}</strong>
</span>

The form should look like this after the if statements have been removed from around the span help blocks.

register.blade.php
...

<form class="form-horizontal" role="form" method="POST" action="{{ url('/register') }}">
    {{ csrf_field() }}

    <div class="form-group{{ $errors->has('name') ? ' has-error' : '' }}">
        <label class="col-md-4 control-label">Name</label>

        <div class="col-md-6">
            <input type="text" class="form-control" name="name" value="{{ old('name') }}">
            <span class="help-block">
                <strong>{{ $errors->first('name') }}</strong>
            </span>
        </div>
    </div>

    <div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}">
        <label class="col-md-4 control-label">E-Mail Address</label>

        <div class="col-md-6">
            <input type="email" class="form-control" name="email" value="{{ old('email') }}">
            <span class="help-block">
                <strong>{{ $errors->first('email') }}</strong>
            </span>
        </div>
    </div>

    <div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}">
        <label class="col-md-4 control-label">Password</label>

        <div class="col-md-6">
            <input type="password" class="form-control" name="password">
            <span class="help-block">
                <strong>{{ $errors->first('password') }}</strong>
            </span>
        </div>
    </div>

    <div class="form-group{{ $errors->has('password_confirmation') ? ' has-error' : '' }}">
        <label class="col-md-4 control-label">Confirm Password</label>

        <div class="col-md-6">
            <input type="password" class="form-control" name="password_confirmation">
            <span class="help-block">
                <strong>{{ $errors->first('password_confirmation') }}</strong>
            </span>
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-6 col-md-offset-4">
            <button type="submit" class="btn btn-primary">
                <i class="fa fa-btn fa-user"></i>Register
            </button>
        </div>
    </div>
</form>

...

JavaScript

For this tutorial, just going to inline the JavaScript to handle the Ajax at the bottom of the base layout view. Creating .js modules in the resources/assets folder and using elixer to deploy them to a public folder is a topic for another tutorial.

Edit the base layout template resources/views/layouts/app.blade.php. Near the top of the file add a meta tag to make the csrf-token available for the JavaScript ajax function.

app.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">

    ...

Instead of using a meta tag to store the token for the ajax request header, retrieve it from the hidden _token input value inside the form. This input is added using the {{ csrf_field() }} blade template helper.

At the bottom of the app.blade base layout file, before the closing body tag, add the JavaScript.

app.blade.php
    ...

    <script>
    $(function() {

        var app = {
            DOM: {},
            init: function () {

                // only applies to register form
                if (window.location.pathname == '/register') {

                    this.DOM.form = $('form');
                    this.DOM.form.name  = this.DOM.form.find('input[name="name"]');
                    this.DOM.form.email = this.DOM.form.find('input[name="email"]');
                    this.DOM.form.pwd   = this.DOM.form.find('input[name="password"]');
                    this.DOM.form.pwdc  = this.DOM.form.find('input[name="password_confirmation"]');

                    this.DOM.form.name.group = this.DOM.form.name.closest('.form-group');
                    this.DOM.form.email.group = this.DOM.form.email.closest('.form-group');
                    this.DOM.form.pwd.group = this.DOM.form.pwd.closest('.form-group');

                    this.DOM.form.submit( function(e) {
                        e.preventDefault();

                        var self = this; // native form object

                        error = {};

                        app.DOM.form.name.group.find('strong').text('');
                        app.DOM.form.email.group.find('strong').text('');
                        app.DOM.form.pwd.group.find('strong').text('');

                        app.DOM.form.name.group.removeClass('has-error');
                        app.DOM.form.email.group.removeClass('has-error');
                        app.DOM.form.pwd.group.removeClass('has-error');

                        var user = {};
                        user.name = app.DOM.form.name.val();
                        user.email = app.DOM.form.email.val();
                        user.password = app.DOM.form.pwd.val();
                        user.password_confirmation = app.DOM.form.pwdc.val();

                        var request = $.ajax({
                            headers: {
                                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
                            },
                            url: '/validate/user',
                            type: 'POST',
                            contentType: 'application/json',
                            data: JSON.stringify(user)
                        });
                        request.done( function(data)
                        {
                            // native form submit
                            self.submit();
                        });
                        request.fail( function(jqXHR)
                        {
                            error = jqXHR.responseJSON;
                            if (error.name) {
                                app.DOM.form.name.group.find('strong').text(error.name[0]);
                                app.DOM.form.name.group.addClass('has-error');
                            }
                            if (error.email) {
                                app.DOM.form.email.group.find('strong').text(error.email[0]);
                                app.DOM.form.email.group.addClass('has-error');
                            }
                            if (error.password) {
                                app.DOM.form.pwd.group.find('strong').text(error.password[0]);
                                app.DOM.form.pwd.group.addClass('has-error');
                            }

                        });

                    });
                }
            }
        }

        app.init();

    });
    </script>
</body>
</html>

In the script above, the form is assigned to the this.DOM.form jQuery object. The event.preventDefault() method is used to prevent the jQuery form from submitting or posting normally so the ajax post can take place instead. On the next line, var self = this; the native JavaScript form object this is assigned to the self variable. When the ajax request.done callback function is ready to submit the form normally after all of the validation is completed, call the native form.submit() method with self.submit(). Since this is not a jQuery form event, the event.PreventDefault handler isn’t triggered, and the form is submitted.

After registering a new user, check the users table in database to verify that the new user was added successfully.

Even though XAMPP comes with phpMyAdmin which you can use in the web browser, I prefer to use the MySQL Workbench when working with MySQL.

Source Code
comments powered by Disqus