Search code examples
phplaravelvuejs3vue-componentinertiajs

laravel-spatie-permissions-vue permissions and roles needs a page refresh to show current values


Laravel 10 app, with Jetstream/Inertia/Vue 3.

I use laravel-spatie-permissions-vue to deal with user permissions and roles, which are assigned ok. Currently I am using default Jetstream views, so as the user logs in the dashboard is shown, where I put this on Components/Welcome.vue:

<div v-if="is('administrator')">is administrator.</div>
<div v-if="is('user')">is user.</div>

For testing purposes there are two roles, administrator and user, and there are some permissions too, but as is the same problem for them both let's stick with roles only. The problem is, the first time you login, let's go with user for example, nothing is shown, which is wrong. If you refresh the page, 'is user.' will appear, as it should. Then, if you log out and log in with administrator, 'is user.' will be shown (incorrect), but if you refresh the page 'is administrator.' will be shown (correct). I'm getting the same behavior in development, with valet and 'npm run dev', than when deployed to an apache server and 'npm run build' is executed.

I followed all the instructions at https://github.com/geowrgetudor/laravel-spatie-permissions-vue .

I've searched for two days, but as I haven't found anybody with the same problem, I guess I made some silly mistake. I think the problem should be related to the app.blade.php part, and perhaps it has to be something cache related, but haven't found anything following that road, so no clues at this point.

What I tried (only related steps):

laravel new application
composer require laravel/jetstream
php artisan jetstream:install inertia
npm i @vitejs/plugin-vue
npm install -D tailwindcss postcss autoprefixer
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
composer require geowrgetudor/laravel-spatie-permissions-vue
  • Added the trait to the User model (User.php):
use SpatiePermissionVue\Traits\RolesPermissionsToVue;

class User extends Authenticatable
{
    // ...
    use RolesPermissionsToVue;
}
  • (app.js) Import and use laravel-spatie-permissions-vue plugin into app.js file:
import RolesPermissionsToVue from "../../vendor/geowrgetudor/laravel-spatie-permissions-vue/src/js";

// createInertiaApp, right after '.use(ZiggyVue, Ziggy)':
            .use(RolesPermissionsToVue)
  • (app.blade.php) Pass the Spatie roles & permissions to a global var called vueSpatiePermissions in app.blade.php:
<script type="text/javascript">
  window.vueSpatiePermissions = {!! auth()->check() ? auth()->user()->getRolesPermissionsAsJson() : 0 !!}
</script>

added this this on Components/Welcome.vue:

<div v-if="is('administrator')">is administrator.</div>
<div v-if="is('user')">is user.</div>

and I was expecting 'is administrator.' or 'is user.' will be shown once a user logs in without having to refresh the page.

The user has its roles and permissions correctly assigned, and it can be checked adding this code in the share() function at HandleInertiaRequests:

        $permissions = Auth::check() ? Auth::user()->permissions->pluck('name') : "";
        $roles = Auth::check() ? Auth::user()->roles->pluck('name') : "";
        return array_merge(parent::share($request), [
            'auth.user.permissions' => $permissions,
            'auth.user.roles' => $roles
        ]);

and then adding {{ $page.props.auth.user.roles }} or {{ $page.props.auth.user.permissions }} at the view, so that's not the problem.

Any help would be appreciated. Thanks.

In the end, thanks to cengsemihsahin's response I changed how I was approaching this, which in the end took me to How to Add Roles and Permission to Laravel Fortify + Inertia + vue? where I ended using Viduranga's approach, as in this way there's no need of laravel-apatie-permissions-vue or laravel-permission-to-vuejs, only change share() function in HandleInertiaRequests (here's mine):

    public function share(Request $request): array
    {
        $permissions = Auth::check() ? Auth::user()->permissions->mapWithKeys(function($pr){ return [$pr['name'] => true]; }) : [];
        $roles = Auth::check() ? Auth::user()->roles->mapWithKeys(function($pr){ return [$pr['name'] => true]; }) : [];
        return array_merge(parent::share($request), [
            'auth.user.permissions' => $permissions,
            'auth.user.roles' => $roles
        ]);
    }

then you can use this in your view:

v-if="$page.props.auth.user.roles['administrator']"

I think this one is a better solution than the one I was using for my project.


Solution


  • First you need to know: The window.Laravel or window.vueSpatiePermissions variables that you assign in the app.blade.php file are assigned when your vue application is created. So, changing the routes in the browser on your vue application or making a login or logout via the api will not change your `app.blade.php` view or not reload.

    So what is the solution and what have I done for my own application?

    First I did these using this [https://github.com/ahmedsaoud31/laravel-permission-to-vuejs][1] :

    In **User** model:
    use LaravelPermissionToVueJS;
    

    In app.blade.php

    <script type="text/javascript">
        window.Laravel = {
            csrfToken: "{{ csrf_token() }}",
            jsPermissions: {!! auth()->check() ? auth()->user()->jsPermissions() : 0 !!}
        };
    </script>
    

    In main.js (when creating app)

    import App from './App.vue';
    import LaravelPermissionToVueJS from 'laravel-permission-to-vuejs';
    const app = createApp(App);
    app.use(LaravelPermissionToVueJS);
    app.mount('#app');
    

    In my solution (in my own application), the page is reloaded as the page is redirected by providing an HTTP request on the browser at every login or logout action.

    This is an unnecessary and unprofessional solution. Other than that, I will explain the solution you can do again a little below.

    My login.vue (in the shortest for example)

    <form class="d-none" method="post" action="/auth/login">
        <input type="hidden" name="_token" :value="csrf">
        <input type="hidden" name="username" :value="user.username">
        <input type="hidden" name="password" :value="user.password">
        <button ref="submitLoginForm" class="d-none"></button>
    </form>
    <form class="text-start" @submit="login">
        <div class="form">
            <div id="username-field" class="field-wrapper input">
                <input type="text" name="username" class="form-control"
                    required v-model="user.username"
                    placeholder="username" />
            </div>
            <div id="password-field" class="field-wrapper input mb-2">
                <input
                    :type="show_password ? 'text' : 'password'"
                    v-model="user.password"
                    name="password"
                    class="form-control"
                    required
                    placeholder="passphrase"
                />
            </div>
            <button type="submit" class="btn btn-primary">login</button>
        </div>
    </form>
    
    <script>
    export default {
        data() {
            return {
                csrf: document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
                user: {
                    username: '',
                    password: '',
                    remember: false,
                }
            }
        },
        methods: {
            login(e) {
                e.preventDefault();
                store.dispatch('login', this.user);
                this.$refs.submitLoginForm.click();
            }
        }
    }
    </script>
    

    You can use the composition api, you can take a reference with defineExpose and submit the form.

    Then the page is automatically refreshed and login is provided on both the api and the web side. There is a reason I do this specific to my own application.

    This is my example. So how to do it without reloading?

    As soon as you log in and assign the user information to variables in the store or elsewhere, you can assign the permissions for that user to the `window.Laravel.jsPermissions.permissions` variable.

    Basically, changing the value of this variable for each login - logout user without refreshing the page will fix your problem.

    Also, let's say someone (for example, from the admin panel) changes a user's role or permissions. You can trigger or broadcast an event at that time.

    In this event you can send permissions and assign them to `window.Laravel.jsPermissions.permissions` variable in the view you captured. Example (with Pusher)
    window.Echo.private('permissions').listen('.user_permissions_changed', (e) => {
        console.log(e);
        window.Laravel.jsPermissions.permissions = e.user_permissions;
    });
    

    Because of I didn't have time, I couldn't go into full details. If you still have a question, please write it as a comment.