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

This commit is contained in:
Bryan Ashby 2015-12-12 16:03:06 -07:00
commit 581b54cb37
16 changed files with 502 additions and 91 deletions

View File

@ -8,24 +8,24 @@ ENiGMA½ is a modern BBS software with a nostalgic flair!
## Feature Available Now
* Multi platform: Anywhere Node.js runs likely works (tested under Linux and OS X)
* 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
* 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
* [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
* Renegade style pipe codes
* Pipe codes (ala Renegade)
* [SQLite](http://sqlite.org/) storage of users and message areas
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password storage
* Door support including common dropfile formats and [DOSEMU](http://www.dosemu.org/)
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption
* Door support including common dropfile formats and legacy DOS doors (See [Doors](docs/doors.md))
* [Bunyan](https://github.com/trentm/node-bunyan) logging
## In the Works
* Lots of code cleanup, ES6+ usage, and **documentation**!
* FTN import & export
* File areas
* Full access checking framework
* SysOp console
* Full access checking framework (ACS)
* SysOp dashboard (ye ol' WFC)
* Missing functionality such as searching, pipe code support in message areas, etc.
* String localization
* 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
* Use [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
* **Discussion on a ENiGMA BBS!**
* IRC: **#enigma-bbs** on **chat.freenode.net**
* Email: bryan -at- l33t.codes
* **Discussion on a ENiGMA BBS!**
* Facebook ENiGMA½ group
## Terminal Clients
@ -50,7 +50,7 @@ ENiGMA has been tested with many terminals. However, the following are suggested
## Boards
* 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

View File

@ -106,13 +106,18 @@ function initialize(cb) {
logger.init();
process.on('SIGINT', function onSigInt() {
// :TODO: for any client in |clientConnections|, if 'ready', send a "Server Disconnecting" + semi-gracefull hangup
// e.g. client.disconnectNow()
logger.log.info('Process interrupted, shutting down...');
logger.log.info('Process interrupted, shutting down');
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]);
}
process.exit();
});
// Init some extensions
require('string-format').extend(String.prototype, require('./string_util.js').stringFormatExtensions);

View File

@ -85,8 +85,11 @@ function getDefaultConfig() {
closedSystem : false, // is the system closed to new users?
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 : '*',
users : {

View File

@ -48,6 +48,8 @@ exports.moduleInfo = {
TL12 - User1
TL13 - User2
TL16 - Error / Information area
Footer - Viewing
HM1 - Menu (prev/next/etc.)
@ -73,7 +75,9 @@ var MCICodeIds = {
ViewCount : 8,
HashTags : 9,
MessageID : 10,
ReplyToMsgID : 11
ReplyToMsgID : 11,
ErrorMsg : 16,
},
ViewModeFooter : {
MsgNum : 6,
@ -538,7 +542,10 @@ function FullScreenEditorModule(options) {
this.mciReadyHandler = function(mciData, cb) {
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) {
if(2 === view.id) { // "to" field
@ -550,7 +557,7 @@ function FullScreenEditorModule(options) {
}
});
}
});
});*/
cb(err);
});
@ -761,6 +768,26 @@ function FullScreenEditorModule(options) {
};
this.menuMethods = {
//
// Validation stuff
//
viewValidationListener : function(err, cb) {
var errMsgView = self.viewControllers.header.getView(MCICodeIds.ViewModeHeader.ErrorMsg);
var newFocusViewId;
if(errMsgView) {
if(err) {
errMsgView.setText(err.message);
if(MCICodeIds.ViewModeHeader.Subject === err.view.getId()) {
// :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel)
}
} else {
errMsgView.clearText();
}
}
cb(newFocusViewId);
},
headerSubmit : function(formData, extraArgs) {
self.switchToBody();
},
@ -879,7 +906,3 @@ FullScreenEditorModule.prototype.mciReady = function(mciData, cb) {
this.mciReadyHandler(mciData, cb);
//this['mciReadyHandler' + _.capitalize(this.editorType)](mciData);
};
FullScreenEditorModule.prototype.validateToUserName = function(un, cb) {
cb(null); // note: to be implemented by sub classes
};

View File

@ -178,6 +178,11 @@ MaskEditTextView.prototype.setPropertyValue = function(propName, value) {
MaskEditTextView.prototype.getData = function() {
var rawData = MaskEditTextView.super_.prototype.getData.call(this);
if(!rawData || 0 === rawData.length) {
return rawData;
}
var data = '';
assert(rawData.length <= this.patternArray.length);

View File

@ -31,7 +31,14 @@ function getMenuConfig(name, cb) {
async.waterfall(
[
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);
});
},

View File

@ -0,0 +1,83 @@
var user = require('./user.js');
var Config = require('./config.js').config;
exports.validateNonEmpty = validateNonEmpty;
exports.validateMessageSubject = validateMessageSubject;
exports.validateUserNameAvail = validateUserNameAvail;
exports.validateEmailAvail = validateEmailAvail;
exports.validateBirthdate = validateBirthdate;
exports.validatePasswordSpec = validatePasswordSpec;
function validateNonEmpty(data, cb) {
cb(data && data.length > 0 ? null : new Error('Field cannot be empty'));
}
function validateMessageSubject(data, cb) {
cb(data && data.length > 1 ? null : new Error('Subject too short'));
};
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) {
//
// This particular method allows empty data - e.g. no email entered
//
if(!data || 0 === data.length) {
return cb(null);
}
//
// Otherwise, it must be a valid email. We'll be pretty lose here, like
// the HTML5 spec.
//
// See http://stackoverflow.com/questions/7786058/find-the-regex-used-by-html5-forms-for-validation
//
var emailRegExp = /[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/;
if(!emailRegExp.test(data)) {
return cb(new Error('Invalid email address'));
}
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);
}

View File

@ -224,6 +224,12 @@ View.prototype.setPropertyValue = function(propName, 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)) {
@ -269,4 +275,4 @@ View.prototype.onKeyPress = function(ch, key) {
};
View.prototype.getData = function() {
};
};

View File

@ -70,9 +70,20 @@ function ViewController(options) {
self.nextFocus();
break;
case 'accept' :
case 'accept' :
if(self.focusedView && self.focusedView.submit) {
self.submitForm(key);
// :TODO: need to do validation here!!!
var focusedView = self.focusedView;
self.validateView(focusedView, function validated(err, newFocusedViewId) {
console.log(err)
if(err) {
var newFocusedView = self.getView(newFocusedViewId) || focusedView;
self.setViewFocusWithEvents(newFocusedView, true);
} else {
self.submitForm(key);
}
});
//self.submitForm(key);
} else {
self.nextFocus();
}
@ -157,17 +168,32 @@ function ViewController(options) {
case 'method' :
case 'systemMethod' :
if(_.isString(propAsset.location)) {
} else {
if('validate' === propName) {
// :TODO: handle propAsset.location for @method script specification
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 {
// 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);
if(_.isFunction(self.client.currentMenuModule.menuMethods[propAsset.asset])) {
propValue = self.client.currentMenuModule.menuMethods[propAsset.asset];
}
}
} 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);
}
}
}
}
@ -297,6 +323,27 @@ function ViewController(options) {
view.setFocus(focused);
};
this.validateView = function(view, cb) {
if(view && _.isFunction(view.validate)) {
view.validate(view.getData(), function validateResult(err) {
var viewValidationListener = self.client.currentMenuModule.menuMethods.viewValidationListener;
if(_.isFunction(viewValidationListener)) {
if(err) {
err.view = view; // pass along the view that failed
}
viewValidationListener(err, function validationComplete(newViewFocusId) {
cb(err, newViewFocusId);
});
} else {
cb(err);
}
});
} else {
cb(null);
}
};
}
util.inherits(ViewController, events.EventEmitter);
@ -362,23 +409,38 @@ ViewController.prototype.setFocus = function(focused) {
};
ViewController.prototype.switchFocus = function(id) {
//this.setFocus(true); // ensure events are attached
this.attachClientEvents();
//
// Perform focus switching validation now
//
var self = this;
var focusedView = self.focusedView;
// remove from old
this.setViewFocusWithEvents(this.focusedView, false);
self.validateView(focusedView, function validated(err, newFocusedViewId) {
if(err) {
var newFocusedView = self.getView(newFocusedViewId) || focusedView;
self.setViewFocusWithEvents(newFocusedView, true);
} else {
self.attachClientEvents();
// set to new
this.setViewFocusWithEvents(this.getView(id), true);
// remove from old
self.setViewFocusWithEvents(focusedView, false);
// set to new
self.setViewFocusWithEvents(self.getView(id), true);
}
});
};
ViewController.prototype.nextFocus = function() {
ViewController.prototype.nextFocus = function() {
var nextId;
if(!this.focusedView) {
this.switchFocus(this.views[this.firstId].id);
nextId = this.views[this.firstId].id;
} else {
var nextId = this.views[this.focusedView.id].nextId;
this.switchFocus(nextId);
nextId = this.views[this.focusedView.id].nextId;
}
this.switchFocus(nextId);
};
ViewController.prototype.setViewOrder = function(order) {

View File

@ -4,29 +4,31 @@ ENiGMA½ is a modern from scratch BBS package written in Node.js.
# Quickstart
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
git clone https://github.com/NuSkooler/enigma-bbs.git
```
2\. Install dependencies
## Install Node Modules
```bash
npm install
```
**Note for Windows users**:<br>
Some dependencies require compilation. You will need at least the following installed for `npm install` to succeed:
* 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.
## Generate a SSH Private Key
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`.
```bash
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.
```hjson
@ -36,6 +38,8 @@ general: {
servers: {
ssh: {
privateKeyPass: YOUR_PK_PASS
enabled: true /* set to false to disable the SSH server */
}
}
messages: {
areas: [
@ -44,12 +48,17 @@ messages: {
}
```
5\. Launch!
## Launch!
```bash
./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:
* 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`

33
mods/art_pool.js Normal file
View 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);
};

View File

@ -77,9 +77,7 @@
art: USERLOG
next: fullLoginSequenceLoginArt
config: {
tooNode: {
art: TOONODE
}
tooNodeMenu: loginAttemptTooNode
}
form: {
0: {
@ -114,6 +112,14 @@
}
}
loginAttemptTooNode: {
art: TOONODE
options: {
cls: true
nextTimeout: 2000
}
}
logoff: {
art: LOGOFF
next: @systemMethod:logoff
@ -122,6 +128,7 @@
TODO: display PRINT before this (Obv/2) or NEWUSER1 (Mystic)
*/
newUserApplication: {
module: nua
art: NUA
next: [
{
@ -141,23 +148,28 @@
focus: true
argName: username
maxLength: @config:users.usernameMax
validate: @systemMethod:validateUserNameAvail
}
ET2: {
argName: realName
maxLength: 32
validate: @systemMethod:validateNonEmpty
}
MET3: {
argName: birthdate
maskPattern: "####/##/##"
validate: @systemMethod:validateBirthdate
}
ME4: {
argName: sex
maskPattern: A
textStyle: upper
validate: @systemMethod:validateNonEmpty
}
ET5: {
argName: location
maxLength: 32
validate: @systemMethod:validateNonEmpty
}
ET6: {
argName: affils
@ -166,6 +178,7 @@
ET7: {
argName: email
maxLength: 255
validate: @systemMethod:validateEmailAvail
}
ET8: {
argName: web
@ -175,11 +188,13 @@
argName: password
password: true
maxLength: @config:users.passwordMax
validate: @systemMethod:validatePasswordSpec
}
ET10: {
argName: passwordConfirm
password: true
maxLength: @config:users.passwordMax
validate: @method:validatePassConfirmMatch
}
TM12: {
argName: submission
@ -192,7 +207,7 @@
*: [
{
value: { "submission" : 0 }
action: @method:apply/submitApplication
action: @method:submitApplication
extraArgs: {
inactive: userNeedsActivated
error: newUserCreateError
@ -227,23 +242,28 @@
focus: true
argName: username
maxLength: @config:users.usernameMax
validate: @systemMethod:validateUserNameAvail
}
ET2: {
argName: realName
maxLength: 32
validate: @systemMethod:validateNonEmpty
}
MET3: {
argName: birthdate
maskPattern: "####/##/##"
validate: @systemMethod:validateBirthdate
}
ME4: {
argName: sex
maskPattern: A
textStyle: upper
validate: @systemMethod:validateNonEmpty
}
ET5: {
argName: location
maxLength: 32
validate: @systemMethod:validateNonEmpty
}
ET6: {
argName: affils
@ -252,6 +272,7 @@
ET7: {
argName: email
maxLength: 255
validate: @systemMethod:validateEmailAvail
}
ET8: {
argName: web
@ -261,11 +282,13 @@
argName: password
password: true
maxLength: @config:users.passwordMax
validate: @systemMethod:validatePasswordSpec
}
ET10: {
argName: passwordConfirm
password: true
maxLength: @config:users.passwordMax
validate: @method:validatePassConfirmMatch
}
TM12: {
argName: submission
@ -278,7 +301,7 @@
*: [
{
value: { "submission" : 0 }
action: @method:apply/submitApplication
action: @method:submitApplication
extraArgs: {
inactive: userNeedsActivated
error: newUserCreateError
@ -350,6 +373,7 @@
maxLength: 72
submit: true
text: New user feedback
validate: @systemMethod:validateMessageSubject
}
}
submit: {
@ -445,7 +469,14 @@
module: last_callers
art: LASTCALL
options: { pause: true }
next: fullLoginSequenceSysStats
next: fullLoginSequenceWhosOnline
}
fullLoginSequenceWhosOnline: {
desc: Who's Online
module: whos_online
art: WHOSON
options: { pause: true }
next: fullLoginSequenceSysStats
}
fullLoginSequenceSysStats: {
desc: System Stats
@ -613,27 +644,31 @@
value: { command: "2" }
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: {
desc: Playing PimpWars
module: abracadabra
config: {
name: PimpWars
dropFileType: DORINFO
cmd: /usr/bin/dosemu
cmd: /home/nuskooler/DOS/scripts/pimpwars.sh
args: [
"-quiet", "-f", "/home/nuskooler/DOS/X/LORD/dosemu.conf", "X:\\PW\\START.BAT {dropFile} {node}"
"{node}",
"{dropFile}",
"{srvPort}",
],
nodeMax: 1
tooManyArt: DOORMANY
io: socket
}
},
}
doorLORD: {
desc: Playing L.O.R.D.
module: abracadabra
@ -646,6 +681,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
///////////////////////////////////////////////////////////////////////
@ -818,7 +869,7 @@
}
{
value: { 1: 3 }
action: @menu:messageArea
action: @systemMethod:prevMenu
}
{
value: { 1: 4 }
@ -882,11 +933,13 @@
ET2: {
argName: to
focus: true
validate: @systemMethod:validateNonEmpty
}
ET3: {
argName: subject
maxLength: 72
submit: true
validate: @systemMethod:validateNonEmpty
}
TL4: {
// :TODO: this is for RE: line (NYI)
@ -1029,11 +1082,14 @@
argName: to
focus: true
text: All
validate: @systemMethod:validateNonEmpty
}
ET3: {
argName: subject
maxLength: 72
submit: true
validate: @systemMethod:validateNonEmpty
// :TODO: Validate -> close/cancel if empty
}
}
submit: {

View File

@ -64,22 +64,3 @@ AreaPostFSEModule.prototype.enter = function(client) {
AreaPostFSEModule.super_.prototype.enter.call(this, client);
};
AreaPostFSEModule.prototype.validateToUserName = function(un, cb) {
var self = this;
if(!un) {
cb(new Error('Username must be supplied!'));
return;
}
if(!self.isLocalEmail()) {
cb(null);
return;
}
user.getUserIdAndName(un, function uidAndName(err, userId, userName) {
self.toUserId = userId;
cb(err);
});
};

136
mods/nua.js Normal file
View File

@ -0,0 +1,136 @@
/* 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 getDefaultMessageArea = require('../core/message_area.js').getDefaultMessageArea;
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 : self.client.term.termHeight,
term_width : self.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 === self.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);
};

View File

@ -29,5 +29,7 @@
"ssh2": "^0.4.12",
"string-format": "davidchambers/string-format#mini-language"
},
"engine": "node >= 0.12.2"
"engines" : {
"node" : ">=0.12.2"
}
}