Writing a serverless app in TypeScript and Vue2 with Firebase

Parth Mudgal
5 min readApr 1, 2018

--

If for some reason you are writing an application with the following as your stack choice, then you may find this post useful.

Project structure

My project structure looks like this

.
├── README.md
├── database.rules.json # structure of your data for firebase database
├── firebase.json # firebase entry point
├── firestore.indexes.json
├── firestore.rules
├── functions # here we store all the functions to be exposed
└── public # this folder has the frontend code

functions and public are the most used folders and both of them are independent npm projects. firebase.json is the file which firebase reads first and decides what to deploy where. Here is what it looks like:

{
"database": {
"rules": "database.rules.json"
},
"functions": {
"predeploy": [
"npm --prefix $RESOURCE_DIR run lint",
"npm --prefix $RESOURCE_DIR run build"
],
"source": "functions"
},
"hosting": {
"public": "public/dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "/public/**",
"destination": "/index.html"
},
{
"source": "**",
"destination": "/index.html"
}
]
}
}

I am using three features namely database, functions and hosting and all the three have the configuration in firebase.json . The Google Cloud Pub/Sub is separately configured from the google cloud console itself.

Each function to be exposed on firebase function will be included in the exports of the functions project. Here is what my index.ts looks like:

‘use strict’;const functions = require(‘firebase-functions’);// Firebase Setupimport {authorize} from ‘./oauth/authorize’
import {newToken} from ‘./oauth/newToken’
import {handleLogout} from ‘./oauth/logout’
exports.hello = functions.https.onRequest((request, response) => {
response.send(“Hello!”);
});
/**
* authorize the User to the mailchimp authentication consent screen. Also the ‘state’ cookie is set for later state
* verification.
*/
exports.authorize = authorize;
/**
* Logout sets the token cookie to null and expiry time to 0 to remove the token cookie
*/
exports.logout = handleLogout;

Vue2 and frontend

The public folder is a standard vue2 npm project, you can set it yourself or use vue init to create one. Since I wanted to use Typescript both in the frontend and backend, I created a Typescript based Vue2 project.

Here is what the tsconfig.json looks like:

{
"compilerOptions": {
"module": "es2015",
"moduleResolution": "node",
"experimentalDecorators": true,
"strict": true,
"baseUrl": ".",
"emitDecoratorMetadata": true,
"strictNullChecks": false,
"target": "ES5",
"noImplicitAny": false,
"strictFunctionTypes": false,
"paths": {
"@router/*": [
"./src/router/*"
],
"@store/*": [
"./src/store/*"
],
"@types/*": [
"./src/types/*"
],
"@components/*": [
"./src/components/*"
]
},
"lib": [
"dom",
"es5",
"es2015.promise"
]
},
"include": [
"./src/**/*.ts"
]
}

Oauth login

For oauth flow, two urls need to be exposed

authorise url

  • sets a state in users browser
  • redirects the user to oauth login page of the service

redirect url

  • handles the response from oauth service
  • use code to generate token
  • provide a token to user for future use

Both the pages are fairly easy to build. Use simple-oauth2 library to build a oauth client to generate redirect urls in /authorize function.

authorize.ts

export const authorize = functions.https.onRequest((req, res) => {
const oauth2 = oAuth2Client(); // Use simple-oauth2 to make a client

cookieParser()(req, res, () => {
try {
const state = crypto.randomBytes(20).toString('hex');
res.cookie('state', state.toString(), {
maxAge: 3600000,
secure: false,
httpOnly: true,
});
console.log("Redirect uri: ", OAUTH_REDIRECT_URI);
const redirectUri = oauth2.authorizationCode.authorizeURL({
redirect_uri: OAUTH_REDIRECT_URI,
scope: OAUTH_SCOPES,
state: state,
});
res.redirect(redirectUri);

} catch (e) {
console.log("failed to sent to authorize", e);
res.redirect("/")
}
});
})

The response handler is also fairly straightforward, although a little longer since we do these things:

  • check the code, obtain oauth token
  • obtain user profile using oauth token
  • use userid to look up user in firebase
  • create firebase user and create a new firebase user token (note this is different from the earlier token which the client never receives)
  • return firebase token to client
// Get the access token object (the authorization code is given from the previous step).
const tokenConfig = {
code: req.query.code,
redirect_uri: OAUTH_REDIRECT_URI,
scope: OAUTH_SCOPES,
};


const result = await oauth2.authorizationCode.getToken(tokenConfig);
const accessToken = oauth2.accessToken.create(result);

Create firebase user

/**
* Creates a Firebase account with the given user profile and returns a custom auth token allowing
* signing-in this account.
* Also saves the accessToken to the datastore at /
foreignAccessToken/$uid
*
*
@returns {Promise<string>} The Firebase custom auth token in a promise.
*/
function createFirebaseAccount(email, displayName, foreignUserId, accessToken, foreignApiData) {
// The UID we'll assign to the user.
const uid = `foreign:${foreignUserId}`;

// Save the access token to the Firebase Realtime Database.
const databaseTask = admin.database().ref(`/foreignToken/${uid}`)
.set(accessToken);

// Save the access token to the Firebase Realtime Database.
const apiDataTask = admin.database().ref(`/foreignApiData/${uid}`)
.set(foreignApiData);

// Create or update the user account.
const userCreationTask = admin.auth().updateUser(uid, {
displayName: displayName,
email: email,
foreignUserId: foreignUserId,
}).catch((error) => {
// If user does not exists we create it.
if (error.code === 'auth/user-not-found') {
return admin.auth().createUser({
uid: uid,
displayName: displayName,
email: email,
foreignUserId: foreignUserId,
});
}
throw error;
});

// Wait for all async task to complete then generate and return a custom auth token.
return Promise.all([userCreationTask, databaseTask, apiDataTask]).then(() => {
// Create a Firebase custom auth token.
return admin.auth().createCustomToken(uid);
}).then((token) => {
return token;
});
}

And give the firebase token to the browser in the url

res.redirect(FRONTEND_URL + "/" + "?token=" + firebaseToken);

On the client side in vue application, you can use the firebase token to

Obtain the user profile on client ide

firebase
.auth()
.signInWithCustomToken(data.token)
.then(function (id: any) {
console.log("signed in ", id);
firebase
.auth().currentUser.getIdToken().then(function (idtoken: any) {
that.setIdToken(idtoken);
that.loggedIn = true;
that.$forceUpdate();
window.localStorage.setItem("idToken", idtoken);
console.log("got id token", idtoken);
that.$router.push({name: "MailingLists"});
});
}).catch(function (err: any) {
console.log("failed to validate custom token", err);
that.$router.push({name: "Home"})
// that.isLoggingIn = true;
// window.location.href = getFirebaseProjectUrl() + "/authorize";
});

Publishing a message

Publishing a message on any topic is simple

// Instantiates a client
const pubsubClient = new PubSub({
projectId: projectId,
});
async function publishMessage(name, data) {
// [START pubsub_publish_message]
// Imports the Google Cloud client library

// Publishes the message as a string, e.g. "Hello, world!" or JSON.stringify(someObject)
let message = JSON.stringify(data);
const dataBuffer = Buffer.from(message);

await pubsubClient
.topic(name)
.publisher()
.publish(dataBuffer)
.then(results => {
const messageId = results[0];
console.log(`Message ${messageId} published: ${message}`);
})
.catch(err => {
console.error('ERROR:', err);
});
// [END pubsub_publish_message]
}

Subscriber functions

Subscribing functions are obviously the most interesting since they do the main work of consuming the payload. They can publish new messages as well.

export const mailingListExporter = functions.pubsub.topic(exportJobTopic).onPublish(async (pubSubMessage) => {
const data = pubSubMessage.data.json;
console.log("Initiated exporter", data);
const uid = data.userId;
const listId = data.listId;
console.log("Check if all buckets imported for list ", listId);
})

Running in the project locally

You should run the functions and hosting locally to have an offline development environment:

firebase serve --only functions,hosting

Deploying

Effectively a #serverless app

firebase deploy

--

--