mirror of
https://github.com/NuSkooler/enigma-bbs.git
synced 2025-08-14 21:43:56 +02:00
Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs
This commit is contained in:
commit
e4cfb2b92e
25 changed files with 488 additions and 91 deletions
20
README.md
20
README.md
|
@ -8,24 +8,24 @@ ENiGMA½ is a modern BBS software with a nostalgic flair!
|
||||||
## Feature Available Now
|
## Feature Available Now
|
||||||
* Multi platform: Anywhere Node.js runs likely works (tested under Linux and OS X)
|
* Multi platform: Anywhere Node.js runs likely works (tested under Linux and OS X)
|
||||||
* Multi node support
|
* Multi node support
|
||||||
* **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JS based mods
|
* **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based mods
|
||||||
* MCI support for lightbars, toggles, input areas, and so on plus many other other bells and whistles
|
* MCI support for lightbars, toggles, input areas, and so on plus many other other bells and whistles
|
||||||
* Telnet & SSH access built in. Additional servers are easy to implement & plug in
|
* Telnet & **SSH** access built in. Additional servers are easy to implement & plug in
|
||||||
* [CP437](http://www.ascii-codes.com/) and UTF-8 output
|
* [CP437](http://www.ascii-codes.com/) and UTF-8 output
|
||||||
* [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior.
|
* [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior
|
||||||
* [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support
|
* [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support
|
||||||
* Renegade style pipe codes
|
* Pipe codes (ala Renegade)
|
||||||
* [SQLite](http://sqlite.org/) storage of users and message areas
|
* [SQLite](http://sqlite.org/) storage of users and message areas
|
||||||
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password storage
|
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption
|
||||||
* Door support including common dropfile formats and [DOSEMU](http://www.dosemu.org/)
|
* Door support including common dropfile formats and legacy DOS doors (See [Doors](docs/doors.md))
|
||||||
* [Bunyan](https://github.com/trentm/node-bunyan) logging
|
* [Bunyan](https://github.com/trentm/node-bunyan) logging
|
||||||
|
|
||||||
## In the Works
|
## In the Works
|
||||||
* Lots of code cleanup, ES6+ usage, and **documentation**!
|
* Lots of code cleanup, ES6+ usage, and **documentation**!
|
||||||
* FTN import & export
|
* FTN import & export
|
||||||
* File areas
|
* File areas
|
||||||
* Full access checking framework
|
* Full access checking framework (ACS)
|
||||||
* SysOp console
|
* SysOp dashboard (ye ol' WFC)
|
||||||
* Missing functionality such as searching, pipe code support in message areas, etc.
|
* Missing functionality such as searching, pipe code support in message areas, etc.
|
||||||
* String localization
|
* String localization
|
||||||
* A lot more! Feel free to request features via [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
|
* A lot more! Feel free to request features via [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
|
||||||
|
@ -37,9 +37,9 @@ See [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) for more
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
* Use [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
|
* Use [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
|
||||||
|
* **Discussion on a ENiGMA BBS!**
|
||||||
* IRC: **#enigma-bbs** on **chat.freenode.net**
|
* IRC: **#enigma-bbs** on **chat.freenode.net**
|
||||||
* Email: bryan -at- l33t.codes
|
* Email: bryan -at- l33t.codes
|
||||||
* **Discussion on a ENiGMA BBS!**
|
|
||||||
* Facebook ENiGMA½ group
|
* Facebook ENiGMA½ group
|
||||||
|
|
||||||
## Terminal Clients
|
## Terminal Clients
|
||||||
|
@ -50,7 +50,7 @@ ENiGMA has been tested with many terminals. However, the following are suggested
|
||||||
|
|
||||||
## Boards
|
## Boards
|
||||||
* WQH: :skull: Xibalba :skull: (**telnet://xibalba.l33t.codes:44510**)
|
* WQH: :skull: Xibalba :skull: (**telnet://xibalba.l33t.codes:44510**)
|
||||||
* Support board: BLACK ƒlag (**telnet://blackflag.acid.org:2425**)
|
* Support board: ☠ BLACK ƒlag ☠ (**telnet://blackflag.acid.org:2425**)
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
11
core/bbs.js
11
core/bbs.js
|
@ -106,10 +106,15 @@ function initialize(cb) {
|
||||||
logger.init();
|
logger.init();
|
||||||
|
|
||||||
process.on('SIGINT', function onSigInt() {
|
process.on('SIGINT', function onSigInt() {
|
||||||
// :TODO: for any client in |clientConnections|, if 'ready', send a "Server Disconnecting" + semi-gracefull hangup
|
logger.log.info('Process interrupted, shutting down...');
|
||||||
// e.g. client.disconnectNow()
|
|
||||||
|
var activeConnections = clientConns.getActiveConnections();
|
||||||
|
var i = activeConnections.length;
|
||||||
|
while(i--) {
|
||||||
|
activeConnections[i].term.write('\n\nServer is shutting down NOW! Disconnecting...\n\n');
|
||||||
|
clientConns.removeClient(activeConnections[i]);
|
||||||
|
}
|
||||||
|
|
||||||
logger.log.info('Process interrupted, shutting down');
|
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -371,7 +371,9 @@ function Client(input, output) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(key || ch) {
|
if(key || ch) {
|
||||||
self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line
|
if(Config.logging.traceUserKeyboardInput) {
|
||||||
|
self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line
|
||||||
|
}
|
||||||
|
|
||||||
self.lastKeyPressMs = Date.now();
|
self.lastKeyPressMs = Date.now();
|
||||||
|
|
||||||
|
@ -415,7 +417,15 @@ Client.prototype.startIdleMonitor = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.end = function () {
|
Client.prototype.end = function () {
|
||||||
this.menuStack.getCurrentModule().leave();
|
if(this.term) {
|
||||||
|
this.term.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentModule = this.menuStack.getCurrentModule();
|
||||||
|
|
||||||
|
if(currentModule) {
|
||||||
|
currentModule.leave();
|
||||||
|
}
|
||||||
|
|
||||||
clearInterval(this.idleCheck);
|
clearInterval(this.idleCheck);
|
||||||
|
|
||||||
|
|
|
@ -132,6 +132,10 @@ function ClientTerminal(output) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClientTerminal.prototype.disconnect = function() {
|
||||||
|
this.output = null;
|
||||||
|
};
|
||||||
|
|
||||||
ClientTerminal.prototype.isANSI = function() {
|
ClientTerminal.prototype.isANSI = function() {
|
||||||
// :TODO: Others??
|
// :TODO: Others??
|
||||||
return [ 'ansi', 'pc-ansi', 'qansi', 'scoansi', 'syncterm' ].indexOf(this.termType) > -1;
|
return [ 'ansi', 'pc-ansi', 'qansi', 'scoansi', 'syncterm' ].indexOf(this.termType) > -1;
|
||||||
|
@ -140,11 +144,17 @@ ClientTerminal.prototype.isANSI = function() {
|
||||||
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)
|
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)
|
||||||
|
|
||||||
ClientTerminal.prototype.write = function(s, convertLineFeeds) {
|
ClientTerminal.prototype.write = function(s, convertLineFeeds) {
|
||||||
this.output.write(this.encode(s, convertLineFeeds));
|
this.rawWrite(this.encode(s, convertLineFeeds));
|
||||||
};
|
};
|
||||||
|
|
||||||
ClientTerminal.prototype.rawWrite = function(s) {
|
ClientTerminal.prototype.rawWrite = function(s) {
|
||||||
this.output.write(s);
|
if(this.output) {
|
||||||
|
this.output.write(s, function written(err) {
|
||||||
|
if(err) {
|
||||||
|
Log.warn('Failed writing to socket: ' + err.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ClientTerminal.prototype.pipeWrite = function(s, spec) {
|
ClientTerminal.prototype.pipeWrite = function(s, spec) {
|
||||||
|
|
|
@ -85,8 +85,11 @@ function getDefaultConfig() {
|
||||||
closedSystem : false, // is the system closed to new users?
|
closedSystem : false, // is the system closed to new users?
|
||||||
|
|
||||||
loginAttempts : 3,
|
loginAttempts : 3,
|
||||||
|
|
||||||
|
menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./mods)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// :TODO: see notes below about 'theme' section - move this!
|
||||||
preLoginTheme : '*',
|
preLoginTheme : '*',
|
||||||
|
|
||||||
users : {
|
users : {
|
||||||
|
|
|
@ -95,7 +95,7 @@ Door.prototype.run = function() {
|
||||||
args[i] = self.exeInfo.args[i].format({
|
args[i] = self.exeInfo.args[i].format({
|
||||||
dropFile : self.exeInfo.dropFile,
|
dropFile : self.exeInfo.dropFile,
|
||||||
node : self.exeInfo.node.toString(),
|
node : self.exeInfo.node.toString(),
|
||||||
inhSocket : self.exeInfo.inhSocket.toString(),
|
//inhSocket : self.exeInfo.inhSocket.toString(),
|
||||||
srvPort : sockServer ? sockServer.address().port.toString() : '-1',
|
srvPort : sockServer ? sockServer.address().port.toString() : '-1',
|
||||||
userId : self.client.user.userId.toString(),
|
userId : self.client.user.userId.toString(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -154,7 +154,8 @@ function DropFile(client, fileType) {
|
||||||
// :TODO: local/serial/telnet need to be configurable -- which also changes socket handle!
|
// :TODO: local/serial/telnet need to be configurable -- which also changes socket handle!
|
||||||
return iconv.encode([
|
return iconv.encode([
|
||||||
'2', // :TODO: This needs to be configurable!
|
'2', // :TODO: This needs to be configurable!
|
||||||
self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows!
|
// :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely
|
||||||
|
'-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows!
|
||||||
'57600',
|
'57600',
|
||||||
Config.general.boardName,
|
Config.general.boardName,
|
||||||
self.client.user.userId.toString(),
|
self.client.user.userId.toString(),
|
||||||
|
|
|
@ -538,7 +538,8 @@ function FullScreenEditorModule(options) {
|
||||||
this.mciReadyHandler = function(mciData, cb) {
|
this.mciReadyHandler = function(mciData, cb) {
|
||||||
|
|
||||||
self.createInitialViews(mciData, function viewsCreated(err) {
|
self.createInitialViews(mciData, function viewsCreated(err) {
|
||||||
|
// :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in
|
||||||
|
// place - if this is for existing usernames else validate spec
|
||||||
self.viewControllers.header.on('leave', function headerViewLeave(view) {
|
self.viewControllers.header.on('leave', function headerViewLeave(view) {
|
||||||
|
|
||||||
if(2 === view.id) { // "to" field
|
if(2 === view.id) { // "to" field
|
||||||
|
|
|
@ -145,5 +145,8 @@ MenuStack.prototype.goto = function(name, options, cb) {
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuStack.prototype.getCurrentModule = function() {
|
MenuStack.prototype.getCurrentModule = function() {
|
||||||
return this.top().instance;
|
var top = this.top();
|
||||||
|
if(top) {
|
||||||
|
return top.instance;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,7 +31,14 @@ function getMenuConfig(name, cb) {
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function loadMenuJSON(callback) {
|
function loadMenuJSON(callback) {
|
||||||
configCache.getModConfig('menu.hjson', function loaded(err, menuJson) {
|
var menuFilePath = Config.general.menuFile;
|
||||||
|
|
||||||
|
// menuFile is assumed to be in 'mods' if a path is not supplied
|
||||||
|
if('.' === paths.dirname(menuFilePath)) {
|
||||||
|
menuFilePath = paths.join(__dirname, '../mods', menuFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
configCache.getConfig(menuFilePath, function loaded(err, menuJson) {
|
||||||
callback(err, menuJson);
|
callback(err, menuJson);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -237,7 +237,7 @@ SSHServerModule.prototype.createServer = function() {
|
||||||
ident : 'enigma-bbs-' + enigVersion + '-srv',
|
ident : 'enigma-bbs-' + enigVersion + '-srv',
|
||||||
// Note that sending 'banner' breaks at least EtherTerm!
|
// Note that sending 'banner' breaks at least EtherTerm!
|
||||||
debug : function debugSsh(dbgLine) {
|
debug : function debugSsh(dbgLine) {
|
||||||
if(true === Config.servers.ssh.debugConnections) {
|
if(true === Config.servers.ssh.traceConnections) {
|
||||||
Log.trace('SSH: ' + dbgLine);
|
Log.trace('SSH: ' + dbgLine);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -499,7 +499,7 @@ function TelnetClient(input, output) {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.connectionDebug = function(info, msg) {
|
this.connectionDebug = function(info, msg) {
|
||||||
if(Config.servers.telnet.debugConnections) {
|
if(Config.servers.telnet.traceConnections) {
|
||||||
self.log.trace(info, 'Telnet: ' + msg);
|
self.log.trace(info, 'Telnet: ' + msg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,26 +22,8 @@ function login(callingMenu, formData, extraArgs) {
|
||||||
userLogin(callingMenu.client, formData.value.username, formData.value.password, function authResult(err) {
|
userLogin(callingMenu.client, formData.value.username, formData.value.password, function authResult(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
// login failure
|
// login failure
|
||||||
if(err.existingConn) {
|
if(err.existingConn && _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) {
|
||||||
client.term.rawWrite(ansi.resetScreen());
|
callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu);
|
||||||
|
|
||||||
var artOpts = {
|
|
||||||
client : client,
|
|
||||||
font : _.has(callingMenu, 'menuConfig.config.tooNode.font') ? callingMenu.menuConfig.config.tooNode.font : null,
|
|
||||||
name : _.has(callingMenu, 'menuConfig.config.tooNode.art') ? callingMenu.menuConfig.config.tooNode.art : 'TOONODE',
|
|
||||||
};
|
|
||||||
|
|
||||||
theme.displayThemeArt(artOpts, function artDisplayed(err) {
|
|
||||||
if(err) {
|
|
||||||
client.term.write('\nA user by that name is already logged in.\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(function timeout() {
|
|
||||||
callingMenu.prevMenu();
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
// Other error
|
// Other error
|
||||||
callingMenu.prevMenu();
|
callingMenu.prevMenu();
|
||||||
|
|
56
core/system_view_validate.js
Normal file
56
core/system_view_validate.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
var user = require('./user.js');
|
||||||
|
var Config = require('./config.js').config;
|
||||||
|
|
||||||
|
|
||||||
|
exports.validateUserNameAvail = validateUserNameAvail;
|
||||||
|
exports.validateEmailAvail = validateEmailAvail;
|
||||||
|
exports.validateBirthdate = validateBirthdate;
|
||||||
|
exports.validatePasswordSpec = validatePasswordSpec;
|
||||||
|
|
||||||
|
function validateUserNameAvail(data, cb) {
|
||||||
|
if(data.length < Config.users.usernameMin) {
|
||||||
|
cb(new Error('Username too short'));
|
||||||
|
} else if(data.length > Config.users.usernameMax) {
|
||||||
|
// generally should be unreached due to view restraints
|
||||||
|
cb(new Error('Username too long'));
|
||||||
|
} else {
|
||||||
|
var usernameRegExp = new RegExp(Config.users.usernamePattern);
|
||||||
|
var invalidNames = Config.users.newUserNames + Config.users.badUserNames;
|
||||||
|
|
||||||
|
if(!usernameRegExp.test(data)) {
|
||||||
|
cb(new Error('Username contains invalid characters'));
|
||||||
|
} else if(invalidNames.indexOf(data.toLowerCase()) > -1) {
|
||||||
|
cb(new Error('Username is blacklisted'));
|
||||||
|
} else {
|
||||||
|
user.getUserIdAndName(data, function userIdAndName(err) {
|
||||||
|
if(!err) { // err is null if we succeeded -- meaning this user exists already
|
||||||
|
cb(new Error('Userame unavailable'));
|
||||||
|
} else {
|
||||||
|
cb(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateEmailAvail(data, cb) {
|
||||||
|
user.getUserIdsWithProperty('email_address', data, function userIdsWithEmail(err, uids) {
|
||||||
|
if(err) {
|
||||||
|
cb(new Error('Internal system error'));
|
||||||
|
} else if(uids.length > 0) {
|
||||||
|
cb(new Error('Email address not unique'));
|
||||||
|
} else {
|
||||||
|
cb(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function validateBirthdate(data, cb) {
|
||||||
|
// :TODO: check for dates in the future, or > reasonable values
|
||||||
|
cb(isNaN(Date.parse(data)) ? new Error('Invalid birthdate') : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePasswordSpec(data, cb) {
|
||||||
|
cb((!data || data.length < Config.users.passwordMin) ? new Error('Password too short') : null);
|
||||||
|
}
|
|
@ -224,6 +224,12 @@ View.prototype.setPropertyValue = function(propName, value) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'argName' : this.submitArgName = value; break;
|
case 'argName' : this.submitArgName = value; break;
|
||||||
|
|
||||||
|
case 'validate' :
|
||||||
|
if(_.isFunction(value)) {
|
||||||
|
this.validate = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(/styleSGR[0-9]{1,2}/.test(propName)) {
|
if(/styleSGR[0-9]{1,2}/.test(propName)) {
|
||||||
|
|
|
@ -72,6 +72,7 @@ function ViewController(options) {
|
||||||
|
|
||||||
case 'accept' :
|
case 'accept' :
|
||||||
if(self.focusedView && self.focusedView.submit) {
|
if(self.focusedView && self.focusedView.submit) {
|
||||||
|
// :TODO: need to do validation here!!!
|
||||||
self.submitForm(key);
|
self.submitForm(key);
|
||||||
} else {
|
} else {
|
||||||
self.nextFocus();
|
self.nextFocus();
|
||||||
|
@ -157,17 +158,32 @@ function ViewController(options) {
|
||||||
|
|
||||||
case 'method' :
|
case 'method' :
|
||||||
case 'systemMethod' :
|
case 'systemMethod' :
|
||||||
if(_.isString(propAsset.location)) {
|
if('validate' === propName) {
|
||||||
|
// :TODO: handle propAsset.location for @method script specification
|
||||||
} else {
|
|
||||||
if('systemMethod' === propAsset.type) {
|
if('systemMethod' === propAsset.type) {
|
||||||
// :TODO:
|
// :TODO: implementation validation @systemMethod handling!
|
||||||
|
var methodModule = require(paths.join(__dirname, 'system_view_validate.js'));
|
||||||
|
if(_.isFunction(methodModule[propAsset.asset])) {
|
||||||
|
propValue = methodModule[propAsset.asset];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// local to current module
|
if(_.isFunction(self.client.currentMenuModule.menuMethods[propAsset.asset])) {
|
||||||
var currentModule = self.client.currentMenuModule;
|
propValue = self.client.currentMenuModule.menuMethods[propAsset.asset];
|
||||||
if(_.isFunction(currentModule.menuMethods[propAsset.asset])) {
|
}
|
||||||
// :TODO: Fix formData & extraArgs... this all needs general processing
|
}
|
||||||
propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs);
|
} else {
|
||||||
|
if(_.isString(propAsset.location)) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if('systemMethod' === propAsset.type) {
|
||||||
|
// :TODO:
|
||||||
|
} else {
|
||||||
|
// local to current module
|
||||||
|
var currentModule = self.client.currentMenuModule;
|
||||||
|
if(_.isFunction(currentModule.menuMethods[propAsset.asset])) {
|
||||||
|
// :TODO: Fix formData & extraArgs... this all needs general processing
|
||||||
|
propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -362,7 +378,54 @@ ViewController.prototype.setFocus = function(focused) {
|
||||||
};
|
};
|
||||||
|
|
||||||
ViewController.prototype.switchFocus = function(id) {
|
ViewController.prototype.switchFocus = function(id) {
|
||||||
//this.setFocus(true); // ensure events are attached
|
//
|
||||||
|
// Perform focus switching validation now
|
||||||
|
//
|
||||||
|
var self = this;
|
||||||
|
var focusedView = self.focusedView;
|
||||||
|
|
||||||
|
function performSwitch() {
|
||||||
|
self.attachClientEvents();
|
||||||
|
|
||||||
|
// remove from old
|
||||||
|
self.setViewFocusWithEvents(focusedView, false);
|
||||||
|
|
||||||
|
// set to new
|
||||||
|
self.setViewFocusWithEvents(self.getView(id), true);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if(focusedView && focusedView.validate) {
|
||||||
|
focusedView.validate(focusedView.getData(), function validated(err) {
|
||||||
|
if(_.isFunction(self.client.currentMenuModule.menuMethods.viewValidationListener)) {
|
||||||
|
if(err) {
|
||||||
|
err.view = focusedView;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.client.currentMenuModule.menuMethods.viewValidationListener(err, function validateComplete(newFocusId) {
|
||||||
|
if(err) {
|
||||||
|
// :TODO: switchFocus() really needs a cb --
|
||||||
|
var newFocusView;
|
||||||
|
if(newFocusId) {
|
||||||
|
newFocusView = self.getView(newFocusId) || focusedView;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setViewFocusWithEvents(newFocusView, true);
|
||||||
|
} else {
|
||||||
|
performSwitch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if(!err) {
|
||||||
|
performSwitch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
performSwitch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
this.attachClientEvents();
|
this.attachClientEvents();
|
||||||
|
|
||||||
// remove from old
|
// remove from old
|
||||||
|
@ -370,15 +433,19 @@ ViewController.prototype.switchFocus = function(id) {
|
||||||
|
|
||||||
// set to new
|
// set to new
|
||||||
this.setViewFocusWithEvents(this.getView(id), true);
|
this.setViewFocusWithEvents(this.getView(id), true);
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
ViewController.prototype.nextFocus = function() {
|
ViewController.prototype.nextFocus = function() {
|
||||||
|
var nextId;
|
||||||
|
|
||||||
if(!this.focusedView) {
|
if(!this.focusedView) {
|
||||||
this.switchFocus(this.views[this.firstId].id);
|
nextId = this.views[this.firstId].id;
|
||||||
} else {
|
} else {
|
||||||
var nextId = this.views[this.focusedView.id].nextId;
|
nextId = this.views[this.focusedView.id].nextId;
|
||||||
this.switchFocus(nextId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.switchFocus(nextId);
|
||||||
};
|
};
|
||||||
|
|
||||||
ViewController.prototype.setViewOrder = function(order) {
|
ViewController.prototype.setViewOrder = function(order) {
|
||||||
|
|
|
@ -174,3 +174,16 @@ doorTradeWars2002BBSLink: {
|
||||||
```
|
```
|
||||||
|
|
||||||
Fill in your credentials in `sysCode`, `authCode`, and `schemeCode` and that's it!
|
Fill in your credentials in `sysCode`, `authCode`, and `schemeCode` and that's it!
|
||||||
|
|
||||||
|
# Resources
|
||||||
|
|
||||||
|
### DOSBox
|
||||||
|
* Custom DOSBox builds http://home.arcor.de/h-a-l-9000/
|
||||||
|
|
||||||
|
## Door Downloads & Support Sites
|
||||||
|
### General
|
||||||
|
* http://bbsfiles.com/
|
||||||
|
* http://bbstorrents.bbses.info/
|
||||||
|
|
||||||
|
### L.O.R.D.
|
||||||
|
* http://lord.lordlegacy.com/
|
|
@ -4,29 +4,31 @@ ENiGMA½ is a modern from scratch BBS package written in Node.js.
|
||||||
# Quickstart
|
# Quickstart
|
||||||
TL;DR? This should get you started...
|
TL;DR? This should get you started...
|
||||||
|
|
||||||
1\. Clone
|
## Prerequisites
|
||||||
|
* [Node.js](https://nodejs.org/) version **v0.12.2 or higher** (v4.2+ is recommended)
|
||||||
|
* [io.js](https://iojs.org/) should also work, though I have not yet tested this.
|
||||||
|
* :information_source: It is suggested to use [nvm](https://github.com/creationix/nvm) to manage your Node/io.js installs
|
||||||
|
* Windows users will need additional dependencies installed for the `npm install` step in order to compile native binaries:
|
||||||
|
* A recent copy of Visual Studio (Express editions OK)
|
||||||
|
* Python 2.7.x
|
||||||
|
|
||||||
|
## Clone
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/NuSkooler/enigma-bbs.git
|
git clone https://github.com/NuSkooler/enigma-bbs.git
|
||||||
```
|
```
|
||||||
|
|
||||||
2\. Install dependencies
|
## Install Node Modules
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note for Windows users**:<br>
|
## Generate a SSH Private Key
|
||||||
Some dependencies require compilation. You will need at least the following installed for `npm install` to succeed:
|
To utilize the SSH server, a SSH Private Key will need generated. This step can be skipped if desired by disabling the SSH server in `config.hjson`.
|
||||||
* A recent copy of Visual Studio (Express editions OK)
|
|
||||||
* Python 2.7.x
|
|
||||||
|
|
||||||
3\. Generate a SSH Private Key<br>
|
|
||||||
Note that you can skip this step and disable the SSH server in your `config.hjson` if desired.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openssl genrsa -des3 -out ./misc/ssh_private_key.pem 2048
|
openssl genrsa -des3 -out ./misc/ssh_private_key.pem 2048
|
||||||
```
|
```
|
||||||
|
|
||||||
4\. Create a minimal config<br>
|
## Create a Minimal Config
|
||||||
The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`. This is a [HJSON](http://hjson.org/) file (compiliant JSON is also OK). See [Configuration](config.md) for more information.
|
The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`. This is a [HJSON](http://hjson.org/) file (compiliant JSON is also OK). See [Configuration](config.md) for more information.
|
||||||
|
|
||||||
```hjson
|
```hjson
|
||||||
|
@ -36,6 +38,8 @@ general: {
|
||||||
servers: {
|
servers: {
|
||||||
ssh: {
|
ssh: {
|
||||||
privateKeyPass: YOUR_PK_PASS
|
privateKeyPass: YOUR_PK_PASS
|
||||||
|
enabled: true /* set to false to disable the SSH server */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
messages: {
|
messages: {
|
||||||
areas: [
|
areas: [
|
||||||
|
@ -44,12 +48,17 @@ messages: {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
5\. Launch!
|
## Launch!
|
||||||
```bash
|
```bash
|
||||||
./main.js
|
./main.js
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Advanced Installation
|
||||||
|
If you've become convinced you would like a "production" BBS running ENiGMA½ a more advanced installation may be in order.
|
||||||
|
|
||||||
|
[PM2](https://github.com/Unitech/pm2) is an excellent choice for managing your running ENiGMA½ instances. Additionally, it is suggested that you run as a specific more locked down user (e.g. 'enigma').
|
||||||
|
|
||||||
Some points of interest:
|
Some points of interest:
|
||||||
* Default ports are 8888 (Telnet) and 8889 (SSH)
|
* Default ports are 8888 (Telnet) and 8889 (SSH)
|
||||||
* The first user you create via applying is the root SysOp.
|
* The first user you create via applying is the SysOp (aka root)
|
||||||
* You may want to tail the logfile with Bunyan: `tail -F ./logs/enigma-bbs.log | ./node_modules/bunyan/bin/bunyan`
|
* You may want to tail the logfile with Bunyan: `tail -F ./logs/enigma-bbs.log | ./node_modules/bunyan/bin/bunyan`
|
|
@ -70,6 +70,7 @@ function AbracadabraModule(options) {
|
||||||
|
|
||||||
this.config = options.menuConfig.config;
|
this.config = options.menuConfig.config;
|
||||||
|
|
||||||
|
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts!
|
||||||
assert(_.isString(this.config.name, 'Config \'name\' is required'));
|
assert(_.isString(this.config.name, 'Config \'name\' is required'));
|
||||||
assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required'));
|
assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required'));
|
||||||
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
|
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
|
||||||
|
@ -140,6 +141,7 @@ function AbracadabraModule(options) {
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
|
self.client.log.warn( { error : err.toString() }, 'Could not start door');
|
||||||
self.lastError = err;
|
self.lastError = err;
|
||||||
self.prevMenu();
|
self.prevMenu();
|
||||||
} else {
|
} else {
|
||||||
|
@ -158,7 +160,7 @@ function AbracadabraModule(options) {
|
||||||
encoding : self.config.encoding || self.client.term.outputEncoding,
|
encoding : self.config.encoding || self.client.term.outputEncoding,
|
||||||
dropFile : self.dropFile.fileName,
|
dropFile : self.dropFile.fileName,
|
||||||
node : self.client.node,
|
node : self.client.node,
|
||||||
inhSocket : self.client.output._handle.fd,
|
//inhSocket : self.client.output._handle.fd,
|
||||||
};
|
};
|
||||||
|
|
||||||
var doorInstance = new door.Door(self.client, exeInfo);
|
var doorInstance = new door.Door(self.client, exeInfo);
|
||||||
|
|
33
mods/art_pool.js
Normal file
33
mods/art_pool.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var MenuModule = require('../core/menu_module.js').MenuModule;
|
||||||
|
|
||||||
|
|
||||||
|
exports.getModule = ArtPoolModule;
|
||||||
|
|
||||||
|
exports.moduleInfo = {
|
||||||
|
name : 'Art Pool',
|
||||||
|
desc : 'Display art from a pool of options',
|
||||||
|
author : 'NuSkooler',
|
||||||
|
};
|
||||||
|
|
||||||
|
function ArtPoolModule(options) {
|
||||||
|
MenuModule.call(this, options);
|
||||||
|
|
||||||
|
var config = this.menuConfig.config;
|
||||||
|
|
||||||
|
//
|
||||||
|
// :TODO: General idea
|
||||||
|
// * Break up some of MenuModule initSequence's calls into methods
|
||||||
|
// * initSequence here basically has general "clear", "next", etc. as per normal
|
||||||
|
// * Display art -> ooptinal pause -> display more if requested, etc.
|
||||||
|
// * Finally exit & move on as per normal
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
require('util').inherits(ArtPoolModule, MenuModule);
|
||||||
|
|
||||||
|
MessageAreaModule.prototype.mciReady = function(mciData, cb) {
|
||||||
|
this.standardMCIReadyHandler(mciData, cb);
|
||||||
|
};
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
var MenuModule = require('../core/menu_module.js').MenuModule;
|
var MenuModule = require('../core/menu_module.js').MenuModule;
|
||||||
var Log = require('../core/logger.js').log;
|
var Log = require('../core/logger.js').log;
|
||||||
|
var resetScreen = require('../core/ansi_term.js').resetScreen;
|
||||||
|
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
@ -35,6 +36,7 @@ var packageJson = require('../package.json');
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// :TODO: BUG: When a client disconnects, it's not handled very well -- the log is spammed with tons of errors
|
// :TODO: BUG: When a client disconnects, it's not handled very well -- the log is spammed with tons of errors
|
||||||
|
// :TODO: ENH: Support nodeMax and tooManyArt
|
||||||
|
|
||||||
exports.getModule = BBSLinkModule;
|
exports.getModule = BBSLinkModule;
|
||||||
|
|
||||||
|
@ -132,6 +134,9 @@ function BBSLinkModule(options) {
|
||||||
|
|
||||||
var clientTerminated;
|
var clientTerminated;
|
||||||
|
|
||||||
|
self.client.term.write(ansi.resetScreen());
|
||||||
|
self.client.term.write(' Connecting to BBSLink.net, please wait...\n');
|
||||||
|
|
||||||
var bridgeConnection = net.createConnection(connectOpts, function connected() {
|
var bridgeConnection = net.createConnection(connectOpts, function connected() {
|
||||||
self.client.log.info(connectOpts, 'BBSLink bridge connection established');
|
self.client.log.info(connectOpts, 'BBSLink bridge connection established');
|
||||||
|
|
||||||
|
|
|
@ -77,9 +77,7 @@
|
||||||
art: USERLOG
|
art: USERLOG
|
||||||
next: fullLoginSequenceLoginArt
|
next: fullLoginSequenceLoginArt
|
||||||
config: {
|
config: {
|
||||||
tooNode: {
|
tooNodeMenu: loginAttemptTooNode
|
||||||
art: TOONODE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
form: {
|
form: {
|
||||||
0: {
|
0: {
|
||||||
|
@ -114,6 +112,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginAttemptTooNode: {
|
||||||
|
art: TOONODE
|
||||||
|
options: {
|
||||||
|
cls: true
|
||||||
|
nextTimeout: 2000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logoff: {
|
logoff: {
|
||||||
art: LOGOFF
|
art: LOGOFF
|
||||||
next: @systemMethod:logoff
|
next: @systemMethod:logoff
|
||||||
|
@ -122,6 +128,7 @@
|
||||||
TODO: display PRINT before this (Obv/2) or NEWUSER1 (Mystic)
|
TODO: display PRINT before this (Obv/2) or NEWUSER1 (Mystic)
|
||||||
*/
|
*/
|
||||||
newUserApplication: {
|
newUserApplication: {
|
||||||
|
module: nua
|
||||||
art: NUA
|
art: NUA
|
||||||
next: [
|
next: [
|
||||||
{
|
{
|
||||||
|
@ -141,6 +148,7 @@
|
||||||
focus: true
|
focus: true
|
||||||
argName: username
|
argName: username
|
||||||
maxLength: @config:users.usernameMax
|
maxLength: @config:users.usernameMax
|
||||||
|
validate: @systemMethod:validateUserNameAvail
|
||||||
}
|
}
|
||||||
ET2: {
|
ET2: {
|
||||||
argName: realName
|
argName: realName
|
||||||
|
@ -149,6 +157,7 @@
|
||||||
MET3: {
|
MET3: {
|
||||||
argName: birthdate
|
argName: birthdate
|
||||||
maskPattern: "####/##/##"
|
maskPattern: "####/##/##"
|
||||||
|
validate: @systemMethod:validateBirthdate
|
||||||
}
|
}
|
||||||
ME4: {
|
ME4: {
|
||||||
argName: sex
|
argName: sex
|
||||||
|
@ -166,6 +175,7 @@
|
||||||
ET7: {
|
ET7: {
|
||||||
argName: email
|
argName: email
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
|
validate: @systemMethod:validateEmailAvail
|
||||||
}
|
}
|
||||||
ET8: {
|
ET8: {
|
||||||
argName: web
|
argName: web
|
||||||
|
@ -175,11 +185,13 @@
|
||||||
argName: password
|
argName: password
|
||||||
password: true
|
password: true
|
||||||
maxLength: @config:users.passwordMax
|
maxLength: @config:users.passwordMax
|
||||||
|
validate: @systemMethod:validatePasswordSpec
|
||||||
}
|
}
|
||||||
ET10: {
|
ET10: {
|
||||||
argName: passwordConfirm
|
argName: passwordConfirm
|
||||||
password: true
|
password: true
|
||||||
maxLength: @config:users.passwordMax
|
maxLength: @config:users.passwordMax
|
||||||
|
validate: @method:validatePassConfirmMatch
|
||||||
}
|
}
|
||||||
TM12: {
|
TM12: {
|
||||||
argName: submission
|
argName: submission
|
||||||
|
@ -445,6 +457,13 @@
|
||||||
module: last_callers
|
module: last_callers
|
||||||
art: LASTCALL
|
art: LASTCALL
|
||||||
options: { pause: true }
|
options: { pause: true }
|
||||||
|
next: fullLoginSequenceWhosOnline
|
||||||
|
}
|
||||||
|
fullLoginSequenceWhosOnline: {
|
||||||
|
desc: Who's Online
|
||||||
|
module: whos_online
|
||||||
|
art: WHOSON
|
||||||
|
options: { pause: true }
|
||||||
next: fullLoginSequenceSysStats
|
next: fullLoginSequenceSysStats
|
||||||
}
|
}
|
||||||
fullLoginSequenceSysStats: {
|
fullLoginSequenceSysStats: {
|
||||||
|
@ -613,27 +632,31 @@
|
||||||
value: { command: "2" }
|
value: { command: "2" }
|
||||||
action: @menu:doorLORD
|
action: @menu:doorLORD
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
value: { command: "4" }
|
||||||
|
action: @menu:doorTradeWars2002BBSLink
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
The 'abracadabra' module's config.args accepts the following format objects:
|
|
||||||
{dropFile} - Path to generated dropfile
|
|
||||||
{node} - Node number
|
|
||||||
*/
|
|
||||||
doorPimpWars: {
|
doorPimpWars: {
|
||||||
desc: Playing PimpWars
|
desc: Playing PimpWars
|
||||||
module: abracadabra
|
module: abracadabra
|
||||||
config: {
|
config: {
|
||||||
name: PimpWars
|
name: PimpWars
|
||||||
dropFileType: DORINFO
|
dropFileType: DORINFO
|
||||||
cmd: /usr/bin/dosemu
|
cmd: /home/nuskooler/DOS/scripts/pimpwars.sh
|
||||||
args: [
|
args: [
|
||||||
"-quiet", "-f", "/home/nuskooler/DOS/X/LORD/dosemu.conf", "X:\\PW\\START.BAT {dropFile} {node}"
|
"{node}",
|
||||||
|
"{dropFile}",
|
||||||
|
"{srvPort}",
|
||||||
],
|
],
|
||||||
nodeMax: 1
|
nodeMax: 1
|
||||||
tooManyArt: DOORMANY
|
tooManyArt: DOORMANY
|
||||||
|
io: socket
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
doorLORD: {
|
doorLORD: {
|
||||||
desc: Playing L.O.R.D.
|
desc: Playing L.O.R.D.
|
||||||
module: abracadabra
|
module: abracadabra
|
||||||
|
@ -646,6 +669,22 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// TradeWars 2000 example via BBSLink
|
||||||
|
//
|
||||||
|
// You will need to register with BBSLink to obtain sysCode, authCode and schemeCode
|
||||||
|
//
|
||||||
|
doorTradeWars2002BBSLink: {
|
||||||
|
desc: Playing TW 2002 (BBSLink)
|
||||||
|
module: bbs_link
|
||||||
|
config: {
|
||||||
|
sysCode: XXXXXXXX
|
||||||
|
authCode: XXXXXXXX
|
||||||
|
schemeCode: XXXXXXXX
|
||||||
|
door: tw
|
||||||
|
}
|
||||||
|
}
|
||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
// Message Area Menu
|
// Message Area Menu
|
||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
@ -818,7 +857,7 @@
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
value: { 1: 3 }
|
value: { 1: 3 }
|
||||||
action: @menu:messageArea
|
action: @systemMethod:prevMenu
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
value: { 1: 4 }
|
value: { 1: 4 }
|
||||||
|
@ -1034,6 +1073,7 @@
|
||||||
argName: subject
|
argName: subject
|
||||||
maxLength: 72
|
maxLength: 72
|
||||||
submit: true
|
submit: true
|
||||||
|
// :TODO: Validate -> close/cancel if empty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
submit: {
|
submit: {
|
||||||
|
|
135
mods/nua.js
Normal file
135
mods/nua.js
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
var MenuModule = require('../core/menu_module.js').MenuModule;
|
||||||
|
var user = require('../core/user.js');
|
||||||
|
var theme = require('../core/theme.js');
|
||||||
|
var login = require('../core/system_menu_method.js').login;
|
||||||
|
var Config = require('../core/config.js').config;
|
||||||
|
|
||||||
|
var async = require('async');
|
||||||
|
|
||||||
|
exports.getModule = NewUserAppModule;
|
||||||
|
|
||||||
|
exports.moduleInfo = {
|
||||||
|
name : 'NUA',
|
||||||
|
desc : 'New User Application',
|
||||||
|
}
|
||||||
|
|
||||||
|
var MciViewIds = {
|
||||||
|
userName : 1,
|
||||||
|
password : 9,
|
||||||
|
confirm : 10,
|
||||||
|
errMsg : 11,
|
||||||
|
};
|
||||||
|
|
||||||
|
function NewUserAppModule(options) {
|
||||||
|
MenuModule.call(this, options);
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this.menuMethods = {
|
||||||
|
//
|
||||||
|
// Validation stuff
|
||||||
|
//
|
||||||
|
validatePassConfirmMatch : function(data, cb) {
|
||||||
|
var passwordView = self.viewControllers.menu.getView(MciViewIds.password);
|
||||||
|
cb(passwordView.getData() === data ? null : new Error('Passwords do not match'));
|
||||||
|
},
|
||||||
|
|
||||||
|
viewValidationListener : function(err, cb) {
|
||||||
|
var errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg);
|
||||||
|
var newFocusId;
|
||||||
|
if(err) {
|
||||||
|
errMsgView.setText(err.message);
|
||||||
|
err.view.clearText();
|
||||||
|
|
||||||
|
if(err.view.getId() === MciViewIds.confirm) {
|
||||||
|
newFocusId = MciViewIds.password;
|
||||||
|
var passwordView = self.viewControllers.menu.getView(MciViewIds.password);
|
||||||
|
passwordView.clearText();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errMsgView.clearText();
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(newFocusId);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Submit handlers
|
||||||
|
//
|
||||||
|
submitApplication : function(formData, extraArgs) {
|
||||||
|
var newUser = new user.User();
|
||||||
|
|
||||||
|
newUser.username = formData.value.username;
|
||||||
|
|
||||||
|
newUser.properties = {
|
||||||
|
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,
|
||||||
|
account_created : new Date().toISOString(),
|
||||||
|
|
||||||
|
message_area_name : getDefaultMessageArea().name,
|
||||||
|
|
||||||
|
term_height : client.term.termHeight,
|
||||||
|
term_width : client.term.termWidth,
|
||||||
|
|
||||||
|
// :TODO: This is set in User.create() -- proabbly don't need it here:
|
||||||
|
//account_status : Config.users.requireActivation ? user.User.AccountStatus.inactive : user.User.AccountStatus.active,
|
||||||
|
|
||||||
|
// :TODO: Other defaults
|
||||||
|
// :TODO: should probably have a place to create defaults/etc.
|
||||||
|
};
|
||||||
|
|
||||||
|
if('*' === Config.defaults.theme) {
|
||||||
|
newUser.properties.theme_id = theme.getRandomTheme();
|
||||||
|
} else {
|
||||||
|
newUser.properties.theme_id = Config.defaults.theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
// :TODO: .create() should also validate email uniqueness!
|
||||||
|
newUser.create( { password : formData.value.password }, function created(err) {
|
||||||
|
if(err) {
|
||||||
|
self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed');
|
||||||
|
|
||||||
|
self.gotoMenu(extraArgs.error, function result(err) {
|
||||||
|
if(err) {
|
||||||
|
self.prevMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.client.log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created');
|
||||||
|
|
||||||
|
// Cache SysOp information now
|
||||||
|
// :TODO: Similar to bbs.js. DRY
|
||||||
|
if(newUser.isSysOp()) {
|
||||||
|
Config.general.sysOp = {
|
||||||
|
username : formData.value.username,
|
||||||
|
properties : newUser.properties,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if(user.User.AccountStatus.inactive === client.user.properties.account_status) {
|
||||||
|
self.gotoMenu(extraArgs.inactive);
|
||||||
|
} else {
|
||||||
|
//
|
||||||
|
// If active now, we need to call login() to authenticate
|
||||||
|
//
|
||||||
|
login(self, formData, extraArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
require('util').inherits(NewUserAppModule, MenuModule);
|
||||||
|
|
||||||
|
NewUserAppModule.prototype.mciReady = function(mciData, cb) {
|
||||||
|
this.standardMCIReadyHandler(mciData, cb);
|
||||||
|
};
|
|
@ -78,7 +78,14 @@ WhosOnlineModule.prototype.mciReady = function(mciData, cb) {
|
||||||
userName : oe.user.username,
|
userName : oe.user.username,
|
||||||
realName : oe.user.properties.real_name,
|
realName : oe.user.properties.real_name,
|
||||||
timeOn : _.capitalize(moment.duration(55, 'minutes').humanize()),
|
timeOn : _.capitalize(moment.duration(55, 'minutes').humanize()),
|
||||||
action : oe.currentMenuModule.menuConfig.desc || 'Unknown',
|
action : function getCurrentAction() {
|
||||||
|
var cmm = oe.currentMenuModule;
|
||||||
|
if(cmm) {
|
||||||
|
return cmm.menuConfig.desc || 'Unknown';
|
||||||
|
}
|
||||||
|
return 'Unknown';
|
||||||
|
//oe.currentMenuModule.menuConfig.desc || 'Unknown',
|
||||||
|
},
|
||||||
location : oe.user.properties.location,
|
location : oe.user.properties.location,
|
||||||
affils : oe.user.properties.affiliation,
|
affils : oe.user.properties.affiliation,
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,5 +29,7 @@
|
||||||
"ssh2": "^0.4.12",
|
"ssh2": "^0.4.12",
|
||||||
"string-format": "davidchambers/string-format#mini-language"
|
"string-format": "davidchambers/string-format#mini-language"
|
||||||
},
|
},
|
||||||
"engine": "node >= 0.12.2"
|
"engines" : {
|
||||||
|
"node" : ">=0.12.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue