Writing a serverless app in TypeScript and Vue2 with Firebase
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 upuser
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