diff --git a/.travis.yml b/.travis.yml index 83caf4b..7aefa07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: node_js -services: mongodb +services: + - redis-server + - mongodb before_install: - npm install -g grunt-cli diff --git a/README.md b/README.md index b3e86c7..10c8bb3 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A fast, simple way to build dynamic websites with [Node.js](http://nodejs.org). # Getting Started -Buckets requires [Node.js](http://nodejs.org) and [MongoDB](http://www.mongodb.org). Once you have those, the easiest way to use Buckets is to install it as an NPM module: +Buckets requires [Node.js](http://nodejs.org), [MongoDB](http://www.mongodb.org) and [Redis](http://redis.io). Once you have those, the easiest way to use Buckets is to install it as an NPM module: ```bash npm install buckets @@ -32,8 +32,10 @@ There is a [skeleton project available](https://github.com/bucketsio/skeleton) w ## Installing from this repo + If you plan on contributing to Buckets' development, you can install and deploy Buckets directly from this repo. You will need to install a few extra dependencies for building the frontend files. + ```bash npm install -g grunt-cli npm install diff --git a/app.json b/app.json new file mode 100644 index 0000000..7d86657 --- /dev/null +++ b/app.json @@ -0,0 +1,19 @@ +{ + "name": "Buckets", + "description": "Buckets is a CMS built with Node.js and MongoDB.", + "keywords": [ + "cms", + "node", + "js", + "javascript", + "mongo" + ], + "website": "http://buckets.io", + "logo": "http://buckets.io/images/buckets-logo.svg", + "success_url": "/admin/install", + "env": { + "BUILDPACK_URL": "https://github.com/mbuchetics/heroku-buildpack-nodejs-grunt.git", + "NODE_ENV": "production" + }, + "addons": ["mongohq", "mandrill", "redistogo"] +} diff --git a/package.json b/package.json index 79fb139..84a939a 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,8 @@ "passport": "^0.2.1", "passport-local": "^1.0.0", "path-to-regexp": "^1.0.0", + "ratelimiter": "^1.0.3", + "redis": "^0.12.1", "response-time": "^2.0.1", "serve-favicon": "^2.1.1", "serve-static": "^1.4.3", diff --git a/server/config.coffee b/server/config.coffee index 89a38e3..3eae8bf 100644 --- a/server/config.coffee +++ b/server/config.coffee @@ -15,6 +15,9 @@ config = autoStart: yes cloudinary: process.env.CLOUDINARY_URL db: "mongodb://localhost/buckets_#{env}" + apiHourlyLimit: 100 + redisURL: "redis://127.0.0.1:6379" + loginFailLimit: 10 production: smtp: service: 'Mandrill' @@ -22,6 +25,7 @@ config = user: process.env.MANDRILL_USERNAME pass: process.env.MANDRILL_APIKEY db: process.env.MONGOHQ_URL + redisURL: process.env.REDISTOGO_URL fastly: api_key: process.env.FASTLY_API_KEY cdn_url: process.env.FASTLY_CDN_URL diff --git a/server/redis.coffee b/server/redis.coffee new file mode 100644 index 0000000..d67b8ed --- /dev/null +++ b/server/redis.coffee @@ -0,0 +1,11 @@ +url = require 'url' +redis = require 'redis' +config = require './config' + +redisURL = url.parse config.redisURL +redisDb = redis.createClient(redisURL.port, redisURL.hostname) + +if redisURL.auth + redisDb.auth redisURL.auth.split(':')[1] + +module.exports = redisDb diff --git a/server/routes/admin.coffee b/server/routes/admin.coffee index 3f4d948..fddfab2 100644 --- a/server/routes/admin.coffee +++ b/server/routes/admin.coffee @@ -15,6 +15,8 @@ passport = require '../lib/auth' pkg = require '../../package' User = require '../models/user' + + module.exports = app = express() {adminSegment} = config @@ -40,6 +42,7 @@ app.post '/login', passport.authenticate('local', failureRedirect: "/#{adminSegm "/#{adminSegment}/" app.post '/checkLogin', (req, res) -> + passport.authenticate('local', (err, user, authErr) -> if authErr res.status(401).send errors: [authErr] diff --git a/server/routes/api/index.coffee b/server/routes/api/index.coffee index 54a4bdf..c6f4f70 100644 --- a/server/routes/api/index.coffee +++ b/server/routes/api/index.coffee @@ -1,6 +1,36 @@ -express = require 'express' +express = require 'express' -module.exports = app = express() +Limiter = require 'ratelimiter' +config = require '../../config' +redisDb = require '../../redis' + + + +# use router instead of just express app. +module.exports = app = express.Router() + +app.use (req, res, next) -> + if req.user? + limit = new Limiter({ id: req.user._id, db: redisDb, max: config.apiHourlyLimit }) + limit.get (err, limit) -> + if err? + return next err + + res.set 'X-RateLimit-Limit', limit.total + res.set 'X-RateLimit-Remaining', limit.remaining + res.set 'X-RateLimit-Reset', limit.reset + + # all good + if limit.remaining + return next() + + # not good + delta = (limit.reset * 1000) - Date.now() | 0 + after = limit.reset - (Date.now() / 1000) | 0 + res.set 'Retry-After', after + res.status(429).send('Rate limit exceeded, retry in ' + delta) + else + next() app.use require './buckets' app.use require './entries'