I have a Nuxt Js 3 + Firebase project. In this project, login is required to access the admin panel.
The code sections I wrote below are working properly.
However, as you can see in the Gif, when I go to the /admin/dashboard page without logging in, the page comes for a few seconds, that is, it renders and sends it to the login page.
What I want is to first check if you are logged in, if you are not logged in, it does not go to /admin/dashboard and stays on the login page.
//middleware/isAuth.js
import { auth } from "~~/firebase/config";
export default defineNuxtRouteMiddleware(async (to, from) => {
await auth.onAuthStateChanged(async (user) => {
if (user == null && to.name != "admin") {
return await navigateTo("/admin");
} else if (user && to.name == "admin") {
return navigateTo("/admin/dashboard");
} else if (user && to.name != "admin") {
return navigateTo(from.fullPath);
}
});
});
I solved this problem without using middleware with a little trick
// layouts/admin/default.vue
<template>
<div v-if="loading">loading</div>
<div v-else>
....
</div>
<template>
<script setup>
import { useAuthStore } from "~~/stores/authStore";
import { auth } from "#/firebase/config";
const currentUser = ref(false);
const loading = ref(true);
const router = useRouter();
const authStore = useAuthStore();
auth.onAuthStateChanged(async () => {
loading.value = true;
currentUser.value = await auth.currentUser;
if (currentUser.value) {
await authStore.setCurrentUser(currentUser.value);
router.push("/admin/dashboard");
} else {
router.push("/admin");
}
loading.value = false;
});
</script>
Related
TL;DR: I found a solution to my problem described below, but I am not sure if it's a good solution (good in the sense that it does not cause any other problems, of which I might not be aware yet)
In my Next.js project, all pages have a Layout component, which is wrapped around any child-components. It has a useEffect-hook, which checks if a user isAuthenticated and hence can access pages where the pathIsProtected or not. This is done via the check_auth_status action creator, which decides about the state of isAuthenticated.
This is my Layout:
// components/ui/Layout.js
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useDispatch, useSelector } from "react-redux";
import { check_auth_status } from "../../store/authActions";
import FullPageLoader from "../ui/FullPageLoader";
const Layout = ({ protectedRoutes, children }) => {
const router = useRouter();
const dispatch = useDispatch();
const loading = useSelector((state) => state.auth.loading);
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
// check if current route is in the pathIsProtected array, if yes return true, if no return false
const pathIsProtected = protectedRoutes.indexOf(router.pathname) !== -1;
useEffect(() => {
if (dispatch && (dispatch !== null) & (dispatch !== undefined))
dispatch(check_auth_status());
if (!loading && !isAuthenticated && pathIsProtected) {
router.push("/login");
}
}, [dispatch, loading, isAuthenticated, pathIsProtected, router]);
if ((loading || !isAuthenticated) && pathIsProtected) {
return <FullPageLoader />;
}
return <div className="min-h-full">{children}</div>;
};
export default Layout;
This is my check_auth_status:
// check_auth_status action creator function
export const check_auth_status = () => async (dispatch) => {
try {
const res = await fetch("/api/account/verify", {
method: "GET",
headers: {
Accept: "application/json",
},
});
if (res.status === 200) {
dispatch(authActions.authenticatedSuccess());
} else {
dispatch(authActions.authenticatedFail());
}
} catch (err) {
dispatch(authActions.authenticatedFail());
}
};
My problem with this is the following: When a user isAuthenticated and refreshes a page, where the pathisProtected, the following happens:
1.) As expected - the page gets rendered with the FullPageLoader because the state of isAuthenticated is initially false.
2.) As expected - the page gets re-rendered for a second with its proper child components, because check_auth_status sets isAuthenticated to true
3.) Not as expected - I get redirected to the login-page, but since the user isAutheticated, after a second I get redirected back again to my original page, rendered with any of its proper child components.
My Question: Why is this happening? My own explanation is that there is a race condition between check_auth_status updating isAuthenticated and the redirection to the login-page if a user is !isAuthenticated right after it.
My solution (which works, but I am not sure if not unproblematic) is to wrap everything inside an async function, await the promise from check_auth_status in quickAuthCheck and check the value quickAuthCheck instead of the state of isAuthenticated
This is my updated Layout:
// components/ui/Layout.js
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useDispatch, useSelector } from "react-redux";
import { check_auth_status } from "../../store/authActions";
import FullPageLoader from "../ui/FullPageLoader";
const Layout = ({ protectedRoutes, children }) => {
const router = useRouter();
const dispatch = useDispatch();
const loading = useSelector((state) => state.auth.loading);
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
const pathIsProtected = protectedRoutes.indexOf(router.pathname) !== -1;
useEffect(() => {
const checkAuth = async () => {
if (dispatch && (dispatch !== null) & (dispatch !== undefined)) {
const quickAuthCheck = await dispatch(check_auth_status());
if (!loading && !quickAuthCheck && pathIsProtected)
router.push("/login");
}
};
checkAuth();
}, [dispatch, loading, isAuthenticated, pathIsProtected, router]);
if ((loading || !isAuthenticated) && pathIsProtected) {
return <FullPageLoader />;
}
return <div className="min-h-full">{children}</div>;
};
export default Layout;
This is my updated check_auth_status:
export const check_auth_status = () => async (dispatch) => {
try {
const res = await fetch("/api/account/verify", {
method: "GET",
headers: {
Accept: "application/json",
},
});
if (res.status === 200) {
dispatch(authActions.authenticatedSuccess());
return(true)
} else {
dispatch(authActions.authenticatedFail());
return(false)
}
} catch (err) {
dispatch(authActions.authenticatedFail())
return(false);
}
};
We are using a token-based authentification and, in some cases, keep token in sessionStorate. When a user opens a new tab, we copy this token from any other opened tab and reload the user object.
All this logic lives in a component that wraps everything else:
export const App = (): JSX.Element => (
<AppPreloader>
<Provider store={store}>
<Routes />
</Provider>
</AppPreloader>
);
The component AppPreloader:
import React, { FC, useState } from 'react';
import { useEffectOnce, useUpdateEffect } from 'react-use';
import { getAnyToken } from '../../../redux/auth/auth.utils';
export const AppPreloader: FC = ({ children }) => {
const persistToken = window.localStorage.getItem('token');
const anyToken = getAnyToken(); // check if any token exists
const [isPrepared, setIsPrepared] = useState(!!anyToken);
// if token is in localStorage we skip all this
if (!persistToken) {
window.addEventListener('storage', event => {
const token = window.sessionStorage.getItem('token');
// this part is trigered on every tabs but executed only where token exists
if (event.key === 'REQUESTING_SHARED_CREDENTIALS' && token) {
window.localStorage.setItem('CREDENTIALS_SHARING', token);
window.localStorage.removeItem('CREDENTIALS_SHARING');
}
// this part is executed after CREDENTIALS_SHARING event and only executed in tabs where token doesn't exist. This part recreate token in the new tab in sessionStorage.
if (
event.key === 'CREDENTIALS_SHARING' &&
!token &&
event.newValue !== null
) {
window.sessionStorage.setItem('token', event.newValue);
setIsPrepared(true);
}
if (event.key === 'CREDENTIALS_FLUSH' && token) {
window.sessionStorage.removeItem('token');
}
});
}
// when component is mounted we trigger event in localStorage and this event appears in all tabs simultaneously
useEffectOnce(() => {
if (!persistToken) {
window.localStorage.setItem(
'REQUESTING_SHARED_CREDENTIALS',
Date.now().toString(),
);
window.localStorage.removeItem('REQUESTING_SHARED_CREDENTIALS');
}
const timeOut = setTimeout(() => setIsPrepared(true), 200);
return () => clearTimeout(timeOut);
});
useUpdateEffect(() => {
anyToken && setIsPrepared(true);
});
return <>{isPrepared ? children : <div>LOADING TOKEN</div>}</>;
};
I try to do something like:
render element
set in sessionStorage token
create new session
render element here
check if token exists in new sessionStorage.
Any idea how to do it? I can't simulate the behavior of 2 open tabs in jest.
So I'm working on a react native authentication screen. I'm storing the token in AsyncStorage (yea I know it's not the best solution).
So what's happening is when I log in, the token is stored, but the getItem on my Authentication.js screen is not being triggered, and the profile screen is not being called.
If I log in and then manually refresh the app, I am redirected to the profile screen.
Login.js
function Login({navigation}) {
const [signIn, {data}] = useMutation(USER_SIGNIN_MUTATION);
const [userName, setUserName] = useState('');
const [password, setPassword] = useState('');
function handleLogIn() {
signIn({
variables: {
email: userName,
password: password,
},
});
}
useEffect(() => {
if (data != null) {
setToken();
}
});
const setToken = async () => {
try {
console.log('before');
await AsyncStorage.setItem('token', data.signIn.token);
console.log('after');
} catch (error) {
console.log(error);
}
};
return(
...
)
}
Authentication.js
function Authentication() {
const [localToken, setLocalToken] = useState(false);
useEffect(() => {
const fetchUser = async () => {
try {
console.log('before get');
const userData = await AsyncStorage.getItem('token');
if (userData !== null) {
setLocalToken(true);
}
} catch (error) {
console.log(error);
}
};
fetchUser();
}, [localToken]);
console.log(`auth screen - ${localToken}`);
return (
<NavigationContainer>
{localToken === true ? <ProfileStack /> : <AuthStack />}
</NavigationContainer>
);
}
export default Authentication;
also same happens with the logout function when fired. (the function runs, but I need to refresh the app to get back to the login screen)
Profile.js
function Profile({navigation}) {
function signOut() {
logOut();
}
const logOut = async () => {
try {
console.log('before clear');
await AsyncStorage.removeItem('token');
console.log('after clear');
} catch (error) {
console.log(error);
}
};
return (
...
)
}
I'm grateful for any insight on this.
useEffect(() => {
const fetchUser = async () => {
try {
console.log('before get');
const userData = await AsyncStorage.getItem('token');
if (userData !== null) {
setLocalToken(true);
}
} catch (error) {
console.log(error);
}
};
fetchUser();
}, [localToken]);
Here you added the localToken variable in the dependency array of the useEffect. So you are basically saying: run this effect only if the localToken variable changes. But you change that from within the effect only. So try to remove it and keep the dependency as []. This way the effect will run when the component is rendered.
About the fact that you have to refresh the page, it is important to understand why this happens.
<NavigationContainer>
{localToken === true ? <ProfileStack /> : <AuthStack />}
</NavigationContainer>
Here you are rendering ProfileStack or AuthStack based on the localToken value. When you logout, you remove the token from the AsyncStorage but this is not enough. You actually need to trigger a rerender in the Authentication component so the localToken is reevaluated. Basically, when you logout you also need to set setLocalToken(false). So you need to access setLocalToken function from the Profile component. You can pass this function as a prop or better you can use Context API
I am trying to create a project in that login functionality is good and working properly but when I logged in and refreshed the screen the logout button disappears and the login link will come and then again logout button will come.to understand perfectly watch the video https://drive.google.com/file/d/1UvTPXPvHf4EhcrifxDEfPuPN0ojUV_mN/view?usp=sharing, this is because of
const AuthContext = React.createContext()
//useauth will return the AuthContext
export const useAuth = () => {
return useContext(AuthContext)
}
export const Authprovider = ({ children }) => {
var name
auth.onAuthStateChanged((user) => {
name = user
})
const [currentuser, setcurrentuser] = useState(name)
const [load, setload] = useState(true)
function signup(email, password) {
return auth.createUserWithEmailAndPassword(email, password)
}
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password)
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setcurrentuser(user)
setload(false)
})
return unsubscribe
}, [])
const value = {
currentuser,
signup,
login,
load,
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
I wrapped the AuthProvider component around the app component so that I can use the values like current user .
in Header component where login link, logout button is there
const { currentuser, load } = useAuth()
const logout = () => {
try {
auth.signOut().then(() => {
console.log('logged out')
})
} catch {
alert('logout is not possible')
}
}
//some code
{currentuser ? (
<button onClick={logout}>Logout</button>
) : (
<Link to='/login'>Login</Link>
)}
if there is a current user then the logout button will appear otherwise login link will appear but when refreshing there is some problem I tried many ways now I am out of ideas. "Even I refresh the page when logged in the logout button should not disappear" can you tell me how to do this?
to understan watch the video in the link
That's because you're not using load state try this:
//some code
{ load ? <div>loading</div>
: currentuser ? (
<button onClick={logout}>Logout</button>
) : (
<Link to='/login'>Login</Link>
)}
I have a redux store a the user that is currently logged in. When I refresh the page the state is lost, and want I want to do is when I refresh the any component of my I want to be redirected to the loggin page,how can I do that?
I am using LocalStorage when I make a redirect about login.
I just stored login data to LocalStorage.
Main Page Component
import React from 'react';
import { Redirect } from 'react-router-dom';
import storage from 'lib/storage';
const MainPage = () => {
function redirect(){
if(storage.get('user')){
return <Redirect to="/login" />
}else{
return null;
}
}
return (
<main>
{redirect()}
/* ... */
</main>
)
}
lib/storage.js
export default (function () {
const st = localStorage || {};
return {
set: (key, object) => {
const arr = [];
arr.push(JSON.stringify(object));
st[key] = arr;
},
add: (key, object) => {
let old = st.getItem(key);
if (old === null) old = '';
st.setItem(key, old + ((typeof object) === 'string' ? object : JSON.stringify(object)));
},
get: (key) => {
if (!st[key]) {
return null;
}
const value = st[key];
try {
const parsed = JSON.parse(value);
return parsed;
} catch (e) {
return value;
}
},
remove: (key) => {
if (localStorage) {
return localStorage.removeItem(key);
}
delete st[key];
}
}
})();
Your Store state will be lost whenever you refresh the page. To persist this state, assuming that you are developing a web application, you must use one of the browser storage options to save the user̈́'s logged state.
Short Example:
// Setting the user info to the local storage
localStorage.setItem('userId', 'Logged');
// Retrieving the information before rendering the route component
var userInfo = localStorage.getItem('userId');
Please, refer for this link to a full example.
Obs: This is a JavaScript problem, not a React.js problem