Framework7 Route Protection - vue.js

import HomePage from './pages/home.vue';
import Home2Page from './pages/home2.vue';
import NotFoundPage from './pages/not-found.vue';
export default [
{
path: '/',
component: HomePage,
// check if the user is logged in
beforeEnter: checkAuth,
},
{
path: '/home2',
component: Home2Page,
},
{
path: '(.*)',
component: NotFoundPage
}
];
function checkAuth(to, from, resolve, reject) {
if (true) {
resolve({
component: Home2Page
});
} else {
reject();
}
}
why is this checkAuth function not working? I try to check if the Page requires Auth, when it requires the function checkAuth should fire up. If the Auth is true in this case, the other Page should be loaded.

You have to use redirect property instead of beforeEnter.
Example (retrieved from the docs) :
redirect: function (route, resolve, reject) {
if (true) {
resolve('myUrl');
}
else reject();
}

Related

Vue router - navigation guard is skipped when using router.push()

I have a Vue SPA with i18n and some views that require authentication via navigation guard.
When i am not authenticated and go to url via my browser:
examplepage.com/en/lockedpage
i get redirected to:
examplepage.com/en/login
which is good, however when i click a button that runs:
#click="$router.push(`/${$i18n.locale}/lockedpage`)"
i get in to the page even if i am not authenticated.
I want to get redirected to the login page if not authenticated
this is my router.js:
import Vue from 'vue';
import Router from 'vue-router';
import Home2 from './views/Home2.vue';
import Login from './views/Login.vue';
import Register from './views/Register.vue';
import ErrorLanding from './views/ErrorLanding.vue'
import Root from "./Root"
import i18n, { loadLocaleMessagesAsync } from "#/i18n"
import {
setDocumentLang
} from "#/util/i18n/document"
Vue.use(Router);
const { locale } = i18n
export const router = new Router({
mode: 'history',
base: '/',
routes: [
{
path: '/',
redirect: locale
},
{
path: "/:locale",
component: Root,
children: [
{
name: 'Home',
path: '',
component: Home2,
},
{
name: 'Home2',
path: '/',
component: Home2,
},
{
name: 'Login',
path: 'login',
component: Login,
},
{
path: 'register',
component: Register,
},
{
path: 'lockedpage',
name: 'lockedpage',
webpackChunkName: "lockedpage",
meta: {authRequired: true},
component: () => import('./views/LockedPage.vue')
},
{
path: '*',
component: ErrorLanding,
name: 'NotFound'
}
]
}
],
router.beforeEach((to, from, next) => {
if (to.params.locale === from.params.locale) {
next()
return
}
const { locale } = to.params
loadLocaleMessagesAsync(locale).then(() => {
setDocumentLang(locale)
const publicPages = [ `/`, `/${locale}`, `/${locale}/`];
const authRequired = !publicPages.includes(to.path);
const loggedIn = localStorage.getItem('user');
const redirect = to.path;
if (authRequired && loggedIn === null) {
if(to.meta.authRequired === false) {
next();
}
else
next({ name: 'Login', query: { redirect: redirect } });
} else {
next();
}
})
next()
});
Why does my navigationguard skip when using router.push() ?
This issue started after adding i18n, with localeredirect. so all routes comes after a locale for example: /en/.../
As Estus points out in the comments, the issue is that the first thing you're checking for is if the locales match, and if they do you're calling next() and sending the user to the page.
As outlined here:
Make sure that the next function is called exactly once in any given pass through the navigation guard. It can appear more than once, but only if the logical paths have no overlap, otherwise the hook will never be resolved or produce errors.
If you need to keep the locale check between the to and from pages, you could do something like:
if (to.params.locale === from.params.locale && loggedIn) {
next()
return
}
which will check if the loggedIn variable is truthy before pushing the user to the page they're trying to navigate to.
I believe just removing the if statement that checks if the locales match would also work.

How to get something to run before vue-router

I am building my first SPA and I am facing some issues. This is how it is designed:
Laravel & Laravel Views handle the login and registration related pages.
SPA starts at the user logged in page.
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.
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?
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']),
});
}
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'
}
}
]
});

Routing guards in Framework 7 Vue

When I do the following:
{
path: '/chat/',
async(routeTo, routeFrom, resolve, reject) {
if (localStorage.getItem('token')) {
resolve({
component: require('./assets/vue/pages/chat.vue'),
});
} else {
resolve({
component: LoginPage
});
}
},
}
Everything works as expected, but if I do this:
{
path: '/chat/',
component: require('./assets/vue/pages/chat.vue'),
async(routeTo, routeFrom, resolve, reject) {
if (localStorage.getItem('token')) {
resolve();
} else {
resolve({
component: LoginPage
});
}
},
}
Then the component always resolves, regardless of the async. This is also the case when I try to use a beforeEnter function instead of async; if the component is defined at the top level of the route, it always resolves.
How could I put an authentication middleware on a route?
Maybe try this.
const checkAuth = (to, from, resolve, reject) => {
if (localStorage.getItem('token')) {
resolve({ component: routeComponentMap[to.name] })
} else {
resolve({ component: LoginPage })
}
}
const routeComponentMap = {
CHAT: require('./assets/vue/pages/chat.vue')
}
const routes = [{
path: "/chat/",
name: "CHAT",
async: checkAuth
}]

Occurs dead loop in router.beforeEach in vue, vue-router

I have issue that i still cant fix it after i try some solution thought google search.
I am using router.beforeEach to check token exist.
If I no login and input localhost/overview in url bar directly,
it need to redirect login page and url redirect to localhost/login but fail and show the error message below,
Maximum call stack size exceeded
How can i modify the router.beforeEach method ?
In router.js
import Vue from 'vue'
import store from '#/store/index'
import Router from 'vue-router'
import Overview from '#/components/Overview'
import Login from '#/components/Login'
Vue.use(Router)
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'Login',
component: Login,
meta: {
requiresAuth: false
}
},
{
path: '/overview',
name: 'Overview',
component: Overview,
meta: {
requiresAuth: true
}
},
{
path: '*',
redirect: '/login',
meta: {
requiresAuth: false
}
}
]
}
router.beforeEach((to, from, next) => {
let token = store.state.token
if (token) {
if (to.query.redirect) {
next({path: to.query.redirect})
} else {
if (['/login'].includes(to.path)) {
next({path: '/overview'})
} else {
next()
}
}
} else {
let requiresAuth = to.matched.some(record => record.meta.requiresAuth)
if (requiresAuth) {
next({path: '/login', query: { redirect: to.fullPath }})
} else {
next()
}
}
})
export default router
Be sure that your logging path does not require auth itself/that your mechanism to detect if a page needs auth is correct.
Keep in mind that your navigation guard function will be called each time a route is reached, including when you are redirecting the user programatically in your navigation guard itself. So you should take that in account to create a function that will not loop (i.e trigger a redirect that trigger a redirect...).

Vue router navigation gaurd from within the component

I use vuex from centralized state management
in my vuex store.js i store the login status as a boolean value like below
export const store = new Vuex.Store({
state: {
loggedIn: false,
userName: 'Guest',
error: {
is: false,
errorMessage: ''
}
},
getters: {
g_loginStatus: state => {
return state.loggedIn;
},
g_userName: state => {
return state.userName;
},
g_error: state => {
return state.error;
}
}
)};
When the user logs in i set the loginstatus to true and remove the login button and replace it with log out button
everything works fine but the problem is when the user is logged in and if i directly enter the path to login component in the search bar i am able to navigate to login page again
I want to preent this behaviour
If the uses is logged in and searches for the path to loginpage in the searchbar he must be redirected to home page
I have tried using beforeRouteEnter in the login component
But we do not have acess to the this instance since the component is not yet loaded
So how can i check for login status from my store
my script in login.vue
script>
export default{
data(){
return{
email: '',
password: ''
};
},
methods: {
loginUser(){
this.$store.dispatch('a_logInUser', {e: this.email, p: this.password}).then(() =>{
this.$router.replace('/statuses');
});
}
},
beforeRouteEnter (to, from, next) {
next(vm => {
if(vm.$store.getters.g_loginStatus === true){
//what shall i do here
}
})
}
}
It is much better to put the navigation guards in routes not in pages/components and call the state getters on route file.
// /router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'
// Protected Pages
import Dashboard from '#/views/dashboard'
// Public Pages
import Dashboard from '#/views/login'
Vue.use(Router)
// If you are not logged-in you will be redirected to login page
const ifNotAuthenticated = (to, from, next) => {
if (!store.getters.loggedIn) {
next()
return
}
next('/') // home or dashboard
}
// If you are logged-in/authenticated you will be redirected to home/dashboard page
const ifAuthenticated = (to, from, next) => {
if (store.getters.loggedIn) {
next()
return
}
next('/login')
}
const router = new Router({
mode: 'history',
linkActiveClass: 'open active',
scrollBehavior: () => ({ y: 0 }),
routes: [
{
path: '/',
redirect: '/dashboard',
name: 'Home',
component: Full,
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: Dashboard,
beforeEnter: ifAuthenticated
},
]
},
{
path: '/login',
name: 'Login',
component: Login,
beforeEnter: ifNotAuthenticated
},
{
path: '*',
name: 'NotFound',
component: NotFound
}
]
})
export default router
You can also use vue-router-sync package to get the value of store values
You can redirect the user to the home page or some other relevant page:
mounted () {
if(vm.$store.getters.g_loginStatus === true){
this.$router('/')
}
}
beforeRouteEnter (to, from, next) {
next(vm => {
if(vm.$store.getters.g_loginStatus === true){
next('/')
}
})
}
From the docs:
next: Function: this function must be called to resolve the hook. The action depends on the arguments provided to next:
next(): move on to the next hook in the pipeline. If no hooks are left, the navigation is confirmed.
next(false): abort the current navigation. If the browser URL was changed (either manually by the user or via back button), it will be reset to that of the from route.
next('/') or next({ path: '/' }): redirect to a different location. The current navigation will be aborted and a new one will be started.