diff options
Diffstat (limited to 'server/lib')
| -rw-r--r-- | server/lib/api/index.js | 1 | ||||
| -rw-r--r-- | server/lib/api/profile.js | 1 | ||||
| -rw-r--r-- | server/lib/api/subscription.js | 22 | ||||
| -rw-r--r-- | server/lib/middleware.js | 2 | ||||
| -rw-r--r-- | server/lib/schemas/Plan.js | 44 | ||||
| -rw-r--r-- | server/lib/schemas/Subscription.js | 26 | ||||
| -rw-r--r-- | server/lib/schemas/User.js | 6 | ||||
| -rw-r--r-- | server/lib/views/index.js | 28 | ||||
| -rw-r--r-- | server/lib/views/staff.js | 231 | ||||
| -rw-r--r-- | server/lib/webhook/index.js | 44 | ||||
| -rw-r--r-- | server/lib/webhook/recurly-config.js | 6 | ||||
| -rw-r--r-- | server/lib/webhook/webhook.js | 140 |
12 files changed, 540 insertions, 11 deletions
diff --git a/server/lib/api/index.js b/server/lib/api/index.js index 11e13fc..9478d9b 100644 --- a/server/lib/api/index.js +++ b/server/lib/api/index.js @@ -8,6 +8,7 @@ var api = { projects: require('./projects'), rooms: require('./rooms'), collaborator: require('./collaborator'), + subscription: require('./subscription'), } module.exports = api diff --git a/server/lib/api/profile.js b/server/lib/api/profile.js index 996505f..d72a2c3 100644 --- a/server/lib/api/profile.js +++ b/server/lib/api/profile.js @@ -32,6 +32,7 @@ var profile = { delete data.old_password delete data.new_password delete data.isStaff + delete data.plan_level data.updated_at = new Date () if (req.files.avatar) { diff --git a/server/lib/api/subscription.js b/server/lib/api/subscription.js new file mode 100644 index 0000000..6fe8c61 --- /dev/null +++ b/server/lib/api/subscription.js @@ -0,0 +1,22 @@ +/* jshint node: true */ + +var _ = require('lodash'), + util = require('../util'), + upload = require('../upload'), + config = require('../../../config.json'), + User = require('../schemas/User'), + Project = require('../schemas/Project'), + Layout = require('../schemas/Layout'), + Subscription = require('../schemas/Subscription'); + +var subscription = module.exports = { + +/* + index: function(req, res){ + Project.find({ user_id: req.user._id }, function(err, docs){ + res.json(docs) + }) + }, +*/ + +};
\ No newline at end of file diff --git a/server/lib/middleware.js b/server/lib/middleware.js index 4848ab0..797d677 100644 --- a/server/lib/middleware.js +++ b/server/lib/middleware.js @@ -75,7 +75,7 @@ var middleware = { } next() }) - } + } else { req.project = null next() diff --git a/server/lib/schemas/Plan.js b/server/lib/schemas/Plan.js new file mode 100644 index 0000000..8a19b99 --- /dev/null +++ b/server/lib/schemas/Plan.js @@ -0,0 +1,44 @@ +/* jshint node: true */ + +var mongoose = require('mongoose'), + _ = require('lodash'), + crypto = require('crypto'), + config = require('../../../config.json'), + util = require('../util'); + +var PlanSchema = new mongoose.Schema({ + name: { type: String }, + slug: { type: String }, + + level: { type: Number }, + + monthly_price: { type: Number }, + yearly_price: { type: Number }, + + basic_layout_monthly_price: { type: Number }, + basic_layout_yearly_price: { type: Number }, + + pro_layout_monthly_price: { type: Number }, + pro_layout_yearly_price: { type: Number }, + + basic_layout_limit: { type: Number }, + pro_layout_limit: { type: Number }, + + stock_project_limit: { type: Number }, + basic_project_limit: { type: Number }, + pro_project_limit: { type: Number }, + + permissions: { + basic_editor: { type: Boolean, default: false }, + pro_editor: { type: Boolean, default: false }, + solids: { type: Boolean, default: false }, + collaborators: { type: Boolean, default: false }, + no_logo: { type: Boolean, default: false }, + }, + + created_at: { type: Date, default: Date.now }, + updated_at: { type: Date, default: Date.now }, +}) + +module.exports = exports = mongoose.model('plan', PlanSchema); +exports.schema = PlanSchema; diff --git a/server/lib/schemas/Subscription.js b/server/lib/schemas/Subscription.js new file mode 100644 index 0000000..2f49ea1 --- /dev/null +++ b/server/lib/schemas/Subscription.js @@ -0,0 +1,26 @@ +/* jshint node: true */ + +var mongoose = require('mongoose'), + _ = require('lodash'), + crypto = require('crypto'), + config = require('../../../config.json'), + util = require('../util'); + +var SubscriptionSchema = new mongoose.Schema({ + user_id: { type: mongoose.Schema.ObjectId, index: true }, + + plan_type: { type: String, default: "free" }, + plan_period: { type: String, default: "monthly" }, + + subscription_uuid: { type: String }, + subscription_add_ons: [{ + name: { type: String }, + quantity: { type: Number }, + }], + + created_at: { type: Date, default: Date.now }, + updated_at: { type: Date, default: Date.now }, +}) + +module.exports = exports = mongoose.model('subscription', SubscriptionSchema); +exports.schema = SubscriptionSchema; diff --git a/server/lib/schemas/User.js b/server/lib/schemas/User.js index 180a140..06ad33d 100644 --- a/server/lib/schemas/User.js +++ b/server/lib/schemas/User.js @@ -54,7 +54,11 @@ var UserSchema = new mongoose.Schema({ type: String, default: "", }, - + + plan_level: { type: Number, default: 0 }, + plan_type: { type: String, default: "free" }, + last_charged: { type: Date, default: null }, + location: { type: String, default: "" }, photo: { type: String, default: "" }, bio: { type: String, default: "" }, diff --git a/server/lib/views/index.js b/server/lib/views/index.js index 8c3e63d..0ce0357 100644 --- a/server/lib/views/index.js +++ b/server/lib/views/index.js @@ -4,6 +4,7 @@ var User = require('../schemas/User'), Project = require('../schemas/Project'), Documentation = require('../schemas/Documentation'), Collaborator = require('../schemas/Collaborator'), + Plan = require('../schemas/Plan'), config = require('../../../config'), marked = require('marked'), util = require('../util'), @@ -88,13 +89,6 @@ var views = module.exports = { }, home: function (req, res) { - // while in development, blank homepage if not logged in -/* - if (! req.user) { - res.send("<html></html>") - return - } -*/ views_middleware.fetchProjects({ featured: true }, null, null, function(err, projects){ res.render('home', { projects: projects || [] @@ -117,6 +111,13 @@ var views = module.exports = { res.render('about/' + name) return } + if (name == "brochure" || name == "plans") { + // TODO: fetch plans + views_middleware.ensurePlans(req, res, function(){ + res.render('about/' + name) + }) + return + } if (name == "about" || name == "index") { res.render('about/' + name) return @@ -225,6 +226,19 @@ var views = module.exports = { } var views_middleware = { + ensurePlans: function(req, res, next){ + Plan.find(function (err, plans) { + res.locals.plans = {} + plans.forEach(function(plan){ + res.locals.plans[ plan.slug ] = plan + "monthly_price yearly_price basic_layout_monthly_price basic_layout_yearly_price pro_layout_monthly_price pro_layout_yearly_price".split(" ").forEach(function(key){ + plan[key] = (plan[key]/100).toFixed(2)+"" + }) + }) + next() + }) + }, + fetchProjects: function (criteria, limit, offset, next) { limit = limit || 7 offset = offset || 0 diff --git a/server/lib/views/staff.js b/server/lib/views/staff.js index 49f492b..74dd7cd 100644 --- a/server/lib/views/staff.js +++ b/server/lib/views/staff.js @@ -4,6 +4,8 @@ var User = require('../schemas/User'), Project = require('../schemas/Project'), Media = require('../schemas/Media'), Collaborator = require('../schemas/Collaborator'), + Plan = require('../schemas/Plan'), + Subscription = require('../schemas/Subscription'), config = require('../../../config'), middleware = require('../middleware'), util = require('../util'), @@ -16,6 +18,10 @@ var staff = module.exports = { fields: { user: "_id username displayName photo created_at updated_at last_seen created_ip last_ip", project: "_id name slug user_id privacy created_at updated_at", + plans: "monthly_price yearly_price basic_layout_monthly_price basic_layout_yearly_price " + + "pro_layout_monthly_price pro_layout_yearly_price " + + "basic_layout_limit pro_layout_limit stock_project_limit basic_project_limit pro_project_limit", + plans_permissions: "basic_editor pro_editor solids collaborators no_logo", }, defaults: { @@ -119,6 +125,77 @@ var staff = module.exports = { }) }, + ensureSubscriptions: function(req, res, next){ + var paginationInfo = res.locals.pagination = {} + var criteria = req.criteria || {} + var limit = paginationInfo.limit = Math.min( Number(req.query.limit) || 50, 200 ) + var offset = paginationInfo.offset = Number(req.query.offset) || 0 + var sort + paginationInfo.sort = req.query.sort + paginationInfo.sortOptions = ["date", "name"] + switch (req.query.sort) { + case 'created': + sort = {'created_at': -1} + break + default: + case 'date': + sort = {'updated_at': -1} + break + } + Subscription.find(criteria) + .select(staff.fields.project) + .sort(sort) + .skip(offset) + .limit(limit) + .exec(function (err, subscriptions) { + res.locals.subscriptions = subscriptions.map(staff.helpers.subscription) + next() + }) + }, + + ensurePlans: function(req, res, next){ + Plan.find(function (err, plans) { + res.locals.plans = (plans || []).map(staff.helpers.plan) + res.locals.plans.sort(function(a,b){ return a.monthly_price }) + next() + }) + }, + ensurePlan: function (req, res, next) { + if (req.params.slug) { + Plan.findOne({ slug: req.params.slug }, function(err, plan){ + if (err || ! plan) { + console.error(err) + res.redirect("/staff/plans/") + } + else { + req.plan = plan + next() + } + }) + } + else { + res.redirect("/staff/plans/") + } + }, + + ensureSubscription: function (req, res, next) { + if (req.params.id) { + Subscription.findOne({ _id: req.params.id }, function(err, subscription){ + if (err || ! subscription) { + console.error(err) + res.redirect("/staff/subscriptions/") + } + else { + req.subscription = subscription + next() + } + }) + } + else { + res.redirect("/staff/subscriptions/") + } + }, + ensureRecentProjects: function(req, res, next){ var dreq = { params: { sort: 'created_at', limit: 20, offset: 0 } } staff.middleware.ensureProjects(dreq, res, next) @@ -129,11 +206,23 @@ var staff = module.exports = { staff.middleware.ensureObjectsUsers(res.locals.projects, next) }, + ensureSubscriptionsUsers: function(req, res, next){ + if (! res.locals.subscriptions || ! res.locals.subscriptions.length) { return next() } + staff.middleware.ensureObjectsUsers(res.locals.subscriptions, next) + }, + ensureMediaUsers: function(req, res, next){ if (! res.locals.media || ! res.locals.media.length) { return next() } staff.middleware.ensureObjectsUsers(res.locals.media, next) }, + ensureSubscriptionUser: function(req, res, next){ + if (! res.locals.subscription) { return next() } + staff.middleware.ensureObjectsUsers([ res.locals.subscription ], function(){ + next() + }) + }, + ensureMediaUser: function(req, res, next){ if (! res.locals.media) { return next() } staff.middleware.ensureObjectsUsers([ res.locals.media ], function(){ @@ -299,7 +388,21 @@ var staff = module.exports = { media.user = {} media.shortUrl = media.url.replace(/^http.:\/\//,"") return media - } + }, + + plan: function(plan){ + plan = plan.toObject() + plan.date = moment( plan.updated_at || plan.created_at ).format("M/DD/YYYY hh:mm a") + plan.user = {} + return plan + }, + + subscription: function(subscription){ + subscription = subscription.toObject() + subscription.date = moment( subscription.updated_at || subscription.created_at ).format("M/DD/YYYY hh:mm a") + subscription.user = {} + return subscription + }, }, route: function(app){ @@ -422,7 +525,67 @@ var staff = module.exports = { staff.media.show ); + + // + // plans + + app.get('/staff/plans', + middleware.ensureAuthenticated, + middleware.ensureIsStaff, + + staff.middleware.ensurePlans, + + staff.plans.index + ); + app.get('/staff/plans/new', + middleware.ensureAuthenticated, + middleware.ensureIsStaff, + + staff.plans.new + ); + app.post('/staff/plans/new', + middleware.ensureAuthenticated, + middleware.ensureIsStaff, + + staff.plans.create + ); + app.get('/staff/plans/:slug', + middleware.ensureAuthenticated, + middleware.ensureIsStaff, + + staff.middleware.ensurePlan, + staff.plans.edit + ); + app.post('/staff/plans/:slug', + middleware.ensureAuthenticated, + middleware.ensureIsStaff, + + staff.middleware.ensurePlan, + + staff.plans.update + ); + + // + // subscriptions + app.get('/staff/subscriptions', + middleware.ensureAuthenticated, + middleware.ensureIsStaff, + + staff.middleware.ensureSubscriptions, + staff.middleware.ensureSubscriptionsUsers, + + staff.subscriptions.index + ); + app.get('/staff/subscriptions/:id', + middleware.ensureAuthenticated, + middleware.ensureIsStaff, + + staff.middleware.ensureSubscription, + staff.middleware.ensureSubscriptionUser, + + staff.subscriptions.show + ); }, paginate: function(req, res){ @@ -540,6 +703,70 @@ var staff = module.exports = { res.render('staff/media/show_404') } }, - } + }, + + plans: { + index: function(req, res){ + res.locals.fields = staff.fields.plans.split(" ") + res.locals.permissions = staff.fields.plans_permissions.split(" ") + res.render('staff/plans/index') + }, + new: function(req, res){ + res.locals.plan = new Plan () + res.render('staff/plans/new') + }, + edit: function(req, res){ + res.locals.plan = req.plan + res.render('staff/plans/edit') + }, + create: function(req, res){ + var plan = new Plan () + var fields = staff.fields.plans.split(" ") + var permissions = staff.fields.plans_permissions.split(" ") + + var data = util.cleanQuery(req.body) + data.name = util.sanitize(data.name) + data.slug = util.sanitize(data.slug.toLowerCase()) + + permissions.forEach(function(field){ + data[field] = data["permissions_" + field] + }) + + new Plan (data).save(function(err, doc){ + if (err || ! doc) { return res.json({ error: err }) } + res.redirect("/staff/plans/") + }) + }, + update: function(req, res){ + var fields = staff.fields.plans.split(" ") + var permissions = staff.fields.plans_permissions.split(" ") + + var data = util.cleanQuery(req.body) + data.name = util.sanitize(data.name) + data.slug = util.sanitize(data.slug.toLowerCase()) + + _.extend(req.plan, data) + permissions.forEach(function(field){ + req.plan.permissions[field] = data["permissions_" + field].length == 2 + }) + + req.plan.save(function(err, doc){ + if (err || ! doc) { return res.json({ error: err }) } + res.redirect("/staff/plans/") + }) + }, + }, + + subscriptions: { + index: function(req, res){ + res.locals.pagination.count = res.locals.subscriptions.length + res.locals.pagination.max = res.locals.subscriptionCount + staff.paginate(req, res) + res.render('staff/subscriptions/index') + }, + show: function(req, res){ + res.render('staff/subscriptions/show') + }, + }, } diff --git a/server/lib/webhook/index.js b/server/lib/webhook/index.js new file mode 100644 index 0000000..11419c2 --- /dev/null +++ b/server/lib/webhook/index.js @@ -0,0 +1,44 @@ +var config = require('../../../config.json'), + http = require('http'), + express = require('express'), + bodyParser = require('body-parser'), + mongoose = require('mongoose'); + +var http = require('http'), + express = require('express'), + bodyParser = require('body-parser'), + multer = require('multer'), + MongoStore = require('connect-mongo')(express), + passport = require('passport'), + path = require('path'), + mongoose = require('mongoose'); + +var webhook = require('./webhook'); + +var app = express() +var server +var DATABASE_URI = process.env.MONGOLAB_URI || ('mongodb://' + config.databaseHost + '/vvalls') + +var site = {} + +site.init = function(){ + mongoose.connect(DATABASE_URI, {}, site.ready); +} + +site.ready = function(){ + app.set('port', config.webhookPort); + app.use(bodyParser()); + app.use(express.query()); + app.set('env', config.env.production ? "production" : "development") + app.get('env') === 'development' && app.use(express.errorHandler()); + + server = http.createServer(app) + server.listen(app.get('port'), function () { + console.log('Webhook server listening on port ' + app.get('port')); + }); + + app.get('/', function(req,res){ res.send('HI THERE') }) + webhook.route(app) +} + +site.init() diff --git a/server/lib/webhook/recurly-config.js b/server/lib/webhook/recurly-config.js new file mode 100644 index 0000000..3d7e1c5 --- /dev/null +++ b/server/lib/webhook/recurly-config.js @@ -0,0 +1,6 @@ +module.exports = { + API_KEY: process.env['VVALLS_RECURLY_SECRET'], + SUBDOMAIN: 'vvalls', + ENVIRONMENT: 'sandbox', + DEBUG: true, +}; diff --git a/server/lib/webhook/webhook.js b/server/lib/webhook/webhook.js new file mode 100644 index 0000000..e9a7925 --- /dev/null +++ b/server/lib/webhook/webhook.js @@ -0,0 +1,140 @@ +// // where should this live? +// app.get('/subscribe/webhook', views.subscription.webhook); + +/* +app.use(express.basicAuth(function(user, pass, callback) { + var result = (user === 'testUser' && pass === 'testPass'); + callback(null, result); +})); +*/ + + +/* jshint node: true */ + +var User = require('../schemas/User'), + Subscription = require('../schemas/Subscription'), + config = require('../../../config'), + middleware = require('../middleware'), + util = require('../util'), + _ = require('lodash'), + moment = require('moment'), + xml2js = require('xml2js'), + Recurly = require('node-recurly'), + recurly = new Recurly(require('./recurly-config')); + +var parser = new xml2js.Parser(); + +var subscribe = module.exports = { + plan_level: { + free: 0, + basic: 1, + pro: 2, + }, + + callbacks: { +/* + // accounts + new_account_notification: function(data, user){ + // fires on successful signup + }, + canceled_account_notification: function(data, user){ + }, + billing_info_updated_notification: function(data, user){ + }, + reactivated_account_notification: function(data, user){ + }, + + // invoices + new_invoice_notification: function(data, user){ + }, + closed_invoice_notification: function(data, user){ + }, + past_due_invoice_notification: function(data, user){ + }, +*/ + + // subscriptions + new_subscription_notification: function(data, user){ + var account = data.account + Subscription.findOne({ "uuid": data.subscription.uuid }, function(err, subscription){ + if (err || subscription) return; + + var plan = data.subscription.plan.plan_code.split("-") + var plan_type = plan[0] + var plan_period = plan[1] + + user.plan_type = plan_type + user.plan_level = subscribe.plan_level[plan_type] + + var subscriber = new Subscription () + subscriber.uuid = data.subscription.uuid + subscriber.user_id = user._id + subscriber.plan_type = plan_type + subscriber.plan_period = plan_period + subscriber.plan_level = subscribe.plan_level[plan_type] + subscriber.add_ons = subscription.add_ons.map(function(add_on){ + return { + name: add_on.plan, + quantity: add_on.quantity, + } + }) + subscriber.save(function(err, data){ + if (err) return; + user.save(function(err){ + // saved! + }) + }) + }) + }, + +/* + updated_subscription_notification: function(data, user){ + }, + canceled_subscription_notification: function(data, user){ + }, + expired_subscription_notification: function(data, user){ + }, + renewed_subscription_notification: function(data, user){ + }, +*/ + // payments + successful_payment_notification: function(data, user){ + var account = data.account + user.last_charged = new Date(data.transaction.date) + user.save(function(){ + }) + }, +/* + failed_payment_notification: function(data, user){ + }, + successful_refund_notification: function(data, user){ + }, + void_payment_notification: function(data, user){ + }, +*/ + }, + + execute: function(action, data){ + User.findOne({ _id: data.account.account_code }, function(err, user){ + if (err) { return } + subscribe.callbacks[action](data, user) + }) + }, + + // then calls to get appropriate info from the recurly api + handle: function(req, res){ + res.status(200).end() + parser.parseString(data, function (err, result) { + console.log(inspect(result, { colors: true, depth: Infinity })); + for (var action in result) { + if (subscribe.callbacks[action]) { + subscribe.execute(action, result[action]); + } + } + }); + }, + + route: function(app){ + app.post('/subscribe/webhook', subscribe.handle); + }, +} |
