From f783a27045402a24efe92f116f25469dfd72215e Mon Sep 17 00:00:00 2001 From: Cihan Bebek Date: Sat, 8 Jul 2017 22:23:16 +0300 Subject: [PATCH] Membership registration & payment API (#4) * remove newrelic from use in devenv * add endpoint for membership payments * fix some wierd spaces * minor code styling and logging stuff * replace non-breaking spaces with normal ones * remove duplicate function * minor code styling * add functionality for writing new member to google sheets * add config example * update example config, start using config in google credentials * remove var creds from google sheets auth * rename config.example to config.template and fix readme * add async and google-spreadsheet packages * rename workingWithRows to addRow * return missing header from readme * minor code styling * flatten google config structure, add address fields * add request validation to membership endpoint * fix config field names * more error handling, fix indentation --- config.template.json | 31 +++++++++++- index.js | 10 ++-- package.json | 5 ++ routes/feeds.js | 6 +-- routes/invite.js | 10 ++-- routes/members.js | 6 +-- routes/membership.js | 103 +++++++++++++++++++++++++++++++++++++++ utils/validateRequest.js | 12 +++++ 8 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 routes/membership.js create mode 100644 utils/validateRequest.js diff --git a/config.template.json b/config.template.json index 2632874..f49fbaa 100644 --- a/config.template.json +++ b/config.template.json @@ -1,5 +1,32 @@ { - "all": { + "development": { + "stripe": { + "secretKey": "" + }, + "slack": { + "token": "", + "privateChannel": "", + "publicChannel": "" + }, + "github": { + "token": "" + }, + "twitter": { + "consumerKey": "", + "consumerSecret": "", + "token": "", + "tokenSecret": "" + }, + "google": { + "spreadsheetId": "", + "clientEmail": "", + "privateKey": "" + } + }, + "production": { + "stripe": { + "secretKey": "" + }, "slack": { "token": "", "privateChannel": "", @@ -14,5 +41,7 @@ "token": "", "tokenSecret": "" } + }, + "all": { } } diff --git a/index.js b/index.js index f7281c6..5e48f71 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,3 @@ -require('newrelic'); 'use strict'; var express = require('express'); @@ -7,9 +6,13 @@ var cors = require('cors'); var bodyParser = require('body-parser'); var app = express(); +if(app.get('env') != 'development') { + require('newrelic'); +} + app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: true})); -app.use(cors()); +app.use(cors({credentials: true, origin: true})); morgan.token('body', function(req) { return JSON.stringify(req.body); @@ -20,8 +23,9 @@ app.use(morgan(':method :url :status :response-time ms - :res[content-length] :b require('./routes/invite')(app); require('./routes/members')(app); require('./routes/feeds')(app); +require('./routes/membership')(app); -app.use(function(err, req, res, next) { +app.use(function(err, req, res, next) { /*jshint unused:false*/ console.error(err); res.status(500).send('Internal server error'); diff --git a/package.json b/package.json index 5464060..48cf6cf 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,19 @@ "homepage": "https://github.com/koodiklinikka/koodiklinikka.fi-api", "dependencies": { "apicache": "0.0.12", + "async": "^2.5.0", "bluebird": "^2.9.3", "body-parser": "^1.10.1", "cors": "^2.7.1", "express": "^4.11.0", + "google-spreadsheet": "^2.0.4", + "joi": "^10.6.0", "lodash": "^3.10.1", + "moment": "^2.18.1", "morgan": "^1.5.1", "newrelic": "^1.18.0", "node-twitter": "0.5.2", + "stripe": "^4.23.1", "superagent": "^0.21.0", "validator": "^3.27.0" } diff --git a/routes/feeds.js b/routes/feeds.js index 848808f..bf09e4b 100644 --- a/routes/feeds.js +++ b/routes/feeds.js @@ -1,16 +1,16 @@ 'use strict'; -var cache = require('apicache').middleware; +var cache = require('apicache').middleware; var Promise = require('bluebird'); var twitter = require('../services/twitter'); -var github = require('../services/github'); +var github = require('../services/github'); module.exports = function (app) { /* * GET /feeds * Endpoint for fetching different information feeds (Twitter, GitHub etc.) */ - app.get('/feeds', cache('10 minutes'), function(req, res, next) { + app.get('/feeds', cache('10 minutes'), function(req, res, next) { Promise.props({ twitter: twitter.getTweets(40), github: github.getEvents(40) diff --git a/routes/invite.js b/routes/invite.js index f5a0250..9013fd4 100644 --- a/routes/invite.js +++ b/routes/invite.js @@ -1,8 +1,8 @@ 'use strict'; var validator = require('validator'); -var slack = require('../services/slack'); -var github = require('../services/github'); +var slack = require('../services/slack'); +var github = require('../services/github'); module.exports = function (app) { /* @@ -10,13 +10,13 @@ module.exports = function (app) { * Endpoint for sending invitations automatically */ - app.post('/invites', function(req, res, next) { + app.post('/invites', function(req, res, next) { if(!validator.isEmail(req.body.email)) { return res.status(400).send('invalid_email'); } - function success() { + function success() { res.status(200).end(); } @@ -34,7 +34,7 @@ module.exports = function (app) { var message = 'User ' + user.login + ' invited to GitHub organization.' slack.createMessage(message); }) - .catch(function(err) { + .catch(function(err) { var message = 'Creating GitHub invitation failed for: ' + req.body.email + ' reason: ' + err; slack.createMessage(message); }); diff --git a/routes/members.js b/routes/members.js index ebd6f77..4a99eda 100644 --- a/routes/members.js +++ b/routes/members.js @@ -8,8 +8,7 @@ module.exports = function (app) { * GET /members * Endpoint for fetching GitHub org public members */ - - app.get('/members', cache('3 hours'), function(req, res, next) { + app.get('/members', cache('3 hours'), function(req, res, next) { github.getMembers().then(function(data) { res.status(200).send(data); }, function(error) { @@ -21,8 +20,7 @@ module.exports = function (app) { * Post /members * Endpoint for getting an invite to GitHub organization */ - - app.post('/members', function(req, res, next) { + app.post('/members', function(req, res, next) { if(!req.body.username) { return res.status(400).send('invalid_username'); } diff --git a/routes/membership.js b/routes/membership.js new file mode 100644 index 0000000..854cfd2 --- /dev/null +++ b/routes/membership.js @@ -0,0 +1,103 @@ +'use strict'; + +var Promise = require('bluebird'); +var GoogleSpreadsheet = require('google-spreadsheet'); +var async = require('async'); +var moment = require('moment'); +var Joi = require('joi'); + +var slack = require('../services/slack'); +var config = require('../lib/config'); +var stripe = require('stripe')(config.stripe.secretKey); +var validateRequest = require('../utils/validateRequest'); + +function log(message) { + console.log(message); + slack.createMessage(message); +} + +function addNewMemberToSheets(data, callback) { + var {name, email, address, postcode, city, handle} = data; + var doc = new GoogleSpreadsheet(config.google.spreadsheetId); + + async.waterfall([ + function setAuth(cb) { + console.log('Start Google Spreadsheed auth.'); + doc.useServiceAccountAuth({ + client_email: config.google.clientEmail, + private_key: config.google.privateKey + }, (err) => cb(err)); + }, + function getInfoAndWorksheets(cb) { + console.log('Start Google Spreadsheet info fetch.'); + doc.getInfo(function(err, info) { + if(err) { + cb(err); + } else { + cb(null, info.worksheets[0]); + } + }); + }, + function addRow(sheet, cb) { + console.log('Start Google Spreadsheet row write.'); + sheet.addRow({ + 'jäsenmaksu': true, + 'koko nimi': name, + 'liittymispäivä': moment().format('DD.MM.YYYY'), + 'lisääjä': 'Koodiklinikka.fi-api', + 'katuosoite': address, + 'postinumero': postcode, + 'paikkakunta': city, + 'slack': handle, + 'sähköposti': email + }, cb); + } + ], callback); +} + +module.exports = function (app) { + /* + * POST /membership + * Endpoint for adding a new member to the association + */ + + const schema = Joi.object().keys({ + userInfo: Joi.object().keys({ + name: Joi.string().required(), + email: Joi.string().email().required(), + handle: Joi.string().required(), + address: Joi.string().required(), + city: Joi.string().required(), + postcode: Joi.string().required() + }), + stripeToken: Joi.string().required() + }) + + app.post('/membership', validateRequest(schema), function(req, res, next) { + + console.log(`Start membership addition with body: ${JSON.stringify(req.body)}`); + + stripe.charges.create({ + amount: config.membership.price, + card: req.body.stripeToken, + currency: 'eur', + description: `Koodiklinikka ry jäsenyys: ${req.body.name}` + }, function(err, charge) { + if (err) { + log(`Membership payment FAILED for: ${JSON.stringify(req.body)}. Reason: ${err.message}`); + res.status(500).send('payment_error'); + return; + } + + log(`Membership payment SUCCESSFUL for: ${JSON.stringify(req.body)}`); + addNewMemberToSheets(req.body.userInfo, (err) => { + if(err) { + log(`Storing membership info FAILED for: ${JSON.stringify(req.body)}. Reason: ${err.message}`); + res.status(500).send('membership_storage_error'); + return; + } + res.status(200).send('payment_success'); + }); + }); + }); +}; diff --git a/utils/validateRequest.js b/utils/validateRequest.js new file mode 100644 index 0000000..bb6bb77 --- /dev/null +++ b/utils/validateRequest.js @@ -0,0 +1,12 @@ +var Joi = require('joi'); + +module.exports = function validateRequest(schema) { + return function handler(req, res, next) { + Joi.validate(req.body, schema, function (err, value) { + if(err) { + return res.status(400).send(err.details) + } + next(); + }); + } +}