React+Firebase. Write to Database after Authentication - reactjs

I'm currently setting up Firebase for my React Native app. Authentication & basic writing/reading work fine.
Though, I struggle to write data to the database right after the initial authentication because my "signUp" and "writeUserData" functions execute at the same time (and at this moment there is no userId yet).
Here is my code:
...
module.exports = React.createClass({
getInitialState(){
return({
email: '',
password: '',
confirmPassword: '',
result: '',
})
},
signUp() {
if (this.state.password === this.state.confirmPassword) {
let {email, password} = this.state;
firebaseApp.auth().createUserWithEmailAndPassword(email, password)
.catch(error => this.setState({result: error.message
}));
this.writeUserData(email);
} else {
this.setState({result: 'Passwords do not match.'})
}
},
writeUserData(email) {
let userId = firebaseApp.auth().currentUser.uid;
firebase.database().ref('users/' + userId + '/').set({
info: {
firstName: 'Jonas',
email: email,
},
currentState: {
currentDay: 0,
currentSeries: 0,
}
});
},
render() {
...
I can't find any good documentation about this. Can someone give me a pointer?
Thanks!

The createUserWithEmailAndPassword() method returns Promise, you have to use it's .then for updating the user details, like this:
firebaseApp.auth()
.createUserWithEmailAndPassword(email, password)
.then( user => {
user.updateProfile({
displayName: 'Jonas'
});
user.updateEmail(email);
})
.catch(error => this.setState({result: error.message}));
Note also the appropriate way to update user' email

I found the solution within Stack Overflow. Much easier than expected: How do you include a username when storing email and password using Firebase (BaaS) in an Android app?
EDIT: I'm using now the onAuthStateChangedmethod instead.
componentDidMount() {
firebaseApp.auth().onAuthStateChanged(user => {
if(user) {
this.writeUserData(user.uid, user.email, this.state.firstName);
}
})
},
writeUserData(userId, email, firstName) {
firebase.database().ref('users/' + userId + '/').set({
info: {
firstName: firstName,
email: email,
},
currentState: {
currentDay: 0,
currentSeries: 0,
}
});
},

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

React and Typescript, createContext where the default value is a Promise

I am learning React and Typescript and following this tutorial (AWS Cognito + React JS Tutorial: Getting Sessions and Logging out (2020) [Cognito Episode #3] https://youtu.be/T7tXYHy0wKE). I am trying to call my new React Context with AccountContext.Provider but the default value is not working because there is a Promise in the function that is being used in the value field, but no Promise in the default value. Can I write something like this?
const AccountContext = createContext<UserAuthTypes>(username: '', password: '' => await new Promise);
I defined AccountContext to have the default value username: '', password: '', but I get this error in the return statement, on the value= line:
TS2739: Type '{ authenticate: (username: string, password: string) => Promise<unknown>; }' is missing the following properties from type 'UserAuthTypes': username, password
I see that authenticate has username and password as the inputs and a Promise as the output.
The code which is based off of this tutorial: AWS Cognito + React JS Tutorial: Getting Sessions and Logging out (2020) [Cognito Episode #3] https://youtu.be/T7tXYHy0wKE):
export interface UserAuthTypes {
username: string;
password: string;
authenticate?: any;
}
export const initialUserAuthState = {
username: '',
password: '',
};
........
const AccountContext = createContext<UserAuthTypes>(initialUserAuthState);
const Account = (props: { children: ReactNode }) => {
const authenticate = async (username: string, password: string) =>
await new Promise((resolve, reject) => {
const user = new CognitoUser({
Username: username,
Pool: UserPool,
});
const authDetails = new AuthenticationDetails({
Username: username,
Password: password,
});
user.authenticateUser(authDetails, {
onSuccess: (data) => {
resolve(data);
},
onFailure: (err) => {
reject(err);
},
newPasswordRequired: (data) => {
resolve(data);
},
});
});
return (
<AccountContext.Provider value={{ authenticate }}>
{props.children}
</AccountContext.Provider>
);
};
export { Account, AccountContext };

React state does not update on time, asynchronous issue

I've build a web application using React that has user registration.
The user data:
{
username: 'string',
password: 'string'
}
exists in a postgresql database and as state.
I'm trying to handle a case where a user is trying to register with a previously used username.
Here's a screenshot of my ui component and react dev tools :
Here's a summary of how I'm handling the case where a username (i'll use the username 'bob' as seen of the screenshot) has already been used:
// Login.js
The 'Register' <input> in the <Login/> component (which contains all the .jsx for the ui form seen on the screen) invokes the userRegister() function via an onClick event. It receives this function as a prop from my <App/> (App.js) component, then the following happens...
// App.js
1 - userRegister() is invoked
2- it invokes 2 functions that check whether the password and username are valid
3- if they are, it invokes a function to check if the username has already been used by checking if it exists as a record in the postgresql database
4- it does this by sending an axios request with the current username + password object as params to the '/users' endpoint, written out in my server.js file
axios({
method: 'get',
url: '/users',
params: { username: 'string', password: 'string'
})
// server.js
1- the request is received by an express route handler at the '/users' endpoint
2- inside the route handler, it invokes an imported getUser() function from postgres.js that communicates directly with the database
const { getData, getUser, addUser } = require('./db/postgres.js');
app.get('/users', (req, res) => {
getUser(req.query)
});
// postgres.js
1- the getUser() function sends the following query to the postgres database
const getUser = (user) => {
return client.query(`select * from users where username='${user.username}'`)
};
after sending that query
this object should reach the client
response.rows: [
{ id: null, username: 'bob', password: 'Pi2#' },
{ id: null, username: 'bob', password: '8*lA' },
{ id: null, username: 'bob', password: '8*lA' },
{ id: null, username: 'bob', password: '7&lL' },
{ id: null, username: 'bob', password: '*lL0' },
{ id: null, username: 'bob', password: 'mM0)' },
{ id: null, username: 'bob', password: '8*fF' },
{ id: null, username: 'bob', password: '7&yY' },
{ id: null, username: 'bob', password: '55$rR' },
{ id: null, username: 'bob', password: '7&yY' },
{ id: null, username: 'bob', password: '8*LAAa' },
{ id: null, username: 'bob', password: '8*lAa' },
{ id: null, username: 'bob', password: '8*lA' }
]
which proves that the username does, in fact already exist in the database
which should, in turn, update the alreadyRegistered state to true and invoke the alert 'user already exists', but, as demonstrated in the top screenshot, alreadyRegistered doesn't update in time
it DOES show the update in Rect devtools a few ms later
I'm fairly certain that the issue is due to state hooks and lifecycle methods being asynchronous, but with that being said how do I update the alreadyRegisted state to true before the code on line 63 executes?
Here are snippets of the code files working together. Let me know if more information/context is needed
Login.js
import React from 'react';
import { Link } from 'react-router-dom';
import { NavBar, Footer } from'../Imports.js';
const Login = ({ userAuth, getUserAuth, userLogin, authorized, userRegister }) => {
console.log('userAuth:', userAuth);
return (
<div id='login'>
<NavBar userAuth={userAuth} getUserAuth={getUserAuth} authorized={authorized} />
<section id='login-main'>
<form>
<input type='text' placeholder='username' onChange={() => {getUserAuth({...userAuth, username: event.target.value})}}/>
<input type='text' placeholder='password'onChange={() => {getUserAuth({...userAuth, password: event.target.value})}}/>
<input type='submit' value='Login' onClick={() => {event.preventDefault(); userLogin()}}/>
<input type='submit' value='Register' onClick={() => {event.preventDefault(); userRegister();}}/>
</form>
</section>
<Footer/>
</div>
)
};
export default Login;
App.js
import React, { useState, useEffect } from 'react';
import Router from './Router.js';
const axios = require('axios');
const App = () => {
// Login/user state
const [userAuth, getUserAuth] = useState({username: '', password: ''});
const [authorized, authorizeUser] = useState(false);
const [alreadyRegistered, userExists] = useState(false);
// new user register
const userRegister = () => { // <- 1)
if (validUsername() && validPassword()) {
checkIfExists();
console.log('alreadyRegistered:', alreadyRegistered);
if (alreadyRegistered) {
alert('user already exists');
} else {
console.log('ERROR: should update alreadyRegistered state to TRUE and invoke ALERT')
}
}
}
// check if username is already registered
const checkIfExists = () => { // <- 3)
axios({
method: 'get',
url: '/users',
params: userAuth
})
.then((res) => {
console.log(res.data.length)
if (res.data.length) {
console.log(res.data.length, 'INNER')
userExists(true);
}
})
.catch((err) => {
throw err;
})
}
};
server.js
// ----- external modules -----
require('dotenv').config();
const express = require('express');
const path = require('path');
const { getData, getUser, addUser } = require('./db/postgres.js');
// check if user exists
app.get('/users', (req, res) => {
console.log('req.body:', req.query)
getUser(req.query)
.then((response) => {
console.log('response.rows:', response.rows);
res.send(response.rows);
})
.catch((err) => {
if (err) {
throw err;
}
});
});
postgres.js
const { Client } = require('pg');
const getUser = (user) => {
return client.query(`select * from users where username='${user.username}'`)
};
Before answering the state question, it's important to know your application as written is vulnerable to SQL injection. Instead of a template string in your postgres.js file, you should use parameterized queries.
As for the value of alreadyRegistered, you're correct that the issue lies in the asynchronous nature of state hooks.
You can use the useEffect function to handle updates to alreadyRegistered like so:
useEffect(() => {
if (alreadyRegistered) {
alert('user already exists');
} else {
// create user...
}
}, [alreadyRegistered]);
You could also avoid tracking alreadyRegistered in state and just call checkIfExists as needed:
// first, remove alreadyRegistered state
const [userAuth, getUserAuth] = useState({username: '', password: ''});
const [authorized, authorizeUser] = useState(false);
// then redefine checkIfExists to return Promise<boolean>
const checkIfExists = async () => {
const res = await axios({
method: 'get',
url: '/users',
params: userAuth
})
return !!res.data.length; // convert fetch result to boolean
}
// then call checkIfExists in userRegister
const userRegister = async () => {
if (validUsername() && validPassword()) {
const alreadyRegistered = await checkIfExists();
if (alreadyRegistered) {
alert('user already exists');
} else {
// handle user creation
}
}
}

How do I fetch a specific result from mongodb

I'm still new to reactjs and mongodb.I am trying to fetch a single record of user and display it on the page. But so far I got no output. I want the page to immediately display the result on load
This is my profile.js:
componentDidMount() {
const user = localStorage.getItem('user')
const userObject = {
username: localStorage.getItem('user'),
};
axios.post('http://localhost:4000/users/getUser', userObject)
.then((res) => {
console.log("hi"+res.data)
const userinfo = res.data
this.setState({
name: userinfo.name,
username: res.username,
email: res.email,
password: res.password,
company: res.company,
position: res.position
})
})
.catch(function (error) {
console.log(error);
})
}
and this is my backend(user.js):
let mongoose = require('mongoose'),
express = require('express'),
router = express.Router(),
user = require('../models/user-schema');
router.post('/getUser', async (req, res) => {
console.log(req.body.username)
const User = await user.find({ name: req.body.username })
if (!User) {
return { status: 'error', error: 'Invalid Login' }
}else {
return res.json({
status: 'ok',
name: User.name,
username: User.username,
email: User.email,
company:User.company,
position: User.position,
level: User.userLevel
})
}
})
user.find({ name: req.body.username }) is going to return an array. So in the code below, all those fields such User.name , User.username, User.email etc are going to be undefined since User = [{name:xxx,username:xxx }]
res.json({
status: 'ok',
name: User.name, // undefined
username: User.username, // undefined
email: User.email, // undefined
company:User.company, // undefined
position: User.position,// undefined
level: User.userLevel // undefined
})
You should use user.findOne({ name: req.body.username }) which will return a single object and then you can access the properties.
Additionally (Personal preference) , one might have multiple users with the same username. To make sure you're retrieving the correct docuemnt, rather use findById().
Instead of this find({}) you can use findOne({}) cause find({}) going to return an array but your are expecting an object that the the problem
Use findOne() instead of find() method, find() method returns an array of objects and findOne() method return a single particular object.
const User = await user.findOne({ name: req.body.username })
and you can pass specific filed which you want to get like
let User = await user.findOne({ name: req.body.username },{username:1, email:1, company:1, position:1, level:1});
if(!User){
return res.status(404).json({ status: 'error', error: 'Invalid Login' });
} else {
return res.status(404).json({status:"ok", ...User});
}

Can I structure document collection of users that sign in using google auth provider?

So I am trying to do an app using React and Firebase and by now everything was fine.
In my app there are a few options to auth a user. Google, Email and Password and also Twitter.
When a user is creating an account using his email I can easily structure how the data will look like in my firestore for that user and I do it that way:
firebase.createUser(
{ email, password },
{
firstName,
lastName,
emailAddress: email,
birthDate: null,
activeStatus: false,
dateJoined: new Date(),
avatarUrl: null,
friends: [],
posts: [
{
content: "I have just joined!",
date: new Date(),
likes: [],
comments: [],
shares: []
}
]
}
).then(() => {
history.push("/login")
})
And the output in my firestore is something like that:
Firestore screen - email
Now I want to structure my data of users that Signed In using Google auth the same way.
This is how my Sign In using Google looks like:
const loginWithGoogle = () => {
firebase.login({ provider: 'google', type: 'popup' })
.then(() => {
localStorage.setItem('authUser', auth)
history.push("/home")
})
.catch(error => {
console.log(error.message)
})
}
And the output in the firestore by the default looks that way: Firestore screen - google
So my question is:
How do I even try to make both auth types collections look the same way? Should I somehow find out that the user is signing in for the first time using google provider and then try to structure that collection or is there a better approach to do that?
Thanks for all responses.
*******************************
For people who are struggling with that:
I did it thanks to #Ajordat response and this is how it looks and works perfectly fine for me:
const loginWithGoogle = () => {
firebase.login({ provider: 'google', type: 'popup' })
.then((user) => {
const fullName = user.profile.displayName.split(" ")
const firstName = fullName[0]
const lastName = fullName[1]
if (user.additionalUserInfo.isNewUser) {
firestore.collection("users").doc(user.user.uid).set({
...user.profile,
firstName,
lastName,
birthDate: null,
activeStatus: false,
dateJoined: new Date(),
friends: [],
posts: [
{
author: firstName + " " + lastName,
content: "I have just joined!",
date: new Date(),
likes: [],
comments: [],
shares: []
}
]
})
} else {
console.log("user exists")
}
localStorage.setItem('authUser', auth)
history.push("/home")
})
.catch(error => {
console.log(error.message)
})
}
You should use the method AdditionalUserInfo.isNewUser() in order to check if it's the first time a user is logging in. It will always be false except that one time, which you need to use to create the user on Firestore as you want it to be.

Resources