Skip to content

Instantly share code, notes, and snippets.

@elnygren
Created August 28, 2017 18:42
Show Gist options
  • Save elnygren/d0b22d085a3bef7dca139af94bc5a870 to your computer and use it in GitHub Desktop.
Save elnygren/d0b22d085a3bef7dca139af94bc5a870 to your computer and use it in GitHub Desktop.
Tactforms.com API (just some interesting bits & pieces, lots of boring parts redacted)
/*
Even in small projects, you should always start with some kind of settings/config file that
- reads ENVs
- has limited amount of logic
- exposes all settings to the rest of your app
*/
// dotenv (.env) is an amazing trick, check it out if not familiar!
require('dotenv').config()
process.env.NODE_ENV = process.env.NODE_ENV || 'development'
const NODE_ENV = process.env.NODE_ENV
console.log('=> NODE_ENV:', NODE_ENV)
if (!process.env.STRIPE_KEY) console.log('=> STRIPE RUNNING IN *TEST* MODE')
if (!process.env.REDIS_DB) console.log('=> REDIS USING DB 0')
// we're gonna be tired when setting those ENVs at prod...
if (NODE_ENV == 'production') {
if (!process.env.STRIPE_KEY) throw 'STRIPE_KEY missing'
if (!process.env.REDIS_DB) throw 'REDIS_DB missing'
if (!process.env.MAILGUN_API_KEY) throw 'MAILGUN_API_KEY missing'
if (!process.env.MAILGUN_DOMAIN) throw 'MAILGUN_DOMAIN missing'
if (!process.env.MAILGUN_FROM) throw 'MAILGUN_FROM missing'
if (!process.env.REDIS_HOST) throw 'REDIS_HOST missing'
}
// hardcode sandbox/test API keys with || 'default' syntax -> less hassle in dev
module.exports = {
NODE_ENV,
STRIPE_KEY: process.env.STRIPE_KEY || 'test-key',
REDIS_DB: process.env.REDIS_DB || 0,
REDIS_HOST: process.env.REDIS_HOST,
MAILGUN_API_KEY: process.env.MAILGUN_API_KEY || 'sandbox-key',
MAILGUN_DOMAIN: process.env.MAILGUN_DOMAIN || 'test-domain',
MAILGUN_FROM: process.env.MAILGUN_FROM || 'Support <[email protected]>',
}
/*
We're using Redis so here's the basic boilerplate + couple queries.
*/
const config = require('./config')
const redis = require('redis')
const bluebird = require('bluebird')
// don't leave home with out bluerbird's promisify ! (Made in Finland <3)
bluebird.promisifyAll(redis.RedisClient.prototype)
bluebird.promisifyAll(redis.Multi.prototype)
// that's it
const client = redis.createClient({
db: config.REDIS_DB,
host: config.REDIS_HOST,
})
/*
In small projects I think the best pattern is to just wrap your
DB calls in functions and then export the functions.
btw. forwarder is an UUIDv4 here. We use them for the secret URL and verification URL.
*/
async function createForwarder(email, forwarder, secret) {
await client.hmsetAsync(`f:${forwarder}`, {
secret: secret, // for verifying
verified: 0, // `await client.hsetAsync(`f:${forwarder}`, 'verified', 1)` to verify
email: email, // where to send stuff
monthlyLimit: 2500, // no trolling
paidUntil: moment().utc().valueOf(), // no freebies!
})
}
module.exports = {
verifyForwarder,
}
FROM node:8.4.0
# Create app directory
WORKDIR /app
# Install app dependencies
COPY package.json .
COPY package-lock.json .
RUN npm install
# Bundle app source
COPY . .
EXPOSE 3000
CMD [ "npm", "start" ]
const config = require('./config')
const mailgun = require('mailgun-js')({apiKey: config.MAILGUN_API_KEY, domain: config.MAILGUN_DOMAIN})
// dat email templatin'
const VERIFICATION_EMAIL = (forwarder, secret) => `Hi,
To verify your Tactforms.com form, click this link:
https://tactforms.com/api/secret/${forwarder}/${secret}
Your form's secret url is:
https://tactforms.com/f/${forwarder}
If you did not create a form at tactforms.com then you may ignore this message.
- Tactforms
`
// it's a me - nice utility function!
async function sendVerificationEmail(email, forwarder, secret) {
await sendEmail(email, 'Tactforms Verification', VERIFICATION_EMAIL(forwarder, secret))
}
module.exports = {
sendVerificationEmail,
}
# I have a tedious and horrible deploy flow:
# 1. make sync
# 2. ssh to prod
# 3. make build and docker run
#
# However, it works, was fast to setup and is sufficiently safe.
#
sync:
rsync -rvz --exclude node_modules/ --exclude .env ./* tactforms:~/tactforms
.PHONY: sync
build:
docker build -t tactforms/api .
.PHONY: build
const Koa = require('koa')
const app = new Koa()
const _ = require('koa-route')
const bodyParser = require('koa-bodyparser')
const R = require('ramda')
const uuidv4 = require('uuid/v4')
const moment = require('moment')
const cors = require('koa-cors')
const {sendVerificationEmail} = require('./src/email.js')
const {createForwarder} = require('./src/db.js')
app.use(bodyParser())
app.use(cors())
// "signup"
app.use(_.post('/api/register', async ctx => {
const {email} = ctx.request.body
// UUIDv4 is a great tool. It's quite random and collisions practically never happen.
// the forwarder's URL could be something shorter for a better UX though.
const [secret, forwarder] = [uuidv4(), uuidv4()]
if (!email) ctx.throw(400)
await createForwarder(email, forwarder, secret)
await sendVerificationEmail(email, forwarder, secret)
ctx.body = {
forwarder_url: `/f/${forwarder}`,
absolute_url: `https://tactforms.com/f/${forwarder}`
}
}))
// "verification"
app.use(_.get('/secret/:forwarder/:secret', async (ctx, forwarder, secret) => {}))
// submit form here
app.use(_.post('/f/:uuid', async (ctx, uuid) => {}))
// submit payment here
app.use(_.post('/api/charge', async (ctx) => {}))
// start & export for tests
if (!module.parent) app.listen(3000)
module.exports = app
const config = require('./config')
const stripe = require('stripe')(config.STRIPE_KEY)
async function stripePay(amount, stripeToken) {
try {
await stripe.charges.create({
amount,
description: 'direct payment',
currency: 'usd',
source: stripeToken
})
return true
} catch (err) {
console.log('StripeError: ', err)
return false
}
}
module.exports = {
stripePay,
}
/*
Basic Mocha testing boilerplate + example test.
This is where all that db.js, config.js etc. modularizing of code really pays off.
Even a small project should have some basic "let's POST that API with stuff
and see what happens in the DB" -style tests.
Also, unit testing should be pretty simple too
*/
// you might have config.js or other code reacting to this
// I gotta admit, my sendEmail fn has "if test then send nothing" logic
// dirty, but works for small pet project
process.env.NODE_ENV = 'test'
const app = require('./server')
const {...} = require('./src/db')
const request = require('supertest').agent(app.listen())
const assert = require('assert')
const moment = require('moment')
const R = require('ramda')
const createForwarder = (email = '[email protected]') => {
return request.post('/api/register').send({email: email})
}
describe('Forwarders:', () => {
it('should be created when given valid email', (done) => {
createForwarder()
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200)
.then(response => {
try {
assert(R.has('forwarder_url')(response.body))
assert(response.body.forwarder_url.startsWith('/f/'))
done()
} catch (e) {
done(e)
}
})
})
})
const bluebird = require('bluebird')
const validateVat = bluebird.promisify(require('validate-vat'))
const vatMoss = require('vat-moss')
/*
Calculates VAT based on:
- country: 2-letter country code from a reliable source e.g credit card
- vatID: FI12345678
- price: integer, cents. (10$ = 1000)
*/
async function calculateVAT(country, vatID, price) {
country = country.trim().toUpperCase()
vatID = vatID.trim().toUpperCase()
// valid VAT ID -> no VAT
if (await validateVatID(country, vatID)) {
return {vatRate: '0', priceWithVat: price, price}
}
// no VAT ID -> must add VAT in EU (luckily, vatmoss takes care of these things)
const vatRate = vatMoss.declaredResidence.calculateRate(country).rate
const priceWithVat = Number(vatRate.times(price).plus(price).toString())
return {
vatRate: vatRate.toString(),
priceWithVat: priceWithVat,
price
}
}
/*
Validates VAT with EU's VAT service (validate-vat library)
- country: 2-letter country code from a reliable source e.g credit card
- vatID: FI12345678
*/
async function validateVatID(country, vatID) {
const vatCountryCode = vatID.slice(0, 2) // First two letters of EE123456789
const countryNumbers = vatID.slice(2) // Numbers part of the VAT number
try {
const res = await validateVat(vatCountryCode, countryNumbers)
return res.valid
} catch (e) {
console.log('VAT-error', e)
return false
}
}
module.exports = {
calculateVAT,
validateVatID
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment