Created
July 17, 2015 17:17
-
-
Save AkarshSatija/71a544c3d4d4ab1c34d7 to your computer and use it in GitHub Desktop.
diff file for Setting up oAuth2 server in MEAN.JS(meanjs.org)
This file contains 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
diff --git a/app/controllers/oauth.server.controller.js b/app/controllers/oauth.server.controller.js | |
new file mode 100644 | |
index 0000000..a3c0c7a | |
--- /dev/null | |
+++ b/app/controllers/oauth.server.controller.js | |
@@ -0,0 +1,208 @@ | |
+'use strict'; | |
+ | |
+/** | |
+ * Module dependencies. | |
+ */ | |
+var mongoose = require('mongoose'), | |
+ _ = require('lodash'); | |
+ | |
+ | |
+var oauth2orize = require('oauth2orize'), | |
+ passport = require('passport'), | |
+ crypto = require('crypto')/*, | |
+ config = require('./config')*/; | |
+ | |
+var faker = require('Faker'); | |
+ | |
+var UserModel = mongoose.model('User'), | |
+ ClientModel = mongoose.model('Client'), | |
+ AccessTokenModel = mongoose.model('AccessToken'), | |
+ RefreshTokenModel = mongoose.model('RefreshToken'); | |
+ | |
+// create OAuth 2.0 server | |
+var server = oauth2orize.createServer(); | |
+ | |
+// Exchange username & password for an access token. | |
+server.exchange(oauth2orize.exchange.password(function(client, username, password, scope, done) { | |
+ UserModel.findOne({ username: username }, function(err, user) { | |
+ if (err) { return done(err); } | |
+ if (!user) { return done(null, false); } | |
+ if (!user.authenticate(password)) { return done(null, false); } | |
+ | |
+ RefreshTokenModel.remove({ userId: user.userId, clientId: client.clientId }, function (err) { | |
+ if (err) return done(err); | |
+ }); | |
+ AccessTokenModel.remove({ userId: user.userId, clientId: client.clientId }, function (err) { | |
+ if (err) return done(err); | |
+ }); | |
+ | |
+ var tokenValue = crypto.randomBytes(32).toString('hex'); | |
+ var refreshTokenValue = crypto.randomBytes(32).toString('hex'); | |
+ var token = new AccessTokenModel({ token: tokenValue, clientId: client.clientId, userId: user.id }); | |
+ | |
+ var refreshToken = new RefreshTokenModel({ | |
+ token: refreshTokenValue, | |
+ clientId: client.clientId, | |
+ userId: user.id }); | |
+ console.log(refreshToken); | |
+ refreshToken.save(function (err) { | |
+ | |
+ if (err) { return done(err); } | |
+ }); | |
+ var info = { scope: '*' } | |
+ token.save(function (err, token) { | |
+ if (err) { return done(err); } | |
+ done(null, tokenValue, refreshTokenValue, { 'expires_in': 3600/*config.get('security:tokenLife')*/ }); | |
+ }); | |
+ }); | |
+})); | |
+ | |
+// Exchange refreshToken for an access token. | |
+server.exchange(oauth2orize.exchange.refreshToken(function(client, refreshToken, scope, done) { | |
+ RefreshTokenModel.findOne({ token: refreshToken }, function(err, token) { | |
+ if (err) { return done(err); } | |
+ if (!token) { return done(null, false); } | |
+ if (!token) { return done(null, false); } | |
+ | |
+ UserModel.findById(token.userId, function(err, user) { | |
+ if (err) { return done(err); } | |
+ if (!user) { return done(null, false); } | |
+ | |
+ RefreshTokenModel.remove({ userId: user.userId, clientId: client.clientId }, function (err) { | |
+ if (err) return done(err); | |
+ }); | |
+ AccessTokenModel.remove({ userId: user.userId, clientId: client.clientId }, function (err) { | |
+ if (err) return done(err); | |
+ }); | |
+ | |
+ var tokenValue = crypto.randomBytes(32).toString('hex'); | |
+ var refreshTokenValue = crypto.randomBytes(32).toString('hex'); | |
+ var token = new AccessTokenModel({ token: tokenValue, clientId: client.clientId, userId: user.id }); | |
+ var refreshToken = new RefreshTokenModel({ token: refreshTokenValue, clientId: client.clientId, userId: user.id }); | |
+ refreshToken.save(function (err) { | |
+ if (err) { return done(err); } | |
+ }); | |
+ var info = { scope: '*' } | |
+ token.save(function (err, token) { | |
+ if (err) { return done(err); } | |
+ done(null, tokenValue, refreshTokenValue, { 'expires_in': 3600/*config.get('security:tokenLife')*/ }); | |
+ }); | |
+ }); | |
+ }); | |
+})); | |
+ | |
+// token endpoint | |
+exports.token = [ | |
+ passport.authenticate(['basic', 'oauth2-client-password'], { session: false }), | |
+ server.token(), | |
+ server.errorHandler() | |
+]; | |
+ | |
+ | |
+ | |
+/** | |
+ * Setting up oauth Tokens and users | |
+ */ | |
+var out=[]; | |
+exports.setup = function(req, res) { | |
+ /*UserModel.remove({}, function(err) { | |
+ var user = new UserModel({ username: "andrey", password: "simplepassword", wpId:'12345678', provider:"local",bookmarks:[] }); | |
+ user.save(function(err, user) { | |
+ if(err){ | |
+ return console.log(err); exit; | |
+ } | |
+ else | |
+ console.log("New user - %s:%s",user.username,user.password); | |
+ });*/ | |
+ | |
+ /*for(var i=0; i<4; i++) { | |
+ var user = new UserModel({ username: faker.random.first_name().toLowerCase(), password: faker.Lorem.words(1)[0] , wpId:'12345678', Provider:"local",bookmarks:[] }); | |
+ user.save(function(err, user) { | |
+ if(err){ | |
+ return console.log(err); exit; | |
+ } | |
+ else | |
+ console.log("New user - %s:%s",user.username,user.password); | |
+ }); | |
+ }*/ | |
+ // }); | |
+ | |
+ ClientModel.remove({}, function(err) { | |
+ var client = new ClientModel({ name: "OurService iOS client v1", clientId: "mobileV1", clientSecret:"abc123456" }); | |
+ client.save(function(err, client) { | |
+ if(err){ | |
+ return console.log(err); exit; | |
+ } | |
+ else | |
+ console.log("New client - %s:%s",client.clientId,client.clientSecret); | |
+ }); | |
+ }); | |
+ | |
+ /*AccessTokenModel.remove({}, function (err) { | |
+ if (err){ | |
+ return console.log(err); exit; | |
+ } | |
+ }); | |
+ | |
+ RefreshTokenModel.remove({}, function (err) { | |
+ if (err){ | |
+ return console.log(err); exit; | |
+ } | |
+ });*/ | |
+ | |
+ /*setTimeout(function() { | |
+ mongoose.disconnect(); | |
+ res.json(out); | |
+ }, 3000);*/ | |
+}; | |
+ | |
+ | |
diff --git a/app/controllers/users/users.authorization.server.controller.js b/app/controllers/users/users.authorization.server.controller.js | |
index 932e490..be1192d 100644 | |
--- a/app/controllers/users/users.authorization.server.controller.js | |
+++ b/app/controllers/users/users.authorization.server.controller.js | |
@@ -5,7 +5,8 @@ | |
*/ | |
var _ = require('lodash'), | |
mongoose = require('mongoose'), | |
- User = mongoose.model('User'); | |
+ User = mongoose.model('User'), | |
+ passport = require('passport'); | |
/** | |
* User middleware | |
@@ -24,14 +25,31 @@ exports.userByID = function(req, res, next, id) { | |
*/ | |
exports.requiresLogin = function(req, res, next) { | |
if (!req.isAuthenticated()) { | |
- return res.status(401).send({ | |
- message: 'User is not logged in' | |
- }); | |
+ //call another auth method | |
+ exports.oauthLogin(req, res, next); | |
} | |
- | |
- next(); | |
+ else | |
+ next(); | |
}; | |
+exports.oauthLogin = function(req, res, next) { | |
+ passport.authenticate('bearer', { session: false })(req,res,function(){ | |
+ exports.requireOauthLogin(req, res, next); | |
+ }); | |
+} | |
+ | |
+ | |
+exports.requireOauthLogin = function(req, res, next) { | |
+ if (!req.isAuthenticated()) { | |
+ console.log("err"); | |
+ | |
+ } | |
+ else | |
+ next(); | |
+} | |
+ | |
+ | |
+ | |
/** | |
* User authorizations routing middleware | |
*/ | |
diff --git a/app/models/client.server.model.js b/app/models/client.server.model.js | |
index 1737b96..563b0e3 100644 | |
--- a/app/models/client.server.model.js | |
+++ b/app/models/client.server.model.js | |
@@ -20,10 +20,15 @@ var ClientSchema = new Schema({ | |
type: Date, | |
default: Date.now | |
}, | |
- user: { | |
- type: Schema.ObjectId, | |
- ref: 'User' | |
- } | |
+ clientId: { | |
+ type: String, | |
+ unique: true, | |
+ required: true | |
+ }, | |
+ clientSecret: { | |
+ type: String, | |
+ required: true | |
+ } | |
}); | |
mongoose.model('Client', ClientSchema); | |
\ No newline at end of file | |
diff --git a/app/models/oauth.server.model.js b/app/models/oauth.server.model.js | |
new file mode 100644 | |
index 0000000..5c65186 | |
--- /dev/null | |
+++ b/app/models/oauth.server.model.js | |
@@ -0,0 +1,58 @@ | |
+'use strict'; | |
+ | |
+/** | |
+ * Module dependencies. | |
+ */ | |
+var mongoose = require('mongoose'), | |
+ Schema = mongoose.Schema; | |
+ | |
+/** | |
+ * Oauth Schema | |
+ */ | |
+ | |
+ | |
+// AccessToken | |
+var AccessToken = new Schema({ | |
+ userId: { | |
+ type: String, | |
+ required: true | |
+ }, | |
+ clientId: { | |
+ type: String, | |
+ required: true | |
+ }, | |
+ token: { | |
+ type: String, | |
+ unique: true, | |
+ required: true | |
+ }, | |
+ created: { | |
+ type: Date, | |
+ default: Date.now | |
+ } | |
+}); | |
+ | |
+mongoose.model('AccessToken', AccessToken); | |
+ | |
+// RefreshToken | |
+var RefreshToken = new Schema({ | |
+ userId: { | |
+ type: String, | |
+ required: true | |
+ }, | |
+ clientId: { | |
+ type: String, | |
+ required: true | |
+ }, | |
+ token: { | |
+ type: String, | |
+ unique: true, | |
+ required: true | |
+ }, | |
+ created: { | |
+ type: Date, | |
+ default: Date.now | |
+ } | |
+}); | |
+ | |
+mongoose.model('RefreshToken', RefreshToken); | |
diff --git a/app/routes/articles.server.routes.js b/app/routes/articles.server.routes.js | |
index 6cdcc96..db0a908 100644 | |
--- a/app/routes/articles.server.routes.js | |
+++ b/app/routes/articles.server.routes.js | |
@@ -9,7 +9,7 @@ var users = require('../../app/controllers/users.server.controller'), | |
module.exports = function(app) { | |
// Article Routes | |
app.route('/articles') | |
- .get(articles.list) | |
+ .get(users.requiresLogin, articles.list) | |
.post(users.requiresLogin, articles.create); | |
app.route('/articles/:articleId') | |
diff --git a/app/routes/oauth.server.routes.js b/app/routes/oauth.server.routes.js | |
new file mode 100644 | |
index 0000000..c5aaf97 | |
--- /dev/null | |
+++ b/app/routes/oauth.server.routes.js | |
@@ -0,0 +1,53 @@ | |
+'use strict'; | |
+ | |
+ | |
+/** | |
+ * Module dependencies. | |
+ */ | |
+var users = require('../../app/controllers/users.server.controller'), | |
+ oauth2 = require('../../app/controllers/oauth.server.controller'); | |
+ | |
+ | |
+var passport = require('passport'); | |
+ | |
+module.exports = function(app) { | |
+ // Routing logic | |
+ // ... | |
+ | |
+ app.post('/oauth/token', oauth2.token); | |
+ | |
+ app.get('/oauth/setup', oauth2.setup); | |
+ | |
+ | |
+ | |
+/* Test URLs*/ | |
+ app.get('/api/userInfo',users.requiresLogin, | |
+ | |
+ function(req, res) { | |
+ // req.authInfo is set using the `info` argument supplied by | |
+ // `BearerStrategy`. It is typically used to indicate a scope of the token, | |
+ // and used in access control checks. For illustrative purposes, this | |
+ // example simply returns the scope in the response. | |
+ if(req.isAuthenticated()) | |
+ res.json(req.user) | |
+ else | |
+ res.json({message:"not logged in"}) | |
+ } | |
+ ); | |
+ | |
+ | |
+ app.get('/api/userinf', | |
+ passport.authenticate('bearer', { session: false }), | |
+ function(req, res) { | |
+ // req.authInfo is set using the `info` argument supplied by | |
+ // `BearerStrategy`. It is typically used to indicate a scope of the token, | |
+ // and used in access control checks. For illustrative purposes, this | |
+ // example simply returns the scope in the response. | |
+ res.json({ user_id: req.user.id, name: req.user.username, authInfo: req.authInfo }) | |
+ } | |
+ ); | |
+ | |
+ | |
+ | |
+ | |
+}; | |
\ No newline at end of file | |
diff --git a/app/tests/oauth.server.model.test.js b/app/tests/oauth.server.model.test.js | |
new file mode 100644 | |
index 0000000..6148085 | |
--- /dev/null | |
+++ b/app/tests/oauth.server.model.test.js | |
@@ -0,0 +1,55 @@ | |
+'use strict'; | |
+ | |
+/** | |
+ * Module dependencies. | |
+ */ | |
+var should = require('should'), | |
+ mongoose = require('mongoose'), | |
+ User = mongoose.model('User'), | |
+ Oauth = mongoose.model('Oauth'); | |
+ | |
+/** | |
+ * Globals | |
+ */ | |
+var user, oauth; | |
+ | |
+/** | |
+ * Unit tests | |
+ */ | |
+describe('Oauth Model Unit Tests:', function() { | |
+ beforeEach(function(done) { | |
+ user = new User({ | |
+ firstName: 'Full', | |
+ lastName: 'Name', | |
+ displayName: 'Full Name', | |
+ email: 'test@test.com', | |
+ username: 'username', | |
+ password: 'password' | |
+ }); | |
+ | |
+ user.save(function() { | |
+ oauth = new Oauth({ | |
+ // Add model fields | |
+ // ... | |
+ }); | |
+ | |
+ done(); | |
+ }); | |
+ }); | |
+ | |
+ describe('Method Save', function() { | |
+ it('should be able to save without problems', function(done) { | |
+ return oauth.save(function(err) { | |
+ should.not.exist(err); | |
+ done(); | |
+ }); | |
+ }); | |
+ }); | |
+ | |
+ afterEach(function(done) { | |
+ Oauth.remove().exec(); | |
+ User.remove().exec(); | |
+ | |
+ done(); | |
+ }); | |
+}); | |
\ No newline at end of file | |
diff --git a/config/env/all.js b/config/env/all.js | |
index 435fe9e..54a47da 100644 | |
--- a/config/env/all.js | |
+++ b/config/env/all.js | |
@@ -31,6 +31,11 @@ module.exports = { | |
}, | |
// The session cookie name | |
sessionName: 'connect.sid', | |
+ // Token Life in seconds | |
+ "security": { | |
+ "tokenLife" : 3600 | |
+ }, | |
+ //Logs | |
log: { | |
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' | |
format: 'combined', | |
diff --git a/config/passport.js b/config/passport.js | |
index 5abfae7..ca5015c 100755 | |
--- a/config/passport.js | |
+++ b/config/passport.js | |
@@ -8,6 +8,10 @@ var passport = require('passport'), | |
path = require('path'), | |
config = require('./config'); | |
+var BasicStrategy = require('passport-http').BasicStrategy, | |
+ ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy, | |
+ BearerStrategy = require('passport-http-bearer').Strategy; | |
+ | |
/** | |
* Module init function. | |
*/ | |
diff --git a/config/strategies/local.js b/config/strategies/local.js | |
index ad56052..ec7ec8a 100644 | |
--- a/config/strategies/local.js | |
+++ b/config/strategies/local.js | |
@@ -5,7 +5,15 @@ | |
*/ | |
var passport = require('passport'), | |
LocalStrategy = require('passport-local').Strategy, | |
- User = require('mongoose').model('User'); | |
+ mongoose = require('mongoose'), | |
+ User = mongoose.model('User'), | |
+ Client = mongoose.model('Client'), | |
+ AccessTokenModel = mongoose.model('AccessToken'); | |
+ | |
+var BasicStrategy = require('passport-http').BasicStrategy, | |
+ ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy, | |
+ BearerStrategy = require('passport-http-bearer').Strategy; | |
+ | |
module.exports = function() { | |
// Use local strategy | |
@@ -35,4 +43,55 @@ module.exports = function() { | |
}); | |
} | |
)); | |
-}; | |
+ | |
+//------- OAuth | |
+ | |
+ passport.use(new BasicStrategy( | |
+ function(username, password, done) { | |
+ Client.findOne({ clientId: username }, function(err, client) { | |
+ if (err) { return done(err); } | |
+ if (!client) { return done(null, false); } | |
+ if (client.clientSecret != password) { return done(null, false); } | |
+ | |
+ return done(null, client); | |
+ }); | |
+ } | |
+ )); | |
+ | |
+ passport.use(new ClientPasswordStrategy( | |
+ function(clientId, clientSecret, done) { | |
+ Client.findOne({ clientId: clientId }, function(err, client) { | |
+ if (err) { return done(err); } | |
+ if (!client) { return done(null, false); } | |
+ if (client.clientSecret != clientSecret) { return done(null, false); } | |
+ | |
+ return done(null, client); | |
+ }); | |
+ } | |
+ )); | |
+ | |
+ passport.use(new BearerStrategy( | |
+ function(accessToken, done) { | |
+ AccessTokenModel.findOne({ token: accessToken }, function(err, token) { | |
+ if (err) { return done(err); } | |
+ if (!token) { return done(null, false); } | |
+ | |
+ if( Math.round((Date.now()-token.created)/1000) > 3600 /*config.get('security:tokenLife')*/ ) { | |
+ AccessTokenModel.remove({ token: accessToken }, function (err) { | |
+ if (err) return done(err); | |
+ }); | |
+ return done(null, false, { message: 'Token expired' }); | |
+ } | |
+ | |
+ User.findById(token.userId, function(err, user) { | |
+ if (err) { return done(err); } | |
+ if (!user) { return done(null, false, { message: 'Unknown user' }); } | |
+ | |
+ var info = { scope: '*' } | |
+ done(null, user, info); | |
+ }); | |
+ }); | |
+ } | |
+ )); | |
+ | |
+}; | |
\ No newline at end of file | |
diff --git a/package.json b/package.json | |
old mode 100755 | |
new mode 100644 | |
index 091f2e1..8325398 | |
--- a/package.json | |
+++ b/package.json | |
@@ -1,74 +1,79 @@ | |
{ | |
- "name": "meanjs", | |
- "description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js.", | |
- "version": "0.3.3", | |
- "private": false, | |
- "author": "https://github.com/meanjs/mean/graphs/contributors", | |
- "repository": { | |
- "type": "git", | |
- "url": "https://github.com/meanjs/mean.git" | |
- }, | |
- "engines": { | |
- "node": ">=0.10.28", | |
- "npm": ">=1.4.28" | |
- }, | |
- "scripts": { | |
- "start": "grunt", | |
- "test": "grunt test", | |
- "postinstall": "bower install --config.interactive=false" | |
- }, | |
- "dependencies": { | |
- "express": "~4.10.1", | |
- "express-session": "~1.9.1", | |
- "body-parser": "~1.9.0", | |
- "cookie-parser": "~1.3.2", | |
- "compression": "~1.2.0", | |
- "method-override": "~2.3.0", | |
- "morgan": "~1.4.1", | |
- "connect-mongo": "~0.4.1", | |
- "connect-flash": "~0.1.1", | |
- "helmet": "~0.5.0", | |
- "consolidate": "~0.10.0", | |
- "swig": "~1.4.1", | |
- "mongoose": "~3.8.8", | |
- "passport": "~0.2.0", | |
- "passport-local": "~1.0.0", | |
- "passport-facebook": "~1.0.2", | |
- "passport-twitter": "~1.0.2", | |
- "passport-linkedin": "~0.1.3", | |
- "passport-google-oauth": "~0.1.5", | |
- "passport-github": "~0.1.5", | |
- "lodash": "~2.4.1", | |
- "forever": "~0.11.0", | |
- "bower": "~1.3.8", | |
- "grunt-cli": "~0.1.13", | |
- "glob": "~4.0.5", | |
- "async": "~0.9.0", | |
- "nodemailer": "~1.3.0", | |
- "chalk": "~1.0.0" | |
- }, | |
- "devDependencies": { | |
- "supertest": "~0.14.0", | |
- "should": "~4.1.0", | |
- "grunt-env": "~0.4.1", | |
- "grunt-node-inspector": "~0.1.3", | |
- "grunt-contrib-watch": "~0.6.1", | |
- "grunt-contrib-jshint": "~0.10.0", | |
- "grunt-contrib-csslint": "^0.3.1", | |
- "grunt-ng-annotate": "~0.4.0", | |
- "grunt-contrib-uglify": "~0.6.0", | |
- "grunt-contrib-cssmin": "~0.10.0", | |
- "grunt-nodemon": "~0.3.0", | |
- "grunt-concurrent": "~1.0.0", | |
- "grunt-mocha-test": "~0.12.1", | |
- "grunt-karma": "~0.9.0", | |
- "load-grunt-tasks": "~1.0.0", | |
- "grunt-contrib-copy": "0.8", | |
- "karma": "~0.12.0", | |
- "karma-jasmine": "~0.2.1", | |
- "karma-coverage": "~0.2.0", | |
- "karma-chrome-launcher": "~0.1.2", | |
- "karma-firefox-launcher": "~0.1.3", | |
- "karma-phantomjs-launcher": "~0.1.2" | |
- } | |
+ "name": "meanjs", | |
+ "description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js.", | |
+ "version": "0.3.3", | |
+ "private": false, | |
+ "author": "https://github.com/meanjs/mean/graphs/contributors", | |
+ "repository": { | |
+ "type": "git", | |
+ "url": "https://github.com/meanjs/mean.git" | |
+ }, | |
+ "engines": { | |
+ "node": ">=0.10.28", | |
+ "npm": ">=1.4.28" | |
+ }, | |
+ "scripts": { | |
+ "start": "grunt", | |
+ "test": "grunt test", | |
+ "postinstall": "bower install --config.interactive=false" | |
+ }, | |
+ "dependencies": { | |
+ "Faker": "^0.7.2", | |
+ "async": "~0.9.0", | |
+ "body-parser": "~1.9.0", | |
+ "bower": "~1.3.8", | |
+ "chalk": "~1.0.0", | |
+ "compression": "~1.2.0", | |
+ "connect-flash": "~0.1.1", | |
+ "connect-mongo": "~0.4.1", | |
+ "consolidate": "~0.10.0", | |
+ "cookie-parser": "~1.3.2", | |
+ "express": "~4.10.1", | |
+ "express-session": "~1.9.1", | |
+ "forever": "~0.11.0", | |
+ "glob": "~4.0.5", | |
+ "grunt-cli": "~0.1.13", | |
+ "helmet": "~0.5.0", | |
+ "lodash": "~2.4.1", | |
+ "method-override": "~2.3.0", | |
+ "mongoose": "~3.8.8", | |
+ "morgan": "~1.4.1", | |
+ "nodemailer": "~1.3.0", | |
+ "oauth2orize": "^1.0.1", | |
+ "passport": "~0.2.0", | |
+ "passport-facebook": "~1.0.2", | |
+ "passport-github": "~0.1.5", | |
+ "passport-google-oauth": "~0.1.5", | |
+ "passport-http": "^0.2.2", | |
+ "passport-http-bearer": "^1.0.1", | |
+ "passport-linkedin": "~0.1.3", | |
+ "passport-local": "~1.0.0", | |
+ "passport-oauth2-client-password": "^0.1.2", | |
+ "passport-twitter": "~1.0.2", | |
+ "swig": "~1.4.1" | |
+ }, | |
+ "devDependencies": { | |
+ "supertest": "~0.14.0", | |
+ "should": "~4.1.0", | |
+ "grunt-env": "~0.4.1", | |
+ "grunt-node-inspector": "~0.1.3", | |
+ "grunt-contrib-watch": "~0.6.1", | |
+ "grunt-contrib-jshint": "~0.10.0", | |
+ "grunt-contrib-csslint": "^0.3.1", | |
+ "grunt-ng-annotate": "~0.4.0", | |
+ "grunt-contrib-uglify": "~0.6.0", | |
+ "grunt-contrib-cssmin": "~0.10.0", | |
+ "grunt-nodemon": "~0.3.0", | |
+ "grunt-concurrent": "~1.0.0", | |
+ "grunt-mocha-test": "~0.12.1", | |
+ "grunt-karma": "~0.9.0", | |
+ "load-grunt-tasks": "~1.0.0", | |
+ "grunt-contrib-copy": "0.8", | |
+ "karma": "~0.12.0", | |
+ "karma-jasmine": "~0.2.1", | |
+ "karma-coverage": "~0.2.0", | |
+ "karma-chrome-launcher": "~0.1.2", | |
+ "karma-firefox-launcher": "~0.1.3", | |
+ "karma-phantomjs-launcher": "~0.1.2" | |
+ } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment