How to get something to run before vue-router - vuejs2

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'
}
}
]
});

Related

Vuex action does not work properly in a vue mounted hook

I'm building a small e-commerce store with an admin panel for myself.
I use Firebase firestore as my backend to store all the user's data.
I have a root 'users' collection with a document for every single registered user and everything else each user has is branching out of the user doc.
Here are firestore commands i perform so you understand the structure better.
db.collection('users').doc(userId).collection('categories').doc(subCategoryId)...
db.collection('users').doc(userId).collection('subcategories').doc(subCategoryId)...
I use Vuex so every time i need to change something on my firestore (update a product category, remove a category etc.), i dispatch an appropriate action.
The first thing any of those actions does is to go ahead and dispatch another action from auth.js that gets the userId.
The problem is that if the action in question should run in a mounted() lifecycle hook, then it fails to grab the userId.
In EditCategory.vue updateCategory action works perfectly well because SubmitHandler() is triggered on click event but in Categories.vue the fetchCategories does not work and spit out an error:
[Vue warn]: Error in mounted hook (Promise/async): "FirebaseError: [code=invalid-argument]: Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: null"
Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: null
Which, as far as i understand it, basically tells me that fetchCategories() action's firestore query could not be performed because the userId was not recieved.
After two days of moving stuff around i noticed that errors only occur if i refresh the page. If i switch to other tab and back on without refreshing, then fetchCategories() from Categories.vue mounted() hook works. Placing the code in to created() hook gives the same result.
I think that there is some fundamental thing i am missing about asynchronous code and lifecycle hooks.
Categories.vue component
<template>
<div class="category-main">
<section>
<div class="section-cols" v-if="!loading">
<EditCategory
v-on:updatedCategory="updatedCategory"
v-bind:categories="categories"
v-bind:key="categories.length + updateCount"
/>
</div>
</section>
</div>
</template>
<script>
import EditCategory from '#/components/admin/EditCategory.vue'
export default {
name: 'AdminCategories',
components: {
EditCategory,
},
data: () => ({
updateCount: 0,
loading: true,
categories: [],
}),
async mounted() {
this.categories = await this.$store.dispatch('fetchCategories');// FAILS!
this.loading = false;
},
methods: {
addNewCategory(category) {
this.categories.push(category);
},
updatedCategory(category) {
const catIndex = this.categories.findIndex(c => c.id === category.id);
this.categories[catIndex].title = category.title;
this.categories[catIndex].path = category.path;
this.updateCount++;
}
}
}
</script>
category.js store file
import firebase, { firestore } from "firebase/app";
import db from '../../fb';
export default {
actions: {
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
},
export default {
state: {
test: 10,
categories: [],
subCategories: [],
currentCategory: '',
},
mutations: {
setCategories(state, payload){
state.categories = payload;
},
},
actions: {
async fetchCategories({commit, dispatch}) {
try {
const userId = await dispatch('getUserId');
const categoryArr = [];
await db.collection('users').doc(userId).collection('categories').get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
categoryArr.push({ id: doc.id, ...doc.data() })
});
})
commit('setCategories', categoryArr);
return categoryArr;
} catch (err) { throw err; }
},
async updateCategory({commit, dispatch}, {title, path, id}) {
try {
const userId = await dispatch('getUserId');
console.log('[category.js] updateCategory', userId);
await db.collection('users').doc(userId).collection('categories').doc(id).update({
title,
path
})
commit('rememberCurrentCategory', id);
return;
} catch (err) {throw err;}
}
},
}
auth.js store file
import firebase, { firestore } from "firebase/app";
import db from '../../fb';
export default {
actions: {
...async login(), async register(), async logout()
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
},
},
}
index.js store file
import Vue from 'vue'
import Vuex from 'vuex'
import auth from './auth'
import products from './products'
import info from './info'
import category from './category'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
auth, products, info, category,
}
})
EditCategory.vue
export default {
name: 'EditCategory',
data: () => ({
select: null,
title: '',
path: '',
current: null
}),
props: {
categories: {
type: Array,
required: true
}
},
methods: {
async submitHandler() {
if (this.$v.invalid){
this.$v.$touch()
return;
}
try {
const categoryData = {
id : this.current,
title: this.title,
path: this.path
};
await this.$store.dispatch('updateCategory', categoryData);// WORKS!
this.$emit('updatedCategory', categoryData);
} catch (err) { throw err; }
},
},
//takes current category id from store getter
computed: {
categoryFromState() {
return this.$store.state.currentCategory;
}
},
created() {
console.log('[EditCategory.vue'], currentCategory);
},
mounted(){
this.select = M.FormSelect.init(this.$refs.select);
M.updateTextFields();
},
destroyed() {
if (this.select && this.select.destroy) {
this.select.destroy;
}
}
}
</script>
First of all, it's just a small detail, but you don't need need to make your 'getUserId' action async, since it does not use the 'await' keyword. So can simplify this :
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
}
const userId = await dispatch('getUserId')
into this :
getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
}
const userId = dispatch('getUserId')
Coming back to your id that seems to be undefined, the problem here is that your 'mounted' event is probably triggered before the 'login' can be completed.
How to solve this case ? Actually, there are a lot of different ways to approch this. What I suggest in your case is to use a middleware (or a 'route guard'). This guard can make you are verified user before accessing some routes (and eventually restrict the access or redirect depending on the user privileges). In this way, you can make sure that your user is defined before accessing the route.
This video is 4 years old so it is not up to date with the last versions of Firebas. But I suggest The Net Ninja tutorial about Vue Route Guards with Firebase if you want to learn more about this topic.
Accepted answer actually pointed me to the correct direction.
In my case i had to make a route guard for child routes.
router.vue
import Vue from 'vue'
import Router from 'vue-router'
import firebase from 'firebase/app';
Vue.use(Router);
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Home.vue')
},
{
path: '/bouquets',
name: 'bouquets',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Bouquets.vue')
},
{
path: '/sets',
name: 'sets',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Sets.vue')
},
{
path: '/cart',
name: 'cart',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Cart.vue')
},
{
path: '/login',
name: 'login',
meta: {layout: 'empty-layout'},
component: () => import('./views/empty/Login.vue')
},
{
path: '/register',
name: 'register',
meta: {layout: 'empty-layout'},
component: () => import('./views/empty/Register.vue')
},
{
path: '/admin',
name: 'admin',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Home.vue'),
children: [
{
path: 'categories',
name: 'adminCategories',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Categories'),
},
{
path: 'subcategories',
name: 'adminSubcategories',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/SubCategories'),
},
{
path: 'products',
name: 'adminProducts',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Products'),
},
]
},
{
path: '/checkout',
name: 'checkout',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Checkout.vue')
},
{
path: '/:subcategory',
name: 'subcategory',
meta: {layout: 'main-layout'},
props: true,
params: true,
component: () => import('./views/main/Subcategory.vue')
},
]
})
router.beforeEach((to, from, next) => {
//if currentUser exists then user is logged in
const currentUser = firebase.auth().currentUser;
//does a route has auth == true
const requireAuth = to.matched.some(record => record.meta.auth);
//if auth is required but user is not authentificated than redirect to login
if (requireAuth && !currentUser) {
// next('/login?message=login');
next('login')
} else {
next();
}
})
export default router;
category.js fetchCategories() action
async fetchCategories({commit, dispatch}) {
const userId = await dispatch('getUserId')
try {
const categoryArr = [];
await db.collection('users').doc(userId).collection('categories').get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
categoryArr.push({ id: doc.id, ...doc.data() })
});
})
commit('setCategories', categoryArr);
return categoryArr;
} catch (err) { throw err; }
},

Nuxt js Router Push Not Working After Delete

I've created a simple CRUD with Nuxt. Data is provided by Lumen. I got a problem with the DELETE, data is deleted but Nuxt does not redirect to the other page.
Here is my script:
<script>
export default {
name: 'EmployeePage',
data() {
return {
fields: ['name','email','image','address'],
emplyees:[],
}
},
mounted() {
this.$axios.get('/employee').then(response => {
this.pegawais = response.data.data
}).catch(error => {
console.log(error.response.data)
})
},
methods: {
async delete(id) {
await this.$axios.delete(`/employee/${id}`).then(response => {
this.$router.push({ name: 'employee' }) <-----this redirect not working
})
}
}
}
</script>
I want Nuxt to redirect to the employee page that display all the data after the deletion.
You should not mix async/await and .then. Use the first approach, that way you will not have the .then callback hell and it will be cleaner overall.
Like this
<script>
export default {
name: 'EmployeePage',
data() {
return {
fields: ['name', 'email', 'image', 'address'],
emplyees: [],
}
},
async mounted() {
try {
const response = await this.$axios.get('/employee')
this.pegawais = response.data.data
} catch (error) {
console.log(error.response.data)
}
},
methods: {
async delete(id) {
await this.$axios.delete(`/employee/${id}`)
await this.$router.push({ name: 'employee' })
},
},
}
</script>
await this.$router.push does not require an await but it's a Promise too, so I'm writing it like that in case you need to call something else afterwards.
this.$axios.$get('/employee') can also be used if you want to remove a .data aka this.pegawais = response.data as shown here.

How to create a middleware or how to manage the below route for front end and admin

How can I manage the url for front and admin panel Via Middleware in Vue.
This is the code I have written in router/index.js file:
const router = new VueRouter({ mode: 'history', routes });
router.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const loggedIn = localStorage.getItem('usertoken') == null ? false : true;
const user = JSON.parse(localStorage.getItem('user'));
//this is for admin
next('/admin/login')
next('/admin/home');
//this is my front URL
next('/terms-condition');
next('/home');
next()
})
export default router;
See the below code it may helps you
/**
* middleware for authentication
*/
router.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const loggedIn = localStorage.getItem('usertoken') == null ? false : true;
const user = JSON.parse(localStorage.getItem('user'));
if (to.meta.portal == 'admin') {
if (to.meta.auth) {
if (!loggedIn) {
next('/admin/login')
} else if (loggedIn) {
next();
}
} else {
if (!loggedIn) {
next();
} else if (loggedIn) {
if (user.role_id == '1') {
next('/admin/home');
} else {
next('/');
}
}
}
} else if (to.meta.portal == 'front') {
if (loggedIn) {
if (user.role_id == '1') {
next('/admin/home');
} else {
next('/');
}
} else if (!loggedIn) {
if (to.path == "/admin") {
next('/admin/login');
} else {
next();
}
}
}
next()
})
export default router;
And you need to create two router files one for front and other for admin:
//front route file will look like
export default [{
path: '/',
meta: { auth: false, portal: 'front' },
component: () => import('#/components/layouts/front/main.vue'),
children: [
{
path: '/',
name: 'front-home',
title: 'Dashboard',
meta: { auth: false, portal: 'front' },
}
]
}]
//admin router file will be like
export default [
{
path: 'user',
name: 'users',
title: 'Users',
meta: { auth: true, portal: 'admin' },
component: () => import('#/components/templates/admin/user'),
}
]
Main difference is the portal that defines which portal will access by the respective route.Without portal inside meta it won't work.
The way you have implemented is correct
Once the user is successfully logged in , use if else condition to redirect to admin panel, also use Navigation guards given in vue-router
https://router.vuejs.org/guide/advanced/navigation-guards.html#per-route-guard
This help to prevent the other user to use this url directly

Framework7 Route Protection

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();
}

Client side OAuth2 app with VueJS - Server component needed?

I am new to building JavaScript client-side apps with OAuth2.
I am using a JavaScript library that uses localStorage for OAuth implementation which implements Implicit Flow where only a client-id is needed -- no client-secret.
My corporation uses an enterprise identity provider/auth server (not Social networks) for issuing tokens. My custom app checks for the token on return and then proceeds with navigation using Vue Router's navigation guards.
Question:
From a security perspective, is the below code a good practical starting point? I plan to convert this into a JavaScript single page application (Webpack) where the app won't load protected pages unless a token is found (just the welcome/landing page is shown with an lOGIN button for auth). Is some kind of server validation necessary or is the client library good enough?
Here's a simple example of my auth flow (using Google as the Auth
server for demonstration purposes)
let client = new jso.JSO({
providerID: "google",
default_lifetime: 1800,
client_id: '<my-app-id>',
redirect_uri: "http://localhost/overview",
authorization: "https://accounts.google.com/o/oauth2/auth",
scopes: { request: ["https://www.googleapis.com/auth/userinfo.profile"] }
})
// check the browser response params when user returns
client.callback()
const LandingPage = Vue.component('landing-page', {
template: `<div>
<h2>Thisis the landing page.</h2>
<button v-on:click="oauthLogin">Login</button>
</div>`,
methods: {
oauthLogin() {
// get the token from the response params
client.getToken()
}
}
})
const OverView = { template: '<div><h1>Welcome!</h1><p>Here is what site is about.</p></div>' }
const PResource = { template: '<h2>Protected Resource Content Here</h2>' }
const router = new VueRouter({
mode: 'history',
routes: [
// {
// path: '*',
// component: NotFound
// },
{
path: '/',
component: LandingPage,
meta: {
requiresAuth: false
}
},
{
path: '/overview',
component: OverView,
meta: {
requiresAuth: true
}
},
{
path: '/protected-resource',
component: PResource,
meta: {
requiresAuth: true
}
}
]
})
router.beforeEach((to, from, next) => {
const token = window.localStorage.getItem('tokens-google')
if (to.matched.some(record => record.meta.requiresAuth)) {
//if token exists, continue user to protected resource
if (token !== null) {
next()
} else {
//if no token exists, send to a landing page
next({
path: '/',
query: {
redirect: to.fullPath
}
})
}
} else {
// if token exists and user navigates to root, redirect to /overview
if (token !== null && to.path == '/') {
next({ path: '/overview' })
} else {
next() // make sure to always call next()!
}
}
})
var app = new Vue({
el: '#app',
computed: {
checkToken() {
return window.localStorage.getItem('tokens-google')
}
},
router
})
Thanks!