I want to login user automatically if refresh_token exists in localStorage but my issue here is, i am not able to change state to authenticated initially when app start. I tried with componentWillMount but i am getting old state 1st time then i get updated state. <PrivateRoute> here getting called 2-3 time, don't know where i am making mistake.
Expected Flow
autoLogin
PrivateRoute with new state
App.js
import React, { Component } from 'react'
import WrappedLoginForm from './containers/Login'
import ChatApp from './containers/Chat'
import { connect } from 'react-redux'
import { checkAuthentication } from './store/actions/auth'
import PrivateRoute from './route'
import {
BrowserRouter as Router,
Route
} from "react-router-dom";
class App extends Component {
componentWillMount() {
this.props.autoLogin()
}
render() {
return (
<Router>
<Route path='/login' component={WrappedLoginForm} />
<PrivateRoute path="/" component={ChatApp} authed={this.props.isAuthenticated} />
</Router>
)
}
}
const mapStateToProps = state => {
return {
isAuthenticated: state.isAuthenticated,
loading: state.loading
}
}
const mapDispatchToProps = dispatch => {
return {
autoLogin: () => {
dispatch(checkAuthentication())
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)
route.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const PrivateRoute = ({ component: Component, authed, ...rest }) => {
console.log('aaa')
return (<Route
render={props => (
authed
? <Component />
: <Redirect to="/login" />
)}
/>)
};
export default PrivateRoute;
action.js
export const checkAuthentication = () => {
return dispatch => {
dispatch(authStart())
let refresh_token = localStorage.getItem('refresh_token')
axiosInstance.post('/api/token/refresh/', {
refresh: refresh_token
}).then(res => {
if (res.status === 200) {
localStorage.setItem("access_token", res.data.access)
dispatch(loginSuccess())
}
}).catch(error => {
console.log("balle")
dispatch(loginFailed(error))
})
}
}
first thing make sure that in your store the isAuthenticated property is false by default.
For PrivateRoute use :
{this.props.isAuthenticated
? <PrivateRoute path="/" component={ChatApp} />
: null
}
instead of <PrivateRoute path="/" component={ChatApp} authed={this.props.isAuthenticated} />
Related
I'm coding an app that requires you to login using Laravel/Sanctum as the backend (no JWT). I'm using localStorage to persist the user's login details in case the browser page is reloaded, I can then re-apply state in my useAuthHandler hook.
This works fine, but I would like to either:
a) Replace the localStorage with an Axios request to ensure I re-apply state with genuine login data from the backend (i.e. the user did not forge localStorage details) before rendering the app.
--- OR ---
b) Keep using localStorage, but send an Axios request back to the server to confirm the localStorage details were indeed valid before rendering any of the app.
With the following code, I'm having trouble making the Axios request synchronous in the getStoredAuth() function where the localStorage is fetched. I need to avoid having my app render before the request has returned. This is so I can avoid any page 'flash' while auth details are being checked.
AuthContext.js
import useAuthHandler from "../utils/custom-hooks/AuthHandler";
import { getStoredUserAuth } from "../helpers/Helpers";
export const authContext = createContext();
const { Provider } = authContext;
const AuthProvider = ({ children }) => {
const { isAuthenticated, user, setAuthStatus, setUnauthStatus } = useAuthHandler(
getStoredUserAuth()
);
return (
<Provider value={{ isAuthenticated, user, setAuthStatus, setUnauthStatus }}>
{children}
</Provider>
);
};
export default AuthProvider;
AuthHandler.js
import { useState } from "react";
import { DEFAULT_USER_AUTH } from "../Consts";
const useAuthHandler = (initialState) => {
const [isAuthenticated, setIsAuthenticated] = useState(initialState.isAuthenticated);
const [user, setUser] = useState(initialState.user);
const setAuthStatus = (userAuth) => {
window.localStorage.setItem("UserAuth", JSON.stringify(userAuth));
setIsAuthenticated(userAuth.isAuthenticated);
setUser(userAuth.user);
};
const setUnauthStatus = () => {
window.localStorage.clear();
setIsAuthenticated(DEFAULT_USER_AUTH.isAuthenticated);
setUser(DEFAULT_USER_AUTH.user);
};
return {
isAuthenticated,
user,
setAuthStatus,
setUnauthStatus,
};
};
export default useAuthHandler;
Helpers.js
import { DEFAULT_USER_AUTH } from "../utils/Consts";
/** Return user auth from local storage value */
export const getStoredUserAuth = () => {
const auth = window.localStorage.getItem("UserAuth");
if (auth) {
/**
* Axios synchronous check with server if login is still valid here
* before returning any data.
*
* If the localStorage data is invalid, or the server's session has
* expired, then redirect to /login before rendering anything.
*/
return JSON.parse(auth);
}
return DEFAULT_USER_AUTH;
};
Consts.js
export const DEFAULT_USER_AUTH = { isAuthenticated: false, user: {} };
App.js
import { IonApp, IonRouterOutlet, setupIonicReact } from '#ionic/react';
import { IonReactRouter } from '#ionic/react-router';
import { Redirect, Route, Switch } from 'react-router-dom';
import routes from './routes';
import { useState, useEffect } from 'react';
import { Content, Page } from "./components/IonComponents";
import { useSelector } from "react-redux";
import Menu from "./components/Menu/Menu";
import { PrivateRoute } from "./components/Menu/PrivateRoute";
import AuthContextProvider from "./contexts/AuthContext";
/* Core CSS required for Ionic components to work properly */
import '#ionic/react/css/core.css';
/* Stylesheets */
import './assets/scss/variables.scss';
import "bootstrap/dist/css/bootstrap.min.css";
import "./assets/scss/propeller-styles.scss";
setupIonicReact();
const App = () => {
const [displayMenuExpanded, setDisplayMenuExpanded] = useState(false);
const menuExpanded = useSelector((state) => state.menuExpanded.value);
useEffect(() => {
setDisplayMenuExpanded(menuExpanded);
}, [menuExpanded]);
return (
<IonApp>
<IonReactRouter basename={process.env.PUBLIC_URL}>
<AuthContextProvider>
<Menu expanded={displayMenuExpanded} />
<IonRouterOutlet
className={displayMenuExpanded ? "router-narrow" : "router-expanded"}
>
<Switch>
<>
<Page>
<Content
className={
displayMenuExpanded ? "content-narrow" : "content-expanded"
}
>
<div id="content-wrapper">
<Route exact path="/">
<Redirect to="/utilities" />
</Route>
{routes.map((route, key) => (
<PrivateRoute
key={key}
path={route.pathname}
exact
component={route.component}
/>
))}
</div>
</Content>
</Page>
</>
</Switch>
</IonRouterOutlet>
</AuthContextProvider>
</IonReactRouter>
</IonApp >
);
};
export default App;
PrivateRoute.js
import { useContext } from 'react'
import { Route, Redirect } from 'react-router-dom';
import { authContext } from "../../contexts/AuthContext";
export { PrivateRoute };
function PrivateRoute({ component: Component, ...rest }) {
const authCtx = useContext(authContext);
if (!Component) return null;
return (
<Route {...rest} render={props => {
if (!authCtx.isAuthenticated && props.location.pathname !== "/login") {
// Not logged-in so redirect to login page with the return url
return <Redirect to={{ pathname: '/login', state: { from: props.location } }} />
}
// Authorised so return component
return <Component {...props} />
}} />
);
}
The above PrivateRoute component is how I redirect the user if they're not authenticated.
Thank you for your help.
I am using react hooks with redux in my project. In the login component from my action file when I try to redirect to another page i.e. another component. It is redirecting to the login component within a few seconds.
Here is the code:
authReducer.js
const authReducer = (state = iState, action) => {
switch (action.type) {
case "IS_LOGIN":
return {
...state,
isLogin: action.payload,
};
}
})
userAction.js
export const loginSubmit = (data, props) => {
return async (dispatch) => {
axios
.post(`${process.env.REACT_APP_API_URL}login`, data)
.then((result) => {
if (result.data.code == 200) {
dispatch({
type: "IS_LOGIN",
payload: {
data: result.data.data,
authToken: result.data.authToken,
},
});
localStorage.setItem("debateAccountToken", result.data.authToken);
localStorage.setItem("email", result.data.data.email);
localStorage.setItem("id", result.data.data.id);
toast.success("Logged in successfully");
// setInterval(() => {
props.history.push("/log");
// }, 3000);
} else {
toast.error("Email or password wrong!!");
}
})
.catch((err) => {
console.log("error .. ", err);
toast.error("Somethihng went wrong!!");
setInterval(() => {
window.location.reload();
}, 3000);
});
};
};
component file -> route of /log
import React from "react";
function LoginPage() {
return <div>hello</div>;
}
export default LoginPage;
route file
import React, { Component } from "react";
import { BrowserRouter, Route } from "react-router-dom";
import Login from "./components/UserLogin";
import Debate from "./components/debate/Debate";
import LandingPage from "./components/LandingPage";
import UserRegister from "./components/UserRegister";
import LoginPage from "./components/LoginPage";
export default class App extends Component {
render() {
return (
<div>
<BrowserRouter>
<Route exact path="/" component={LandingPage} />
<Route exact path="/register" component={UserRegister} />
<Route exact path="/debate" component={Debate} />
<Route path="/login" component={Login} />
<Route path="/log" component={LoginPage} />
</BrowserRouter>
</div>
);
}
}
From useraction it is redirecting to /log component but eventually it is returning back to login component too. Where might I be mistaken?
I am trying to use history.push method in my redux react app. Its working fine but the problem is my component won't change, do you guys know why?
route.js:
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import { history } from '../helper/history'
export default class route extends React.Component {
render() {
return (
<Provider store={store}>
<Router history={history}>
<Switch>
<Route exact path="/login" component={Login} />
<Route path="/error" component={Error} />
</Switch>
</Router>
</Provider>
)
}
}
Redux action.js where a history is called, this is where I dispatch my actions:
export const loginRequest = () => {
return {
type: userType.LOGIN_REQUEST,
}
}
export const loginSuccess = () => {
return {
type: userType.LOGIN_SUCCESS,
}
}
export const loginFailure = () => {
return {
type: userType.LOGIN_FAILURE,
}
}
export const fetchUser = (data) => {
return (dispatch) => {
dispatch(loginRequest)
axios
.post(env.API_URL + 'login', { email: data.email, password: data.password })
.then((res) => {
dispatch(loginSuccess(res.data.user))
history.push({
pathname: '/profile',
})
})
.catch((err) => {
dispatch(loginFailure(error))
})
}
}
As you are providing history props, you should use Router. See this, Router and BrowserRouter:
import { Router, Route, Switch } from 'react-router-dom'
instead of
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
I have a component I am using for authentication, if a user is not authenticated I want to push them to the login page.
A basic example of the setup is...
Auth Component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { auth } from '../../firebase';
import { replace } from 'react-router-redux';
export default (WrappedComponent, {}) => {
class Authentication extends Component {
componentWillMount() {
auth.onAuthStateChanged(user => {
if (user) {
// User is signed in.
console.log('USER IS SIGNED IN');
} else {
// No user is signed in.
console.log('USER IS NOT SIGNED IN');
this.props.dispatch(replace('login'));
}
});
}
render() {
return <WrappedComponent>{this.props.children}</WrappedComponent>;
}
}
return connect(
null,
null
)(Authentication);
};
Routes
import React from 'react';
import Loadable from 'react-loadable';
import { Router, Route, IndexRoute } from 'react-router';
import AuthenticationComponent from './containers/Authentication';
import App from './components/App';
const AsyncRoute = loader =>
Loadable({
loader,
loading: () => <h3>Loading...</h3>,
delay: 300,
});
const LandingPage = AsyncRoute(() =>
import(/* webpackPrefetch: true, webpackChunkName: "landingPage" */ './containers/LandingPage')
);
const Login = AsyncRoute(() =>
import(/* webpackPrefetch: true, webpackChunkName: "login" */ './containers/Login')
);
const NotYetImplemented = () => <h6>Not Yet Implemented...</h6>;
export default ({ history }) => (
<Router history={history}>
<Route path="/" component={AuthenticationComponent(App, {})}>
<IndexRoute component={LandingPage} />
</Route>
<Route path="/login" component={Login} />
</Router>
);
Currently, when Firebase reports the user is not authenticated, the route is updated and shows as http://localhost:3001/login however the LandingPage component is rendered.
If I refresh the page on /login I do then get the correct component.
I have swapped out replace for push but had the same result.
like #Boo said you have to use exact or you can use switch and Route instead like this
import { Switch, Route} from 'react-router-dom'
<Switch>
<Route path='/' component={ HomePage } exact />
<Route path='/login' component={ LogIn } exact />
</Switch>
and using
Redirect
you can do something like this (the code below) instead of dispatching action in redux and this will work for sure
import { Redirect } from 'react-router'
export default (WrappedComponent, {}) => {
class Authentication extends Component {
state = { redirect : false }
componentWillMount() {
auth.onAuthStateChanged(user => {
if (user) {
// User is signed in.
console.log('USER IS SIGNED IN');
} else {
// No user is signed in.
console.log('USER IS NOT SIGNED IN');
//this.props.dispatch(replace('login'));
this.setState({redirect : true})
}
});
}
render() {
const {redirect } = this.state
if(!redirect){
return <WrappedComponent>{this.props.children}</WrappedComponent>;
} else {
return <Redirect to='where ever you want' />
}
}
I have a state that is getting the right object from my action but It won't seem to actually append to the state. Does anyone have any experience working with reducers in React-Redux that can lend a hand? I'm not sure why I can't return the new state.
Here is the code in progress I have at the moment.
import * as types from '../constants'
const defaultState = {}
const userReducer = (state = defaultState, action) =>{
switch(action.type){
case types.USER_LOGGED_IN:
console.log("in the logged in reducer")
console.log(action.cookie)
return {
...state,
cookie: action.cookie
}
case types.USER_LOGGED_OUT:
return state
default:
return state
}
}
export default userReducer
The console.log will actually print out the correct cookie value for me. Any ideas or help would be greatly appreciated.
per Request here is the container,
import React, { Component, PropTypes } from 'react'
import { routerActions } from 'react-router-redux'
import { connect } from 'react-redux'
import GoogleLogin from '../components/GoogleLogin'
import actions from '../actions/'
import cookie from 'react-cookie'
const login = actions.userActions.login
function select(state, ownProps) {
const isAuthenticated = state.user.cookie || false
console.log(isAuthenticated)
const redirect = ownProps.location.query.redirect || '/'
return {
isAuthenticated,
redirect
}
}
class LoginContainer extends Component {
componentWillMount() {
const { isAuthenticated, replace, redirect } = this.props
if (isAuthenticated) {
replace(redirect)
}
}
componentWillReceiveProps(nextProps) {
const { isAuthenticated, replace, redirect } = nextProps
const { isAuthenticated: wasAuthenticated } = this.props
if (!wasAuthenticated && isAuthenticated) {
replace(redirect)
}
}
onClick(e){
e.preventDefault()
//console.log("in the onClick")
var status = cookie.load("MarketingStatsApp",false)
if(!(status == undefined)){
const login = actions.userActions.login
this.props.login({
cookie: status
})
}
window.location.href='auth/google'
};
render() {
return (
<div>
<h1>Login using Google</h1>
<GoogleLogin onClick={this.onClick.bind(this)}/>
</div>
)
}
}
export default connect(select, { login, replace: routerActions.replace })(LoginContainer)
Here you can see the false get printed out the first time through and then the cookie get printed out. You can even see the action but I can't click into the arrows for that action and the the other ones don't display the updated state as how I expect it to be
Thanks for the help everyone
The problem ended up being on my double verification on my router page. I needed to re-bring in the action variable for it to launch properly. Here is the code for that
import { Router, Route, browserHistory,IndexRedirect } from 'react-router'
import React from 'react';
import Layout from './components/Layout'
import Home from './containers/Home'
import Login from './containers/LoginContainer'
import Dashboard from './containers/Dashboard'
import { UserIsAuthenticated} from './util/wrapper'
import cookie from 'react-cookie'
import axios from 'axios'
import actions from './actions'
import store from './reducers';
const Authenticated = UserIsAuthenticated((props) => props.children)
function doubleCheckGUID(guid){
return axios.post("/auth/guid",{guid: guid}).then(response => {
const login = actions.userActions.login
store.dispatch(login({
cookie: cookie.load("MarketingStatsApp",false)
}))
return true;
}).catch(error => {
return false;
})
}
if(cookie.load("MarketingStatsApp",false)){
doubleCheckGUID(cookie.load("MarketingStatsApp",false))
}
const AppRouter = () =>
<Router history={browserHistory} >
<Route path="/" component={Layout}>
<IndexRedirect to="/home" />
<Route path="home" component={Home} />
<Route path="login" component={Login}/>
<Route component={Authenticated} >
<Route path="dash" component={Dashboard} />
</Route>
</Route>
</Router>
export default AppRouter