Firebase, React: How do I display http errors on client side? - reactjs

I'm building a project with React and Firebase. I've split it into two repos - a backend repo for firebase functions, and a client-side repo for React. Part of my project involves user registration, and I want to be able to send any errors (eg, email already in use, weak password) to the client side, but I'm not sure how. Does anyone have suggestions about how I can pass backend errors to the client?
I know that I could move the firebase API call to client side which would make error handling much easier, but this would create complications.
Code as follows:
Registration component VenueRegister
import React from 'react'
import Header from './Header'
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button'
import axios from 'axios'
import { auth } from 'firebase/app'
class VenueRegister extends React.Component {
constructor() {
super();
this.state = {
email: '',
password:'',
confirmPassword:'',
venueName:'',
venueAddress:''
};
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange(e){
this.setState({
[e.target.name]: e.target.value
})
}
handleSubmit(e) {
console.log("submit function reached");
e.preventDefault();
const venueData = {
email: this.state.email,
password: this.state.password,
confirmPassword: this.state.confirmPassword,
venueName: this.state.venueName,
venueAddress: this.state.venueAddress
};
axios("http://localhost:5000/gig-fort/us-central1/api/registerVenue", {
method: "POST",
headers: {
"content-type": "application/json",
},
data: venueData,
})
.then((res) => {
console.log(res.data);
})
.catch((err) => {
console.error(err);
});
}
render() {
return (
<>
<Header />
<h1 className="heading-venue-reg">Register your venue</h1>
<form onSubmit = {this.handleSubmit}>
<TextField
required
id="standard-required"
name = 'email'
label="Required"
defaultValue="email"
placeholder="email"
onChange = {this.handleChange}
/>
<TextField
required
id="standard-required"
name = 'password'
label="Required"
defaultValue="password"
placeholder="password"
onChange = {this.handleChange}
/>
<TextField
required
id="standard-required"
name = 'confirmPassword'
label="Required"
defaultValue="confirm password"
placeholder="confirm password"
onChange = {this.handleChange}
/>
<TextField
required
id="standard-required"
name = 'venueName'
label="Required"
defaultValue="venue name"
placeholder="venue name"
onChange = {this.handleChange}
/>
<TextField
required
id="standard-required"
name = 'venueAddress'
label="Required"
defaultValue="venue address"
placeholder="venue address"
onChange = {this.handleChange}
/>
<Button type="submit">Submit</Button>
</form>
</>
);
}
}
export default VenueRegister
Backend function for venue registration:
app.post('/registerVenue',(req,res) =>{
const newVenue = {
email: req.body.email,
password: req.body.password,
confirmPassword: req.body.confirmPassword,
venueName: req.body.venueName,
venueAddress: req.body.venueAddress
}
let errors = {}
if(isEmpty(newVenue.email)){
errors.email = 'Email must not be empty'
}else if (!isEmail(newVenue.email)){
errors.email = 'must be a valid email address'
}
if(Object.keys(errors).length >0){
return res.status(400).json(errors)
}
let token, userId
db.doc(`/user/${newVenue.venueName}`).get()
.then(doc => {
if(doc.exists){
return res.status(400).json({venueName: `This venue name is already taken`})
} else {
return firebase
.auth()
.createUserWithEmailAndPassword(newVenue.email, newVenue.password)
}
})
.then(data=> {
userId = data.user.uid;
return data.user.getIdToken()
})
.then(idToken => {
token = idToken
const venueDetails ={
email: newVenue.email,
venueName: newVenue.venueName,
venueAddress: newVenue.venueAddress,
createdAt: new Date().toISOString(),
userId
}
return db.doc(`/users/${newVenue.venueName}`).set(venueDetails)
})
.then(() => {
return res.status(201).json({token})
})
.catch(err => {
console.error(err)
if(err.code === 'auth/email-already-in-use'){
return res.status(400).json({email: 'Email is already in use'})
} else {
return res.status(500).json({error:err.code})
}
//TODO: put in conditional statement that sends 'weak password error' to client
})
})

In the frontend, when receiving the data from the backend, you should check your response data for errors. It is very likely you would want a util function for that since it is a common task across almost all requests (hence re-utilize).
So you would like to check the response.status and then set a new state like a hasError or something that will cause a re-render to your component and show, i.e, a child component that displays the error to the user and gives the ability to retry the operation.

Related

Having issues connecting to express backend with React

Trying to set up a register form using react and then sending the form to my express backed to create the user. The request is getting to the back-end but none of the form data is there.
When i console.log the request body and params, only an empty object gets returned
Here is my react register code
import React from 'react'
class Register extends React.Component {
state = {
name: "",
username: "",
password: ""
}
handleChange = event => {
const { name, value } = event.target
this.setState({[name]:value})
}
handleSubmit = event => {
event.preventDefault();
fetch("/api/register", {
method: "POST",
body: JSON.stringify(this.state)
})
.then((result)=> result.json())
.then((info) => { console.log(info); })
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input type="text" name="name" value={this.state.name} placeholder="name" onChange={this.handleChange} />
<input type="text" name="username" value={this.state.username} placeholder="username" onChange={this.handleChange} />
<input type="password" name="password" value={this.state.password} placeholder="password" onChange={this.handleChange} />
<button>Register</button>
</form>
</div>
)
}
}
export default Register
Express code
const express = require("express"),
passport = require("passport"),
User = require("../models/user");
const router = express.Router({mergeParams: true});
// /api before this
router.post("/register", (req, res)=>{
const newUser = new User({
username: req.body.username,
name: req.body.name
});
User.register(newUser, req.body.password, (err, user)=>{
if(err) {
console.log(err);
}
passport.authenticate("local")(req, res, ()=>{
res.redirect("/user/" + user._id);
});
});
});
module.exports = router;
Now im getting a 400 bad request error and another error that says: register:1 Uncaught (in promise) SyntaxError: Unexpected token B in JSON at position 0
I would say that the url from the fetch action is not matching your express route
/api/register > /register
Try adding /api to the express route or remove from the fetch action
I also noticed your route is not returning anything.
You have to return something in the response object.
res.json(newUser);
router.post("/register", (req, res)=>{
const newUser = new User({
username: req.body.username,
name: req.body.name
});
// some code
return res.json(newUser);
});

How to solve Uncaught (in promise) TypeError in react authentication?

I am pretty new to MERN stack. I am trying to setup user authentication using jwt.
The problem is only occurring when I am using the front end to login. When I using POST man to make an http request, login successful is succesfull
However when I login using email and password I get the following error:
console
Uncaught (in promise) TypeError: Cannot read property 'data' of undefined
at authActions.js:40
I know it is showing where the error is but I am still unable to fix it.
authActions.js
import axios from "axios";
import setAuthToken from "../utils/setAuthToken";
import jwt_decode from "jwt-decode";
import { GET_ERRORS, SET_CURRENT_USER, USER_LOADING } from "./types";
// Register User
export const registerUser = (userData, history) => dispatch => {
axios
.post("/api/users/register", userData)
.then(res => history.push("/login")) // re-direct to login on successful register
.catch(err =>
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
);
};
// Login - get user token
export const loginUser = userData => dispatch => {
axios
.post("/api/users/login", userData)
.then(res => {
// Save to localStorage
// Set token to localStorage
const { token } = res.data;
localStorage.setItem("jwtToken", 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 = () => dispatch => {
// Remove token from local storage
localStorage.removeItem("jwtToken");
// Remove auth header for future requests
setAuthToken(false);
// Set current user to empty object {} which will set isAuthenticated to false
dispatch(setCurrentUser({}));
};
Following is my code for login front end.
login.jsx
class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: "",
password: ""
};
}
componentDidMount() {
// If logged in and user navigates to Login page, should redirect them to dashboard
if (this.props.auth.isAuthenticated) {
this.props.history.push("/dashboard");
}
}
validateForm() {
return this.state.email.length > 0 && this.state.password.length > 0;
}
componentWillReceiveProps(nextProps) {
if (nextProps.auth.isAuthenticated) {
this.props.history.push("/dashboard"); // push user to dashboard when they login
}
if (nextProps.errors) {
this.setState({
errors: nextProps.errors
});
}
}
handleChange = event => {
this.setState({
[event.target.id]: event.target.value
});
};
handleSubmit = event => {
event.preventDefault();
console.log("Submit called");
const userData = {
email: this.state.email,
password: this.state.password
};
this.props.loginUser(userData);
};
render() {
const errors = this.state;
return (
<div className="Login">
<form onSubmit={this.handleSubmit}>
<FormGroup controlId="email" bs="large">
<FormLabel>Email</FormLabel>
<span className="red-text">{errors.emailnotfound}</span>
<FormControl
autoFocus
type="email"
value={this.state.email}
onChange={this.handleChange}
className={classnames("", {
invalid: errors.email || errors.emailnotfound
})}
/>
</FormGroup>
<FormGroup controlId="password" bs="large">
<FormLabel>Password</FormLabel>
<span className="red-text">
{errors.password}
{errors.passwordincorrect}
</span>
<FormControl
value={this.state.password}
onChange={this.handleChange}
type="password"
className={classnames("", {
invalid: errors.password || errors.passwordincorrect
})}
/>
</FormGroup>
<Button
block
bs="large"
disabled={!this.validateForm()}
type="submit"
>
Login
</Button>
<br />
<p> Dont have account ? </p>
<Link to="/register">
{" "}
<p style={{ color: "blue" }}> Join Us </p>{" "}
</Link>
</form>
<br />
</div>
);
}
}
I have left out some import statements to shorten the code.
The error is on line 40, wich is in payload: err.response.data.
If it says
Cannot read property 'data' of undefined
It means that err.response is ndefined.
You should do some checking before passing that. Maybe it's returning a different error that doesn't have .response.
Try console.log(err) to check what is inside of it.
It looks that you don't stringify the data before you send it. Require the qs library and change your code to that:
axios
.post("/api/users/register", qs.stringify( userData ))
.then(res => history.push("/login")) // re-direct to login on successful register
.catch(err =>
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
);
You can also check that the data is not sent correctly by looking the Network tab in your Browser's Dev Tools. I expect to send the data in a wrong format. I hope that I am correct.
You need to update your code from
onSubmit={this.handleSubmit} to onSubmit={()=>this.handleSubmit()}
And
onChange={this.handleChange}to onChange={()=>this.handleChange()}
because of this your password and email are not getting set to state and your API axios.post("/api/users/login", userData) is throwing exception.

Losing JWT token after browser refresh

I'm learning Full Stack Development with Spring Boot 2.0 and React .
The authentication and authorization are managed by JWT and the app works as expected except I have to re-login after I refresh the browser.
How to maintain JWT token even after browser refresh ?
import React, { Component } from 'react';
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
import Snackbar from '#material-ui/core/Snackbar';
import Carlist from './Carlist';
import {SERVER_URL} from '../constants.js';
class Login extends Component {
constructor(props) {
super(props);
this.state = {username: '', password: '', isAuthenticated: false, open: false};
}
logout = () => {
sessionStorage.removeItem("jwt");
this.setState({isAuthenticated: false});
}
login = () => {
const user = {username: this.state.username, password: this.state.password};
fetch(SERVER_URL + 'login', {
method: 'POST',
body: JSON.stringify(user)
})
.then(res => {
const jwtToken = res.headers.get('Authorization');
if (jwtToken !== null) {
sessionStorage.setItem("jwt", jwtToken);
this.setState({isAuthenticated: true});
}
else {
this.setState({open: true}); // maintient snackbar ouvert
}
})
.catch(err => console.error(err))
}
handleChange = (event) => {
this.setState({[event.target.name] : event.target.value});
}
handleClose = (event) => {
this.setState({ open: false });
}
render() {
if (this.state.isAuthenticated === true) {
return (<Carlist />)
}
else {
return (
<div>
<br/>
<TextField tpye="text" name="username" placeholder="Username"
onChange={this.handleChange} /><br/>
<TextField type="password" name="password" placeholder="Password"
onChange={this.handleChange} /><br /><br/>
<Button variant="raised" color="primary" onClick={this.login}>Login</Button>
<Snackbar
open={this.state.open} onClose={this.handleClose}
autoHideDuration={1500} message='Check your username and password' />
</div>
);
}
}
}
export default Login;
I think you simply don't check for the token in local storage in your constructor. When you reload the page, your constructor executes and sets isAuthenticated = false, whether there is a token in local storage or not. You should add additional logic to check the token in local storage before finally setting isAuthenticated. Probably the best place to put this code would be componentDidMount() function. I mean set it initially to false and then update in componentDidMount() according to current authorization status. Have a look at my GitHub, I have a small boilerplate project with such auth flow set-up. Hope this helps, happy coding!
I would use local storage instead of session storage like this
localStorage.setItem("jwt", jwtToken)
instead of the line
sessionStorage.setItem("jwt", jwtToken);
The check the local storage in the dev console, refresh the page and see if it is still there. It may require some other changes in your auth flow to build it off localStorage instead of sessionStorage; however, this will solve the immediate problem of losing the jwt on page refresh.

Redux-form to pass data by a POST request.

I have to update my user's profile that has 5 fields name, bio address, image, and gender. I have created perfectly working API on Django that uses auth Knox token for authentication.
I have stored the auth token during login in the state. Of which the reducer looks like this:
case 'LOGIN_SUCCESSFUL':
case 'REGISTRATION_SUCCESSFUL':
localStorage.setItem("token", action.data.token);
return {...state, ...action.data, isAuthenticated: true, isLoading: false, errors: null};
I can access the token later on like this:
let headers = {"Content-Type": "application/json"};
let {token} = getState().auth;
if (token) {
headers["Authorization"] = `Token ${token}`;
}
My question is:
How can I make a form that takes this token as a header and makes a post request? What will be the reducers and what will be the actions?
class Profile extends Component {
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleSubmit(e) {
e.preventDefault()
console.log(this.props.Name)
}
change = e => {
console.log(e.target.name)
values.push(e.target.value)
[e.target.name]: e.target.value
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<div>
<label htmlFor="Name">Name</label>
<input name="Name" onChange={e => this.change(e)} component="input" type="text" />
</div>
<div>
<label htmlFor="Bio">Bio</label>
<input name="Bio" component="input" onChange={e => this.change(e)} type="text" />
</div>
<div>
<label htmlFor="Address">Address</label>
<input name="Address" component="input" onChange={e => this.change(e)} type="text" />
</div>
<button type="submit">Submit</button>
</form>
)
}
}
const mapStateToProps = (state) => {
return {
profile: state.user,
}
}
const mapDiapatchToProps = (dispatch) => {
return {
updateprofile: (values) => dispatch(updateprofile(values))
}
}
export default connect(mapStateToProps, mapDiapatchToProps)(Profile);
I tried this but I got confused how to send values to action?
Or Should I have to use redux-form?
I want to make a put request on this API: api/update/profile/${id}
Please help me out.
You need to use an external library to make a HTTP call, like Axios.
In your action file you need to create the function updateProfile. Inside this function you need to make the HTTP call using Axios, or wathever you want. With axios your function will be something like this:
function updateProfile() {
return (dispatch) => {
axios({
method:'get',
url:'[YOUR API ADDRESS]',
headers: {Authorization: '[YOUR TOKEN]'},
data: {
name: 'bruce',
lastName: 'bane'
}
}).then(function(response) {
dispatch({
type: UPDATE_PROFILE,
payload: response
});
});
return null
}
}
In youe Profile component you need to change the mapDispatchToProps function to call the updateProfile function from the action file, like this:
const mapDispatchToProps = (dispatch) => {
return {
updateprofile: (values) => dispatch(profileActions.updateprofile(values))
}
}
Note: I didn't tested this code, but it will be something close to this. Hope it could help.

Axios.post returning :ERR_EMPTY_RESPONSE after API call

I've made a axios post request in my react component, which makes a request to twilio to send a text message to my phone from a route on my server.
The text message and payload are transmitted successfully however when opening the network tab in the console I get this error in a minute or two.
POST http://localhost:8080/api/twilio net::ERR_EMPTY_RESPONSE
Any notion out there how to solve this?
This is the code from my react component:
import React, { Component } from 'react';
import axios from 'axios';
import { Grid, Segment, Form } from 'semantic-ui-react';
import './test.css';
class Test extends Component {
constructor(props) {
super(props);
this.state = { phonenumber: '' };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({ phonenumber: event.target.value });
}
handleSubmit() {
return axios
.post('/api/twilio', {
phonenumber: this.state.phonenumber,
})
.then(resp => resp.data)
.catch(error => console.log(error));
}
render() {
const { phonenumber } = this.state;
console.log('phonenumber', phonenumber);
return (
<Grid columns={1} stackable textAlign="center">
<Grid.Column width={1} />
<Grid.Column width={14}>
<Form onSubmit={this.handleSubmit}>
<Segment stacked>
<Form.Group id="form-group" inline>
<label>Phone Number</label>
<Form.Input onChange={this.handleChange} value={phonenumber} placeholder="+12223334444" />
</Form.Group>
<Form.Button id="form-group-button" content="Submit" />
</Segment>
</Form>
</Grid.Column>
<Grid.Column width={1} />
</Grid>
);
}
}
export default Test;
Update:
This is the twilio route on the backend.
const router = require('express').Router();
module.exports = router;
router.post('/', (req, res) => {
let SID = 'ACc5b16ad0cefc3b514e69bc30636726e2';
let TOKEN = '3145fb41afe308f22b0b7c647e6a8e17';
let SENDER = '+18622256079';
if (!SID || !TOKEN) {
return res.json({ message: 'add TWILIO_SID and TWILIO_TOKEN to .env file.' });
}
let client = require('twilio')(SID, TOKEN);
client.messages
.create({
to: req.body.phonenumber,
from: SENDER,
body: 'This is the ship that made the Kessel Run in fourteen parsecs?',
})
.then(message => console.log(message.sid));
});
In your route on the server, nothing will ever be returned to the client because SID and TOKEN are always defined (at least in your example). To make sure that the request will not fail, you will need to send at least some response back after the Twilio request, e.g.:
client.messages
.create({
to: req.body.phonenumber,
from: SENDER,
body: 'This is the ship that made the Kessel Run in fourteen parsecs?'
})
.then(message => {
console.log(message.sid);
// Either just send an empty, successful response or some data (e.g. the `sid`)
res.status(200).send(message.sid);
})
.catch(err => {
console.log(err);
// In case of an error, let the client know as well.
res.status(500).send(err);
});

Resources