Oh Dear is the all-in-one monitoring tool for your entire website. We monitor uptime, SSL certificates, broken links, scheduled tasks and more. You'll get a notifications for us when something's wrong. All that paired with a developer friendly API and kick-ass documentation. O, and you'll also be able to create a public status page under a minute. Start monitoring using our free trial now.

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.

You can follow me on these platforms:

On all these platforms, regularly share programming tips, and what I myself have learned in ongoing projects.

Every month 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.

Comments

What are your thoughts on "How to handle front-end authorization using Laravel, Inertia and TypeScript"?

Comments powered by Laravel Comments
Want to join the conversation? Log in or create an account to post a comment.

Webmentions

Ivan Radunovic liked on 27th February 2020
Alexander Jank liked on 27th February 2020
Siddharth Ghedia liked on 27th February 2020
Kashyap Merai liked on 27th February 2020
Owen Voke (pxgamer) liked on 27th February 2020
Bhavesh Daswani #LaravelAhm retweeted on 27th February 2020
Hardik Shah liked on 27th February 2020
Chris SFR liked on 27th February 2020
Bhavesh Daswani #LaravelAhm liked on 27th February 2020
Bhavesh Daswani #LaravelAhm replied on 27th February 2020
We have to write the typescript definition files by hand It's not necessary to put all definition in one file You can check out my PR for type definition github.com/iamshadmirza/r… Where I splited the type definition into files and then exported the definition from index.d.ts.
. liked on 27th February 2020
Gaurav Makhecha replied on 27th February 2020
The typescript definitions file was very large. Is it generally coded by hand or generated from multiple files?
Mithicher Baro retweeted on 27th February 2020
बेद retweeted on 27th February 2020
Mithicher Baro liked on 27th February 2020
Felipe Dalcin liked on 27th February 2020
José Cage retweeted on 27th February 2020
José Cage liked on 27th February 2020
Daniel Schmitz liked on 27th February 2020
Luca liked on 27th February 2020
Khai Rahman retweeted on 26th February 2020
Thibault Lavoisey liked on 26th February 2020
Khai Rahman liked on 26th February 2020
Tony Ingall liked on 26th February 2020
Matthew Poulter liked on 26th February 2020
André liked on 26th February 2020
LaravelLive Punjab retweeted on 26th February 2020
Jeffrey Way retweeted on 26th February 2020
Tom retweeted on 26th February 2020
Flare retweeted on 26th February 2020
علي المجرشي retweeted on 26th February 2020
LaravelLive Punjab liked on 26th February 2020
Guus liked on 26th February 2020
Steve McDougall liked on 26th February 2020
Aron Peyroteo liked on 26th February 2020
Ahmed Shawky liked on 26th February 2020
Coderatio liked on 26th February 2020
Adrià Canal 🎗️ liked on 26th February 2020
Craig Lovelock liked on 26th February 2020
Tom liked on 26th February 2020
M. Vug liked on 26th February 2020
Freek Van der Herten replied on 26th February 2020
Thanks for your kind words, really appreciate it! 👍
Tom replied on 26th February 2020
Saw your talk and would like to thank you for sharing this information. Really appreciate it!
Ken V. retweeted on 26th February 2020
Simon Rigby liked on 26th February 2020
Ken V. liked on 26th February 2020
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 ? retweeted 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
Ruslan liked on 29th August 2019
Cristian Cardiño liked on 29th August 2019
etnan liked on 29th August 2019
oluwajubelo loves VueJS ? retweeted on 29th August 2019
oluwajubelo loves VueJS ? liked on 29th August 2019
philippe damen liked on 29th August 2019
Zubair Mohsin liked on 29th August 2019
jim mcmillan 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