So I'm making a protected route with react-router-dom. And it works, sort of.
However it only works when I manually remove token from localStorage (from the browser' GUI).
Then I'm redirected to /login, and after successful login I can access my /profile. However, once token expires, it won't remove token from localStorage and won't return false from checkAuth() to <AuthRoute /> which is inside render(). Any tips how to deal with this?
import React, { Component } from 'react';
import jwt from 'jsonwebtoken';
import {
BrowserRouter,
Route,
Redirect,
Switch
} from 'react-router-dom';
import Login from './components/Login';
import Profile from './components/Profile';
const checkAuth = () => {
const token = localStorage.getItem('token');
const currentTime = Math.floor(new Date().getTime() / 1000);
const { exp } = jwt.decode(token);
if (!token) {
return false;
}
if (exp < currentTime) {
localStorage.removeItem('token');
return false;
}
return true;
};
const AuthRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
checkAuth() ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/login',
state: { from: props.location }
}}
/>
)
}
/>
);
class App extends Component {
render() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/login" component={Login} />
<AuthRoute exact path="/profile" component={Profile} />
</Switch>
</BrowserRouter>
);
}
}
export default App;
checkAuth() will call on refresh the page and on changing routes. Also make sure your if condition working properly.
Related
I hope you are doing well!
I am trying out react-router-dom v6 with the public and private routes and this is the code I currently have
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Login from "./user/pages/Login";
import { LoginRedirect } from "./auth/redirects/LoginRedirect";
import Home from "./shared/pages/Home";
import Authors from "./author/pages/Authors";
import Books from "./book/pages/Books";
import BookInformation from "./book/pages/BookInformation";
import CreateAuthor from "./author/pages/CreateAuthor";
import AuthorInformation from "./author/pages/AuthorInformation";
import { UserRoleRedirect } from "./auth/redirects/UserRoleRedirect";
import "./App.css";
function App() {
return (
<div className="App">
<BrowserRouter>
<Routes>
<Route path="login" element={<Login />} />
<Route path="/" element={<LoginRedirect />}>
<Route path="/" element={<Home />} />
<Route path="authors" element={<Authors />} />
<Route path="books" element={<Books />} />
<Route path="books/:bookId" element={<BookInformation />} />
</Route>
<Route path="/" element={<UserRoleRedirect />}>
<Route path="authors/new" element={<CreateAuthor />} />
<Route path="authors/:authorId" element={<AuthorInformation />} />
</Route>
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
and this is the code of my LoginRedirect
import React from "react";
import { useLocation, Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../useAuth";
export const LoginRedirect = () => {
const { user } = useAuth();
const location = useLocation();
if (user.email === "" || user.email === null || user.email === undefined)
return <Navigate to="/login" state={{ from: location }} replace />;
return <Outlet />;
};
and my UserRoleRedirect
import React from "react";
import { useLocation, Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../useAuth";
export const UserRoleRedirect = () => {
const { user } = useAuth();
const location = useLocation();
if (user.email === "" || user.email === null || user.email === undefined)
return <Navigate to="/login" state={{ from: location }} replace />;
if (user.roleType === "N")
return <Navigate to="/" state={{ from: location }} replace />;
return <Outlet />;
};
and my page is working fine; however, I want to make the login accessible only if the User is not logged in (I am not using any backend at the moment, I am only storing user values in redux for testing purposes) mostly because if I want to access '/books', I redirect the user to the login page if they are not logged in and on log in redirect them to the '/books' page. But if the User clicks the back button of the browser I want to return them to the Home page and not the log in page.
I tried wrapping the login route like the other routes and added a component that would test if logged in to redirect to the "/" path as follows
import React from "react";
import { useLocation, Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../useAuth";
export const AuthenticationRedirect= () => {
const { user } = useAuth();
const location = useLocation();
if (user.email !== "" && user.email !== null && user.email !== undefined)
return <Outlet />;
return <Navigate to="/" state={{ from: location }} replace />;
};
but the component rerendered infinitely and didn't work.
I could add a test in the component
if(user.email!=="")
return <Navigate to="/" />
but I want to see if there is a way to manage it from the routes themselves.
How can I make the /login route only accessible if user not logged in?
The basic idea is to wrap routes that require authentication with a custom component (PrivateRoute in the example below). PrivateRoute will use some logic to determine if the user is authenticated and then either; allow the requested route to render, or redirect to the login page.
Attached link has the answer
https://stackoverflow.com/a/47476903/4563919
I am working on a basic react auth app, right now the routes /signup and /login work when I run this repo with my .env.local file that contains firebase auth variables.
https://github.com/MartinBarker/react-auth-app
I am trying to make it so that the '/' route that points to Dashboard will only be accessible for a user who is currently signed in, and if a user is not signed in but tries to access the '/' route they will be redirected to the '/login' page.
But whenever I use the route
<PrivateRoute exact path="/" element={Dashboard} />
my chrome devtools console shows a blank page with error messages:
index.tsx:24 Uncaught Error: [PrivateRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
my PrivateRoute.js looks like this:
// This is used to determine if a user is authenticated and
// if they are allowed to visit the page they navigated to.
// If they are: they proceed to the page
// If not: they are redirected to the login page.
import React from 'react'
import { Navigate, Route } from 'react-router-dom'
import { useAuth } from '../Contexts/AuthContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
// Add your own authentication on the below line.
//const isLoggedIn = AuthService.isLoggedIn()
const { currentUser } = useAuth()
console.log('PrivateRoute currentUser = ', currentUser)
return (
<Route
{...rest}
render={props =>
currentUser ? (
<Component {...props} />
) : (
//redirect to /login if user is not signed in
<Navigate to={{ pathname: '/login'}} />
)
}
/>
)
}
export default PrivateRoute
Im not sure why this error is occurring, any help is appreciated
This behaviour seems to have changed in ReactRouter V6 here is the solution we came up with for a project.
Private route
*Re-creating the users question code
import React from 'react'
import { Navigate, Route } from 'react-router-dom'
import { useAuth } from '../Contexts/AuthContext'
const PrivateRoute = ({ children }) => {
// Add your own authentication on the below line.
//const isLoggedIn = AuthService.isLoggedIn()
const { currentUser } = useAuth()
console.log('PrivateRoute currentUser = ', currentUser)
return (
<>
{
currentUser ? (
children
) : (
//redirect to /login if user is not signed in
<Navigate to={{ pathname: '/login'}} />
)
}
</>
)
}
export default PrivateRoute
Typescript
*Our actual code implementation of this issue
const PrivateRoute: React.FC = ({ children }) => {
const navigate = useNavigate();
const { isAuthenticated, isAuthLoading } = useAuth();
const { user, isLoadingUser } = useContext(UserContext);
// Handle users which are not authenticated
// For example redirect users to different page
// Show loader if token is still being retrieved
if (isAuthLoading || isLoadingUser) {
// TODO: show full page loader
return (
<div>Loading...</div>
);
}
// Proceed with render if user is authenticated
return (
<>
{children}
</>
);
};
Router
<Router>
<Routes>
<Route
path={routes.user.accountSignup.path}
element={
<PrivateRoute>
<AccountSignup />
</PrivateRoute>
}
/>
</Routes>
</Router>
Bhai log ye adnan ka pucha wa question hai jab m usko react sikha ra tha usko questions etc krne k liye m apna account diya tha cz pehly account verify krwany k liye baqaida coding challange pass krna hota tha (khud ko legit programmer show krwane k liye), Lekin ab kiu k log meri profile dekhty hain tu mujhe embarrassment hoti hai ;(
I'm using React with Node & express and authenticating with passport and passport local, I want to know that how can I protect a route in React, I mean I want to show a component only if user is authenticated and otherwise I want it to redirect on login page.
In my case I want to protect Notes route,
The method I'm trying is not working at all...
My React Code
import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import axios from 'axios';
import Register from './Components/Register';
import Login from './Components/Login';
import Notes from './Components/Notes';
function App () {
//Function to check authentication from server
const checkAuth = () => {
axios.get('http://localhost:8000/notes', { withCredentials : true })
.then(res => res.data)
}
return (
<Switch>
<Route exact path='/' component={Register} />
<Route exact path='/login' component={Login} />
<Route exact path='/notes' render={() => {
return checkAuth()? (
<Notes />
) : (
<Redirect to="/login" />
)
}} />
</Switch>
);
};
export default App;
And My server side code
//Notes Page
router.get('/notes', (req, res) => {
if(req.isAuthenticated()) {
res.send(req.user.email);
}else{
res.send(false);
}
};
Consider showing a loader until the api call returns a value. Once the api call has returned a value then render the required component using a higher order component.
App.jsx
class App extends Component {
state = {
login: false,
loading: false,
}
componentDidMount() {
//Function to check authentication from server
this.setState( {loadnig:true}, () => {
this.checkAuth()
}
}
checkAuth = () => {
// API request to check if user is Authenticated
axios.get('http://localhost:8000/notes', { withCredentials : true })
.then( res => {
console.log(res.data);
// API response with data => login success!
this.setState({
login: true,
loading:false
});
});
.catch( error => {
console.error(error);
// Handle error
this.setState({
loading:false
});
});
}
render() {
let {login, loading} = this.state
let protectedRoutes = null;
if(loading){
return <loader/>
}
return (
<Switch>
//use authorize HOC with three parameters: component, requiresLogin, isLoggedIn
<Route exact path='/path' component={authorize(component, true, login)} />
<Route exact path='/' component={Register} />
<Route exact path='/login' component={Login} />
</Switch>
);
}
};
export default App;
Using a Higher Order Component will give you the flexibility to control routes based on the login status of the users.
Authorize,jsx
export default function (componentToRender, requiresLogin, isLoggedIn) {
class Authenticate extends React.Component<> {
render() {
if (requiresLogin) {
//must be logged in to view the page
if (!isLoggedIn) {
// redirect to an unauthorised page or homepage
this.props.history.push('/unauthorised');
return;
// or just return <unauthoriesd {...this.props} />;
}
} else {
// to be shown to non loggedin users only. Like the login or signup page
if (isLoggedIn) {
this.props.history.push('/unauthorised');
return;
//or just return <home {...this.props} />;
}
}
//if all the conditions are satisfied then render the intended component.
return <componentToRender {...this.props} />;
}
}
return Authenticate;
}
Also, if you ever decide to add in more conditions for the routing then you can easily add those to the HOC.
checking login status via "server request" takes time! Consider then to create a Class with state!, i'll give you an example below:
import React, { Component } from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import axios from 'axios';
import Register from './Components/Register';
import Login from './Components/Login';
import Notes from './Components/Notes';
class App extends Component {
state = {
login: false
}
componentDidMount() {
//Function to check authentication from server
this.checkAuth()
}
checkAuth = () => {
// API request to check if user is Authenticated
axios.get('http://localhost:8000/notes', { withCredentials : true })
.then( res => {
console.log(res.data);
// API response with data => login success!
this.setState({
login: true
});
});
}
render() {
let protectedRoutes = null;
if(this.state.login === true) {
// if login state is true then make available this Route!
protectedRoutes = <Route exact path='/notes' component={Notes} />
}
return (
<Switch>
<Route exact path='/' component={Register} />
<Route exact path='/login' component={Login} />
{ protectedRoutes }
</Switch>
);
}
};
export default App;
I hope this example will be useful to you.
Your function checkAuth since it makes a network call may not return the correct value in time for rendering, what you SHOULD do is create a state for whether or not the user is authenticated and have checkAuth update that state.
const [authenticated, updateAuthenticated] = useState(false);
...
update your checkAuth to update the authenticated state
//Function to check authentication from server
const checkAuth = () => {
axios.get('http://localhost:8000/notes', { withCredentials : true })
.then(res => updateAuthenticated(res.data)); // double check to ensure res.data contains the actual value returned
}
...
update your rendering to use the state
<Route exact path='/notes' render={() => {
return (authenticated ?
(<Notes />)
:
(<Redirect to="/login" />)
)
}} />
... with this approach you can also set the default in useState to true or false and determine whether or not it's your server side code that's giving trouble
... dont for get to call checkAuth in a useEffect somewhere at the top
useEffect(()=>{
checkAuth()
},[])
Replace your code below
<Route exact path='/notes' render={() => {
checkAuth()? (
<Notes />
) : (
<Redirect to="/login" />
)
}} />
With this code please
<Route exact path="/notes">{checkAuth ? <Notes /> : <Redirect to="/login" />}</Route>
I have:
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from "prop-types";
import {Router, Route, Switch} from 'react-router-dom'
import { Redirect } from "react-router";
import history from './History';
import Home from '../containers/Home';
import Login from '../containers/LogIn';
import CreateUsers from '../containers/CreateUsers';
import Dashboard from '../containers/Dashboard';
import NavBar from './NavBar';
class App extends Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false
};
fetch("/api/user")
.then(function (response) {
return response.json();
})
.then(function (res) {
console.log(res);
if (res.enabled === 1) {
this.setState({
isAuthenticated: true
});
// Redirect the user only if they are on the login page.
history.push("/dashboard");
} else {
history.push("/login");
}
});
}
componentDidMount() {
}
render() {
return (
<Router history={history}>
<div>
<NavBar />
<Route>
<Redirect from="/" to="/login" /> // If not authenticated, how???
</Route>
<Route path="/login" component={Login}/>
<Route path="/dashboard" component={Dashboard}/>
</div>
</Router>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'))
I have two issues, the first is how can I get it to redirect back to the login using Router if the user failed authentication, currently I'm getting the error: React.Children.only expected to receive a single React element child
The other issue is that it can't see this from:
.then(function (res) {
console.log(res);
if (res.enabled === 1) {
this.setState({
isAuthenticated: true
});
...
Giving me Uncaught (in promise) TypeError: Cannot read property 'setState' of undefined
1) use isAuthenticated state to redirect
!this.state.isAuthenticated && (<Route>
<Redirect from="/" to="/login" /> // If not authenticated, how???
</Route>)
2) use arrow function instead then you will have this from current context binded to the callback
.then((res) => {
console.log(res);
if (res.enabled === 1) {
this.setState({
isAuthenticated: true
});
// Redirect the user only if they are on the login page.
history.push("/dashboard");
} else {
history.push("/login");
}
});
1st issue:
Use state to trigger your <Redirect/>. When your fetch finishes we can use the response to update the isAuthenticated state so that it triggers a re-render.
.then((res) => {
this.setState({
isAuthenticated: res.enabled === 1,
});
});
render() {
return (
<Router history={history}>
<div>
{this.state.isAuthenticated && <Redirect from="/" to="/login" />}
</div>
</Router>
)
}
2nd issue:
This doesn't work because you're creating a new function for the response, you can solve this by changing it into an arrow function so that this points to the class OR bind then to this.
.then((res) => {
if (res.enabled === 1) {
this.setState({
isAuthenticated: true
});
}
});
.then(function(res) => {
if (res.enabled === 1) {
this.setState({
isAuthenticated: true
});
}
}).bind(this);
Trying to solve your question step by step
1st how can I get it to redirect back to the login using Router if the user failed authentication
I won't advise for your current approach because everytime you do an authentication (let's say from Facebook, Google), The callback will again reach your App Component, Again make a request and this would cause the app to go in endless loop.
Also, Too avoid side effects all the requests should be made in componentDidMount
Personal Suggestion: Use Redux here.
This is how I did client side Authentication using Redux
const AuthenticatedRoutes = ({component: Component, ...props})=> {
return (
<Route {...props} render= {(prop) => {
return (
props.prop.isAuthenticated ? <Component {...prop} /> :
<Redirect to={{
pathname: "/login",
}}/>
)}
}/>
)}
//These roots can;t be seen by users who are authenticated
const NotAuthenticatedRoutes = ({component: Component, ...props}) => (
<Route {...props} render= {(prop) => {
return (
!props.prop.isAuthenticated ? <Component {...prop} /> :
<Redirect to={{
pathname: "/",
}}/>
)}
}/>
)
class route extends Component {
render () {
return(
<BrowserRouter>
<div>
<Switch>
<NotAuthenticatedRoutes exact path ="/login" component={Login} prop={this.props} />
<AuthenticatedRoutes exact path ="/" component={HomeScreen} prop={this.props} />
</Switch>
</div>
</BrowserRouter>
)
}
}
const mapStateToProps = state => {
return {
isAuthenticated: state.profileInfo.isAuthenticated,
isLoaded: state.profileInfo.isLoaded,
googleProfileLoading: state.profileInfo.googleProfileLoading
}
};
export default connect(mapStateToProps,
{
googleProfile
})(route)
Here, If the state of the user in my app isn't set to authenticated, Inside my login Component, I make an API call for login and then set Redux State
2nd: Copying this from Win answer, Check your function for lexical scoping
.then((res) => {
if (res.enabled === 1) {
this.setState({
isAuthenticated: true
});
}
});
.then(function(res) => {
if (res.enabled === 1) {
this.setState({
isAuthenticated: true
});
}
}).bind(this);
Also, Use ComponentDidMount for Making api calls to avoid any side effects
Your initial code will work same as it is with minor change. Before making promise, take this in a variable like below-
let me = this;
and when you promise is completed and you are updating state, use like below -
me.setState({
isAuthenticated: true
});
It will work fine.
I'm using Meteor.loginWithFacebook. All is working well except after login the user is redirected back to the / page. Instead I would like to redirect the user to /dashboard.
Is this possible to do from meteor, or do I have to create a <Route> that will handle this? I am using react-router. Here's my routes:
import React from 'react'
import { Router, Route, Switch, Redirect } from 'react-router'
import createBrowserHistory from 'history/createBrowserHistory'
import { withTracker } from 'meteor/react-meteor-data'
import { Meteor } from 'meteor/meteor'
import HomePage from '../../ui/pages/HomePage.js'
import DashboardPage from '../../ui/pages/DashboardPage.js'
import NotFoundPage from '../../ui/pages/NotFoundPage.js'
const browserHistory = createBrowserHistory();
const ProtectedRoute = ({
component: Component,
user,
loggingIn,
...rest
}) =>
(!user && loggingIn && <div>Loading</div>) ||
(user && <Route {...rest} render={props =>
<Component {...props} user={user} />}
/>) ||
<Redirect to="/"/>
const Routes = (props) => (
<Router history={browserHistory}>
<Switch>
<ProtectedRoute exact path="/dashboard" component={DashboardPage} {...props} />
<Route exact path="/" component={HomePage} />
<Route component={NotFoundPage} />
</Switch>
</Router>
)
export default withTracker((props) => {
return {
user: Meteor.user(),
loggingIn: Meteor.loggingIn()
}
})(Routes)
I figured it out. The documentation shows loginWithFacebook accepts a redirectUrl property. I simply set this to the dashboard URL. After the user has logged in they will be redirected to the dashboard.
To show an example here is the click handler for button component I created. When the user clicks they are redirected to accept the Facebook application permissions, once complete they will be redirected to the dashboard route:
handleClick = () => {
Meteor.loginWithFacebook(
{
requestPermissions: this.props.permissions,
loginStyle: this.props.loginStyle,
redirectUrl: Meteor.absoluteUrl('dashboard'),
},
err => {
// These will be unsed when using redirect flow
if (err) return this.props.onError(err)
return this.props.onSuccess()
}
)
}