How can I properly handle request errors with React front end? - reactjs

Currently I have my backend set up as such on the '/register' route:
registerRouter.post('/', async (req, res) => {
// Validate submitted registration form
const { error } = registerValidation(req.body)
if(error) {
return res.status(400).send(error.details[0].message)
}
try {
// Check if email exists already
const user = await User.findOne({ email: req.body.email })
if(user) {
return res.status(400).send('Email already exists')
}
// If not, begin registering user by hashing the password
const hashedPassword = await bcrypt.hash(req.body.password, 10)
const newUser = new User({
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
password: hashedPassword
})
const savedUser = await newUser.save()
res.send(savedUser)
} catch(error) {
res.sendStatus(500)
}
})
Using Postman I get the proper responses when I make correct/incorrect requests. But when I make requests on my frontend, if it is an incorrect request, e.g. not long enough password, missing a required field, then I just get a 400 response. How can I use the error response to, for example, display the error on-screen for the user to see?
This is my current onSubmit function for the form:
const register = async event => {
event.preventDefault()
axios
.post('/register', newUser)
.then(res => console.log(res))
.catch(err => console.log(err))
}

try to use:
axios
.post('/register', newUser)
.catch(function (error) {
console.log(error.toJSON()); // or maybe exist .toText()
});
(https://github.com/axios/axios#handling-errors)
also convert it on server side:
return res.status(400).send('Email already exists')
to
return res.status(400).send({ error: 'Email already exists' });

Related

When sign in with google firebase always register user as new

Tech: Firebase, Next.js, Google Sign in, Firebase Stripe exstension
Bug reproduction:
When login with Google
Subscribe on stripe
Stripe saves subscription data for that user in firestore
Logout
Login in with Google and old data are overide with new one, and Subscription is lost
Does anyone had similar problem?
Maybe my implementation of Sign-in is bad, here is the Google Sign in code:
const handleGoogleLogin = () => {
signInWithPopup(auth, googleProvider)
.then(async result => {
if (!result.user) return;
const { displayName, email, uid, providerData, photoURL, phoneNumber } =
result.user;
const name = splitName(displayName as string);
const providerId =
(providerData.length && providerData[0]?.providerId) || '';
const data = {
firstName: name?.firstName || '',
lastName: name?.lastName || '',
email,
photoURL,
phoneNumber,
providerId,
};
await updateUser(uid, data);
})
.catch(error => {
console.log('Google login error: ', error);
});
};
Update user function:
export const updateUser = async (uid: string, data: UpdateUserParams) => {
try {
if (!uid) {
return;
}
await setDoc(doc(firestore, 'users', uid), {
account: {
...data,
initials: `${data.firstName[0]}${data.lastName[0]}`,
},
});
} catch (error) {
console.error('Error updating user: ', error);
}
};
setDoc is overwriting the contents of the document with each sign-in. You should instead use set with merge to prevent overwriting the fields you don't want to lose, or check first if the document exists before creating it.
See also:
https://firebase.google.com/docs/firestore/manage-data/add-data#set_a_document
Difference Between Firestore Set with {merge: true} and Update

AXIOS Status 400 Bad Request on React Front End

I am building an authentication component on React.
When the wrong password/username is entered, I am expecting a Status 400 with the message: 'Invalid email or password' on the front end
Instead, I am getting Status 400 with the message: 'Request failed with status code 400'. I used postman to simulate a bad login and I do get the message : 'Invalid email or password'
When I tried a successful login on my frontend, everything works and I get a JWT token.
I also did a console.log on the backend and I can see that the data did reach the backend. The problem seems to be that the error is not handled by the front end properly.
Can someone take a look and let me know what is the problem? thank you.
Backend Post routes
router.post('/signin', async (req, res) => {
console.log(req.body)
let user = await User.findOne({ email: req.body.email })
if (!user) return res.status(400).send('Invalid email or password')
//compare the password with the password in database
const validPassword = await bcrypt.compare(req.body.password, user.password)
if (!validPassword) return res.status(400).send('Invalid email or password')
const token = user.generateAuthToken()
// res.send(token)
res.header('x-auth-token', token).send(_.pick(user, ['_id', 'name)', 'email']))
})
Frontend React
doSubmit = async (e) => {
e.preventDefault()
const { data } = this.state
try {
console.log(data)
await userService.signIn(data)
} catch (ex) {
console.log(ex.message)
if (ex && ex.response.status === 400) {
let errors = { ...this.state.errors }
errors.email = ex.message
this.setState({errors})
}
}
}
userService
import axios from 'axios'
import { SIGN_UP, SIGN_IN } from '../Components/constant/constant';
import { Redirect } from 'react-router-dom';
export default {
register: (user) => {
console.log(user, 'axios')
axios.post(SIGN_UP, {
email: user.email,
password: user.password,
name: user.name
}).then(function (response) {
console.log(response, 'response')
console.log(response)
if (response.status === 200) {
window.location = '/signupsuccessful'
}
})
.catch(function (error) {
console.log(error);
})
},
signIn: async (data) => {
console.log('sign in user service')
await axios.post(SIGN_IN, {
email: data.email,
password: data.password,
})
}
}
I think you just missed the response part of the exception in the doSubmit function of the React code, so you get the exception message and not the response message from the request.
Change
errors.email = ex.message
To
errors.email = ex.response.data
Example
if (ex && ex.response.status === 400) {
let errors = { ...this.state.errors }
errors.email = ex.response.data
this.setState({errors})
}
Nothing is wrong in your code just to get the response from the error case in axios you have to get like so:
...
.catch((error)=>console.log(error.response.data))
EDIT: for more details
So what you have to do in your code is:
Backend
don't send a string i recommend
send a json
res.status(400).send({message:'Invalid email or password'})
FrontEnd
if (ex && ex.response.status === 400) {
let errors = { ...this.state.errors }
errors.email = ex.response.data.message
this.setState({errors})
}

How to store my JWT token correctly and return data in MERN app

I'm creating a MERN application as I'm learning how to use React to create a blog/social media like website. I already have routes and jsx setup in order to display posts via axios requests and pulling the data from my MongoDB cluster.
This all works perfectly but I am now struggling on finding a proper way to handle authentication for users and display data back into my MERN application.
My current code that registers the user takes their name, email and password; using bcrpyt to hash/salt the password for security.
Register Code
exports.register = (req, res, next) => {
let {
name,
email,
password,
password_confirmation
} = req.body;
User.findOne({
email: email
})
.then(user => {
if (user) {
return res.status(422).json({
errors: [{
user: "email already exists"
}]
});
} else {
const user = new User({
name: name,
email: email,
password: password,
});
bcrypt.genSalt(10, function (err, salt) {
bcrypt.hash(password, salt, function (err, hash) {
if (err) throw err;
user.password = hash;
user.save()
.then(response => {
res.status(200).json({
success: true,
result: response
})
})
.catch(err => {
res.status(500).json({
errors: [{
error: err
}]
});
});
});
});
}
}).catch(err => {
res.status(500).json({
errors: [{
error: 'Something went wrong'
}]
});
})
}
my login function then uses bcrypt.compare in order to make sure the password types in matches with the hashed password stored in the database. If succesful, my code will call a function I created called createJWT which has a payload containing the email, userId and duration of the token before expiring.
const jwt = require("jsonwebtoken");
exports.createJWT = (email, userId, duration) => {
const payload = {
email,
userId,
duration
};
return jwt.sign(payload, process.env.TOKEN_SECRET, {
expiresIn: duration,
});
};
Login code
exports.login = (req, res) => {
let {
email,
password
} = req.body;
User.findOne({
email: email
}).then(user => {
if (!user) {
return res.status(404).json({
errors: [{
user: "not found"
}],
});
} else {
bcrypt.compare(password, user.password).then(isMatch => {
if (!isMatch) {
return res.status(400).json({
errors: [{
password: "incorrect"
}]
});
}
let access_token = createJWT(
user.email,
user._id,
3600
);
jwt.verify(access_token, process.env.TOKEN_SECRET, (err,
decoded) => {
if (err) {
res.status(500).json({
errors: err
});
}
if (decoded) {
return res.status(200).json({
success: true,
token: access_token,
message: user
});
}
});
}).catch(err => {
res.status(500).json({
errors: err
});
});
}
}).catch(err => {
res.status(500).json({
errors: err
});
});
}
If the login function is succesful, the code will decode the JWT code if the token matches my TOKEN_SECRET stored in my env file.
This all works perfectly fine but I am stuck at this point on where to go next. I have created a piece of middleware that I can succesfully implement into my routes in order to stop them loading if the user is not authorized.
//Auth middleware
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
try {
const token = req.header('x-auth-token');
jwt.verify(token, process.env.TOKEN_SECRET);
next();
} catch (error) {
res.status(401).json({
message: "Token not authorized!"
})
}
};
const express = require('express');
const router = express.Router();
const multer = require("multer");
const authorize = require('../middleware/auth');
const {
getPosts,
createPost,
findPostById,
updatePost,
deletePost
} = require('../controllers/posts');
const storage = multer.diskStorage({
destination: (req, file, callback) => {
callback(null, './client/public/uploads');
},
filename: (req, file, callback) => {
callback(null, file.originalname);
}
});
const upload = multer({
storage: storage
});
router.get('/', authorize, getPosts);
router.post('/add', upload.single("postImage"), createPost);
router.get('/:id', findPostById);
router.put('/update/:id', upload.single("postImage"), updatePost);
router.delete('/:id', deletePost);
module.exports = router;
From this point, what would be the best way to have the user login and gain access to the posts (containing my authorization middleware) if they have logged in succesfully and have the JWT token. I was also wondering how i would achieve something like pulling the users name from the decoded token and have it shown (for example) in the navbar for as long as they logged in / the session persists.
Any help here would be greatly appreciated, thanks everyone!
let access_token = createJWT(
user.email,
user._id,
3600
);
jwt.verify(access_token, process.env.TOKEN_SECRET, (err,
decoded) => {
if (err) {
res.status(500).json({
errors: err
});
}
if (decoded) {
return res.status(200).json({
success: true,
token: access_token,
message: user
});
}
});
There's no point to verify the JWT here as you just generated it on the server-side. This is the process:
Register: The user provides a username and a password. You save the user in the database and for the password, you save a hash.
When logging in, the user is providing the username and the password. If they are correct you create a JWT and return that to the client application. In the JWT you will encode the username/user_id.
The client needs to store that JWT somewhere and then send it in the following requests(usually as a header).
On the server you have a middleware that parses that header, decodes the JWT, and identify the user based on username/user_id or whatever you encoded there.
The client should not decode the JWT and pull information like the username from there. It should make an API request to the backend asking for the user information.
There are lots of things to take into consideration here like. Just to name a few:
how do you store the JWT on the client in a secure way?
on the client app, how do you prevent an unauthenticated user to access some private routes?
what do you do after the JWT expires?
how do you handle CORS?
are there any vulnerabilities?
I highly recommend this free course which will teach you a lot about how to implement these things.

How to send data from expressjs to react client?

I have a payments.js page that use a custom form to make a payemnt with stripe which looks like this
async handleSubmit() {
const { firstName, lastName, email, cardNo, expiryDate, cvv, nameOnCard } = this.state
const { price } = this.props.location.state.item
this.setState({
loading: true
});
await axios.post("/charge", {
firstName,
lastName,
email,
cardNo,
expiryDate,
cvv,
nameOnCard,
price
}).then((res) => this.setState({ iframeLink: res.data.next_action.redirect_to_url.url, loading: false }))
.catch((error) => console.log('ERR', error))
// I want this to wait for changes
await axios.post("/api/stripe/webhooks").then((res) => console.log(res)).catch((err) => console.log(err))
}
I am opening the url received in first post in iframe and once the 3d secure is done I want to get stripe hooks with this line which is called once after first post and then will not update once another hook is received
await axios.post("/api/stripe/webhooks").then((res) => console.log(res)).catch((err) => console.log(err))
My problem here is that the data is never received in this action
In server the post function looks like this
// Stripe Webhooks
app.post("/api/stripe/webhooks", async function (req, res) {
try {
const query = req.body;
res.sendStatus(200);
console.log(query)
res.end(JSON.stringify(query))
} catch (err) {
console.log("/webhooks route error: ", err);
}
});
Any idea how to catch the data when is received?

Can't set user data after firebase auuthentication

I'm working on a react app on firebase. The issue that fire store doesn't set up userdata after fire authentication although it's been successfully authenticated on Google. Please help me if you have some clues to solve that. And I can't get error texts here.
login = () => {
const provider = new firebase.auth.GoogleAuthProvider();
firebase
.auth()
.signInWithRedirect(provider)
.then(result => {
const user = result.user;
const userRef = firebase
.firestore()
.collection("users")
.doc(user.uid);
userRef.get().then(function(doc) {
if (doc.exists) {
console.log("User data:", doc.data());
} else {
// doc.data() will be undefined in this case
userRef.set({
uid: user.uid,
name: user.displayName,
photoURL: user.photoURL
});
}
});
});
this.props.history.push("/");
};
Additional
'user' is not defined.
'userRef' is not defined
I have tried the code. But user and userRef could not be defined here:
if (doc.exists) {
console.log("Doc for user " + user.uid + " already exists");
throw new Error("Doc for user " + user.uid + " already exists");
} else {
return userRef.set({
uid: user.uid,
name: user.displayName,
photoURL: user.photoURL
});
}
Additional 2
I don't know why but that would not work then on Firebase Authentication.
login = () => {
let user;
const provider = new firebase.auth.GoogleAuthProvider();
firebase
.auth()
.signInWithRedirect(provider)
.then(result => {
console.log("result", result);
// result has been skipped
Actually, the signInWithRedirect() method returns a promise with no return value (i.e. Promise<void>). Therefore doing
firebase
.auth()
.signInWithRedirect(provider)
.then(result => {...});
will not work.
You need to use the getRedirectResult() method, as explained in the doc:
Authenticates a Firebase client using a full-page redirect flow. To
handle the results and errors for this operation, refer to
firebase.auth.Auth.getRedirectResult.
Therefore, in your login function, you should just have two lines:
login = () => {
const provider = new firebase.auth.GoogleAuthProvider();
firebase
.auth()
.signInWithRedirect(provider);
}
And somewhere else in your code (I don't know exactly where as I don't know reactjs...) you need to have the following code (note how the different Promises are chained):
let user;
firebase
.auth()
.getRedirectResult()
.then(result => {
user = result.user;
const userRef = firebase
.firestore()
.collection('users')
.doc(user.uid);
return userRef.get();
})
.then(doc => {
if (doc.exists) {
console.log('Doc for user ' + user.uid + ' already exists');
throw new Error('Doc for user ' + user.uid + ' already exists');
} else {
return doc.ref.set({
uid: user.uid,
name: user.displayName,
photoURL: user.photoURL
});
}
})
.then(() => {
this.props.history.push('/');
})
.catch(error => {
console.log(error);
this.props.history.push('/errorPage');
});
Note that in case several users are able to sign-in with the same Google Account, you may need to use a Transaction when checking the non-existence of the doc at userRef.

Resources