Private route /profile rendering me an error.
My app works until I try to go on /profile.
I have never seen this error yet, so while Im searching Im posting here, maybe someone has an idea.
Also Im looking for a good documentation/tutorial on how to make private routes using jwt and js-cookie.
error
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
App.js
import { useState } from 'react';
import Axios from "axios";
import Cookies from "js-cookie";
import { BrowserRouter as Router, Link, Route, Switch } from "react-router-dom";
import ProtectedRoute from './components/ProtectedRoute';
import Profile from "./pages/Profile";
const App = () => {
const [emailRegistration, setEmailRegistration] = useState("");
const [passwordRegistration, setPasswordRegistration] = useState("");
const [emailLogin, setEmailLogin] = useState("");
const [passwordLogin, setPasswordLogin] = useState("");
const [isAuth, setIsAuth] = useState(false);
const register = () => {
Axios.post('http://localhost:3000/api/signup', {
user:{
email: emailRegistration,
password: passwordRegistration
}
}).then((response) => {
console.log(response);
});
};
const login = () => {
fetch("http://localhost:3000/api/login", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user: {
email: emailLogin,
password: passwordLogin,
},
}),
})
.then((res) => {
if (res.ok) {
console.log(res.headers.get("Authorization"));
localStorage.setItem("token", res.headers.get("Authorization"));
setIsAuth(true)
return res.json();
} else {
return res.text().then((text) => Promise.reject(text));
}
})
.then((json) => console.dir(json))
.catch((err) => console.error(err));
};
const getArticles = () => {
fetch("http://localhost:3000/articles", {
headers: {
"Content-Type": "application/json",
Authorization: localStorage.getItem("token"),
},
})
.then((res) => {
if (res.ok) {
return res.json();
} else if (res.status == "401") {
throw new Error("Unauthorized Request. Must be signed in.");
}
})
.then((json) => console.dir(json))
.catch((err) => console.error(err));
}
const logout = () => {
fetch("http://localhost:3000/api/logout", {
method: "delete",
headers: {
"Content-Type": "application/json",
Authorization: localStorage.getItem("token"),
},
})
.then((res) => {
if (res.ok) {
setIsAuth(false)
return res.json();
} else {
return res.json().then((json) => Promise.reject(json));
}
})
.then((json) => {
console.dir(json);
})
.catch((err) => console.error(err));
};
const testBearer = () => {
let cookie = Cookies.get('token');
console.log(cookie)
}
return (
<Router>
<div className="app">
{isAuth ?
(
<button onClick={logout} > logout </button>
)
:
(
<div className="auth">
<div className="registration">
<h1> Registration </h1>
<label> Email </label>
<input
type="email"
onChange={(e) => {
setEmailRegistration(e.target.value);
}}
/>
<label> Password </label>
<input
type="password"
onChange={(e) => {
setPasswordRegistration(e.target.value);
}}
/>
<button onClick={register} > Register </button>
</div>
<div className="login">
<h1> Login </h1>
<label> Email </label>
<input
type="email"
onChange={(e) => {
setEmailLogin(e.target.value);
}}
/>
<label> Password </label>
<input
type="password"
onChange={(e) => {
setPasswordLogin(e.target.value);
}}
/>
<button onClick={login} > Login </button>
</div>
</div>
)
}
<br />
<hr />
<br />
<button onClick={getArticles} >getArticles </button>
<button onClick={testBearer} >testCookie </button>
<button onClick={logout} > logout </button>
</div>
<Route path="/">
</Route>
<ProtectedRoute exact path="/profile" component={Profile} isAuth={isAuth} />
</Router>
)};
export default App;
Profile.js
import React from 'react';
import {withRouter} from "react-router-dom";
const Profile = () => {
return (
<div>
if you see this you entered the auth.
</div>
);
};
export default withRouter(Profile);
ProtectedRoute.js
import React from 'react';
import {Route, Redirect} from "react-router-dom";
const ProtectedRoute = ({isAuth: isAuth, component: Component, ...rest }) => {
return (
<Route {...rest} render={(props) => {
if (isAuth) {
return <Component />
} else {
return (
<Redirect to={{pathname: "/profile", state: {from: props.location }}} />
);
}
}}/>
);
};
export default ProtectedRoute;
Let your ProtectedRoutes to have a type based on their auth capabilities such as LoginRoute and PrivateRoute. Your Profile route acts as the login page. Therefore, it doesn't need to be user authenticated to view that page. Then create another route as dashboard, which will be the page user able to view in a successful log in. Now, keep your routes as follows.
<Route path="/"></Route>
<ProtectedRoute
exact
path="/profile"
component={Profile}
isAuth={isAuth}
type="LoginRoute"
/>
<ProtectedRoute
exact
path="/dashboard"
component={() => <div>User Entered into Dashboard</div>}
isAuth={isAuth}
type="PrivateRoute"
/>
Then, you need to handle the type based authentication functionality inside your ProtectedRoute component as follows.
import React from "react";
import { Route, Redirect } from "react-router-dom";
const getRouteVariables = (type, isAuth) => {
switch (type) {
case "LoginRoute":
return [!isAuth, "/dashboard"];
case "PrivateRoute":
return [isAuth, "/profile"];
default:
return [isAuth, "/profile"];
}
};
const ProtectedRoute = ({ type, isAuth, component: Component, ...rest }) => {
const [isRouteChanged, redirectPath] = getRouteVariables(type, isAuth);
return (
<Route
{...rest}
render={(props) => {
if (isRouteChanged) {
return <Component />;
} else {
return (
<Redirect
to={{ pathname: redirectPath, state: { from: props.location } }}
/>
);
}
}}
/>
);
};
export default ProtectedRoute;
getRouteVariables function return isRouteChanged and redirectPath. isRouteChanged means whether route needs to be changed based on isAuth and redirectPath means which path needs to be taken if redirection happens.
For LoginRoute type /profile route only comes up when isAuth becomes false. If isAuth is true, then moving to /profile route will redirect to /dashboard.
For PrivateRoute type /dashboard route only possible to access when isAuth becomes true. If isAuth is false, then moving to /dashboard route will redirect to /profile.
Related
for some reason function, that is passed into a functional component never gets called.
The onSubmit function gets called but then it never calls login(email, password). Basically I pass the login function from auth.tsx into the Login component and then I try to call the function in onSubmit function inside Login component but the function never gets called. I tried adding console.log inside the login function but nothing gets returned in console nor I see any API requests in backend console.
Here is my code:
App.tsx
import React, { FunctionComponent } from 'react'
import Home from './pages/Home/Home'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Activate, Login, Signup, ResetPassword, ResetPasswordConfirm } from './pages/Account'
import Layout from './components/Layout'
import { login } from "./actions/auth";
import { Provider } from 'react-redux'
import store from './store'
const App: FunctionComponent = () => (
<Provider store={store}>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login login={login}/>} />
<Route path="/signup" element={<Signup />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/password/reset/confirm/:uid/:token" element={<ResetPasswordConfirm />} />
<Route path="/activate/:uid/:token" element={<Activate />} />
</Routes>
</Router>
</Provider>
)
export default App
Login.tsx
import { FunctionComponent, useState } from "react";
import { Link, redirect } from "react-router-dom";
import { connect } from "react-redux";
interface IFormData {
email: string;
password: string;
}
interface ILogin {
login: (email: string, password: string) => void;
}
const Login: FunctionComponent<ILogin> = ({ login }) => {
const [formData, setFormData] = useState<IFormData>({
email: "",
password: "",
});
const { email, password } = formData;
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setFormData({
...formData,
[e.target.name]: e.target.value,
})
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
login(email, password)
}
return (
<div>
<h1>Sign In</h1>
<p>Sign into your account</p>
<form onSubmit={e => onSubmit(e)}>
<div>
<input
type="email"
placeholder="email"
name="email"
value={email}
onChange={e => onChange(e)}
required
/>
</div>
<div>
<input
type="password"
placeholder="password"
name="password"
value={password}
onChange={e => onChange(e)}
minLength={6}
required
/>
</div>
<button type="submit" >
Login
</button>
</form>
<p>
Don't have an account? <Link to="/signup">Sign Up</Link>
</p>
<p>
Forgot your password? <Link to="/reset-password">Reset Password</Link>
</p>
</div>
)
}
export default connect(null, { })(Login);
auth.tsx
import {
LOGIN_SUCCESS,
LOGIN_FAIL,
USER_LOAD_SUCCESS,
USER_LOAD_FAIL
} from './types';
import axios from 'axios';
export const load_user = () => async (dispatch: any) => {
if (localStorage.getItem('access')) {
const config = {
headers: {
'Content-Type': 'application/json',
'Authorization': `JWT ${localStorage.getItem('access')}`,
'Accept': 'application/json'
}
};
try {
const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/users/me/`, config);
dispatch({
type: USER_LOAD_SUCCESS,
payload: res.data
});
} catch (err) {
dispatch({
type: USER_LOAD_FAIL
});
}
} else {
dispatch({
type: USER_LOAD_FAIL
});
}
};
export const login = (username: string, password: string) => async (dispatch: any) => {
const config = {
headers: {
'Content-Type': 'application/json'
}
}
const body = JSON.stringify({ username, password });
console.log("yay")
try {
const res = await axios.post(`${process.env.REACT_APP_API_URL}/api/jwt/create/`, body, config);
dispatch({
type: LOGIN_SUCCESS,
payload: res.data
});
dispatch(load_user());
} catch (err) {
dispatch({
type: LOGIN_FAIL
});
}
}
This is because your login function is a curried function and hence if you wish to execute it directly you would need to do login(email, password)()
Your login function (defined in auth.tsx) seems to look like a thunk/saga in which case you should be dispatching the login function call from the Login component (defined in Login.tsx) using the MapDispatchToProps callback for the connect HOC.
I am trying to create protected routing and have created a component called Protected which fetches the access token from the backend if user exists and displays the protected components if user exists or gets navigated to the Not logged in component. This functionality is not working as expected. For some reason I am getting current user as undefined. Is it because the protected route's Navigate is getting rendered even before access token gets generated and stored in localStorage?
Below is the code
import React, { useEffect, useState } from 'react'
import { Outlet, Navigate } from 'react-router-dom'
import authService from '../services/auth.service';
function Protected() {
const [currentUser, setCurrentUser] = useState(undefined);
console.log(currentUser);
useEffect(() => {
const user = authService.getCurrentUser();
if (user) {
setCurrentUser(user);
}
}, []);
return (
currentUser ? <Outlet/> : <Navigate to='/notloggedin'/>
)
}
export default Protected
authService.js
import axios from "axios";
const API_URL = "http://localhost:3000/auth";
const signup = (email, password) => {
try {
return axios
.post(API_URL + "/signup", {
email,
password,
})
.then((response) => {
//console.log(response.data.errors);
if (response.data.accessToken) {
localStorage.setItem("user", JSON.stringify(response.data));
}
console.log(response.data.errors?.map((error) => console.log(error.msg)))
return response.data;
});
} catch(error) {
return error;
}
};
const login = (email, password) => {
return axios
.post(API_URL + "/login", {
email,
password,
})
.then((response) => {
if (response.data.accessToken) {
localStorage.setItem("user", JSON.stringify(response.data));
}
console.log(response.data.errors?.map((error) => console.log(error.msg)))
return response.data;
});
};
const logout = () => {
localStorage.removeItem("user");
};
const getCurrentUser = () => {
return JSON.parse(localStorage.getItem("user"));
};
const authService = {
signup,
login,
logout,
getCurrentUser,
};
export default authService;
App.js
function App() {
return (
<>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/notloggedin" element={<NotLoggedIn/>} />
<Route element={<Protected/>}>
<Route path="/restaurants" element={<Food_Main />} />
<Route path="/:food/:id" element={<Food_Detail />} />
<Route path="/payment" element={<PaymentDetails />} />
<Route path="/thankyou" element={<ThankYou />} />
</Route>
</Routes>
</>
);
}
export default App;
The initial currentUser value is undefined which is also a falsey value and the useEffect hook runs at the end of the render cycle, so on the initial render the Navigate component is returned and the user is navigated off the route you are trying to protect.
Render null or some loading indicator while the auth status is being validated/verified.
Example:
function Protected() {
const [currentUser, setCurrentUser] = useState(undefined);
useEffect(() => {
const user = authService.getCurrentUser();
setCurrentUser(!!user); // <-- sets to defined boolean value
}, []);
if (currentUser === undefined) {
return null; // or loading indicator/spinner/etc...
return currentUser
? <Outlet/>
: <Navigate to='/notloggedin' replace />;
}
I am new at React and i am trying to develop a login page. I did almost all but I could not re-render after successful login. I need to refresh the page to see the protected content. As far as I know, using custom Hook and changing its value should trigger a component re-render but although I change the value of it (token), page is not re-rendered. What is the point i missed?
//App.js
function App() {
const { token, setToken } = useToken();
if(!token) {
return <Login setToken={setToken} />
}
return (
<div className="wrapper">
<h1>Application</h1>
<BrowserRouter>
<Routes>
<Route path="/" element={<Dashboard />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
//UseToken.js
export default function useToken() {
const getToken = () => {
const tokenString = localStorage.getItem('token');
const userToken = JSON.parse(tokenString);
return userToken;
};
const [token, setToken] = useState(getToken());
const saveToken = userToken => {
localStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
};
return {
setToken: saveToken,
token
}
}
//Login.js
async function loginUser(credentials) {
return fetch('http://localhost:8080/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
}).then(data => data.json())
.then(response => response.token)
}
export default function Login({ setToken }) {
const [username, setUserName] = useState();
const [password, setPassword] = useState();
const handleSubmit = async e => {
e.preventDefault();
const token = await loginUser({
username,
password
});
setToken(token);
}
return(
<div className="login-wrapper">
<h1>Please Log In</h1>
<form onSubmit={handleSubmit}>
<label>
<p>Username</p>
<input type="text" onChange={e => setUserName(e.target.value)}/>
</label>
<label>
<p>Password</p>
<input type="password" onChange={e => setPassword(e.target.value)}/>
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
)
}
Login.propTypes = {
setToken: PropTypes.func.isRequired
}
//Dashboard.js
export default function Dashboard() {
const logout = () => {
localStorage.clear();
}
return(
<div>
<h2>Dashboard</h2>
<button type="button" onClick={logout}>Logout</button>
</div>
);
}
In useToken.js
const saveToken = userToken => {
localStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
}
should be replaced to
const saveToken = userToken => {
localStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken); //userToken.token changed to userToken
}
Because, there is no variable named as 'token' in userToken object. It is token itself. So the hook thinks that there is no change and because of that it does not re-render.
Problem: I am trying to authenticate a user through isAuth() helpers but it is acting weird. I want it to look for access token if any or call for access token from backend if refresh token is available, and though it works perfectly and sets access token cookie, the issue is if called from PrivateRoutes.jsx, it does not sees the tokens at all and sends the user to login page.
Adding required code for refs:
isAuth():
export const isAuth = () => {
if (window !== undefined) {
const accessCookieChecked = getCookie("_mar_accounts_at");
const refreshCookieChecked = getCookie("_mar_accounts_rt");
if (accessCookieChecked) {
return true;
} else if (refreshCookieChecked) {
console.log(refreshCookieChecked);
axios({
method: "POST",
url: `${API_URL}/api/token`,
data: { refresh_token: refreshCookieChecked },
}).then((res) => {
console.log(res);
setCookie("_mar_accounts_at", res.data.accessToken);
return true;
});
} else {
return false;
}
} else {
return false;
}
};
PrivateRoutes.jsx
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { isAuth } from "../helpers/auth";
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={(props) =>
isAuth() ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
)
}
></Route>
);
export default PrivateRoute;
Can someone please see this? And help!
You are running into an async issue most likely, when you make the call in axios, the return true; in the callback never actually returns to your funciton call in the PrivateRoute. Instead, you need to use a Promise/setState/useEffect:
export const isAuth = () => {
if (window === undefined) {
return Promise.resolve(false);
} else {
const accessCookieChecked = getCookie("_mar_accounts_at");
const refreshCookieChecked = getCookie("_mar_accounts_rt");
if (accessCookieChecked) {
return Promise.resolve(true);
} else if (refreshCookieChecked) {
console.log(refreshCookieChecked);
return new Promise(resolve => {
axios({
method: "POST",
url: `${API_URL}/api/token`,
data: { refresh_token: refreshCookieChecked },
}).then((res) => {
console.log(res);
setCookie("_mar_accounts_at", res.data.accessToken);
resolve(true);
});
})
} else {
return Promise.resolve(false);
}
}
};
import React, { useState, useEffect } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { isAuth } from '../helpers/auth';
const PrivateRoute = ({ component: Component, ...rest }) => {
const [isAuthTrue, setIsAuthTrue] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => {
isAuth().then(res => {
setIsAuthTrue(res);
setLoading(false);
})
})
return (
<>
{loading ? (
<div>some loading state</div>
) : (
<Route
{...rest}
render={(props) =>
isAuthTrue ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: '/login', state: { from: props.location } }}
/>
)
}
/>
)}
</>
);
};
export default PrivateRoute;
I use the axios post to request to the back-end if the user have access to the application. The problem is the axios returns undefined and then true or false . Have a private Route to manage what to do in case returns true or false (in this case undefined = false) ,is axios the problem or is there some other way? like wait until returns true or false
IsLogin.jsx
import React from 'react'
const axios = require('axios');
export const AuthContext = React.createContext({})
export default function Islogin({ children }) {
const isAuthenticated =()=>{
try{
axios.post('/api/auth').then(response => {
var res = response.data.result;
console.log(res)
return res
})
} catch (error) {
console.error(error);
return false
}
}
var auth = isAuthenticated()
console.log(auth);
return (
<AuthContext.Provider value={{auth}}>
{children}
</AuthContext.Provider>
)
}
privateRoute.js
import React, { useContext } from 'react';
import { Route, Redirect } from 'react-router-dom';
import {AuthContext} from '../utils/IsLogin';
const PrivateRoute = ({component: Component, ...rest}) => {
const {isAuthenticated} = useContext(AuthContext)
return (
// Show the component only when the user is logged in
// Otherwise, redirect the user to /unauth page
<Route {...rest} render={props => (
isAuthenticated ?
<Component {...props} />
: <Redirect to="/unauth" />
)} />
);
};
export default PrivateRoute;
app.js
class App extends Component {
render() {
return (
<>
<BrowserRouter>
<Islogin>
<Header/>
<Banner/>
<Switch>
<PrivateRoute exact path="/index" component={Landing} />
<PrivateRoute path="/upload" component={Upload} exact />
<PublicRoute restricted={false} path="/unauth" component={Unauthorized} exact />
</Switch>
</Islogin>
</BrowserRouter>
</>
);
}
}
You don't want to return anything in your post request. You should be updating your context store
const isAuthenticated = () => {
try {
axios.post('/api/auth').then(response => {
var res = response.data.result;
console.log(res)
// update your context here instead of returning
return res
})
} catch (error) {
console.error(error);
return false
}
}
In your private route, have a componentDidUpdate style useEffect hook to check for changes in authentication status and update an internal flag on an as-needed basis
const PrivateRoute = ({ component: Component, ...rest }) => {
const { isAuthenticated } = useContext(AuthContext)
const [validCredentials, setValidCredentials] = React.useState(false)
React.useEffect(() => {
if (typeof isAuthenticated === 'boolean') {
setValidCredentials(isAuthenticated)
}
}, [isAuthenticated])
return (
// Show the component only when the user is logged in
// Otherwise, redirect the user to /unauth page
<Route {...rest} render={props => (
validCredentials ?
<Component {...props} />
: <Redirect to="/unauth" />
)} />
);
};
I am curious as to why you didn't use 'async await',lol.
You are making a post request to the endpoint '/api/auth',but you didn't give it any data to post,like:
try{
axios.post('/api/auth',{username,password}).then(response => {
var res = response.data.result;
console.log(res)
return res
})
} catch (error) {
console.error(error);
return false
}