diff --git a/package.json b/package.json index 548ca78..d540035 100644 --- a/package.json +++ b/package.json @@ -1,95 +1,96 @@ { "name": "manticore", "version": "0.0.0", "main": "server/app.js", "dependencies": { "express": "~4.0.0", "morgan": "~1.0.0", "body-parser": "~1.5.0", "method-override": "~1.0.0", "serve-favicon": "~2.0.1", "cookie-parser": "~1.0.1", "express-session": "~1.0.2", "errorhandler": "~1.0.0", "compression": "~1.0.1", "lodash": "~2.4.1", "jade": "~1.2.0", "mongoose": "~3.8.8", "gridfs-stream": "1.1.1", "jsonwebtoken": "^0.3.0", "express-jwt": "^0.1.3", "passport": "~0.2.0", "passport-local": "~0.1.6", "composable-middleware": "^0.3.0", "connect-mongo": "^0.4.1", "multer": "0.1.8", "socket.io": "1.3.5", "socketio-jwt": "4.2.0", - "async": "~1.3.0" + "async": "~1.3.0", + "dav": "~1.7.6" }, "devDependencies": { "grunt": "~0.4.4", "grunt-autoprefixer": "~0.7.2", "grunt-wiredep": "~1.8.0", "grunt-concurrent": "~0.5.0", "grunt-contrib-clean": "~0.5.0", "grunt-contrib-concat": "~0.4.0", "grunt-contrib-copy": "~0.5.0", "grunt-contrib-cssmin": "~0.9.0", "grunt-contrib-htmlmin": "~0.2.0", "grunt-contrib-imagemin": "~0.7.1", "grunt-contrib-jshint": "~0.10.0", "grunt-contrib-uglify": "~0.4.0", "grunt-contrib-watch": "~0.6.1", "grunt-contrib-jade": "^0.11.0", "grunt-google-cdn": "~0.4.0", "grunt-newer": "~0.7.0", "grunt-ng-annotate": "^0.2.3", "grunt-filerev": "~2.2.0", "grunt-svgmin": "~0.4.0", "grunt-usemin": "~3.0.0", "grunt-env": "~0.4.1", "grunt-node-inspector": "~0.1.5", "grunt-nodemon": "~0.2.0", "grunt-angular-templates": "^0.5.4", "grunt-dom-munger": "^3.4.0", "grunt-protractor-runner": "^1.1.0", "grunt-asset-injector": "^0.1.0", "grunt-karma": "~0.8.2", "grunt-build-control": "DaftMonk/grunt-build-control", "grunt-mocha-test": "~0.10.2", "grunt-contrib-stylus": "latest", "jit-grunt": "^0.5.0", "time-grunt": "~0.3.1", "grunt-express-server": "~0.4.17", "grunt-open": "~0.2.3", "open": "~0.0.4", "jshint-stylish": "~0.1.5", "connect-livereload": "~0.4.0", "karma-ng-scenario": "~0.1.0", "karma-firefox-launcher": "~0.1.3", "karma-script-launcher": "~0.1.0", "karma-html2js-preprocessor": "~0.1.0", "karma-ng-jade2js-preprocessor": "^0.1.2", "karma-jasmine": "~0.1.5", "karma-chrome-launcher": "~0.1.3", "requirejs": "~2.1.11", "karma-requirejs": "~0.2.1", "karma-coffee-preprocessor": "~0.2.1", "karma-jade-preprocessor": "0.0.11", "karma-phantomjs-launcher": "~0.1.4", "karma": "~0.12.9", "karma-ng-html2js-preprocessor": "~0.1.0", "supertest": "~0.11.0", "should": "~3.3.1" }, "engines": { "node": ">=0.10.0" }, "scripts": { "start": "node server/app.js", "test": "grunt test", "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update" }, "private": true } diff --git a/server/api/user/user.model.js b/server/api/user/user.model.js index f82ad18..2a96013 100644 --- a/server/api/user/user.model.js +++ b/server/api/user/user.model.js @@ -1,142 +1,145 @@ 'use strict'; var mongoose = require('mongoose'); var Schema = mongoose.Schema; var crypto = require('crypto'); +var authTypes = ['webdav']; + var UserSchema = new Schema({ name: String, email: { type: String, lowercase: true }, role: { type: String, default: 'user' }, hashedPassword: String, provider: String, salt: String }); /** * Virtuals */ UserSchema .virtual('password') .set(function(password) { this._password = password; this.salt = this.makeSalt(); this.hashedPassword = this.encryptPassword(password); }) .get(function() { return this._password; }); // Public profile information UserSchema .virtual('profile') .get(function() { return { 'name': this.name, 'role': this.role }; }); // Non-sensitive info we'll be putting in the token UserSchema .virtual('token') .get(function() { return { '_id': this._id, 'role': this.role }; }); /** * Validations */ // Validate empty email UserSchema .path('email') .validate(function(email) { return email.length; }, 'Email cannot be blank'); // Validate empty password UserSchema .path('hashedPassword') .validate(function(hashedPassword) { + if (authTypes.indexOf(this.provider) !== -1) return true; return hashedPassword.length; }, 'Password cannot be blank'); // Validate email is not taken UserSchema .path('email') .validate(function(value, respond) { var self = this; this.constructor.findOne({email: value}, function(err, user) { if(err) throw err; if(user) { if(self.id === user.id) return respond(true); return respond(false); } respond(true); }); }, 'The specified email address is already in use.'); var validatePresenceOf = function(value) { return value && value.length; }; /** * Pre-save hook */ UserSchema .pre('save', function(next) { if (!this.isNew) return next(); - if (!validatePresenceOf(this.hashedPassword)) + if (!validatePresenceOf(this.hashedPassword) && authTypes.indexOf(this.provider) === -1) next(new Error('Invalid password')); else next(); }); /** * Methods */ UserSchema.methods = { /** * Authenticate - check if the passwords are the same * * @param {String} plainText * @return {Boolean} * @api public */ authenticate: function(plainText) { return this.encryptPassword(plainText) === this.hashedPassword; }, /** * Make salt * * @return {String} * @api public */ makeSalt: function() { return crypto.randomBytes(16).toString('base64'); }, /** * Encrypt password * * @param {String} password * @return {String} * @api public */ encryptPassword: function(password) { if (!password || !this.salt) return ''; var salt = new Buffer(this.salt, 'base64'); return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64'); } }; module.exports = mongoose.model('User', UserSchema); diff --git a/server/auth/index.js b/server/auth/index.js index 0b79cbc..a5df81f 100644 --- a/server/auth/index.js +++ b/server/auth/index.js @@ -1,15 +1,15 @@ 'use strict'; var express = require('express'); var passport = require('passport'); var config = require('../config/environment'); var User = require('../api/user/user.model'); // Passport Configuration -require('./local/passport').setup(User, config); +require('./' + config.auth + '/passport').setup(User, config); var router = express.Router(); -router.use('/local', require('./local')); +router.use('/local', require('./' + config.auth)); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/server/auth/local/passport.js b/server/auth/local/passport.js index ac82b42..88ddc80 100644 --- a/server/auth/local/passport.js +++ b/server/auth/local/passport.js @@ -1,25 +1,25 @@ var passport = require('passport'); var LocalStrategy = require('passport-local').Strategy; exports.setup = function (User, config) { passport.use(new LocalStrategy({ usernameField: 'email', passwordField: 'password' // this is the virtual field on the model }, function(email, password, done) { User.findOne({ email: email.toLowerCase() }, function(err, user) { if (err) return done(err); if (!user) { return done(null, false, { message: 'This email is not registered.' }); } if (!user.authenticate(password)) { return done(null, false, { message: 'This password is not correct.' }); } return done(null, user); }); } )); -}; \ No newline at end of file +}; diff --git a/server/auth/webdav/index.js b/server/auth/webdav/index.js new file mode 100644 index 0000000..73ff224 --- /dev/null +++ b/server/auth/webdav/index.js @@ -0,0 +1,20 @@ +'use strict'; + +var express = require('express'); +var passport = require('passport'); +var auth = require('../auth.service'); + +var router = express.Router(); + +router.post('/', function(req, res, next) { + passport.authenticate('local', function (err, user, info) { + var error = err || info; + if (error) return res.json(401, error); + if (!user) return res.json(404, {message: 'Something went wrong, please try again.'}); + + var token = auth.signToken(user._id, user.role); + res.json({token: token}); + })(req, res, next) +}); + +module.exports = router; diff --git a/server/auth/webdav/passport.js b/server/auth/webdav/passport.js new file mode 100644 index 0000000..8a0ce50 --- /dev/null +++ b/server/auth/webdav/passport.js @@ -0,0 +1,54 @@ +var passport = require('passport'); +var LocalStrategy = require('passport-local').Strategy; +var dav = require('dav'); +dav.debug.enabled = true; +exports.setup = function (User, config) { + passport.use(new LocalStrategy({ + usernameField: 'email', + passwordField: 'password' // this is the virtual field on the model + }, + function(email, password, done) { + var client = new dav.Client( + new dav.transport.Basic(new dav.Credentials({ + username: email, + password: password + })), + { + baseUrl: config.storage.server + } + ); + client.send( + dav.request.basic({ method: 'OPTIONS', data: ''}), + config.storage.path + ).then( + function success() { + User.findOne({ + email: email.toLowerCase() + }, function (err, user) { + if (err) { return done(err); } + + if (!user) { + var newUser = new User({ + name: email, + email: email, + provider: 'webdav', + role: 'user' + }); + newUser.save(function (err, user) { + if (err) { return done(err); } + if (!err) { + return done(null, user); + } + }); + } else { + return done(null, user); + } + }); + }, + function failure(err) { + return done(err); + } + ); + } + )); +}; diff --git a/server/config/environment/index.js b/server/config/environment/index.js index 4a13d5d..70c1667 100644 --- a/server/config/environment/index.js +++ b/server/config/environment/index.js @@ -1,50 +1,56 @@ 'use strict'; var path = require('path'); var _ = require('lodash'); function requiredProcessEnv(name) { if(!process.env[name]) { throw new Error('You must set the ' + name + ' environment variable'); } return process.env[name]; } // All configurations will extend these options // ============================================ var all = { env: process.env.NODE_ENV, // Root path of server root: path.normalize(__dirname + '/../../..'), // Server port port: process.env.PORT || 9000, // Should we populate the DB with sample data? seedDB: true, // Secret for session, you will want to change this and make it an environment variable secrets: { session: 'manticore-secret' }, // List of user roles userRoles: ['guest', 'user', 'admin'], // MongoDB connection options mongo: { options: { db: { safe: true } } }, + auth: process.env.STORAGE || 'local', + storage: { + type: process.env.STORAGE || 'local', + server: process.env.WEBDAV_SERVER, + path: process.env.WEBDAV_PATH + } }; // Export the config object based on the NODE_ENV // ============================================== module.exports = _.merge( all, require('./' + process.env.NODE_ENV + '.js') || {}); diff --git a/server/config/local.env.sample.js b/server/config/local.env.sample.js index 982ca82..8604415 100644 --- a/server/config/local.env.sample.js +++ b/server/config/local.env.sample.js @@ -1,14 +1,25 @@ 'use strict'; // Use local.env.js for environment variables that grunt will set when the server starts locally. // Use for your api keys, secrets, etc. This file should not be tracked by git. // // You will need to set these on the server you deploy to. module.exports = { DOMAIN: 'http://localhost:9000', SESSION_SECRET: 'manticore-secret', // Control debug level for modules using visionmedia/debug - DEBUG: '' + DEBUG: '', + + /* + * Supported authentication strategies. + * 1. 'local' for storing everything in Mongo/GridFS, auth using username/password + * 2. 'webdav' for linking with a WebDAV server, auth using WebDAV credentials + */ + STORAGE: 'webdav', + + // More configuration for the chosen auth type. None required for 'local' + WEBDAV_SERVER: 'https://apps.kolabnow.com', + WEBDAV_PATH: '/' };