Welcome to the third and final part of this series on React and Firebase. Before working through this tutorial, make sure you go through parts one and two. You can clone the project and start following this tutorial by working on the branch named part-two
The state of the application so far is that anyone can read and write the todo items that are stored in the database. Today we'll add authentication to the project so that only authenticated users can read and write content. Then of course, we'll further restrict what content each user can read and write, resulting in each users having their own items.
01. Setting up firebase
We're going to use two methods of authentication. Users will be able to register/login with email and password or through facebook.
The steps to enable these two methods over at firebase are already covered in my tutorial on authentication with firebase - you would only need to follow heading "03. Authentication With Email And Password" and "06. Authentication with Facebook" after which you may get back here.
At this point, you should have enabled the login methods "Email/password" and "Facebook".
Lastly, change the database rules to the following.
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
Those rules make sure that no unauthorised user can read or write content.
From this point forward ... it's code all the way down.
02. Login with facebook
Let's start by setting up facebook authentication. As you've already seen in apps you've used around the web, users click on a link/button and authentication happens through a popup.
Head over at ./src/fire.js
. There, we'll initialize FacebookAuthProvider
which is made available by the firebase
package.
import firebase from 'firebase';
const config = {
...
};
const fire = firebase.initializeApp(config)
const facebookProvider = new firebase.auth.FacebookAuthProvider();
export { fire, facebookProvider }
In the sixth line, or the second from the bottom, we initialize the provider, then export it, making it available for any other file to import.
Let's do that at ./src/App.js
by adding facebookProvider
as follows:
import { fire, facebookProvider } from './fire';
Now let's create an authentication method
authWithFacebook=()=>{
fire.auth().signInWithPopup(facebookProvider)
.then((result,error) => {
if(error){
console.log('unable to signup with firebase')
} else {
this.setState({authenticated: true })
}
})
}
authWithFacebook
is a random name I chose, the authentication magic is inside it. Actually it should be very familiar if you read Introduction to Authentication with Firebase tutorial.
To test that this works, go ahead and add a link inside the rendered menu
render() {
return (
<BrowserRouter>
...
<ul className="menu">
<li><Link to={'/'}>To do</Link></li>
<li><Link to={'/completed'}>Completed</Link></li>
<li onClick={this.authWithFacebook}>Login with Facebook</li>
</ul>
...
</BrowserRouter>
);
}
If authentication is successful we're adding {authenticated: true}
to the App
component state.
But that's not enough.
As we've already explored in the authentication with firebase tutorial, the auth
method gives us the ability to listen to authentication changes
fire.auth().onAuthStateChanged()
We can make use of it inside the componentWillMount
"lifecycle" component.
03. Accessing data based on authenticated users
The way this works is that when we click on "Login with Facebook" the authentication popup runs. When successful the App
component re-renders. Hence re-running componentWillMount
making it the perfect place for us to update the application state upon authentication status change.
At the moment this is the code we have.
componentWillMount(){
this.itemsRef.on('value', data=> {
this.setState({
items: data.val()
})
})
}
As it stands it does do the job. If no user is authenticated it will still try to get some data but our firebase database rules will prevent access hence data.val()
would return nothing. Then when authenticated, the same code re-requests some data and data.val()
returns our todo items.
But this would be a nightmare, with the above configuration every user that signs in would have access to the same data, just as before we added authentication.
We want to store user data in an object which only the user can access. Lets re-write some code:
class App extends Component {
state = {
items: {},
authenticated: false,
loading: true
}
itemsRef = '';
componentWillMount(){
this.removeAuthListener = fire.auth().onAuthStateChanged(user=>{
if(user){
this.itemsRef = fire.database().ref(`items/${user.uid}`)
this.itemsRef.on('value', data => {
this.setState({
authenticated: true,
items: data.val(),
loading: false
})
})
} else {
this.setState({
authenticated: false,
loading: false
})
}
})
}
...
We're basically doing the same thing but with a slight modification. The most notable thing is that we are no longer writing to an items
object in the database but items/${user.uid}
. The uid
is provided by onAuthStateChanged()
.
Also, note that we are changing the value of this.itemsRef
from within onAuthStateChanged()
so that the user's unique ID is available on the component state level.
Visually we are carving a slot in the non-sql database that looks something like this
{
"items" : {
"wINebMADEUPCfbvJUSTINZpvCASE1qVRiI2" : {
"-L1Of70brslQ_JIg922l" : {
"completed" : false,
"item" : "item one"
}
}
}
}
Inside items
we have user.uid
and inside that we have the user's items. This way each user now has access only to their own data.
04. Login out
As I've already covered in the authenticating with firebase tutorial, logging out is very easy:
logOut=()=>{
fire.auth().signOut().then((user)=> {
this.setState({items:null})
})
}
Then we simply have a button which fires the above method upon click.
05. UI design
Before we move on to authenticating with email and password, let's build a better UI. We now have all the means to give users better UI based on whether they are logged in or not.
Of course the code is going to be in github, so here's a quick overview
In our initial state of the App
component we have a property loading: true
, Then in the componentWillMount
we set loading: false
indicating that no matter what we do next, the component has mounted. Hence, we are able to render conditional code.
render() {
if (this.state.loading) {
return (<h3>Loading</h3>)
}
return ( ... )
}
If the condition is true, the h3
renders on the page. When that's no longer true, the second return statement runs - rendering the code we always had.
We do the same thing to determine whether a user is logged in or not. We have authenticated
boolian in our state, which switches from false
to true
based on authentication status
At the moment, we are already loading ItemsComponent
in part one of this series. We are now going to create another component for the menu. But before we do that, let's write the code we want to return in the App
component.
import Menu from './components/Menu';
...
return (
<BrowserRouter>
<div className="wrap">
<h2>A simple todo app</h2>
<Menu
logOut={this.logOut}
authenticated={this.state.authenticated}
authWithFacebook={this.authWithFacebook}
/>
In order to keep the code clean, we moved the links into their own component. Here is what we are doing there.
import React from 'react';
import { Link } from 'react-router-dom';
const Menu = (props) => {
if(props.authenticated){
return (
<ul className="menu">
<li><Link to={'/'}>To do</Link></li>
<li><Link to={'/completed'}>Completed</Link></li>
<li className="logOut" onClick={ props.logOut }>sign out</li>
</ul>
);
} else {
return (
<div className="auth">
<p className="facebook" onClick={props.authWithFacebook}>
Facebook
</p>
<form>
<label> Email <input type="email" /> </label>
<label> Password <input type="password" /> </label>
</form>
</div>
);
}
}
export default Menu;
Simple, We check if user is authenticated. If no user is authenticated, we render the facebook button (which executes authWithFacebook
which we've created above), We also display a form. The end result (with css included, which you're able to get in the repository) looks like this
Authenticating with Email and Password
Lets create an EmailAndPasswordAuthentication
in our App
Component.
EmailAndPasswordAuthentication=(e)=>{
e.preventDefault()
const email = this.emailInput.value;
const password = this.passwordInput.value;
fire.auth().fetchProvidersForEmail(email)
.then(provider => {
if(provider.length === 0){
return fire.auth().createUserWithEmailAndPassword(email, password)
}else if (provider.indexOf("password") === -1) {
console.log("you already have an account with " + provider[0] )
} else {
return fire.auth().signInWithEmailAndPassword(email, password)
}
})
}
First we prevent the form from running, then get the form input values. Then we run fetchProvidersForEmail
by providing it with the email received. That method checks firebase authentication to see if a user with the provided email exists. We therefore use an if
statement to act appropriately. Firstly, we say, if nothing is returned, then create a user, with the email and password provided. If this is the case, if the email is new, then a user is created and automatically logged in.
In the second if
statement we check if an array with the element of password
doesn't exist! This is how it works, when users sign in with, say, facebook, their email is stored in firebase. So if someone tries to register with the same email address, provider
returns ["facebook.com"]
.
Final condition (else
) returns an array of ["password"]
. I guess that's how firebase chose to tell us whether a user exists and from which provider.
The form
Remember that the form is located at ./src/components/Menu.js
, we render it at ./src/App.js
like so
<Menu
...
emailInput={el => this.emailInput = el}
passwordInput={el => this.passwordInput = el}
EmailAndPasswordAuthentication={this.EmailAndPasswordAuthentication}
/>
emailInput
and passwordInput
will take the element passed to it and attach it to a local variable within the App
component (this.emailInput
and this.passwordInput
) and of course this.EmailAndPasswordAuthentication
refers to the method we just created.
Now in ./src/components/Menu.js
the form looks like this
return (
<div className="auth">
<p className="facebook" onClick={props.authWithFacebook}>Facebook</p>
<form
onSubmit={(event) => {props.EmailAndPasswordAuthentication(event) }}
>
<label>
Email <input type="email" ref={ props.emailInput} />
</label>
<label>
Password <input type="password" ref={ props.passwordInput} />
</label>
<input type="submit" value="Register/Login" />
</form>
</div>
);
ref
sort of hands the element to the props. So in the App
component, this.emailInput
would return the same thing as document.querySelector('input[type="email"]')
.
Conclusion
That's it. We now are able to sign users in with facebook, or email and password. And with that, this mini-project spanned across three posts is complete. The code from this tutorial is at the same github repository in the branch named part-three.
Top comments (0)