I'm using firebase authentication for my app. I used useAuth hook from here. Integrate with react-router guide about redirect (Auth).
SignIn,SignOut function is working as expected. But when I try to refresh the page. It redirects to /login again.
My expected: Redirect to / route when authenticated.
I tried to add this code in PrivateRoute.js
if (auth.loading) {
return <div>authenticating...</div>;
}
So I can refresh the page without redirect to /login but it only show authenticating... when click the log out button.
Here is my code: https://codesandbox.io/s/frosty-jennings-j1m1f?file=/src/PrivateRoute.js
What I missed? Thanks!
Issue
Seems you weren't rendering the "authenticating" loading state quite enough.
I think namely you weren't clearing the loading state correctly in the useEffect in useAuth when the initial auth check was resolving.
Solution
Set loading true whenever initiating an auth check or action, and clear when the check or action completes.
useAuth
function useProvideAuth() {
const [loading, setLoading] = useState(true); // <-- initial true for initial mount render
const [user, setUser] = useState(null);
// Wrap any Firebase methods we want to use making sure ...
// ... to save the user to state.
const signin = (email, password) => {
setLoading(true); // <-- loading true when signing in
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((response) => {
setUser(response.user);
return response.user;
})
.finally(() => setLoading(false)); // <-- clear
};
const signout = () => {
setLoading(true); // <-- loading true when signing out
return firebase
.auth()
.signOut()
.then(() => {
setUser(false);
})
.finally(() => setLoading(false)); // <-- clear
};
// Subscribe to user on mount
// Because this sets state in the callback it will cause any ...
// ... component that utilizes this hook to re-render with the ...
// ... latest auth object.
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
setUser(false);
}
setLoading(false); // <-- clear
});
// Cleanup subscription on unmount
return () => unsubscribe();
}, []);
// Return the user object and auth methods
return {
loading,
user,
signin,
signout
};
}
Check the loading state in PrivateRoute as you were
function PrivateRoute({ children, ...rest }) {
const auth = useAuth();
if (auth.loading) return "authenticating";
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
Demo
Try this approach, it works for me :
const mapStateToProps = state => ({
...state
});
function ConnectedApp() {
const [auth, profile] = useAuth()
const [isLoggedIn, setIsLoggedIn] = useState(false)
useEffect(() => {
if (auth && auth.uid) {
setIsLoggedIn(true)
} else {
setIsLoggedIn(false)
}
}, [auth, profile]);
return (<Router>
<Redirect to="/app/home"/>
<div className="App">
<Switch>
<Route path="/home"><Home/></Route>
<Route path="/login"><Login styles={currentStyles}/></Route>
<Route path="/logout"><Logout styles={currentStyles}/></Route>
<Route path="/signup" render={isLoggedIn
? () => <Redirect to="/app/home"/>
: () => <Signup styles={currentStyles}/>}/>
<Route path="/profile" render={isLoggedIn
? () => <Profile styles={currentStyles}/>
: () => <Redirect to="/login"/>}/>
</Switch>
</div>
</Router>);
}
const App = connect(mapStateToProps)(ConnectedApp)
export default App;
Related
I am having some issues with my routing currently when authenticated. Whenever I try to access my ViewPortfolio page at localhost:3000/portfolio/portfolioId it will redirect me back to my homepage. I am not sure what is going on. I have also tried manipulating the URL by modifying it to the correct URL link but it also redirects me back to /homepage when I am authenticated. The source codes can be found below. App.js is my router with PrivateRoute as the private route component and finally, CreateInterview.js where I redirect using js windows.location function to ViewPortfolio.js which will use useParams() react hook to get the param. But instead now after creating successfully and redirect to the correct URL with the portfolioId it will redirect back to homepage within less than a second.
PrivateRoute.js
import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
const { currentUser } = useAuth()
return (
<Route
{...rest}
render={(props) => {
if (currentUser) {
return <Component {...props} />
} else {
return <Redirect to={{
pathname: "/",
state:{
from: props.location
}
}}/>
}
}
}>
</Route>
)
}
export default PrivateRoute
App.js
import React from "react"
.
.
.
import PublicRoute from "./PublicRoute";
function App() {
return (
<AuthProvider>
<Router>
<Switch>
{/* Auth Routes */}
<PublicRoute exact path='/' component={Login} />
.
.
.
<PrivateRoute exact path='/createInterview' component={CreateInterview} />
<PrivateRoute path='/manageInterview' component={ManageInterview} />
<PrivateRoute path='/portfolio/:portfolioId' component={ViewPortfolio} />
{/* Non-Existance Routes */}
<Route path="*" component={() => "404 NOT FOUND"} />
</Switch>
</Router>
</AuthProvider>
)
}
export default App
CreatInterview.js redirecting in js (onSubmit of the form)
async function handleSubmit(e) {
e.preventDefault();
setError('');
setLoading(true);
await database.portfolioRef.add({
intervieweeName: intervieweeNameRef.current.value,
intervieweeEmail: intervieweeEmailRef.current.value,
intervieweeMobileNumber: intervieweeMobileRef.current.value,
projectTitle: projectTitleRef.current.value,
portfolioTitle: portfolioNameRef.current.value,
dateCreated: new Date().toLocaleString('en-SG'),
createdBy: currentUser.displayName
}).then(function(docRef) {
console.log("This is the Document ID " + docRef.id.toString());
console.log(docRef.id);
window.location = '/portfolio/' + docRef.id;
})
setLoading(false)
}
Part of ViewPortfolio.js to receive the portfolioId from CreateInterview.js
const ViewPortfolio = () => {
let { portfolioId } = useParams();
AuthContext.js
import React, { useContext, useState, useEffect } from "react"
import { auth, database } from "../firebase";
import { getDocs, query, where } from "firebase/firestore";
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(null)
const [loading, setLoading] = useState(true)
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password).then(() => {
const Doc = query(database.usersRef, where("email", "==", email));
getDocs(Doc).then((querySnapshot) => {
let values = '';
querySnapshot.forEach((doc) => {
values = doc.id;
});
var userUpdate = database.usersRef.doc(values);
userUpdate.update({
lastActive: new Date().toLocaleString('en-SG'),
})
})
});
}
function logout() {
return auth.signOut();
}
function forgetPassword(email) {
return auth.sendPasswordResetEmail(email);
}
function updateEmail(email) {
return currentUser.updateEmail(email)
}
function updatePassword(password) {
return currentUser.updatePassword(password)
}
function updateDisplayName(name) {
return currentUser.updateDisplayName(name)
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged( user => {
setLoading(false)
setCurrentUser(user)
})
return unsubscribe
}, [])
const value = {
currentUser,
login,
forgetPassword,
logout,
updateEmail,
updatePassword,
updateDisplayName,
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
The initial currentUser state matches the unauthenticated state, so when the app initially renders, if you are accessing a protected route the redirection will occur because the currentUser state hasn't updated yet.
Since onAuthStateChanged returns null for unauthenticated users then I suggest using anything other than null for the initial currentUser state. undefined is a good indeterminant value. You can use this indeterminant value to conditionally render a loading indicator, or nothing at all, while the auth status is confirmed on the initial render.
AuthProvider
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(); // <-- undefined
...
PrivateRoute
const PrivateRoute = (props) => {
const { currentUser } = useAuth();
if (currentUser === undefined) {
return null; // or loading spinner, etc...
}
return currentUser
? (
<Route {...props} />
)
: (
<Redirect
to={{
pathname: "/",
state: {
from: props.location
}
}}
/>
);
}
You should also really replace the window.location = '/portfolio/' + docRef.id; logic with a history.push('/portfolio/' + docRef.id); so you are not unnecessarily reloading the page.
const history = useHistory();
...
async function handleSubmit(e) {
e.preventDefault();
setError('');
setLoading(true);
try {
const docRef = await database.portfolioRef.add({
intervieweeName: intervieweeNameRef.current.value,
intervieweeEmail: intervieweeEmailRef.current.value,
intervieweeMobileNumber: intervieweeMobileRef.current.value,
projectTitle: projectTitleRef.current.value,
portfolioTitle: portfolioNameRef.current.value,
dateCreated: new Date().toLocaleString('en-SG'),
createdBy: currentUser.displayName
});
history.push('/portfolio/' + docRef.id);
} catch (error) {
// handle error, clear loading state
setLoading(false);
}
}
i want to run the useEffect first before the render function which is placed inside the <Route /> tag starts to render. i expect to get currently available user details through the API and assigne them to render function.
but render function runs before the UseEffect retrieve data from the API. so help me to find the solution.
import React, { useEffect, useState } from "react";
import { Route, Redirect } from "react-router-dom";
import { Auth } from "aws-amplify";
const ProtectedRoute = ({ children, ...rest }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
setIsAuthenticated(
Auth.currentAuthenticatedUser({
// bypassCache: false,
})
.then((user) => console.log(user))
.catch((err) => console.log(err))
);
}, []);
return (
<Route
{...rest}
render={({ location }) =>
(isAuthenticated ) ? (
children
) : (
<Redirect
to={{
// pathname: "/login",
pathname: "/create-profile",
state: { from: location },
}}
/>
)
}
/>
);
};
export default ProtectedRoute;
Try this
useEffect(() => {
Auth.currentAuthenticatedUser({
// bypassCache: false,
})
.then((user) => user && setIsAuthenticated(true))
.catch((err) => err && setIsAuthenticated(false));
}, []);
You could wrap that authentication stuff into a hook of your own, and then simply not render anything until it's ready:
function useIsAuthenticated() {
const [isAuthenticated, setIsAuthenticated] = useState(null);
useEffect(() => {
Auth.currentAuthenticatedUser({})
.then(setIsAuthenticated)
.catch((err) => {
console.log(err);
setIsAuthenticated(false);
});
}, []);
return isAuthenticated;
}
const ProtectedRoute = ({ children, ...rest }) => {
const isAuthenticated = useIsAuthenticated(); // Will be the user if authenticated, null if busy, or false if error.
if (isAuthenticated === null) {
return null; // Don't render anything if authentication state is unknown
}
return <>...</>;
};
I want to run getUser function every time the user goes to some other link.
The following is my getUser function
const getUser = async () => {
if (localStorage.getItem('access') === null || localStorage.getItem('refresh') === null || localStorage.getItem('user') === null) {
setUser({ email: null });
setIsLoggedIn(false);
return;
}
const responseForAccessToken = await verifyTokenAPI(localStorage.getItem('access'));
console.log(responseForAccessToken);
if (responseForAccessToken.status >= 400) {
const newAccessTokenResponse = await getAccessTokenAPI(localStorage.getItem('refresh'));
console.log(newAccessTokenResponse);
if (newAccessTokenResponse.status >= 400) {
localStorage.removeItem('access');
localStorage.removeItem('refresh');
localStorage.removeItem('user');
setUser({ email: null });
setIsLoggedIn(false);
return;
}
localStorage.removeItem('access');
localStorage.setItem('access', newAccessTokenResponse.access);
}
I want to verify token every time the user changes routes. Therefore, I used getUser function in useEffect in my App.js
const history = useHistory();
const { getUser } = useAuth();
useEffect(() => {
history.listen((location) => {
console.log(`You changed the page to: ${location.pathname}`);
});
getUser();
}, [history]);
Every time I change routes the useEffect runs and console logs the message but does not run getUser function.
I am using Link from react-router-dom
<h1>Welcome {user.email}</h1>
<Link to="/protected-route-2">Protected Route 2</Link>
<button
onClick={() => logout({ callBack: () => history.push("/login") })}
>
Logout
</button>
Additionally, I also have a PrivateRoute component
const Privateroute = ({ component: Component, ...rest }) => {
const { isLoggedIn, getUser } = useAuth()
console.log(isLoggedIn);
const location = useLocation()
if (isLoggedIn) return <Route {...rest} render={props => <Component {...props} />} />;
return <Redirect to={{ pathname: '/login', state: { from: location.pathname } }} />
}
I am not sure if I am doing things right. Can someone suggest another approach to this problem? Any suggestion will be appreciated. Thanks in advance.
You should use the useLocation hook (as shown in the documentation) instead of the useHistory, which would give you the current location and use that as the dependency for the useEffect:
const location = useLocation();
const { getUser } = useAuth();
useEffect(() => {
console.log(`You changed the page to: ${location.pathname}`);
getUser();
}, [location]);
In your code, the history object does not change and the effect is only fired once, the reason you keep getting the console logs when the location changes is that you added a listener to the history.
I'm using reactjs to build a login/register system with authentication and authorization. if authenticated(jsonwebtoken), it should route me to the dashboard else redirect me back to login.
but whenever I reload it hits the login endpoint for a second then back to dashboard. how can I fix this?
Below is a giphy to show what I'm talking about
Here are the components associated with the issue stated above
App.js
const App = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false)
// set isAuthenticated to true or false
const setAuth = (boolean) => {
setIsAuthenticated(boolean)
}
useEffect(() => {
// check if the person is still Authenticated
const isAuth = async () => {
try {
const res = await fetch('/auth/verify', {
method: 'GET',
headers: { token: localStorage.token},
})
const data = await res.json()
// if authenticated, then
if(data === true) {
await setIsAuthenticated(true)
} else {
await setIsAuthenticated(false)
}
} catch (err) {
console.error(err.message)
}
}
isAuth()
})
return (
<Fragment>
<Router>
<div className='container'>
<Switch>
<Route exact path='/login' render={props => !isAuthenticated ? <Login {...props} setAuth={setAuth} /> : <Redirect to='/dashboard' /> } />
<Route exact path='/register' render={props => !isAuthenticated ? <Register {...props} setAuth={setAuth} /> : <Redirect to='/login' />} />
<Route exact path='/dashboard' render={props => isAuthenticated ? <Dashboard {...props} setAuth={setAuth} /> : <Redirect to='/login' /> } />
</Switch>
</div>
</Router>
</Fragment>
);
Login Component
const Login = ({ setAuth }) => {
const [text, setText] = useState({
email: '',
password: ''
})
const { email, password } = text
const onChange = e => setText({ ...text, [e.target.name]: e.target.value})
const onSubmit = async (e) => {
e.preventDefault()
try {
// Get the body data
const body = { email, password }
const res = await fetch('/auth/login', {
method: 'POST',
headers: {"Content-Type": "application/json"},
body: JSON.stringify(body)
})
const data = await res.json()
if(data.token) {
// save token to local storage
localStorage.setItem("token", data.token)
setAuth(true)
toast.success('Login Successful')
} else {
setAuth(false)
toast.error(data)
}
} catch (err) {
console.error(err.message)
}
}
return (
<Fragment>
<h1 className='text-center my-5'>Login</h1>
<form onSubmit={onSubmit}>
Dashboard Component
const Dashboard = ({ setAuth }) => {
const [name, setName] = useState('')
useEffect(() => {
const getName = async () => {
try {
const res = await fetch('/dashboard', {
method: 'GET',
// Get the token in localStorage into the header
headers: { token: localStorage.token }
})
const data = await res.json()
setName(data.user_name)
} catch (err) {
console.error(err.message)
}
}
getName()
// eslint-disable-next-line
}, [])
// Log out
const logOut = (e) => {
e.preventDefault()
localStorage.removeItem("token")
setAuth(false)
toast.success('Logged Out')
}
return (
<Fragment>
<h1 className='mt-5'>Dashboard</h1>
<p>Hello, {name}</p>
<button className='btn btn-primary my-3' onClick={e => logOut(e)}>Log Out</button>
</Fragment>
There are two problems that I found in your code above.
The first is that your ueEffect does not specify any dependency.
When the dependencies are not specified in this way the useEffect would run anytime any state changes.
useEffect(()=> {
// code here
}); // this one would run anytime any state changes in the component. You usually don't want this.
When a dependency array is specified, the code in the useEffect would run anytime any of the state in the dependencies changes.
useEffect(()=> {
// code here
},
[state1, state2, ...others...] //code would run when any of the state in this array changes
In your case, however, you probably want to run that useEffect once. To do this we add an empty array as the dependency value.
useEffect(()=> {
// code here
},
[] //empty deps means that the code runs only once. When the component mounts
)
Extra ideas
I also suggest that you add a loading state to your component so that you can show a loader while the API call is being made.
You might want to show a loader while the API call is being made(or even set this state to true by default since the API call is the first thing you do in your app)
.
Also, consider putting useEffect in a custom Hook
I'm using react-router-dom to secure the entire application. All routes are protected under a ProtectedRoute component (see code below), which redirects to an external url, a single-sign-on (SSO) page if the user is not logged in.
Problem:
When the user goes to '/home', they get a brief glimpse (a "flash") of the protected route before getting redirected to 'external-login-page.com/' (the login page). How do I avoid the flashing so that the user only sees the login page?
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
...rest
}) => {
if (!isAuthenticated) { // redirect if not logged in
return (
<Route
component={() => {
window.location.href = 'http://external-login-page.com/';
return null;
}}
/>
);
} else {
return <Route {...rest} />;
}
};
window.location.href can be called earlier to prevent flashing. Also in your specific case what you probably want is to render nothing at all when the user is not authenticated.
The code may look like this:
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
...rest
}) => {
if (!isAuthenticated) { // redirect if not logged in
window.location.href = 'http://external-login-page.com/';
return null;
} else {
return <Route {...rest} />;
}
};
You might consider the Redirect component
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
...rest
}) => {
if (!isAuthenticated) {
return <Redirect to='https://external-login-page.com/' />
} else {
return <Route {...rest} />;
}
};
I would guess that invoking window directly + return null is rendering the React app for a split second before the page reloads.
You can use the Redirect component in a simpler way like this.
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isAuthenticated,
children,
...rest
}) => {
return <Route {...rest} render={() => isAuthenticated ? children : <Redirect to='http://external-login-page.com/' />}
}
Posting the solution that eventually worked for me: instead of blocking by Router, block by App.
The key is to split your App into two components, AuthenticatedApp and UnauthenticatedApp. From there, lazy load the correct component depending on the user's level of access. This way, if they're not authorized, their browser won't even load AuthenticatedApp at all.
AuthenticatedApp is a component to your entire app, providers, routers, etc. Whatever you had in App.tsx originally should go here.
UnauthenticatedApp is a component that you want your users to see when they're not allowed to access the application. Something like "Not authorized. Please contact admin for help."
App.tsx
const AuthenticatedApp = React.lazy(() => import('./AuthenticatedApp'));
const UnauthenticatedApp = React.lazy(() => import('./UnauthenticatedApp'));
// Dummy function to check if user is authenticated
const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time));
const getUser = () => sleep(3000).then(() => ({ user: '' }));
const App: React.FC = () => {
// You should probably use a custom `AuthContext` instead of useState,
// but I kept this for simplicity.
const [user, setUser] = React.useState<{ user: string }>({ user: '' });
React.useEffect(() => {
async function checkIfUserIsLoggedInAndHasPermissions() {
let user;
try {
const response = await getUser();
user = response.user;
console.log(user);
setUser({ user });
} catch (e) {
console.log('Error fetching user.');
user = { user: '' };
throw new Error('Error authenticating user.');
}
}
checkIfUserIsLoggedInAndHasPermissions();
}, []);
return (
<React.Suspense fallback={<FullPageSpinner />}>
{user.user !== '' ? <AuthenticatedApp /> : <UnauthenticatedApp />}
</React.Suspense>
);
};
export default App;
Read Kent C Dodd's great post about it here [0]!
EDIT: Found another great example with a similar approach, a bit more complex - [1]
[0] https://kentcdodds.com/blog/authentication-in-react-applications?ck_subscriber_id=962237771
[1] https://github.com/chenkie/orbit/blob/master/orbit-app/src/App.js