King Somto
24 Aug 2021
•
7 min read
In this article, we will be continuing our series of articles on creating a chat app by employing firebase functionality with React-Ionic. You can follow up this article from here. Today we will be working with firebase cloud functions to perform some operations that we wouldn't want to perform on the frontend.
Firebase Cloud Functions is a framework that enables users to run javascript/typescript code triggered by HTTPS requests or events. An easy example would be adding an item to a collection, let's say a collection called cart
, we would like to know if the item added to the cart is still available, the firebase cloud function is able to check the database to see if the item is still available and either return an “Item not available response” or add the item to cart collection.
The flow would be
Firebase cloud functions come in handy for things we would normally like to do on the backend if we were using a traditional stack like MEAN or MERN stack, mostly confidential things we wouldn't want the user to have access to or basically system-intensive processes that can slow the user's devices.
An example of some things you may want to do with firebase functions would be to:
Setting up the firebase cloud functions is straightforward. We would be focusing on running an instance of firebase on our local (firebase emulator) development device (firebase cloud functions is unfortunately not a free service 😭).
We will be installing the firebase functions using node package installer, open the terminal and type
npm install -g firebase-tools
Note you may need to run this command with sudo
or use the admin mode for windows users.
Now type firebase
in the terminal to test if the package has been installed.
Run
firebase init
command
This sets up firebase in the system following the prompts showed by the system, a very detailed explanation on setting up the firebase emulator for firebase cloud function can be found here.
Run firebase emulators:start
in the directory firebase was just set up
The output would be Now we have an instance of firebase emulator running which comes with firestore, firestorage, and of course firebase-cloud functions.
We have done some cool things with out of the box functions by firebase but our app is getting more complex and we would like to add more functionality to some things like our signup process, the current implementation lets users create accounts and adds a username in the user collection our current way of knowing if the username has been used already is okay but we can always make it better, so here we would be using firebase functions, the flow would be when the function is called we first check if the username or email exist if it doesn't we go ahead and call firebase signup functions and add the user to the app/DB, else we return an error response to the user saying the user name or email has been used
We would like to add more laws to create a password on our platform eg we want the passwords to have
The code implementation involves 2 parts the frontend and the backend cloud functions are written in node. I chose Ts for my project set up (Why? Because if it's not hard then why do it?😄), we have to build our project with every change with
npm run build
.
This updates our JS file.
So for those that have previous Ts experience, you would know Ts is basically not running on the system as it is compiled down to JS and then ran on the node V8 engine, so we need to run our build process to create a new JS file.
import * as functions from "firebase-functions";
export const createAnAccount = functions.https.onCall(
async ({ email, password, userName }, context) => {
console.log({email, password, userName})
return {
msg:’Done’
};
});
Breaking the code into smaller pieces
import * as functions from "firebase-functions";
Setting up our import statements to get the firebase functions from the firebase library.
export const createAnAccount = functions.https.onCall(
async ({ email, password, userName }, context) => {
/////empty code here
});
This part of the codebase creates a function that is exposed to our frontend application.
///////
{
console.log({email, password, userName})
return {
msg:’Done’
};
}
///////
This last part of the code logs the param data passed with the context variable.
The second part of this would be to add the needed code in our frontend codebase, this lets us access the local emulator instead of the firebase live server.
For us to use the emulator we have to tell our app to check for the hostname our app is running on, in our test environment you are probably running it on http://localhost:3000 or something similar so our hostname here is localhost
, so we tell our app to initialize with the local instead(not the best idea though but we should have a cleaner iteration later).
// eslint-disable-next-line no-restricted-globals
if (location.hostname === 'localhost') {
firebase.firestore().useEmulator('localhost', 8080);
firebase.functions().useEmulator('localhost', 5001);
firebase
.auth()
.useEmulator('http://localhost:9099/', { disableWarnings: true });
}
We also need to expose our new signup function to all parts of our codebase, go to the config file and append the code below to the end of the file.
export const allFunctions = {
createAnAccount: firebase.functions().httpsCallable('createAnAccount'),
};
Then in our signup.jsx
we change our signup onSubmit function to
onSubmit={async e => {
e.preventDefault()
try {
const {data} = await allFunctions.createAnAccount({
email: inputHold.current.email,
password: inputHold.current.password,
userName: inputHold.current.userName
})
if (data.error || !data.token) {
throw new Error("Error signing up")
}
auth.signInWithCustomToken(data.token)
} catch (error) {
console.log(error)
}
}}
Open your browser and navigate to http://localhost:4000/logs here we would see an interface similar to,
This is the logs interface that updates whenever a function or an event is called or just a simple console.log()
.
Our code above is called whenever a new user is added to the user collection, so let's open up our signup page and signup a new user.
Notice that we are able to log the user data as we specified in our codebase, so everything is set up to start the hard part.
Let us change our signup flow.
From the process above we can see that this has a big potential flaw, here the user object is created before we verify if the username exist or not, this can lead to some users sharing usernames saved because they chose to use a username already in use, this can be changed on the frontend but putting this as a cloud function is cleaner and less prone to hacks.
The flow we would be using now would be
So let's edit our createAccount
cloud function to follow this process.
import * as functions from "firebase-functions";
import * as admin from 'firebase-admin';
admin.initializeApp()
export const createAnAccount = functions.https.onCall(
async ({ email, password, userName }, context) => {
try {
////check if username exist
const usersRef = admin.firestore().collection('users')
const checkUserNameIsUsed = await (await usersRef.where('userName', '==', userName).get())
const checkUEmailIsUsed = await (await usersRef.where('email', '==', email).get())
if (checkUserNameIsUsed.size > 0 || checkUEmailIsUsed.size > 0) {
///user exist with theis username in the collection
return {
error: true,
msg: 'user with username exist'
}
}
const { uid } = await admin.auth().createUser({
email, password
})
///create users collection now
await usersRef.doc(`${uid}`).set({
userName, email
})
const token = await admin.auth().createCustomToken(uid)
console.log({ uid, userName, email,token })
return { uid, userName, email,token }
} catch (error) {
return { error: 'Auth Error' };
}
},
)
Let's break the code down
const usersRef = admin.firestore().collection('users')
const checkUserNameIsUsed = await (await usersRef.where('userName', '==', userName).get())
const checkUEmailIsUsed = await (await usersRef.where('email', '==', email).get())
if (checkUserNameIsUsed.size > 0 || checkUEmailIsUsed.size > 0) {
///user exist with theis username in the collection
return {
error: true,
msg: 'user with username or email exist'
}
}
This part of the code checks if the email or username has been used before and if it has it returns an error.
const { uid } = await admin.auth().createUser({
email, password
})
///create users collection now
await usersRef.doc(`${uid}`).set({
userName, email
})
const token = await admin.auth().createCustomToken(uid)
console.log({ uid, userName, email,token })
return { uid, userName, email,token }
The next part of the code performs the signup and adds the user document to the collection returning the user data needed on the client-side.
Now that's done we can move to scenario 2 which involves password checks on the backend before creating a user, to perform this we would use a simple regex function to perform this operation.
Reviewing the laws we listed earlier
Let's edit the codebase to support this
/////
var strongRegex =/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})(?=.*🩸🩸)/.test(`${password}`)
if (checkUserNameIsUsed.size > 0 || checkUEmailIsUsed.size > 0 || !strongRegex ) {
///user exist with theis username in the collection
return {
error: true,
msg: 'user with username exist'
}
}
////
Firebase functions are very useful, to say the least, we were able to separate the frontend from doing things like validating the signup process for users, also we found out how to use firebase-cloud functions to query Databases and build on top of that. We will be using more firebase functions going further into the series, to run functions like processing messages sent, so stay tuned!
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!