need help making private route with react redux and firebase
the issue is:
when i click on sign in the user is sign it correctly and the state is updated on redux store but the user is not redirected to dashboard but typing "/dashboard" URL works after clicking the button signing in also trying to go "/dashboard" when the user state user null works
App.js
import React, { useEffect } from "react";
import { connect, Provider } from "react-redux";
import store from "./store";
import { setUser, clearUser } from "./actions/userActions";
import {
BrowserRouter as Router,
Route,
Switch,
withRouter,
} from "react-router-dom";
import Login from "./components/auth/Login";
import Register from "./components/auth/Register";
import Home from "./components/pages/Home";
import Dashboard from "./components/pages/Dashboard";
import firebase from "./firebase";
import PrivateRoute from "./components/PrivateRoute";
function App({ setUser, clearUser, currentUser, history, isLoading }) {
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
clearUser();
}
});
return () => {
unsubscribe();
clearUser();
};
}, [clearUser, history, setUser]);
return (
<Switch>
<Route path="/" component={Home} exact />
<PrivateRoute
path="/dashboard"
component={Dashboard}
currentUser={currentUser}
isLoading={isLoading}
/>
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
</Switch>
);
}
const mapStateToProps = (state) => ({
isLoading: state.user.isLoading,
currentUser: state.user.currentUser,
});
const AppWithRouter = withRouter(
connect(mapStateToProps, { setUser, clearUser })(App)
);
const AppWithAuth = () => (
<Provider store={store}>
<Router>
<AppWithRouter />
</Router>
</Provider>
);
export default AppWithAuth;
PrivateRoute.js
import React from "react";
import { Redirect, Route } from "react-router-dom";
const PrivateRoute = ({
component: Component,
currentUser,
isLoading,
...rest
}) => {
return isLoading ? (
<h1>Spinner</h1>
) : (
<Route
{...rest}
render={(props) =>
currentUser ? <Component {...props} /> : <Redirect to="login" />
}
/>
);
};
export default PrivateRoute;
Login.js
import React, { useState } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
//firebase
import firebase from "../../firebase";
import {
Grid,
Box,
TextField,
CssBaseline,
Avatar,
Container,
Typography,
LinearProgress,
Button,
makeStyles,
} from "#material-ui/core";
const Login = (props) => {
const [form, setForm] = useState({
email: "",
password: "",
});
const [errors, setErrors] = useState([]);
const [loading, setLoading] = useState(false);
const handleChange = (event) =>
setForm({ ...form, [event.target.name]: event.target.value });
const handleSubmit = (e) => {
e.preventDefault();
if (!form.email || !form.password) {
return setErrors(["Fill in all fields"]);
}
setErrors([]);
setLoading(true);
firebase
.auth()
.signInWithEmailAndPassword(form.email, form.password)
.then((signedInUser) => {
setLoading(false);
props.history.push("dashboard");
})
.catch((err) => {
setLoading(false);
setErrors([err.message]);
});
};
const useStyles = makeStyles((theme) => ({
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center",
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main,
},
form: {
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(1),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
}));
const classes = useStyles();
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}></Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<form className={classes.form} noValidate onsubmit={handleSubmit}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
onChange={handleChange}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
onChange={handleChange}
/>
<Box color="red">
{errors.length > 0 && errors.map((err, i) => <span>{err}</span>)}
</Box>
<Grid justify="center"></Grid>
{loading ? <LinearProgress size={14} /> : ""}
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={handleSubmit}
>
Sign in
</Button>
<Grid container>
<Grid item>
<Link to="/register" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</form>
</div>
<Box mt={8}></Box>
</Container>
);
};
const mapStateToProps = (state) => ({
isLoading: state.user.isLoading,
currentUser: state.user.currentUser,
});
export default connect(mapStateToProps)(Login);
Related
I'm trying to make a private route from my login and signup pages to my dashboard page, but all the tutorials and guides that I've stumbled upon all require some sorta AuthContext thing and I didn't implement my authentication procedure using AuthContext.
I've tried different ways but none of them work and just end up giving me a blank page when I get to the dashboard page, what can I do to make it a private route? Using Firebase v9 btw.
SignUp.js
import './Signup.css'
import React, { useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { auth }from '../../../firebase';
import { createUserWithEmailAndPassword, onAuthStateChanged } from 'firebase/auth';
import { Typography, Container, TextField, Button, Alert } from '#mui/material';
const Signup = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [user, setUser] = useState({});
const history = useHistory();
onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
})
const signup = async (e) => {
e.preventDefault();
if (password !== confirmPassword) {
return setError("Passwords do not match")
}
try {
const user = await createUserWithEmailAndPassword(
auth,
email,
password
);
history.push("/dashboard/canvas");
} catch (err) {
setError(err.message);
}
}
return (
<>
<div className="text-div">
<Typography textAlign="center" variant="h3">Create a new account</Typography>
</div>
<Container className="cont" maxWidth="xl" sx={{ backgroundColor: "#ffffff", width: 500, height: "auto", borderRadius: 4, marginTop: 5, display: "flex", flexDirection: "column", padding: 5, }}>
{ error && <Alert severity="error">{error}</Alert> }
<TextField label="Email" margin="dense" type="email" onChange={ (e) => {
setEmail(e.target.value);
}}/>
<TextField label="Password" margin="dense" type="password" onChange={ (e) => {
setPassword(e.target.value);
}}/>
<TextField label="Confirm Password" margin="dense" type="password" onChange={ (e) => {
setConfirmPassword(e.target.value);
}}/>
<Button onClick={signup} variant="contained" sx={{ marginTop: 2, }}>Sign Up</Button>
<div>
Already have an account? <Link to="/login" style={{ color: '#000' }}>Log In</Link>
</div>
</Container>
</>
)
}
export default Signup;
Login.js
import './Login.css'
import React, { useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth }from '../../../firebase';
import { Typography, Container, TextField, Button, Alert } from '#mui/material';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const history = useHistory();
const login = async () => {
try {
const user = await signInWithEmailAndPassword(
auth,
email,
password
);
//alert("Success, user is recognized");
history.push("/dashboard/canvas");
} catch (err) {
setError("The email or password you entered is incorrect");
}
}
return (
<>
<div className="text-div">
<Typography textAlign="center" variant="h3">Login</Typography>
</div>
<Container className="cont" maxWidth="xl" sx={{ backgroundColor: "#ffffff", width: 500, height: "auto", borderRadius: 4, marginTop: 5, display: "flex", flexDirection: "column", padding: 5, }}>
{ error && <Alert severity="error">{error}</Alert> }
<TextField label="Email" margin="dense" type="email" onChange={(e) => {
setEmail(e.target.value);
}}/>
<TextField label="Password" margin="dense" type="password" onChange={(e) => {
setPassword(e.target.value);
}}/>
<Button onClick={login} variant="contained" sx={{ marginTop: 2, }}>Login</Button>
<div>
Don't have an account? <Link to="/signup" style={{ color: '#000' }}>Create one here</Link>
</div>
<div>
<Link to="/request-password-reset" style={{ color: '#000' }}>Forgot your password?</Link>
</div>
</Container>
</>
)
}
export default Login;
firebase.js
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
const firebaseConfig = {
apiKey,
authDomain,
projectId,
storageBucket,
messagingSenderId,
appId,
measurementId
}
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
export {
auth
};
App.js
import './App.css';
import Home from './components/Pages/Home';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Signup from './components/Pages/Signup/Signup';
import Login from './components/Pages/Login/Login';
import UserDashboard from './components/Pages/UserDashboard/UserDashboard';
import ForgotPassword from './components/Pages/Forgot-Password';
function App() {
return (
<Router>
<div>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/signup" component={Signup}/>
<Route path="/login" component={Login}/>
<Route path="/dashboard" component={UserDashboard}/>
<Route path="/request-password-reset" component={ForgotPassword}/>
</Switch>
</div>
</Router>
);
}
export default App;
If you are trying to create a private route component without persisting the authentication state somewhere in your app and exposed out via a React context then you will need to check the auth status asynchronously on each route change. This means you'll also need a "loading" or "pending" state while the auth status check occurring.
Here's an example implementation of just a custom private route sans any persisted state.
import { useEffect, useState } from 'react';
import { Route, Redirect } from 'react-router-dom'; // v4/5
import { onAuthStateChanged } from 'firebase/auth';
import { auth }from '../../../firebase';
const PrivateRoute = props => {
const [pending, setPending] = useState(true);
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
const unsubscribe = onAuthStateChanged(
auth,
user => {
setCurrentUser(user);
setPending(false);
},
error => {
// any error logging, etc...
setPending(false);
}
);
return unsubscribe; // <-- clean up subscription
}, []);
if (pending) return null; // don't do anything yet
return currentUser
? <Route {...props} /> // <-- render route and component
: <Redirect to="/login" />; // <-- redirect to log in
};
react-router-dom#6
Custom route components are out in v6, use a layout route. The PrivateRoute component will replace Route with Outlet for nested routes to render their matched element prop into, and Navigate replaces Redirect.
import { useEffect, useState } from 'react';
import { Outlet, Navigate } from 'react-router-dom'; // v4/5
import { onAuthStateChanged } from 'firebase/auth';
import { auth }from '../../../firebase';
const PrivateRoute = props => {
const [pending, setPending] = useState(true);
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
const unsubscribe = onAuthStateChanged(
auth,
user => {
setCurrentUser(user);
setPending(false);
},
error => {
// any error logging, etc...
setPending(false);
}
);
return unsubscribe; // <-- clean up subscription
}, []);
if (pending) return null; // don't do anything yet
return currentUser
? <Outlet /> // <-- render outlet for routes
: <Navigate to="/login" replace />; // <-- redirect to log in
};
Wrap the routes you want to protect.
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/signup" element={<Signup />} />
<Route path="/login" element={<Login />} />
<Route path="/request-password-reset" element={<ForgotPassword />} />
<Route element={<PrivateRoute />}>
<Route path="/dashboard" element={<UserDashboard />} />
</Route>
</Routes>
</Router>
);
}
I have a React ecommerce site that is currently integrated with Stripe. When a successful payment is submitted, the cart is emptied (managed in localStorage), however the Cart quantity in the Navbar isn't resetting to 0.
The cart quantity is being managed in state in the <App /> component, setQty. The stripe payment is submitted in the <PaymentForm> component which is nested 4 components deep:
<App /> > <Checkout /> > <PaymentForm /> > <CheckoutForm />
In CheckoutForm, I'm using setQty({quantity: 0}); which I thought would pass "0" up to <App /> and reset the Cart quantity, instead I get an error of "Unhandled Rejection (TypeError): setQty is not a function". Why is this? How can I get this to work? Also, is there an easier way of resetting the Cart without passing props through so many components?
A breakdown of each component so you can see how I'm passing setQty through each component:
App
import React, { useState, useEffect } from 'react';
import './App.css';
import Nav from './Nav';
import Shop from './Components/Shop';
import Info from './Components/Info';
import Cart from './Components/Cart';
import Item from './Components/Item';
import Checkout from './Components/CheckoutForm/Checkout';
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
import { getQuantity } from './helpers/helperTools';
function App() {
const storageItems = JSON.parse(localStorage.getItem('product'));
const [qty, setQty] = useState({quantity: getQuantity(storageItems || [])});
console.log("Apppp", qty)
return (
<Router>
<div className="App">
<Nav qty={qty.quantity} />
<Route path="/" exact component={Shop} />
<Route path="/Info" component={Info} />
<Route path="/Cart/" render={(props) => <Cart {...props} setQty={setQty} />} />
<Route path="/Item/:item" component={Item} />
<Route path="/Checkout" component={Checkout} setQty={setQty} />
</div>
</Router>
)
}
export default App;
Checkout
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import {
Paper,
Stepper,
Step,
StepLabel,
Typography,
CircularProgress,
Divider,
Button,
} from '#material-ui/core';
import useStyles from './styles';
import AddressForm from './AddressForm';
import PaymentForm from './PaymentForm';
const steps = ['Shipping address', 'Payment details'];
function Checkout({setQty}) {
const classes = useStyles();
const [activeStep, setActiveStep] = useState(0);
const [shippingData, setShippingData] = useState({});
const nextStep = () => setActiveStep((prevActiveStep) => prevActiveStep + 1);
const backStep = () => setActiveStep((prevActiveStep) => prevActiveStep - 1);
const next = (data) => {
setShippingData(data);
nextStep();
};
const Form = () =>
activeStep === 0 ? (
<AddressForm next={next} />
) : (
<PaymentForm shippingData={shippingData} backStep={backStep} nextStep={nextStep} setQty={setQty} />
);
const Confirmation = () => <div>Confirmation</div>;
return (
<div>
<div className={classes.toolbar} />
<main className={classes.layout}>
<Paper className={classes.paper}>
<Typography variant='h4' align='center'>
Checkout
</Typography>
<Stepper activeStep={activeStep} className={classes.stepper}>
{steps.map((step) => (
<Step key={step}>
<StepLabel>{step}</StepLabel>
</Step>
))}
</Stepper>
{activeStep === steps.length ? <Confirmation /> : <Form />}
</Paper>
</main>
</div>
);
}
export default Checkout;
PaymentForm AND CheckoutForm, both in the same file
import React, { useState } from 'react';
import { Typography, Button, Divider } from '#material-ui/core';
import {
Elements,
CardElement,
ElementsConsumer,
useStripe,
useElements,
} from '#stripe/react-stripe-js';
import { loadStripe } from '#stripe/stripe-js';
import axios from 'axios';
import { getTotal } from '../../helpers/helperTools';
import Review from './Review';
const stripePromise = loadStripe(
'pk_HIDDEN_FOR_NOW'
);
const CheckoutForm = ({ shippingData, backStep, nextStep, setQty }) => {
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) {
return;
}
const storageItems = JSON.parse(localStorage.getItem('product'));
const products = storageItems || [];
const totalPrice = getTotal(products);
let productTitle = '';
products.map((item, index) => {
productTitle = `${productTitle} | ${item.title}`;
});
const cardElement = elements.getElement(CardElement);
const { error, source } = await stripe.createSource(cardElement);
console.log(error, source);
const order = await axios.post('http://localhost:7000/api/stripe/charge', {
amount: totalPrice * 100,
source: source.id,
receipt_email: shippingData.email,
title: productTitle,
customerName: `${shippingData.firstName} ${shippingData.lastName}`,
address: {
city: shippingData.City,
country: shippingData.shippingCountry,
line1: shippingData.address1,
postal_code: shippingData.ZIP,
state: shippingData.shippingState,
},
});
if (error) {
console.log('[error]', error);
} else {
console.log('[PaymentMethod]', order);
localStorage.setItem('product', JSON.stringify([]));
nextStep();
setQty({quantity: 0});
}
};
return (
<form onSubmit={handleSubmit}>
<CardElement
options={{
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': {
color: '#aab7c4',
},
},
invalid: {
color: '#9e2146',
},
},
}}
/>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button variant='outlined' onClick={backStep}>
Back
</Button>
<Button type='submit' variant='contained' disabled={!stripe} color='primary'>
Pay
</Button>
</div>
</form>
);
};
function PaymentForm({ shippingData, backStep, nextStep, setQty }) {
return (
<Elements stripe={stripePromise}>
<Review />
<br />
<br />
<CheckoutForm shippingData={shippingData} nextStep={nextStep} backStep={backStep} setQty={setQty} />
</Elements>
);
}
export default PaymentForm;
Screenshot of my file structure
In the App component, you need to pass setQty as below. Props that are mentioned in the Route component would not be transmitted to the component by itself, and we need to use the render function to pass props.
<Route path="/Checkout" render={(props) => <Checkout setQty={setQty} {...props}/>
I transitioned from a previous React app to a new template. Issue is i am quite confused about how redux is setup and how i can implement authentication.
LoginForm
// validation functions
const required = value => (value === null ? 'Required' : undefined);
const email = value =>
value && !/^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value) ? 'Invalid email' : undefined;
const LinkBtn = React.forwardRef(function LinkBtn(props, ref) {
// eslint-disable-line
return <NavLink to={props.to} {...props} innerRef={ref} />; // eslint-disable-line
});
// eslint-disable-next-line
class LoginForm extends React.Component {
// state = {
// showPassword: false,
// };
constructor() {
super();
this.state = {
email: '',
password: '',
errors: {},
showPassword: false,
};
}
handleClickShowPassword = () => {
const { showPassword } = this.state;
this.setState({ showPassword: !showPassword });
};
handleMouseDownPassword = event => {
event.preventDefault();
};
onSubmit = e => {
e.preventDefault();
const userData = {
email: this.state.email,
password: this.state.password,
};
loginUser(userData);
};
render() {
console.log(this.props);
const { classes, handleSubmit, pristine, submitting, deco } = this.props;
const { showPassword } = this.state;
return (
<Fragment>
<Hidden mdUp>
<NavLink to="/" className={classNames(classes.brand, classes.outer)}>
<img src={logo} alt={brand.name} />
{brand.name}
</NavLink>
</Hidden>
<Paper className={classNames(classes.paperWrap, deco && classes.petal)}>
<Hidden smDown>
<div className={classes.topBar}>
<NavLink to="/" className={classes.brand}>
<img src={logo} alt={brand.name} />
{brand.name}
</NavLink>
<Button
size="small"
className={classes.buttonLink}
component={LinkBtn}
to="/register"
>
<Icon className={classes.icon}>arrow_forward</Icon>
Create new account
</Button>
</div>
</Hidden>
<Typography variant="h4" className={classes.title} gutterBottom>
Sign In
</Typography>
<Typography variant="caption" className={classes.subtitle} gutterBottom align="center">
Lorem ipsum dolor sit amet
</Typography>
<section className={classes.socmedLogin}>
<div className={classes.btnArea}>
<Button variant="outlined" size="small" className={classes.redBtn} type="button">
<AllInclusive className={classNames(classes.leftIcon, classes.iconSmall)} />
Socmed 1
</Button>
<Button variant="outlined" size="small" className={classes.blueBtn} type="button">
<Brightness5 className={classNames(classes.leftIcon, classes.iconSmall)} />
Socmed 2
</Button>
<Button variant="outlined" size="small" className={classes.cyanBtn} type="button">
<People className={classNames(classes.leftIcon, classes.iconSmall)} />
Socmed 3
</Button>
</div>
<ContentDivider content="Or sign in with email" />
</section>
<section className={classes.formWrap}>
<form onSubmit={handleSubmit}>
<div>
<FormControl className={classes.formControl}>
<Field
name="email"
component={TextFieldRedux}
placeholder="Your Email"
label="Your Email"
required
validate={[required, email]}
className={classes.field}
/>
</FormControl>
</div>
<div>
<FormControl className={classes.formControl}>
<Field
name="password"
component={TextFieldRedux}
type={showPassword ? 'text' : 'password'}
label="Your Password"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Toggle password visibility"
onClick={this.handleClickShowPassword}
onMouseDown={this.handleMouseDownPassword}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
required
validate={required}
className={classes.field}
/>
</FormControl>
</div>
<div className={classes.optArea}>
<FormControlLabel
className={classes.label}
control={<Field name="checkbox" component={CheckboxRedux} />}
label="Remember"
/>
<Button
size="small"
component={LinkBtn}
to="/reset-password"
className={classes.buttonLink}
>
Forgot Password
</Button>
</div>
<div className={classes.btnArea}>
<Button variant="contained" color="primary" size="large" type="submit">
Continue
<ArrowForward
className={classNames(classes.rightIcon, classes.iconSmall)}
disabled={submitting || pristine}
/>
</Button>
</div>
</form>
</section>
</Paper>
</Fragment>
);
}
}
const mapDispatchToProps = dispatch => ({
init: bindActionCreators(loginUser, dispatch),
loginUser:
});
LoginForm.propTypes = {
classes: PropTypes.object.isRequired,
handleSubmit: PropTypes.func.isRequired,
pristine: PropTypes.bool.isRequired,
submitting: PropTypes.bool.isRequired,
loginUser: PropTypes.func.isRequired,
deco: PropTypes.bool.isRequired,
};
const LoginFormReduxed = reduxForm({
form: 'immutableExample',
enableReinitialize: true,
})(LoginForm);
const reducerLogin = 'login';
const reducerUi = 'ui';
const FormInit = connect(
state => ({
force: state,
initialValues: state.getIn([reducerLogin, 'usersLogin']),
deco: state.getIn([reducerUi, 'decoration']),
}),
mapDispatchToProps,
)(LoginFormReduxed);
export default withStyles(styles)(FormInit);
login.js
import React from 'react';
import { Helmet } from 'react-helmet';
import brand from 'dan-api/dummy/brand';
import PropTypes from 'prop-types';
import { withStyles } from '#material-ui/core/styles';
import { LoginForm } from 'dan-components';
import styles from 'dan-components/Forms/user-jss';
class Login extends React.Component {
state = {
valueForm: [],
};
submitForm(values) {
const { valueForm } = this.state;
setTimeout(() => {
this.setState({ valueForm: values });
console.log(`You submitted:\n\n${valueForm}`);
window.location.href = '/app';
}, 500); // simulate server latency
}
render() {
const title = brand.name + ' - Login';
const description = brand.desc;
const { classes } = this.props;
return (
<div className={classes.root}>
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
</Helmet>
<div className={classes.container}>
<div className={classes.userFormWrap}>
<LoginForm onSubmit={values => this.submitForm(values)} />
</div>
</div>
</div>
);
}
}
Login.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(Login);
My authactions i am trying to add.
import axios from 'axios';
import jwt_decode from 'jwt-decode';
import setAuthToken from '../../utils/setAuthToken';
import { GET_ERRORS, SET_CURRENT_USER, USER_LOADING } from '../constants/authConstants';
// Login - get user token
export const loginUser = userData => dispatch => {
axios
.post('/api/total/users/login', userData)
.then(res => {
// Save to localStorage
// Set token to localStorage
const { token } = res.data;
localStorage.setItem('jwtToken', JSON.stringify(token));
// Set token to Auth header
setAuthToken(token);
// Decode token to get user data
const decoded = jwt_decode(token);
// Set current user
dispatch(setCurrentUser(decoded));
})
.catch(err =>
dispatch({
type: GET_ERRORS,
payload: err.response.data,
}),
);
};
// Set logged in user
export const setCurrentUser = decoded => {
return {
type: SET_CURRENT_USER,
payload: decoded,
};
};
// User loading
export const setUserLoading = () => {
return {
type: USER_LOADING,
};
};
// Log user out
export const logoutUser = history => dispatch => {
// Remove token from local storage
localStorage.removeItem('jwtTokenTeams');
// Remove auth header for future requests
setAuthToken(false);
// Set current user to empty object {} which will set isAuthenticated to false
dispatch(setCurrentUser({}));
history.push('/dashboard');
};
and the authreducer
import { Map, fromJS } from 'immutable';
import { INIT } from '../constants/reduxFormConstants';
import { SET_CURRENT_USER, USER_LOADING } from '../constants/authConstants';
const isEmpty = require('is-empty');
const initialState = {
usersLogin: Map({
isAuthenticated: false,
user: {},
loading: false,
remember: false,
}),
};
const initialImmutableState = fromJS(initialState);
export default function reducer(state = initialImmutableState, action = {}) {
switch (action.type) {
case INIT:
return state;
case SET_CURRENT_USER:
return {
...state,
isAuthenticated: !isEmpty(action.payload),
user: action.payload,
};
case USER_LOADING:
return {
...state,
loading: true,
};
default:
return state;
}
}
I'm having a really hard time understaning how i can make this work together.
adding app.js
/**
* app.js
*
* This is the entry file for the application, only setup and boilerplate
* code.
*/
// Needed for redux-saga es6 generator support
import '#babel/polyfill';
// Import all the third party stuff
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router/immutable';
import FontFaceObserver from 'fontfaceobserver';
import history from 'utils/history';
import 'sanitize.css/sanitize.css';
// Import root app
import App from 'containers/App';
import './styles/layout/base.scss';
// Import Language Provider
import LanguageProvider from 'containers/LanguageProvider';
// Load the favicon and the .htaccess file
import '!file-loader?name=[name].[ext]!../public/favicons/favicon.ico'; // eslint-disable-line
import 'file-loader?name=.htaccess!./.htaccess'; // eslint-disable-line
import configureStore from './redux/configureStore';
// Import i18n messages
import { translationMessages } from './i18n';
// Observe loading of Open Sans (to remove open sans, remove the <link> tag in
// the index.html file and this observer)
const openSansObserver = new FontFaceObserver('Open Sans', {});
// When Open Sans is loaded, add a font-family using Open Sans to the body
openSansObserver.load().then(() => {
document.body.classList.add('fontLoaded');
});
// Create redux store with history
const initialState = {};
const store = configureStore(initialState, history);
const MOUNT_NODE = document.getElementById('app');
const render = messages => {
ReactDOM.render(
<Provider store={store}>
<LanguageProvider messages={messages}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</LanguageProvider>
</Provider>,
MOUNT_NODE,
);
};
if (module.hot) {
// Hot reloadable React components and translation json files
// modules.hot.accept does not accept dynamic dependencies,
// have to be constants at compile-time
module.hot.accept(['./i18n', 'containers/App'], () => {
ReactDOM.unmountComponentAtNode(MOUNT_NODE);
render(translationMessages);
});
}
// Chunked polyfill for browsers without Intl support
if (!window.Intl) {
new Promise(resolve => {
resolve(import('intl'));
})
.then(() =>
Promise.all([import('intl/locale-data/jsonp/en.js'), import('intl/locale-data/jsonp/de.js')]),
) // eslint-disable-line prettier/prettier
.then(() => render(translationMessages))
.catch(err => {
throw err;
});
} else {
render(translationMessages);
}
// Install ServiceWorker and AppCache in the end since
// it's not most important operation and if main code fails,
// we do not want it installed
if (process.env.NODE_ENV === 'production') {
require('offline-plugin/runtime').install(); // eslint-disable-line global-require
}
the app component
import React from 'react';
import jwt_decode from 'jwt-decode';
import { Switch, Route } from 'react-router-dom';
import NotFound from 'containers/Pages/Standalone/NotFoundDedicated';
import store from '../../redux/configureStore';
import { setCurrentUser, logoutUser } from '../../redux/actions/authActions';
import setAuthToken from '../../utils/setAuthToken';
import Auth from './Auth';
import Application from './Application';
import ThemeWrapper, { AppContext } from './ThemeWrapper';
window.__MUI_USE_NEXT_TYPOGRAPHY_VARIANTS__ = true;
class App extends React.Component {
render() {
console.log(this.props);
return (
<ThemeWrapper>
<AppContext.Consumer>
{changeMode => (
<Switch>
<Route path="/" exact component={Auth} />
<Route
path="/app"
render={props => <Application {...props} changeMode={changeMode} />}
/>
<Route component={Auth} />
<Route component={NotFound} />
</Switch>
)}
</AppContext.Consumer>
</ThemeWrapper>
);
}
}
export default App;
the auth component
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import Outer from '../Templates/Outer';
import {
Login,
LoginV2,
LoginV3,
Register,
RegisterV2,
RegisterV3,
ResetPassword,
LockScreen,
ComingSoon,
Maintenance,
NotFound,
} from '../pageListAsync';
class Auth extends React.Component {
render() {
return (
<Outer>
<Switch>
<Route path="/login" component={Login} />
{/* <Route path="/login-v2" component={LoginV2} />
<Route path="/login-v3" component={LoginV3} />
<Route path="/register" component={Register} />
<Route path="/register-v2" component={RegisterV2} />
<Route path="/register-v3" component={RegisterV3} /> */}
<Route path="/reset-password" component={ResetPassword} />
<Route path="/lock-screen" component={LockScreen} />
{/* <Route path="/maintenance" component={Maintenance} />
<Route path="/coming-soon" component={ComingSoon} /> */}
<Route component={NotFound} />
</Switch>
</Outer>
);
}
}
export default Auth;
To maintain authentication using PLain redux is not quite possible because when ever you reload, the store get refreshed . However, redux has a functionality called Persisted store
Persisted Store store the data in memory and will not be refreshed with page reload or anything like that.
You can check this Link
Update with out persisted store:
In that case, Get the IsLoggedin state from store.
In App component
const App = () => {
const isLoggedIn = localStorage.getItem("jwtToken") !== null ? true: false
return (
<Router>
<Switch>
<PrivateRoute isLoggedIn={isLoggedIn} path="/dashboard">
<Dashboard />
</PrivateRoute>
<Route path="/login">
<Login authToken={authToken} />
</Route>
</Switch>
</Router>
);
and then in private route component:
const PrivateRoute = (props) => {
const { children, IsLoggedin, ...rest } = props;
return (
<Route
{...rest}
render={({ location }) =>
IsLoggedin ? (
children
) : (
<Redirect
to={{
pathname: '/login',
state: { from: location },
}}
/>
)
}
/>
);
};
I have a problem on redirect to the previous page since i do a check isLoggedIn. The problem right now is after check isLoggedIn it redirect to the default route. How do i maintain the page where I'm into?
What i did right now is using the referer but it is undefined. Pls help me find another way.
Pls check my code below:
Login.js
const Form = (props) => {
const classes = useStyles();
const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
const referer = props.referer;
const history = useHistory();
console.log(history);
const { values, touched, errors, isSubmitting, handleChange, handleBlur, handleSubmit } = props;
if (isLoggedIn) {
return <Redirect to={referer} />;
}
return (
<div className={classes.root}>
<Grid container direction="row" justify="center">
<Grid item lg={4} md={5} xs={10}>
<Card>
<form onSubmit={handleSubmit}>
<CardHeader
title="LOGIN"
classes={{
title: classes.cardHeader,
}}
className={classes.cardHeader}
/>
<CardContent className={classes.textFieldSection}>
<TextField
fullWidth
label="Username"
name="username"
type="text"
variant="outlined"
value={values.username}
onChange={handleChange}
onBlur={handleBlur}
helperText={touched.username ? errors.username : ''}
error={touched.username && Boolean(errors.username)}
InputProps={{
endAdornment: (
<InputAdornment>
<AccountCircle />
</InputAdornment>
),
}}
/>
<TextField
fullWidth
label="Password"
name="password"
style={{ marginTop: '1rem' }}
type="password"
variant="outlined"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
helperText={touched.password ? errors.password : ''}
error={touched.password && Boolean(errors.password)}
InputProps={{
endAdornment: (
<InputAdornment>
<LockIcon />
</InputAdornment>
),
}}
/>
</CardContent>
<CardActions className={classes.loginButtonSection}>
<Button
type="submit"
color="primary"
variant="contained"
className={classes.loginButton}
disabled={isSubmitting}
>
Log In
</Button>
</CardActions>
</form>
</Card>
</Grid>
</Grid>
</div>
);
};
let yup = require('yup');
export const Login = (props) => {
const dispatch = useDispatch();
const MyFormWithFormik = withFormik({
mapPropsToValues: ({ username, password }) => {
return {
username: username || '',
password: password || '',
};
},
validationSchema: yup.object().shape({
username: yup.string().required('Enter your username'),
password: yup.string().required('Enter your password'),
}),
handleSubmit: (values, { setSubmitting }) => {
dispatch(login(values.username, values.password));
setSubmitting(false);
},
})(Form);
return <MyFormWithFormik />;
};
Form.propTypes = {
className: PropTypes.string,
};
export default Login;
PrivateRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import axios from 'axios';
import { useSelector } from 'react-redux';
function PrivateRoute({ component: Component, ...rest }) {
const authTokens = useSelector((state) => state.auth.access_token);
function checkTokenExpiry() {
if (authTokens) {
axios.interceptors.request.use(
function (config) {
const token = `Bearer ${authTokens}`;
config.headers.Authorization = token;
console.log(config);
return config;
},
(error) => {
console.log(error);
Promise.reject(error);
}
);
}
}
if (authTokens != null) {
checkTokenExpiry();
}
return (
<Route
{...rest}
render={(props) =>
authTokens ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { referer: props.location } }} />
)
}
/>
);
}
export default PrivateRoute;
PrivateAdminOnlyRoute
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import axios from 'axios';
import { useSelector } from 'react-redux';
function PrivateAdminOnlyRoute({ component: Component, ...rest }) {
const authTokens = useSelector((state) => state.auth.access_token);
const isAdmin = useSelector((state) => state.auth.is_admin);
function checkTokenExpiry() {
if (authTokens) {
axios.interceptors.request.use(
function (config) {
const token = `Bearer ${authTokens}`;
config.headers.Authorization = token;
console.log(config);
return config;
},
(error) => {
console.log(error);
Promise.reject(error);
}
);
}
}
if (authTokens != null) {
checkTokenExpiry();
}
return (
<Route
{...rest}
render={(props) =>
authTokens && isAdmin === true ? (
<Component {...props} />
) : authTokens && (isAdmin === false || undefined || null) ? (
<Redirect to={{ pathname: '/pending', state: { referer: props.location } }} />
) : (
<Redirect to={{ pathname: '/login', state: { referer: props.location } }} />
)
}
/>
);
}
export default PrivateAdminOnlyRoute;
Routes.js
import React, { useState } from 'react';
import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom';
import PrivateRoute from './PrivateRoute';
import Login from './pages/Login/Login';
import Signup from './pages/Signup/Signup';
import Common from './pages/Common';
function Routes() {
return (
<Router>
<Switch>
<Route path="/login" component={Login} />
<Route path="/signup" component={Signup} />
<PrivateRoute path="/" component={Common} />
</Switch>
</Router>
);
}
export default Routes;
Using state inside Redirect isn't assigan state to props but to location
import { useLocation } from 'react-router-dom';
const location = useLocation();
const referer = location.state && location.state.referer
another thing about your code (that not connect directly to your issue) is
(isAdmin === false || undefined || null)
isAdmin compared just to false, and in case isAdmin isn't false it is not being compared to undefined, but undefined stand by itself (with falsy behavior...), and so goes for null
hope it's helpful :)
I'm getting started with ReactJS and I wanted to restrict the sign-in page for not authorized users only and I want to redirect those users to the page they were before after they log in (that part is not working as well).
The problem is that when I log in and then go back to /sign-in page, it says that I'm still not authorized, even tho I can literally see the access token in local storage (look at the picture below). However, if I refresh the page, NotAuthorizedRoute works and it redirects me back to home page.
How can I fix this in React? How can I also redirect back to the previously opened page and not / only?
In other words, const isLoggedIn = localStorage.getItem('access_token') !== null; is being activated only after a page refresh. In angular, I used to fix such things by #Input/#Output and EventEmitter but I'm new to React and I don't know how to deal with it yet.
Image:
Login.js
import React, { Fragment, useState } from 'react';
import { useHistory } from 'react-router-dom';
import UserService from '../../Services/UserService';
import {
makeStyles,
Typography,
Container,
TextField,
FormControlLabel,
Checkbox,
Button,
Grid,
CircularProgress
} from '#material-ui/core';
const useStyles = makeStyles(theme => ({
content: {
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(8, 0, 6)
},
form: {
marginTop: theme.spacing(1)
},
submit: {
margin: theme.spacing(3, 0, 2)
}
}));
const Login = () => {
const classes = useStyles();
const history = useHistory();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
const user = {
username,
password,
};
setLoading(true);
UserService.loginUser(user)
.then(res => {
localStorage.setItem('access_token', res.token);
setLoading(false);
history.push('/');
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}
return (
<Fragment>
<div className={classes.content}>
<Container maxWidth="sm">
<Typography component="h1" variant="h2" align="center" color="textPrimary" gutterBottom>
Sign In
</Typography>
<Typography variant="h5" align="center" color="textSecondary" paragraph>
If you are not registered, you should sign up.
</Typography>
</Container>
</div>
<Container maxWidth="md">
<Grid container justify="center" spacing={3}>
<Grid item xs={6}>
{!UserService.isLoggedIn ?
<form className={classes.form} noValidate onSubmit={handleSubmit}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
type="text"
label="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
error={error !== ''}
helperText={error}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
type="password"
label="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<FormControlLabel
control={<Checkbox value="remember" color="primary" />}
label="Remember me"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
disabled={loading}
>
Sign In
</Button>
</form>
:
<Grid container justify="center">
Logged in.
</Grid>
}
{loading &&
<Grid container justify="center">
<CircularProgress className={classes.spinner} />
</Grid>
}
</Grid>
</Grid>
</Container>
</Fragment>
);
}
export default Login;
App.js
import React, { Fragment } from 'react';
import './App.css';
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import NotAuthorizedRoute from './Helpers/NotAuthorizedRoute';
import Navbar from './Components/Navbar/Navbar';
import Home from './Components/Home/Home';
import User from './Components/User/User';
import Login from './Components/Login/Login';
function App() {
return (
<Fragment>
<Router>
<Navbar />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/users" component={User} />
<NotAuthorizedRoute path="/sign-in" component={Login} />
<Redirect from="*" to="/" />
</Switch>
</Router>
</Fragment>
);
}
export default App;
Helpers/NotAuthorizedRoute.js
import React from 'react';
import UserService from '../Services/UserService';
import { Redirect, Route } from 'react-router-dom';
const NotAuthorizedRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={props =>
!UserService.isLoggedIn ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
)
}
export default NotAuthorizedRoute;
Service/UserService.js
const isLoggedIn = localStorage.getItem('access_token') !== null;
const loginUser = async (user) => {
const { username, password } = user;
if (username === 'qwe' && password === '123') {
return { token: 'access_token' };
} else {
throw new Error('Wrong username or password');
}
}
export default { isLoggedIn, loginUser };
EDIT: If I put !localStorage.getItem('access_token') instead of !UserService.isLoggedIn in NotAuthorizedRoute, it works. Why?
In React, the component only re-renders when props or state changes. In your NotAuthorizedRoute you're directly using isLoggedIn param from file. Instead, you should pass isLoggedIn as prop from parent component. So you can re-write NotAuthorizedRoute.js as:
import React from 'react';
import UserService from '../Services/UserService';
import { Redirect, Route } from 'react-router-dom';
const NotAuthorizedRoute = ({ component: Component, isLoggedIn=false, ...rest }) => {
return (
<Route
{...rest}
render={props =>
!UserService.isLoggedIn ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
)
}
export default NotAuthorizedRoute;
and call this component as
<NotAuthorizedRoute ... isLoggedIn={UserService.isLoggedIn} />