My team at Spatie is currenlty building Mailcoach, a solution to self host your e-mail newsletter. Mailcoach can be used a stand alone software or as a Laravel package. Subscribe now at Mailcoach to get a notification as soon as we release it.

How to handle front-end authorization using Laravel, Inertia and TypeScript

Original – by Freek Van der Herten – 6 minute read

Recently Jeffrey Way published a video titled "Frontend Authorization Brainstorming" on Laracasts. In that video, he shows three ways of passing authorization results to the front-end.

Currently I'm working on a big project that uses Inertia, React and TypeScript. In this blog post, I won't cover those things in detail, but I'd like to show you have we, using those technologies, pass authorization (and routes) to the front-end.

Using policies

In the app I'm working on the are teams and projects. A team owns a project. A project can also be accessible by guests.

In our app, all of our authorization checks are made using Laravel Policies. Here's the policy for projects.

namespace App\Domain\Project\Policies;

use App\Domain\Project\Models\Project;
use App\Domain\Team\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class ProjectPolicy
{
    public const ADMINISTER = 'administer';
    public const LEAVE = 'leave';

    use HandlesAuthorization;

    public function administer(User $user, Project $project)
    {
        if (! $user->hasTeamWithProject($project)) {
            return false;
        }

        return $user->isAdminOrOwner($project->team);
    }

    public function leave(User $user, Project $project)
    {
        return $user->isGuestOnProject($project);
    }
}

Don't mind the logic in those methods, that's not the focus. Do notice that for every method in our policy there is a constant with the same name. In a small project, you won't do this. But this code is taken from quite a sizeable app, with many policies each having different methods on it. By having a constant on your can do a gate check like this:

Gate::allows(ProjectPolicy::ADMINISTER, $project);

Why starting to type ProjectPolicy any decent IDE will show you the constants you have on your policy.

Using a constant also has a benefit that changing the name of a policy method becomes easy. Just change the method name and using your IDE perform a rename refactor on the constant. A decent IDE can update all usages of the constant.

Using resources

In our app, we're using Inertia. It's a very cool collection of packages that Jonathan Reinink currently is building. If you want to know more about the project, read this blog post.

Using Inertia, each page is its own React (or Vue component). So in our app Blade isn't used to render anything. So we can't use serverside logic when rendering our React powered views.

This is what our ProjectsIndexController looks like:

namespace App\Http\App\Controllers\Projects;

use App\Http\App\Resources\Project\ProjectResource;
use Inertia\Inertia;

class ProjectsIndexController
{
    public function __invoke()
    {
        $projects = $this->getProjectsForCurrentUser();

        return Inertia::render('projects.index', [
            'projects' => ProjectResource::collection($projects),
        ]);
    }
}

The important bit here is that a collection of projects is passed to the ProjectResource, which is an API Resource. An API resource in Laravel is a dedicated class to transform an Eloquent model into an API response. Let's take a look at that ProjectResource.

namespace App\Http\App\Resources\Project;

use App\Domain\Project\Policies\ProjectPolicy;
use App\Http\App\Controllers\Projects\Settings\DeleteProjectController;
use App\Http\App\Controllers\Projects\Settings\LeaveProjectController;
use Illuminate\Http\Resources\Json\JsonResource;

class ProjectResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            // ... other attributes redacted for brevity
            'can' => [
                ProjectPolicy::ADMINISTER => auth()->user()->can(ProjectPolicy::ADMINISTER, $this->resource),
                ProjectPolicy::LEAVE => auth()->user()->can(ProjectPolicy::LEAVE, $this->resource)
            ],
            'links' => [
                'edit' => action(EditProjectController::class, $this->resource),
                'delete' => action([DeleteProjectController::class, 'show'], $this->resource),
                'leave' => action([LeaveProjectController::class, 'show'], $this->resource),
            ],
        ];
    }
}

In all resources we use the can key to put authorization results that are of interest to the front-end. The key of each entry in that array is the name of the policy method, the value is the result of the check: true or false.

Routes that are of interest to the front-end are put in the links key. The front-end can use the routes to render links to the detail screen, the delete endpoint and so on. We can use the action helper and the fully qualified class name because we've removed the default controller namespace.

At the front-end

In this project, we use TypeScript to define custom types. Each API resource has a matching type. Here's the type definition for Project:

declare type Project = {
    id: number;
    name: string;
    can: {
        administer: boolean;
        leave: boolean;
    };
    links: {
        show: string;
        delete: string;
        leave: string;
    };
};

Here's the project.index React component that renders the list of projects.

import React from 'react';
import Layout from 'app/views/layouts/Layout';

import {can} from "app/util";

type Props = {
    projects: Array<Project>;
};

export default function index({projects}: Props) {
    return (
        <Layout title="Projects">
            <table>
                <th>
                    <td>Name</td>
                    <td>&nbsp;</td>
                </th>
                {projects.map(project => (
                    <tr>
                        <td>{project.name}</td>
                        <td>{can('administer', project) && <a href={project.links.edit}>Edit</a>}</td>
                        <td>{can('leave', project) && <a href={project.links.leave}>Leave</a>}</td>
                    </tr>

                ))}
            </table>
        </Layout>
    );
}

Let's take a look at the things that are happening here. Remember those projects we passed to Inertia::render? Behind the scenes, Inertia will take care that those projects are being passed to the React component above as a projects prop. Using TypeScript, we explicitly say that the projects prop is an array of Project objects.

type Props = {
    projects: Array<Project>;
};

export default function index({projects}: Props) {

// ...

IDEs that support TypeScript can now autocomplete the properties on a Project object. So when typing project.links the IDE can show us the available links:

Let's turn our attention to the can method. It was created by my colleague Seb. This is its definition:

export function can<T extends Authorizable>(ability: keyof T['can'] & string, authorizable: T) {
    return authorizable.can[ability];
}

This function will check if the can property of the object being passed as the second argument contains a key that's being passed as the first argument. With this in place, can('administer', project) will return a boolean (the result of the authorization check). If we try to use a non-existing check, the IDE will warn us.

Closing thoughts

I hope you enjoyed this walkthrough of how we pass authorization checks (and routes) to the front end. In essence, we add a can and links entry to the API resource. On the front-end, we use TypeScript to enable autocompletion and error detection.

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Manojkiran retweeted on 31st October 2019
Manojkiran liked on 31st October 2019
Mike liked on 31st October 2019
Stuart Wilsdon liked on 31st October 2019
Michael Dyrynda retweeted on 31st October 2019
Roman Pronskiy liked on 31st October 2019
Shane Smith liked on 31st October 2019
Jaxson Terra liked on 31st October 2019
Bas de Groot liked on 31st October 2019
Matthew Poulter liked on 31st October 2019
Semyon Chetvertnyh liked on 31st October 2019
Shane Oliver liked on 31st October 2019
oluwajubelo loves VueJS 🚨 liked on 31st October 2019
Shane Smith retweeted on 31st October 2019
Mickaël Viaud liked on 31st October 2019
loreias liked on 31st October 2019
Alex liked on 31st October 2019
Laracon AU retweeted on 31st October 2019
Zayn Buksh liked on 31st October 2019
mydailydev liked on 30th August 2019
Chris Brown liked on 29th August 2019
Filipe liked on 29th August 2019
Wyatt liked on 29th August 2019
Niels liked on 29th August 2019
Ryan Rosiek liked on 29th August 2019
Simon Kollross liked on 29th August 2019
George From Ohio liked on 29th August 2019
Jami Suomalainen liked on 29th August 2019
Cristian Cardiño liked on 29th August 2019
Ruslan liked on 29th August 2019
etnan liked on 29th August 2019
oluwajubelo loves VueJS 🚨 liked on 29th August 2019
philippe damen liked on 29th August 2019
jim mcmillan liked on 29th August 2019
Zubair Mohsin liked on 29th August 2019
Niels liked on 26th July 2019
Prasad Chinwal replied on 26th July 2019
@freekmurze thanks a bunch👍
Prasad Chinwal liked on 26th July 2019