After experimenting with Google's Cloud Functions while working on "Serverless Cloud Maker" at Next 2019 and seeing first hand how easy and cheaper it is to deploy javascript, I wanted to find a good use case for myself. Looking for something that warranted a solution like this, a situation where building a full back end server would be over kill (I really don't want to have to monitor and keep up another small node server). This website is entirely static, and I'm constantly building and rebuilding it as I learn new skills. Using basic node.js and express.js patterns, I wanted an easy way to handle an email contact form in a secure manner. While it ended up needing a small node server anyways to handle the captcha code, this is a great tool that's easy to deploy. The Cloud Function adds a row to a spreadsheet and sends a notification to a slack channel, making it easy to receive emails from users without needing to interface with any SMTP.
Assuming some familiarty with node and javascript, create a folder somewhere logical (for me, this is in a directory right next to all the web stuff, in a parent project folder). npm init
, create a file to deploy as a Cloud Function.
The simplest function is something like this:
// /index.js
exports.contactForm = (req, res) => {
console.log("Function Fired!");
console.log("___________________________________");
// CORS headers
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
//respond to CORS preflight requests
if (req.method == 'OPTIONS') {
res.status(204).send('');
}
// Response
res.status(200).send("Success");
};
It needs CORS to handle client-side AJAX requests. We can add more interesting actions later using any node module, like sending data to a Slack webhook or integrating with the Sheets API.
This requires some initial legwork to get a GCP account started and credentials worked out.
reate an account (with $300 free credits for a year) at console.cloud.google.com, and start a new project. Name it something descriptive, because the management UX is still being worked out.
Download the gcloud
SDK for the command line.
Navigate to the IAM & Admin sidebar tab, and create a Service Account and download the .json
key.
Enable these APIs:
gcloud auth login
gcloud config set project <project_name>
gcloud beta functions deploy contactForm --trigger-http
And that's it for deployment! The command will return a URL to make requests to, and anytime it receives a POST request, the console logs will appear in Stackdriver.
Now we can do the fun stuff.
This is a bit complicated, because Sheets require us to get access tokens using the Service Account creds before accessing the API, but easy enough to follow along. Most of it comes from the google-spreadsheet module.
npm i --save google-spreadsheet
const GoogleSpreadsheet = require('google-spreadsheet');
const async = require('async');
exports.contactForm = (req, res) => {
console.log("Function Fired!");
console.log("___________________________________");
// CORS headers
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
//respond to CORS preflight requests
if (req.method == 'OPTIONS') {
res.status(204).send('');
}
var doc = new GoogleSpreadsheet('<spreadsheet ID from Sheets URL');
var sheet;
async.series([
function setAuth(step) {
var creds = require('./credentials.json');
doc.useServiceAccountAuth(creds, step);
},
function sheetInfo(step) {
doc.getInfo(function(err, info) {
console.log("Doc Loaded!!!!! -------------");
sheet = info.worksheets[0];
console.log('sheet 1: ' + sheet.title + ' ' + sheet.rowCount + 'x' + sheet.colCount);
step();
});
},
function appendRow(step) {
console.log("APPENDING ROW!!!! --------------");
sheet.addRow(bodyData, function(err, info) {
console.log("callback");
step();
});
},
function success(step) {
res.status(200).send("Success");
step();
}
], function(err) {
if (err) {
console.log('Error: ' + err);
res.status(500).send("Error");
}
});
};
This is a super easy integration. After creating a special slack channel to post messages into and registering an application in the team settings, all it takes is a webhook URL. Most of the pain here comes from crafting the message to fit into Slack's spec.
const GoogleSpreadsheet = require('google-spreadsheet');
const async = require('async');
const { IncomingWebhook } = require('@slack/client');
const url = process.env.SLACK_WEBHOOK_URL;
const webhook = new IncomingWebhook(url);
const rawSlack = {
"attachments": [{
"fallback": "Email received from _________ on _________",
"color": "#36a64f",
"pretext": "Message from ________ on ________",
"fields": [],
"footer": "Contact Bot! Email Back.",
"footer_icon": "https://platform.slack-edge.com/assets/img/default_application_icon.png",
"ts": 123456789
}]
}
exports.contactForm = (req, res) => {
console.log("Function Fired!");
console.log("___________________________________");
// CORS headers
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
//respond to CORS preflight requests
if (req.method == 'OPTIONS') {
res.status(204).send('');
}
var bodyData = {}
function parseBody(data) {
bodyData.date = new Date();
if (data.name) {
bodyData.name = data.name
rawSlack.attachments[0].pretext = "Email received from " + data.name + " on " + bodyData.date;
rawSlack.attachments[0].fallback = "Email received from " + data.name + " on " + bodyData.date;
var push = {
"title": "Name",
"value": data.name,
"short": false
}
rawSlack.attachments[0].fields.push(push);
var push = {}
}
if (data.email) {
bodyData.email = data.email
var push = {
"title": "Email",
"value": "<mailto:" + data.email + ">",
"short": false
}
rawSlack.attachments[0].fields.push(push);
var push = {}
}
if (data.company) {
bodyData.company = data.company
var push = {
"title": "Company",
"value": data.company,
"short": false
}
rawSlack.attachments[0].fields.push(push);
var push = {}
}
if (data.message) {
bodyData.message = data.message
var push = {
"title": "Message",
"value": data.message,
"short": false
}
rawSlack.attachments[0].fields.push(push);
var push = {}
}
console.log("parsed", bodyData, data);
}
async.series([
function parse(step) {
parseBody(req.body);
step();
},
function success(step) {
// Send simple text to the webhook channel
webhook.send(rawSlack, function(err, res) {
if (err) {
console.log('Error:', err);
} else {
console.log('Message sent');
}
});
res.status(200).send("Success");
step();
}
], function(err) {
if (err) {
console.log('Error: ' + err);
res.status(500).send("Error");
}
});
};
Just an AJAX call, no sweat.
$('#contact').submit(function(event) {
event.preventDefault();
var formData = $('#contact').serialize();
$.ajax({
url: "<functions URL>",
type: 'POST',
data: formData,
success: function (data) {
$('#contact')[0].reset();
}
});
});