diff --git a/core/menu_module.js b/core/menu_module.js index de81a2dd..8c811cab 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -15,7 +15,7 @@ function MenuModule(options) { var self = this; this.menuConfig = options.menuConfig; - + this.menuMethods = {}; this.viewControllers = []; this.initSequence = function() { diff --git a/core/text_view.js b/core/text_view.js index 9e375d3d..fd8ebb6b 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -28,6 +28,31 @@ function TextView(client, options) { this.dimens.height = 1; } + + + this.drawText = function(s) { + var ansiColor = this.getANSIColor(this.hasFocus ? this.getFocusColor() : this.getColor()); + + if(this.isPasswordTextStyle) { + this.client.term.write(strUtil.pad( + new Array(s.length + 1).join(this.textMaskChar), + this.dimens.width + 1, + this.fillChar, + this.justify, + ansiColor, + this.getANSIColor(this.getColor()))); + } else { + var text = strUtil.stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); + this.client.term.write(strUtil.pad( + text, + this.dimens.width + 1, + this.fillChar, + this.justify, + ansiColor, + this.getANSIColor(this.getColor()))); + } + }; + this.setText(this.options.text || ''); if(this.isPasswordTextStyle) { @@ -40,26 +65,7 @@ util.inherits(TextView, View); TextView.prototype.redraw = function() { TextView.super_.prototype.redraw.call(this); - var ansiColor = this.getANSIColor(this.hasFocus ? this.getFocusColor() : this.getColor()); - - if(this.isPasswordTextStyle) { - this.client.term.write(strUtil.pad( - new Array(this.text.length + 1).join(this.textMaskChar), - this.dimens.width, - this.fillChar, - this.justify, - ansiColor, - this.getANSIColor(this.getColor()))); - } else { - var text = strUtil.stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); - this.client.term.write(strUtil.pad( - text, - this.dimens.width, - this.fillChar, - this.justify, - ansiColor, - this.getANSIColor(this.getColor()))); - } + this.drawText(this.text); }; TextView.prototype.setFocus = function(focused) { @@ -81,7 +87,7 @@ TextView.prototype.setText = function(text) { this.text = this.text.substr(0, this.maxLength); } - this.text = strUtil.stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); + this.text = strUtil.stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); if(!this.multiLine && !this.dimens.width) { this.dimens.width = this.text.length; @@ -89,3 +95,7 @@ TextView.prototype.setText = function(text) { this.redraw(); }; + +TextView.prototype.clearText = function() { + this.setText(''); +}; diff --git a/core/user.js b/core/user.js index 110b0b85..b4474573 100644 --- a/core/user.js +++ b/core/user.js @@ -7,20 +7,20 @@ var assert = require('assert'); var async = require('async'); exports.User = User; -exports.getUserId = getUserId; +exports.getUserIdAndName = getUserIdAndName; exports.createNew = createNew; exports.persistAll = persistAll; -exports.authenticate = authenticate; +//exports.authenticate = authenticate; function User() { var self = this; - this.id = 0; - this.userName = ''; + this.userId = 0; + this.username = ''; this.properties = {}; this.isValid = function() { - if(self.id <= 0 || self.userName.length < 2) { + if(self.userId <= 0 || self.username.length < 2) { return false; } @@ -37,10 +37,11 @@ function User() { }; this.isRoot = function() { - return 1 === this.id; + return 1 === this.userId; }; this.isSysOp = this.isRoot; // alias + } User.PBKDF2 = { @@ -53,20 +54,90 @@ User.StandardPropertyGroups = { password : [ 'pw_pbkdf2_salt', 'pw_pbkdf2_dk' ], }; -function getUserId(userName, cb) { +User.prototype.authenticate = function(username, password, cb) { + var self = this; + + var cachedInfo = {}; + + async.waterfall( + [ + function fetchUserId(callback) { + // get user ID + getUserIdAndName(username, function onUserId(err, uid, un) { + cachedInfo.userId = uid; + cachedInfo.username = un; + + callback(err); + }); + }, + + function getRequiredAuthProperties(callback) { + // fetch properties required for authentication + loadProperties( { userId : cachedInfo.userId, names : User.StandardPropertyGroups.password }, function onProps(err, props) { + callback(err, props); + }); + }, + function getDkWithSalt(props, callback) { + // get DK from stored salt and password provided + generatePasswordDerivedKey(password, props.pw_pbkdf2_salt, function onDk(err, dk) { + callback(err, dk, props.pw_pbkdf2_dk); + }); + }, + function validateAuth(passDk, propsDk, callback) { + // + // Use constant time comparison here for security feel-goods + // + var passDkBuf = new Buffer(passDk, 'hex'); + var propsDkBuf = new Buffer(propsDk, 'hex'); + + if(passDkBuf.length !== propsDkBuf.length) { + callback(new Error('Invalid password')); + return; + } + + var c = 0; + for(var i = 0; i < passDkBuf.length; i++) { + c |= passDkBuf[i] ^ propsDkBuf[i]; + } + + callback(0 === c ? null : new Error('Invalid password')); + }, + function initProps(callback) { + loadProperties({ userId : cachedInfo.userId }, function onProps(err, allProps) { + if(!err) { + cachedInfo.properties = allProps; + } + + callback(err); + }); + } + ], + function complete(err) { + if(!err) { + self.userId = cachedInfo.userId; + self.username = cachedInfo.username; + self.properties = cachedInfo.properties; + } + + cb(err); + } + ); +}; + +function getUserIdAndName(username, cb) { userDb.get( - 'SELECT id ' + + 'SELECT id, user_name ' + 'FROM user ' + 'WHERE user_name LIKE ?;', - [ userName ], + [ username ], function onResults(err, row) { if(err) { cb(err); } else { if(row) { - cb(null, row.id); + cb(null, row.id, row.user_name); } else { - cb(new Error('No matching user name')); + cb(new Error('No matching username')); } } } @@ -74,7 +145,7 @@ function getUserId(userName, cb) { } function createNew(user, cb) { - assert(user.userName && user.userName.length > 1, 'Invalid userName'); + assert(user.username && user.username.length > 1, 'Invalid userName'); async.series( [ @@ -87,12 +158,12 @@ function createNew(user, cb) { userDb.run( 'INSERT INTO user (user_name) ' + 'VALUES (?);', - [ user.userName ], + [ user.username ], function onUserInsert(err) { if(err) { callback(err); } else { - user.id = this.lastID; + user.userId = this.lastID; callback(null); } } @@ -132,7 +203,7 @@ function createNew(user, cb) { if(err) { cb(err); } else { - cb(null, user.id); + cb(null, user.userId); } }); } @@ -182,14 +253,14 @@ function generatePasswordDerivedKey(password, salt, cb) { } function persistProperties(user, cb) { - assert(user.id > 0); + assert(user.userId > 0); var stmt = userDb.prepare( 'REPLACE INTO user_property (user_id, prop_name, prop_value) ' + 'VALUES (?, ?, ?);'); async.each(Object.keys(user.properties), function onProp(propName, callback) { - stmt.run(user.id, propName, user.properties[propName], function onRun(err) { + stmt.run(user.userId, propName, user.properties[propName], function onRun(err) { callback(err); }); }, function onComplete(err) { @@ -203,7 +274,35 @@ function persistProperties(user, cb) { }); } -function getProperties(userId, propNames, cb) { +function loadProperties(options, cb) { + assert(options.userId); + + var sql = + 'SELECT prop_name, prop_value ' + + 'FROM user_property ' + + 'WHERE user_id = ?'; + + if(options.names) { + sql +=' AND prop_name IN("' + options.names.join('","') + '");'; + } else { + sql += ';'; + } + + var properties = {}; + + userDb.each(sql, [ options.userId ], function onRow(err, row) { + if(err) { + cb(err); + return; + } else { + properties[row.prop_name] = row.prop_value; + } + }, function complete() { + cb(null, properties); + }); +} + +/*function getProperties(userId, propNames, cb) { var properties = {}; async.each(propNames, function onPropName(propName, next) { @@ -225,7 +324,7 @@ function getProperties(userId, propNames, cb) { } } ); - }, function onCompleteOrError(err) { + }, function complete(err) { if(err) { cb(err); } else { @@ -233,9 +332,10 @@ function getProperties(userId, propNames, cb) { } }); } +*/ function persistAll(user, useTransaction, cb) { - assert(user.id > 0); + assert(user.userId > 0); async.series( [ @@ -276,6 +376,7 @@ function persistAll(user, useTransaction, cb) { ); } +/* function authenticate(userName, password, client, cb) { assert(client); @@ -283,7 +384,7 @@ function authenticate(userName, password, client, cb) { [ function fetchUserId(callback) { // get user ID - getUserId(userName, function onUserId(err, userId) { + getUserIdAndName(userName, function onUserId(err, userId) { callback(err, userId); }); }, @@ -325,4 +426,5 @@ function authenticate(userName, password, client, cb) { } } ); -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/core/view_controller.js b/core/view_controller.js index 2d7059ea..a4fd8104 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -214,6 +214,7 @@ ViewController.prototype.loadFromMCIMapAndConfig = function(options, cb) { var self = this; var formIdKey = options.formId ? options.formId.toString() : '0'; var initialFocusId; + var formConfig; // :TODO: remove all the passing of fromConfig - use local // :TODO: break all of this up ... a lot @@ -221,15 +222,20 @@ ViewController.prototype.loadFromMCIMapAndConfig = function(options, cb) { async.waterfall( [ function getFormConfig(callback) { - menuUtil.getFormConfig(options.menuConfig, formIdKey, options.mciMap, function onFormConfig(err, formConfig) { + menuUtil.getFormConfig(options.menuConfig, formIdKey, options.mciMap, function onFormConfig(err, fc) { + formConfig = fc; + if(err) { - Log.warn(err, 'Unable to load menu configuration'); + // :TODO: fix logging of err here: + Log.warn( + { err : err, mci : Object.keys(options.mciMap), formIdKey : formIdKey } , + 'Unable to load menu configuration'); } - callback(null, formConfig); + callback(null); }); }, - function createViewsFromMCIMap(formConfig, callback) { + function createViewsFromMCIMap(callback) { async.each(Object.keys(options.mciMap), function onMciEntry(name, eachCb) { var mci = options.mciMap[name]; var view = factory.createFromMCI(mci); @@ -244,10 +250,10 @@ ViewController.prototype.loadFromMCIMapAndConfig = function(options, cb) { function eachMciComplete(err) { self.setViewOrder(); - callback(err, formConfig); + callback(err); }); }, - function applyFormConfig(formConfig, callback) { + function applyFormConfig(callback) { if(formConfig) { async.each(Object.keys(formConfig.mci), function onMciConf(mci, eachCb) { var viewId = parseInt(mci[2]); // :TODO: what about auto-generated ID's? Do they simply not apply to menu configs? @@ -274,13 +280,13 @@ ViewController.prototype.loadFromMCIMapAndConfig = function(options, cb) { eachCb(null); }, function eachMciConfComplete(err) { - callback(err, formConfig); + callback(err); }); } else { callback(null); } }, - function mapMenuSubmit(formConfig, callback) { + function mapMenuSubmit(callback) { if(formConfig) { // // If we have a 'submit' section, create a submit handler diff --git a/mods/art/LOGIN1.ANS b/mods/art/LOGIN1.ANS index c29ab7d0..5b6e6dc5 100644 Binary files a/mods/art/LOGIN1.ANS and b/mods/art/LOGIN1.ANS differ diff --git a/mods/login.js b/mods/login.js index 084f105c..5198f2ed 100644 --- a/mods/login.js +++ b/mods/login.js @@ -5,6 +5,7 @@ var ansi = require('../core/ansi_term.js'); var art = require('../core/art.js'); var user = require('../core/user.js'); var theme = require('../core/theme.js'); +var Log = require('../core/logger.js').log; var MenuModule = require('../core/menu_module.js').MenuModule; var ViewController = require('../core/view_controller.js').ViewController; @@ -26,16 +27,39 @@ function LoginModule(menuConfig) { var self = this; - this.menuMethods = { - attemptLogin : function(args) { - user.authenticate(args.username, args.password, self.client, function onAuth(err) { - if(err) { - console.log(err); - } else { - console.log('logged in!') - } - }); - } + // :TODO: Handle max login attempts before hangup + // :TODO: probably should persist failed login attempts to user DB + + this.menuMethods.attemptLogin = function(args) { + self.client.user.authenticate(args.username, args.password, function onAuth(err) { + if(err) { + Log.info( { username : args.username }, 'Failed login attempt %s', err); + + // :TODO: localize: + // :TODO: create a blink label of sorts - simulate blink with ICE + self.viewController.getView(5).setText('Invalid username or password!'); + self.clearForm(); + self.viewController.switchFocus(1); + + setTimeout(function onTimeout() { + // :TODO: should there be a block input type of pattern here? self.client.ignoreInput() ... self.client.acceptInput() + + self.viewController.getView(5).clearText(); // :TODO: for some reason this doesn't clear the last character + self.viewController.switchFocus(1); + }, 2000); + + } else { + Log.info( { username : self.client.user.username }, 'Successful login'); + + // :TODO: persist information about login to user + } + }); + }; + + this.clearForm = function() { + [ 1, 2, ].forEach(function onId(id) { + self.viewController.getView(id).clearText(); + }); }; } @@ -56,7 +80,7 @@ LoginModule.prototype.mciReady = function(mciMap) { var self = this; - var vc = self.addViewController(new ViewController(self.client)); - vc.loadFromMCIMapAndConfig( { mciMap : mciMap, menuConfig : self.menuConfig }, function onViewReady(err) { + self.viewController = self.addViewController(new ViewController(self.client)); + self.viewController.loadFromMCIMapAndConfig( { mciMap : mciMap, menuConfig : self.menuConfig }, function onViewReady(err) { }); }; \ No newline at end of file diff --git a/mods/menu.json b/mods/menu.json index 111dcee3..23285523 100644 --- a/mods/menu.json +++ b/mods/menu.json @@ -55,7 +55,7 @@ "module" : "login", "form" : { "0" : { - "BN3BN4ET1ET2" :{ + "BN3BN4ET1ET2TL5" :{ "mci" :{ "ET1" : { "focus" : true