Vue + Firebase route to prevent user back to login page after login - firebase

I develop SPA using VueJS 3 and firebase. I want prevent user from accessing login page after they login. But it seems the routes are keep looped or error after trying different code.
the routes:
{
path: '/', name: 'login', component: () => import('../views/LoginView.vue')
},
{
path: '/dashboard', name: 'dashboard', component: () => import('../views/DashboardView.vue'),
meta: {
requiresAuth: true,
},
},
the logic(didnt show anything):
const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const removeListener = onAuthStateChanged(
getAuth(),
(user) => {
removeListener()
resolve(user)
},
reject
)
})
}
router.beforeEach(async (to, from, next) =>{
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (await getCurrentUser()) {
next()
} else {
next("/")
}
} else {
next("/dashboard")
}
})
this one keep looping:
router.beforeEach(async (to, from, next) =>{
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (await getCurrentUser()) {
next()
} else {
next("/")
}
}
next("/dashboard")
})

As mentionned in the Vue Router Doc :
You must call "next" exactly once in any given pass through a 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.:
They provide those examples :
// BAD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// if the user is not authenticated, `next` is called twice
next()
})
// GOOD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
Also, make sure you do not have a infinite loop :
next("/dashboad") forces a redirection to the Dashboard page, no matter what page we come from. You may end in a loop where every page redirects to Dashboard, including the Dashboard page itself

Related

firebase auth with vue router

I have the authorization check code and it works. Now I need to implement path protection for an unauthorized user.
The problem is that the function in the storage does not have time to work out as it is already necessary to go along the path. AuthState and LoginStatus
try do it from getters, get actual state and try get data from state, but nothing happened
When I reload the page or clear the cache everything resets
//STORE
// call it first from app in created()
state: () => ({
isAuthReady: null,
}),
async AuthState({ dispatch }) {
await auth.onAuthStateChanged((userFirebase) => {
dispatch("LoginStatus", userFirebase);
});
},
LoginStatus({ commit, dispatch }, user) {
//console.log(user)
if (user) {
commit("setAuthReady", true);
commit("setUser", user);
dispatch("UserProfile", user);
dispatch("isAdmin");
} else {
// User is signed out
// ...
}
},
//ROUTER
{
path: "/admin",
component: () => import("#/pages/adminPage/admin"),
meta: { requiresAuth: true },
}
router.beforeEach(async (to, from, next) => {
if (to.meta.requiresAuth) {
if (store.state.user.userInfo.length || store.state.user.userInfo.id) {
next();
} else {
await store.dispatch("auth/openLoginForm");
next("/");
}
} else next();
});
I don’t know if I did it right, but as recommended in this Answer, I think this is possible in firebase.
router.beforeEach((to, from, next) => {
auth.onAuthStateChanged(async (userFirebase) => {
if (to.meta.requiresAuth) {
if (userFirebase) {
next();
} else {
await store.dispatch("auth/openLoginForm");
next("/");
}
} else next();
});
});

Why are my redux actions not firing correctly?

I am trying to implement a check for authentication and to login/logout users using redux and firebase. I have the following code:
Action Types:
export const LOGIN_REQ = 'AUTH_REQ';
export const LOGOUT_REQ = 'LOGOUT_REQ';
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
export const AUTH_FAILED = 'AUTH_FAILED';
export const GET_AUTH = 'GET_AUTH';
Reducers:
import * as ActionTypes from './ActionTypes';
export const auth = (state = {
isAuth: false,
user: null
}, action) => {
switch (action.type) {
case ActionTypes.LOGIN_REQ:
return { ...state, isAuth: false, user: null };
case ActionTypes.LOGOUT_REQ:
return { ...state, isAuth: false, user: null };
case ActionTypes.AUTH_FAILED:
return { ...state, isAuth: false, user: null };
case ActionTypes.AUTH_SUCCESS:
return { ...state, isAuth: true, user: action.payload };
case ActionTypes.GET_AUTH:
return state;
default:
return state;
}
}
Thunks:
export const getAuth = () => (dispatch) => {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
console.log('Get AUTH called');
dispatch(authSuccess());
}
else {
console.log('Get AUTH called');
dispatch(authFailed());
}
});
}
export const loginReq = (email, password, remember) => (dispatch) => {
firebase.auth().signInWithEmailAndPassword(email, password)
.then((cred) => {
if (remember === false) {
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
console.log('Logged In with Redux without persist');
}
else {
console.log('Logging in with Persist');
}
console.log('Dispatching Success !');
dispatch(authSuccess(cred.user.uid));
})
.catch((err) => {
console.log(err);
dispatch(authFailed(err));
});
}
export const logoutReq = () => (dispatch) => {
firebase.auth().signOut()
.then(() => dispatch(getAuth()))
.catch((err) => console.log(err));
}
export const authSuccess = (uid = null) => ({
type: ActionTypes.AUTH_SUCCESS,
payload: uid
});
export const authFailed = (resp) => ({
type: ActionTypes.AUTH_FAILED,
payload: resp
});
And I am calling it from a component as shown below:
const mapStateToProps = state => {
return {
isAuth: state.isAuth,
user: state.user
}
}
const mapDispatchToProps = dispatch => ({
getAuth: () => { dispatch(getAuth()) },
loginReq: (email, password, remember) => { dispatch(loginReq(email, password, remember)) },
logoutReq: () => { dispatch(logoutReq()) }
})
handleLogin() {
this.props.loginReq(this.state.email, this.state.password, this.state.remember);
}
handleLogOut() {
this.props.logoutReq();
}
<BUTTON onClick=()=>this.handleLogOut()/handleLogin()>
I am close to tears because I cannot figure out why my loginReq fires one or many gitAuth() methods even when i click on the button once. This happens only for the loginReq() action. I have not specified anywhere that loginReq() should fire it.
Also i have called the getAuth() method in the component did mount method of my main screen which checks authentication status once at the start of the app.
EDIT: I have console logged in the component did mount method in the main component so I know that this getAuth() call is not coming from there.
Imo the answer is badly done, try to reestructure it better, what you call "Thunks" are actually "Actions". But if I were to tell you something that could help is that maybe the problem lies in the thunk middleware config or with the way firebase is beign treated by the dispatcher, so I would say that you better try coding an apporach with the react-redux-firebase library (this one: http://react-redux-firebase.com/docs/getting_started ) it makes easier to connect redux with a firebase back end. Other great reference, the one that I learned with, is The Net Ninja's tutorial playlist about react, redux and firebase.
A friend of mine told me this has to do with something known as an 'Observer' which is in the onAuthStateChange() provided by firebase. Basically there is a conflict between me manually considering the user as authenticated and the observer doing so.

Vue.js: How to protect login component from authenticated user

I'm designing an application where auth is implemented using firebase-auth. I have already protected the user component from unauthenticated users. But I now want to protect the login component from authenticated users. I've tried this so far. Here is my router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import {
fb
} from "#/firebase/init";
import Home from '#/views/Home'
Vue.use(VueRouter)
const routes = [{
path: '/',
name: "home",
component: Home
},
{
path: '/login',
name: 'login',
component: () => import('#/components/auth/Login'),
meta: {
alreadyAuth: true
}
},
{
path: '/signup',
name: 'signup',
component: () => import('#/components/auth/Signup'),
},
{
path: '/user/:id',
name: 'user',
component: () => import('#/components/user/User'),
meta: {
requiresAuth: true
}
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
router.beforeEach((to, from, next) => {
let user = fb.auth().currentUser
if (to.matched.some(rec => rec.meta.requiresAuth)) {
if (user) {
next()
} else {
next({
name: 'login'
})
}
} else if (to.matched.some(rec => rec.meta.alreadyAuth)) {
if (user) {
next({
name: 'user'
})
} else {
next()
}
} else {
next()
}
})
export default router
And one more problem is that when I'm authenticated and refresh the user component it redirects me back to the login component. Why so?
Thanks in advance
Since I don't know your main.js this is just an assumption, but I think currentUser is undefined. You should log user and check if it is defined on page load. Firebase acts async, so in order to have everything prepared you need to wrap your whole application in a firebase callback.
firebase.auth().onAuthStateChanged(() => {
new Vue({
router,
render: h => h(App)
}).$mount('#app');
});
Otherwise currentUser probably is not yet initialized.
So you want to block logged-in users from hitting your login route, but you also want to stop non-logged in users from hitting certain pages in your Vue app. Pretty standard use case. Here's how I implemented it:
First, my routes have meta tags requiresAuth and forVisitors. If the user is logged in and tries to hit a forVisitors route, I return the user to the homepage (routeObject.path = '/' in my example below). If the user is not logged in and tries to hit a requiresAuth path, I kick them to /login. I found this approach is pretty flexible for route construction.
My router needs to know what constitutes a logged-in user. Using Firebase's auth module, I can subscribe to onAuthStateChanged and figure out whether the user is logged in from the first emission.
Note, I store the current user in the VueX store (store.state.userModule.user), you can ignore that bit if you do not. Also I store the Firebase Auth object in my store as store.state.userModule.auth, so when you see that, it's the same as Firebase auth.
Additionally, you can find more details on why I wrap my onAuthStateChanged subscription in a Promise here: Getting 'Uncaught' after catching error in firebase.auth() with async
const router = new Router({
/* example of forVisitors path */
{
path: '/Login',
name: 'Login',
component: Login,
meta: {
requiresAuth: false,
forVisitors: true
}
},
/* example of requiresAuth path */
{
path: '/Submit/:mode?/:primary?/:secondary?',
name: 'Submit',
component: Submit,
props: true,
meta: {
requiresAuth: true
}
}
});
router.beforeEach(async (to, from, next) => {
/* Check for a user in my store, or fallback to Firebase auth() user */
let currentUser =
store.state.userModule.user ||
(await new Promise((resolve) => {
store.state.userModule.auth().onAuthStateChanged(user => {
resolve(user);
});
}));
const requiresAuth = to.meta.requiresAuth;
const forVisitors = to.meta.forVisitors;
const routeObject = {};
if (forVisitors && currentUser) {
routeObject.path = '/';
} else if (requiresAuth && !currentUser) {
routeObject.path = '/login';
}
next(routeObject);
});

Change layout according to wordpress page template NuxtJs

I want to get change layout according to wordpress page template (i get data with rest api)
API Data
{
"id": 47,
"template": "test.php", // need vue template according this
}
export default {
validate ({ params }) {
return !isNaN(+params.id)
},
async asyncData ({ params, error }) {
return fetch('http://wordpress.local/wp-json/wp/v2/'+params.postType+'/'+params.id)
.then(response => response.json())
.then(res => {
return { users: res }
})
},
layout () {
return 'blog' // Change here according to wordpress page template
},
}
I found a way to pass something from middleware to store which you can use inside the layout function. Here is the basic example I put together.
middleware/get-layout.js I simulate an async call here, could also be result of axios.post() for example
export default async (ctx) => {
return new Promise((resolve, reject) => {
// you can also access ctx.params here
ctx.store.commit('setLayout', 'new');
resolve();
});
}
store/index.js nothing crazy here
export const state = () => ({
layout: ''
})
export const mutations = {
setLayout(state, layout) {
state.layout = layout;
}
}
Middleware can either be registered globally for every route in nuxt.config.js or only for pages where you need this logic.
Finally using it in page component layout property:
layout(ctx) {
return ctx.store.state.layout;
}
I tested it with new.vue inside layout folder.

Why does redux-mock-store don't show an action dispatched in catch promises?

I'm very bad when it comes to thinking of a title question, sorry for that.
My Problem:
I'm unit testing my async redux actions like it's suggested in the docs. I mock the API calls with nock and check for the dispatched actions with redux-mock-store. It works great so far, but I have one test that fails even though it clearly does work. The dispatched action neither does show up in the array returned by store.getActions() nor is the state changed in store.getState(). I'm sure that it does happen because I can see it when I test manually and observe it with Redux Dev Tools.
The only thing that is different in this action dispatch is that it is called in a promise in a catch of another promise. (I know that sounds confusing, just look at the code!)
What my code looks like:
The action:
export const login = (email, password) => {
return dispatch => {
dispatch(requestSession());
return httpPost(sessionUrl, {
session: {
email,
password
}
})
.then(data => {
dispatch(setUser(data.user));
dispatch(push('/admin'));
})
.catch(error => {
error.response.json()
.then(data => {
dispatch(setError(data.error))
})
});
};
}
This httpPost method is just a wrapper around fetch that throws if the status code is not in the 200-299 range and already parses the json to an object if it doesn't fail. I can add it here if it seems relevant, but I don't want to make this longer then it already is.
The action that doesn't show up is dispatch(setError(data.error)).
The test:
it('should create a SET_SESSION_ERROR action', () => {
nock(/example\.com/)
.post(sessionPath, {
session: {
email: fakeUser.email,
password: ''
}
})
.reply(422, {
error: "Invalid email or password"
})
const store = mockStore({
session: {
isFetching: false,
user: null,
error: null
}
});
return store.dispatch(actions.login(
fakeUser.email,
""))
.then(() => {
expect(store.getActions()).toInclude({
type: 'SET_SESSION_ERROR',
error: 'Invalid email or password'
})
})
});
Thanks for even reading.
Edit:
The setErroraction:
const setError = (error) => ({
type: 'SET_SESSION_ERROR',
error,
});
The httpPostmethod:
export const httpPost = (url, data) => (
fetch(url, {
method: 'POST',
headers: createHeaders(),
body: JSON.stringify(data),
})
.then(checkStatus)
.then(response => response.json())
);
const checkStatus = (response) => {
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
};
Because of you are using nested async function in catch method - you need to return the promise:
.catch(error => {
return error.response.json()
.then(data => {
dispatch(setError(data.error))
})
});
Otherwise, dispatch will be called after your assertion.
See primitive examples:
https://jsfiddle.net/d5fynntw/ - Without returning
https://jsfiddle.net/9b1z73xs/ - With returning

Resources