i'm using Passport-local for authentication and Gatsby on front end
Generally, the code works fine. When I click on signout, the server returns a 200 call and I get a response "User sign out successfully". I'm then navigated to the signin page. From there, I am unable to access my Post page which is private route. My signin and post page are client side routes
The issue comes when I click on the home page (which is a static page). From there, when I click on the post link, I'm navigated to the post page which supposedly is inaccessible now that I have signed out. My fetchuser action creator runs and is able to fetch the user detail even though I have already signed out from my app
Anyone knows how to resolve this issue? Thanks in advance
SERVER
signout api
router.get("/signout", (req, res) => {
req.logout();
res.send("Sign Out Successfully");
});
me api
router.get("/me", (req, res) => {
res.send(req.user);
});
CLIENT
app
const App = () => {
useEffect(() => {
store.dispatch(fetchUser())
}, [])
return (
<Layout>
<Alert />
<Router basepath="/app">
<Signin path="/signin" />
<Signup path="/signup" />
<PrivateRoute path="/post" component={Post} />
{/* <Default path="/" /> */}
</Router>
</Layout>
)
}
export default App
fetchUser action creator
export const fetchUser = () => async dispatch => {
try {
const res = await axios.get("http://localhost:5000/api/users/me", {
withCredentials: true,
})
dispatch({
type: FETCH_USER,
payload: res.data,
})
} catch (err) {
console.log(err)
dispatch({
type: AUTH_ERROR,
})
}
}
signout action creator
export const signOut = () => async dispatch => {
const res = await axios.get("http://localhost:5000/api/users/signout")
console.log(res)
dispatch({
type: SIGNOUT,
})
navigate("/app/signin")
}
I think your approach is correct and valid, despite personally thinking that handling it with cookies or localStorage could be easily maintained.
Your <PrivateRoute> component should handle your logic and perform some actions depending on the user state (logged or not), something like:
import React from "react"
import { navigate } from "gatsby"
import { isLoggedIn } from "../services/auth"
const PrivateRoute = ({ component: Component, location, ...rest }) => {
if (!isLoggedIn() && location.pathname !== `/app/login`) {
navigate("/app/login") // or your desireed page
return null
}
return <Component {...rest} />
}
export default PrivateRoute
Your auth service, should handle your requests, in this case using localStorage but it can be replaced for your API requests:
export const isBrowser = () => typeof window !== "undefined"
export const getUser = () =>
isBrowser() && window.localStorage.getItem("gatsbyUser")
? JSON.parse(window.localStorage.getItem("gatsbyUser"))
: {}
const setUser = user =>
window.localStorage.setItem("gatsbyUser", JSON.stringify(user))
export const handleLogin = ({ username, password }) => {
if (username === `john` && password === `pass`) {
return setUser({
username: `john`,
name: `Johnny`,
email: `johnny#example.org`,
})
}
return false
}
export const isLoggedIn = () => {
const user = getUser()
return !!user.username
}
export const logout = callback => {
setUser({})
callback()
}
Related
I am trying to learn how to effectively use React Hooks but am having an issue. I would like to reflect whether or not a user is "logged in" to the site using a JWT in local storage. When I first visit the page, the hook works as I intend, retrieving the user data. But if I click the "Log Out" button in the example below, the component does not update to reflect this, although it will if I refresh the page. How might I properly implement this hook to get it to update when logging in/out?
Custom hooks:
export const useUser = () => {
const [token] = useToken();
const getPayloadFromToken = token => {
const encodedPayload = token.split('.')[1];
return JSON.parse(atob(encodedPayload))
}
const [user,setUser] = useState(() => {
if(!token) return null;
return getPayloadFromToken(token);
})
useEffect(() => {
if(!token) {
setUser(null);
} else {
setUser(getPayloadFromToken(token));
}
}, [token]);
return user;
}
export const useToken = () => {
const [token, setTokenInternal] = useState(() => {
return localStorage.getItem('token');
});
const setToken = newToken => {
localStorage.setItem('token',newToken);
setTokenInternal(newToken);
}
return [token, setToken];
}
Navigation Bar Component:
const NavigationBar = () => {
const user = useUser();
const logOut = () => {
localStorage.removeItem('token');
};
return(
<>
<div>{user ? 'logged in' : 'logged out'}</div>
<button onClick={logout}>Log Out</button>
</>
);
}
I think at a minimum you could expose out the setToken function directly via the useUser hook and when you call logout call setToken(null) (or similar) and this would be sufficient enough to trigger a render. Ideally though you'd have all this authentication "state" centrally located in a React context so the hooks all reference the same single state.
I suggest actually encapsulating the logout function within the useUser hook and exposing that out instead of directly exposing the setToken function. You want the hooks to maintain control over the state invariant and not rely on consumers to pass/set the correct state values.
Example:
export const useUser = () => {
const [token, setToken] = useToken();
const getPayloadFromToken = token => {
const encodedPayload = token.split('.')[1];
return JSON.parse(atob(encodedPayload));
}
const [user, setUser] = useState(() => {
if (!token) return null;
return getPayloadFromToken(token);
});
useEffect(() => {
if (!token) {
setUser(null);
} else {
setUser(getPayloadFromToken(token));
}
}, [token]);
const logout = () => {
setUser(null);
setToken(null);
};
return { user, logout };
}
...
export const useToken = () => {
const [token, setTokenInternal] = useState(() => {
// Initialize from localStorage
return JSON.parse(localStorage.getItem('token'));
});
useEffect(() => {
// Persist updated state to localStorage
localStorage.setItem('token', JSON.stringify(newToken));
}, [token]);
const setToken = newToken => {
setTokenInternal(newToken);
}
return [token, setToken];
};
...
const NavigationBar = () => {
const { logout, user } = useUser();
return(
<>
<div>{user ? 'logged in' : 'logged out'}</div>
<button onClick={logout}>Log Out</button>
</>
);
}
I encountered something similar. A solution I found that worked for me was to save whether or not the user is logged in using Context. Effectively this would involve creating a wrapper around components in your app which need access to whether or not a user is logged in as an alternative to using local storage to save this sort of stuff.
https://reactjs.org/docs/context.html
This is how I redirect a user depending if he is logged in or not. I am just checking if there is a cookie or not in this example.
import { Route, Redirect } from "react-router-dom";
import cookies from "js-cookies";
export const PrivateRoute = ({ children, ...rest }) => {
const userSignedIn = () => {
return cookies.getItem("jwt");
};
return (
<Route
{...rest}
render={({ location }) =>
userSignedIn() ? (
children
) : (
<Redirect to={{ pathname: "/login", state: { from: location } }} />
)
}
></Route>
);
};
I am trying to improve my code by checking if a user is logged in with my backend.
What is the proper way to check if a user is logged in with react express and JWT ?
Where should I call my api ?
I have been stuck with this problem for a while now...
Thanks
When you logged in then you set a variable auth true in global state and set localstorage.setItem("auth", true) as like. You need to understand about private route and public route and define private route and public route, when you logged in then get auth true then call private route otherwise call public route.In this time you set again global state from localstorage.getItem("auth") because when you reload this page then auth variable true get in localstorage, hope you understand thanks
So this is how I solved my question.
Might not be the best...
import { Route, Redirect } from "react-router-dom";
import { useEffect, useState } from "react";
import { Loading } from "../Loading/Loading";
import axios from "axios";
export const PrivateRoute = ({ children, isAuthenticated, ...rest }) => {
const [isLoading, setIsLoading] = useState(true);
const [isAuth, setIsAuth] = useState(false);
const getIsAuthenticated = async () => {
try {
const res = await axios.get("http://localhost:5000/api/user");
console.log(res);
setIsAuth(true);
} catch (error) {
console.log(error);
setIsAuth(false);
}
setIsLoading(false);
};
useEffect(() => {
getIsAuthenticated();
return () => {};
}, [isAuth]);
if (isLoading) {
return <Loading />;
}
return (
<Route
{...rest}
render={({ location }) =>
isAuth ? (
children
) : (
<Redirect to={{ pathname: "/login", state: { from: location } }} />
)
}
></Route>
);
};
backend code
const user_GET = async (req, res) => {
res.status(400).send("wrong token");
//res.status(200).send("right token");// DEBUG
};
I am trying to make a protected route with Reatjs, nodejs and JWT. The problem is that my component renders before my API checked the client token. This is the code I am trying :
import React, {useState, useEffect} from 'react';
import { Route, Redirect } from 'react-router-dom';
import AuthAPI from './../utils/AuthAPI';
const ProtectedRoute = ({children, ...rest}) => {
const [isAuth, setIsAuth] = useState(false);
const fetchData = async () => {
await AuthAPI.isAuth((res)=>{ //API call
setIsAuth(res);
});
}
useEffect(()=>{
fetchData();
},[]);
return (
<Route {...rest}
render={(props)=>{
return(
isAuth ? children : <Redirect to='/' />
);
}}
/>
);
};
And this is the API call :
static isAuth(callback){ //static method from the class 'AuthAPI' imported above
const url = 'http://localhost:5000/api/Auth/checking';
const options = {
method: 'GET',
url: url,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json;charset=UTF-8',
},
data: {}
}
return axios(options)
.then((response)=>{
callback(true);
}).catch((err)=>{
callback(false);
});
}
When I load the page, it directly redirects since the state isAuth is set to false by default.
I already used this model of code to display a list of things gotten from an API and it worked fine. I assume it is not the best way to do that but most of the examples I have found are not using an actual API but just fake auth without using promises.
EDIT 1.2 :
I've tried this code, from Udendu Abasili :
import React, {useState, useEffect} from 'react';
import { Route, Redirect } from 'react-router-dom';
import AuthAPI from './../utils/AuthAPI';
const ProtectedRoute = ({children, ...rest}) => {
const [isAuth, setIsAuth] = useState(false);
const [isLoaded, setIsLoaded] = useState(false)
useEffect(()=>{
let mounted = true;
AuthAPI.isAuth().then(()=>{
if (mounted) {
console.log("Worked"); //display Worked
setIsLoaded(true); // This line 1
setIsAuth(true); // This line 2
}
}).catch(()=>{
if (mounted) {
console.log("Failed");
setIsLoaded(true);
setIsAuth(false);
}
});
return () => {
mounted = false;
}
},[]);
return (
!isLoaded ?
<h5>Loading</h5> : (
<Route {...rest}
render={(props)=>{
console.log("--->",isAuth,",",isLoaded); // displays false, true
return(
isAuth ? children : <Redirect to='/' />
);
}}
/>
)
);
};
export default ProtectedRoute;
I have found a weird bug. If I swap the lines commented as 'line 1' and 'line 2', it works otherwise it doesn't.
The way react js lifecycle works, the return component gets called before useEffect(which the hook equivalent of componentDidMount on the first mount). So you need to create a form of loader component ( replace the <Text>Loading</Text> with an actual CSS loader ) that waits for your isAuth function to finish.
const ProtectedRoute = ({children, ...rest}) => {
const [isAuth, setIsAuth] = useState(false);
const [loaded, setLoaded] = useState(false);
const fetchData = async () => {
//you need to add try catch here
await AuthAPI.isAuth((res)=>{ //API call
setIsAuth(res);
setLoaded(true)
});
}
useEffect(()=>{
fetchData();
},[]);
return (
loaded ?
<Text>Loading</Text> : (
<Route {...rest}
render={(props)=>{
return(
isAuth ? children : <Redirect to='/' />
);
}}
)
/>
);
};
As you rightfully said, this is not the best way to do it. I won't recommend calling a function to check authentication in the protected route component. Typically, I just pass an isAuthenticated paramter to ProctectedRoute component which gets updated with help of Redux. You should look it up
I used passport local for user authentication on my backend when the user is authenticated successfully session is stored on database. In the frontend i'm using react, i can login successfully on the front end and when i checked the browser session, session is been saved but the problem if can still navigate back to login page while still the session is saved on the browser
Here is how I handle my passport-local setup redirect or render statements:
class App extends Component {
constructor() {
super();
this.state = {
loggedInUser: null
};
}
componentWillMount() {
this.fetchLoggedInUser();
}
fetchLoggedInUser = () => {
fetchUser().then(res => {
if (res.message) {
this.setState({
loggedInUser: false
});
} else {
this.setState({
loggedInUser: true,
user: res
});
}
});
};
isLoggedIn = () => {
return this.state.loggedInUser;
};
isAdmin = () => { // You won't have this, but it's part of my route/middleware
console.log(this.state.user.accessLevel);
return this.state.user.accessLevel === "Admin";
};
initializeLoad = () => {
if (this.state.loggedInUser === null) {
return <h1>Loading...</h1>;
} else {
return (
<React.Fragment>
// Removed a bunch of routes to shorten things up.
<Route
exact
path="/login"
render={() =>
// Determine if logged in // if not redirect
// Some routes I pass in through props => <SomeView {...props} ... /> so I have access to the user.
this.isLoggedIn() ? (
<Redirect to="/profile" />
) : (
<LoginView onAuthUpdate={this.handleAuthUpdate} />
)
}
/>
</React.Fragment>
);
}
};
render() {
return this.initializeLoad();
}
}
export default App;
Not sure if I understand. You can check if session exists and if it does, dont show the login page but redirect to homepage.
I have followed some tutorial to build an authentication in React, Node and Redux. The basic functionality works, however, when I keep the application open and then get back to it (when the session expired), I get this error message:
Unhandled Rejection (TypeError): Cannot read property 'uploadURL' of undefined
Then I refresh the page and I get this error message:
TypeError: Cannot read property 'push' of undefined
Then, I refresh the page again and I am finally redirected on the homepage. The first 2 errors are a problem I am not sure how to get rid off them.
This is what my code looks like:
...
class Event extends Component {
constructor() {
super();
...
}
UNSAFE_componentWillMount() {
// I thought this if-block will redirect the user if the session is expired
if(!this.props.auth.isAuthenticated) {
console.log('unauthorized');
this.props.history.push('/');
}
this.uppy2 = new Uppy({ id: 'uppy2', autoProceed: true, debug: true })
.use(Tus, { endpoint: 'https://master.tus.io/files/' })
.on('complete', (result) => {
console.log(`Upload complete! We’ve uploaded these files: ${result.successful[0].uploadURL}`);
});
}
...
}
Event.propTypes = {
registerUser: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
errors: state.errors
});
export default connect(mapStateToProps,{ registerUser })(withRouter(Event))
Here's the Redux code (I am beginner with the MERN stack):
import axios from 'axios';
import { GET_ERRORS, SET_CURRENT_USER } from './types'; // we list here the actions we'll use
import setAuthToken from '../../setAuthToken';
import jwt_decode from 'jwt-decode';
export const registerUser = (user, history) => dispatch => {
axios.post('/api/users/register', user)
.then(res => history.push('/login'))
.catch(err => {
dispatch({
type: GET_ERRORS,
payload: err.response.data
});
});
}
export const loginUser = (user) => dispatch => {
axios.post('/api/users/login', user)
.then(res => {
//console.log(res.data);
const { token } = res.data;
localStorage.setItem('jwtToken', token);
setAuthToken(token);
const decoded = jwt_decode(token);
dispatch(setCurrentUser(decoded));
})
.catch(err => {
dispatch({
type: GET_ERRORS,
payload: err.response.data
});
});
}
export const setCurrentUser = decoded => {
return {
type: SET_CURRENT_USER,
payload: decoded
}
}
export const logoutUser = (history) => dispatch => {
localStorage.removeItem('jwtToken');
setAuthToken(false);
dispatch(setCurrentUser({}));
history.push('/login');
}
How do I prevent the errors happening when the session is expired?
Thank you in advance!
ComponentWillMount won't be called if the page is loaded before the session expires. I suspect the first error is caused by some missing data because the request with the expired token failed. You would need to make sure the 401 or 403 error is handled and clear out the Redux state so the login page is shown when that happens.
I am not sure with this part !this.props.auth.isAuthenticated. Did you use mapDispatchToProps and connect for redux? You need to do this in your Event class to reach your reducer.
Also the thing that you can do is, before rendering your jsx code, declare a variable like let redirect = null and if !this.props.auth.isAuthenticated is correct, set this redirect variable to redirect = <Redirect to="/" /> (If you use browser routing!) and use this variable like this,
render() {
return (
{redirect}
)
}