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;
- A form that is initially hidden and shows up when you click a link
- A component that is wired to your form
- 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;
-
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
- Build the docker image
docker build -t="email-api" .
- 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;
- a form that shows w/ a nav click
- form should submit via axios -> your api endpoint
- 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;
- format a request url
- grab the form json
- add “to” your email
- 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!
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
- ReCaptcha: I’d like to thwart bots from spamming me
- validation: Using native JS validation only is lame. We need to;
- validate email addresses w/ mailgun API
- Validate @ the flask api level
- error styles
- 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;
- data should be a function, not a flat object
- 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!