trouble making firebase authcheck with express/mongodb server from react - reactjs

I am working on a project that requires firebase authorization with a custom server. I am using mongodb with express and mongoose. Client side is react. The trouble i am having can be seen using the login page as an example.
import React, { useState, useEffect } from 'react'
import {NavLink} from 'react-router-dom'
import axios from 'axios'
import * as Yup from 'yup';
import { Formik, Form, Field } from 'formik';
import {
CssBaseline,
Box,
Divider,
Container,
Card,
CardContent,
InputAdornment,
Icon,
IconButton,
Avatar,
FormHelperText,
Button,
TextField,
Link,
Grid,
Typography,
} from '#material-ui/core';
import Visibility from '#material-ui/icons/Visibility'
import VisibilityOff from '#material-ui/icons/VisibilityOff'
import { auth, googleAuthProvider } from '../../../firebase'
import { useSnackbar } from 'notistack'
import LockOutlinedIcon from '#material-ui/icons/LockOutlined';
import useStyles from './login-styles'
import googleIcon from '../../../assets/google.svg'
import useMediaQuery from '#material-ui/core/useMediaQuery';
import { useTheme } from '#material-ui/core/styles';
import { useDispatch, useSelector } from 'react-redux'
import useIsMountedRef from '../../../hooks/useIsMountedRef'
import AuthPage from '../AuthPage'
const initialValues = {
email: '',
password: ''
}
const validationSchema = Yup.object({
email: Yup.string().email().max(255).required(),
password: Yup.string().min(7).max(255).required('Password is required')
})
const createOrUpdateUser = async (authtoken) => {
return await axios.post(`${process.env.REACT_APP_API}/create-or-update-user`, {}, {
headers: {
authtoken
}
})
}
const Login = ({history}) => {
let dispatch = useDispatch()
const theme = useTheme();
const {user} = useSelector((state) => ({...state}))
const {enqueueSnackbar} = useSnackbar()
const classes = useStyles();
const isMountedRef = useIsMountedRef();
const isMobile = useMediaQuery(theme.breakpoints.down('xs'));
const [passwordVisible, setPasswordVisible] = useState(false)
const togglePasswordVisibility = () => {
setPasswordVisible({ passwordVisible : !passwordVisible})
}
useEffect(() => {
if (user && user.token) {
history.push('/client-dash')
}
}, [user])
const handleGoogleClick = async () => {
auth.signInWithPopup(googleAuthProvider)
.then(async(result) => {
const { user } = result
const idTokenResult = await user.getIdTokenResult();
createOrUpdateUser(idTokenResult.token)
.then(res => console.log(' CREATE OR UPDATE RESPONSE', res))
.catch()
//send snackbar success email sent
enqueueSnackbar(`Welcome back, ${user.displayName}!`, {
variant: 'success'
})
// dispatch({
// type: 'LOGGED_IN_USER',
// payload: {
// email: user.email,
// token: idTokenResult,
// }
// });
// history.push('/client-dash')
})
.catch((err) => {
console.log(err)
enqueueSnackbar((err.message), {
variant: 'error'
});
})
}
return (
<AuthPage>
<CardContent className="flex flex-col items-center justify-center w-full py-96 max-w-320">
<div className={classes.paper}>
<Avatar className={classes.avatar}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<Button
className={classes.googleButton}
fullWidth
onClick={handleGoogleClick}
size="large"
variant="contained"
>
<img
alt="Google"
className={classes.providerIcon}
src={googleIcon}
/>
Sign in with Google
</Button>
<Box
alignItems="center"
display="flex"
mt={2}
>
<Divider
style={{color: '#161616'}}
className={classes.divider}
orientation="horizontal"
/>
<Typography
style={{color: '#161616'}}
variant="body1"
className={classes.dividerText}
>
OR
</Typography>
<Divider
orientation="horizontal"
/>
</Box>
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={async(values, {
setErrors,
setStatus,
setSubmitting
}) => {
try {
const result = await auth.signInWithEmailAndPassword(values.email, values.password)
console.log(result);
const { user } = result
const idTokenResult = await user.getIdTokenResult()
createOrUpdateUser(idTokenResult.token)
.then(res => console.log(' CREATE OR UPDATE RESPONSE', res))
.catch()
//send snackbar success email sent
enqueueSnackbar(`Welcome back, ${user.displayName}!`, {
variant: 'success'
})
dispatch({
type: 'LOGGED_IN_USER',
payload: {
email: user.email,
token: idTokenResult,
}
});
if (isMountedRef.current) {
setStatus({ success: true });
setSubmitting(false);
history.push('/client-dash')
}
} catch (err) {
console.error(err);
enqueueSnackbar((err.message), {
variant: 'error'
});
if (isMountedRef.current) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}
}}>
{({ values,
errors,
handleBlur,
handleChange,
isSubmitting,
touched }) => (
<Form className={classes.form}>
<Field
name="email"
as={TextField}
error={Boolean(touched.email && errors.email)}
fullWidth
helperText={touched.email && errors.email}
label="Email Address"
margin="normal"
name="email"
onBlur={handleBlur}
onChange={handleChange}
type="email"
value={values.email}
variant='outlined'
autoFocus
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Icon className="text-20" color="action">
mail
</Icon>
</InputAdornment>
)
}}
/>
<Field
as={TextField}
error={Boolean(touched.password && errors.password)}
fullWidth
helperText={touched.password && errors.password}
label="Password"
margin="normal"
name="password"
onBlur={handleBlur}
onChange={handleChange}
type={passwordVisible ? 'text' : 'password'}
value={values.password}
variant="outlined"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
className="focus:outline-none"
aria-label="toggle password visible"
onClick={togglePasswordVisibility}
edge="end">
{passwordVisible ? <Visibility/> : <VisibilityOff/>}
</IconButton>
</InputAdornment>
)
}}
/>
{errors.submit && (
<Box mt={3}>
<FormHelperText error>
{errors.submit}
</FormHelperText>
</Box>
)}
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
disabled={(Object.keys(touched).length === 0 && touched.constructor === Object) || isSubmitting}
className={classes.submit}
>
Sign In
</Button>
<Grid container>
<Grid item xs>
<Link component={NavLink} to="/forgot-password" variant="body2">
Forgot password?
</Link>
</Grid>
<Grid item>
<Link component={NavLink} to="/create-account" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</Form>
)}
</Formik>
</div>
</CardContent>
</AuthPage>
)
}
export default Login
the firebase function in handled in the onSubmit fuction. It sends the token to endpoint in the server.
const express = require('express')
const router = express.Router()
//import middleware
const {authCheck} = require("../middleware/auth")
//import controllers
const {createOrUpdateUser} = require("../controllers/auth")
//route
router.post('/create-or-update-user', authCheck, createOrUpdateUser)
module.exports = router;
i am using the authcheck middleware to verify the token.
const admin = require('../firebase');
exports.authCheck = async (req, res, next) => {
console.log(req.headers.authtoken); //token
try {
const firebaseUser = await admin.auth().verifyIdToken(req.headers.authtoken);
console.log('FIREBASE USER IN AUTHCHECK', firebaseUser)
req.user = firebaseUser;
next();
} catch (err) {
console.log(err)
// res.status(401).json({
// err: "Invalid or expired token",
// });
}
};
however this returns a 401 token expired. i know the token is being sent because it is logged in the console. Im at a loss. i dont see any other way to write the code.

I found the answer here on stack overflow. A simple time sync on the machine i am working on did the trick.
Error: Firebase ID token has expired

Related

how to upload an array of images to cloudinary using axios and react hook form using next js

here in this code I am able to upload only a single image and upload it to cloudinary api and it works fine, the PROBLEM is it keeps only sending only a single image even though i've specified multiple in the input tag and it loops after it has the event target files and append them to the formData, while what I want is to upload as many images as I want and I have the mongodb model image field as an array. so backend is very solid and doesn't have a problem. the issue is on the front end not being able to send an array of images. it only sends one. i have removed the react hook form validation just incase to see if it works
import axios from 'axios';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect, useReducer, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { toast } from 'react-toastify';
import { getError } from '../../utils/error';
import { tokens } from '../../utils/theme';
import {
Grid,
Box,
List,
ListItem,
Typography,
Card,
Button,
ListItemText,
TextField,
useTheme,
CircularProgress,
FormControlLabel,
Checkbox,
} from '#mui/material';
import Header from '../../components/Header';
import Topbar from '../../components/global/Topbar';
function reducer(state, action) {
switch (action.type) {
case 'FETCH_REQUEST':
return { ...state, loading: true, error: '' };
case 'FETCH_SUCCESS':
return { ...state, loading: false, error: '' };
case 'FETCH_FAIL':
return { ...state, loading: false, error: action.payload };
case 'UPDATE_REQUEST':
return { ...state, loadingUpdate: true, errorUpdate: '' };
case 'UPDATE_SUCCESS':
return { ...state, loadingUpdate: false, errorUpdate: '' };
case 'UPDATE_FAIL':
return { ...state, loadingUpdate: false, errorUpdate: action.payload };
case 'UPLOAD_REQUEST':
return { ...state, loadingUpload: true, errorUpload: '' };
case 'UPLOAD_SUCCESS':
return {
...state,
loadingUpload: false,
errorUpload: '',
};
case 'UPLOAD_FAIL':
return { ...state, loadingUpload: false, errorUpload: action.payload };
default:
return state;
}
}
export default function AdminProductEditScreen() {
const theme = useTheme();
const colors = tokens(theme.palette.mode);
const [isFeatured, setIsFeatured] = useState(false);
const [inStock, setInStock] = useState(false);
const [imports, setImports] = useState(false);
const [exports, setExports] = useState(false);
const [image, setImage] = useState([]);
const [category, setCategory] = useState([]);
const { query } = useRouter();
const productId = query.id;
const [
{ loading, error, loadingUpdate, loadingUpload }
dispatch,
] = useReducer(reducer, {
loading: true,
error: '',
});
const {
register,
handleSubmit,
control,
formState: { errors },
setValue,
} = useForm();
useEffect(() => {
const fetchData = async () => {
try {
dispatch({ type: 'FETCH_REQUEST' });
const { data } = await axios.get(`/api/admin/products/${productId}`);
dispatch({ type: 'FETCH_SUCCESS' });
setValue('name', data.name);
setValue('slug', data.slug);
setValue('headerTitle', data.headerTitle);
setImports(data.imports);
setExports(data.exports);
setValue('image', data.image);
} catch (err) {
dispatch({ type: 'FETCH_FAIL', payload: getError(err) });
}
};
fetchData();
}, [productId, setValue]);
const router = useRouter();
const uploadHandler = async (e, imageField = 'image') => {
const url = `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/upload`;
try {
dispatch({ type: 'UPLOAD_REQUEST' });
const {
data: { signature, timestamp },
} = await axios('/api/admin/cloudinary-sign');
const files = Array.from(e.target.files);
const formData = new FormData();
files.forEach((file) => {
formData.append('file', file);
});
formData.append('signature', signature);
formData.append('timestamp', timestamp);
formData.append('api_key', process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY);
const { data } = await axios.post(url, formData);
dispatch({ type: 'UPLOAD_SUCCESS' });
setValue(imageField, data.secure_url);
toast.success('File uploaded successfully');
} catch (err) {
dispatch({ type: 'UPLOAD_FAIL', payload: getError(err) });
toast.error(getError(err));
}
};
const updateCategory = (e) => {
const cats = e.target.files.split(',');
cats?.map((cat) => {
console.log(cat);
setCategory(cat);
});
};
const submitHandler = async ({
name,
slug,
headerTitle,
category,
price,
image,
}) => {
try {
dispatch({ type: 'UPDATE_REQUEST' });
await axios.put(`/api/admin/products/${productId}`, {
name,
slug,
headerTitle,
imports,
exports,
category,
image,
});
dispatch({ type: 'UPDATE_SUCCESS' });
toast.success('Product updated successfully');
router.push('/products');
} catch (err) {
dispatch({ type: 'UPDATE_FAIL', payload: getError(err) });
toast.error(getError(err));
}
};
return (
<Box title={`Edit Product ${productId}`} m="20px">
<Box display="flex" justifyContent="space-between">
<Header title="Product Edit" subtitle="List of Product to be edited" />
<Topbar />
</Box>
{loading && <CircularProgress></CircularProgress> ? (
<Box>Loading...</Box>
) : error ? (
<Box color="error">{error}</Box>
) : (
<ListItem width="100%">
<form onSubmit={handleSubmit(submitHandler)}>
<List sx={{ width: '60vw', border: '2px solid red' }}>
<ListItem>
<Controller
name="name"
control={control}
defaultValue=""
render={({ field }) => (
<TextField
variant="outlined"
fullWidth
id="name"
label="Name"
error={Boolean(errors.name)}
helperText={errors.name ? 'Name is required' : ''}
{...field}
></TextField>
)}
></Controller>
</ListItem>
<ListItem>
<Controller
name="slug"
control={control}
defaultValue=""
render={({ field }) => (
<TextField
variant="outlined"
fullWidth
id="slug"
label="Slug"
error={Boolean(errors.slug)}
helperText={errors.slug ? 'Slug is required' : ''}
{...field}
></TextField>
)}
></Controller>
</ListItem>
<ListItem>
<Controller
name="headerTitle"
control={control}
defaultValue=""
render={({ field }) => (
<TextField
variant="outlined"
fullWidth
id="headerTitle"
label="header Title"
error={Boolean(errors.headerTitle)}
helperText={
errors.headerTitle ? 'headertitle is required' : ''
}
{...field}
></TextField>
)}
></Controller>
</ListItem>
<ListItem>
<FormControlLabel
label="Import"
control={
<Checkbox
onClick={(e) => setImports(e.target.checked)}
checked={imports}
name="imports"
/>
}
></FormControlLabel>
</ListItem>
<ListItem>
<FormControlLabel
label="Export"
control={
<Checkbox
onClick={(e) => setExports(e.target.checked)}
checked={exports}
name="exports"
/>
}
></FormControlLabel>
</ListItem>
<ListItem>
<Controller
name="price"
control={control}
defaultValue=""
render={({ field }) => (
<TextField
variant="outlined"
fullWidth
id="price"
label="Price"
{...field}
></TextField>
)}
></Controller>
</ListItem>
<ListItem>
<Controller
name="image"
control={control}
defaultValue=""
rules={{
required: true,
}}
render={({ field }) => (
<TextField
variant="outlined"
fullWidth
id="image"
label="Image"
{...field}
></TextField>
)}
></Controller>
</ListItem>
<ListItem>
<Button variant="contained" component="label">
Upload File
<input
type="file"
onChange={(e) => uploadHandler(e)}
hidden
multiple
/>
</Button>
{loadingUpload && <CircularProgress className="mx-4" />}
</ListItem>
{/** BUTTON */}
<ListItem>
<Button
variant="contained"
type="submit"
fullWidth
color="primary"
>
Update
</Button>
{loadingUpdate && <CircularProgress />}
</ListItem>
</List>
</form>
</ListItem>
)}
</Box>
);
}
AdminProductEditScreen.auth = { adminOnly: true };
i have tried in the uploadHandler function to loop through the e.target.files but only keeps sending a single image to the cloudinary api and its uploaded there and i can see it, even though i have selected multiple image files
It looks like you're trying to send a single request to Cloudinary's API, where that single request has multiple values for file.
That's not supported by the Cloudinary API - each call to the API should specify a single file: https://cloudinary.com/documentation/image_upload_api_reference#upload_required_parameters
You should change your code so that when you loop through the array of files, you make a separate call to the API for each one, then continue after all files are uploaded.

Material UI with Login Template with React.js

I'm brand new to Material UI. I am currently trying to implement it into my project. Unfortunately, it's not cooperating. I think I am putting in everything right, but because I have never used MUI before, I am not sure. Can someone please help me figure out where the error is? I have tried multiple times, putting in the code I had before I tried to implement MUI. Thank you!
I will include the code before I tried to implement MUI, and after. The first is before MUI implementation. The last block of code is my Auth.jsx file.
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
export default function Login({ updateToken, toggleView }) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleUsername = (e) => setUsername(e.target.value);
const handlePassword = (e) => setPassword(e.target.value);
const history = useHistory();
const handleSubmit = async (e) => {
e.preventDefault();
try {
const fetchResults = await fetch(`http://localhost:3001/user/`, {
method: "POST",
body: JSON.stringify({ username, password }),
headers: new Headers({
"Content-Type": "application/json",
}),
});
const json = await fetchResults.json();
if (!json.user || !json.sessionToken) {
alert(json.message);
return;
}
updateToken(json.sessionToken);
history.push("/home");
} catch (err) {
console.error(err);
}
};
return (
<div>
<div className='signin'>
<form onSubmit={handleSubmit}>
<input
type='username'
onChange={handleUsername}
name='username'
placeholder='Username'
required
value={username}
className='signin__input'
/>
<input
type='password'
onChange={handlePassword}
name='password'
placeholder='Password'
type='password'
required
value={password}
className='signin__input'
/>
<button className='login__button' type='submit'>
Login
</button>
</form>{" "}
<p className='signin__toggle' onClick={() => history.push("./signup")}>
Don't have an account? Sign up here.{" "}
</p>
</div>
</div>
);
}
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import Avatar from "#mui/material/Avatar";
import Button from "#mui/material/Button";
import CssBaseline from "#mui/material/CssBaseline";
import TextField from "#mui/material/TextField";
import FormControlLabel from "#mui/material/FormControlLabel";
import Checkbox from "#mui/material/Checkbox";
import Link from "#mui/material/Link";
import Grid from "#mui/material/Grid";
import Box from "#mui/material/Box";
import LockOutlinedIcon from "#mui/icons-material/LockOutlined";
import Typography from "#mui/material/Typography";
import Container from "#mui/material/Container";
import { createTheme, ThemeProvider } from "#mui/material/styles";
function Copyright(props) {
return (
<Typography
variant='body2'
color='text.secondary'
align='center'
{...props}
>
{"Copyright © "}
<Link color='inherit' href='https://courtney-downs.web.app/'>
Courtney Downs Portfolio
</Link>{" "}
{new Date().getFullYear()}
{"."}
</Typography>
);
}
const theme = createTheme();
export default function Login2({ updateToken, toggleView }) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleUsername = (e) => setUsername(e.target.value);
const handlePassword = (e) => setPassword(e.target.value);
const history = useHistory();
const handleSubmit = async (e) => {
e.preventDefault();
try {
const fetchResults = await fetch(`http://localhost:3001/user/`, {
method: "POST",
body: JSON.stringify({ username, password }),
headers: new Headers({
"Content-Type": "application/json",
}),
});
const json = await fetchResults.json();
if (!json.user || !json.sessionToken) {
alert(json.message);
return;
}
updateToken(json.sessionToken);
history.push("/home");
} catch (err) {
console.error(err);
}
};
return (
<ThemeProvider theme={theme}>
<Container component='main' maxWidth='xs'>
<CssBaseline />
<Box
sx={{
marginTop: 8,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Avatar sx={{ m: 1, bgcolor: "secondary.main" }}>
<LockOutlinedIcon />
</Avatar>
<Typography component='h1' variant='h5'>
Sign in
</Typography>
<Box
component='form'
onSubmit={handleSubmit}
noValidate
sx={{ mt: 1 }}
>
<TextField
margin='normal'
required
fullWidth
id='username'
label='Username'
name='username'
autoComplete='username'
value={username}
onChange={handleUsername}
autoFocus
/>
<TextField
margin='normal'
required
fullWidth
name='password'
label='Password'
type='password'
id='password'
value={password}
onChange={handlePassword}
autoComplete='current-password'
/>
<FormControlLabel
control={<Checkbox value='remember' color='primary' />}
label='Remember me'
/>
<Button
type='submit'
fullWidth
variant='contained'
sx={{ mt: 3, mb: 2 }}
>
Sign In
</Button>
<Grid container>
<Grid item xs>
<Link href='#' variant='body2'>
Forgot password?
</Link>
</Grid>
<Grid item>
<Link onClick={toggleView} variant='body2'>
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</Box>
</Box>
<Copyright sx={{ mt: 8, mb: 4 }} />
</Container>
</ThemeProvider>
);
}
import React, { useState } from "react";
import Login2 from "./Login2";
import Signup from "./Signup";
export default function Auth({ sessionToken, updateToken }) {
const [loginShowing, setLoginShowing] = useState(true);
const toggleView = () => setLoginShowing(!loginShowing);
return (
<div>
{loginShowing ? (
<Login2
token={sessionToken}
updateToken={updateToken}
toggleView={toggleView}
/>
) : (
<Signup
updateToken={updateToken}
token={sessionToken}
toggleView={toggleView}
/>
)}
</div>
);
}

How to pop up a material-ui snackbar alert after a "bad login"

I want a material-ui snackbar alert to pop up when someone send a wrong username or password, and the main issue is that I have 0 experience with react and material-ui. This was a preconfigured exercise to handle the login page that got stuck after a 401 error.I "solved it" with a simple try and cath on the AuthService.js like this:
const login = async (usernameOrEmail, password) => {
let response;
try {
response = await api.post('/auth/signin', {usernameOrEmail, password});
} catch (error) {
response = error;
alert("Bad credentials, please try again");
}
return response;
}
Now what I want is to use a material-ui snackbar instead of an alert, this is how the Auth.page.js looks like:
...all the imports
const AuthPage = ({history}) => {
const { auth, dispatch } = useContext(AuthContext);
const classes = styles();
if (auth.isAuthenticated) {
history.push("/");
}
const submitLoginForm = async ({email, password}, actions) => {
const loginResponse = await authService.login(email, password);
if (!_.isEmpty(loginResponse.data)) {
const {accessToken, tokenType} = loginResponse.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('tokenType', tokenType);
const userResponse = await authService.getCurrentUser();
if (!_.isEmpty(userResponse.data)) {
const user = userResponse.data;
localStorage.setItem('user', JSON.stringify(user));
dispatch({type: 'login'});
}
}
history.push('/');
};
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<Formik
onSubmit={submitLoginForm}
validationSchema={validationSchema}
render={(
values,
errors,
setSubmitting,
setValues,
isSubmitting,
) => (
<Form className={classes.form}>
<Field
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
component={TextField}
/>
<Field
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
component={TextField}
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Sign In
</Button>
</Form>
)}
>
</Formik>
</div>
</Container>
);
};
AuthPage.propTypes = {
match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
export default withRouter(AuthPage);
So finally to the main question where and how do I set up the material-ui snackbar to pop up when I catch the error on my try and catch or if I need a different approach to accomplish it.
https://codesandbox.io/s/gracious-sky-fem1x?fontsize=14
Just put below line when you want to show snackbar alert
setStatusBase({ msg: "Success", key: Math.random() });

how to remove error messages asynchronously in React

I'm fetching an array of errors, one of them says "Username is taken" and the other one says "Password must be at least 5 chars".
It's displayed like this in React.
{this.props.auth.errors ? (
this.props.auth.errors.map( (err, i) => (
<div key={i} style={{color: 'red'}}>
{err}
</div>
))
):(
null
)}
this.props.auth.errors is an array containing the error messages, after registerUser gets called.
Actions.js
export const registerUser = (userData) => dispatch => {
Axios
.post('/users/register', userData)
.then( res => {
const token = res.data.token;
// console.log(token);
// pass the token in session
sessionStorage.setItem("jwtToken", token);
// set the auth token
setAuthToken(token);
// decode the auth token
const decoded = jwt_decode(token);
// pass the decoded token
dispatch(setCurrentUser(decoded))
// this.props.history.push("/dashboard")
}).catch( err => {
// console.log(err.response.data.error[0].msg)
Object.keys(err.response.data.error).forEach( (key) => {
dispatch({
type: GET_ERRORS,
payload: err.response.data.error[key].msg
})
})
})
};
How would i be able to remove the error, if for example the user does make a password that is minimum of 5 chars, or uses a username that does exist ? You know what i mean ? Also could this be done asynchronously ? As if you were to sign in or register on a top end social media website where it shows the error on async and gos away once you complied to the error message.
Full react code
SignUp.js
import React, { Component } from "react";
import {connect} from 'react-redux';
import {registerUser} from '../actions/authActions';
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
import Grid from '#material-ui/core/Grid';
import PropTypes from "prop-types";
import Typography from '#material-ui/core/Typography';
class SignUp extends Component{
constructor() {
super();
this.state = {
formData:{
email:'',
username:'',
password:'',
passwordConf: "",
isAuthenticated: false,
},
errors:{},
passErr:null
}
}
componentDidMount() {
// console.log(this.props.auth);
if (this.props.auth.isAuthenticated) {
this.props.history.push("/dashboard");
}
}
// this line is magic, redirects to the dashboard after user signs up
componentWillReceiveProps(nextProps) {
if (nextProps.auth.isAuthenticated) {
this.props.history.push("/dashboard");
}
if (nextProps.errors) {
this.setState({ errors: nextProps.errors });
}
}
handleChange = (e) => {
e.preventDefault();
const {formData} = this.state;
this.setState({
formData: {
...formData,
[e.target.name]: e.target.value
}
});
}
handleSubmit = (e) => {
e.preventDefault();
const {formData} = this.state;
const {username, email, password, passwordConf} = formData;
this.setState({
username: this.state.username,
password: this.state.password,
passwordConf: this.state.passwordConf,
email: this.state.email
});
const creds = {
username,
email,
password
}
console.log(creds);
if (password === passwordConf) {
this.props.registerUser(creds, this.props.history);
} else {
this.setState({passErr: "Passwords Don't Match"})
}
}
render(){
return(
<div>
<Grid container justify="center" spacing={0}>
<Grid item sm={10} md={6} lg={4} style={{ margin:'20px 0px'}}>
<Typography variant="h4" style={{ letterSpacing: '2px'}} >
Sign Up
</Typography>
{this.props.auth.errors ? (
this.props.auth.errors.map( (err, i) => (
<div key={i} style={{color: 'red'}}>
{err}
</div>
))
):(
null
)}
{this.state.passErr && (
<div style={{color: 'red'}}>
{this.state.passErr}
</div>
)}
<form onSubmit={this.handleSubmit}>
<TextField
label="Username"
style={{width: '100%' }}
name="username"
value={this.state.username}
onChange={this.handleChange}
margin="normal"
/>
<br></br>
<TextField
label="Email"
className=""
style={{width: '100%' }}
name="email"
value={this.state.email}
onChange={this.handleChange}
margin="normal"
/>
<br></br>
<TextField
label="Password"
name="password"
type="password"
style={{width: '100%' }}
className=""
value={this.state.password}
onChange={this.handleChange}
margin="normal"
/>
<br></br>
<TextField
label="Confirm Password"
name="passwordConf"
type="password"
style={{width: '100%' }}
className=""
value={this.state.passwordConf}
onChange={this.handleChange}
margin="normal"
/>
<br></br>
<br></br>
<Button variant="outlined" color="primary" type="submit">
Sign Up
</Button>
</form>
</Grid>
</Grid>
</div>
)
}
}
SignUp.propTypes = {
registerUser: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
auth: state.auth
})
const mapDispatchToProps = (dispatch) => ({
registerUser: (userData) => dispatch(registerUser(userData))
})
export default connect(mapStateToProps, mapDispatchToProps)(SignUp)
AuthReducer
import {SET_CURRENT_USER, GET_ERRORS} from '../actions/types';
import isEmpty from '../actions/utils/isEmpty';
const initialState = {
isAuthenticated: false,
errors: []
}
export default (state = initialState, action) => {
switch (action.type) {
case SET_CURRENT_USER:
return{
...state,
isAuthenticated: !isEmpty(action.payload),
user:action.payload
}
case GET_ERRORS:
console.log(action.payload)
// allows for us to loop through an array of errors.
return Object.assign({}, state, {
errors: [...state.errors, action.payload]
})
default:
return state;
}
}
a UI example of how this plays out
Could you clear the errors from your state inside the GET_ERRORS case in your reducer? Basically change your case GET_ERRORS to this:
case GET_ERRORS:
console.log(action.payload)
// allows for us to loop through an array of errors.
return Object.assign({}, state, {
errors: [action.payload]
})
Then in your action creator, do this in your catch instead:
const errors = [];
Object.keys(err.response.data.error).forEach( (key) => {
errors.push(err.response.data.error[key].msg)
})
dispatch({
type: GET_ERRORS,
payload: errors,
})
To get the errors all on individual lines do something like this in your render function:
{this.props.auth.errors ? (
this.props.auth.errors.map( (err, i) => (
<div key={i} style={{color: 'red'}}>
{err}
</div>
<br />
))
):(
null
)}

Can't get jest test to fire button onclick

I am completely new to both react and testing and am a bit stumped.
I was just wondering if someone could tell me why my test fails. I assume I making a basic mistake in how this should work.
I am trying to test a log in page. At the moment I am just trying to get my test to fire an onclick from a button and check that that a function has been called.
The log in component code can be seen below.
import React, { Component, Fragment } from "react";
import { Redirect } from "react-router-dom";
// Resources
//import logo from "assets/img/white-logo.png";
//import "./Login.css";
// Material UI
import {
withStyles,
MuiThemeProvider,
createMuiTheme
} from "#material-ui/core/styles";
import Button from "#material-ui/core/Button";
import TextField from "#material-ui/core/TextField";
import Person from "#material-ui/icons/Person";
import InputAdornment from "#material-ui/core/InputAdornment";
// Custom Components
import Loading from "components/Loading/Loading.jsx";
// bootstrat 1.0
import { Alert, Row } from "react-bootstrap";
// MUI Icons
import LockOpen from "#material-ui/icons/LockOpen";
// remove
import axios from "axios";
// API
import api2 from "../../helpers/api2";
const styles = theme => ({
icon: {
color: "#fff"
},
cssUnderline: {
color: "#fff",
borderBottom: "#fff",
borderBottomColor: "#fff",
"&:after": {
borderBottomColor: "#fff",
borderBottom: "#fff"
},
"&:before": {
borderBottomColor: "#fff",
borderBottom: "#fff"
}
}
});
const theme = createMuiTheme({
palette: {
primary: { main: "#fff" }
}
});
class Login extends Component {
constructor(props, context) {
super(props, context);
this.state = {
username: "",
password: "",
isAuthenticated: false,
error: false,
toggle: true,
// loading
loading: false
};
}
openLoading = () => {
this.setState({ loading: true });
};
stopLoading = () => {
this.setState({ loading: false });
};
toggleMode = () => {
this.setState({ toggle: !this.state.toggle });
};
handleReset = e => {
const { username } = this.state;
this.openLoading();
api2
.post("auth/admin/forgotPassword", { email: username })
.then(resp => {
this.stopLoading();
console.log(resp);
})
.catch(error => {
this.stopLoading();
console.error(error);
});
};
handleSubmit = event => {
event.preventDefault();
localStorage.clear();
const cred = {
username: this.state.username,
password: this.state.password
};
api2
.post("auth/admin", cred)
.then(resp => {
console.log(resp);
localStorage.setItem("api_key", resp.data.api_key);
localStorage.setItem("username", cred.username);
return this.setState({ isAuthenticated: true });
})
.catch(error => {
if (error.response) {
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
console.log(error.request);
} else {
console.log("Error", error.message);
}
console.log(error.config);
return this.setState({ error: true });
});
};
handleInputChange = event => {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
[name]: value
});
};
forgotPassword = () => {
console.log("object");
};
render() {
const { error } = this.state;
const { isAuthenticated } = this.state;
const { classes } = this.props;
if (isAuthenticated) {
return <Redirect to="/home/dashboard" />;
}
return (
<div className="login-page">
<video autoPlay muted loop id="myVideo">
<source
src=""
type="video/mp4"
/>
</video>
<div className="videoOver" />
<div className="midl">
<Row className="d-flex justify-content-center">
<img src={''} className="Login-logo" alt="logo" />
</Row>
<br />
<Row className="d-flex justify-content-center">
{error && (
<Alert style={{ color: "#fff" }}>
The username/password entered is incorrect. Try again!
</Alert>
)}
</Row>
<MuiThemeProvider theme={theme}>
<Row className="d-flex justify-content-center">
<TextField
id="input-username"
name="username"
type="text"
label="username"
value={this.state.username}
onChange={this.handleInputChange}
InputProps={{
className: classes.icon,
startAdornment: (
<InputAdornment position="start">
<Person className={classes.icon} />
</InputAdornment>
)
}}
/>
</Row>
{this.state.toggle ? (
<Fragment>
<br />
<Row className="d-flex justify-content-center">
<TextField
id="input-password"
name="password"
type="password"
label="pasword"
value={this.state.password}
onChange={this.handleInputChange}
className={classes.cssUnderline}
InputProps={{
className: classes.icon,
startAdornment: (
<InputAdornment position="start">
<LockOpen className={classes.icon} />
</InputAdornment>
)
}}
/>
</Row>
</Fragment>
) : (
""
)}
</MuiThemeProvider>
<br />
<Row className="d-flex justify-content-center">
{this.state.toggle ? (
<Button
className="button login-button"
data-testid='submit'
type="submit"
variant="contained"
color="primary"
onClick={this.handleSubmit}
name = "logIn"
>
Login
</Button>
) : (
<Button
className="button login-button"
type="submit"
variant="contained"
color="primary"
onClick={this.handleReset}
>
Reset
</Button>
)}
</Row>
<Row className="d-flex justify-content-center">
<p onClick={this.toggleMode} className="text-link">
{this.state.toggle ? "Forgot password?" : "Login"}
</p>
</Row>
</div>
<Loading open={this.state.loading} onClose={this.handleClose} />
</div>
);
}
}
export default withStyles(styles)(Login);
My current test which fails.
import React from 'react'
import {render, fireEvent, getByTestId} from 'react-testing-library'
import Login from './login'
describe('<MyComponent />', () => {
it('Function should be called once', () => {
const functionCalled = jest.fn()
const {getByTestId} = render(<Login handleSubmit={functionCalled} />)
const button = getByTestId('submit');
fireEvent.click(button);
expect(functionCalled).toHaveBeenCalledTimes(1)
});
});
I'm also fairly new to React Testing Library, but it looks like your button is using this.handleSubmit, so passing your mock function as a prop won't do anything. If you were to pass handleSubmit in from some container, then I believe your tests, as you currently have them, would pass.

Resources