UNPKG

haibu-ishiki

Version:

Node deployment server - wrapper for haibu, http-proxy and carapace with permissions and automatic node versions

377 lines (301 loc) 10.8 kB
/* The MIT License Copyright (c) 2013 Hadrien Jouet https://github.com/grownseed Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ var crypto = require('crypto'), bcrypt = require('bcrypt'), userModel = require('../models/user'); var Auth = exports.Auth = function(app, haibu) { var self = this, admin_username = app.config.get('auth:admin'); this.app = app; this.haibu = haibu; //make sure default admin user is created userModel.get({username: admin_username}, function(err, users) { if (users.length == 0) { self.addUser({username: admin_username, admin: true}, function(err, user) { if (err) return console.log(err); console.log('Initial admin account created:\n> username: ' + user.username + '\n> password: ' + user.password); }); } }); //users api app.router.path('/users', function() { //return all users this.get(function() { var route = this; userModel.get(function(err, users) { if (err) return haibu.sendResponse(route.res, 500, err); haibu.sendResponse(route.res, 200, users); }); }); //new user this.post(function() { var route = this, err = null; if (route.req.body) { if (!route.req.body.username || (route.req.body.username && !route.req.body.username.trim())) err = 'A username is required'; }else{ err = 'New user details missing'; } if (err) return haibu.sendResponse(route.res, 500, {message: err}); self.addUser(route.req.body, function(err, user) { if (err) return haibu.sendResponse(route.res, 500, err); haibu.sendResponse(route.res, 200, user); }); }); //login this.post('/login', function() { var route = this; self.login(route.req.body, function(err, token) { if (err) return haibu.sendResponse(route.res, 500, err); haibu.sendResponse(route.res, 200, {token: token}); }); }); //logout this.post('/logout', function() { var route = this; userModel.edit(route.req.user._id.toString(), {$set: {token: false, last_access: false}}, function(err, result) { if (err) return haibu.sendResponse(route.res, 500, err); haibu.sendResponse(route.res, 200, {message: 'You are no longer authenticated'}); }); }); //update user this.post('/:userid', function(userid) { var route = this, user; function updateUser() { userModel.get({username: userid}, function(err, users) { if (err) return haibu.sendResponse(route.res, 500, err); if (users.length == 0) return haibu.sendResponse(route.res, 404, {message: 'User does not exist'}); userModel.edit(users[0]._id.toString(), {$set: user}, function(err, result) { if (err) return haibu.sendResponse(route.res, 500, err); var update_keys = Object.keys(user), msg = update_keys.length > 0 ? 'Updated ' + update_keys.join(', ') : 'Nothing to update'; haibu.sendResponse(route.res, 200, {message: msg}); }); }); } //only allow self to update password if not admin if (!route.req.user.admin) user = {password: route.req.body.password}; else user = route.req.body; //don't unset admin if (!(user.admin && typeof user.admin === 'boolean')) delete user.admin; //don't allow username change delete user.username; if (!user.password || (user.password && !user.password.trim())) { delete user.password; updateUser(); }else{ user.password = self._encryptPassword(user.password, function(err, hash) { if (err) return haibu.sendResponse(route.res, 500, err); user.password = hash; updateUser(); }); } }); }); }; //check permissions Auth.prototype.check = function(req, res, next) { var self = this, url = req.url.split('?'), route = url[0].split('/'), params = (url[1] && url[1].trim() ? url[1].split('&') : []), parsed_params = {}; //parse params if (params.length > 0) { for (var i = 0, n = params.length; i < n; i++) { var param = params[i].split('='); parsed_params[decodeURIComponent(param[0])] = decodeURIComponent(param[1]); } } function errorMsg(err, status, expired) { if (expired) return self.haibu.sendResponse(res, 401, {message: 'Your authentication token has expired'}); self.haibu.sendResponse(res, status, err); } function respond(err, user) { if (err) errorMsg(err, 500); var now = new Date(), token_expiry = self.app.config.get('auth:token_expiry'), expired = false; if (user) { //check for outdated token if (!user.last_access || (user.last_access && token_expiry)) { if (!user.last_access || (now.getTime() - user.last_access.getTime()) / 1000 > token_expiry) { expired = true; }else{ //update token expiry userModel.edit(user._id.toString(), {$set: {last_access: now}}, function(){}); //assign user to request req.user = user; } } //if admin, no need to check further if (user.admin && !expired) return next(); } //0: go ahead, 1: require auth, 2: require admin var require_auth = 0; if (route.length > 1) { switch (route[1]) { case 'drones': case 'users': if (user) { if (!route[2] || (route[2] && route[2] != user.username.toString() && route[2] != 'logout')) require_auth = 2; }else{ require_auth = 1; } break; //proxy stuff admin only case 'proxies': require_auth = 2; break; } if (!require_auth) { return next(); }else{ if (require_auth == 1) errorMsg({message: 'You need to be authenticated to access this resource'}, 401, expired); else errorMsg({message: 'You are not authorized to access this resource'}, 403, expired); } }else{ return next(); } } //only check if route needs to be checked if ((route.length > 1 && ['drones', 'users', 'proxies'].indexOf(route[1]) == -1) || req.url == '/users/login') return next(); //look for active token if (parsed_params.token) { userModel.get({token: parsed_params.token}, function(err, users) { if (err) return respond(err); if (users.length == 1) return respond(null, users[0]); respond(); }); }else{ respond(); } }; //generate a random string Auth.prototype._randomString = function(n, callback) { if (!callback) { callback = n; n = 8; } crypto.randomBytes(n, function(ex, buf) { callback(buf.toString('hex')); }); }; //encrypt password Auth.prototype._encryptPassword = function(password, callback) { bcrypt.genSalt(10, function(err, salt) { if (err) return callback(err); bcrypt.hash(password, salt, callback); }); }; //log user in and generate new token Auth.prototype.login = function(user, callback) { var self = this; if (!user || !user.username || !user.password) return callback({message: 'Please provide a username and a password'}); //find user userModel.get({username: user.username}, function(err, users) { if (err) return callback(err); //generic message to avoid figuring out usernames var user_err = {message: 'Username/Password could not be matched'}; if (users.length == 0) return callback(user_err); //compare passwords bcrypt.compare(user.password, users[0].password, function(err, match) { if (err || !match) return callback(user_err); //save and return token self._randomString(32, function(token) { userModel.edit(users[0]._id.toString(), {$set: {token: token, last_access: new Date()}}, function(err, result) { if (err) return callback(user_err); callback(null, token); }); }); }); }); }; //create a new user Auth.prototype.addUser = function(user, callback) { var self = this; if (!user.username) return callback({message: 'You need to specify a username'}); //default to non-admin if (!user.admin) user.admin = false; //check username doesn't already exist and add it if not function createUser(user) { userModel.get({username: user.username}, function(err, users) { if (err) return callback(err); if (users.length > 0) return callback({message: 'Username is already taken'}); self._encryptPassword(user.password, function(err, password) { if (err) return callback(err); var clear_password = user.password; user.password = password; userModel.add(user, function(err, result) { if (err) return callback(err); user.password = clear_password; callback(null, user); }); }); }); } //generate password if not provided if (!user.password) { this._randomString(function(pw) { user.password = pw; createUser(user); }); }else{ createUser(user); } };