ReactJS - redirect on the login page if session expired - node.js

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}
)
}

Related

NodeJS ReactJS Redux displays undefined for token api token

I am using NodeJS ReactJS Redux with JWT in an application and I have issue getting expiresIn and token at the reactjs frontend. When I console log the token at the NodeJS controller, it displays the token nd everything perfectly but it pointed to the jwt.verify() with error JsonWebTokenError: jwt malformed but at the ReactJS frontend, it displays the payLoad userData part of the token and displays undefined for both expiresIn and token.
As you can see in the Redux authSlice class that I set localStorage Item in there for token, expire and userData but when I tried to get the localStorage item in another page, I could only get the userData payloads but the token and expire are undefined.
I don't know what is wrong here because the NodeJS sent the token for real as I can get the token from console of NodeJS and ThunderClient API as well gave an 200 OK when i test the api using ThunderClient in VS Code.
My concern is that ThunderClient displays 200 OK and return Token, expiresIn and userData complete and everything perfect, NodeJS console displays correct information on console but gave JsonWebTokenError: jwt malformed and at ReactJS frontend, I got the userData from the token sent by the API but accessToken and expiresIn are missing i.e out of the 3 string that JWT encrypted, I got only the Payload which is the userData.
How can I solve this?
*******************NODEJS
jwtHelper.js
exports.extractToken = (req) => {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
return req.headers.authorization.split(' ')[1];
} else if (req.query && req.query.token) {
return req.query.token;
}
return null;
}
jwtVerify.js
module.exports = function (req, res, next) {
try {
const token = extractToken(req);
if (token == null) {
return res.status(StatusCodes.UNAUTHORIZED).send("Unauthorized");
}
jwt.verify(token, common_helper.getJWTAccessToken(), {}, (err, user) => {
if (err) {
console.log(err);
return res.status(StatusCodes.FORBIDDEN).send("Invalid user");
}
req.user = user["userData"];
next();
});
} catch (e) {
next(e);
}
};
Login (Controller)
const token = jwt.sign({userData}, common_helper.getJWTAccessToken(), {
algorithm: 'HS256',
expiresIn: common_helper.getJWTExpiryTime(),
});
res.status(StatusCodes.OK).send({"expires_in": common_helper.getJWTExpiryTime(),"access_token":token,"token_type": "bearer","userData":userData});
console.log(`The token is ${token}`) // This displays the ciper token
console.log(`The secret_token is ${common_helper.getJWTExpiryTime()}`) //this displays the real secret key
*******************REACTJS
Redux Slice. Note that the localStorage is set here
import { createSlice, PayloadAction } from "#reduxjs/toolkit";
interface IAuthToken {
isAuthenticated?:boolean,
jwtToken: any;
expiryDate: any;
errorMessage?:string;
userData?:any;
notverified?: string;
}
const initialState: IAuthToken = {
jwtToken: undefined,
expiryDate: undefined,
errorMessage:'',
isAuthenticated:false,
userData:undefined,
notverified: undefined,
};
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
setJWTToken: (state, _action: PayloadAction<IAuthToken>) => {
state.jwtToken = _action.payload.jwtToken;
state.expiryDate = _action.payload.expiryDate;
state.userData=_action.payload.userData;
localStorage.setItem('token', state.jwtToken);
//localStorage.setItem('token', JSON.stringify(state.jwtToken));
localStorage.setItem('expire', state.expiryDate);
//localStorage.setItem('expire', JSON.stringify(state.expiryDate));
if(state.userData)
localStorage.setItem('userData',JSON.stringify(state.userData));
state.isAuthenticated = true;
state.notverified = _action.payload.notverified;
},
removeJWTToken: (state) => {
localStorage.clear();
state.jwtToken = '';
state.expiryDate=undefined;
state.isAuthenticated = false;
},
setError: (state, _action: PayloadAction<string>) => {
state.errorMessage = _action.payload;
},
},
});
export const { setJWTToken, removeJWTToken,setError } = authSlice.actions;
export default authSlice.reducer;
ReactJS Login
Axios.post(`${baseURL}/signin`, { username: formik.values.username, password: formik.values.password})
.then((response) => {
if(response.data.notverified)
{
setSubmitting("");
navigate("/needemailconfirmation", { replace: true });
}
setSubmitting("");
console.log(response.data)
dispatch(setJWTToken(response.data));
navigate("/dashboardd", { replace: true });
authAction
export const signIn = (email, password) => {
return (dispatch) => {
axios
.post(`${url}/signin`, { email, password })
.then((token) => {
localStorage.setItem("token", token.data);
dispatch({
type: "SIGN_IN",
token: token.data,
});
})
.catch((error) => {
console.log(error.response);
toast.error(error.response?.data, {
position: toast.POSITION.BOTTOM_RIGHT,
});
});
};
};
authReducer
const authReducer = (state = initialState, action) => {
switch (action.type) {
case "SIGN_IN":
case "SIGN_UP":
case "USER_LOADED":
toast("Welcome...", {
position: toast.POSITION.BOTTOM_RIGHT,
});
const user = jwtDecode(action.token);
return {
...initialState,
token: action.token,
name: user.name,
email: user.email,
_id: user._id,
};
case "SIGN_OUT":
localStorage.removeItem("token");
toast("Goodbye...", {
position: toast.POSITION.BOTTOM_RIGHT,
});
return {
token: null,
name: null,
email: null,
_id: null,
};
default:
return state;
}
};
In my ReactJS frontend, from the token sent by the NodeJS api I was able to get userData payload from the token but I could not get the token and expiresIn for reason I do not know.
You can find the token details from the browser inspect here where I got userData payload but got undefines for accesstoken and expiresIn
Update
In my question above, I made the point of not getting the token value in my ReactJS frontendm while debugging the code I set the localStorage at the login page immediately after a successful login and i used the localStorage getItem() to get the value in my Redux slice but the issue that persist is that my ReactJS frontend is not allowing me to get to dashboard as it is acting like the value is not set i.e the middleware cannot get the value at the and the route allow the user to navigate to the dashboard is bouncing the navigation back to login page despite the fact that the token is set and I can get the token at the browser inspect to be sure that it is set but Auth Middleware is not getting it. See the code I have now below:
******************ReactJS
Login.js
//call the api
Axios.post(`${baseURL}/signin`, { username: formik.values.username, password: formik.values.password})
.then((response) => {
if(response.data.notverified)
{
setSubmitting("");
navigate("/needemailconfirmation", { replace: true });
}
setSubmitting("");
console.log(response.data)
if(response.data.access_token){
dispatch(setJWTToken(response.data));
localStorage.setItem('token', JSON.stringify(response.data.access_token));
localStorage.setItem('expire', JSON.stringify(response.data.expires_in));
localStorage.setItem('userData', JSON.stringify(response.data.userData));
}
navigate("/dashboardd", { replace: true });
}) .catch((error) =>{
setSubmitting("");
//console.log(error.data);
setErrorMsg("Invalid Login Credentials Supplied.");
//alert("Invalid Login Credentials Supplied, Error : <br>" + error.data);
});
}
Redux Slice Auth File
interface IAuthToken {
isAuthenticated?:boolean,
jwtToken: any;
expiryDate: any;
errorMessage?:string;
userData?:any;
notverified?: string;
}
const initialState: IAuthToken = {
jwtToken: undefined,
expiryDate: undefined,
errorMessage:'',
isAuthenticated:false,
userData:undefined,
notverified: undefined,
};
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
setJWTToken: (state, _action: PayloadAction<IAuthToken>) => { state.jwtToken = localStorage.getItem("token");
state.expiryDate = localStorage.getItem("expire");
state.userData = localStorage.getItem("userData");
if(localStorage.getItem("userData"))
state.isAuthenticated = true;
state.notverified = '';
},
removeJWTToken: (state) => {
localStorage.clear();
state.jwtToken = '';
state.expiryDate=undefined;
state.isAuthenticated = false;
},
setError: (state, _action: PayloadAction<string>) => {
state.errorMessage = _action.payload;
},
},
});
export const { setJWTToken, removeJWTToken,setError } = authSlice.actions;
export default authSlice.reducer;
AuthenticatedRoute.js : this is used in bouncing user back if they token is not found
const AuthenticatedRoute: FC<{ element: any }> = ({ element }) => {
const token = localStorage.getItem('token');
const expire = localStorage.getItem('expire');
const now = Date.now();
if (token && expire && parseInt(expire) > parseInt(now.toString())) {
return element;
} else {
return <Navigate to="/" replace />
}
};
export default AuthenticatedRoute;
My React Router Dom
<Route path="/checkpoint" element={<AuthenticatedRoute element={<CheckpointList />} />}></Route>
<Route path="/vendour" element={<AuthenticatedRoute element={<VendourList />} />}></Route>
React Router Dom path are working normal but just that if I put AuthenticatedRoute to dashboard Route, it would not allow me in because the AuthenticatedRoute is not seeing the token I think but when I console the token in dashboard if i remove AuthenticatedRoute, the values are there.
Using React Router Dom, I was debugging this code and remove the AuthenticatedRoute middleware from dashboard and I was able to navigate to dashboard and get the localStorage value for the token and expiresIn but the point remains that when I put the AuthenticatedRoute middleware in the dashboad Route, the AuthenticatedRoute would bounce me back to login page meaning it cannot find the token.

Passport-Local with Gatsby log out does not completely log out

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()
}

How do i dispatch a redux action from class and not component

import axios from "axios";
class ApiCaller {
CallApi = (SERVICE_URL, data) => {
return new Promise((resolve, reject) => {
axios.post(SERVICE_URL, data).then(function (response) {
var data = response.data
if (data.status) {
data = data.data
resolve(data)
} else {
if (data.isExpired != undefined && data.isExpired) {
console.log('here in expired')
}
reject(data.message)
}
}).catch(function (error) {
console.log(error);
reject(error)
});
})
}
}
export default new ApiCaller();
// export default connect()(new ApiCaller());
here i am calling all the webservices throughout application
and data.isExpired is a boolean where server tells me that provided jwt token is expired and i need to remove user information from redux and navigate to login again i have used this method almost more than 50 places and i cant afford it to change on all those places
how can i dispatch user logout from here ?
import { logoutUser } from "app/redux/actions/UserActions";
const mapStateToProps = state => ({
logoutUser: PropTypes.func.isRequired
});
export default withRouter(connect(mapStateToProps, { logoutUser })(ApiCaller));
this might be the solution , to do that i need to extend component and it will break ApiCaller implementation on other places
i have tried functional components too but it isnt helping, i need to logout from my ApiCaller so i don't need to handle jwt expire exception on every single view
i am new to React Redux
anyone please
I think this is a pretty good example of where you can use Redux Thunks.
// this is an action that can be dispatched to call the api
function callApi() {
return (dispatch) => {
apiCaller.CallApi()
.then(() => {
dispatch(callApiSuccess()); // success action defined elsewhere
})
.catch(() => {
dispatch(callApiError()); // error action defined elsewhere
});
}
}

Cannot GET /post/api/posts/5c804ec6ad029f21201c686e Not found, because of unwanted appending of word "posts"

whenever I try to get the post api , I am getting error saying Cannot GET /posts/api/posts/5c804ec6ad029f21201c686e
I am not able to figure out where the "posts" word is getting appended in my api call with axios. I checked my code thourouly but I am not able to catch the bug. Can anyone please help me with this.
Post.js ;-
import React, { Fragment, useEffect } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import { getPost } from '../../actions/post';
import PostItem from '../posts/PostItem';
const Post = ({ getPost, post: { post, loading }, match }) => {
useEffect(() => {
getPost(match.params.id)
}, [getPost]);
return <h1>Post</h1>
// loading || post === null ? <Spinner /> :
// <Fragment>
// <PostItem post={post} showActions={false} />
// </Fragment>
}
Post.propTypes = {
getPost: PropTypes.func.isRequired,
post: PropTypes.object.isRequired,
}
const mapStateToProps = state => ({
post: state.post
})
export default connect(mapStateToProps, { getPost })(Post)
Post actions :-
import axios from "axios";
import { setAlert } from './alert';
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
GET_POST
} from './types';
// Get post
export const getPost = id => async dispatch => {
try {
const res = await axios.get(`api/posts/${id}`);
dispatch({
type: GET_POST,
payload: res.data
})
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
})
}
}
beckend code in Node.js
//#route Get api/posts/:id
//#desc GET Post by Id
//#access Private
router.get("/:id", auth, async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ msg: 'Post not found' })
}
res.json(post);
} catch (err) {
console.log(err.message);
if (err.kind === 'ObjectId') {
return res.status(404).json({ msg: 'Post not found' })
}
res.status(500).send('Server Error');
}
});
My immediate thought is that the api call happens on a page other than root, in this case in /posts page (aka http://your-site.com/posts)
By adding a forward slash in front of you call will force your api request go from the root.
Try
await axios.get(`/api/posts/${id}`);

Problem requesting data based on URL parameters in React-Redux

On a MERN-stack application, I would like to request data from a particular user's schema based on the url parameters. For instance, http://localhost:3000/user/User1 would render data from User 1. I am using redux-react for state management. Here is what I currently have, and here is what's happening.
Userpage Component
import React, { Component } from "react";
import {getUser} from '../../actions/userActions';
import {connect} from 'react-redux'
class Userpage extends Component {
componentDidMount() {
getUser(this.props.match.params.username)
}
render() {
return (
<div>
<h1>Hello</h1>
<h3>{this.props.user.name}</h3>
<h3>{this.props.user.email}</h3>
</div>
);
}
}
const mapStatetoProps = state => {
return{user: state.user}
};
export default connect(mapStatetoProps, getUser) (Userpage)
userActions
import axios from 'axios';
import {GET_USER} from './types';
export const getUser = username => dispatch => {
axios
.get(`/api/users/${username}`)
.then(({res}) =>
dispatch({
type: GET_USER,
payload: username
})
)
};
userReducer
import { GET_USER } from "../actions/types";
const initialState = {
user: {},
};
export default function(state = initialState, action) {
switch (action.type) {
case GET_USER:
return{
...state,
user: action.payload,
}
}
Then in the api/users route, I have the following:
router.get("/:username", (req, res) => {
User.findOne({username: req.params.username})
.then(user => res.json(user))
});
Beyond the user state returning as empty, here are some errors I am getting.
On the command line, I occasionally get a
Could not proxy request /api/users/function%20(action)%20%7B%20%20%20%20%20%20%20%20if%20(typeof%20action%20===%20'function')%20%7B%20%20%20%20%20%20%20%20%20%20return%20action(dispatch,%20getState,%20extraArgument);%20%20%20%20%20%20%20%20%7D%20%20%20%20%20%20%20%20return%20next(action);%20%20%20%20%20%20%7D from localhost:3000 to http://localhost:5000.
As well as a
mapDispatchToProps() in Connect(Userpage) must return a plain object. Instead received undefined.
Any help or guidance would be greatly appreciated.

Resources