paint-brush
How to add a realtime audit trail to your Laravel projectby@shalvah
924 reads
924 reads

How to add a realtime audit trail to your Laravel project

by Shalvah AdebayoJune 6th, 2018
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

An audit trail is a record of all changes made to a data model. A model is any entity that can be stored in a database, such as a user or a product. An audit entry typically contains information about the type of change (creation, update, or deletion), <em>who</em> made the change, and <em>when</em> the change was made. Audit trails are often used in large applications where there is need to track changes to one or more models over time.

People Mentioned

Mention Thumbnail

Company Mentioned

Mention Thumbnail
featured image - How to add a realtime audit trail to your Laravel project
Shalvah Adebayo HackerNoon profile picture

An audit trail is a record of all changes made to a data model. A model is any entity that can be stored in a database, such as a user or a product. An audit entry typically contains information about the type of change (creation, update, or deletion), who made the change, and when the change was made. Audit trails are often used in large applications where there is need to track changes to one or more models over time.

In this tutorial, we’ll set up an audit trail dashboard accessible to admins for a simple stock application. Our dashboard will update in realtime, allowing us to see updates as they happen. Here’s a preview of our app in action:

Prerequisites

  1. PHP 7.1.3 or higher
  2. Composer
  3. A Pusher account. Create one here.

Setup

We’ll start out with a small stock management application I built. You can clone the project from GitHub by running:

git clone https://github.com/shalvah/stockt.git

You can also download the source directly from this link.

Then cd into the project folder and install dependencies:

composer install

Next, copy the .env.example to a new file called .env. Run the following command to generate an application encryption key:

php artisan key:generate

Lastly, create a file called database.sqlite in the database directory and run the following command to set up and populate the database:

php artisan migrate --seed

Creating the audit dashboard

We’ll use the laravel-auditing package to handle auditing. Let’s install the package:

composer require owen-it/laravel-auditing

Next, we’ll publish the database migrations for the audit tables and run them:

php artisan auditing:installphp artisan migrate

We’re going to be auditing changes to products. Let’s configure our product model so the auditing package can track it. In your Product model (app/Models/Product.php):

  • use the OwenIt\Auditing\Auditable trait
  • implement the OwenIt\Auditing\Contracts\Auditable interface:

<?php

namespace App\\Models;  
  
use Illuminate\\Database\\Eloquent\\Model;  
use OwenIt\\Auditing\\Contracts\\Auditable;  
  
class Product extends Model implements Auditable  
{  
  use \\OwenIt\\Auditing\\Auditable;  
  
  // ...  
}

Now, whenever a change is made to a product, the details of the change will be recorded in the audits table.

We need to make a small change to our auditing configuration so the Audit model can properly map to our User model. This will enable us to write code like $audit→user→name to retrieve the name of the user who made the change. In the file config/audit.php, replace the value of model in the user array with the class name of our User model (App\Models\User::class)



'user' => ['primary_key' => 'id','foreign_key' => 'user_id',

  // replace the line below  
  'model'       => App\\User::class,  
      
  // with this  
  'model'       => App\\Models\\User::class,  
\],

Now, on to the audits dashboard. First, we’ll create a middleware that allows only admin users to view the page. Create a file called AllowOnlyAdmin.php in app/Http/Middleware with the following content:

<?php

namespace App\\Http\\Middleware;  
  
use Closure;  
use Illuminate\\Support\\Facades\\Auth;  
  
class AllowOnlyAdmin  
{  
    public function handle($request, Closure $next)  
    {  
        if (Auth::user()->is\_admin) {  
            return $next($request);  
        }  
          
        abort(403);  
    }  
}

Next, add the route for the audits at the end of your routes/web.php:


Route::get('audits', 'AuditController@index')->middleware('auth', \App\Http\Middleware\AllowOnlyAdmin::class);

Let’s create the controller. We’ll generate the file app/Http/Controllers/AuditController.php by running:

php artisan make:controller AuditController

Create an index method within the AuditController class with the following content:






public function index(){$audits = \OwenIt\Auditing\Models\Audit::with('user')->orderBy('created_at', 'desc')->get();return view('audits', ['audits' => $audits]);}

Let’s build the view for our audits. Create the file resources/views/audits.blade.php with the following content:

@extends('layouts.app')

[@section](http://twitter.com/section "Twitter profile for @section")('content')  
  <div class="container">  
    <table class="table" >  
      <thead class="thead-dark">  
        <tr>  
          <th scope="col">Model</th>  
          <th scope="col">Action</th>  
          <th scope="col">User</th>  
          <th scope="col">Time</th>  
          <th scope="col">Old Values</th>  
          <th scope="col">New Values</th>  
        </tr>  
      </thead>  
      <tbody id="audits">  
        [@foreach](http://twitter.com/foreach "Twitter profile for @foreach")($audits as $audit)  
          <tr>  
            <td>{{ $audit->auditable\_type }} (id: {{ $audit->auditable\_id }})</td>  
            <td>{{ $audit->event }}</td>  
            <td>{{ $audit->user->name }}</td>  
            <td>{{ $audit->created\_at }}</td>  
            <td>  
              <table class="table">  
                [@foreach](http://twitter.com/foreach "Twitter profile for @foreach")($audit->old\_values as $attribute => $value)  
                  <tr>  
                    <td><b>{{ $attribute }}</b></td>  
                    <td>{{ $value }}</td>  
                  </tr>  
                [@endforeach](http://twitter.com/endforeach "Twitter profile for @endforeach")  
              </table>  
            </td>  
            <td>  
              <table class="table">  
                [@foreach](http://twitter.com/foreach "Twitter profile for @foreach")($audit->new\_values as $attribute => $value)  
                  <tr>  
                    <td><b>{{ $attribute }}</b></td>  
                    <td>{{ $value }}</td>  
                  </tr>  
                [@endforeach](http://twitter.com/endforeach "Twitter profile for @endforeach")  
              </table>  
            </td>  
          </tr>  
        [@endforeach](http://twitter.com/endforeach "Twitter profile for @endforeach")  
      </tbody>  
    </table>  
  
  </div>  
[@endsection](http://twitter.com/endsection "Twitter profile for @endsection")

You can start your app by running:

php artisan serve

Then visit your app on http://localhost:8000. The stockt app comes with two default users: an admin user (Administrator, [email protected]), and a regular user (John Doe, [email protected]). (Both passwords: secret) Sign in to your app as John Doe and as Administrator and make changes to some of the products displayed on the homepage. Then visit http://localhost:8000/audits as Administrator to see the list of all changes made by all users.

Displaying new audits in realtime

Now we’ve got our audit dashboard working, but we need to reload the page whenever we wish to see any new changes. This is where our realtime functionality, powered by Pusher, comes in. Let’s implement it.

First, we’ll set up Pusher on the backend. Install the Pusher Laravel package:

composer require pusher/pusher-http-laravel php artisan vendor:publish --provider="Pusher\Laravel\PusherServiceProvider"

Edit your config/pusher.php so it looks like this:













'connections' => ['main' => ['auth_key' => env('PUSHER_APP_KEY'),'secret' => env('PUSHER_APP_SECRET'),'app_id' => env('PUSHER_APP_ID'),'options' => ['cluster' => env('PUSHER_APP_CLUSTER'),],'host' => null,'port' => null,'timeout' => null,],],

Sign in to your Pusher dashboard and create a new app. Copy your app credentials from the App Keys section and add them to your .env file:




PUSHER_APP_ID=your-app-idPUSHER_APP_KEY=your-app-keyPUSHER_APP_SECRET=your-app-secretPUSHER_APP_CLUSTER=your-app-cluster

Note: Laravel sometimes caches old configuration, so for the project to see your new configuration values, you might need to run the command _php artisan config:clear_

The laravel-auditing package fires an event called Audited whenever a new audit is created. We’ll listen for this event and trigger a new-audit event on Pusher. Our frontend will listen for this event and add the new audit item to the table.

Create the event listener, app/Listeners/AuditedListener.php with the following content:

<?php

namespace App\\Listeners;  
  
use OwenIt\\Auditing\\Events\\Audited;  
use Pusher\\Laravel\\Facades\\Pusher;  
  
class AuditedListener  
{  
  public function handle(Audited $event)  
  {  
    $audit = $event->audit->toArray();  
    $audit\['user\_name'\] = $event->audit->user->name;  
    Pusher::trigger('audits', 'new-audit', \['audit' => $audit\]);  
  }  
}

Next, we’ll register the event listener in the app/Providers/EventServiceProvider.php:







class EventServiceProvider extends ServiceProvider{protected $listen = [\OwenIt\Auditing\Events\Audited::class => [\App\Listeners\AuditedListener::class]];

  // ...  
}

Here’s the code we’ll use to handle the event. We pull in the pusher-js library, subscribe to the audits channel and bind to the new-audit event. When an event comes in, we build up a new row and insert it at the top of the table. Add the code to the end of your resources/views/audits.blade.php:












<script src="https://js.pusher.com/4.2/pusher.min.js"></script><script>var socket = new Pusher("your-app-key", {cluster: 'your-app-cluster',});socket.subscribe('audits').bind('new-audit', function (data) {var audit = data.audit;var $modelCell = $('<td>').text(audit.auditable_type + '(id: ' + audit.auditable_id + ')');var $eventCell = $('<td>').text(audit.event);var $userCell = $('<td>').text(audit.user_name);var $timeCell = $('<td>').text(audit.created_at);

          function createSubTable(values) {  
            var $table = $('<table>').addClass('table');  
              for (attribute in values) {  
                $table.append(  
                  $('<tr>').append(  
                    $('<td>').text(attribute),  
                    $('<td>').text(values\[attribute\])  
                  )  
                );  
              }  
              return $table;  
          }  
  
          var $oldValuesTable = createSubTable(audit.old\_values)  
          var $newValuesTable = createSubTable(audit.new\_values)  
  
          var $oldValuesCell = $('<td>').append($oldValuesTable);  
          var $newValuesCell = $('<td>').append($newValuesTable);  
  
          $newRow = $('<tr>').append(  
            $modelCell,  
            $eventCell,  
            $userCell,  
            $timeCell,  
            $oldValuesCell,  
            $newValuesCell  
          );  
          $('#audits').prepend($newRow);  
      });  
</script>

Replace your-app-key and your-app-cluster with your Pusher app key and cluster, and we’re done!

Let’s test the app. Start your app as described earlier. Sign in as John Doe in one browser and Administrator in another so you can maintain concurrent sessions. Try making changes to some products as John Doe while viewing the dashboard as Administrator. The changes should show up on the dashboard in realtime.

Conclusion

In this article, we’ve added an audit dashboard to an existing application. We’ve gone ahead to add realtime functionality by displaying audits on the dashboard as they happen. Thanks to Laravel and Pusher, we were able to achieve these with minimal stress. You can check out the source code of the completed application on GitHub.

Originally published on Pusher’s blog.