Laravel Admin Routes and Security in a SPA

Part 43 of 48 in API Driven Development With Laravel and VueJS
Dan Pastori avatar
Dan Pastori August 28th, 2018
⚡️ Updated content is available We learned a lot since we originally wrote this article. We now have this updated for Laravel 10, Vue 3/NuxtJS 3, and Capacitor 3.

So right now, we have planned our security structure Planning your Laravel and VueJS SPA Application Admin Section, and we’ve implemented our first policy Laravel Gates and Policies in an API Driven SPA. By implementing the policy, we now place what we refer to as actions in the database if the user does not have the permission to directly add, update, or delete a cafe. Right now, these actions are just sitting in the database and we need a way to act on them. This retrieving the actions and approving or denying them accordingly.

Since this is huge tutorial, I’m breaking it up into two parts. The first part will discuss adding the routes and implementing another policy in combination with some middleware. The second tutorial will begin adding the UI to use these routes and an admin screen to the single page application.

Let’s just focus on getting those actions approved or denied right now!

Adding Owner Middleware

The first step of this section is to create some middleware that will block unauthorized users from accessing admin routes. In the last tutorial Laravel Gates and Policies in an API Driven SPA, we added authorization security using policies, so why are we adding middleware. Well, in the last tutorial, any authenticated user could access the create, update, or delete method for a cafe. An action just got created if they didn’t have permission to run the method.

Now with the admin routes, we need middleware because we want to block with a 403 right away if the user doesn’t have access to an admin route. We will then fine grain the security with policies after that.

Let’s get started!

You will need to create a file in your middleware directory: app/Http/Middleware/Owner.php. In that file add the following code:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class Owner
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        if (Auth::user()->permission < 1 ) {
            abort(403, 'Unauthorized action.');
        }

        return $next($request);
    }
}

What this does is check to see if the user is an owner or not. Right now it doesn’t matter what company they own, it just matters that they have the permission to be an owner. What company they own will be filtered in our policy. Any user who has a permission column less than 1 is instantly denied access and returned a 403 response.

Now remember with our user structure, an admin or super admin has a permission of 2 or 3 respectively. They can do everything, so we don’t really care if they own the cafe or not. We just want to block any users less than an owner.

Before we go any further, open up your app/Http/Kernel.php file and add the middleware to the $routeMiddleware array like so:

/**
 * The application's route middleware.
 *
 * These middleware may be assigned to groups or used individually.
 *
 * @var array
 */
protected $routeMiddleware = [
    'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'owner' => \App\Http\Middleware\Owner::class
];

Now we can use the middleware on a route or in a route grouping!

Next, we will add some the routes required for to approve or deny the actions.

Add Admin Action Routes

With any RESTful API, you will want your routes to clearly define your action. In this case, we will want a route to grab all unprocessed actions for the user, a route to approve an action, and a route to deny the action.

Since these routes have so much in common, let’s create a route group at the end of our /routes/api.php file like this:

Route::group(['prefix' => 'v1/admin', 'middleware' => ['auth:api', 'owner']], function(){

});

Let’s unpack this a little bit. First, we have a prefix key that is set to v1/admin. This will prefix all routes in this group as api/v1/admin . The additional api prefix comes from being in the api.php routes file. This makes all routes in this group super scoped and clear to the developer that they are admin routes.

The next key is the middleware key that contains an array of middleware. The first middleware auth:api ensures that any user accessing this route is authenticated via the API. This is the number one priority. We don’t want any person to just access our admin routes. The next middleware is the owner middleware which we just created. This ensures the user is at least an owner of a company before accessing these routes. Don’t worry, we will handle users who are owners, but don’t have permission on an action through a policy. We will get to that soon!

Let’s add a few routes to our group and then we will add a controller to handle these routes:

Route::group(['prefix' => 'v1/admin', 'middleware' => ['auth:api', 'owner']], function(){
  Route::get('/actions', 'API\Admin\ActionsController@getActions');
  Route::put('/actions/{id}/approve', 'API\Admin\ActionsController@putApproveAction');
  Route::put('/actions/{id}/deny', 'API\Admin\ActionsController@putDenyAction');
});

The first route will get all actions that are not processed for the authenticated user to take action on. The second route will approve a specific action and the third route will deny a specific action. Let’s make a controller to handle these!

Adding our Admin Action Controller

In the spirit of code organization, let’s make sure we keep our project organized. We will be adding our first admin controller to approve actions, so let’s create a folder for that to reside in. I created app/Http/Controllers/API/Admin.

In that folder, I added a new controller called ActionsController.php. This controller will handle all of the routes regarding approving or denying actions. For now, let’s just add the following code to the file:

<?php
/*
  Defines the namespace for the controller
*/
namespace App\Http\Controllers\API\Admin;

/*
  Uses the controller interface
*/
use App\Http\Controllers\Controller;

/*
  Defines the models used in the controller
*/
use App\Models\Company;
use App\Models\Cafe;
use App\Models\Action;

/*
  Defines the servies used in the controller
*/
use App\Services\CafeService;
use App\Services\ActionService;

/*
  Uses the Auth facade.
*/
use Auth;

/**
 * Handles the retrieval, approval, and denial of actions
 */
class ActionsController extends Controller
{

}

We have all of the models we will be using defined, along with the namespace for our controller. Notice that the namespace is App\Http\Controllers\API\Admin? It has to be since we are in the \API\Admin directory.

In the ActionsController class let’s add the following methods so we can handle our routes:

public function getActions(){

}

public function putApproveAction( Action $action ){

}

public function putDenyAction( Action $action ){

}

Now all of our routes can be handled for the administration side of the actions.

Getting Un Processed Actions For The User

I mentioned this earlier as we began adding our routes and controller that the getActions() method would return un-processed actions that the user can take action on. Why only un-processed you may ask? The reason for this is once an action is processed, it’s the equivalent of being soft deleted. You won’t be able to do anything else to the action. However, we will want to see the edit history for cafes, so we don’t hard delete it.

This is a unique route because a policy or middleware won’t help us adjust the query accordingly. If an owner accesses this route, we only want actions on the companies that are owned by the user. If an admin or super admin accesses this route we want all un-processed actions.

Our middleware blocks any user that wouldn’t fit the owner or admin permission. Let’s add the following code to the getActions() method:

/**
 * Gets all of the unprocessed actions for a user
 * URL: /api/v1/admin/actions
 * Method: GET
 */
public function getActions(){
    /*
      If the user is an admin grab all of the actions that haven't been
      processed.
    */
    if( Auth::user()->permission >= 2 ){
      $actions = Action::with('cafe')
                            ->with('company')
                            ->where('status', '=', 0)
                               ->with('by')
                            ->get();
    }else{
      /*
        Geta all of the un processed actions owned by the user.
      */
      $actions = Action::with('cafe')
                           ->with('company')
                           ->whereIn('company_id', Auth::user()->companiesOwned()->pluck('id')->toArray())
                           ->where('status', '=', 0)
                              ->with('by')
                           ->get();
    }

    return response()->json( $actions );
  }

What this does is check to see if the user is an admin or owner. If they are an admin grab any un-processed action. If they are an owner, make sure to grab any un-processed action that the company_id is in the companies owned by the user. This prevents owners grabbing actions from companies they don’t own. The pluck() method grabs the ID of the companies owned and the toArray() method converts the ids to an array so we can use it in the whereIn() query.

Time to build out our next routes. These might require a little more security in the form of a policy.

Adding A Cafe Action Policy

In the last tutorial Laravel Gates and Policies in an API Driven SPA, we added our first policy to the app. This policy prevented certain users from directly adding cafe entities. We will now need to add another policy. This will prevent users from approving or denying actions.

First, let’s run: php artisan make:policy ActionPolicy. This will create a ActionPolicy.php file in our /app/Policies directory. For now, let’s open that file and add make sure we include all of our necessary models:

<?php
/*
  Defines the namespace for the policy.
*/
namespace App\Policies;

/*
  Defines the models used by the policy.
*/
use App\Models\User;
use App\Models\Action;

/*
  Defines that the policy can handle authorization.
*/
use Illuminate\Auth\Access\HandlesAuthorization;

/**
 * Define the cafe action policy.
 */
class ActionPolicy
{

}

Before we go any further, we have to add this policy to our /app/Providers/AuthServiceProvider.php file. First, include the model and policy on top of the AuthServiceProvider.php like this:

use App\Models\Action;
use App\Policies\ActionPolicy;

Next, register the cafe action policy in the $policies array like this:

/**
 * The policy mappings for the application.
 *
 * @var array
 */
protected $policies = [
    Cafe::class => CafePolicy::class,
    Action::class => ActionPolicy::class
];

We are now ready to rock and roll with our policy and implement it. Before we dive too deep let’s think about the differences between this policy and the CafePolicy that we created in Laravel Gates and Policies in an API Driven SPA. The CafePolicy allows any authenticated user to access the controller function. We just blocked whether they could manage the cafe entities directly or have to create an action to be approved by a higher level user first. In this case, we are allowing or denying access based on the action they are trying to approve or deny. Essentially, we don’t want to block access in the controller method, we want to block access before the user even gets to the the method. Kind of like a middleware? Yup! This is where Laravel really steps up. You can actually apply a policy as a middleware to a certain route! Let’s get that going!

Before we apply the middleware, let’s make some policy methods. Add the following to your ActionPolicy.php:

/**
 * If a user is an admin or super admin they can approve all actions.
 * If the user owns the company that owns the cafe then they can approve actions.
 *
 * @param \App\Models\User $user
 * @param \App\Models\Action $action
 * @return bool
 */
public function approve( User $user, Action $action ){
  if( $user->permission == 2 || $user->permission == 3 ){
    return true;
  }else if( $user->companiesOwned->contains( $Action->company_id ) ){
    return true;
  }else{
    return false;
  }
}

/**
 * If a user is an admin or super admin they can deny all actions.
 * If the user owns the company that owns the cafe then they can deny actions.
 *
 * @param \App\Models\User $user
 * @param \App\Models\Action $action
 * @return bool
 */
public function deny( User $user, Action $action ){
  if( $user->permission == 2 || $user->permission == 3 ){
    return true;
  }else if( $user->companiesOwned->contains( $action->company_id ) ){
    return true;
  }else{
    return false;
  }
}

Both of these functions block in the similar way. You could argue that I could make a simple gate that allows access to manage an action, but for future upgrades and possibilities of extending permissions, I’m going to leave it as it is.

Each method takes a User object and a Action object. If the user has a permission of an admin, then return true. Otherwise check to see if the user owns the company that the action is taking place on. If they do, return true. If neither are true deny access to the action. Like we mentioned, we can do this BEFORE we even hit the method that handles the route.

Let’s open up our routes/api.php file and find the admin routes we added. First, let’s add a middleware for approving an action directly to our approve route like this:

Route::put('/actions/{action}/approve', 'API\Admin\ActionsController@putApproveAction')
       ->middleware('can:approve,action');

The way this works, is we apply the middleware can and after the colon approve which is the name of the method in our policy. We also add after the comma action which is our cafe action. Remember, the first parameter of the approve() method is a user which is automatically injected into the method. How do know that we are using the Action policy?

Let’s quickly look in our app\Http\Controllers\API\Admin\ActionsController.php and at the method:

public function putApproveAction( Action $action ){

}

See how a Action is type hinted? That means that the parameter passed to this route {action} is the id of a Action. It also tells Laravel to use the Action policy since it is bound to that route. Sweet isn’t it?

Now we can add the deny policy to our deny route like this:

Route::put('/actions/{action}/deny', 'API\Admin\ActionsController@putDenyAction')
       ->middleware('can:deny,action');

We have this all set up as middleware now so any unauthorized user will get a 403 response before any logic runs! For more information check out:
https://laravel.com/docs/5.6/authorization#authorizing-actions-using-policies. The docs show the multiple ways to apply policies.

Implementing Action Approval and Denial

This is very roastandbrew.coffee specific, but if you are interested, I’ll touch on this slightly.

When we approve an action, we have to check which type of action we are approving. This will determine the function we will run. Remember in the last tutorial Laravel Gates and Policies in an API Driven SPA where we added services? This will come in handy here!

Let’s start with action approval. In the putApproveAction() method, add the following code:

/**
 * Approves an action for a user
 * URL: /api/v1/admin/actions/{action}/approve
 * Method: PUT
 *
 * @param \App\Models\Action $action
 */
public function putApproveAction( Action $action ){
  /*
    Determine the proper action based on action type.
  */
  switch( $action->type ){
    case 'cafe-added':
      /*
        Unserialize the new cafe data
      */
      $newActionData = json_decode( $action->content, true );

      /*
        Add the cafe
      */
      CafeService::addCafe( $newActionData, $action->user_id );

      /*
        Approve the action
      */
      ActionService::approveAction( $action );
    break;
    case 'cafe-updated':
      /*
        Unserialize the content.
      */
      $actionData = json_decode( $action->content, true );

      /*
        Get the updated data for the cafe.
      */
      $updatedActionData = $actionData['after'];

      /*
        Apply updates to the cafe
      */
      CafeService::editCafe( $action->cafe_id, $updatedActionData);

      /*
        Approve the action
      */
      ActionService::approveAction( $action );
    break;
    case 'cafe-deleted':
      /*
        Grab the cafe and flag it as deleted.
      */
      $cafe = $cafe = Cafe::where('id', '=', $action->cafe_id)->first();

      $cafe->deleted = 1;
      $cafe->save();

      /*
        Approve the action
      */
      ActionService::approveAction( $action );
    break;
  }

  /*
    Returns a successful no content response.
  */
  return response()->json('', 204);
}

What we do is switch based off of the action type. Some would say this is too much logic for a controller. Right now, I believe it’s fine. As we add more actions we can abstract this into a separate method. Depending on the type of action, we handle the process through the CafeService. We then approve the action through the ActionService. This eliminates a load of code and makes the code re-usable throughout the application. When adding a cafe, the method that handles the action has a second parameter of user id. This is the action user id. We want to give credit to the person who added the cafe even if they didn’t have the permission right off the bat.

Another note, is we return an empty response with a 204 action code with means empty response. We will handle this accordingly in the next tutorial where we add a UI to our actions.

In the putDenyAction() method, add the following code:

/**
 * Denies an action for the user.
 * URL: /api/v1/admin/actions/{action}/deny
 * Method: PUT
 *
 * @param \App\Models\Action $action
 */
public function putDenyAction( Action $action ){
  /*
    Denies the action
  */
  ActionService::denyAction( $action );

  /*
    Returns a successful no content response.
  */
  return response()->json('', 204);
}

All this does is simply deny the action which and return an empty response with a 204 code which is proper RESTful for endpoints that don’t return data.

Conclusion

That was a long one! We have now combined multiple forms of security (middleware and policies) to authorize a user to access certain resources within our app. The next tutorial will show how to implement this functionality in a user interface within a single page application. There are some tricks I’ll show you there, but since it’s locked down on the back end, it will be a little easier.

As always, if there is anything you have questions on or need more information, send a message my way through the comments below! If you are interested in learning way more about API Driven Development, sign up here: Server Side Up General List

Keep Reading
View the course View the Course API Driven Development With Laravel and VueJS
Up Next → Sorting in VueJS Components and Vuex State

Support future content

The Ultimate Guide to Building APIs and Single-Page Applications with Laravel + VueJS + Capacitor book cover.

Psst... any earnings that we make off of our book is being reinvested to bringing you more content. If you like what you read, consider getting our book or get sweet perks by becoming a sponsor.

Written By Dan

Dan Pastori avatar Dan Pastori

Builder, creator, and maker. Dan Pastori is a Laravel certified developer with over 10 years experience in full stack development. When you aren't finding Dan exploring new techniques in programming, catch him at the beach or hiking in the National Parks.

Like this? Subscribe

We're privacy advocates. We will never spam you and we only want to send you emails that you actually want to receive. One-click unsubscribes are instantly honored.