How to use sign in with email and password with Firebase? - reactjs

This is going to be my longest post on Stack Overflow and I'm not proud of it. The focus should be on the first two code blocks in which I define the functions handling firebase. The other bits are just a sanity check that I am not missing anything simple.
The problem is that my form won't be submitted (and the functions inside of the onSubmit don't even run). After clicking the 'submit' button, I'd like to register a user to Firebase with email and password. That doesn't work as it stands.
async createUserProfileDocument(user, additionalData) {
if (!user) return
const userRef = this.firestore.doc(`users/${user.uid}`)
const snapshot = await userRef.get()
if (!snapshot.exists) {
const { displayName, email, photoURL } = user
const createdAt = moment().format('MMMM Do YYYY, h:mm:ss a')
try {
await userRef.set({
displayName,
email,
photoURL,
createdAt,
...additionalData,
})
} catch (error) {
console.error('error creating user: ', error)
}
}
return this.getUserDocument(user.uid)
}
export const Register = () => {
const history = useHistory()
async function writeToFirebase(email, senha, values) {
try {
const { user } = await firebaseService.auth.createUserWithEmailAndPassword(email, senha)
firebaseService.createUserProfileDocument(user, values)
history.push('/')
} catch (error) {
console.error('error: ', error)
}
}
function onSubmit(values, { setSubmitting }) {
values.displayName = values.user.nome
writeToFirebase(values.user.email, values.user.senha, values)
setSubmitting(false)
}
return (
<div className="booking">
<Formik
initialValues={initialValues}
validationSchema={Yup.object({ ...validationService.generateBasicUserSchema() })}
onSubmit={onSubmit}>
<Form>
<EWUserBasicDataFields />
<button type="submit">Submit</button>
</Form>
</Formik>
</div>
)
}

As I said in the comments
Do some testing, remove validationSchema and see if you can submit the form. If you can, the problem is that the form is invalid. Also check in the console to see if you don't have any errors.

Related

Email verification MERN stack app fails to handle wrong or expired token

I'm trying to implement email verification in a MERN stack app, and while a big deal of it works, I'm struggling to handle the case when the token is either invalid or expired.
To try and be brief, this is more or less what happens:
User registers
Towards the end of the registration function (backend) I have:
// Create user
const user = new User({
name,
email,
password: hashedPassword,
});
// Email verification
jwt.sign(
{
user: user._id,
},
process.env.JWT_SECRET,
{
expiresIn: 3600,
},
(err, emailToken) => {
mailTransport().sendMail({
from: process.env.USER_EMAIL,
to: user.email,
subject: 'Verify your email address',
html: generateEmail(emailToken),
});
}
);
await user.save();
The function generateEmail() creates and emails a verification link such as:
${process.env.FRONTEND}/verify-email/${token}
The React component in the frontend, VerifyEmail.jsx is roughly like this (deleted redundant code):
const API_URL = '/api/users/verify-email';
const VerifyEmail = () => {
const { verificationToken } = useParams();
const navigate = useNavigate();
const onVerify = async () => {
try {
await axios.get(`${API_URL}/${verificationToken}`);
toast.success('Email verified. You can now log in');
navigate('/login');
} catch (error) {
toast.error(error);
}
};
return (
<section>
<h1 className="text-6xl border-b-2 pb-2">Verify your email</h1>
<p className="my-5">Please click below to verify your email</p>
<button type="button" className="btn btn-info" onClick={onVerify}>
Verify Email
</button>
</section>
);
};
The backend route for that request is:
router.get('/verify-email/:verificationToken', verifyToken);
And the function verifyToken is:
const verifyToken = asyncHandler(async (req, res) => {
try {
const { user } = jwt.verify(
req.params.verificationToken,
process.env.JWT_SECRET
);
await User.findByIdAndUpdate(user, { verified: true }, { new: true });
} catch (error) {
res.status(400);
throw new Error(error.message);
}
return res.redirect(process.env.FRONTEND);
});
All this effectively verifies the email and updates the user in the database with verified: true BUT it doesn't handle when the token is invalid or expired.
What am I missing?
Need to add that the only error I get is, in the console, a 400 saying I'm attempting a GET request on the API on the frontend (port 3000) rather than the backend (5001):
GET http://localhost:3000/api/users/verify-email/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiNjFmZDUyNzFhNzIyMjNjZWE4MGM0MDcxIiwiaWF0IjoxNjQzOTkxNjY1LCJleHAiOjE2NDM5OTUyNjV9.zyWhGtfV2coNZKYVW6yUOUbHo6gBnZZ4aRU79OE0Nbw
(Only when the JWT is expired or invalid)

Is there any way to run an onClick event before a formik onSubmit?

I am trying to create a login page with the following submit button
<Button
color="primary"
disabled={isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
onClick={() => { login(values.email, values.password); }}>
Sign in
</Button>
The login logic looks like this
const login = async (email, password) => {
try {
const user = await signInWithEmailAndPassword(
auth,
email,
password
);
console.log('User: ', user);
authenticated = true;
console.log('Authenticated: ', authenticated);
} catch (error) {
console.log('Error: ', error);
authenticated = false;
}
};
My formik component looks like this
<Formik
initialValues={{
email: '',
password: ''
}}
validationSchema={Yup.object().shape({
email: Yup.string().email('Must be a valid email').max(255).required('Email is required'),
password: Yup.string().max(255).required('Password is required')
})}
onSubmit={() => {
console.log('Authenticated: ', authenticated);
if (authenticated === true) {
navigate('/app/dashboard', { replace: true });
}
}}
>
My idea is to get the onClick of the submit button, to run the login logic before the onSubmit runs, this would set authenticated to true and allow the user to be redirected to Dashboard.js. Currently, it seems like the onSubmit runs before the onClick, causing authenticated to have it's default value of false and thus not redirecting the user to the Dashboard.
Since your login function is an async function, it returns a promise and hence the login inside onSubmit runs asynchronously.
You can call the login function inside onSubmit but have to wait for the promsie to fulfill
for example:
<Formik
initialValues={{
email: '',
password: ''
}}
validationSchema={Yup.object().shape({
email: Yup.string().email('Must be a valid email').max(255).required('Email is required'),
password: Yup.string().max(255).required('Password is required')
})}
onSubmit={() => {
login(email, password)
.then(() => {
console.log('Authenticated: ', authenticated);
if (authenticated === true) {
navigate('/app/dashboard', { replace: true });
}
})
.catch((error) => {
// handle error
})
}}
>
Also rather than setting a variable authenticated outside a scope you can simply return from the login function and use it in the promise success handler.
const login = async (email, password) => {
try {
const user = await signInWithEmailAndPassword(
auth,
email,
password
);
console.log('User: ', user);
return true
} catch (error) {
console.log('Error: ', error);
throw error
}
};
onSubmit={() => {
login(email, password)
.then((isAuthenticated) => {
console.log('Authenticated: ', isAuthenticated);
if (isAuthenticated === true) {
navigate('/app/dashboard', { replace: true });
}
})
.catch((error) => {
// handle error
})
}}
Please read and understand more about javascript promises, asynchronous javascript and how they work.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

why is this redirecting me even if the user is not in the database?

I'm trying to build a login verification form with a mongoDB database. And whenever the user logs in with the correct details he should be redirected to the homepage and if it is wrong, he should stay in the same page. Everything works fine with storing the user and veryfing it from server side. But whenever I try to log the user in even if the user is incorrect it still redirects me.
Here is the login form:
import React, {useState} from 'react'
import './login.css'
import axios from 'axios'
import { useHistory } from "react-router-dom";
function Login() {
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [data, setData] = useState(null)
const history = useHistory()
const onChangeUsername = (e) => {
setUsername(e.target.value)
}
const onChangePassword = (e) => {
setPassword(e.target.value)
}
const onSubmit = (e) => {
e.preventDefault()
const users = {
username: username,
password: password
}
axios.post('http://localhost:4000/users/login', users)
.then(res => console.log(res.data))
}
const loginUser = () => {
axios.get("http://localhost:4000/users/user", {
withCredentials: true
}).then(res => {
setData(res)
if(res.status === 200) return history.push("/home")
else return history.push("/login")
})
}
console.log(data)
return (
<div>
<img src="https://www.freepnglogos.com/uploads/twitter-logo-png/twitter-logo-vector-png-clipart-1.png" className="twitterlogo____image"/>
<h1 className="login_____headertext">Log in to Twitter</h1>
<div className="placeholder_____global">
<form onSubmit={onSubmit}>
<input className="placeholder____div" placeholder="Phone, email or username" onChange={onChangeUsername}/>
<div>
<input className="placeholder____div" placeholder="Password" type="password" onChange={onChangePassword}/>
</div>
<div>
<button className="twitter___loginbuttonpage" onClick={loginUser}>Log in</button>
</div>
</form>
<div className="forgetPassword_____div">
<p>Forgot password?</p>
<p>ยท</p>
<p>Sign up for Twitter</p>
</div>
</div>
</div>
)
}
export default Login
server side passport authentication:
const express = require('express');
const router = express.Router();
const Users = require('../models/users.model.js')
const passport = require("passport")
require('../authentication/passportConfig.js')(passport)
router.route('/').get((req, res) => {
Users.find()
.then(users => res.json(users))
.catch(err => res.status(400).json('Error:' + err))
})
router.route('/user').get((req, res) => {
res.send(req.user)
})
router.route('/login').post((req, res, next) => {
passport.authenticate("local" , (err, user, info) => {
if (err) throw err;
if (!user) res.send("No user exists")
else {
req.logIn(user, err => {
if (err) throw error;
res.send("Succesfully Authenticated")
})
}
})(req, res, next)
})
router.route('/add').post(async(req,res) => {
const hashedPassword = await bcrypt.hash(req.body.password, 10)
const username = req.body.username
const password = hashedPassword
const email = req.body.email
const phone = req.body.phone
const monthOfBirth = req.body.monthOfBirth
const dayOfBirth = req.body.dayOfBirth
const yearOfBirth = req.body.yearOfBirth
const newUsers = new Users({
username,
password,
email,
phone,
monthOfBirth,
dayOfBirth,
yearOfBirth
})
newUsers.save()
.then (() => res.json("User Added"))
.catch(err => res.status(400).json('error' + err))
})
module.exports = router
Whenever you click on the Login button, it executes two methods:
loginUser(): Because it is being called via onClick attribute of the button
onSubmit(): Because it is being called on form's submission via onSubmit
Now let's talk about the server-side of things. The loginUser method on client-side calls the /user endpoint which always returns a 200 status code because all it does is send back a user from the request via res.send(req.user), while onSubmit method on client-side actually triggers authentication through the /login endpoint.
On the client side, the implementation of loginUser method checks for the status 200 of the response via:
if(res.status === 200) return history.push("/home")
else return history.push("/login")
In this scenario, since /user endpoint that loginUser calls will always simply send the response back via res.send(req.user), it is bound to always have status equals to 200, hence, the if-else expression on the client-side will never execute the else part of it, and continue to redirect to /home, irrespective of the correctness of the credentials.
Solution
A better approach IMO would be to avoid executing 2 methods on clicking of Login button, and only have onSubmit method executed on form submission on the client-side, letting it trigger /login endpoint to handle the authentication and then according to the API's response, managing redirection to /login page or /home page.

ReactJS Formik, Why is the form not clearing?

I have Form
const TextForm = props => (
<Formik
initialValues = {{
text: '',
target: props.target
}}
onSubmit = {(values, { setSubmitting, resetForm }) => {
if (values.target == 'add') {
Request('POST', {"text":values.text});
resetForm({"text":""});
setSubmitting(false);
}
}}
>
{({
handleChange,
handleSubmit,
values,
}) => (
<form onSubmit={handleSubmit}>
<input type="hidden" name="target"/>
<textarea className={styles.text} value={values.text} name="text" onChange={handleChange}></textarea><br/>
<button type="submit">
Submit
</button>
</form>
)}
</Formik>
);
And data send ok. But the form doesn't clear. Why??
I also try this:
TextForm.setFieldValue('text', '');
But this doesn`t work too.
Check that the following function call is not throwing an exception:
Request('POST', {"text":values.text});
If an exception is being thrown from Request(..), then the subsequent call to resetForm(..) and setSubmitting(..) will be skipped which would be the reason for the problem you're experiencing.
Also, some improvements you might consider would be to make the following changes to your onSubmit handler:
/* Define onSubmit callback as async */
onSubmit = { async (values, { setSubmitting, resetForm }) => {
try {
if(values.target == 'add') {
/*
If request is async or returns a promise, wait
for it before resetting form
*/
await Request('POST', {"text":values.text});
resetForm({"text":""});
}
}
/*
If you want to handle errors here, do so with a
catch block like so:
catch(err) {
handleErrorHereIfYouChose(err);
}
*/
finally {
/*
Always ensure form submit state is reset, even if
unhandled is exception thrown
*/
setSubmitting(false);
}
}}
Me helped values.text = '';
onSubmit = {async (values, { setSubmitting, resetForm }) => {
try {
if (values.target == 'add') {
await fetch('http://localhost:3000/notes', {
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({"text":values.text}),
});
values.text = '';
}
}
finally {
setSubmitting(false);
}

How to pass additional data to a function that adds things to an object?

I am trying to create a user profile document for regular users and for merchants on Firebase. I am trying to add additional to data this document when a merchant signs up, but haven't succeeded. The difference is that merchants are supposed to have a roles array with their roles. If this is not the right approach to deal with differentiating users, I'd also be happy to hear what's best practice.
My userService file
async createUserProfileDocument(user, additionalData) {
console.log('additionalData: ', additionalData) //always undefined
if (!user) return
const userRef = this.firestore.doc(`users/${user.uid}`)
const snapshot = await userRef.get()
if (!snapshot.exists) {
const { displayName, email } = user
try {
await userRef.set({
displayName,
email,
...additionalData,
})
} catch (error) {
console.error('error creating user: ', error)
}
}
return this.getUserDocument(user.uid)
}
async getUserDocument(uid) {
if (!uid) return null
try {
const userDocument = await this.firestore.collection('users').doc(uid).get()
return { uid, ...userDocument.data() }
} catch (error) {
console.error('error getting user document: ', error)
}
}
This is what happens when the user signs up as a merchant in the RegisterMerchant component:
onSubmit={(values, { setSubmitting }) => {
async function writeToFirebase() {
//I can't pass the 'roles' array as additionalData
userService.createUserProfileDocument(values.user, { roles: ['businessOnwer'] })
authService.createUserWithEmailAndPassword(values.user.email, values.user.password)
await merchantsPendingApprovalService.collection().add(values)
}
writeToFirebase()
I am afraid this might have something to do with onAuthStateChange, which could be running before the above and not passing any additionalData? This is in the Middleware, where I control all of the routes.
useEffect(() => {
authService.onAuthStateChanged(async function (userAuth) {
if (userAuth) {
//is the below running before the file above and not passing any additional data?
const user = await userService.createUserProfileDocument(userAuth) //this should return the already created document?
//** do logic here depending on whether user is businessOwner or not
setUserObject(user)
} else {
console.log('no one signed in')
}
})
}, [])
There is onCreate callback function which is invoked when user is authenticated.
Here's how you could implement it
const onSubmit = (values, { setSubmitting }) => {
const { user: {email, password} } = values;
const additionalData = { roles: ['businessOnwer'] };
auth.user().onCreate((user) => {
const { uid, displayName, email } = user;
this.firestore.doc(`users/${uid}`).set({
displayName,
email,
...additionalData
});
});
authService.createUserWithEmailAndPassword(email, password);
}

Resources