Losing data properties from firebase realtime database user collection - reactjs

I know this question has been asked in other contexts. I've read those answers. I've tried the answers. I've read through the firebase docs. So, far it's not helping. I'm new to firebase. Learning how to use the product by going through a tutorial on using React and Firebase together. Building an authentication/authorization module for users. Can successfully register/SignUp a user and see the results in both the authorization module and the realtime database in the firebase project overview. When the user Signs Out, a check of the RDb confirms that all the data properties persist. When the user Signs In (email/password) again, a check of the RDb shows that only the uid and the email address persist in the RDb. The non-auth properties of the user like username and role become empty strings or objects, present but not populated. I have done considerable reading of the firebase docs and debugging of the code, but I cannot determine why this happens and therefore cannot determine how to fix it.
Here are some code segments:
First the firebase module:
import app from 'firebase/app'
import 'firebase/auth'
import 'firebase/database'
... set up the config ...
// set the config constant to the appropriate value
const config = process.env.NODE_ENV === 'production' ? prodConfig : devConfig
class Firebase {
constructor() {
// initialize the app using the Firebase config to instatiate all of the firebase services
app.initializeApp(config)
// instantiate the initial auth state within the app
this.auth = app.auth() // Gets the Auth service for the current app
// instantiate the database within the app
this.db = app.database() // Gets the Database service for the current app
console.log('this.db ', this.db)
// Social media login providers (authorization methods within Firebase)
this.googleProvider = new app.auth.GoogleAuthProvider()
this.facebookProvider = new app.auth.FacebookAuthProvider()
this.twitterProvider = new app.auth.TwitterAuthProvider()
}
... the comments are mostly for my benefit ...
// ***** Firebase Auth API *****
// create user with email and password (equate doCreate... with Firebase createUser...)
doCreateUserWithEmailAndPassword = ( email, password ) => this.auth.createUserWithEmailAndPassword( email, password )
// ***** SignIn with email ***** (equate doSignIn... with Firebase signIn...)
doSignInWithEmailAndPassword = ( email, password ) => this.auth.signInWithEmailAndPassword( email, password )
// ***** SignIn with facebook ***** (equate as above)
doSignInWithFacebook = () => this.auth.signInWithPopup( this.facebookProvider )
// ***** SignIn with google ***** (equate as above)
doSignInWithGoogle = () => this.auth.signInWithPopup( this.googleProvider )
// ***** SignIn with twitter ***** (equate as above)
doSignInWithTwitter = () => this.auth.signInWithPopup( this.twitterProvider )
// ***** SignOut ***** (equate as above)
doSignOut = () => this.auth.signOut()
// ***** Password Reset ***** (equate as above)
doPasswordReset = email => this.auth.sendPasswordResetEmail( email )
// ***** Password Update ***** (equate as above)
doPasswordUpdate = password => this.auth.currentUser.updatePassword( password )
// ***** User API *****
// set the current user id
user = uid => this.db.ref(`users/${uid}`)
// set the reference to the users collection in the firebase database
users = () =>this.db.ref('users')
// ***** Merge Auth and DB User API *****
... big block of comments to me ...
onAuthUserListener = (next, fallback) =>
this.auth.onAuthStateChanged(authUser => {
if ( authUser ) {
this.user(authUser.uid)
.once('value')
.then(snapshot => {
const dbUser = snapshot.val()
console.log('this.user ',this.user)
console.log('dbUser ',dbUser)
// default empty roles
if ( !dbUser.roles ) {
dbUser.roles = {}
}
// merge auth and db user
this.db.ref(`users/${this.user}`).set({
uid: authUser.uid,
email: authUser.email,
username: authUser.username,
roles: authUser.roles,
...dbUser,
})
console.log('firebase.js authUser ', authUser)
next(authUser)
})
} else { // there is no authUser
fallback()
}
})
}
export default Firebase
Here is the Sign UP component:
// index.js - SignUp
// the entry point for the SignUp component
import React, { Component } from 'react'
import { Link, withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import { Typography, Input, Checkbox, FormLabel, Button } from '#material-ui/core'
import { withFirebase } from '../Firebase'
import * as ROUTES from '../../constants/routes'
import * as ROLES from '../../constants/roles'
import '../../styles/auth.css'
const SignUpPage = () => (
<div id='wrapper' className='signup-page'>
<SignUpForm />
</div>
)
// initialize the state of the component using destructuring
// allows INITIAL_STATE to be reset after successful SignUp
const INITIAL_STATE = {
username: '',
email: '',
passwordOne: '',
passwordTwo: '',
isAdmin: false,
error: null,
}
class SignUpFormBase extends Component {
constructor(props) {
super(props)
// spread operator (...) spreads out to reach all properties individually
this.state = { ...INITIAL_STATE }
}
onSubmit = event => {
// get necessary info from this.state to pass to the Firebase authentication API
const { username, email, passwordOne, isAdmin } = this.state
const roles = {}
if ( isAdmin ) { roles[ROLES.ADMIN] = ROLES.ADMIN } // set roles if the admin checkbox is checked
this.props.firebase
// create a user (limited access) in the authentication database
.doCreateUserWithEmailAndPassword( email, passwordOne )
// successful
.then( authUser => {
// create a user in Firebase realtime database -- this is where you manage user properties
// because in the firebase auth module, users cannot be manipulated.
console.log('signup authUser ',authUser)
return this.props.firebase
.user(authUser.user.uid) // use the authUser.uid to:
.set({ username, email, roles }) // write username, email & roles to the rdb
})
.then(() => {
// update state and redirect to Home page
this.setState({ ...INITIAL_STATE })
this.props.history.push(ROUTES.HOME)
})
// error - setState, error (if something is wrong)
.catch(error => {
this.setState({ error })
})
// prevent default behavior (a reload of the browser)
event.preventDefault()
}
onChange = event => {
// dynamically set state properties when they change, based on which input call is executed
// each <input> element (in the return) operates on a different property of state (according to value)
this.setState({ [event.target.name]: event.target.value })
}
onChangeCheckbox = event => {
this.setState({ [event.target.name]: event.target.checked })
}
render() {
// parse each of the values from current state
const {
username,
email,
passwordOne,
passwordTwo,
isAdmin,
error
} = this.state
// list of invalid conditions for which to check (validation of form elements)
const isInvalid =
passwordOne !== passwordTwo ||
passwordOne === '' ||
email === '' ||
username === ''
return (
// the input form -- with fields (username, email, passwordOne, passwordTwo)
<div className='container'>
<Typography
variant='h6'
align = 'center'
className='item'
>
Sign Up Page
</Typography>
<br />
<form className='signup-form item' onSubmit={ this.onSubmit }>
<Input
className='item'
name='username'
value={username}
onChange={this.onChange}
type='text'
placeholder='Full Name'
/>
... input email, password, confirm password, checkbox to designate Admin ...
<Button
variant='contained'
className='item btn btn-secondary'
disabled={ isInvalid }
type='submit'
>
Sign Up
</Button>
{/* if there is an error (a default Firebase property), render the error message */}
{error && <p>{ error.message }</p>}
</form>
</div>
)
}
}
const SignUpLink = () => (
<Typography
variant = 'body1'
align = 'center'
className = 'item'
>
Don't have an account? <Link to={ ROUTES.SIGN_UP }>Sign Up</Link>
</Typography>
)
const SignUpForm = compose(withRouter, withFirebase)(SignUpFormBase)
export default SignUpPage
export { SignUpForm, SignUpLink }
Here is the Sign IN (email/password) component:
// SignInEmail.js - SignIn
// the email/password SignIn component
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import { Typography, Input } from '#material-ui/core'
import Button from 'react-bootstrap/Button'
import { withFirebase } from '../Firebase'
import * as ROUTES from '../../constants/routes'
// initialize the state of the component using destructuring
// allows INITIAL_STATE to be reset after successful SignUp
const INITIAL_STATE = {
username: '',
email: '',
password: '',
roles: {},
error: null,
}
// ======================================
// ***** signin with email/password *****
// ======================================
class SignInEmailBase extends Component {
constructor(props) {
super(props)
// spread operator (...) spreads out to reach all properties individually
this.state = {
...INITIAL_STATE
}
}
onSubmit = event => {
// get necessary info from this.state to pass to the Firebase authentication API
// const { username, email, password, roles } = this.state
const { username, email, password, roles } = this.state
this.props.firebase
// execute SignIn function (create a user)
.doSignInWithEmailAndPassword( email, password )
// successful
.then(authUser => {
// create a user in Firebase Realtime database
console.log('signin authUser ',authUser)
return this.props.firebase
.user(authUser.user.uid)
.set({ username, email, roles })
})
.then(() => {
// update state and redirect to Home page
this.setState({ ...INITIAL_STATE })
this.props.history.push(ROUTES.HOME)
})
// error - setState, error (if something is wrong)
.catch(error => {
this.setState({
error
})
})
// prevent default behavior (a reload of the browser)
event.preventDefault()
}
onChange = event => {
// dynamically set state properties when they change, based on which input call is executed
// each <input> element (in the return) operates on a different property of state (according to value)
this.setState({ [event.target.name]: event.target.value })
}
render() {
// parse each of the values from current state
const { email, password, error } = this.state
// list of invalid conditions for which to check (validation of form elements)
const isInvalid = password === '' || email === ''
return (
// the input form -- with fields (username, email, passwordOne, passwordTwo)
<div className = 'container signin-page'>
<Typography
variant = 'h6'
align = 'center'
className = 'item'
>
Sign In Page
</Typography>
<br />
<form className = 'item email-form' onSubmit = { this.onSubmit} >
... input email, password...
{ /* disable the button if the form is invalid -- see isInvalid above */ }
<Button
className = 'item btn btn-secondary'
type = 'submit'
disabled = { isInvalid }
>
SIGN IN WITH EMAIL
</Button>
{ /* if there is an error (a default Firebase property), render the error message */ }
{ error && <p> { error.message } </p> }
</form>
</div>
)
}
}
const SignInEmail = compose(withRouter, withFirebase, )(SignInEmailBase)
export default SignInEmail
Here is the screenshot of the browser inspect and the screenshot of the Realtime Database entry after Sign UP
Here is the same user after sign out and sign in -- no other data manipulation
I sure could use some help. Thanks.

I sort of stumbled on to the answer to this myself. As I was reviewing my code and comparing the SignUp component with the SignIn component I realized that looked out of place within the SignIn component. It was this block within the onSubmit block:
this.props.firebase
// execute SignIn function (create a user)
.doSignInWithEmailAndPassword( email, password )
// successful
.then(authUser => {
// create a user in Firebase Realtime database
return this.props.firebase
.user(authUser.user.uid)
.set({ username, email, roles })
})
.then(() => {
// update state and redirect to Home page
this.setState({ ...INITIAL_STATE })
this.props.history.push(ROUTES.HOME)
})
// error - setState, error (if something is wrong)
.catch(error => {
this.setState({
error
})
})
It is identical to a similar block in the signUp component. It is necessary with the sign Up because I am creating a user with a signUp and the information that was disappearing in the signIn component is being entered in the signUp component so it is temporarily available in the authUser for transfer to the RdB. That information is not being entered in the signIn component so it is not available to transfer to the Realtime Database and hence (because of the way firebase works in syncing users between the auth module and the RdB) those properties were being over written with empty strings or empty objects. Further because the function is a signIn function there is no need at signIn time to update the RdB so the solution is to eliminate this block of code in the in the signIn component.
.then(authUser => {
// create a user in Firebase Realtime database
return this.props.firebase
.user(authUser.user.uid)
.set({ username, email, roles })
})
Sorry if I was a bother. But I did want to post that I figured it out.

Related

React state setter fails to set the state of user object in firebase authentication

Hey guys I am trying to use Firebase Authentication in my React App and I am doing this by setting up an context. I have checked a lot of questions regarding this and I wasn't able to get a satisfactory answer and so I am asking this here. Everything works except for the signInWithEmailAndPassword .My logic is that when the user signs in and the user has verified its email then take him to another page.
Here in the context whenever the user object changes the onAuthStateChanged gets trigger and there I am updating the user object.But then to make this sign in work I have to click on the sign in button twice and then the page gets redirected.
When the sign in button is triggered the first time , the setUser() doesnt update the user object quickly.
Here is the snippet of code for the context I have set up
const userAuthContext = createContext();
//Use the created context
export function useUserAuth() {
return useContext(userAuthContext);
}
export function UserAuthContextProvider({ children }) {
const [user, setUser] = useState({});
const [loading, setLoading] = useState(true);
// Sign in a user with email and password
function logIn(email, password) {
return signInWithEmailAndPassword(auth, email, password);
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
console.log(currentUser);
setUser(currentUser);
setLoading(false);
console.log("User is " , user);
});
const value = {
user,
logIn,
}
return (
<userAuthContext.Provider
value={value}
>
{!loading && children}
</userAuthContext.Provider>
);
}
And this is the login.js where I check that if the user has verified its email then I redirect to another page
export default function LoginForm() {
const { register, setError ,formState, handleSubmit } = useForm();
const { user, logIn } = useUserAuth();
const onSubmit = async(data) => {
console.log(data);
try{
await logIn(data.email,data.password);
}catch (error) {
setError("email", {
type: "manual",
message: error.message,
});
}
console.log(user);
if(user && user.emailVerified===true)
{
navigate('/studDash');
}else if(user){
setError("email", {
type: "manual",
message: "Email Not Verified !! Check your mail for verification mail !!",
});
}
}
return (
<div>
//Login Page Div is here
</div>
);
}
I have tried to do console.log(user) at onAuthStateChanged in the context and in the onSubmit function here is the screenshot of it. This is the console when I click the sign in button once , if i click signin button again then the user gets directed to next page successfully.

Questions about designing a "settings" page on which users can edit their profiles

I am using React, Redux, and Firebase to build a website. Users can edit their profiles on the "settings" page. My design objective and approaches are the following:
Design objective:
Users' existing profile information (profile picture, name, and etc.) is displayed once the page is loaded.
Approach:
I set all users' data fields to empty strings/array in the state of the "settings" component and use mapStateToProps to pass into the component existing user profile and set the state using componentDidMount. Each data field has its own handleXXXChange so that if users do not change a data field, its existing value will be passed as its existing value to the Redux action object called editProfile that updates the user's document in Firebase.
Problems I ran into:
1. componentDidMount is only called once during the lifecycle of a component, so if the user refreshes the page all the fields in the state will be set to "undefined."
2. It is weird that when I clicked the "Upload Profile Image" button, the page "reloaded" itself and there was a question mark appearing at the end of the URL. Indeed, a window did pop up for users to select an image file, but the selected image failed to be uploaded to firebase storage. It is very weird because the exact same uploadImage and openFiles methods worked before I added componentDidMount to achieve the objective mentioned above.
Thank you so much for your patience and help!
"Settings" component
class Settings extends Component {
state = {
firstname: "",
lastname: "",
gender: "",
institution: "",
role: "",
selfdescription: "",
areasofinterest: "",
email: "",
profileimage: ""
};
componentDidMount() {
const { profile, auth } = this.props;
const id = auth.uid;
const image = firebase.storage().ref(`images/`+id);
image.getDownloadURL().then((url) => this.setState({ profileimage: url }));
this.setState({
firstname: profile.first_name,
lastname: profile.last_name,
gender: profile.gender,
institution: profile.institution,
role: profile.role,
selfdescription: profile.self_description,
areasofinterest: profile.areas_of_interest,
email: profile.email
}
)
}
handleSubmit = (e) => {
e.preventDefault();
const { auth } = this.props;
this.props.editProfile(this.state, auth.uid, this.props.history);
}
uploadImage = (e) => {
e.preventDefault();
const image = e.target.files[0];
const { auth } = this.props;
firebase.storage().ref('images/'+auth.uid).put(image);
}
openFiles = (e) => {
const input = document.getElementById("imageinput");
input.click();
}
handleFirstNameChange = (e) => {
e.preventDefault();
this.setState({ firstname: document.getElementById("fn").value });
}
handleLastNameChange = (e) => {
e.preventDefault();
this.setState({ lastname : document.getElementById("ln").value });
}
......
render(){
const { auth, profile } = this.props;
return(
<form>
<img src={firebase.storage().ref(`images/`+auth.uid).getDownloadURL()}></img>
<input type="file" id="imageinput" hidden="hidden" onChange={this.uploadImage}></input>
<button onClick={this.openFiles}>Upload Profile Image</button>
<span>First Name</span>
<input id="fn" type="text" placeholder={profile.first_name} onChange={this.handleFirstNameChange}></input>
<span><br/>Last Name</span>
<input id="ln" placeholder={profile.last_name} onChange={this.handleLastNameChange}></input>
......
<button onClick={this.handleSubmit}>Save</button>
</form>
)
}
}
const mapStateToProps = (state) => {
return {
profile: state.firebase.profile,
auth: state.firebase.auth
}
}
export default connect(mapStateToProps, {editProfile} )(withRouter(Settings));
"editProfile" Redux action object (I'm pretty sure this part is fine but just put it up to make my code more understandable):
export const editProfile = (credentials, userid, history) => (dispatch, getState) => {
const profile = getState().firebase.profile;
firebase.firestore().collection("users").doc(userid).update({
first_name: credentials.firstname,
last_name: credentials.lastname,
gender: credentials.gender,
institution: credentials.institution,
role: credentials.role,
self_description: credentials.selfdescription,
areas_of_interest: credentials.areasofinterest,
email: credentials.email
})
.then(() => {
dispatch({ type: EDIT_SUCCESS });
history.push(profile.profile_url);
})
}

How to pass on my error messages on my Login page

I have a handleLogin function in an auth.js file, I would like to retrieve the errors received in the .catch and transmit them in my Alert.js file in order to display the errors in an alert in Login.js...
This is my handleLogin function on auth.js
export const handleLogin = async ({ email, password }) => {
const user = await ooth.authenticate('local', 'login', {
username: email,
password: password,
}).catch(e => {
alert(e.message)
});
await navigate(`/app/profile`);
if (user) {
return setUser({
id: user._id,
username: `jovaan`,
name: `Jovan`,
email: user.local.email,
avatar: `3`,
telephone: `0788962157`,
bio: `I'm a front-end dev`
})
}
return false
}
My Alert.js
import React from "react";
import { Alert } from "shards-react";
export default class DismissibleAlert extends React.Component {
constructor(props) {
super(props);
this.dismiss = this.dismiss.bind(this);
this.state = { visible: true, message: "Message par défaut" };
}
render() {
return (
<Alert dismissible={this.dismiss} open={this.state.visible} theme="success">
{this.message()}
</Alert>
);
}
dismiss() {
this.setState({ visible: false });
}
message() {
return this.state.message
}
}
I imported my Alert.js into my Login page, so I currently have the default message
You will need to somehow pass the results of the failed login via props wherever you have the <DismissableAlert /> in your code, which I'm presuming would be at /login or similar. My guess would be to do something like this, by passing the error via the 'state' option that navigate provides:
const error = null;
const user = await ooth.authenticate('local', 'login', {
username: email,
password: password,
}).catch(e => {
error = e
console.err(e.message)
});
if(error) {
await navigate(`/login`, {
state: { error: error },
});
}
Then in your LoginPage component (or whatever it is called), you should then have access to the result of the error as props that you can then pass down to the Alert:
render() {
const {error} = this.props
return(
<div>
<LoginForm />
{error &&
<DismissibleAlert message={error.message} />
}
</div>
)
}
Or something along those lines, depending on your components. I notice you have a default message in the DismissableAlert, so this is just an example to show that you could pass the error in to use it to show a relevant message.

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.

Why can't I set React component state in a life Cycle method

I am using the following Container code to show a SnackBar if users email already exists with another account, If i remove this line of code (this.setState({ open: true })) The snack bar shows up fine by setting the state using a button :
class HomeContainer extends Component {
static propTypes = {
authError: PropTypes.string,
removeError: PropTypes.func.isRequired
}
static contextTypes = {
router: PropTypes.object.isRequired
}
constructor (props) {
super(props)
this.state = {
open: false
}
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// the following componetdid mount function is only required when using SigninwithRedirect Method - remove when using SigninWithPopup Method
componentDidMount () {
firebaseAuth.getRedirectResult().then((authUser) => {
// The signed-in user info.
console.log('User Data from Oauth Redirect: ', authUser)
const userData = authUser.user.providerData[0]
const userInfo = formatUserInfo(userData.displayName, userData.photoURL, userData.email, authUser.user.uid)
return (this.props.fetchingUserSuccess(authUser.user.uid, userInfo))
})
.then((user) => {
return saveUser((user.user))
})
.then((user) => {
return (this.props.authUser(user.uid))
}).then((user) => {
this.context.router.replace('feed')
}).catch(function (error) {
// Handle Errors here.
const errorCode = error.code
const errorMessage = error.message
// The email of the user's account used.
const email = error.email
// The firebase.auth.AuthCredential type that was used.
const credential = error.credential
// if error is that there is already an account with that email
if (error.code === 'auth/account-exists-with-different-credential') {
// console.log(errorMessage)
firebaseAuth.fetchProvidersForEmail(email).then(function (providers) {
// If the user has several providers,
// the first provider in the list will be the "recommended" provider to use.
console.log('A account already exists with this email, Use this to sign in: ', providers[0])
if (providers[0] === 'google.com') {
this.setState({ open: true })
}
})
}
})
}
handleRequestClose = () => {
this.setState({
open: false
})
}
// /////////////////////////////////////////////////////////////////////////////////////////////////////
render () {
return (
<div>
<Snackbar
open={this.state.open}
message= 'A account already exists with this email'
autoHideDuration={4000}
onRequestClose={this.handleRequestClose.bind(this)}/>
<Home />
</div>
)
}
}
I was using older function syntax at function (providers) & function (error) . switched them to arrow functions and the outside scope of this went down is defined. :)

Resources