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
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.
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>
);
}
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() });
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
)}
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.