Various updates.
This commit is contained in:
parent
ae091cdb8c
commit
fd05a1a9cf
|
@ -0,0 +1,2 @@
|
||||||
|
/node_modules/
|
||||||
|
npm-debug.log
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"author": {
|
||||||
|
"name": "Julien Fontanet",
|
||||||
|
"email": "julien.fontanet@vates.fr",
|
||||||
|
"url": "http://vates.fr/"
|
||||||
|
},
|
||||||
|
"name": "xo-server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"homepage": "http://github.com/vatesfr/xo-server/",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/vatesfr/xo-server.git"
|
||||||
|
},
|
||||||
|
"main": "src/main.js",
|
||||||
|
"dependencies": {
|
||||||
|
"backbone": ">=1.0.0",
|
||||||
|
"hashy": ">=0.1.0",
|
||||||
|
"underscore": ">=1.4.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {},
|
||||||
|
"optionalDependencies": {},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
}
|
205
src/api.js
205
src/api.js
|
@ -2,17 +2,38 @@ var _ = require('underscore');
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
var api = {}
|
|
||||||
|
|
||||||
function Api(xo)
|
function Api(xo)
|
||||||
{
|
{
|
||||||
this.xo = xo;
|
this.xo = xo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Api.prototype.exec = function (session, req, res) {
|
||||||
|
var method = this.get(req.method);
|
||||||
|
|
||||||
|
if (!method)
|
||||||
|
{
|
||||||
|
res.sendError(Api.err.INVALID_METHOD);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = method.call(this.xo, session, req, res);
|
||||||
|
if (undefined !== result)
|
||||||
|
{
|
||||||
|
res.sendResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
res.sendError(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Api.prototype.get = function (name) {
|
Api.prototype.get = function (name) {
|
||||||
var parts = name.split('.');
|
var parts = name.split('.');
|
||||||
|
|
||||||
var current = api;
|
var current = Api.fn;
|
||||||
for (
|
for (
|
||||||
var i = 0, n = parts.length;
|
var i = 0, n = parts.length;
|
||||||
(i < n) && (current = current[parts[i]]);
|
(i < n) && (current = current[parts[i]]);
|
||||||
|
@ -32,16 +53,180 @@ module.exports = function (xo) {
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
api.session = {
|
function err(code, message)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
'code': code,
|
||||||
|
'message': message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
'signInWithPassword': function (req, res)
|
Api.err = {
|
||||||
{
|
|
||||||
if (!req.hasParams('user', 'password'))
|
//////////////////////////////////////////////////////////////////
|
||||||
|
// JSON errors.
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
'INVALID_JSON': err(-32700, 'invalid JSON'),
|
||||||
|
|
||||||
|
'INVALID_REQUEST': err(-32600, 'invalid JSON-RPC request'),
|
||||||
|
|
||||||
|
'INVALID_METHOD': err(-326001, 'method not found'),
|
||||||
|
|
||||||
|
'INVALID_PARAMS': err(-32602, 'invalid parameter(s)'),
|
||||||
|
|
||||||
|
'SERVER_ERROR': err(-32603, 'unknown error from the server'),
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
// XO errors.
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
'ALREADY_AUTHENTICATED': err(0, 'already authenticated'),
|
||||||
|
|
||||||
|
// Invalid email & passwords or token.
|
||||||
|
'INVALID_CREDENTIAL': err(1, 'invalid credential'),
|
||||||
|
|
||||||
|
// Not authenticated or not enough permissions.
|
||||||
|
'UNAUTHORIZED': err(2, 'not authenticated'),
|
||||||
|
)};
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
Api.fn = {};
|
||||||
|
|
||||||
|
Api.fn.session = {
|
||||||
|
|
||||||
|
'signInWithPassword': function (session, req, res) {
|
||||||
|
var p_email = req.params.email;
|
||||||
|
var p_pass = req.params.password;
|
||||||
|
|
||||||
|
if (!p_email || !p_pass)
|
||||||
{
|
{
|
||||||
res.sendInvalidParamsError();
|
throw Api.err.INVALID_PARAMS;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ()
|
if (session.has('user_id'))
|
||||||
|
{
|
||||||
|
throw Api.err.ALREADY_AUTHENTICATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = this.users.findWhere({'email': p_email});
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
throw Api.err.INVALID_CREDENTIAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.checkPassword(p_pass).then(function (success) {
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
res.sendError(Api.err.INVALID_CREDENTIAL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendResult(true);
|
||||||
|
}).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
'signInWithToken': function (session, req, res) {
|
||||||
|
var p_token = req.params.token;
|
||||||
|
|
||||||
|
if (!p_token)
|
||||||
|
{
|
||||||
|
throw Api.err.INVALID_PARAMS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.has('user_id'))
|
||||||
|
{
|
||||||
|
throw Api.err.ALREADY_AUTHENTICATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = this.tokens.get(p_token);
|
||||||
|
if (!token)
|
||||||
|
{
|
||||||
|
throw Api.err.INVALID_CREDENTIAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo How to disconnect when the token is deleted?
|
||||||
|
//
|
||||||
|
// @todo How to not leak the event callback when the
|
||||||
|
// connection is closed?
|
||||||
|
|
||||||
|
session.set('token_id', token.id);
|
||||||
|
session.set('user_id', token.user_id);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
'getUser': function (session, req, res) {
|
||||||
|
var user_id = session.get('user_id');
|
||||||
|
if (undefined === user_id)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.pick(users.get(user_id), 'id', 'email');
|
||||||
|
};
|
||||||
|
|
||||||
|
'createToken': function (session, req, res) {
|
||||||
|
var user_id = session.get('user_id');
|
||||||
|
if ((undefined === user_id)
|
||||||
|
|| session.has('token_id'))
|
||||||
|
{
|
||||||
|
throw Api.err.UNAUTHORIZED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo Ugly.
|
||||||
|
var token = this.tokens.model.generate(user_id);
|
||||||
|
this.tokens.add(token);
|
||||||
|
|
||||||
|
return token.id;
|
||||||
|
},
|
||||||
|
|
||||||
|
'destroyToken': function (session, req, res) {
|
||||||
|
var p_token = req.params.token;
|
||||||
|
|
||||||
|
if (!this.tokens.get(p_token))
|
||||||
|
{
|
||||||
|
throw Api.err.INVALID_PARAMS;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tokens.remove(p_token);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Api.fn.user = {
|
||||||
|
'create': function (session, req, res) {
|
||||||
|
var p_email = req.params.email;
|
||||||
|
var p_pass = req.params.password;
|
||||||
|
var p_perm = req.params.permission;
|
||||||
|
|
||||||
|
if (!p_email || !p_pass || !p_perm)
|
||||||
|
{
|
||||||
|
throw Api.err.INVALID_PARAMS;
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = new this.users.model({
|
||||||
|
'email': p_email,
|
||||||
|
'password': p_pass,
|
||||||
|
'permission': p_perm,
|
||||||
|
});
|
||||||
|
|
||||||
|
// @todo How to save it and to retrieve its unique id?
|
||||||
|
},
|
||||||
|
|
||||||
|
'delete': function (session, req, res) {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
'changePassword': function (session, req, res) {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
'getAll': function (session, req, res) {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
'set': function (session, req, res) {
|
||||||
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
100
src/main.js
100
src/main.js
|
@ -1,7 +1,55 @@
|
||||||
|
var xo = require('./xo')();
|
||||||
|
var api = require('./api')(xo);
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
var xo = {};
|
function Session()
|
||||||
var api = require('./api')(xo);
|
{
|
||||||
|
this.data = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Session.prototype.get = function (name, def) {
|
||||||
|
if (undefined !== this.data[name])
|
||||||
|
{
|
||||||
|
return this.data[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return def;
|
||||||
|
};
|
||||||
|
|
||||||
|
Session.prototype.has = function (name) {
|
||||||
|
return (undefined !== this.data[name]);
|
||||||
|
};
|
||||||
|
|
||||||
|
Session.prototype.set = function (name, value) {
|
||||||
|
this.data[name] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function Response(transport, id)
|
||||||
|
{
|
||||||
|
this.transport = transport;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response.prototype.sendResult = function (value)
|
||||||
|
{
|
||||||
|
this.transport(JSON.stringify({
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'result': value,
|
||||||
|
'id': this.id,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
Response.prototype.sendError = function (error)
|
||||||
|
{
|
||||||
|
this.transport(JSON.stringify({
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'error': error,
|
||||||
|
'id': this.id,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@ -9,14 +57,46 @@ require('socket.io')
|
||||||
.listen(8080)
|
.listen(8080)
|
||||||
.sockets.on('connection', function (socket) {
|
.sockets.on('connection', function (socket) {
|
||||||
|
|
||||||
|
// @todo comment
|
||||||
|
var transport = function (message) {
|
||||||
|
socket.send(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
var session = new Session();
|
||||||
|
|
||||||
// When a message is received.
|
// When a message is received.
|
||||||
socket.on('message', function () {});
|
socket.on('message', function (message) {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var req = JSON.parse(message.toString());
|
||||||
|
}
|
||||||
|
catch (e if e instanceof SyntaxError)
|
||||||
|
{
|
||||||
|
new Response(transport, null).sendError(
|
||||||
|
api.err
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.method || !req.params
|
||||||
|
|| (undefined === req.id)
|
||||||
|
|| ('2.0' !== req.jsonrpc))
|
||||||
|
{
|
||||||
|
new Response(transport, null).sendError(
|
||||||
|
-32600,
|
||||||
|
'the JSON sent is not a valid request object'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.exec(
|
||||||
|
session,
|
||||||
|
{
|
||||||
|
'method': req.method,
|
||||||
|
'params': req.params,
|
||||||
|
},
|
||||||
|
new Response(transport, req.id)
|
||||||
|
);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var json_rpc = require('./json-rpc');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
var _ = require('underscore');
|
||||||
|
var Backbone = require('backbone');
|
||||||
|
var crypto = require('crypto');
|
||||||
|
var hashy = require('hashy');
|
||||||
|
var Q = require('q');
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
var check = function () {
|
||||||
|
var errors = undefined;
|
||||||
|
|
||||||
|
var validator = new require('validator').Validator();
|
||||||
|
validator.error = function (err) {
|
||||||
|
(errors || (errors = [])).push(err);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
var check = function (data) {
|
||||||
|
validator.check(data);
|
||||||
|
};
|
||||||
|
check.pop = function () {
|
||||||
|
var res = errors;
|
||||||
|
errors = undefined;
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
return check;
|
||||||
|
}();
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
// Models
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// @todo We could also give a permission level to tokens (<=
|
||||||
|
// user.permission).
|
||||||
|
var Token = Backbone.Model.extend({
|
||||||
|
// Validates model attributes.
|
||||||
|
'validate': function (attr) {
|
||||||
|
check(attr.id).len(10);
|
||||||
|
check(attr.user_id).isInt();
|
||||||
|
|
||||||
|
return check.pop();
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'generate': function (user_id) {
|
||||||
|
return Q.ninvoke(crypto, 'randomBytes', 32).then(function (buf) {
|
||||||
|
return new Token({
|
||||||
|
'id': buf.toString('base64'),
|
||||||
|
'user_id': user_id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var User = Backbone.Model.extend({
|
||||||
|
'default': {
|
||||||
|
'permission': 'none',
|
||||||
|
},
|
||||||
|
|
||||||
|
'initialize': function ()
|
||||||
|
{
|
||||||
|
this.on('change:password', function (model, password) {
|
||||||
|
this.unset('password', {'silent': true});
|
||||||
|
|
||||||
|
var user = this;
|
||||||
|
hashy.hash(password).then(function (hash) {
|
||||||
|
user.set('pw_hash', hash);
|
||||||
|
}).done();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Validates model attributes.
|
||||||
|
'validate': function (attr) {
|
||||||
|
check(attr.id).isInt();
|
||||||
|
check(attr.email).isEmail();
|
||||||
|
check(attr.pw_hash).len(40);
|
||||||
|
check(attr.permission).isIn('none', 'read', 'write', 'admin');
|
||||||
|
|
||||||
|
return check.pop();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Checks and updates the hash if necessary.
|
||||||
|
'checkPassword': function (password) {
|
||||||
|
var hash = this.get('pw_hash');
|
||||||
|
var user = this;
|
||||||
|
|
||||||
|
return hashy.verify(password, hash).then(function (success) {
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashy.needsRehash(hash))
|
||||||
|
{
|
||||||
|
user.set('password', password);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
// Collections
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
var Tokens = Backbone.Collection.extend({
|
||||||
|
'model': Token,
|
||||||
|
});
|
||||||
|
|
||||||
|
var Users = Backbone.Collection.extend({
|
||||||
|
'model': User,
|
||||||
|
});
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function Xo()
|
||||||
|
{
|
||||||
|
this.tokens = new Tokens();
|
||||||
|
this.users = new Users();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function () {
|
||||||
|
return new Xo;
|
||||||
|
};
|
Loading…
Reference in New Issue