Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs

This commit is contained in:
Bryan Ashby 2015-12-24 11:59:37 -07:00
commit 513ff7109f
14 changed files with 330 additions and 50 deletions

View File

@ -96,8 +96,16 @@ function getDefaultConfig() {
usernameMin : 2, usernameMin : 2,
usernameMax : 16, // Note that FidoNet wants 36 max usernameMax : 16, // Note that FidoNet wants 36 max
usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+]+$', usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+]+$',
passwordMin : 6, passwordMin : 6,
passwordMax : 128, passwordMax : 128,
realNameMax : 32,
locationMax : 32,
affilsMax : 32,
emailMax : 255,
webMax : 255,
requireActivation : true, // require SysOp activation? requireActivation : true, // require SysOp activation?
invalidUsernames : [], invalidUsernames : [],

View File

@ -103,6 +103,14 @@ MaskEditTextView.maskPatternCharacterRegEx = {
'&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255 '&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255
}; };
MaskEditTextView.prototype.setText = function(text) {
MaskEditTextView.super_.prototype.setText.call(this, text);
if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText()
this.patternArrayPos = this.patternArray.length;
}
};
MaskEditTextView.prototype.setMaskPattern = function(pattern) { MaskEditTextView.prototype.setMaskPattern = function(pattern) {
this.dimens.width = pattern.length; this.dimens.width = pattern.length;

View File

@ -64,6 +64,12 @@ SpinnerMenuView.prototype.setFocus = function(focused) {
this.redraw(); this.redraw();
}; };
SpinnerMenuView.prototype.setFocusItemIndex = function(index) {
SpinnerMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
this.updateSelection(); // will redraw
};
SpinnerMenuView.prototype.onKeyPress = function(ch, key) { SpinnerMenuView.prototype.onKeyPress = function(ch, key) {
if(key) { if(key) {
if(this.isKeyMapped('up', key.name)) { if(this.isKeyMapped('up', key.name)) {

View File

@ -14,7 +14,7 @@ function validateNonEmpty(data, cb) {
function validateMessageSubject(data, cb) { function validateMessageSubject(data, cb) {
cb(data && data.length > 1 ? null : new Error('Subject too short')); cb(data && data.length > 1 ? null : new Error('Subject too short'));
}; }
function validateUserNameAvail(data, cb) { function validateUserNameAvail(data, cb) {
if(data.length < Config.users.usernameMin) { if(data.length < Config.users.usernameMin) {

View File

@ -89,6 +89,7 @@ function TextView(options) {
return this.position.col + offset; return this.position.col + offset;
}; };
// :TODO: Whatever needs init here should be done separately from setText() since it redraws/etc.
this.setText(options.text || ''); this.setText(options.text || '');
} }
@ -97,7 +98,9 @@ util.inherits(TextView, View);
TextView.prototype.redraw = function() { TextView.prototype.redraw = function() {
TextView.super_.prototype.redraw.call(this); TextView.super_.prototype.redraw.call(this);
this.drawText(this.text); if(_.isString(this.text)) {
this.drawText(this.text);
}
}; };
TextView.prototype.setFocus = function(focused) { TextView.prototype.setFocus = function(focused) {

View File

@ -16,8 +16,10 @@ var async = require('async');
var _ = require('lodash'); var _ = require('lodash');
var assert = require('assert'); var assert = require('assert');
exports.loadTheme = loadTheme; exports.loadTheme = loadTheme;
exports.getThemeArt = getThemeArt; exports.getThemeArt = getThemeArt;
exports.getAvailableThemes = getAvailableThemes;
exports.getRandomTheme = getRandomTheme; exports.getRandomTheme = getRandomTheme;
exports.initAvailableThemes = initAvailableThemes; exports.initAvailableThemes = initAvailableThemes;
exports.displayThemeArt = displayThemeArt; exports.displayThemeArt = displayThemeArt;
@ -72,7 +74,7 @@ function refreshThemeHelpers(theme) {
return format; return format;
} }
} };
} }
function loadTheme(themeID, cb) { function loadTheme(themeID, cb) {
@ -83,8 +85,11 @@ function loadTheme(themeID, cb) {
if(err) { if(err) {
cb(err); cb(err);
} else { } else {
if(!_.isObject(theme.info)) { if(!_.isObject(theme.info) ||
cb(new Error('Invalid theme or missing \'info\' section')); !_.isString(theme.info.name) ||
!_.isString(theme.info.author))
{
cb(new Error('Invalid or missing "info" section!'));
return; return;
} }
@ -128,6 +133,8 @@ function initAvailableThemes(cb) {
}); });
Log.debug( { info : theme.info }, 'Theme loaded'); Log.debug( { info : theme.info }, 'Theme loaded');
} else {
Log.warn( { themeId : themeId, error : err.toString() }, 'Failed to load theme');
} }
}); });
@ -146,6 +153,10 @@ function initAvailableThemes(cb) {
); );
} }
function getAvailableThemes() {
return availableThemes;
}
function getRandomTheme() { function getRandomTheme() {
if(Object.getOwnPropertyNames(availableThemes).length > 0) { if(Object.getOwnPropertyNames(availableThemes).length > 0) {
var themeIds = Object.keys(availableThemes); var themeIds = Object.keys(availableThemes);

View File

@ -375,6 +375,25 @@ User.prototype.persistAllProperties = function(cb) {
*/ */
}; };
User.prototype.setNewAuthCredentials = function(password, cb) {
var self = this;
generatePasswordDerivedKeyAndSalt(password, function dkAndSalt(err, info) {
if(err) {
cb(err);
} else {
var newProperties = {
pw_pbkdf2_salt : info.salt,
pw_pbkdf2_dk : info.dk,
};
self.persistProperties(newProperties, function persisted(err) {
cb(err);
});
}
});
};
User.prototype.getAge = function() { User.prototype.getAge = function() {
if(_.has(this.properties, 'birthdate')) { if(_.has(this.properties, 'birthdate')) {
return moment().diff(this.properties.birthdate, 'years'); return moment().diff(this.properties.birthdate, 'years');

View File

@ -3,6 +3,8 @@
var MenuModule = require('./menu_module.js').MenuModule; var MenuModule = require('./menu_module.js').MenuModule;
var ViewController = require('./view_controller.js').ViewController; var ViewController = require('./view_controller.js').ViewController;
var theme = require('./theme.js');
var sysValidate = require('./system_view_validate.js');
var async = require('async'); var async = require('async');
var assert = require('assert'); var assert = require('assert');
@ -18,35 +20,134 @@ exports.moduleInfo = {
}; };
var MciCodeIds = { var MciCodeIds = {
Email : 1, RealName : 1,
Loc : 2, BirthDate : 2,
Web : 3, Sex : 3,
Affils : 4, Loc : 4,
Affils : 5,
BirthDate : 5, Email : 6,
Sex : 6, Web : 7,
TermHeight : 8,
Theme : 10, Theme : 9,
ScreenSize : 11, Password : 10,
PassConfirm : 11,
ThemeInfo : 20,
ErrorMsg : 21,
SaveCancel : 25,
}; };
function UserConfigModule(options) { function UserConfigModule(options) {
MenuModule.call(this, options); MenuModule.call(this, options);
var self = this; var self = this;
self.getView = function(viewId) {
return self.viewControllers.menu.getView(viewId);
};
self.setViewText = function(viewId, text) { self.setViewText = function(viewId, text) {
var v = self.viewControllers.menu.getView(viewId); var v = self.getView(viewId);
if(v) { if(v) {
v.setText(text); v.setText(text);
} }
}; };
this.menuMethods = { this.menuMethods = {
exitKeyPressed : function(formData, extraArgs) { //
// :TODO: save/etc. // Validation support
self.prevMenu(); //
} validateEmailAvail : function(data, cb) {
//
// If nothing changed, we know it's OK
//
if(self.client.user.properties.email_address.toLowerCase() === data.toLowerCase()) {
return cb(null);
}
// Otherwise we can use the standard system method
return sysValidate.validateEmailAvail(data, cb);
},
validatePassword : function(data, cb) {
//
// Blank is OK - this means we won't be changing it
//
if(!data || 0 === data.length) {
return cb(null);
}
// Otherwise we can use the standard system method
return sysValidate.validatePasswordSpec(data, cb);
},
validatePassConfirmMatch : function(data, cb) {
var passwordView = self.getView(MciCodeIds.Password);
cb(passwordView.getData() === data ? null : new Error('Passwords do not match'));
},
viewValidationListener : function(err, cb) {
var errMsgView = self.getView(MciCodeIds.ErrorMsg);
var newFocusId;
if(errMsgView) {
if(err) {
errMsgView.setText(err.message);
if(err.view.getId() === MciCodeIds.PassConfirm) {
newFocusId = MciCodeIds.Password;
var passwordView = self.getView(MciCodeIds.Password);
passwordView.clearText();
err.view.clearText();
}
} else {
errMsgView.clearText();
}
}
cb(newFocusId);
},
saveChanges : function(formData, extraArgs) {
assert(formData.value.password === formData.value.passwordConfirm);
var newProperties = {
real_name : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(),
sex : formData.value.sex,
location : formData.value.location,
affiliation : formData.value.affils,
email_address : formData.value.email,
web_address : formData.value.web,
term_height : formData.value.termHeight.toString(),
theme_id : self.availThemeInfo[formData.value.theme].themeId,
};
self.client.user.persistProperties(newProperties, function persisted(err) {
if(err) {
self.client.log.warn( { error : err.toString() }, 'Failed persisting updated properties');
// :TODO: warn end user!
self.prevMenu();
} else {
//
// New password if it's not empty
//
self.client.log.info('User updated properties');
if(formData.value.password.length > 0) {
self.client.user.setNewAuthCredentials(formData.value.password, function newAuthStored(err) {
if(err) {
// :TODO: warn the end user!
self.client.log.warn( { error : err.toString() }, 'Failed storing new authentication credentials');
} else {
self.client.log.info('User changed authentication credentials');
}
self.prevMenu();
});
} else {
self.prevMenu();
}
}
});
},
}; };
} }
@ -55,6 +156,8 @@ require('util').inherits(UserConfigModule, MenuModule);
UserConfigModule.prototype.mciReady = function(mciData, cb) { UserConfigModule.prototype.mciReady = function(mciData, cb) {
var self = this; var self = this;
var vc = self.viewControllers.menu = new ViewController( { client : self.client} ); var vc = self.viewControllers.menu = new ViewController( { client : self.client} );
var currentThemeIdIndex = 0;
async.series( async.series(
[ [
@ -64,17 +167,57 @@ UserConfigModule.prototype.mciReady = function(mciData, cb) {
function loadFromConfig(callback) { function loadFromConfig(callback) {
vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
}, },
function prepareAvailableThemes(callback) {
self.availThemeInfo = _.sortBy(_.map(theme.getAvailableThemes(), function makeThemeInfo(t, themeId) {
return {
themeId : themeId,
name : t.info.name,
author : t.info.author,
desc : _.isString(t.info.desc) ? t.info.desc : '',
group : _.isString(t.info.group) ? t.info.group : '',
};
}), 'name');
currentThemeIdIndex = _.findIndex(self.availThemeInfo, function cmp(ti) {
return ti.themeId === self.client.user.properties.theme_id;
});
callback(null);
},
function populateViews(callback) { function populateViews(callback) {
var user = self.client.user; var user = self.client.user;
self.setViewText(MciCodeIds.Email, user.properties.email_address); self.setViewText(MciCodeIds.RealName, user.properties.real_name);
self.setViewText(MciCodeIds.Loc, user.properties.location);
self.setViewText(MciCodeIds.Web, user.properties.web_address);
self.setViewText(MciCodeIds.Affils, user.properties.affiliation);
self.setViewText(MciCodeIds.BirthDate, moment(user.properties.birthdate).format('YYYYMMDD')); self.setViewText(MciCodeIds.BirthDate, moment(user.properties.birthdate).format('YYYYMMDD'));
self.setViewText(MciCodeIds.Sex, user.properties.sex); self.setViewText(MciCodeIds.Sex, user.properties.sex);
self.setViewText(MciCodeIds.Loc, user.properties.location);
self.setViewText(MciCodeIds.Affils, user.properties.affiliation);
self.setViewText(MciCodeIds.Email, user.properties.email_address);
self.setViewText(MciCodeIds.Web, user.properties.web_address);
self.setViewText(MciCodeIds.TermHeight, user.properties.term_height.toString());
var themeView = self.getView(MciCodeIds.Theme);
if(themeView) {
themeView.setItems(_.map(self.availThemeInfo, 'name'));
themeView.setFocusItemIndex(currentThemeIdIndex);
}
var realNameView = self.getView(MciCodeIds.RealName);
if(realNameView) {
realNameView.setFocus(true); // :TODO: HACK! menu.hjson sets focus, but manual population above breaks this. Needs a real fix!
}
callback(null);
} }
] ],
function complete(err) {
if(err) {
self.client.log.warn( { error : err.toString() }, 'User configuration failed to init');
self.prevMenu();
} else {
cb(null);
}
}
); );
}; };

View File

@ -146,6 +146,12 @@ VerticalMenuView.prototype.setFocus = function(focused) {
this.redraw(); this.redraw();
}; };
VerticalMenuView.prototype.setFocusItemIndex = function(index) {
VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
this.redraw();
};
VerticalMenuView.prototype.onKeyPress = function(ch, key) { VerticalMenuView.prototype.onKeyPress = function(ch, key) {
if(key) { if(key) {

View File

@ -103,7 +103,7 @@ function View(options) {
this.restoreCursor = function() { this.restoreCursor = function() {
//this.client.term.write(ansi.setCursorStyle(this.cursorStyle)); //this.client.term.write(ansi.setCursorStyle(this.cursorStyle));
this.client.term.rawWrite('show' === this.cursor ? ansi.showCursor() : ansi.hideCursor()); this.client.term.rawWrite('show' === this.cursor ? ansi.showCursor() : ansi.hideCursor());
}; };
} }
util.inherits(View, events.EventEmitter); util.inherits(View, events.EventEmitter);

View File

@ -190,7 +190,7 @@
} }
ET2: { ET2: {
argName: realName argName: realName
maxLength: 32 maxLength: @config:users.realNameMax
validate: @systemMethod:validateNonEmpty validate: @systemMethod:validateNonEmpty
} }
MET3: { MET3: {
@ -206,21 +206,21 @@
} }
ET5: { ET5: {
argName: location argName: location
maxLength: 32 maxLength: @config:users.locationMax
validate: @systemMethod:validateNonEmpty validate: @systemMethod:validateNonEmpty
} }
ET6: { ET6: {
argName: affils argName: affils
maxLength: 32 maxLength: @config:users.affilsMax
} }
ET7: { ET7: {
argName: email argName: email
maxLength: 255 maxLength: @config:users.emailMax
validate: @systemMethod:validateEmailAvail validate: @systemMethod:validateEmailAvail
} }
ET8: { ET8: {
argName: web argName: web
maxLength: 255 maxLength: @config:users.webMax
} }
ET9: { ET9: {
argName: password argName: password
@ -287,7 +287,7 @@
} }
ET2: { ET2: {
argName: realName argName: realName
maxLength: 32 maxLength: @config:users.realNameMax
validate: @systemMethod:validateNonEmpty validate: @systemMethod:validateNonEmpty
} }
MET3: { MET3: {
@ -303,21 +303,21 @@
} }
ET5: { ET5: {
argName: location argName: location
maxLength: 32 maxLength: @config:users.locationMax
validate: @systemMethod:validateNonEmpty validate: @systemMethod:validateNonEmpty
} }
ET6: { ET6: {
argName: affils argName: affils
maxLength: 32 maxLength: @config:users.affilsMax
} }
ET7: { ET7: {
argName: email argName: email
maxLength: 255 maxLength: @config:users.emailMax
validate: @systemMethod:validateEmailAvail validate: @systemMethod:validateEmailAvail
} }
ET8: { ET8: {
argName: web argName: web
maxLength: 255 maxLength: @config:users.webMax
} }
ET9: { ET9: {
argName: password argName: password
@ -625,6 +625,7 @@
} }
} }
} }
mainMenuUserConfig: { mainMenuUserConfig: {
module: @systemModule:user_config module: @systemModule:user_config
art: CONFSCR art: CONFSCR
@ -632,30 +633,83 @@
0: { 0: {
mci: { mci: {
ET1: { ET1: {
argName: email argName: realName
maxLength: @config:users.realNameMax
validate: @systemMethod:validateNonEmpty
focus: true
} }
ET2: { ME2: {
argName: location argName: birthdate
maskPattern: "####/##/##"
} }
ET3: { ME3: {
argName: webAddress argName: sex
maskPattern: A
textStyle: upper
validate: @systemMethod:validateNonEmpty
} }
ET4: { ET4: {
argName: location
maxLength: @config:users.locationMax
validate: @systemMethod:validateNonEmpty
}
ET5: {
argName: affils argName: affils
maxLength: @config:users.affilsMax
} }
ME5: { ET6: {
maskPattern: "####/##/##" argName: email
argName: birthdate maxLength: @config:users.emailMax
validate: @method:validateEmailAvail
} }
ME11: { ET7: {
maskPattern: "##x##" argName: web
argName: termSize maxLength: @config:users.webMax
}
ME8: {
maskPattern: "##"
argName: termHeight
validate: @systemMethod:validateNonEmpty
}
SM9: {
argName: theme
}
ET10: {
argName: password
maxLength: @config:users.passwordMax
password: true
validate: @method:validatePassword
}
ET11: {
argName: passwordConfirm
maxLength: @config:users.passwordMax
password: true
validate: @method:validatePassConfirmMatch
}
TM25: {
argName: submission
items: [ "save", "cancel" ]
submit: true
} }
} }
submit: {
*: [
{
value: { submission: 0 }
action: @method:saveChanges
}
{
value: { submission: 1 }
action: @systemMethod:prevMenu
}
]
}
actionKeys: [ actionKeys: [
{ {
keys: [ "escape" ] keys: [ "escape" ]
action: @method:exitKeyPressed action: @systemMethod:prevMenu
} }
] ]
} }

View File

@ -14,7 +14,7 @@ exports.getModule = NewUserAppModule;
exports.moduleInfo = { exports.moduleInfo = {
name : 'NUA', name : 'NUA',
desc : 'New User Application', desc : 'New User Application',
} };
var MciViewIds = { var MciViewIds = {
userName : 1, userName : 1,

View File

@ -273,6 +273,28 @@
ST8: { width: 17 } ST8: { width: 17 }
} }
} }
mainMenuUserConfig: {
mci: {
ET1: { width: 27 }
ME2: { width: 27 }
ME3: { width: 27 }
ET4: { width: 27 }
ET5: { width: 27 }
ET6: { width: 27 }
ET7: { width: 27 }
ET8: { width: 27 }
ET9: { width: 27 }
ET10: { width: 18 }
ET11: { width: 18 }
TL20: { width: 71 }
TL21: { width: 43 }
TM25: {
focusTextStyle: first lower
}
}
}
} }
} }
} }