Skip to content

Commit

Permalink
Merge branch 'master' into connect-middleware
Browse files Browse the repository at this point in the history
Conflicts:
	lib/validation.js
  • Loading branch information
maritz committed Nov 14, 2011
2 parents 8a0e696 + 44ccb50 commit 22ba679
Show file tree
Hide file tree
Showing 6 changed files with 376 additions and 71 deletions.
3 changes: 3 additions & 0 deletions examples/rest-user-server/README.md
@@ -0,0 +1,3 @@
Simple example of a basic REST api using nohm. This is in no way secure and has no authentication checks.

It requires express
176 changes: 176 additions & 0 deletions examples/rest-user-server/UserModel.js
@@ -0,0 +1,176 @@
var nohm = require(__dirname+'/../../lib/nohm.js').Nohm,
crypto = require('crypto');

/**
* Given a password and salt this creates an SHA512 hash.
*/
var hasher = function hasher (password, salt) {
var hash = crypto.createHash('sha512');
hash.update(password);
hash.update(salt);
return hash.digest('base64');
};

/**
* Create a random id
*/
var uid = function uid () {
return ((Date.now() & 0x7fff).toString(32) + (0x100000000 * Math.random()).toString(32));
};


var password_minlength = 6; // we use this multiple times and store it here to only have one place where it needs to be configured

/**
* Model definition of a simple user
*/
var userModel = module.exports = nohm.model('User', {
idGenerator: 'increment',
properties: {
name: {
type: 'string',
unique: true,
validations: [
'notEmpty',
['minLength', 4]
]
},
email: {
type: 'string',
validations: [
['email', true] // this means only values that pass the email regexp are accepted. BUT it is also optional, thus a falsy value is accepted as well.
]
},
password: {
load_pure: true, // this ensures that there is no typecasting when loading from the db.
type: function (value, key, old) { // because when typecasting, we create a new salt and hash the pw.
var pwd, salt,
valueDefined = value && typeof(value.length) !== 'undefined';
if ( valueDefined && value.length >= password_minlength) {
pwd = hasher(value, this.p('salt'));
if (pwd !== old) {
// if the password was changed, we change the salt as well, just to be sure.
salt = uid();
this.p('salt', salt);
pwd = hasher(value, salt);
}
return pwd;
} else {
return value;
}
},
validations: [
'notEmpty',
['minLength', password_minlength]
]
},
salt: {
// we store the salt so we can check the hashed passwords.
// this is done so that if someone gets access to the database, they can't just use the same salt for every password. this increases the time they need for the decryption and thus makes it less likely that they'll succeed.
// Note: this is not very secure. There should also be an application salt and some other techniques to make password decryption more difficult
defaultValue: uid()
}
},
methods: {
// custom methods we define here to make handling this model easier.

/**
* Check a given username/password combination for validity.
*/
login: function (name, password, callback) {
var self = this;
if (!name || name === '' || !password || password === '') {
callback(false);
return;
}
this.find({name: name}, function (err, ids) {
if (ids.length === 0) {
callback(false);
} else {
self.load(ids[0], function (err) {
if (!err && self.p('password') === hasher(password, self.p('salt'))) {
callback(true);
} else {
callback(false);
}
});
}
});
},

/**
* This function makes dealing with user input a little easier, since we don't want the user to be able to do things on certain fields, like the salt.
* You can specify a data array that might come from the user and an array containing the fields that should be used from used from the data.
* Optionally you can specify a function that gets called on every field/data pair to do a dynamic check if the data should be included.
* The principle of this might make it into core nohm at some point.
*/
fill: function (data, fields, fieldCheck) {
var props = {},
passwordInField,
passwordChanged = false,
self = this,
doFieldCheck = typeof(fieldCheck) === 'function';

fields = Array.isArray(fields) ? fields : Object.keys(data);

fields.forEach(function (i) {
var fieldCheckResult;

if (i === 'salt' || // make sure the salt isn't overwritten
! self.properties.hasOwnProperty(i))
return;

if (doFieldCheck)
fieldCheckResult = fieldCheck(i, data[i]);

if (doFieldCheck && fieldCheckResult === false)
return;
else if (doFieldCheck && typeof (fieldCheckResult) !== 'undefined' &&
fieldCheckResult !== true)
return (props[i] = fieldCheckResult);


props[i] = data[i];
});

this.p(props);
return props;
},

/**
* This is a wrapper around fill and save.
* It also makes sure that if there are validation errors, the salt field is not included in there. (although we don't have validations for the salt, an empty entry for it would be created in the errors object)
*/
store: function (data, callback) {
var self = this;

this.fill(data);
this.save(function () {
delete self.errors.salt;
callback.apply(self, Array.prototype.slice.call(arguments, 0));
});
},

/**
* Wrapper around fill and valid.
* This makes it easier to check user input.
*/
checkProperties: function (data, fields, callback) {
var self = this;
callback = typeof(fields) === 'function' ? fields : callback;

this.fill(data, fields);
this.valid(false, false, callback);
},

/**
* Overwrites nohms allProperties() to make sure password and salt are not given out.
*/
allProperties: function (stringify) {
var props = this._super_allProperties.call(this);
delete props.password;
delete props.salt;
return stringify ? JSON.stringify(props) : props;
}
}
});
71 changes: 71 additions & 0 deletions examples/rest-user-server/rest-server.js
@@ -0,0 +1,71 @@
var express = require(__dirname+'/../../../stack/node_modules/express');
var Nohm = require(__dirname+'/../../lib/nohm.js').Nohm;
var UserModel = require(__dirname+'/UserModel.js');
var redis = require('redis');

var redisClient = redis.createClient();

Nohm.setPrefix('rest-user-server-example');
Nohm.setClient(redisClient);

var server = express.createServer();

server.get('/User/list', function (req, res, next) {
UserModel.find(function (err, ids) {
if (err) {
return next(err);
}
var users = [];
var len = ids.length;
var count = 0;
if (len === 0) {
return res.json(users);
}
ids.forEach(function (id) {
var user = new UserModel();
user.load(id, function (err, props) {
if (err) {
return next(err);
}
users.push({id: this.id, name: props.name});
if (++count === len) {
res.json(users);
}
});
});
});
});

server.get('/User/create', function (req, res, next) {
var data = {
name: req.param('name'),
password: req.param('password'),
email: req.param('email')
};

var user = new UserModel();
user.store(data, function (err) {
if (err === 'invalid') {
next(user.errors);
} else if (err) {
next(err);
} else {
res.json({result: 'success', data: user.allProperties()});
}
});
});

server.use(Nohm.getConnectValidationMiddleware([{
model: UserModel,
blacklist: ['salt']
}]));

server.use(function (err, req, res, next) {
if (err instanceof Error) {
err = err.message;
}
res.json({result: 'error', data: err});
});

server.listen(3000);
console.log('listening on 3000');
68 changes: 52 additions & 16 deletions lib/connectMiddleware.js
Expand Up @@ -5,10 +5,38 @@
var fs = require('fs');
var validators = require(__dirname + '/validators.js');

var regexpStr = "__regexps: {url: /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i,email: /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i}";

var customToString = function (obj) {

var maxDepth = 5;
var customToString = function (obj, depth) {
depth = depth || 0;
if (depth > maxDepth) {
console.log('maxdepth exceeded');
console.dir(obj);
return '';
}
switch(typeof(obj)) {
case 'string':
return '"'+obj+'"';
case 'number':
return obj;
case 'boolean':
return obj ? 'true' : 'false';
case 'function':
break;
case 'object':
if (Array.isArray(obj)) {
var arr = [];
obj.forEach(function (val) {
arr.push(customToString(val, depth+1));
});
return '['+arr.join(',')+']';
} else {
var arr = [];
Object.keys(obj).forEach(function (val) {
arr.push('"'+val+'":'+customToString(obj[val], depth+1));
});
return '{'+arr.join(',')+'}';
}
}
};

var validationsFlatten = function (obj, namespace) {
Expand All @@ -23,11 +51,7 @@ var validationsFlatten = function (obj, namespace) {
str += ""+key+': [';
var strVals = [];
vals.forEach(function (val) {
if (typeof(val) === 'string') {
strVals.push(val);
} else if (Array.isArray(val)) {
strVals.push('array');
}
strVals.push(customToString(val));
});
str += strVals.join(',')+'], ';
}
Expand All @@ -41,6 +65,7 @@ var validationsFlatten = function (obj, namespace) {
* and the modelspecific serial validations as a JSON object to the browser.
* This is useful if you want to save some bandwith by doing the serial validations
* in the browser before saving to the server.
* Custom validations are not supported by this yet. (TODO: we might add a way to do custom validations by just supplying a special name/string that will get called)
*
* Options:
* - `models` - object of all models you want to extract the validations from and possible attribute exception array
Expand All @@ -64,24 +89,35 @@ var validationsFlatten = function (obj, namespace) {

module.exports = function getConnectValidationMiddleware(models, options){
options = options || {};
var url = options.url || '/nohmValidation.js';
var url = options.url || '/nohmValidations.js';
var namespace = options.namespace || 'nohmValidations';
var str = 'var '+namespace+'={'+regexpStr;
var arr = [];
var maxAge = options.maxAge || 3600; // 1 hour

if (Array.isArray(models) && models.length > 0) {
var comma = ',';
models.forEach(function (item) {
str = str+comma+validationsFlatten(item, namespace);
arr.push(validationsFlatten(item, namespace));
});
}

str = str+'};';
var str = 'var nohmValidationsNamespaceName = "'+namespace+'";var '+namespace+'={'+arr.join(',')+'};';

console.dir(str);

return function (req, res, next) {
if (req.url === url) {

fs.readFile(__dirname+'/validators.js', function(err, buf){
if (err) return next(err);

var body = str+buf.toString('utf-8');
var headers = {
'Content-Type': 'text/javascript',
'Content-Length': body.length,
'Cache-Control': 'public, max-age=' + maxAge
};
res.writeHead(200, headers);
res.end(body);
});
} else {
next();
}
Expand Down Expand Up @@ -117,4 +153,4 @@ module.exports = function getConnectValidationMiddleware(models, options){
next();
}
};
};
};

0 comments on commit 22ba679

Please sign in to comment.