Created
August 28, 2017 18:42
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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]>', | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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) | |
} | |
}) | |
}) | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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