Show Menu
Cheatography

Firebase-Express Template with User Auth Cheat Sheet by

Firebase-Express backend functions - written as in Node.js May be published to firebase or use local firebase server utility for testing.

Overview

This "­tem­pla­te" allows you to set up all the necessary code for a Google Firebase (Node.j­s-­like) server using their "­Fun­cti­ons­" option.

For initial develo­pme­nt/­tes­ting, you can use the locally installed firebase server tool and it will update your database. Once you are ready for produc­tion, simply deploy your functions to Firebase which will return an endpoint to use.

This template focuses on the code snippet functions needed to have user signup, login, and authen­tic­ation.

Basically Firebase will have the functions operate like a backend server being hosted on their site. The database resides in their "­Cloud Firest­ore­".

Once this project is running, you can now focus on creating all of the other functions for your project database.

Creating a startup enviro­nment

create project on firebase console
register your new app: <p­roj­ect­-na­me>
Take note of the SDK/config data
npm install -g fireba­se-­tools
Global install of CLI
firebase login
Login to Firebase (may launch browser window)
mkdir <p­roj­ect­-na­me>
Create your project directory
cd <p­roj­ect­-na­me>
Move into your project directory
firebase init
Choose "­fun­cti­ons­"
JavaScript or Typescript
Choose your coding language prefer­ence. (JavaS­cript)
eslint ?
Do you wish to use eslint? Suggest - 'No"
code .
run VS Code
Add two folders under the "­fun­cti­ons­" folder
util, handlers
Copy the code snippets to the util folder each in its own .js file.
admin, config, valida­tors, and fbAuth
Copy the users code to user.js file in the handlers folder
users
Replace the contents of index.js with the code listed here.
index.js
-
-
firebase serve
CLI command to test code on your local host.
firebase deploy
For a list of comands for the CLI, see the npm webpage: Firebase CLI

Firebase Functions vs. Node.js server

Firebase "­Fun­cti­ons­" are written in Node.js format, so for example you will have the standard request and response options with a callback:

expor­ts.h­el­loworld = functi­ons.ht­tps.on­Req­uest( (request, response) => {
respon­se.s­en­d('­Hello world!') });

However it is not necessary to code the full blown Node.js server. Once you have written your function handlers, you will deploy them up to firebase and then receive valid endpoi­nt(s) that will serve your the response to your request. - In the example above, you would receive an endpoint similar to the following:

https­://­<...fi­rebase assigned server...>.<...p­roject name...>-­<...some random letters ...>.c­lou­dfu­nct­ion­s.n­et/­hel­loW­orld

This endpoint would then return "­Hello world!­"

Utility - Admin

// Import admin
const admn = require('firebase-admin')

// Initialize app
admin.initializeApp

// Setup a 'shortcut' variable to save keystrokes
const db = admin.firestore()

// Export
module.exports = { admin, db }
This utility file setups up the import statem­ents, app initia­liz­ation, and a short cut variable.

Utility - Config

const config = {
  apiKey: "<...your key goes here...>",
  authDomain: "<...project id...>.firebaseapp.com",
  databaseURL: "https://<...project id...>-default-rtdb.firebaseio.com",
  projectId: "<...project id...>",
  storageBucket: "<...project id...>.appspot.com",
  messagingSenderId: "< ...messaging sender id...>",
  appId: "<...app id...>"
  };

  module.exports = { config }
Replace all "­fie­lds­" with the actual data.
The easiest way is to simply copy the entire block from the firebase screen and paste it in place.
This inform­ation is found under the project overview, in the Firebase SDK Snippet under your registered "­app­".

Utility - fbAuth

const { admin, db } = require('./admin')

module.exports = (req, res, next) => {
    let idToken;
    if (req.headers.authorization && req.headers.authorization.startsWith("Bearer ")) {
      idToken = req.headers.authorization.split("Bearer ")[1];
    } else {
      console.error("No token found");
      return res.status(403).json({error: "Unauthorized"});
    }

    admin.auth().verifyIdToken(idToken)
      .then((decodedToken) => {
        req.user = decodedToken;
        return db.collection("users")
            .where("userId", "==", req.user.uid)
            .limit(1)
            .get();
      })
      .then((data) => {
        req.user.userHandle = data.docs[0].data().userHandle;
        return next();
      })
      .catch((err) =>{
        console.error("Error while verifying token", err);
        return res.status(403).json(err);
      });
  };
This code snippet allows for security based routes.
Simply place the import in the code with your Express import.

Since Express allows for chained middl­eware calls, we add a callback to FBAuth between the route endpoint and the function handler.

(See "­Pro­tected Routes­" section below.)

Protected Routes (Using "­fbA­uth­")

// -------------------- Route without authentication -------------------- //
app.post('/signup', signup);

// -------------------- Route with authentication -------------------- //
// (e.g. requires a valid token)
app.post('/user', FBAuth, addUserDetails);
For any route that you wish to be protected (e.g. only valid/­log­ged-in users can see or use), simply add the callback to FBAuth between the route and the callback function that handles the route.

Of course the imports will require fireb­ase­-fu­nct­ions, express, and the fbAuth file.
For a more complete example - see the Index.js section.

Utility - Basic "­Use­r" validators

// --------------- HELPER FUNCTIONS ---------------

// ----- Email validation using a regex expression -----
const isEmail = (email) => {
  const regEx = /(([<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
  if (email.match(regEx)) return true
    else return false
}

// ----- Non-blank/Required entry -----
const isEmpty = (string) => {
  if (string.trim() === '') return true;
  else return false;
};

// --------------- SIGNUP and LOGIN Functions ---------------

// ---------- Validate "SignUp" fields: userid, password, confirm password, & email
exports.validateSignupData = (data) => {
  const errors = {}

  // validate email
  if (isEmpty(data.email)) {
    errors.email = "Must not be empty"
  } else if (!isEmail(data.email)) {
    errors.email = "Must be a valid email address"
  }

  // validate password
  if (isEmpty(data.password)) errors.password = "Must not be empty";

  // validate confirmpassword
  if (data.confirmPassword !== data.password) errors.confirmPassword = "Passwords must match";

  // validate userHandle
  if (isEmpty(data.userHandle)) errors.userHandle = "Must not be empty";

  return {
    errors,
    valid: Object.keys(errors).length === 0 ? true : false
  }
}

// ---------- Validate "Login" fields: email & password
exports.validateLoginData = (data) => {
  const errors = {}

  // validate email
  if (isEmpty(data.email)) errors.email = "Must not be empty"
  // validate password
  if (isEmpty(data.password)) errors.password = "Must not be empty";

  return {
    errors,
    valid: Object.keys(errors).length === 0 ? true : false
  }
}

exports.reduceUserDetails = (data) => {
  let userDetails = {}

  // strip out empty string fields so that they are not added
  // to the document
  if(!isEmpty(data.firstName.trim())) userDetails.firstName = data.firstName
  if(!isEmpty(data.lastName.trim())) userDetails.lastName = data.lastName
  if(userDetails.length > 0 ){
    userDetails.updatedAt = new Date().toISOString()
  }
  return userDetails
}
This code section validates the "­fie­lds­" for non-blank and correct formatting prior to sending them to the Firebase server for authen­tic­ation.
 

Users Handler

// Import Admin
const { admin, db } = require ('../util/admin')
const { config } = require("../util/config")
const { v4: uuidv4 } = require('uuid')

const firebase = require('firebase')
firebase.initializeApp(config)

const { validateSignupData,
        validateLoginData,
        reduceUserDetails
      } = require('../util/validators')

// --------------------- SIGNUP ------------------- //
exports.signup = (req, res) => {
    const newUser = {
      email: req.body.email,
      password: req.body.password,
      confirmPassword: req.body.confirmPassword,
      userHandle: req.body.userHandle,
      firstName: req.body.firstName,
      lastName: req.body.lastName,
    };

    const { valid, errors } = validateSignupData(newUser)
    if(!valid) return res.status(400).json(errors)

    const noImg = 'no-img.png'

    let token;
    let userId;

    db.doc(/users/${newUser.userHandle}).get()
      .then((doc) => {
        if (doc.exists) {
          return res.status(400).json({userHandle: "This handle is already taken."});
        } else {
          return firebase
              .auth()
              .createUserWithEmailAndPassword(newUser.email, newUser.password);
        }
      })
      .then((data) => {
        userId = data.user.uid;
        return data.user.getIdToken();
      })
      .then((idToken) => {
        token = idToken;
        const userCredentials = {
          // userHandle: newUser.userHandle,
          email: newUser.email,
          createdAT: new Date().toISOString(),
          updatedAt: new Date().toISOString(),
          userId,
          firstName: newUser.firstName,
          lastName: newUser.lastName,
          imageUrl: https://firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${noImg}?alt=media
        };
        db.doc(/users/${newUser.userHandle}).set(userCredentials);
      })
      .then(() => {
        return res.status(201).json({token});
      }

      )
      .catch((err) => {
        console.error(err);
        if (err.code === "auth/email-already-in-use") {
          return res.status(400).json({email: "Email is already in use."});
        } else {
          return res.status(500).json({error: err.code});
        }
      });
}

// --------------------- LOGIN ------------------- //
exports.login = (req, res) => {
    const user = {
      email: req.body.email,
      password: req.body.password,
    };

    const { valid, errors } = validateLoginData(user)
    if(!valid) return res.status(400).json(errors)

    firebase
        .auth()
        .signInWithEmailAndPassword(user.email, user.password)
        .then((data) => {
          // TODO: Rtv userId from user's file (i.e. userHandle)
          return data.user.getIdToken();
        })
        .then((token) => {
          return res.json({token});
        })
        .catch((err) => {
          console.error(err);
          if (err.code === "auth/wrong-password") {
            return res.status(403).json({general: "Wrong credentials, please try again"});
          } else return res.status(500).json({error: err.code});
        });
}

// --------------------- GET ALL USERS ------------------- //
exports.getAllUsers = (req, res) => {
  db
    .collection("users")
    .orderBy("userId", "asc")
    .get()
    .then((data) => {
    const users = []
    console.log(data.doc)
    data.forEach((doc) => {
        users.push({
        userId: doc.id,
        userHandle: doc.data().userHandle,
        firstName: doc.data().firstName,
        lastName: doc.data().lastName,
        email: doc.data().email,
        createdAt: doc.data().createdAt,
        updatedAt: doc.data().updatedAt,
        });
    });
    return res.json(users);
    })
    .catch((err) => console.error(err));
}

// --------------------- ADD USER DETAILS ------------------- //
exports.addUserDetails = (req, res) => {
  let userDetails = reduceUserDetails(req.body)

  // TODO: Replace userHandle with userId once rtvd
  db.doc(/user/${req.user.userHandle})
    .update(userDetails)
    .then(() => {
      return res.json({ message: 'Details added successfully'})
    })
    .catch(err => {
      console.error(err)
      return res.status(500).json({ error: err.code})
    })
}

// --------------------- UPLOAD AN IMAGE ------------------- //
exports.uploadImage = (req, res) => {
  const BusBoy = require("busboy");
  const path = require("path");
  const os = require("os");
  const fs = require("fs");

  const busboy = new BusBoy({ headers: req.headers });

  let imageToBeUploaded = {};
  let imageFileName;
  // String for image token
  let generatedToken = uuidv4();

  busboy.on("file", (fieldname, file, filename, encoding, mimetype) => {
    console.log(fieldname, file, filename, encoding, mimetype);
    if (mimetype !== "image/jpeg" && mimetype !== "image/png") {
      return res.status(400).json({ error: "Wrong file type submitted" });
    }
    // my.image.png => ['my', 'image', 'png']
    const imageExtension = filename.split(".")[filename.split(".").length - 1];
    // 32756238461724837.png
    imageFileName = `${Math.round(
      Math.random() * 1000000000000
    ).toString()}.${imageExtension}`;
    const filepath = path.join(os.tmpdir(), imageFileName);
    imageToBeUploaded = { filepath, mimetype };
    file.pipe(fs.createWriteStream(filepath));
  });
  busboy.on("finish", () => {
    admin
      .storage()
      .bucket()
      .upload(imageToBeUploaded.filepath, {
        resumable: false,
        metadata: {
          metadata: {
            contentType: imageToBeUploaded.mimetype,
            //Generate token to be appended to imageUrl
            firebaseStorageDownloadTokens: generatedToken,
          },
        },
      })
      .then(() => {
        // Append token to url
        const imageUrl = https://firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${imageFileName}?alt=media&token=${generatedToken};
        // console.log(' User info *: ', req.user)
        // TODO: Replace userHandle with userId/uid
        // Then remove userHandle from database documents
        return db.doc(/users/${req.user.userHandle}).update({ imageUrl });
      })
      .then(() => {
        return res.json({ message: "image uploaded successfully" });
      })
      .catch((err) => {
        console.error(err);
        return res.status(500).json({ error: "something went wrong" });
      });
  });
  busboy.end(req.rawBody);
};
This code section has the following functions:
signup,
login,
getAll­Users,
addUse­rDe­tails,
upload­Image

The uploa­dImage function uses the third party library "­bus­boy­" to parse and upload a file to your image store. For more info on this package see NPM Busboy

Index.js (main server app)

// Imports
const functions = require("firebase-functions");
const app = require('express')();
const FBAuth = require('./util/fbAuth')

// Import Handlers
const { signup,
        login,
        getAllUsers,
        uploadImage,
        addUserDetails
      } = require('./handlers/users')


// ** ----- Routes ----- //

// -------------------- USERS -------------------- //
app.post('/signup', signup);
app.post('/login', login);
app.post('/user/image', FBAuth, uploadImage)
app.post('/user', FBAuth, addUserDetails)
app.get('/users', FBAuth, getAllUsers);

// --------------------- EXPORT ALL FUNCS -------------------- //
exports.api = functions.https.onRequest(app)
;
Of course you can now add your own additional handlers and and routes for project database.

Support Cheatography!

 

Comments

No comments yet. Add yours below!

Add a Comment

Your Comment

Please enter your name.

    Please enter your email address

      Please enter your Comment.

          Related Cheat Sheets

          SailsJS Cheat Sheet
          Express.js Cheat Sheet