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; }
},
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.
I am trying to route to another page after getting response from adonis project. Calling to post method is working. However router.push('/') is not functioning. Only login page reloaded every time when I submitted the b-from.
async login({ commit, state }) {
console.log("Login")
try {
const response = await HTTP()
.post('/admin/login', {
email: state.loginEmail,
password: state.loginPassword
})
.then(response => {
console.log("Ok")
console.log(response.data)
if (response.data == 'UserNotFoundException') {
alert('User not found')
router.push('/')
}
if (response.data == 'PasswordMisMatchException') {
alert('password not ms')
router.push('/')
}
if (response.data.token) {
console.log(response)
//commit('setToken', response.data.token)
router.push('/')
} else {
router.push('/')
}
})
console.log(response)
//return router.push('/')
} catch (error) {
console.log(error)
}
},
Routes:
routes: [
{
name: "FullLogin",
path: "/login",
component: () => import("#/views/authentication/FullLogin"),
},
{
path: "/",
redirect: "/dashboard/docs-dashboard",
component: () => import("#/layouts/full-layout/FullLayout"),
children: [
{
name: "Dashboard",
path: "/dashboard/docs-dashboard",
component: () => import("#/views/dashboards/docsDashboard"),
},
]
}
]
router.beforeEach((to, from, next) => {
next()
})
I can't figure out why router.push('/') is not routing.
According to docs:
Note: Inside of a Vue instance, you have access to the router instance as $router. You can therefore call this.$router.push.
When I login with a user, it redirects me to the dashboard as expected. As soon as I logout and try to login again (even with another user, and WITHOUT refreshing the page) it gives me back this error in console:
I just want to redirect the user in the dashboard if authenticated, even when the page is not refreshed cause I did notice that if I refresh the page I can login without problems.
Help me if you can. Down here some code:
Login method
methods: {
...mapActions({
attempt: "auth/attempt",
}),
submit(credentials) {
axios
.post("http://127.0.0.1:8000/api/login", credentials)
.then((res) => {
// console.log(res.data);
if (res.data.success) {
this.attempt(res.data.token)
}
if (res.data.errors) {
this.loginErrors = res.data.errors;
} else {
this.$router.push({ name: 'dashboard' })
}
})
.catch((err) => {
if (
err.name !== "NavigationDuplicated" &&
!err.message.includes(
"Avoided redundant navigation to current location"
)
) {
console.log(err);
}
});
},
},
dashboard path in the router
{
path: '/dashboard',
name: 'dashboard',
component: DashboardComponent,
beforeEnter: (to, from, next) => {
if (!store.getters['auth/authenticated']) {
return next({
name: 'home'
})
}
next()
}
},
attempt action in vuex store
async attempt({ commit, state }, token) {
if (token) {
commit('SET_TOKEN', token)
}
// se non c'รจ
if(!state.token) {
return
}
try {
await axios.get('http://127.0.0.1:8000/api/user')
.then(res => {
commit('SET_USER', res.data)
})
} catch (e) {
commit('SET_TOKEN', null)
commit('SET_USER', null)
}
},
others from vuex
namespaced: true,
state: {
token: null,
form: null,
},
getters: {
authenticated(state) {
return state.token && state.form
},
user(state) {
return state.form
},
},
mutations: {
SET_TOKEN(state, token) {
state.token = token
},
SET_USER(state, data) {
state.form = data
},
},
Update: the call to attempt() should be awaited, otherwise this.$router.push({ name: 'dashboard' }) (and therefore the guard function on the /dashboard route) will be called before the call to the /api/user API has completed:
submit(credentials) {
axios
.post("http://127.0.0.1:8000/api/login", credentials)
.then(async (res) => {
// console.log(res.data);
if (res.data.success) {
await this.attempt(res.data.token)
}
if (res.data.errors) {
this.loginErrors = res.data.errors;
} else {
this.$router.push({ name: 'dashboard' })
}
})
.catch((err) => {
if (
err.name !== "NavigationDuplicated" &&
!err.message.includes(
"Avoided redundant navigation to current location"
)
) {
console.log(err);
}
});
},
next is a function that should be called exactly once (not returned).
Try changing the code in the router to:
{
path: '/dashboard',
name: 'dashboard',
component: DashboardComponent,
beforeEnter: (to, from, next) => {
if (!store.getters['auth/authenticated']) {
next({ name: 'home' })
} else {
next()
}
}
},
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();
}