3
votes

I am building my first SPA and I am facing some issues. This is how it is designed:

  1. Laravel & Laravel Views handle the login and registration related pages.
  2. SPA starts at the user logged in page.
  3. My app.js defines a default VueJS app in which I use the mounted() method to set the state (VUEX) of the logged in user. Ideally, all it does is get the user details via an axios call to the Laravel backend and populate the VUEX state.
  4. I use beforeEnter() methods in the route definitions to ensure only authorized people can navigate to the route.

This is where I face the problem. When a user logs in, it seems like router is executed before the vuex is set. Say I have a url /dashboard and /user/1. When I try to go to user/1 it works perfectly if it is after I load the application. But, if I refresh the webpage when I am in user/1, then router beforeEnter cannot find vuex state of the user so it redirects user to dashboard. This is because when the router runs beforeEnter, if it is a fresh page load, it wouldn't have access to the user Vuex state or it has access, but the value isn't set yet.

Because of this my biggest problem is I can't link to a route page directly since it always lands in dashboard and then the user will have to go to the route for it to work. How can I handle this situation?

2
Maybe you should use local storage - a cookie to store auth credentials and then you will have access to it in beforeEnter method.latovic
A cookie exists with encrypted data on the user details (default created by Laravel). But I am not sure how to decrypt it using js. And moreover wouldn't it be security issue of I am able to decrypt it using js on the front end? I'd really like to use some other option if possible! Local storage might be possible, but I want it to expire the session when it should. Isn't local storage for long term storage?Mathew Paret
On the route you can send the cookie to the server decrypt it and in respond to it accordingly.Kushagra Agarwal

2 Answers

3
votes

This is what I ended up doing. I defined a function with which I initialized Vue.

At the end of the app.js I used Axios to retrieve the current user via Ajax. In the then method of the promise, I set the store with the user details I received in the promise and then call the function that I defined for Vue initialization above. This way when vue is initialized the store user already has the data.

The code change was very minimal and I didn't have to change the existing axios implementation.

This is my new implementation:

Axios.get('/api/user/info')
    .then(response => {
        (new Vue).$store.commit('setUser', response.data);
        initializeVue();
    })
    .catch(error => initializeVue());

function initializeVue()
{
    window.app = new Vue({
        el: '#app',
        router,
        components: {
            UserCard,
            Sidebar,
        },
        methods: mapMutations(['setUser']),
        computed: mapState(['user']),
    });
}
1
votes

I use $root as a bus and turn to VueX as a last resort, Here is some code i have stripped out of a plugin i am working on, I have adapted it slightly for you to just drop in to your code.., Should get you going.

This configuration supports VUE Cli.

Don't worry about session expiery, an interceptor will watching for a 401 response from Laravel will do to prompt the user to re-authenticate.

Ditch the axios configuration in bootstrap.js and replace it with this setup and configure Access-Control-Allow-Origin, the wildcard will do for local dev.

axios.defaults.withCredentials = true;

axios.defaults.headers.common = {
    'X-Requested-With': 'XMLHttpRequest',
    'X-CSRF-TOKEN': undefined,
    'Access-Control-Allow-Origin': '*'
};

axios.interceptors.response.use(

    function (response) {

        if(response.headers.hasOwnProperty('x-csrf-token')) {
            axios.defaults.headers['X-CSRF-TOKEN'] = response.headers['x-csrf-token'];
        }

        return response;

    },

    function (error) {

        if(typeof error !== 'object' || !error.response) {
            return Promise.reject(error);
        }

            if(error.response.hasOwnProperty('status')) {

                switch(error.response.status) {

                    case 401:
                    case 419:
                        // DO RE-AUTHENTICATE CALL
                        break;

                }

            }

        return Promise.reject(error);

    }

);

For the rest...

In main.js

data() {
  return {
    user: {},
    authenticating: false
  }
},

computed: {

    isAuthenticated() {
        // Check a credential only an authorized user would have.

        if(this.$router.app.hasOwnProperty('user') === false || this.$router.app.user === null) {
            return false;
        }

        return this.$router.app.user.hasOwnProperty('id');
    }

},

methods: {

    checkAuth: function () {

        this.$set(this.$router.app, 'authenticating', true);

        axios.get('/auth/user').then(response => {

            this.$set(this.$router.app, 'user', response.data.user);

            if (this.$router.app.isAuthenticated()) {

                this.$router.push(this.$router.currentRoute.query.redirect || '/', () => {
                    this.$set(this.$router.app, 'authenticating', false);
                });

            }

        }).catch(error => {

            // TODO Handle error response
            console.error(error);
            this.$set(this.$router.app, 'user', {});

        }).finally(() => {

            this.$set(this.$router.app, 'authenticating', false);

        });


    },

    login: function (input) {

        axios.post('/login', input).then(response => {

            this.$set(this.$router.app, 'user', response.data.user);

            this.$router.push(this.$router.currentRoute.query.redirect || '/');

        }).catch(error => {
            // TODO Handle errors
            console.error(error);
        });

    },

    logout: function () {

        axios.post('/logout').then(response => {

            this.$set(this.$router.app, 'user', {});

            this.$nextTick(() => {
                window.location.href = '/';
            });

        }).catch(error => {
            console.error(error);
        });

    },

}

beforeCreate: function () {

    this.$router.beforeResolve((to, from, next) => {

        if (to.matched.some(record => record.meta.requiresAuth) && !this.$router.app.isAuthenticated()) {

            next({
                name: 'login',
                query: {
                    redirect: to.fullPath
                }
            });

            return;
        }

        next();
    });
}

In Auth/LoginController.php add method

public final function authenticated(Request $request)
{
    return response()->json([
        'user' => Auth::user()
    ]);
}

Create app/Http/Middleware/AfterMiddleware.php It will pass back a new CSRF token only when it changes rather than on every request. The axios interceptor will ingest the new token when detected.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Cookie;

class AfterMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if(Cookie::get('X-CSRF-TOKEN',false) !== csrf_token())
            return $next($request)->header('X-CSRF-TOKEN',csrf_token());

        return $next($request);
    }
}

You effectively can replace the static login form with a Vue login form with this setup.

Here is what router setup looks like:

new Router({
    mode: 'history',
    routes: [
        {
            path: '/login',
            name: 'login',
            component: AuthLogin,
            meta: {
                requiresAuth: false,
                layout: 'auth'
            }
        },
        {
            path: '/login/recover',
            name: 'login-recover',
            component: AuthLoginRecover,
            meta: {
                requiresAuth: false,
                layout: 'auth'
            }
        },
        {
            path: '/',
            name: 'index',
            component: Dashboard,
            meta: {
                requiresAuth: true,
                layout: 'default'
            }
        },
        {
            path: '/settings',
            name: 'settings',
            component: Settings,
            meta: {
                requiresAuth: true,
                layout: 'default'
            }
        }
    ]
});