Earlier this week I decided to build a rather simple application with Auth0 Webtask, a form handler. This was something I did many months ago with OpenWhisk and I was curious what the experience would be with Auth0 Webtask. As usual in such things, even though it was pretty simple, I did run into a few things that helped me understand Auth0 Webtask a bit more. Here is how I approached it.

First, I built a form. I actually had a simple form I used last week in my (very last) OpenWhisk talk, so I just copied that over.

<form method="post" action="">
	
	<div class="form-group">
		<label for="name">Name</label>
		<input type="text" class="form-control" name="name" id="name" required>
	</div>

	<div class="form-group">
		<label for="email">Email</label>
		<input type="email" class="form-control" name="email" id="email" required>
	</div>

	<div class="form-group">
		<label for="acc">Current Accuracy</label>
		<select name="acc" id="acc" class="form-control" required>
			<option></option>
			<option>Sharp Shooter!</option>
			<option>Reasonable Accuracy</option>
			<option>I can hit anything except a rebel</option>
		</select>
	</div>


	<div class="form-group">
		<label for="emp">The Empire is...</label>
		<select name="emp" id="emp" class="form-control" required>
			<option></option>
			<option>Awesome!</option>
			<option>Totally Not Evil!</option>
			<option>Whatever "Better than Awesome" is!</option>
		</select>
	</div>


	<div class="form-group">
		<label for="why">Why you want to join the Stormtrooper ranks:</label>
		<textarea name="why" id="wh" class="form-control" required></textarea>
	</div>

	<button type="submit" class="btn btn-primary">Submit (No, seriously, submit!)</button>

</form>

I used Bootstrap to make it a bit nicer. If you want to see the final result, I'll be sharing the URL at the end. I created the first draft of my handler with the smallest code possible.

'use strict';

module.exports = function(context, cb) {
	//first, gather the form fields
	let form = context.body;
	console.log(form);
	cb(null, {form:form})
}

Form data is available in the context argument so I've simply made a copy of it to address it a bit nicer. I log it and return it. To deploy this, I saved that code as form_handler_v1.js and used the following command line:

wt create form_hander_v1.js --name form_handler

What's with the --name part? By default the CLI will give your webtask a name based on the filename. I was planning on writing multiple different versions of this application so I used the name argument to specify exactly what I wanted. As I go on and add newer versions, I'll just keep using the same name.

The command line spit out the URL for me: https://wt-c2bde7d7dfc8623f121b0eb5a7102930-0.run.webtask.io/form_handler and I simply pasted that into my form. I filled out some data, hit submit, and got a nice, but not very helpful, JSON response:

JSON output

I also opened up a new tab in my terminal and ran wt logs so I could see the log output as the webtask was run:

Log output

Sweet. Practically done, right? I then switched to a new version, form_handler_v2.js:

'use strict';

const RECIPS = ["raymondcamden@gmail.com","ray@camdenfamily.com"];

module.exports = function(context, req, res) {
	//first, gather the form fields
	let form = context.body;

	let from = RECIPS[0];
	let to = RECIPS[0];

	//let the form specify a from
	if(form._from) {
		from = form["_from"];
	}

	if(form["_to"]) {
		if(RECIPS.indexOf(form["_to"]) === -1) {
			cb("Invalid _to address: "+form["_to"]);
		} else {
			to = form["_to"];
		}
	}

	let subject = form["_subject"] || 'Form Submission';

	let next = form["_next"] || context.headers.referer;

	//Generate the text
	let date = new Date();
    let content = `
Form Submitted at ${date}
--------------------------------
`;

    for(let key in form) {
        //blanket ignore if _*
        if(key.indexOf("_") != 0) {
            content += `
${key}:         ${form[key]}
`;
        }
    }
	
	//fire off the request to send an email - we don't want the user to wait so this is fire and forget
	sendEmail(to,from,subject,content);

	res.writeHead(301, {'Location': next });
	res.end();

}

function sendEmail(to, from, subject, body) {
}

This one kicks it up a notch by adding features and functionality from my original OpenWhisk demo.

First - I have support for looking for a form variable called _from. If it exists, I'll use it as the sender for the email generated by the form. I also allow for a dynamic address as well, but restrict it to a hard coded list of valid values (RECIPS). As before, I use a form field with an underscore (_to).

Next, I look for a _subject field to allow customization for the email.

Finally, I look for _next as a way to know where to send the user after the form is processed. If nothing was specified for that, I simply send them back to where they came from. That's probably not a good idea, but I needed some default.

After working with the "special" variables, I then gather up the form fields into a simple string. Note we skip any field that begins with an underscore. I wrote a mocked out sendEmail function and then did my redirect. I deployed this version by using wt create form_handler_v2.js --name form_handler and...

It broke. Why? I kept getting an error about form._form not existing. Turns out, when you use a webtask with this form: function(context, req, res), the expectation is that you are going to parse the request body to get what you want out of it. I had switched to this form so I could do the redirect at the end. This issue with the context value is documented, but needs to be cleaned up a bit (my job!). Luckily, there's an easy fix - add --parse-body to your CLI call. You can find this if you run wt create -h. So my new command was:

wt create form_handler_v2.js --name form_hamdler --parse-body

I didn't need to update my URL in the form as it already had the right value. I did change the email field to use _from though.

Ok, almost there! Now let's add in actual email support. I used sendgrid before so I figured I'd use it again. I already had a developer key so that saved me the trouble of getting a new one. Here's the updated version (form_handler_v3.js):

'use strict';

const helper = require('sendgrid').mail;
const RECIPS = ["raymondcamden@gmail.com","ray@camdenfamily.com"];

module.exports = function(context, req, res) {
	//first, gather the form fields
	let form = context.body;

	let from = RECIPS[0];
	let to = RECIPS[0];

	//let the form specify a from
	if(form._from) {
		from = form["_from"];
	}

	if(form["_to"]) {
		if(RECIPS.indexOf(form["_to"]) === -1) {
			cb("Invalid _to address: "+form["_to"]);
		} else {
			to = form["_to"];
		}
	}

	let subject = form["_subject"] || 'Form Submission';

	let next = form["_next"] || context.headers.referer;

	//Generate the text
	let date = new Date();
    let content = `
Form Submitted at ${date}
--------------------------------
`;

    for(let key in form) {
        //blanket ignore if _*
        if(key.indexOf("_") != 0) {
            content += `
${key}:         ${form[key]}
`;
        }
    }
	
	//fire off the request to send an email - we don't want the user to wait so this is fire and forget
	sendEmail(to,from,subject,content, context.secrets.sg_key)
	.then(() => {
		res.writeHead(301, {'Location': next });
		res.end();
	}).catch(e => {
		// handle error
	});

}

function sendEmail(to, from, subject, body, key, cb) {

	let to_email = new helper.Email(to);
	let from_email = new helper.Email(from);
    let mailContent = new helper.Content('text/plain', body);
    let mail = new helper.Mail(from_email, subject, to_email, mailContent);
	let sg = require('sendgrid')(key);

	var request = sg.emptyRequest({
		method: 'POST',
		path: '/v3/mail/send',
		body: mail.toJSON()
	});
        
	return new Promise((resolve, reject) => {
		sg.API(request, function(error, response) {
			if(error) {
				console.log(error.response.body);
				reject(error.response.body);
			} else {
				console.log('good response from sg');
				resolve();
			}
		});
	});

}

So notice I changed how I called sendEmail. Instead of "fire and forget", I actually wait for it to finish. The code inside sendEmail is virtually the same from my previous demo and is just boilerplate email code. However, to get it to work, I need to add the sendgrid npm module. The visual editor has a slick UI for this, but as I'm using the CLI, I'll just use that. Luckily there's yet another argument for this, --dependency. Note, --dependancy will not work, because that's not how it's spelled. Learn to spell. (I'm talking to myself, by the way.)

Here's the final version of the CLI call:

wt create form_handler_v3.js --name form_handler --parse-body --dependency sendgrid --secret sg_key=mysecretsarebetterthanyours

Notice I also added my sendgrid key as well. That was used here: context.secrets.sg_key. At this point, the CLI is a bit long, so I'd probably create a simple shell script to simplify it a bit. To finish testing, I added this to my form:

<input type="hidden" name="_subject" value="Stormtrooper Form">
<input type="hidden" name="_next" value="https://auth0.com">

This provides a custom subject line for the email and an address to go to when done. You are more than welcome to test the form here - https://cfjedimaster.github.io/Serverless-Examples/webtask/test_form.html. Please note I won't actually be reading the emails. ;) You can find the full source code for all three versions of the handler here: https://github.com/cfjedimaster/Serverless-Examples/tree/master/webtask. Enjoy!

Header photo by Samuel Zeller on Unsplash