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
This commit is contained in:
Cihan Bebek
2017-07-08 22:23:16 +03:00
committed by Riku Rouvila
parent 181a48c0f6
commit f783a27045
8 changed files with 167 additions and 16 deletions

View File

@@ -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": { "slack": {
"token": "", "token": "",
"privateChannel": "", "privateChannel": "",
@@ -14,5 +41,7 @@
"token": "", "token": "",
"tokenSecret": "" "tokenSecret": ""
} }
},
"all": {
} }
} }

View File

@@ -1,4 +1,3 @@
require('newrelic');
'use strict'; 'use strict';
var express = require('express'); var express = require('express');
@@ -7,9 +6,13 @@ var cors = require('cors');
var bodyParser = require('body-parser'); var bodyParser = require('body-parser');
var app = express(); var app = express();
if(app.get('env') != 'development') {
require('newrelic');
}
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true})); app.use(bodyParser.urlencoded({extended: true}));
app.use(cors()); app.use(cors({credentials: true, origin: true}));
morgan.token('body', function(req) { morgan.token('body', function(req) {
return JSON.stringify(req.body); 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/invite')(app);
require('./routes/members')(app); require('./routes/members')(app);
require('./routes/feeds')(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*/ /*jshint unused:false*/
console.error(err); console.error(err);
res.status(500).send('Internal server error'); res.status(500).send('Internal server error');

View File

@@ -19,14 +19,19 @@
"homepage": "https://github.com/koodiklinikka/koodiklinikka.fi-api", "homepage": "https://github.com/koodiklinikka/koodiklinikka.fi-api",
"dependencies": { "dependencies": {
"apicache": "0.0.12", "apicache": "0.0.12",
"async": "^2.5.0",
"bluebird": "^2.9.3", "bluebird": "^2.9.3",
"body-parser": "^1.10.1", "body-parser": "^1.10.1",
"cors": "^2.7.1", "cors": "^2.7.1",
"express": "^4.11.0", "express": "^4.11.0",
"google-spreadsheet": "^2.0.4",
"joi": "^10.6.0",
"lodash": "^3.10.1", "lodash": "^3.10.1",
"moment": "^2.18.1",
"morgan": "^1.5.1", "morgan": "^1.5.1",
"newrelic": "^1.18.0", "newrelic": "^1.18.0",
"node-twitter": "0.5.2", "node-twitter": "0.5.2",
"stripe": "^4.23.1",
"superagent": "^0.21.0", "superagent": "^0.21.0",
"validator": "^3.27.0" "validator": "^3.27.0"
} }

View File

@@ -10,7 +10,7 @@ module.exports = function (app) {
* GET /feeds * GET /feeds
* Endpoint for fetching different information feeds (Twitter, GitHub etc.) * 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({ Promise.props({
twitter: twitter.getTweets(40), twitter: twitter.getTweets(40),
github: github.getEvents(40) github: github.getEvents(40)

View File

@@ -10,13 +10,13 @@ module.exports = function (app) {
* Endpoint for sending invitations automatically * 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)) { if(!validator.isEmail(req.body.email)) {
return res.status(400).send('invalid_email'); return res.status(400).send('invalid_email');
} }
function success() { function success() {
res.status(200).end(); res.status(200).end();
} }
@@ -34,7 +34,7 @@ module.exports = function (app) {
var message = 'User ' + user.login + ' invited to GitHub organization.' var message = 'User ' + user.login + ' invited to GitHub organization.'
slack.createMessage(message); slack.createMessage(message);
}) })
.catch(function(err) { .catch(function(err) {
var message = 'Creating GitHub invitation failed for: ' + req.body.email + ' reason: ' + err; var message = 'Creating GitHub invitation failed for: ' + req.body.email + ' reason: ' + err;
slack.createMessage(message); slack.createMessage(message);
}); });

View File

@@ -8,8 +8,7 @@ module.exports = function (app) {
* GET /members * GET /members
* Endpoint for fetching GitHub org public 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) { github.getMembers().then(function(data) {
res.status(200).send(data); res.status(200).send(data);
}, function(error) { }, function(error) {
@@ -21,8 +20,7 @@ module.exports = function (app) {
* Post /members * Post /members
* Endpoint for getting an invite to GitHub organization * 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) { if(!req.body.username) {
return res.status(400).send('invalid_username'); return res.status(400).send('invalid_username');
} }

103
routes/membership.js Normal file
View File

@@ -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');
});
});
});
};

12
utils/validateRequest.js Normal file
View File

@@ -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();
});
}
}