21

I'm not sure how this hasn't been dealt with before, but how do I go about using VueJs and authorizing actions in Vue template?

If I'm using Laravel's blade, this is easy (using @can directive), but there's no documentation or any way to perform this in Vue after searching for hours on Google.

Now, I know I can simply load the users permissions into an array / JSON object inside the view, but there seems to be no way of displaying / hiding actions in Vue templates using Laravel's gate methods to determine if the user is allowed to perform the action on a specific record.

For example, there's a list of comments, but the user must own the comment to be able to see the 'edit' button.

The thing is, if I implement the logic in Vue, I'd be duplicating authorization logic throughout my entire backend and frontend for this.

Using Laravel's policy's, I'm able to perform complex authorization of specific actions. But I'm stumped as to how I would implement the policy in Vue.

There's also more complex scenarios, such as if a user that has an admin role is browsing comments, they should be able to edit it even if they don't own the comment.

Does anyone have any suggestions for this type of scenario?

EDIT:

Now I could add an attribute accessor to my models, for example:

Model:

class Comment extends Model
{
    protected $appends = ['can_update'];

    public function getCanUpdateAttribute()
    {
        return Gate::allows('update', $this);
    }
}

Vue:

<button v-if="comment.can_update">Edit</button>

But this seems like I'm again duplicating logic that already exists inside my policies.

Steve Bauman
  • 8,165
  • 7
  • 40
  • 56
  • 2
    This is a good point to think about. – Ian Rodrigues Aug 24 '17 at 19:01
  • When you fetch the records from the server, at that point you know if someone is an admin, whether they can edit / create / delete etc. - why don't you deliver that in form of simple boolean fields. Say you're listing these imaginary attributes. On serverside, simply add `is_admin` or `can_edit` fields and deliver that back to vue app. Depending on the value (true / false), render the row / button / color / etc. Simply use Vue to render data provided by server. – N.B. Mar 19 '18 at 16:18
  • 1
    Check this article, It's use Laravel ACL in Front-end https://pineco.de/implementing-laravels-authorization-front-end/ – Hujjat Nazari Oct 01 '18 at 13:44
  • You can evaluate gates in a Vue component because Vue is client-side, and policies are defined (and evaluated server-side). If you do a database query in a policy method, you can’t do that on the client side. You’ll need to evaluate the policy methods server-side, and pass the results to your view components. – Martin Bean Apr 26 '19 at 15:50

4 Answers4

36

I ended up using Laravel resources to accomplish this.

Here's an example (notice the can array key):

class Ticket extends Resource
{
    /**
     * The "data" wrapper that should be applied.
     *
     * @var string
     */
    public static $wrap = 'ticket';

    /**
     * Transform the resource into an array.
     *
     * @param \Illuminate\Http\Request $request
     *
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'answer_id' => $this->answer_id,
            'summary' => $this->summary,
            'description' => $this->description,
            'creator' => $this->creator,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
            'reported_at' => $this->reported_at,
            'closed_at' => $this->closed_at,
            'closed' => $this->closed,
            'answered' => $this->answered,
            'can' => $this->permissions(),
        ];
    }

    /**
     * Returns the permissions of the resource.
     *
     * @return array
     */
    protected function permissions()
    {
        return [
            'update' => Gate::allows('update', $this->resource),
            'delete' => Gate::allows('delete', $this->resource),
            'open' => Gate::allows('open', $this->resource),
        ];
    }
}

This allowed me to control access on the front-end using simple boolean logic in Vue templates, rather than duplicating actual permission logic on the front-end as well:

<router-link v-if="ticket.can.update" :to="{name:'tickets.edit', params: {ticketId: ticket.id}}" class="btn btn-sm btn-secondary">
    <i class="fa fa-edit"></i> Edit
</router-link>

Also, I used Laravel resource collections to be able to apply permissions if the user is able to create a resource:

class TicketCollection extends ResourceCollection
{
    /**
     * The "data" wrapper that should be applied.
     *
     * @var string
     */
    public static $wrap = 'tickets';

    /**
     * Get any additional data that should be returned with the resource array.
     *
     * @param \Illuminate\Http\Request $request
     *
     * @return array
     */
    public function with($request)
    {
        return [
            'can' => [
                'create' => Gate::allows('create', Ticket::class),
            ],
        ];
    }
}

Then in my API controller:

public function index()
{
    $tickets = Ticket::paginate(25);

    return new TicketCollection($tickets);
}

public function show(Ticket $ticket)
{
    $ticket->load('media');

    return new TicketResource($ticket);
}

This allowed me to validate if the currently authenticated user has access to be able to create the resource that is being listed, since we won't have an actual resource to validate on, we can do this on the returned collection since it relates to it entirely.

Implementing this pattern seemed to me the simplest way of managing authorization without duplicating the actual authorizing logic throughout my Vue app and using blade to inject permissions into components individually.

Injecting permissions into components eventually lead me to problems if you have nested components that also require permissions, because then you'll need to pass the child components permissions into the parents to be able to validate them.

For nested permissions, you can return sub-resources from your parent resource for relationships that also include a can permissions array, so you can easily loop through these using Vue and use simple logic for determining the users access on those as well.

This approach was also beneficial so I could cache the permissions for each user via server-side on resources that don't change often.

Steve Bauman
  • 8,165
  • 7
  • 40
  • 56
  • 3
    I really like this solution, very elegant – Thijs Mar 29 '19 at 08:36
  • 1
    This is excellent. Although I think it would be better included as meta. You can add meta data using the with() method. https://laravel.com/docs/5.8/eloquent-resources#adding-meta-data – John Mellor May 28 '19 at 12:21
  • 1
    There is perfomance hit when using this method for list of records, can you share the strategy you use to cache the Policy authorization? – cyberfly Jun 13 '19 at 07:21
  • If the permissions are contained within the resource itself, and you wanted to prevent the user from even reading the Ticket then they could inspect the response and see the ticket content anyway. – digout Jun 16 '20 at 14:10
  • @digout In that case of wanting to restrict viewing content, you must add the data conditionally depending on the current users permissions, instead of retuning it all. My solution simply appends permissions to the resource -- it does not restrict actual content returned. – Steve Bauman Jun 16 '20 at 14:48
  • Is this still a good idea in 2021 or are there alternatives now? – waterloomatt Feb 25 '21 at 21:01
  • @waterloomatt I haven't seen any other approach since I've created this post. I'd still reach for this pattern anytime I'm needing to apply complex permissions in a Vue SPA and a Laravel backend. However, since InertiaJS has came out, it could potentially be leveraged to assist in the transferring of the permissions into the Vue page component. – Steve Bauman Feb 26 '21 at 02:37
11

Currently, there is no way to achieve this without duplicating code of the backend into the frontend.

In this episode of Fullstack Radio(17:15), Jeffrey Way and Adam Wathan talked exactly about that point. They have the same opinion as mine and currently, they're doing the same you did.

They also talked about using props like:

<post-component :can-update="{{ $user->can('update', $post) }}"></post-component>

I hope this answer can be helpful.

Ian Rodrigues
  • 743
  • 5
  • 21
  • Thanks Ian, great listen! Thanks for linking that episode. It's kind of crazy how this hasn't really been sorted out yet? – Steve Bauman Aug 24 '17 at 19:22
  • 1
    Yup. I guess Jeffrey said he'll work in something toward this. – Ian Rodrigues Aug 24 '17 at 19:24
  • That's awesome, I hope something comes up soon. In the meantime, I'm going to continue to use the props like you've mentioned as there doesn't really seem to be another option at the moment :( Thanks again for the answer!! – Steve Bauman Aug 24 '17 at 19:48
  • 2
    Check this article, It's use Laravel ACL in Front-end https://pineco.de/implementing-laravels-authorization-front-end/ – Hujjat Nazari Oct 01 '18 at 13:43
  • @MuhammadMohsen unfortunately no. I ended up using my own solution by returning booleans depending on the returned resource requested by the user. This prevented me from having to recreate my authorization logic on both ends (which would be a nightmare tbh). See my answer for more information. – Steve Bauman Dec 10 '18 at 20:11
1

In one of my projects, permissions are stored as strings i.e. 'post.read', 'post.view'. These permissions are passed to the front end upon login.

I built a small and simple plugin Vue Quick ACL which abstracts the functionality away and manages the reactivity when changing users and user permissions.

// UserResource

return [
 'name' => $this->name,
 'permissions' => $this->permissions
];

In the frontend you can then store those permissions with the user:

// Login.vue

export default {
  methods: {
    login() {
      Api.login()
        then(({data}) => {
          this.$setUserPermissions(data.permissions)
        })
    }
  }
}
// Component.vue

<template>
  <button v-if="$can('post.delete')">Delete</button>
</template>
digout
  • 4,041
  • 1
  • 31
  • 38
0

if you using inertia js on laravel you can add to your handleInertiaRequests.php 'can' => ['manage-super-admin' => $request->user()>can('manage_super_admin')] on components

example-components v-if="$page.props.can.manage-super-admin" />