Motivation For Sharing

One of the reasons I was resistant to migrate away from a database driven CMS to jekyll was the complexity it introduces regarding security. If its “just” a static blog; no big deal. It’s when you want to add dynamic functionality that things become more complicated. No comments. No users. No forms.

Fortunately, I have a bootstrap project demonstrating how one might securely submit forms from a single page application. Unfortunately, thats way too much for this problem. I want a contact form up ASAP and that additional complexity doesn’t actually buy me anything. Lets get started by examining what this needs to be.

Requirements

  • Smooth UI/UX
    • Mobile Friendly
    • User does not leave the page they are on
  • Submit a contact form with: From / Subject / Body
  • Obscure the MailGun api key
  • CORS protection
  • ReCaptcha v3
  • Basic validation

Using Django + Redux to submit a form to MailGun would be akin to a jet engine powered station wagon. I’ve wanted to check out some new simple frameworks for simple tasks like this. I landed on Vue.js and Flask.

Why Flask?

Flask seems to be an obvious choice for teeny-tiny api that has one job; listen and validate form submissions from derekadair.com. I dont need MVC, User Management or any of the amazing utilities the django community has produced. This will be a bare bones api that validates and proxies to MailGun.

Why Vue.js?

I’ve enjoyed react/redux. It’s complicated implementation is exactly what you want for a large single page app. However, I am looking to explore other frameworks that may be useful in a more piecemeal fashion. Additionally, Vue.js has a TON of momentum and a helluva sales pitch; “Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.”

Building The UI Component

Install Vue.js / Axios in Jekyll

Jekyll serves static files from the /assets/ directory, as does my production nginx config. So, lets configure yarn to dump node_modules into /assets/! Creating a .yarnrc that tells where yarn to put the things is what we want here.

echo --modules-folder assets/node_modules/ >> .yarnrc
yarn add vue.js axios

Make sure you add Vue/Axios to your pages

<script src="/assets/node_modules/vue/dist/vue.js"></script>
<script src="/assets/node_modules/axios/dist/axios.js"></script>

Build a pretty form

I put the ‘Smooth UI/UX’ at the top for a reason; what’s the point of any software if its not a pleasant User Experience. With Twitter Bootstrap and Vue.js that should be pretty easy. My vision is to have an easy way for someone to reach out to me at any point during their reading. So for now I’ll just have a contact button in the main nav that makes a form slide out form beneath the nav.

You can pick any form styles you want, as long as you have the properly labeled form inputs;

<input type="text" id="name" name="name" required>
<input type="text" id="from" name="from" required>
<input type="text" id="subject" name="subject">
<textarea type="text" id="text" name="text" required></textarea>

I got my layout from Material Design.

Setting up the UI

I want a Vue component to control the UX. Vue has a modal component example that I will repurpose. It leverages Vue Transitions, which is what I will be using to show/hide this form on each page.

Initial Vue.js app

The (stripped down) x-template:

<script type="text/x-template" id="contact-template">
  <transition name="contact">
    <!-- your form -->
  </transition>
</script>

Lets wire up the markup in our Jekyll Site. I’m lazy so I will just wrap all of my pages in the vue app. My default.html layout looks like this now;

<!DOCTYPE html>
<html>
  <!-- include 'head.html' here, this breaks the jekyll rendering... so... -->
  <body>
    <div id="vue-app-wrapper">
    <!-- The rest of the template here -->
    </div>
  </body>
</html>

Add the contact module where you want it to show up!

<contact v-if="showContact" />

Finally declaring the component.

// App init
new Vue({
  el: "#vue-wrapper",
  data: {
    showContact: false
  },
  methods: {
    toggleShowContact(){
        this.showContact = !this.showContact
    }
  }
});

Add a link that toggles the showContact value. NOTE: @click.prevent prevents the default behavior of the event immiter, in this case… following the anchor tag.

<a href="#contact" @click.prevent="toggleShowContact">@me</a>

Your form should be hidden initially. Clicking the @me link will show it. toggleShowContact is manipulating the state. v-if="showContact" is the magic showing/hiding the form.

Wire up the contact component

Lets take a look at how we can prevent form submission w/ Vue.js and execute this API call! Two things need to happen here;

Add @submit listener to the form to interupt

<input @click.prevent="submitForm" type="submit" class="btn btn-primary" id="submit-form" value="Send" />

Wire up the form <-> vue component via v-model

<input v-model="name" type="text" id="name" name="name" required>
<input v-model="from" type="text" id="from" name="from" required>
<input v-model="subject" type="text" id="subject" name="subject">
<textarea v-model="text" type="text" id="text" name="text" required></textarea>

Update contact component

Vue.component("contact", {
  template: "#contact-template",
  // MUST be a function that returns an object
  data: function() {
    return {
      name: null,
      from: null,
      subject: null,
      text: null
    }
  },
  methods: {
    submitForm: function (event) {
      // this is where we will use axios in the next step
      // for now just close the window by setting the parent property
      this.$parent.showContact= false
    }

  }
});

At this point you should have;

  1. A form that is initially hidden and shows up when you click a link
  2. A component that is wired to your form
  3. A button that hides the form w/o submitting anything

Axios up in this mother

Lets make this form submit via ajax. fetch is a massive improvement from XMLHttpRequest(), it’s just too low level for my liking. Promise driven ajax client?? SIGN ME UP. I’m lazy and look for every chance to be (within the realm of reason).

Modify the contact component’s submitForm method

    submitForm: function (event) {
      // grab the form target
      let destination = event.target.action,
        app = this.$parent
      ;

      axios(destination, {
        method: 'POST',
        //submit the form as json
        data: {
          'name': this.name,
          'subject': this.subject,
          'text': this.text,
          'from': this.from
        }	
      })
        .then(function(response){
          //feels wrong, probably needs to emit an event somehow
          app.showContact= false
        })
        .catch(function(error){
          console.log(error)
        })
    }

Now you should be seeing a failed POST request, with the form data submitted as json, whenever you click send. The form will remain visible. The request will fail.

Building a Flask email proxy… thing

Now that we have a form submitting w/ ajax and its all pretty and whatnot its time to get cracking on this python code. There are still some finishing touches (Validation/ReCaptcha/Success Message), but I’d like to have the API in place before diving into this.

Set up Flask

I’m using Docker, if you’re not… you should be. Lets take a look at my python-workflow image. I have an image that allows for the running of simple python scripts. It is a container that already has python 3.5 installed, and has ONBUILD commands that add your code and install the requirements.

NOTE: Depending on your hosting setup you can ignore the cors setup. My eventual API endpoint will be contact.derekadair.com

Three steps to get a stupid simple flask app running;

  1. Create the files…

    requirements.txt

    flask
    flask-cors 
    requests
    

    app.py

    from flask import Flask, request
    from flask_cors import CORS
    import requests as req
       
    app = Flask(__name__)
    CORS(app, resources={r"/*": {"origins": ["YOUR_FRONT_END_URI"]}})
    
    @app.route('/', methods=['POST])
    def contact():
        return request.get_json()
    
    if __name__ == "__main__":
        app.run(host="0.0.0.0", debug=True)
    

    Dockerfile

    FROM derekadair/python-workflow:onbuild
    
  2. Build the docker image
    docker build -t="email-api" .
    
  3. Run it
    docker run email-api python app.py
    

Add x-origin headers to axios

If you’re using CORS you need to add this to modify your axios post like so;

      axios(destination, {
        headers: {
          'Access-Control-Allow-Origin': '*',
        },
        //... rest of stuff
      })

Where are we?

You should now have;

  1. a form that shows w/ a nav click
  2. form should submit via axios -> your api endpoint
  3. Endpoint should just echo the form submitted in json

Send an email with python

Now that we have a simple Flask app running in Docker and accepting/echoing a form submission, lets take a crack at sending an email with the MailGun API. First you will need to signup and have your private api key handy. You can test your account setup with the folloing curl command:

curl -s --user 'api:YOUR_API_KEY' \
    https://api.mailgun.net/v3/YOUR_DOMAIN_NAME/messages \
    -F from='Excited User <mailgun@YOUR_DOMAIN_NAME>' \
    -F to=YOU@YOUR_DOMAIN_NAME \
    -F to=bar@example.com \
    -F subject='Hello' \
    -F text='Testing some Mailgun awesomeness!'

If you have an email from mailgun we’re in business!

Enviornment Variables!

First thing is first, one of our goals here is to secure our API keys. For stuff like this I use environment variables. Leveraging os.getenv() and docker environment variables is a very powerful combo. This is useful for configuration as well as some basic security. Lets go all in on environment variables!

app.py

import os
MG_DOMAIN = os.getenv('MG_DOMAIN', "REPLACE_ME_YOUR_DOMAIN")
MG_TO = os.getenv('MG_TO', "REPLACE_ME_WITH_YOUR_EMAIL")
MG_KEY = os.getenv('MG_KEY', "REPLACE_ME_YOUR_KEY")

FRONTEND_URI = os.getenv('FRONTEND_URI', '')
app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": [FRONTEND_URI]}})

You can see leveraging environment variables can be apowerful tool! We are able to avoid putting any sensitive info directly in the code. We’ve also made this little script modular and deployable with properly configured environment variables.

Proxy the request

Lets try to keep this proxy as dumb as possible! Mailgun can handle the validation and whatnot. All we need to do is;

  1. format a request url
  2. grab the form json
  3. add “to” your email
  4. return the response.text from mailgun
@app.route('/', methods=['POST'])
def contact():
    endpoint = 'https://api.mailgun.net/v3/{}/messages'.format(MG_DOMAIN)
    email = request.get_json()
    email['to'] = MG_TO
    response = req.post(endpoint, auth=('api', MG_KEY), data=request.get_json())
    return response.text

BINGO! We have a working…ish contact form! You should now be able to naively send yourself emails from your static website!

Flask and mailgun

Future Improvements

I cut this post a bit short because it was getting quite long. I will be doing a couple small updates in the future w/ the following

  1. ReCaptcha: I’d like to thwart bots from spamming me
  2. validation: Using native JS validation only is lame. We need to;
    • validate email addresses w/ mailgun API
    • Validate @ the flask api level
    • error styles
  3. Success Message- currently just closes the form

The Takeaway

Vue.js

Vue.js is rather intuitive and pretty easy to step into. I ran into a couple minor traps;

  1. data should be a function, not a flat object
  2. components dont share your vue apps scope.(should have bene obvious)

Flask

ZERO complaints here. Flask was a breeze to implement and the flask-cors plugin saved me from implementing CORS myself.

Thoughts?

What do you think? Did you have any troubles? Did you have a FUCKING BLAST? Is my code utter garbage?

Now you can email me through my website!

Next Week

You can look forward to me taking a step back and explaining how I develop 100% in an amazon ec2 instance. Rather powerful stuff! I will be diving in to how I use jwilders nginx-proxy, tmux, and vim to develop on any machine and avoid ever losing anything critical!