Merge branch '0.0.9-alpha' of github.com:NuSkooler/enigma-bbs into user-interruptions

This commit is contained in:
Bryan Ashby 2018-11-23 22:19:18 -07:00
commit 36e9356663
84 changed files with 1759 additions and 643 deletions

View file

@ -61,6 +61,7 @@ webSocket: {
* The `system.db` `user_event_log` table has been updated to include a unique session ID. Previously this table was not used, but you will need to perform a slight maintenance task before it can be properly used. After updating to `0.0.9-alpha`, please run the following: `sqlite3 db/system.db DROP TABLE user_event_log;`. The new table format will be created and used at startup. * The `system.db` `user_event_log` table has been updated to include a unique session ID. Previously this table was not used, but you will need to perform a slight maintenance task before it can be properly used. After updating to `0.0.9-alpha`, please run the following: `sqlite3 db/system.db DROP TABLE user_event_log;`. The new table format will be created and used at startup.
* If you have art configured for message conference or area selection via the `art` configuration value, you will need to include a `show_art` menu reference. Defaulted to `changeMessageConfPreArt` for conferences and `changeMessageAreaPreArt` for areas & included in the example `menu.hjson`. * If you have art configured for message conference or area selection via the `art` configuration value, you will need to include a `show_art` menu reference. Defaulted to `changeMessageConfPreArt` for conferences and `changeMessageAreaPreArt` for areas & included in the example `menu.hjson`.
* Config `defaults` section was theme related and as such, has been renamed to `theme`. `defaults.theme` is now `theme.default`, and `preLoginTheme` is now `theme.preLogin`. See `config.js` if this isn't clear as mud. * Config `defaults` section was theme related and as such, has been renamed to `theme`. `defaults.theme` is now `theme.default`, and `preLoginTheme` is now `theme.preLogin`. See `config.js` if this isn't clear as mud.
* Similar to the last item, `defaults.general.passwordChar` in `theme.hjson` is now just `defaults.passwordChar`.
# 0.0.7-alpha to 0.0.8-alpha # 0.0.7-alpha to 0.0.8-alpha

View file

@ -21,7 +21,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
* New MCI codes including general purpose movement codes. See [MCI codes](docs/art/mci.md) * New MCI codes including general purpose movement codes. See [MCI codes](docs/art/mci.md)
* `install.sh` will now attempt to use NPM's `--build-from-source` option when ARM is detected. * `install.sh` will now attempt to use NPM's `--build-from-source` option when ARM is detected.
* `oputil.js config new` will now generate a much more complete configuration file with comments, examples, etc. `oputil.js config cat` dumps your current config to stdout. * `oputil.js config new` will now generate a much more complete configuration file with comments, examples, etc. `oputil.js config cat` dumps your current config to stdout.
* Handling of failed login attempts is now fully in. Disconnect clients, lock out accounts, ability to auto or unlock at (email-driven) password reset, etc. See `users.failedLogin` in `config.hjson`.
## 0.0.8-alpha ## 0.0.8-alpha

Binary file not shown.

Binary file not shown.

View file

@ -9,9 +9,7 @@
customization: { customization: {
defaults: { defaults: {
general: { passwordChar: *
passwordChar: *
}
dateTimeFormat: { dateTimeFormat: {
short: MMM Do h:mm a short: MMM Do h:mm a
@ -256,6 +254,16 @@
} }
} }
messageAreaSetNewScanDate: {
mci: {
SM2: {
width: 54
itemFormat: "|00|07{conf.name} |08- |07{area.name}"
focusItemFormat: "|00|15{conf.name} |07- |15{area.name}"
}
}
}
mailMenuCreateMessage: { mailMenuCreateMessage: {
0: { 0: {
mci: { mci: {
@ -800,16 +808,13 @@
} }
fileBaseDownloadManager: { fileBaseDownloadManager: {
config: {
queueListFormat: "|00|03{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
focusQueueListFormat: "|00|19|15{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
}
0: { 0: {
mci: { mci: {
VM1: { VM1: {
height: 11 height: 11
width: 69 width: 69
itemFormat: "|00|03{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
focusItemFormat: "|00|19|15{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
} }
HM2: { HM2: {
width: 50 width: 50
@ -821,8 +826,6 @@
fileBaseWebDownloadManager: { fileBaseWebDownloadManager: {
config: { config: {
queueListFormat: "|00|03{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
focusQueueListFormat: "|00|19|15{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
queueManagerInfoFormat10: "|03batch|08: |03{webBatchDlLink}" queueManagerInfoFormat10: "|03batch|08: |03{webBatchDlLink}"
queueManagerInfoFormat11: "|03exp |08: |03{webBatchDlExpire}" queueManagerInfoFormat11: "|03exp |08: |03{webBatchDlExpire}"
} }
@ -831,6 +834,8 @@
mci: { mci: {
VM1: { VM1: {
height: 8 height: 8
itemFormat: "|00|03{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
focusItemFormat: "|00|19|15{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
} }
HM2: { HM2: {
width: 50 width: 50

View file

@ -847,6 +847,8 @@ function peg$parse(input, options) {
const client = options.client; const client = options.client;
const user = options.client.user; const user = options.client.user;
const UserProps = require('./user_property.js');
const moment = require('moment'); const moment = require('moment');
function checkAccess(acsCode, value) { function checkAccess(acsCode, value) {
@ -863,7 +865,7 @@ function peg$parse(input, options) {
value = [ value ]; value = [ value ];
} }
const userAccountStatus = parseInt(user.properties.account_status, 10); const userAccountStatus = user.getPropertyAsNumber(UserProps.AccountStatus);
return value.map(n => parseInt(n, 10)).includes(userAccountStatus); return value.map(n => parseInt(n, 10)).includes(userAccountStatus);
}, },
EC : function isEncoding() { EC : function isEncoding() {
@ -888,15 +890,15 @@ function peg$parse(input, options) {
return value.map(n => parseInt(n, 10)).includes(client.node); return value.map(n => parseInt(n, 10)).includes(client.node);
}, },
NP : function numberOfPosts() { NP : function numberOfPosts() {
const postCount = parseInt(user.properties.post_count, 10) || 0; const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0;
return !isNaN(value) && postCount >= value; return !isNaN(value) && postCount >= value;
}, },
NC : function numberOfCalls() { NC : function numberOfCalls() {
const loginCount = parseInt(user.properties.login_count, 10); const loginCount = user.getPropertyAsNumber(UserProps.LoginCount);
return !isNaN(value) && loginCount >= value; return !isNaN(value) && loginCount >= value;
}, },
AA : function accountAge() { AA : function accountAge() {
const accountCreated = moment(user.properties.account_created); const accountCreated = moment(user.getProperty(UserProps.AccountCreated));
const now = moment(); const now = moment();
const daysOld = accountCreated.diff(moment(), 'days'); const daysOld = accountCreated.diff(moment(), 'days');
return !isNaN(value) && return !isNaN(value) &&
@ -905,36 +907,36 @@ function peg$parse(input, options) {
daysOld >= value; daysOld >= value;
}, },
BU : function bytesUploaded() { BU : function bytesUploaded() {
const bytesUp = parseInt(user.properties.ul_total_bytes, 10) || 0; const bytesUp = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0;
return !isNaN(value) && bytesUp >= value; return !isNaN(value) && bytesUp >= value;
}, },
UP : function uploads() { UP : function uploads() {
const uls = parseInt(user.properties.ul_total_count, 10) || 0; const uls = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0;
return !isNaN(value) && uls >= value; return !isNaN(value) && uls >= value;
}, },
BD : function bytesDownloaded() { BD : function bytesDownloaded() {
const bytesDown = parseInt(user.properties.dl_total_bytes, 10) || 0; const bytesDown = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0;
return !isNaN(value) && bytesDown >= value; return !isNaN(value) && bytesDown >= value;
}, },
DL : function downloads() { DL : function downloads() {
const dls = parseInt(user.properties.dl_total_count, 10) || 0; const dls = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0;
return !isNaN(value) && dls >= value; return !isNaN(value) && dls >= value;
}, },
NR : function uploadDownloadRatioGreaterThan() { NR : function uploadDownloadRatioGreaterThan() {
const ulCount = parseInt(user.properties.ul_total_count, 10) || 0; const ulCount = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0;
const dlCount = parseInt(user.properties.dl_total_count, 10) || 0; const dlCount = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0;
const ratio = ~~((ulCount / dlCount) * 100); const ratio = ~~((ulCount / dlCount) * 100);
return !isNaN(value) && ratio >= value; return !isNaN(value) && ratio >= value;
}, },
KR : function uploadDownloadByteRatioGreaterThan() { KR : function uploadDownloadByteRatioGreaterThan() {
const ulBytes = parseInt(user.properties.ul_total_bytes, 10) || 0; const ulBytes = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0;
const dlBytes = parseInt(user.properties.dl_total_bytes, 10) || 0; const dlBytes = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0;
const ratio = ~~((ulBytes / dlBytes) * 100); const ratio = ~~((ulBytes / dlBytes) * 100);
return !isNaN(value) && ratio >= value; return !isNaN(value) && ratio >= value;
}, },
PC : function postCallRatio() { PC : function postCallRatio() {
const postCount = parseInt(user.properties.post_count, 10) || 0; const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0;
const loginCount = parseInt(user.properties.login_count, 10); const loginCount = user.getPropertyAsNumber(UserProps.LoginCount) || 0;
const ratio = ~~((postCount / loginCount) * 100); const ratio = ~~((postCount / loginCount) * 100);
return !isNaN(value) && ratio >= value; return !isNaN(value) && ratio >= value;
}, },

View file

@ -10,6 +10,7 @@ const conf = require('./config.js');
const logger = require('./logger.js'); const logger = require('./logger.js');
const database = require('./database.js'); const database = require('./database.js');
const resolvePath = require('./misc_util.js').resolvePath; const resolvePath = require('./misc_util.js').resolvePath;
const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
@ -229,18 +230,21 @@ function initialize(cb) {
}, },
function getOpProps(opUserName, next) { function getOpProps(opUserName, next) {
const propLoadOpts = { const propLoadOpts = {
names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ], names : [
UserProps.RealName, UserProps.Sex, UserProps.EmailAddress,
UserProps.Location, UserProps.Affiliations,
],
}; };
User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => { User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => {
return next(err, opUserName, opProps); return next(err, opUserName, opProps, propLoadOpts);
}); });
} }
], ],
(err, opUserName, opProps) => { (err, opUserName, opProps, propLoadOpts) => {
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
if(err) { if(err) {
[ 'username', 'real_name', 'sex', 'email_address', 'location', 'affiliation' ].forEach(v => { propLoadOpts.concat('username').forEach(v => {
StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A'); StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A');
}); });
} else { } else {

View file

@ -2,13 +2,14 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const logger = require('./logger.js'); const logger = require('./logger.js');
const Events = require('./events.js'); const Events = require('./events.js');
const UserProps = require('./user_property.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
const hashids = require('hashids'); const hashids = require('hashids');
exports.getActiveConnections = getActiveConnections; exports.getActiveConnections = getActiveConnections;
exports.getActiveConnectionList = getActiveConnectionList; exports.getActiveConnectionList = getActiveConnectionList;
@ -47,11 +48,11 @@ function getActiveConnectionList(authUsersOnly) {
// //
if(ac.user.isAuthenticated()) { if(ac.user.isAuthenticated()) {
entry.userName = ac.user.username; entry.userName = ac.user.username;
entry.realName = ac.user.properties.real_name; entry.realName = ac.user.properties[UserProps.RealName];
entry.location = ac.user.properties.location; entry.location = ac.user.properties[UserProps.Location];
entry.affils = entry.affiliation = ac.user.properties.affiliation; entry.affils = entry.affiliation = ac.user.properties[UserProps.Affiliations];
const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes'); const diff = now.diff(moment(ac.user.properties[UserProps.LastLoginTs]), 'minutes');
entry.timeOn = moment.duration(diff, 'minutes'); entry.timeOn = moment.duration(diff, 'minutes');
} }
return entry; return entry;
@ -62,7 +63,7 @@ function addNewClient(client, clientSock) {
const id = client.session.id = clientConnections.push(client) - 1; const id = client.session.id = clientConnections.push(client) - 1;
const remoteAddress = client.remoteAddress = clientSock.remoteAddress; const remoteAddress = client.remoteAddress = clientSock.remoteAddress;
// create a uniqe identifier one-time ID for this session // create a unique identifier one-time ID for this session
client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]); client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]);
// Create a client specific logger // Create a client specific logger

View file

@ -110,7 +110,8 @@ function renegadeToAnsi(s, client) {
result += s.substr(lastIndex, m.index - lastIndex) + attr; result += s.substr(lastIndex, m.index - lastIndex) + attr;
} else if(m[4] || m[1]) { } else if(m[4] || m[1]) {
// |AA MCI code or |Cx## movement where ## is in m[1] // |AA MCI code or |Cx## movement where ## is in m[1]
const val = getPredefinedMCIValue(client, m[4] || m[1], m[2]) || (m[0]); // value itself or literal let val = getPredefinedMCIValue(client, m[4] || m[1], m[2]);
val = _.isString(val) ? val : m[0]; // value itself or literal
result += s.substr(lastIndex, m.index - lastIndex) + val; result += s.substr(lastIndex, m.index - lastIndex) + val;
} else if(m[5]) { } else if(m[5]) {
// || -- literal '|', that is. // || -- literal '|', that is.

View file

@ -42,17 +42,53 @@ function hasMessageConferenceAndArea(config) {
return result; return result;
} }
const ArrayReplaceKeyPaths = [
'loginServers.ssh.algorithms.kex',
'loginServers.ssh.algorithms.cipher',
'loginServers.ssh.algorithms.hmac',
'loginServers.ssh.algorithms.compress',
];
const ArrayReplaceKeys = [
'args',
'sendArgs', 'recvArgs', 'recvArgsNonBatch',
];
function mergeValidateAndFinalize(config, cb) { function mergeValidateAndFinalize(config, cb) {
const defaultConfig = getDefaultConfig();
const arrayReplaceKeyPathsMutable = _.clone(ArrayReplaceKeyPaths);
const shouldReplaceArray = (arr, key) => {
if(ArrayReplaceKeys.includes(key)) {
return true;
}
for(let i = 0; i < arrayReplaceKeyPathsMutable.length; ++i) {
const o = _.get(defaultConfig, arrayReplaceKeyPathsMutable[i]);
if(_.isEqual(o, arr)) {
arrayReplaceKeyPathsMutable.splice(i, 1);
return true;
}
}
return false;
};
async.waterfall( async.waterfall(
[ [
function mergeWithDefaultConfig(callback) { function mergeWithDefaultConfig(callback) {
const mergedConfig = _.mergeWith( const mergedConfig = _.mergeWith(
getDefaultConfig(), defaultConfig,
config, (conf1, conf2) => { config,
// Arrays should always concat (defConfig, userConfig, key) => {
if(_.isArray(conf1)) { if(Array.isArray(defConfig) && Array.isArray(userConfig)) {
// :TODO: look for collisions & override dupes //
return conf1.concat(conf2); // Arrays are special: Some we merge, while others
// we simply replace.
//
if(shouldReplaceArray(defConfig, key)) {
return userConfig;
} else {
return _.uniq(defConfig.concat(userConfig));
}
} }
} }
); );
@ -136,7 +172,6 @@ function getDefaultConfig() {
// :TODO: closedSystem and loginAttemps prob belong under users{}? // :TODO: closedSystem and loginAttemps prob belong under users{}?
closedSystem : false, // is the system closed to new users? closedSystem : false, // is the system closed to new users?
loginAttempts : 3,
menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path
promptFile : 'prompt.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path promptFile : 'prompt.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path
@ -181,6 +216,13 @@ function getDefaultConfig() {
preAuthIdleLogoutSeconds : 60 * 3, // 3m preAuthIdleLogoutSeconds : 60 * 3, // 3m
idleLogoutSeconds : 60 * 6, // 6m idleLogoutSeconds : 60 * 6, // 6m
failedLogin : {
disconnect : 3, // 0=disabled
lockAccount : 9, // 0=disabled; Mark user status as "locked" if >= N
autoUnlockMinutes : 60 * 6, // 0=disabled; Auto unlock after N minutes.
},
unlockAtEmailPwReset : true, // if true, password reset via email will unlock locked accounts
}, },
theme : { theme : {

View file

@ -67,7 +67,13 @@ function loadDatabaseForMod(modInfo, cb) {
function getISOTimestampString(ts) { function getISOTimestampString(ts) {
ts = ts || moment(); ts = ts || moment();
return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); if(!moment.isMoment(ts)) {
if(_.isString(ts)) {
ts = ts.replace(/\//g, '-');
}
ts = moment(ts);
}
return ts.utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]');
} }
function sanatizeString(s) { function sanatizeString(s) {

View file

@ -2,6 +2,7 @@
'use strict'; 'use strict';
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const UserProps = require('./user_property.js');
// deps // deps
const { partition } = require('lodash'); const { partition } = require('lodash');
@ -11,8 +12,8 @@ module.exports = class DownloadQueue {
this.client = client; this.client = client;
if(!Array.isArray(this.client.user.downloadQueue)) { if(!Array.isArray(this.client.user.downloadQueue)) {
if(this.client.user.properties.dl_queue) { if(this.client.user.properties[UserProps.DownloadQueue]) {
this.loadFromProperty(this.client.user.properties.dl_queue); this.loadFromProperty(this.client.user.properties[UserProps.DownloadQueue]);
} else { } else {
this.client.user.downloadQueue = []; this.client.user.downloadQueue = [];
} }

View file

@ -4,6 +4,7 @@
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const UserProps = require('./user_property.js');
// deps // deps
const fs = require('graceful-fs'); const fs = require('graceful-fs');
@ -84,6 +85,8 @@ module.exports = class DropFile {
const prop = this.client.user.properties; const prop = this.client.user.properties;
const now = moment(); const now = moment();
const secLevel = this.client.user.getLegacySecurityLevel().toString(); const secLevel = this.client.user.getLegacySecurityLevel().toString();
const fullName = prop[UserProps.RealName] || this.client.user.username;
const bd = moment(prop[UserProp.Birthdate).format('MM/DD/YY');
// :TODO: fix time remaining // :TODO: fix time remaining
// :TODO: fix default protocol -- user prop: transfer_protocol // :TODO: fix default protocol -- user prop: transfer_protocol
@ -97,13 +100,13 @@ module.exports = class DropFile {
'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)"
'Y', // "Page Bell - Y=On N=Off (Default to Y)" 'Y', // "Page Bell - Y=On N=Off (Default to Y)"
'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)"
prop.real_name || this.client.user.username, // "User Full Name" fullName, // "User Full Name"
prop.location || 'Anywhere', // "Calling From" prop[UserProps.Location]|| 'Anywhere', // "Calling From"
'123-456-7890', // "Home Phone" '123-456-7890', // "Home Phone"
'123-456-7890', // "Work/Data Phone" '123-456-7890', // "Work/Data Phone"
'NOPE', // "Password" (Note: this is never given out or even stored plaintext) 'NOPE', // "Password" (Note: this is never given out or even stored plaintext)
secLevel, // "Security Level" secLevel, // "Security Level"
prop.login_count.toString(), // "Total Times On" prop[UserProps.LoginCount].toString(), // "Total Times On"
now.format('MM/DD/YY'), // "Last Date Called" now.format('MM/DD/YY'), // "Last Date Called"
'15360', // "Seconds Remaining THIS call (for those that particular)" '15360', // "Seconds Remaining THIS call (for those that particular)"
'256', // "Minutes Remaining THIS call" '256', // "Minutes Remaining THIS call"
@ -120,7 +123,7 @@ module.exports = class DropFile {
'0', // "Total Downloads" '0', // "Total Downloads"
'0', // "Daily Download "K" Total" '0', // "Daily Download "K" Total"
'999999', // "Daily Download Max. "K" Limit" '999999', // "Daily Download Max. "K" Limit"
moment(prop.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" bd, // "Caller's Birthdate"
'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)"
'X:\\GEN\\', // "Path to the GEN directory" 'X:\\GEN\\', // "Path to the GEN directory"
StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)" StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)"
@ -141,7 +144,7 @@ module.exports = class DropFile {
'0', // "Files d/led so far today" '0', // "Files d/led so far today"
'0', // "Total "K" Bytes Uploaded" '0', // "Total "K" Bytes Uploaded"
'0', // "Total "K" Bytes Downloaded" '0', // "Total "K" Bytes Downloaded"
prop.user_comment || 'None', // "User Comment" prop[UserProps.UserComment] || 'None', // "User Comment"
'0', // "Total Doors Opened" '0', // "Total Doors Opened"
'0', // "Total Messages Left" '0', // "Total Messages Left"
@ -168,7 +171,7 @@ module.exports = class DropFile {
'115200', '115200',
Config().general.boardName, Config().general.boardName,
this.client.user.userId.toString(), this.client.user.userId.toString(),
this.client.user.properties.real_name || this.client.user.username, this.client.user.properties[UserProps.RealName] || this.client.user.username,
this.client.user.username, this.client.user.username,
this.client.user.getLegacySecurityLevel().toString(), this.client.user.getLegacySecurityLevel().toString(),
'546', // :TODO: Minutes left! '546', // :TODO: Minutes left!
@ -189,21 +192,22 @@ module.exports = class DropFile {
const opUserName = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; const opUserName = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0];
const userName = /[^\s]*/.exec(this.client.user.username)[0]; const userName = /[^\s]*/.exec(this.client.user.username)[0];
const secLevel = this.client.user.getLegacySecurityLevel().toString(); const secLevel = this.client.user.getLegacySecurityLevel().toString();
const location = this.client.user.properties[UserProps.Location];
return iconv.encode( [ return iconv.encode( [
Config().general.boardName, // "The name of the system." Config().general.boardName, // "The name of the system."
opUserName, // "The sysop's name up to the first space." opUserName, // "The sysop's name up to the first space."
opUserName, // "The sysop's name following the first space." opUserName, // "The sysop's name following the first space."
'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console."
'57600', // "The current port (DTE) rate." '57600', // "The current port (DTE) rate."
'0', // "The number "0"" '0', // "The number "0""
userName, // "The current user's name, up to the first space." userName, // "The current user's name, up to the first space."
userName, // "The current user's name, following the first space." userName, // "The current user's name, following the first space."
this.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown." location || '', // "Where the user lives, or a blank line if unknown."
'1', // "The number "0" if TTY, or "1" if ANSI." '1', // "The number "0" if TTY, or "1" if ANSI."
secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops."
'546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software."
'-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." '-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines."
].join('\r\n') + '\r\n', 'cp437'); ].join('\r\n') + '\r\n', 'cp437');
} }

View file

@ -34,8 +34,9 @@ exports.Errors = {
ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode),
MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode),
UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode),
MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode), MissingParam : (reason, reasonCode) => new EnigError('Missing paramter(s)', -32008, reason, reasonCode),
MissingMci : (reason, reasonCode) => new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode), MissingMci : (reason, reasonCode) => new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode),
BadLogin : (reason, reasonCode) => new EnigError('Bad login attempt', -32010, reason, reasonCode),
}; };
exports.ErrorReasons = { exports.ErrorReasons = {
@ -44,4 +45,9 @@ exports.ErrorReasons = {
NoPreviousMenu : 'NOPREV', NoPreviousMenu : 'NOPREV',
NoConditionMatch : 'NOCONDMATCH', NoConditionMatch : 'NOCONDMATCH',
NotEnabled : 'NOTENABLED', NotEnabled : 'NOTENABLED',
}; AlreadyLoggedIn : 'ALREADYLOGGEDIN',
TooMany : 'TOOMANY',
Disabled : 'DISABLED',
Inactive : 'INACTIVE',
Locked : 'LOCKED',
};

View file

@ -5,6 +5,7 @@
const PluginModule = require('./plugin_module.js').PluginModule; const PluginModule = require('./plugin_module.js').PluginModule;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const { Errors } = require('./enig_error.js');
const _ = require('lodash'); const _ = require('lodash');
const later = require('later'); const later = require('later');
@ -116,7 +117,7 @@ class ScheduledEvent {
methodModule[this.action.what](this.action.args, err => { methodModule[this.action.what](this.action.args, err => {
if(err) { if(err) {
Log.debug( Log.debug(
{ error : err.toString(), eventName : this.name, action : this.action }, { error : err.message, eventName : this.name, action : this.action },
'Error performing scheduled event action'); 'Error performing scheduled event action');
} }
@ -124,7 +125,7 @@ class ScheduledEvent {
}); });
} catch(e) { } catch(e) {
Log.warn( Log.warn(
{ error : e.toString(), eventName : this.name, action : this.action }, { error : e.message, eventName : this.name, action : this.action },
'Failed to perform scheduled event action'); 'Failed to perform scheduled event action');
return cb(e); return cb(e);
@ -138,7 +139,22 @@ class ScheduledEvent {
env : process.env, env : process.env,
}; };
const proc = pty.spawn(this.action.what, this.action.args, opts); let proc;
try {
proc = pty.spawn(this.action.what, this.action.args, opts);
} catch(e) {
Log.warn(
{
error : 'Failed to spawn @execute process',
reason : e.message,
eventName : this.name,
action : this.action,
what : this.action.what,
args : this.action.args
}
);
return cb(e);
}
proc.once('exit', exitCode => { proc.once('exit', exitCode => {
if(exitCode) { if(exitCode) {

View file

@ -7,6 +7,7 @@ const ViewController = require('./view_controller.js').ViewContro
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
const FileBaseFilters = require('./file_base_filter.js'); const FileBaseFilters = require('./file_base_filter.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
@ -111,7 +112,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
// //
// If the item was also the active filter, we need to make a new one active // If the item was also the active filter, we need to make a new one active
// //
if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) { if(filterUuid === this.client.user.properties[UserProps.FileBaseFilterActiveUuid]) {
const newActive = this.filtersArray[this.currentFilterIndex]; const newActive = this.filtersArray[this.currentFilterIndex];
if(newActive) { if(newActive) {
filters.setActive(newActive.uuid); filters.setActive(newActive.uuid);

View file

@ -14,6 +14,7 @@ const resolveMimeType = require('./mime_util.js').resolveMimeType;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const wordWrapText = require('./word_wrap.js').wordWrapText; const wordWrapText = require('./word_wrap.js').wordWrapText;
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const UserProps = require('./user_property.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
@ -136,11 +137,11 @@ function changeFileAreaWithOptions(client, areaTag, options, cb) {
}, },
function changeArea(area, callback) { function changeArea(area, callback) {
if(true === options.persist) { if(true === options.persist) {
client.user.persistProperty('file_area_tag', areaTag, err => { client.user.persistProperty(UserProps.FileAreaTag, areaTag, err => {
return callback(err, area); return callback(err, area);
}); });
} else { } else {
client.user.properties['file_area_tag'] = areaTag; client.user.properties[UserProps.FileAreaTag] = areaTag;
return callback(null, area); return callback(null, area);
} }
} }
@ -705,7 +706,7 @@ function scanFile(filePath, options, iterator, cb) {
// up to many seconds in time for larger files. // up to many seconds in time for larger files.
// //
const chunkSize = 1024 * 64; const chunkSize = 1024 * 64;
const buffer = new Buffer(chunkSize); const buffer = Buffer.allocUnsafe(chunkSize);
fs.open(filePath, 'r', (err, fd) => { fs.open(filePath, 'r', (err, fd) => {
if(err) { if(err) {

View file

@ -150,11 +150,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}'; queueView.setItems(this.dlQueue.items);
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
queueView.on('index update', idx => { queueView.on('index update', idx => {
const fileEntry = this.dlQueue.items[idx]; const fileEntry = this.dlQueue.items[idx];

View file

@ -1,9 +1,11 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const UserProps = require('./user_property.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const uuidV4 = require('uuid/v4'); const uuidV4 = require('uuid/v4');
module.exports = class FileBaseFilters { module.exports = class FileBaseFilters {
constructor(client) { constructor(client) {
@ -64,7 +66,7 @@ module.exports = class FileBaseFilters {
} }
load() { load() {
let filtersProperty = this.client.user.properties.file_base_filters; let filtersProperty = this.client.user.properties[UserProps.FileBaseFilters];
let defaulted; let defaulted;
if(!filtersProperty) { if(!filtersProperty) {
filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters()); filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters());
@ -90,7 +92,7 @@ module.exports = class FileBaseFilters {
} }
persist(cb) { persist(cb) {
return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb); return this.client.user.persistProperty(UserProps.FileBaseFilters, JSON.stringify(this.filters), cb);
} }
cleanTags(tags) { cleanTags(tags) {
@ -102,7 +104,7 @@ module.exports = class FileBaseFilters {
if(activeFilter) { if(activeFilter) {
this.activeFilter = activeFilter; this.activeFilter = activeFilter;
this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid); this.client.user.persistProperty(UserProps.FileBaseFilterActiveUuid, filterUuid);
return true; return true;
} }
@ -129,11 +131,11 @@ module.exports = class FileBaseFilters {
} }
static getActiveFilter(client) { static getActiveFilter(client) {
return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid); return new FileBaseFilters(client).get(client.user.properties[UserProps.FileBaseFilterActiveUuid]);
} }
static getFileBaseLastViewedFileIdByUser(user) { static getFileBaseLastViewedFileIdByUser(user) {
return parseInt((user.properties.user_file_base_last_viewed || 0)); return parseInt((user.properties[UserProps.FileBaseLastViewedId] || 0));
} }
static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) { static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) {
@ -150,6 +152,6 @@ module.exports = class FileBaseFilters {
return; return;
} }
return user.persistProperty('user_file_base_last_viewed', fileId, cb); return user.persistProperty(UserProps.FileBaseLastViewedId, fileId, cb);
} }
}; };

View file

@ -121,11 +121,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}'; queueView.setItems(this.dlQueue.items);
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
queueView.on('index update', idx => { queueView.on('index update', idx => {
const fileEntry = this.dlQueue.items[idx]; const fileEntry = this.dlQueue.items[idx];

View file

@ -24,6 +24,7 @@ const {
const Config = require('./config.js').get; const Config = require('./config.js').get;
const { getAddressedToInfo } = require('./mail_util.js'); const { getAddressedToInfo } = require('./mail_util.js');
const Events = require('./events.js'); const Events = require('./events.js');
const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
@ -479,7 +480,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
Events.emit(Events.getSystemEvents().UserPostMessage, { user : this.client.user, areaTag : this.message.areaTag }); Events.emit(Events.getSystemEvents().UserPostMessage, { user : this.client.user, areaTag : this.message.areaTag });
return StatLog.incrementUserStat(this.client.user, 'post_count', 1, cb); return StatLog.incrementUserStat(this.client.user, UserProps.MessagePostCount, 1, cb);
} }
redrawFooter(options, cb) { redrawFooter(options, cb) {
@ -542,7 +543,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
theme.displayThemedAsset( theme.displayThemedAsset(
art[n], art[n],
self.client, self.client,
{ font : self.menuConfig.font, acsCondMember : 'art' }, { font : self.menuConfig.font },
function displayed(err) { function displayed(err) {
next(err); next(err);
} }
@ -622,7 +623,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
theme.displayThemedAsset( theme.displayThemedAsset(
art[n], art[n],
self.client, self.client,
{ font : self.menuConfig.font, acsCondMember : 'art' }, { font : self.menuConfig.font },
function displayed(err, artData) { function displayed(err, artData) {
if(artData) { if(artData) {
mciData[n] = artData; mciData[n] = artData;
@ -738,7 +739,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
const fromView = self.viewControllers.header.getView(MciViewIds.header.from); const fromView = self.viewControllers.header.getView(MciViewIds.header.from);
const area = getMessageAreaByTag(self.messageAreaTag); const area = getMessageAreaByTag(self.messageAreaTag);
if(area && area.realNames) { if(area && area.realNames) {
fromView.setText(self.client.user.properties.real_name || self.client.user.username); fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username);
} else { } else {
fromView.setText(self.client.user.username); fromView.setText(self.client.user.username);
} }

View file

@ -7,6 +7,7 @@ const StatLog = require('./stat_log.js');
const User = require('./user.js'); const User = require('./user.js');
const sysDb = require('./database.js').dbs.system; const sysDb = require('./database.js').dbs.system;
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const UserProps = require('./user_property.js');
// deps // deps
const moment = require('moment'); const moment = require('moment');
@ -165,7 +166,7 @@ exports.getModule = class LastCallersModule extends MenuModule {
loadUserForHistoryItems(loginHistory, cb) { loadUserForHistoryItems(loginHistory, cb) {
const getPropOpts = { const getPropOpts = {
names : [ 'real_name', 'location', 'affiliation' ] names : [ UserProps.RealName, UserProps.Location, UserProps.Affiliations ]
}; };
const actionIndicatorNames = _.map(this.actionIndicators, (v, k) => k); const actionIndicatorNames = _.map(this.actionIndicators, (v, k) => k);
@ -185,9 +186,9 @@ exports.getModule = class LastCallersModule extends MenuModule {
item.userName = item.text = userName; item.userName = item.text = userName;
User.loadProperties(item.userId, getPropOpts, (err, props) => { User.loadProperties(item.userId, getPropOpts, (err, props) => {
item.location = (props && props.location) || ''; item.location = (props && props[UserProps.Location]) || '';
item.affiliation = item.affils = (props && props.affiliation) || ''; item.affiliation = item.affils = (props && props[UserProps.Affiliations]) || '';
item.realName = (props && props.real_name) || ''; item.realName = (props && props[UserProps.RealName]) || '';
if(!indicatorSumsSql) { if(!indicatorSumsSql) {
return next(null, item); return next(null, item);

View file

@ -6,6 +6,7 @@ const conf = require('./config.js');
const logger = require('./logger.js'); const logger = require('./logger.js');
const ServerModule = require('./server_module.js').ServerModule; const ServerModule = require('./server_module.js').ServerModule;
const clientConns = require('./client_connections.js'); const clientConns = require('./client_connections.js');
const UserProps = require('./user_property.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
@ -25,12 +26,12 @@ module.exports = class LoginServerModule extends ServerModule {
// //
const preLoginTheme = _.get(conf.config, 'theme.preLogin'); const preLoginTheme = _.get(conf.config, 'theme.preLogin');
if('*' === preLoginTheme) { if('*' === preLoginTheme) {
client.user.properties.theme_id = theme.getRandomTheme() || ''; client.user.properties[UserProps.ThemeId] = theme.getRandomTheme() || '';
} else { } else {
client.user.properties.theme_id = preLoginTheme; client.user.properties[UserProps.ThemeId] = preLoginTheme;
} }
theme.setClientTheme(client, client.user.properties.theme_id); theme.setClientTheme(client, client.user.properties[UserProps.ThemeId]);
return cb(null); // note: currently useless to use cb here - but this may change...again... return cb(null); // note: currently useless to use cb here - but this may change...again...
} }

View file

@ -47,6 +47,11 @@ exports.MenuModule = class MenuModule extends PluginModule {
const mciData = {}; const mciData = {};
let pausePosition; let pausePosition;
const hasArt = () => {
return _.isString(self.menuConfig.art) ||
(Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs'));
};
async.series( async.series(
[ [
function beforeArtInterrupt(callback) { function beforeArtInterrupt(callback) {
@ -56,7 +61,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
return self.beforeArt(callback); return self.beforeArt(callback);
}, },
function displayMenuArt(callback) { function displayMenuArt(callback) {
if(!_.isString(self.menuConfig.art)) { if(!hasArt()) {
return callback(null); return callback(null);
} }

View file

@ -8,6 +8,7 @@ const Message = require('./message.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const msgNetRecord = require('./msg_network.js').recordMessage; const msgNetRecord = require('./msg_network.js').recordMessage;
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
@ -222,8 +223,8 @@ function changeMessageConference(client, confTag, cb) {
}, },
function changeConferenceAndArea(conf, areaInfo, callback) { function changeConferenceAndArea(conf, areaInfo, callback) {
const newProps = { const newProps = {
message_conf_tag : confTag, [ UserProps.MessageConfTag ] : confTag,
message_area_tag : areaInfo.areaTag, [ UserProps.MessageAreaTag ] : areaInfo.areaTag,
}; };
client.user.persistProperties(newProps, err => { client.user.persistProperties(newProps, err => {
callback(err, conf, areaInfo); callback(err, conf, areaInfo);
@ -262,11 +263,11 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) {
}, },
function changeArea(area, callback) { function changeArea(area, callback) {
if(true === options.persist) { if(true === options.persist) {
client.user.persistProperty('message_area_tag', areaTag, function persisted(err) { client.user.persistProperty(UserProps.MessageAreaTag, areaTag, function persisted(err) {
return callback(err, area); return callback(err, area);
}); });
} else { } else {
client.user.properties['message_area_tag'] = areaTag; client.user.properties[UserProps.MessageAreaTag] = areaTag;
return callback(null, area); return callback(null, area);
} }
} }
@ -303,8 +304,8 @@ function tempChangeMessageConfAndArea(client, areaTag) {
return false; return false;
} }
client.user.properties.message_conf_tag = confTag; client.user.properties[UserProps.MessageConfTag] = confTag;
client.user.properties.message_area_tag = areaTag; client.user.properties[UserProps.MessageAreaTag] = areaTag;
return true; return true;
} }
@ -353,13 +354,19 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) {
}); });
} }
function getMessageListForArea(client, areaTag, cb) { function getMessageListForArea(client, areaTag, filter, cb)
const filter = { {
areaTag, if(!cb && _.isFunction(filter)) {
resultType : 'messageList', cb = filter;
sort : 'messageId', filter = {
order : 'ascending', areaTag,
}; resultType : 'messageList',
sort : 'messageId',
order : 'ascending'
};
} else {
Object.assign(filter, { areaTag } );
}
if(Message.isPrivateAreaTag(areaTag)) { if(Message.isPrivateAreaTag(areaTag)) {
filter.privateTagUserId = client.user.userId; filter.privateTagUserId = client.user.userId;

View file

@ -4,6 +4,8 @@
const paths = require('path'); const paths = require('path');
const os = require('os'); const os = require('os');
const moment = require('moment');
const packageJson = require('../package.json'); const packageJson = require('../package.json');
exports.isProduction = isProduction; exports.isProduction = isProduction;
@ -57,4 +59,4 @@ function valueAsArray(value) {
return []; return [];
} }
return Array.isArray(value) ? value : [ value ]; return Array.isArray(value) ? value : [ value ];
} }

View file

@ -2,8 +2,10 @@
'use strict'; 'use strict';
const messageArea = require('../core/message_area.js'); const messageArea = require('../core/message_area.js');
const { get } = require('lodash'); const UserProps = require('./user_property.js');
// deps
const { get } = require('lodash');
exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { exports.MessageAreaConfTempSwitcher = Sup => class extends Sup {
@ -15,8 +17,8 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup {
if(recordPrevious) { if(recordPrevious) {
this.prevMessageConfAndArea = { this.prevMessageConfAndArea = {
confTag : this.client.user.properties.message_conf_tag, confTag : this.client.user.properties[UserProps.MessageConfTag],
areaTag : this.client.user.properties.message_area_tag, areaTag : this.client.user.properties[UserProps.MessageAreaTag],
}; };
} }
@ -27,8 +29,8 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup {
tempMessageConfAndAreaRestore() { tempMessageConfAndAreaRestore() {
if(this.prevMessageConfAndArea) { if(this.prevMessageConfAndArea) {
this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag; this.client.user.properties[UserProps.MessageConfTag] = this.prevMessageConfAndArea.confTag;
this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag; this.client.user.properties[UserProps.MessageAreaTag] = this.prevMessageConfAndArea.areaTag;
} }
} }
}; };

View file

@ -5,6 +5,7 @@
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
@ -110,7 +111,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
initList() { initList() {
let index = 1; let index = 1;
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag( this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag(
this.client.user.properties.message_conf_tag, this.client.user.properties[UserProps.MessageConfTag],
{ client : this.client } { client : this.client }
).map(area => { ).map(area => {
return { return {

View file

@ -3,6 +3,7 @@
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
const persistMessage = require('./message_area.js').persistMessage; const persistMessage = require('./message_area.js').persistMessage;
const UserProps = require('./user_property.js');
const _ = require('lodash'); const _ = require('lodash');
const async = require('async'); const async = require('async');
@ -58,8 +59,10 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
} }
enter() { enter() {
if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) { if(_.isString(this.client.user.properties[UserProps.MessageAreaTag]) &&
this.messageAreaTag = this.client.user.properties.message_area_tag; !_.isString(this.messageAreaTag))
{
this.messageAreaTag = this.client.user.properties[UserProps.MessageAreaTag];
} }
super.enter(); super.enter();

View file

@ -8,6 +8,7 @@ const messageArea = require('./message_area.js');
const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const Message = require('./message.js'); const Message = require('./message.js');
const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
@ -167,7 +168,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
if(this.config.messageAreaTag) { if(this.config.messageAreaTag) {
this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag); this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag);
} else { } else {
this.config.messageAreaTag = this.client.user.properties.message_area_tag; this.config.messageAreaTag = this.client.user.properties[UserProps.MessageAreaTag];
} }
} }
} }

View file

@ -8,9 +8,14 @@ const theme = require('./theme.js');
const login = require('./system_menu_method.js').login; const login = require('./system_menu_method.js').login;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const {
getISOTimestampString
} = require('./database.js');
const UserProps = require('./user_property.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
name : 'NUA', name : 'NUA',
@ -80,20 +85,20 @@ exports.getModule = class NewUserAppModule extends MenuModule {
areaTag = areaTag || ''; areaTag = areaTag || '';
newUser.properties = { newUser.properties = {
real_name : formData.value.realName, [ UserProps.RealName ] : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format [ UserProps.Birthdate ] : getISOTimestampString(formData.value.birthdate),
sex : formData.value.sex, [ UserProps.Sex ] : formData.value.sex,
location : formData.value.location, [ UserProps.Location ] : formData.value.location,
affiliation : formData.value.affils, [ UserProps.Affiliations ] : formData.value.affils,
email_address : formData.value.email, [ UserProps.EmailAddress ] : formData.value.email,
web_address : formData.value.web, [ UserProps.WebAddress ] : formData.value.web,
account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format [ UserProps.AccountCreated ] : getISOTimestampString(),
message_conf_tag : confTag, [ UserProps.MessageConfTag ] : confTag,
message_area_tag : areaTag, [ UserProps.MessageAreaTag ] : areaTag,
term_height : self.client.term.termHeight, [ UserProps.TermHeight ] : self.client.term.termHeight,
term_width : self.client.term.termWidth, [ UserProps.TermWidth ] : self.client.term.termWidth,
// :TODO: Other defaults // :TODO: Other defaults
// :TODO: should probably have a place to create defaults/etc. // :TODO: should probably have a place to create defaults/etc.
@ -101,9 +106,9 @@ exports.getModule = class NewUserAppModule extends MenuModule {
const defaultTheme = _.get(config, 'theme.default'); const defaultTheme = _.get(config, 'theme.default');
if('*' === defaultTheme) { if('*' === defaultTheme) {
newUser.properties.theme_id = theme.getRandomTheme(); newUser.properties[UserProps.ThemeId] = theme.getRandomTheme();
} else { } else {
newUser.properties.theme_id = defaultTheme; newUser.properties[UserProps.ThemeId] = defaultTheme;
} }
// :TODO: User.create() should validate email uniqueness! // :TODO: User.create() should validate email uniqueness!
@ -133,7 +138,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
}; };
} }
if(User.AccountStatus.inactive === self.client.user.properties.account_status) { if(User.AccountStatus.inactive === self.client.user.properties[UserProps.AccountStatus]) {
return self.gotoMenu(extraArgs.inactive, cb); return self.gotoMenu(extraArgs.inactive, cb);
} else { } else {
// //

View file

@ -38,7 +38,7 @@ function getAnswers(questions, cb) {
const ConfigIncludeKeys = [ const ConfigIncludeKeys = [
'theme', 'theme',
'users.preAuthIdleLogoutSeconds', 'users.idleLogoutSeconds', 'users.preAuthIdleLogoutSeconds', 'users.idleLogoutSeconds',
'users.newUserNames', 'users.newUserNames', 'users.failedLogin', 'users.unlockAtEmailPwReset',
'paths.logs', 'paths.logs',
'loginServers', 'loginServers',
'contentServers', 'contentServers',

View file

@ -26,10 +26,11 @@ commands:
actions: actions:
pw USERNAME PASSWORD set password to PASSWORD for USERNAME pw USERNAME PASSWORD set password to PASSWORD for USERNAME
rm USERNAME permanantely removes USERNAME user from system rm USERNAME permanently removes USERNAME user from system
activate USERNAME sets USERNAME's status to active activate USERNAME sets USERNAME's status to active
deactivate USERNAME sets USERNAME's status to deactive deactivate USERNAME sets USERNAME's status to inactive
disable USERNAME sets USERNAME's status to disabled disable USERNAME sets USERNAME's status to disabled
lock USERNAME sets USERNAME's status to locked
group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP
`, `,
@ -57,7 +58,7 @@ cat args:
actions: actions:
scan AREA_TAG[@STORAGE_TAG] scan specified area scan AREA_TAG[@STORAGE_TAG] scan specified area
may also contain optional GLOB as last parameter, may also contain optional GLOB as last parameter,
for examle: scan some_area *.zip for example: scan some_area *.zip
info CRITERIA display information about areas and/or files info CRITERIA display information about areas and/or files
where CRITERIA is one of the following: where CRITERIA is one of the following:

View file

@ -17,7 +17,7 @@ module.exports = function() {
process.exitCode = ExitCodes.SUCCESS; process.exitCode = ExitCodes.SUCCESS;
if(true === argv.version) { if(true === argv.version) {
return console.info(require('../package.json').version); return console.info(require('../../package.json').version);
} }
if(0 === argv._.length || if(0 === argv._.length ||

View file

@ -8,25 +8,13 @@ const argv = require('./oputil_common.js').argv;
const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases;
const getHelpFor = require('./oputil_help.js').getHelpFor; const getHelpFor = require('./oputil_help.js').getHelpFor;
const Errors = require('../enig_error.js').Errors; const Errors = require('../enig_error.js').Errors;
const UserProps = require('../user_property.js');
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.handleUserCommand = handleUserCommand; exports.handleUserCommand = handleUserCommand;
function getUser(userName, cb) {
const User = require('../../core/user.js');
User.getUserIdAndName(userName, (err, userId) => {
if(err) {
process.exitCode = ExitCodes.BAD_ARGS;
return cb(err);
}
const u = new User();
u.userId = userId;
return cb(null, u);
});
}
function initAndGetUser(userName, cb) { function initAndGetUser(userName, cb) {
async.waterfall( async.waterfall(
[ [
@ -34,12 +22,12 @@ function initAndGetUser(userName, cb) {
initConfigAndDatabases(callback); initConfigAndDatabases(callback);
}, },
function getUserObject(callback) { function getUserObject(callback) {
getUser(userName, (err, user) => { const User = require('../../core/user.js');
User.getUserIdAndName(userName, (err, userId) => {
if(err) { if(err) {
process.exitCode = ExitCodes.BAD_ARGS;
return callback(err); return callback(err);
} }
return callback(null, user); return User.getUser(userId, callback);
}); });
} }
], ],
@ -55,15 +43,38 @@ function setAccountStatus(user, status) {
} }
const AccountStatus = require('../../core/user.js').AccountStatus; const AccountStatus = require('../../core/user.js').AccountStatus;
status = {
activate : AccountStatus.active,
deactivate : AccountStatus.inactive,
disable : AccountStatus.disabled,
lock : AccountStatus.locked,
}[status];
const statusDesc = _.invert(AccountStatus)[status]; const statusDesc = _.invert(AccountStatus)[status];
user.persistProperty('account_status', status, err => {
if(err) { async.series(
process.exitCode = ExitCodes.ERROR; [
console.error(err.message); (callback) => {
} else { return user.persistProperty(UserProps.AccountStatus, status, callback);
console.info(`User status set to ${statusDesc}`); },
(callback) => {
if(AccountStatus.active !== status) {
return callback(null);
}
return user.unlockAccount(callback);
}
],
err => {
if(err) {
process.exitCode = ExitCodes.ERROR;
console.error(err.message);
} else {
console.info(`User status set to ${statusDesc}`);
}
} }
}); );
} }
function setUserPassword(user) { function setUserPassword(user) {
@ -147,21 +158,6 @@ function modUserGroups(user) {
} }
} }
function activateUser(user) {
const AccountStatus = require('../../core/user.js').AccountStatus;
return setAccountStatus(user, AccountStatus.active);
}
function deactivateUser(user) {
const AccountStatus = require('../../core/user.js').AccountStatus;
return setAccountStatus(user, AccountStatus.inactive);
}
function disableUser(user) {
const AccountStatus = require('../../core/user.js').AccountStatus;
return setAccountStatus(user, AccountStatus.disabled);
}
function handleUserCommand() { function handleUserCommand() {
function errUsage() { function errUsage() {
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
@ -195,11 +191,12 @@ function handleUserCommand() {
del : removeUser, del : removeUser,
delete : removeUser, delete : removeUser,
activate : activateUser, activate : setAccountStatus,
deactivate : deactivateUser, deactivate : setAccountStatus,
disable : disableUser, disable : setAccountStatus,
lock : setAccountStatus,
group : modUserGroups, group : modUserGroups,
}[action] || errUsage)(user); }[action] || errUsage)(user, action);
}); });
} }

View file

@ -2,17 +2,18 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const { const {
getMessageAreaByTag, getMessageAreaByTag,
getMessageConferenceByTag getMessageConferenceByTag
} = require('./message_area.js'); } = require('./message_area.js');
const clientConnections = require('./client_connections.js'); const clientConnections = require('./client_connections.js');
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const FileBaseFilters = require('./file_base_filter.js'); const FileBaseFilters = require('./file_base_filter.js');
const { formatByteSize } = require('./string_util.js'); const { formatByteSize } = require('./string_util.js');
const ANSI = require('./ansi_term.js'); const ANSI = require('./ansi_term.js');
const UserProps = require('./user_property.js');
// deps // deps
const packageJson = require('../package.json'); const packageJson = require('../package.json');
@ -80,62 +81,66 @@ const PREDEFINED_MCI_GENERATORS = {
UN : function userName(client) { return client.user.username; }, UN : function userName(client) { return client.user.username; },
UI : function userId(client) { return client.user.userId.toString(); }, UI : function userId(client) { return client.user.userId.toString(); },
UG : function groups(client) { return _.values(client.user.groups).join(', '); }, UG : function groups(client) { return _.values(client.user.groups).join(', '); },
UR : function realName(client) { return userStatAsString(client, 'real_name', ''); }, UR : function realName(client) { return userStatAsString(client, UserProps.RealName, ''); },
LO : function location(client) { return userStatAsString(client, 'location', ''); }, LO : function location(client) { return userStatAsString(client, UserProps.Location, ''); },
UA : function age(client) { return client.user.getAge().toString(); }, UA : function age(client) { return client.user.getAge().toString(); },
BD : function birthdate(client) { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY BD : function birthdate(client) { // iNiQUiTY
US : function sex(client) { return userStatAsString(client, 'sex', ''); }, return moment(client.user.properties[UserProps.Birthdate]).format(client.currentTheme.helpers.getDateFormat());
UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); }, },
UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); }, US : function sex(client) { return userStatAsString(client, UserProps.Sex, ''); },
UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); }, UE : function emailAddres(client) { return userStatAsString(client, UserProps.EmailAddress, ''); },
UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); }, UW : function webAddress(client) { return userStatAsString(client, UserProps.WebAddress, ''); },
UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); }, UF : function affils(client) { return userStatAsString(client, UserProps.Affiliations, ''); },
UT : function themeId(client) { return userStatAsString(client, UserProps.ThemeId, ''); },
UC : function loginCount(client) { return userStatAsString(client, UserProps.LoginCount, 0); },
ND : function connectedNode(client) { return client.node.toString(); }, ND : function connectedNode(client) { return client.node.toString(); },
IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version
ST : function serverName(client) { return client.session.serverName; }, ST : function serverName(client) { return client.session.serverName; },
FN : function activeFileBaseFilterName(client) { FN : function activeFileBaseFilterName(client) {
const activeFilter = FileBaseFilters.getActiveFilter(client); const activeFilter = FileBaseFilters.getActiveFilter(client);
return activeFilter ? activeFilter.name : ''; return activeFilter ? activeFilter.name : '(Unknown)';
}, },
DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 DN : function userNumDownloads(client) { return userStatAsString(client, UserProps.FileDlTotalCount, 0); }, // Obv/2
DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes
const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes'); const byteSize = StatLog.getUserStatNum(client.user, UserProps.FileDlTotalBytes);
return formatByteSize(byteSize, true); // true=withAbbr return formatByteSize(byteSize, true); // true=withAbbr
}, },
UP : function userNumUploads(client) { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2 UP : function userNumUploads(client) { return userStatAsString(client, UserProps.FileUlTotalCount, 0); }, // Obv/2
UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes
const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes'); const byteSize = StatLog.getUserStatNum(client.user, UserProps.FileUlTotalBytes);
return formatByteSize(byteSize, true); // true=withAbbr return formatByteSize(byteSize, true); // true=withAbbr
}, },
NR : function userUpDownRatio(client) { // Obv/2 NR : function userUpDownRatio(client) { // Obv/2
return getUserRatio(client, 'ul_total_count', 'dl_total_count'); return getUserRatio(client, UserProps.FileUlTotalCount, UserProps.FileDlTotalCount);
}, },
KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio
return getUserRatio(client, 'ul_total_bytes', 'dl_total_bytes'); return getUserRatio(client, UserProps.FileUlTotalBytes, UserProps.FileDlTotalBytes);
}, },
MS : function accountCreatedclient(client) { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, MS : function accountCreatedclient(client) {
PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); }, return moment(client.user.properties[UserProps.AccountCreated]).format(client.currentTheme.helpers.getDateFormat());
PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); }, },
PS : function userPostCount(client) { return userStatAsString(client, UserProps.MessagePostCount, 0); },
PC : function userPostCallRatio(client) { return getUserRatio(client, UserProps.MessagePostCount, UserProps.LoginCount); },
MD : function currentMenuDescription(client) { MD : function currentMenuDescription(client) {
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
}, },
MA : function messageAreaName(client) { MA : function messageAreaName(client) {
const area = getMessageAreaByTag(client.user.properties.message_area_tag); const area = getMessageAreaByTag(client.user.properties[UserProps.MessageAreaTag]);
return area ? area.name : ''; return area ? area.name : '';
}, },
MC : function messageConfName(client) { MC : function messageConfName(client) {
const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); const conf = getMessageConferenceByTag(client.user.properties[UserProps.MessageConfTag]);
return conf ? conf.name : ''; return conf ? conf.name : '';
}, },
ML : function messageAreaDescription(client) { ML : function messageAreaDescription(client) {
const area = getMessageAreaByTag(client.user.properties.message_area_tag); const area = getMessageAreaByTag(client.user.properties[UserProps.MessageAreaTag]);
return area ? area.desc : ''; return area ? area.desc : '';
}, },
CM : function messageConfDescription(client) { CM : function messageConfDescription(client) {
const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); const conf = getMessageConferenceByTag(client.user.properties[UserProps.MessageConfTag]);
return conf ? conf.desc : ''; return conf ? conf.desc : '';
}, },
@ -169,8 +174,9 @@ const PREDEFINED_MCI_GENERATORS = {
// Clean up CPU strings a bit for better display // Clean up CPU strings a bit for better display
// //
return os.cpus()[0].model return os.cpus()[0].model
.replace(/\(R\)|\(TM\)|processor|CPU/g, '') .replace(/\(R\)|\(TM\)|processor|CPU/ig, '')
.replace(/\s+(?= )/g, ''); .replace(/\s+(?= )/g, '')
.trim();
}, },
// :TODO: MCI for core count, e.g. os.cpus().length // :TODO: MCI for core count, e.g. os.cpus().length

View file

@ -1746,7 +1746,7 @@ function FTNMessageScanTossModule() {
} }
return callback(null, localInfo); // continue even if we couldn't find an old match return callback(null, localInfo); // continue even if we couldn't find an old match
}); });
} else if(fileIds.legnth > 1) { } else if(fileIds.length > 1) {
return callback(Errors.General(`More than one existing entry for TIC in ${localInfo.areaTag} ([${fileIds.join(', ')}])`)); return callback(Errors.General(`More than one existing entry for TIC in ${localInfo.areaTag} ([${fileIds.join(', ')}])`));
} else { } else {
return callback(null, localInfo); return callback(null, localInfo);

View file

@ -17,6 +17,7 @@ const {
} = require('../../message_area.js'); } = require('../../message_area.js');
const { sortAreasOrConfs } = require('../../conf_area_util.js'); const { sortAreasOrConfs } = require('../../conf_area_util.js');
const AnsiPrep = require('../../ansi_prep.js'); const AnsiPrep = require('../../ansi_prep.js');
const { wordWrapText } = require('../../word_wrap.js');
// deps // deps
const net = require('net'); const net = require('net');
@ -27,9 +28,10 @@ const moment = require('moment');
const ModuleInfo = exports.moduleInfo = { const ModuleInfo = exports.moduleInfo = {
name : 'Gopher', name : 'Gopher',
desc : 'Gopher Server', desc : 'A RFC-1436-ish Gopher Server',
author : 'NuSkooler', author : 'NuSkooler',
packageName : 'codes.l33t.enigma.gopher.server', packageName : 'codes.l33t.enigma.gopher.server',
notes : 'https://tools.ietf.org/html/rfc1436',
}; };
const Message = require('../../message.js'); const Message = require('../../message.js');
@ -158,7 +160,7 @@ exports.getModule = class GopherModule extends ServerModule {
defaultGenerator(selectorMatch, cb) { defaultGenerator(selectorMatch, cb) {
this.log.debug( { selector : selectorMatch[0] }, 'Serving default content'); this.log.debug( { selector : selectorMatch[0] }, 'Serving default content');
let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'startup_banner.asc'); let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'gopher_banner.asc');
bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile); bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile);
fs.readFile(bannerFile, 'utf8', (err, banner) => { fs.readFile(bannerFile, 'utf8', (err, banner) => {
if(err) { if(err) {
@ -182,21 +184,43 @@ exports.getModule = class GopherModule extends ServerModule {
} }
prepareMessageBody(body, cb) { prepareMessageBody(body, cb) {
//
// From RFC-1436:
// "User display strings are intended to be displayed on a line on a
// typical screen for a user's viewing pleasure. While many screens can
// accommodate 80 character lines, some space is needed to display a tag
// of some sort to tell the user what sort of item this is. Because of
// this, the user display string should be kept under 70 characters in
// length. Clients may truncate to a length convenient to them."
//
// Messages on BBSes however, have generally been <= 79 characters. If we
// start wrapping earlier, things will generally be OK except:
// * When we're doing with FTN-style quoted lines
// * When dealing with ANSI/ASCII art
//
// Anyway, the spec says "should" and not MUST or even SHOULD! ...so, to
// to follow the KISS principle: Wrap at 79.
//
const WordWrapColumn = 79;
if(isAnsi(body)) { if(isAnsi(body)) {
AnsiPrep( AnsiPrep(
body, body,
{ {
cols : 79, // Gopher std. wants 70, but we'll have to deal with it. cols : WordWrapColumn, // See notes above
forceLineTerm : true, // ensure each line is term'd forceLineTerm : true, // Ensure each line is term'd
asciiMode : true, // export to ASCII asciiMode : true, // Export to ASCII
fillLines : false, // don't fill up to |cols| fillLines : false, // Don't fill up to |cols|
}, },
(err, prepped) => { (err, prepped) => {
return cb(prepped || body); return cb(prepped || body);
} }
); );
} else { } else {
return cb(cleanControlCodes(body, { all : true } )); const prepped = splitTextAtTerms(cleanControlCodes(body, { all : true } ) )
.map(l => (wordWrapText(l, { width : WordWrapColumn } ).wrapped || []).join('\n'))
.join('\n');
return cb(prepped);
} }
} }
@ -225,7 +249,7 @@ exports.getModule = class GopherModule extends ServerModule {
return message.load( { uuid : msgUuid }, err => { return message.load( { uuid : msgUuid }, err => {
if(err) { if(err) {
this.log.debug( { uuid : msgUuid }, 'Attempted access to non-existant message UUID!'); this.log.debug( { uuid : msgUuid }, 'Attempted access to non-existent message UUID!');
return this.notFoundGenerator(selectorMatch, cb); return this.notFoundGenerator(selectorMatch, cb);
} }
@ -268,10 +292,17 @@ ${msgBody}
return this.notFoundGenerator(selectorMatch, cb); return this.notFoundGenerator(selectorMatch, cb);
} }
return getMessageListForArea(null, areaTag, (err, msgList) => { const filter = {
resultType : 'messageList',
sort : 'messageId',
order : 'descending', // we want newest messages first for Gopher
};
return getMessageListForArea(null, areaTag, filter, (err, msgList) => {
const response = [ const response = [
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`), this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`),
this.makeItem(ItemTypes.InfoMessage, '(newest first)'),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
...msgList.map(msg => this.makeItem( ...msgList.map(msg => this.makeItem(
ItemTypes.TextFile, ItemTypes.TextFile,

View file

@ -10,6 +10,10 @@ const userLogin = require('../../user_login.js').userLogin;
const enigVersion = require('../../../package.json').version; const enigVersion = require('../../../package.json').version;
const theme = require('../../theme.js'); const theme = require('../../theme.js');
const stringFormat = require('../../string_format.js'); const stringFormat = require('../../string_format.js');
const {
Errors,
ErrorReasons
} = require('../../enig_error.js');
// deps // deps
const ssh2 = require('ssh2'); const ssh2 = require('ssh2');
@ -36,8 +40,6 @@ function SSHClient(clientConn) {
const self = this; const self = this;
let loginAttempts = 0;
clientConn.on('authentication', function authAttempt(ctx) { clientConn.on('authentication', function authAttempt(ctx) {
const username = ctx.username || ''; const username = ctx.username || '';
const password = ctx.password || ''; const password = ctx.password || '';
@ -52,26 +54,56 @@ function SSHClient(clientConn) {
return clientConn.end(); return clientConn.end();
} }
function alreadyLoggedIn(username) { function promptAndTerm(msg) {
ctx.prompt(`${username} is already connected to the system. Terminating connection.\n(Press any key to continue)`); if('keyboard-interactive' === ctx.method) {
ctx.prompt(msg);
}
return terminateConnection(); return terminateConnection();
} }
function accountAlreadyLoggedIn(username) {
return promptAndTerm(`${username} is already connected to the system. Terminating connection.\n(Press any key to continue)`);
}
function accountDisabled(username) {
return promptAndTerm(`${username} is disabled.\n(Press any key to continue)`);
}
function accountInactive(username) {
return promptAndTerm(`${username} is waiting for +op activation.\n(Press any key to continue)`);
}
function accountLocked(username) {
return promptAndTerm(`${username} is locked.\n(Press any key to continue)`);
}
function isSpecialHandleError(err) {
return [ ErrorReasons.AlreadyLoggedIn, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked ].includes(err.reasonCode);
}
function handleSpecialError(err, username) {
switch(err.reasonCode) {
case ErrorReasons.AlreadyLoggedIn : return accountAlreadyLoggedIn(username);
case ErrorReasons.Inactive : return accountInactive(username);
case ErrorReasons.Disabled : return accountDisabled(username);
case ErrorReasons.Locked : return accountLocked(username);
default : return terminateConnection();
}
}
// //
// If the system is open and |isNewUser| is true, the login // If the system is open and |isNewUser| is true, the login
// sequence is hijacked in order to start the applicaiton process. // sequence is hijacked in order to start the application process.
// //
if(false === config.general.closedSystem && self.isNewUser) { if(false === config.general.closedSystem && self.isNewUser) {
return ctx.accept(); return ctx.accept();
} }
if(username.length > 0 && password.length > 0) { if(username.length > 0 && password.length > 0) {
loginAttempts += 1;
userLogin(self, ctx.username, ctx.password, function authResult(err) { userLogin(self, ctx.username, ctx.password, function authResult(err) {
if(err) { if(err) {
if(err.existingConn) { if(isSpecialHandleError(err)) {
return alreadyLoggedIn(username); return handleSpecialError(err, username);
} }
return ctx.reject(SSHClient.ValidAuthMethods); return ctx.reject(SSHClient.ValidAuthMethods);
@ -92,15 +124,13 @@ function SSHClient(clientConn) {
const interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; const interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false };
ctx.prompt(interactivePrompt, function retryPrompt(answers) { ctx.prompt(interactivePrompt, function retryPrompt(answers) {
loginAttempts += 1;
userLogin(self, username, (answers[0] || ''), err => { userLogin(self, username, (answers[0] || ''), err => {
if(err) { if(err) {
if(err.existingConn) { if(isSpecialHandleError(err)) {
return alreadyLoggedIn(username); return handleSpecialError(err, username);
} }
if(loginAttempts >= config.general.loginAttempts) { if(Errors.BadLogin().code === err.code) {
return terminateConnection(); return terminateConnection();
} }

View file

@ -14,7 +14,7 @@ const {
updateMessageAreaLastReadId, updateMessageAreaLastReadId,
getMessageIdNewerThanTimestampByArea getMessageIdNewerThanTimestampByArea
} = require('./message_area.js'); } = require('./message_area.js');
const stringFormat = require('./string_format.js'); const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
@ -153,11 +153,13 @@ exports.getModule = class SetNewScanDate extends MenuModule {
selections.push({ selections.push({
conf : { conf : {
confTag : conf.confTag, confTag : conf.confTag,
text : conf.conf.name, // standard
name : conf.conf.name, name : conf.conf.name,
desc : conf.conf.desc, desc : conf.conf.desc,
}, },
area : { area : {
areaTag : area.areaTag, areaTag : area.areaTag,
text : area.area.name, // standard
name : area.area.name, name : area.area.name,
desc : area.area.desc, desc : area.area.desc,
} }
@ -168,19 +170,21 @@ exports.getModule = class SetNewScanDate extends MenuModule {
selections.unshift({ selections.unshift({
conf : { conf : {
confTag : '', confTag : '',
text : 'All conferences',
name : 'All conferences', name : 'All conferences',
desc : 'All conferences', desc : 'All conferences',
}, },
area : { area : {
areaTag : '', areaTag : '',
text : 'All areas',
name : 'All areas', name : 'All areas',
desc : 'All areas', desc : 'All areas',
} }
}); });
// Find current conf/area & move it directly under "All" // Find current conf/area & move it directly under "All"
const currConfTag = this.client.user.properties.message_conf_tag; const currConfTag = this.client.user.properties[UserProps.MessageConfTag];
const currAreaTag = this.client.user.properties.message_area_tag; const currAreaTag = this.client.user.properties[UserProps.MessageAreaTag];
if(currConfTag && currAreaTag) { if(currConfTag && currAreaTag) {
const confAreaIndex = selections.findIndex( confArea => { const confAreaIndex = selections.findIndex( confArea => {
return confArea.conf.confTag === currConfTag && confArea.area.areaTag === currAreaTag; return confArea.conf.confTag === currConfTag && confArea.area.areaTag === currAreaTag;
@ -236,14 +240,9 @@ exports.getModule = class SetNewScanDate extends MenuModule {
scanDateView.setText(today.format(scanDateFormat)); scanDateView.setText(today.format(scanDateFormat));
if('message' === self.target) { if('message' === self.target) {
const messageSelectionsFormat = self.menuConfig.config.messageSelectionsFormat || '{conf.name} - {area.name}';
const messageSelectionFocusFormat = self.menuConfig.config.messageSelectionFocusFormat || messageSelectionsFormat;
const targetSelectionView = vc.getView(MciViewIds.main.targetSelection); const targetSelectionView = vc.getView(MciViewIds.main.targetSelection);
targetSelectionView.setItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection))); targetSelectionView.setItems(self.targetSelections);
targetSelectionView.setFocusItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection)));
targetSelectionView.setFocusItemIndex(0); targetSelectionView.setFocusItemIndex(0);
} }

View file

@ -68,7 +68,7 @@ exports.getModule = class ShowArtModule extends MenuModule {
} }
showByExtraArgs(cb) { showByExtraArgs(cb) {
this.getArtKeyValue( (err, artSpec) => { this.getArtKeyValue(this.config.key, (err, artSpec) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
@ -89,7 +89,7 @@ exports.getModule = class ShowArtModule extends MenuModule {
} }
showByFileBaseArea(cb) { showByFileBaseArea(cb) {
this.getArtKeyValue( (err, key) => { this.getArtKeyValue('areaTag', (err, key) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
@ -98,7 +98,7 @@ exports.getModule = class ShowArtModule extends MenuModule {
} }
showByMessageConf(cb) { showByMessageConf(cb) {
this.getArtKeyValue( (err, key) => { this.getArtKeyValue('confTag', (err, key) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
@ -107,7 +107,7 @@ exports.getModule = class ShowArtModule extends MenuModule {
} }
showByMessageArea(cb) { showByMessageArea(cb) {
this.getArtKeyValue( (err, key) => { this.getArtKeyValue('areaTag', (err, key) => {
if(err) { if(err) {
return cb(err); return cb(err);
} }
@ -133,8 +133,8 @@ exports.getModule = class ShowArtModule extends MenuModule {
return this.displaySingleArtWithOptions(artSpec, options, cb); return this.displaySingleArtWithOptions(artSpec, options, cb);
} }
getArtKeyValue(cb) { getArtKeyValue(defaultKey, cb) {
const key = this.config.key; const key = this.config.key || defaultKey;
if(!_.isString(key)) { if(!_.isString(key)) {
return cb(Errors.MissingConfig('Config option "key" is required for method "extraArgs"')); return cb(Errors.MissingConfig('Config option "key" is required for method "extraArgs"'));
} }

View file

@ -2,10 +2,12 @@
'use strict'; 'use strict';
const sysDb = require('./database.js').dbs.system; const sysDb = require('./database.js').dbs.system;
const {
getISOTimestampString
} = require('./database.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment');
/* /*
System Event Log & Stats System Event Log & Stats
@ -68,6 +70,7 @@ class StatLog {
}; };
} }
// :TODO: fix spelling :)
setNonPeristentSystemStat(statName, statValue) { setNonPeristentSystemStat(statName, statValue) {
this.systemStats[statName] = statValue; this.systemStats[statName] = statValue;
} }
@ -148,7 +151,9 @@ class StatLog {
} }
// the time "now" in the ISO format we use and love :) // the time "now" in the ISO format we use and love :)
get now() { return moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } get now() {
return getISOTimestampString();
}
appendSystemLogEntry(logName, logValue, keep, keepType, cb) { appendSystemLogEntry(logName, logValue, keep, keepType, cb) {
sysDb.run( sysDb.run(

View file

@ -2,10 +2,12 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const removeClient = require('./client_connections.js').removeClient; const { removeClient } = require('./client_connections.js');
const ansiNormal = require('./ansi_term.js').normal; const ansiNormal = require('./ansi_term.js').normal;
const userLogin = require('./user_login.js').userLogin; const { userLogin } = require('./user_login.js');
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const { ErrorReasons } = require('./enig_error.js');
const UserProps = require('./user_property.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
@ -25,13 +27,23 @@ function login(callingMenu, formData, extraArgs, cb) {
userLogin(callingMenu.client, formData.value.username, formData.value.password, err => { userLogin(callingMenu.client, formData.value.username, formData.value.password, err => {
if(err) { if(err) {
// login failure // already logged in with this user?
if(err.existingConn && _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) { if(ErrorReasons.AlreadyLoggedIn === err.reasonCode &&
_.has(callingMenu, 'menuConfig.config.tooNodeMenu'))
{
return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb); return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb);
} else {
// Other error
return callingMenu.prevMenu(cb);
} }
const ReasonsMenus = [
ErrorReasons.TooMany, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked
];
if(ReasonsMenus.includes(err.reasonCode)) {
const menu = _.get(callingMenu, [ 'menuConfig', 'config', err.reasonCode.toLowerCase() ]);
return menu ? callingMenu.gotoMenu(menu, cb) : logoff(callingMenu, {}, {}, cb);
}
// Other error
return callingMenu.prevMenu(cb);
} }
// success! // success!
@ -94,7 +106,7 @@ function reloadMenu(menu, cb) {
function prevConf(callingMenu, formData, extraArgs, cb) { function prevConf(callingMenu, formData, extraArgs, cb) {
const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client);
const currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties.message_conf_tag) || confs.length; const currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties[UserProps.MessageConfTag]) || confs.length;
messageArea.changeMessageConference(callingMenu.client, confs[currIndex - 1].confTag, err => { messageArea.changeMessageConference(callingMenu.client, confs[currIndex - 1].confTag, err => {
if(err) { if(err) {
@ -107,7 +119,7 @@ function prevConf(callingMenu, formData, extraArgs, cb) {
function nextConf(callingMenu, formData, extraArgs, cb) { function nextConf(callingMenu, formData, extraArgs, cb) {
const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client);
let currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties.message_conf_tag); let currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties[UserProps.MessageConfTag]);
if(currIndex === confs.length - 1) { if(currIndex === confs.length - 1) {
currIndex = -1; currIndex = -1;
@ -123,8 +135,8 @@ function nextConf(callingMenu, formData, extraArgs, cb) {
} }
function prevArea(callingMenu, formData, extraArgs, cb) { function prevArea(callingMenu, formData, extraArgs, cb) {
const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties[UserProps.MessageConfTag]);
const currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag) || areas.length; const currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties[UserProps.MessageAreaTag]) || areas.length;
messageArea.changeMessageArea(callingMenu.client, areas[currIndex - 1].areaTag, err => { messageArea.changeMessageArea(callingMenu.client, areas[currIndex - 1].areaTag, err => {
if(err) { if(err) {
@ -136,8 +148,8 @@ function prevArea(callingMenu, formData, extraArgs, cb) {
} }
function nextArea(callingMenu, formData, extraArgs, cb) { function nextArea(callingMenu, formData, extraArgs, cb) {
const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties[UserProps.MessageConfTag]);
let currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag); let currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties[UserProps.MessageAreaTag]);
if(currIndex === areas.length - 1) { if(currIndex === areas.length - 1) {
currIndex = -1; currIndex = -1;

View file

@ -13,7 +13,9 @@ const Errors = require('./enig_error.js').Errors;
const ErrorReasons = require('./enig_error.js').ErrorReasons; const ErrorReasons = require('./enig_error.js').ErrorReasons;
const Events = require('./events.js'); const Events = require('./events.js');
const AnsiPrep = require('./ansi_prep.js'); const AnsiPrep = require('./ansi_prep.js');
const UserProps = require('./user_property.js');
// deps
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const async = require('async'); const async = require('async');
@ -38,7 +40,7 @@ function refreshThemeHelpers(theme) {
getPasswordChar : function() { getPasswordChar : function() {
let pwChar = _.get( let pwChar = _.get(
theme, theme,
'customization.defaults.general.passwordChar', 'customization.defaults.passwordChar',
Config().theme.passwordChar Config().theme.passwordChar
); );
@ -427,8 +429,8 @@ function getThemeArt(options, cb) {
// random // random
// //
const config = Config(); const config = Config();
if(!options.themeId && _.has(options, 'client.user.properties.theme_id')) { if(!options.themeId && _.has(options, [ 'client', 'user', 'properties', UserProps.ThemeId ])) {
options.themeId = options.client.user.properties.theme_id; options.themeId = options.client.user.properties[UserProps.ThemeId];
} else { } else {
options.themeId = config.theme.default; options.themeId = config.theme.default;
} }
@ -682,8 +684,9 @@ function displayThemedAsset(assetSpec, client, options, cb) {
options = {}; options = {};
} }
if(Array.isArray(assetSpec) && _.isString(options.acsCondMember)) { if(Array.isArray(assetSpec)) {
assetSpec = client.acs.getConditionalValue(assetSpec, options.acsCondMember); const acsCondMember = options.acsCondMember || 'art';
assetSpec = client.acs.getConditionalValue(assetSpec, acsCondMember);
} }
const artAsset = asset.getArtAsset(assetSpec); const artAsset = asset.getArtAsset(assetSpec);

View file

@ -187,10 +187,10 @@ module.exports = class TicFileInfo {
// send the file to be distributed and the accompanying TIC file. // send the file to be distributed and the accompanying TIC file.
// Some File processors (Allfix) only insert a line with this // Some File processors (Allfix) only insert a line with this
// keyword when the file and the associated TIC file are to be // keyword when the file and the associated TIC file are to be
// file routed through a third sysem instead of being processed // file routed through a third system instead of being processed
// by a file processor on that system. Others always insert it. // by a file processor on that system. Others always insert it.
// Note that the To keyword may cause problems when the TIC file // Note that the To keyword may cause problems when the TIC file
// is proecessed by software that does not recognise it and // is processed by software that does not recognize it and
// passes the line "as is" to other systems. // passes the line "as is" to other systems.
// //
// Example: To 292/854 // Example: To 292/854

View file

@ -1,11 +1,18 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½
const userDb = require('./database.js').dbs.user; const userDb = require('./database.js').dbs.user;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const userGroup = require('./user_group.js'); const userGroup = require('./user_group.js');
const Errors = require('./enig_error.js').Errors; const {
Errors,
ErrorReasons
} = require('./enig_error.js');
const Events = require('./events.js'); const Events = require('./events.js');
const UserProps = require('./user_property.js');
const Log = require('./logger.js').log;
const StatLog = require('./stat_log.js');
// deps // deps
const crypto = require('crypto'); const crypto = require('crypto');
@ -39,18 +46,31 @@ module.exports = class User {
static get StandardPropertyGroups() { static get StandardPropertyGroups() {
return { return {
password : [ 'pw_pbkdf2_salt', 'pw_pbkdf2_dk' ], password : [ UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk ],
}; };
} }
static get AccountStatus() { static get AccountStatus() {
return { return {
disabled : 0, disabled : 0, // +op disabled
inactive : 1, inactive : 1, // inactive, aka requires +op approval/activation
active : 2, active : 2, // standard, active
locked : 3, // locked out (too many bad login attempts, etc.)
}; };
} }
static isSamePasswordSlowCompare(passBuf1, passBuf2) {
if(passBuf1.length !== passBuf2.length) {
return false;
}
let c = 0;
for(let i = 0; i < passBuf1.length; i++) {
c |= passBuf1[i] ^ passBuf2[i];
}
return 0 === c;
}
isAuthenticated() { isAuthenticated() {
return true === this.authenticated; return true === this.authenticated;
} }
@ -60,16 +80,21 @@ module.exports = class User {
return false; return false;
} }
return this.hasValidPassword(); return this.hasValidPasswordProperties();
} }
hasValidPassword() { hasValidPasswordProperties() {
if(!this.properties || !this.properties.pw_pbkdf2_salt || !this.properties.pw_pbkdf2_dk) { const salt = this.getProperty(UserProps.PassPbkdf2Salt);
const dk = this.getProperty(UserProps.PassPbkdf2Dk);
if(!salt || !dk ||
(salt.length !== User.PBKDF2.saltLen * 2) ||
(dk.length !== User.PBKDF2.keyLen * 2))
{
return false; return false;
} }
return ((this.properties.pw_pbkdf2_salt.length === User.PBKDF2.saltLen * 2) && return true;
(this.properties.pw_pbkdf2_dk.length === User.PBKDF2.keyLen * 2));
} }
isRoot() { isRoot() {
@ -101,31 +126,85 @@ module.exports = class User {
return 10; // :TODO: Is this what we want? return 10; // :TODO: Is this what we want?
} }
processFailedLogin(userId, cb) {
async.waterfall(
[
(callback) => {
return User.getUser(userId, callback);
},
(tempUser, callback) => {
return StatLog.incrementUserStat(
tempUser,
UserProps.FailedLoginAttempts,
1,
(err, failedAttempts) => {
return callback(null, tempUser, failedAttempts);
}
);
},
(tempUser, failedAttempts, callback) => {
const lockAccount = _.get(Config(), 'users.failedLogin.lockAccount');
if(lockAccount > 0 && failedAttempts >= lockAccount) {
const props = {
[ UserProps.AccountStatus ] : User.AccountStatus.locked,
[ UserProps.AccountLockedTs ] : StatLog.now,
};
if(!_.has(tempUser.properties, UserProps.AccountLockedPrevStatus)) {
props[UserProps.AccountLockedPrevStatus] = tempUser.getProperty(UserProps.AccountStatus);
}
Log.info( { userId, failedAttempts }, '(Re)setting account to locked due to failed logins');
return tempUser.persistProperties(props, callback);
}
return cb(null);
}
],
err => {
return cb(err);
}
);
}
unlockAccount(cb) {
const prevStatus = this.getProperty(UserProps.AccountLockedPrevStatus);
if(!prevStatus) {
return cb(null); // nothing to do
}
this.persistProperty(UserProps.AccountStatus, prevStatus, err => {
if(err) {
return cb(err);
}
return this.removeProperties( [ UserProps.AccountLockedPrevStatus, UserProps.AccountLockedTs ], cb);
});
}
authenticate(username, password, cb) { authenticate(username, password, cb) {
const self = this; const self = this;
const cachedInfo = {}; const tempAuthInfo = {};
async.waterfall( async.waterfall(
[ [
function fetchUserId(callback) { function fetchUserId(callback) {
// get user ID // get user ID
User.getUserIdAndName(username, (err, uid, un) => { User.getUserIdAndName(username, (err, uid, un) => {
cachedInfo.userId = uid; tempAuthInfo.userId = uid;
cachedInfo.username = un; tempAuthInfo.username = un;
return callback(err); return callback(err);
}); });
}, },
function getRequiredAuthProperties(callback) { function getRequiredAuthProperties(callback) {
// fetch properties required for authentication // fetch properties required for authentication
User.loadProperties(cachedInfo.userId, { names : User.StandardPropertyGroups.password }, (err, props) => { User.loadProperties(tempAuthInfo.userId, { names : User.StandardPropertyGroups.password }, (err, props) => {
return callback(err, props); return callback(err, props);
}); });
}, },
function getDkWithSalt(props, callback) { function getDkWithSalt(props, callback) {
// get DK from stored salt and password provided // get DK from stored salt and password provided
User.generatePasswordDerivedKey(password, props.pw_pbkdf2_salt, (err, dk) => { User.generatePasswordDerivedKey(password, props[UserProps.PassPbkdf2Salt], (err, dk) => {
return callback(err, dk, props.pw_pbkdf2_dk); return callback(err, dk, props[UserProps.PassPbkdf2Dk]);
}); });
}, },
function validateAuth(passDk, propsDk, callback) { function validateAuth(passDk, propsDk, callback) {
@ -135,30 +214,57 @@ module.exports = class User {
const passDkBuf = Buffer.from(passDk, 'hex'); const passDkBuf = Buffer.from(passDk, 'hex');
const propsDkBuf = Buffer.from(propsDk, 'hex'); const propsDkBuf = Buffer.from(propsDk, 'hex');
if(passDkBuf.length !== propsDkBuf.length) { return callback(User.isSamePasswordSlowCompare(passDkBuf, propsDkBuf) ?
return callback(Errors.AccessDenied('Invalid password')); null :
} Errors.AccessDenied('Invalid password')
);
let c = 0;
for(let i = 0; i < passDkBuf.length; i++) {
c |= passDkBuf[i] ^ propsDkBuf[i];
}
return callback(0 === c ? null : Errors.AccessDenied('Invalid password'));
}, },
function initProps(callback) { function initProps(callback) {
User.loadProperties(cachedInfo.userId, (err, allProps) => { User.loadProperties(tempAuthInfo.userId, (err, allProps) => {
if(!err) { if(!err) {
cachedInfo.properties = allProps; tempAuthInfo.properties = allProps;
} }
return callback(err); return callback(err);
}); });
}, },
function checkAccountStatus(callback) {
const accountStatus = parseInt(tempAuthInfo.properties[UserProps.AccountStatus], 10);
if(User.AccountStatus.disabled === accountStatus) {
return callback(Errors.AccessDenied('Account disabled', ErrorReasons.Disabled));
}
if(User.AccountStatus.inactive === accountStatus) {
return callback(Errors.AccessDenied('Account inactive', ErrorReasons.Inactive));
}
if(User.AccountStatus.locked === accountStatus) {
const autoUnlockMinutes = _.get(Config(), 'users.failedLogin.autoUnlockMinutes');
const lockedTs = moment(tempAuthInfo.properties[UserProps.AccountLockedTs]);
if(autoUnlockMinutes && lockedTs.isValid()) {
const minutesSinceLocked = moment().diff(lockedTs, 'minutes');
if(minutesSinceLocked >= autoUnlockMinutes) {
// allow the login - we will clear any lock there
Log.info(
{ username, userId : tempAuthInfo.userId, lockedAt : lockedTs.format() },
'Locked account will now be unlocked due to auto-unlock minutes policy'
);
return callback(null);
}
}
return callback(Errors.AccessDenied('Account is locked', ErrorReasons.Locked));
}
// anything else besides active is still not allowed
if(User.AccountStatus.active !== accountStatus) {
return callback(Errors.AccessDenied('Account is not active'));
}
return callback(null);
},
function initGroups(callback) { function initGroups(callback) {
userGroup.getGroupsForUser(cachedInfo.userId, (err, groups) => { userGroup.getGroupsForUser(tempAuthInfo.userId, (err, groups) => {
if(!err) { if(!err) {
cachedInfo.groups = groups; tempAuthInfo.groups = groups;
} }
return callback(err); return callback(err);
@ -166,15 +272,44 @@ module.exports = class User {
} }
], ],
err => { err => {
if(!err) { if(err) {
self.userId = cachedInfo.userId; //
self.username = cachedInfo.username; // If we failed login due to something besides an inactive or disabled account,
self.properties = cachedInfo.properties; // we need to update failure status and possibly lock the account.
self.groups = cachedInfo.groups; //
// If locked already, update the lock timestamp -- ie, extend the lockout period.
//
if(![ErrorReasons.Disabled, ErrorReasons.Inactive].includes(err.reasonCode) && tempAuthInfo.userId) {
self.processFailedLogin(tempAuthInfo.userId, persistErr => {
if(persistErr) {
Log.warn( { error : persistErr.message }, 'Failed to persist failed login information');
}
return cb(err); // pass along original error
});
} else {
return cb(err);
}
} else {
// everything checks out - load up info
self.userId = tempAuthInfo.userId;
self.username = tempAuthInfo.username;
self.properties = tempAuthInfo.properties;
self.groups = tempAuthInfo.groups;
self.authenticated = true; self.authenticated = true;
}
return cb(err); self.removeProperty(UserProps.FailedLoginAttempts);
//
// We need to *revert* any locked status back to
// the user's previous status & clean up props.
//
self.unlockAccount(unlockErr => {
if(unlockErr) {
Log.warn( { error : unlockErr.message }, 'Failed to unlock account');
}
return cb(null);
});
}
} }
); );
} }
@ -190,7 +325,7 @@ module.exports = class User {
const self = this; const self = this;
// :TODO: set various defaults, e.g. default activation status, etc. // :TODO: set various defaults, e.g. default activation status, etc.
self.properties.account_status = config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; self.properties[UserProps.AccountStatus] = config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active;
async.waterfall( async.waterfall(
[ [
@ -211,7 +346,7 @@ module.exports = class User {
// Do not require activation for userId 1 (root/admin) // Do not require activation for userId 1 (root/admin)
if(User.RootUserID === self.userId) { if(User.RootUserID === self.userId) {
self.properties.account_status = User.AccountStatus.active; self.properties[UserProps.AccountStatus] = User.AccountStatus.active;
} }
return callback(null, trans); return callback(null, trans);
@ -224,8 +359,8 @@ module.exports = class User {
return callback(err); return callback(err);
} }
self.properties.pw_pbkdf2_salt = info.salt; self.properties[UserProps.PassPbkdf2Salt] = info.salt;
self.properties.pw_pbkdf2_dk = info.dk; self.properties[UserProps.PassPbkdf2Dk] = info.dk;
return callback(null, trans); return callback(null, trans);
}); });
}, },
@ -289,20 +424,32 @@ module.exports = class User {
); );
} }
static persistPropertyByUserId(userId, propName, propValue, cb) {
userDb.run(
`REPLACE INTO user_property (user_id, prop_name, prop_value)
VALUES (?, ?, ?);`,
[ userId, propName, propValue ],
err => {
if(cb) {
return cb(err, propValue);
}
}
);
}
getProperty(propName) {
return this.properties[propName];
}
getPropertyAsNumber(propName) {
return parseInt(this.getProperty(propName), 10);
}
persistProperty(propName, propValue, cb) { persistProperty(propName, propValue, cb) {
// update live props // update live props
this.properties[propName] = propValue; this.properties[propName] = propValue;
userDb.run( return User.persistPropertyByUserId(this.userId, propName, propValue, cb);
`REPLACE INTO user_property (user_id, prop_name, prop_value)
VALUES (?, ?, ?);`,
[ this.userId, propName, propValue ],
err => {
if(cb) {
return cb(err);
}
}
);
} }
removeProperty(propName, cb) { removeProperty(propName, cb) {
@ -321,6 +468,15 @@ module.exports = class User {
); );
} }
removeProperties(propNames, cb) {
async.each(propNames, (name, next) => {
return this.removeProperty(name, next);
},
err => {
return cb(err);
});
}
persistProperties(properties, transOrDb, cb) { persistProperties(properties, transOrDb, cb) {
if(!_.isFunction(cb) && _.isFunction(transOrDb)) { if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb; cb = transOrDb;
@ -360,8 +516,8 @@ module.exports = class User {
} }
const newProperties = { const newProperties = {
pw_pbkdf2_salt : info.salt, [ UserProps.PassPbkdf2Salt ] : info.salt,
pw_pbkdf2_dk : info.dk, [ UserProps.PassPbkdf2Dk ] : info.dk,
}; };
this.persistProperties(newProperties, err => { this.persistProperties(newProperties, err => {
@ -371,8 +527,9 @@ module.exports = class User {
} }
getAge() { getAge() {
if(_.has(this.properties, 'birthdate')) { const birthdate = this.getProperty(UserProps.Birthdate);
return moment().diff(this.properties.birthdate, 'years'); if(birthdate) {
return moment().diff(birthdate, 'years');
} }
} }
@ -439,7 +596,7 @@ module.exports = class User {
WHERE id = ( WHERE id = (
SELECT user_id SELECT user_id
FROM user_property FROM user_property
WHERE prop_name='real_name' AND prop_value LIKE ? WHERE prop_name='${UserProps.RealName}' AND prop_value LIKE ?
);`, );`,
[ realName ], [ realName ],
(err, row) => { (err, row) => {

View file

@ -1,11 +1,17 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const theme = require('./theme.js'); const theme = require('./theme.js');
const sysValidate = require('./system_view_validate.js'); const sysValidate = require('./system_view_validate.js');
const UserProps = require('./user_property.js');
const {
getISOTimestampString
} = require('./database.js');
// deps
const async = require('async'); const async = require('async');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
@ -49,7 +55,7 @@ exports.getModule = class UserConfigModule extends MenuModule {
// //
// If nothing changed, we know it's OK // If nothing changed, we know it's OK
// //
if(self.client.user.properties.email_address.toLowerCase() === data.toLowerCase()) { if(self.client.user.properties[UserProps.EmailAddress].toLowerCase() === data.toLowerCase()) {
return cb(null); return cb(null);
} }
@ -101,15 +107,15 @@ exports.getModule = class UserConfigModule extends MenuModule {
assert(formData.value.password === formData.value.passwordConfirm); assert(formData.value.password === formData.value.passwordConfirm);
const newProperties = { const newProperties = {
real_name : formData.value.realName, [ UserProps.RealName ] : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), [ UserProps.Birthdate ] : getISOTimestampString(formData.value.birthdate),
sex : formData.value.sex, [ UserProps.Sex ] : formData.value.sex,
location : formData.value.location, [ UserProps.Location ] : formData.value.location,
affiliation : formData.value.affils, [ UserProps.Affiliations ] : formData.value.affils,
email_address : formData.value.email, [ UserProps.EmailAddress ] : formData.value.email,
web_address : formData.value.web, [ UserProps.WebAddress ] : formData.value.web,
term_height : formData.value.termHeight.toString(), [ UserProps.TermHeight ] : formData.value.termHeight.toString(),
theme_id : self.availThemeInfo[formData.value.theme].themeId, [ UserProps.ThemeId ] : self.availThemeInfo[formData.value.theme].themeId,
}; };
// runtime set theme // runtime set theme
@ -176,22 +182,22 @@ exports.getModule = class UserConfigModule extends MenuModule {
}), 'name'); }), 'name');
currentThemeIdIndex = Math.max(0, _.findIndex(self.availThemeInfo, function cmp(ti) { currentThemeIdIndex = Math.max(0, _.findIndex(self.availThemeInfo, function cmp(ti) {
return ti.themeId === self.client.user.properties.theme_id; return ti.themeId === self.client.user.properties[UserProps.ThemeId];
})); }));
callback(null); callback(null);
}, },
function populateViews(callback) { function populateViews(callback) {
var user = self.client.user; const user = self.client.user;
self.setViewText('menu', MciCodeIds.RealName, user.properties.real_name); self.setViewText('menu', MciCodeIds.RealName, user.properties[UserProps.RealName]);
self.setViewText('menu', MciCodeIds.BirthDate, moment(user.properties.birthdate).format('YYYYMMDD')); self.setViewText('menu', MciCodeIds.BirthDate, moment(user.properties[UserProps.Birthdate]).format('YYYYMMDD'));
self.setViewText('menu', MciCodeIds.Sex, user.properties.sex); self.setViewText('menu', MciCodeIds.Sex, user.properties[UserProps.Sex]);
self.setViewText('menu', MciCodeIds.Loc, user.properties.location); self.setViewText('menu', MciCodeIds.Loc, user.properties[UserProps.Location]);
self.setViewText('menu', MciCodeIds.Affils, user.properties.affiliation); self.setViewText('menu', MciCodeIds.Affils, user.properties[UserProps.Affiliations]);
self.setViewText('menu', MciCodeIds.Email, user.properties.email_address); self.setViewText('menu', MciCodeIds.Email, user.properties[UserProps.EmailAddress]);
self.setViewText('menu', MciCodeIds.Web, user.properties.web_address); self.setViewText('menu', MciCodeIds.Web, user.properties[UserProps.WebAddress]);
self.setViewText('menu', MciCodeIds.TermHeight, user.properties.term_height.toString()); self.setViewText('menu', MciCodeIds.TermHeight, user.properties[UserProps.TermHeight].toString());
var themeView = self.getView(MciCodeIds.Theme); var themeView = self.getView(MciCodeIds.Theme);

View file

@ -5,6 +5,7 @@
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const { getUserList } = require('./user.js'); const { getUserList } = require('./user.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const UserProps = require('./user_property.js');
// deps // deps
const moment = require('moment'); const moment = require('moment');
@ -44,7 +45,7 @@ exports.getModule = class UserListModule extends MenuModule {
} }
const fetchOpts = { const fetchOpts = {
properties : [ 'real_name', 'location', 'affiliation', 'last_login_timestamp' ], properties : [ UserProps.RealName, UserProps.Location, UserProps.Affiliations, UserProps.LastLoginTs ],
propsCamelCase : true, // e.g. real_name -> realName propsCamelCase : true, // e.g. real_name -> realName
}; };
getUserList(fetchOpts, (err, userList) => { getUserList(fetchOpts, (err, userList) => {

View file

@ -8,34 +8,44 @@ const StatLog = require('./stat_log.js');
const logger = require('./logger.js'); const logger = require('./logger.js');
const Events = require('./events.js'); const Events = require('./events.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const {
Errors,
ErrorReasons
} = require('./enig_error.js');
const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash');
exports.userLogin = userLogin; exports.userLogin = userLogin;
function userLogin(client, username, password, cb) { function userLogin(client, username, password, cb) {
client.user.authenticate(username, password, function authenticated(err) { client.user.authenticate(username, password, err => {
const config = Config();
if(err) { if(err) {
client.user.sessionFailedLoginAttempts = _.get(client.user, 'sessionFailedLoginAttempts', 0) + 1;
const disconnect = config.users.failedLogin.disconnect;
if(disconnect > 0 && client.user.sessionFailedLoginAttempts >= disconnect) {
err = Errors.BadLogin('To many failed login attempts', ErrorReasons.TooMany);
}
client.log.info( { username : username, error : err.message }, 'Failed login attempt'); client.log.info( { username : username, error : err.message }, 'Failed login attempt');
// :TODO: if username exists, record failed login attempt to properties
// :TODO: check Config max failed logon attempts/etc. - set err.maxAttempts = true
return cb(err); return cb(err);
} }
const user = client.user;
const user = client.user;
// Good login; reset any failed attempts
delete user.sessionFailedLoginAttempts;
// //
// Ensure this user is not already logged in. // Ensure this user is not already logged in.
// Loop through active connections -- which includes the current --
// and check for matching user ID. If the count is > 1, disallow.
// //
let existingClientConnection; const existingClientConnection = clientConnections.find(cc => {
clientConnections.forEach(function connEntry(cc) { return user !== cc.user && // not current connection
if(cc.user !== user && cc.user.userId === user.userId) { user.userId === cc.user.userId; // ...but same user
existingClientConnection = cc;
}
}); });
if(existingClientConnection) { if(existingClientConnection) {
@ -48,12 +58,10 @@ function userLogin(client, username, password, cb) {
'Already logged in' 'Already logged in'
); );
const existingConnError = new Error('Already logged in as supplied user'); return cb(Errors.BadLogin(
existingConnError.existingConn = true; `User ${user.username} already logged in.`,
ErrorReasons.AlreadyLoggedIn
// :TODO: We should use EnigError & pass existing connection as second param ));
return cb(existingConnError);
} }
// update client logger with addition of username // update client logger with addition of username
@ -67,24 +75,24 @@ function userLogin(client, username, password, cb) {
client.log.info('Successful login'); client.log.info('Successful login');
// User's unique session identifier is the same as the connection itself // User's unique session identifier is the same as the connection itself
user.sessionId = client.session.uniqueId; // convienence user.sessionId = client.session.uniqueId; // convenience
Events.emit(Events.getSystemEvents().UserLogin, { user } ); Events.emit(Events.getSystemEvents().UserLogin, { user } );
async.parallel( async.parallel(
[ [
function setTheme(callback) { function setTheme(callback) {
setClientTheme(client, user.properties.theme_id); setClientTheme(client, user.properties[UserProps.ThemeId]);
return callback(null); return callback(null);
}, },
function updateSystemLoginCount(callback) { function updateSystemLoginCount(callback) {
return StatLog.incrementSystemStat('login_count', 1, callback); return StatLog.incrementSystemStat('login_count', 1, callback); // :TODO: create system_property.js
}, },
function recordLastLogin(callback) { function recordLastLogin(callback) {
return StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback); return StatLog.setUserStat(user, UserProps.LastLoginTs, StatLog.now, callback);
}, },
function updateUserLoginCount(callback) { function updateUserLoginCount(callback) {
return StatLog.incrementUserStat(user, 'login_count', 1, callback); return StatLog.incrementUserStat(user, UserProps.LoginCount, 1, callback);
}, },
function recordLoginHistory(callback) { function recordLoginHistory(callback) {
const loginHistoryMax = Config().statLog.systemEvents.loginHistoryMax; const loginHistoryMax = Config().statLog.systemEvents.loginHistoryMax;

53
core/user_property.js Normal file
View file

@ -0,0 +1,53 @@
/* jslint node: true */
'use strict';
//
// Common user properties used throughout the system.
//
// This IS NOT a full list. For example, custom modules
// can utilize their own properties as well!
//
module.exports = {
PassPbkdf2Salt : 'pw_pbkdf2_salt',
PassPbkdf2Dk : 'pw_pbkdf2_dk',
AccountStatus : 'account_status', // See User.AccountStatus enum
RealName : 'real_name',
Sex : 'sex',
Birthdate : 'birthdate',
Location : 'location',
Affiliations : 'affiliation',
EmailAddress : 'email_address',
WebAddress : 'web_address',
TermHeight : 'term_height',
TermWidth : 'term_width',
ThemeId : 'theme_id',
AccountCreated : 'account_created',
LastLoginTs : 'last_login_timestamp',
LoginCount : 'login_count',
UserComment : 'user_comment', // NYI
DownloadQueue : 'dl_queue', // download_queue.js
FailedLoginAttempts : 'failed_login_attempts',
AccountLockedTs : 'account_locked_timestamp',
AccountLockedPrevStatus : 'account_locked_prev_status', // previous account status before lock out
EmailPwResetToken : 'email_password_reset_token',
EmailPwResetTokenTs : 'email_password_reset_token_ts',
FileAreaTag : 'file_area_tag',
FileBaseFilters : 'file_base_filters',
FileBaseFilterActiveUuid : 'file_base_filter_active_uuid',
FileBaseLastViewedId : 'user_file_base_last_viewed',
FileDlTotalCount : 'dl_total_count',
FileUlTotalCount : 'ul_total_count',
FileDlTotalBytes : 'dl_total_bytes',
FileUlTotalBytes : 'ul_total_bytes',
MessageConfTag : 'message_conf_tag',
MessageAreaTag : 'message_area_tag',
MessagePostCount : 'post_count',
};

View file

@ -10,6 +10,7 @@ const User = require('./user.js');
const userDb = require('./database.js').dbs.user; const userDb = require('./database.js').dbs.user;
const getISOTimestampString = require('./database.js').getISOTimestampString; const getISOTimestampString = require('./database.js').getISOTimestampString;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
@ -17,6 +18,7 @@ const crypto = require('crypto');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const url = require('url'); const url = require('url');
const querystring = require('querystring'); const querystring = require('querystring');
const _ = require('lodash');
const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT =
`%USERNAME%: `%USERNAME%:
@ -57,7 +59,7 @@ class WebPasswordReset {
} }
User.getUser(userId, (err, user) => { User.getUser(userId, (err, user) => {
if(err || !user.properties.email_address) { if(err || !user.properties[UserProps.EmailAddress]) {
return callback(Errors.DoesNotExist('No email address associated with this user')); return callback(Errors.DoesNotExist('No email address associated with this user'));
} }
@ -77,8 +79,8 @@ class WebPasswordReset {
token = token.toString('hex'); token = token.toString('hex');
const newProperties = { const newProperties = {
email_password_reset_token : token, [ UserProps.EmailPwResetToken ] : token,
email_password_reset_token_ts : getISOTimestampString(), [ UserProps.EmailPwResetTokenTs ] : getISOTimestampString(),
}; };
// we simply place the reset token in the user's properties // we simply place the reset token in the user's properties
@ -103,13 +105,13 @@ class WebPasswordReset {
function buildAndSendEmail(user, textTemplate, htmlTemplate, callback) { function buildAndSendEmail(user, textTemplate, htmlTemplate, callback) {
const sendMail = require('./email.js').sendMail; const sendMail = require('./email.js').sendMail;
const resetUrl = webServer.instance.buildUrl(`/reset_password?token=${user.properties.email_password_reset_token}`); const resetUrl = webServer.instance.buildUrl(`/reset_password?token=${user.properties[UserProps.EmailPwResetToken]}`);
function replaceTokens(s) { function replaceTokens(s) {
return s return s
.replace(/%BOARDNAME%/g, Config().general.boardName) .replace(/%BOARDNAME%/g, Config().general.boardName)
.replace(/%USERNAME%/g, user.username) .replace(/%USERNAME%/g, user.username)
.replace(/%TOKEN%/g, user.properties.email_password_reset_token) .replace(/%TOKEN%/g, user.properties[UserProps.EmailPwResetToken])
.replace(/%RESET_URL%/g, resetUrl) .replace(/%RESET_URL%/g, resetUrl)
; ;
} }
@ -120,7 +122,7 @@ class WebPasswordReset {
} }
const message = { const message = {
to : `${user.properties.display_name||user.username} <${user.properties.email_address}>`, to : `${user.properties[UserProps.RealName]||user.username} <${user.properties[UserProps.EmailAddress]}>`,
// from will be filled in // from will be filled in
subject : 'Forgot Password', subject : 'Forgot Password',
text : textTemplate, text : textTemplate,
@ -283,8 +285,15 @@ class WebPasswordReset {
} }
// delete assoc properties - no need to wait for completion // delete assoc properties - no need to wait for completion
user.removeProperty('email_password_reset_token'); user.removeProperties([ UserProps.EmailPwResetToken, UserProps.EmailPwResetTokenTs ]);
user.removeProperty('email_password_reset_token_ts');
if(true === _.get(config, 'users.unlockAtEmailPwReset')) {
Log.info(
{ username : user.username, userId : user.userId },
'Remove any lock on account due to password reset policy'
);
user.unlockAccount( () => { /* dummy */ } );
}
resp.writeHead(200); resp.writeHead(200);
return resp.end('Password changed successfully'); return resp.end('Password changed successfully');

View file

@ -14,15 +14,17 @@
- [Creating Config Files]({{ site.baseurl }}{% link configuration/creating-config.md %}) - [Creating Config Files]({{ site.baseurl }}{% link configuration/creating-config.md %})
- [SysOp Setup]({{ site.baseurl }}{% link configuration/sysop-setup.md %}) - [SysOp Setup]({{ site.baseurl }}{% link configuration/sysop-setup.md %})
- [Editing hjson]({{ site.baseurl }}{% link configuration/editing-hjson.md %}) - [Editing hjson]({{ site.baseurl }}{% link configuration/editing-hjson.md %})
- [config.hjson]({{ site.baseurl }}{% link configuration/config-hjson.md %}) - [System Configuration]({{ site.baseurl }}{% link configuration/config-hjson.md %})
- [menu.hjson]({{ site.baseurl }}{% link configuration/menu-hjson.md %}) - [HJSON General]({{ site.baseurl }}{% link configuration/hjson.md %})
- [prompt.hjson]({{ site.baseurl }}{% link configuration/prompt-hjson.md %}) - [Menus]({{ site.baseurl }}{% link configuration/menu-hjson.md %})
- [Prompts]({{ site.baseurl }}{% link configuration/prompt-hjson.md %})
- [Directory Structure]({{ site.baseurl }}{% link configuration/directory-structure.md %}) - [Directory Structure]({{ site.baseurl }}{% link configuration/directory-structure.md %})
- [Archivers]({{ site.baseurl }}{% link configuration/archivers.md %}) - [Archivers]({{ site.baseurl }}{% link configuration/archivers.md %})
- [File Transfer Protocols]({{ site.baseurl }}{% link configuration/file-transfer-protocols.md %}) - [File Transfer Protocols]({{ site.baseurl }}{% link configuration/file-transfer-protocols.md %})
- [Email]({{ site.baseurl }}{% link configuration/email.md %}) - [Email]({{ site.baseurl }}{% link configuration/email.md %})
- [Colour Codes]({{ site.baseurl }}{% link configuration/colour-codes.md %}) - [Colour Codes]({{ site.baseurl }}{% link configuration/colour-codes.md %})
- [Access Condition System (ACS)]({{ site.baseurl }}{% link configuration/acs.md %}) - [Access Condition System (ACS)]({{ site.baseurl }}{% link configuration/acs.md %})
- [Event Scheduler]({{ site.baseurl }}{% link configuration/event-scheduler.md %})
- Scheduled jobs - Scheduled jobs
- File Base - File Base
@ -73,11 +75,13 @@
- [Rumorz]({{ site.baseurl }}{% link modding/rumorz.md %}) - [Rumorz]({{ site.baseurl }}{% link modding/rumorz.md %})
- [File Transfer Protocol Select]({{ site.baseurl }}{% link modding/file-transfer-protocol-select.md %}) - [File Transfer Protocol Select]({{ site.baseurl }}{% link modding/file-transfer-protocol-select.md %})
- [Onelinerz]({{ site.baseurl }}{% link modding/onelinerz.md %}) - [Onelinerz]({{ site.baseurl }}{% link modding/onelinerz.md %})
- [Show Art]({{ site.baseurl }}{% link modding/show-art.md %})
- [Download Manager]({{ site.baseurl }}{% link modding/file-base-download-manager.md %})
- [Web Download Manager]({{ site.baseurl }}{% link modding/file-base-web-download-manager.md %})
- [Set Newscan Date]({{ site.baseurl }}{% link modding/set-newscan-date.md %})
- Administration - Administration
- [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) - [oputil]({{ site.baseurl }}{% link admin/oputil.md %})
- [Oputil]({{ site.baseurl }}{% link oputil/index.md %})
- Troubleshooting - Troubleshooting
- [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.md %}) - [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.md %})

View file

@ -27,7 +27,7 @@ Commands break up operations by groups:
| Command | Description | | Command | Description |
|-----------|---------------| |-----------|---------------|
| `user` | User management | | `user` | User management |
| `config` | System configuration and maintentance | | `config` | System configuration and maintenance |
| `fb` | File base configuration and management | | `fb` | File base configuration and management |
| `mb` | Message base configuration and management | | `mb` | Message base configuration and management |
@ -45,11 +45,12 @@ usage: optutil.js user <action> [<args>]
actions: actions:
pw USERNAME PASSWORD set password to PASSWORD for USERNAME pw USERNAME PASSWORD set password to PASSWORD for USERNAME
rm USERNAME permanantely removes USERNAME user from system rm USERNAME permanently removes USERNAME user from system
activate USERNAME sets USERNAME's status to active activate USERNAME sets USERNAME's status to active
deactivate USERNAME sets USERNAME's status to deactive deactivate USERNAME sets USERNAME's status to inactive
disable USERNAME sets USERNAME's status to disabled disable USERNAME sets USERNAME's status to disabled
group USERNAME [+|-]GROUP adds (+) or removes (-) USERNAME from GROUP lock USERNAME sets USERNAME's status to locked
group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP
``` ```
| Action | Description | Examples | Aliases | | Action | Description | Examples | Aliases |
@ -59,6 +60,7 @@ actions:
| `activate` | Activates user | `./oputil.js user activate joeuser` | N/A | | `activate` | Activates user | `./oputil.js user activate joeuser` | N/A |
| `deactivate` | Deactivates user | `./oputil.js user deactivate joeuser` | N/A | | `deactivate` | Deactivates user | `./oputil.js user deactivate joeuser` | N/A |
| `disable` | Disables user (user will not be able to login) | `./oputil.js user disable joeuser` | N/A | | `disable` | Disables user (user will not be able to login) | `./oputil.js user disable joeuser` | N/A |
| `lock` | Locks the user account (prevents logins) | `./oputil.js user lock joeuser` | N/A |
| `group` | Modifies users group membership | Add to group: `./oputil.js user group joeuser +derp`<br/>Remove from group: `./oputil.js user group joeuser -derp` | N/A | | `group` | Modifies users group membership | Add to group: `./oputil.js user group joeuser +derp`<br/>Remove from group: `./oputil.js user group joeuser -derp` | N/A |
## Configuration ## Configuration
@ -82,7 +84,7 @@ import-areas args:
| Action | Description | Examples | | Action | Description | Examples |
|-----------|-------------------|---------------------------------------| |-----------|-------------------|---------------------------------------|
| `new` | Generates a new/initial configuration | `./oputil.js config new` (follow the prompts) | | `new` | Generates a new/initial configuration | `./oputil.js config new` (follow the prompts) |
| `import-areas` | Imports areas using a Fidonet style *.NA or AREAS.BBS formatted file | `./oputil.js config import-areas /some/path/l33tnet.na` | | `import-areas` | Imports areas using a FidoNet style *.NA or AREAS.BBS formatted file | `./oputil.js config import-areas /some/path/l33tnet.na` |
When using the `import-areas` action, you will be prompted for any missing additional arguments described in "import-areas args". When using the `import-areas` action, you will be prompted for any missing additional arguments described in "import-areas args".
@ -138,7 +140,7 @@ general information:
The `scan` action can (re)scan a file area for new entries as well as update (`--update`) existing entry records (description, etc.). When scanning, a valid area tag must be specified. Optionally, storage tag may also be supplied in order to scan a specific filesystem location using the `@the_storage_tag` syntax. If a [GLOB](http://man7.org/linux/man-pages/man7/glob.7.html) is supplied as the last argument, only file entries with filenames matching will be processed. The `scan` action can (re)scan a file area for new entries as well as update (`--update`) existing entry records (description, etc.). When scanning, a valid area tag must be specified. Optionally, storage tag may also be supplied in order to scan a specific filesystem location using the `@the_storage_tag` syntax. If a [GLOB](http://man7.org/linux/man-pages/man7/glob.7.html) is supplied as the last argument, only file entries with filenames matching will be processed.
##### Examples ##### Examples
Performing a quick scan of a specific area's storage location ("retro_warez", "retro_warez_games) matching only *.zip extentions: Performing a quick scan of a specific area's storage location ("retro_warez", "retro_warez_games) matching only *.zip extensions:
``` ```
$ ./oputil.js fb scan --quick retro_warez@retro_warez_games *.zip` $ ./oputil.js fb scan --quick retro_warez@retro_warez_games *.zip`
``` ```

View file

@ -1,5 +1,111 @@
--- ---
layout: page layout: page
title: General title: General Art Information
--- ---
General art lives in the `art/general` directory. 'General' art is ANSI you want to stay consistent across themes, such as a welcome ANSI or a rotation of logoff ANSIs. ## General Art Information
One of the most basic elements of BBS customization is through it's artwork. ENiGMA½ supports a variety of ways to select, display, and manage art.
As a general rule, art files live in one of two places:
1. The `art/general` directory. This is where you place command non-themed art files.
2. Within a theme such as `art/themes/super_fancy_theme`.
### Menu Entries
While art can be displayed programmatically such as from a custom module, the most basic and common form is via `menu.hjson` entries. This usually falls into one of two forms: a "standard" entry where a single `art` spec is utilized or a entry for a custom module where multiple pieces are declared and used. The second style usually takes the form of a `config.art` block with two or more entries.
A menu entry has a few elements that control how art is choosen and displayed. First, the `art` *spec* tells teh system how to look for the art asset. Second, the `config` block can further control aspecs of lookup and display:
| Item | Description|
|------|------------|
| `font` | Sets the [SyncTERM](http://syncterm.bbsdev.net/) style font to use when displaying this art. If unset, the system will use the art's embedded [SAUCE](http://www.acid.org/info/sauce/sauce.htm) record if present or simply use the current font. See Fonts below. |
| `pause` | If set to `true`, pause after displaying. |
| `baudRate` | Set a [SyncTERM](http://syncterm.bbsdev.net/) style emulated baud rate when displaying this art. In other words, slow down the display. |
| `cls` | Clear the screen before display if set to `true`. |
| `random` | Set to `false` to explicitly disable random lookup. |
| `types` | An optional array of types (aka file extensions) to consider for lookup. For example : `[ '.ans', '.asc' ]` |
| `readSauce` | May be set to `false` if you need to explictly disable SAUCE support. |
#### Art Spec
It was mentioned that the `art` member is a *spec*. The value of a `art` member controls how the system looks for an asset. The following forms are supported:
* `FOO`: The system will look for `FOO.ANS`, `FOO.ASC`, `FOO.TXT`, etc. using the default search path. Unless otherwise specified if `FOO1.ANS`, `FOO2.ANS`, and so on exist, a random selection will be made.
* `FOO.ANS`: By specifying an extension, only that type will be searched for.
* `rel/path/to/BAR.ANS`: Only match a path (relative to the system's `art` directory).
* `/path/to/BAZ.ANS`: Exact path only.
ENiGMA½ uses a fallback system for art selection. When a menu entry calls for a piece of art, the following search is made:
1. If a direct or relative path is supplied, look there first.
2. In the users current theme directory.
3. In the system default theme directory.
4. In the `art/general` directory.
#### SyncTERM Style Fonts
ENiGMA½ can set a [SyncTERM](http://syncterm.bbsdev.net/) style font for art display. This is supported by many popular BBS terminals besides just SyncTERM and is common for displaying Amiga style fonts for example. The system will use the `font` specifier or look for a font declared in an artworks SAUCE record (unless `readSauce` is `false`).
The most common fonts are probably as follows:
* `cp437`
* `c64_upper`
* `c64_lower`
* `c128_upper`
* `c128_lower`
* `atari`
* `pot_noodle`
* `mo_soul`
* `microknight_plus`
* `topaz_plus`
* `microknight`
* `topaz`
Other fonts fonts also available:
* `cp1251`
* `koi8_r`
* `iso8859_2`
* `iso8859_4`
* `cp866`
* `iso8859_9`
* `haik8`
* `iso8859_8`
* `koi8_u`
* `iso8859_15`
* `iso8859_4`
* `koi8_r_b`
* `iso8859_4`
* `iso8859_5`
* `ARMSCII_8`
* `iso8859_15`
* `cp850`
* `cp850`
* `cp885`
* `cp1251`
* `iso8859_7`
* `koi8-r_c`
* `iso8859_4`
* `iso8859_1`
* `cp866`
* `cp437`
* `cp866`
* `cp885`
* `cp866_u`
* `iso8859_1`
* `cp1131`
See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information.
#### SyncTERM Style Baud Rates
The `baudRate` member can set a [SyncTERM](http://syncterm.bbsdev.net/) style emulated baud rate. May be `300`, `600`, `1200`, `2400`, `4800`, `9600`, `19200`, `38400`, `57600`, `76800`, or `115200`. A value of `ulimited`, `off`, or `0` resets (disables) the rate. See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information.
## Common Example
```hjson
fullLogoffSequenceRandomBoardAd: {
art: OTHRBBS
desc: Logging Off
next: logoff
config: {
baudRate: 57600
pause: true
cls: true
}
}
```

View file

@ -72,13 +72,14 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et
| `SP` | Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.) | | `SP` | Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.) |
Some additional special case codes also exist: Some additional special case codes also exist:
| Code | Description | | Code | Description |
|--------|--------------| |--------|--------------|
| `CF##` | Moves the cursor position forward _##_ characters | | `CF##` | Moves the cursor position forward _##_ characters |
| `CB##` | Moves the cursor position back _##_ characters | | `CB##` | Moves the cursor position back _##_ characters |
| `CU##` | Moves the cursor position up _##_ characters | | `CU##` | Moves the cursor position up _##_ characters |
| `CD##` | Moves the cursor position down _##_ characters | | `CD##` | Moves the cursor position down _##_ characters |
| `XY` | A special code that may be utilized for placement identification when creating menus or to extend an otherwise empty space in an art file down the screen. | `XY` | A special code that may be utilized for placement identification when creating menus or to extend an otherwise empty space in an art file down the screen. |
## Views ## Views
@ -104,7 +105,7 @@ see additional information.
## Properties & Theming ## Properties & Theming
Predefined MCI codes and other Views can have properties set via `menu.hjson` and further *themed* via `theme.hjson`. Predefined MCI codes and other Views can have properties set via `menu.hjson` and further *themed* via `theme.hjson`. See [Themes](themes.md) for more information on this subject.
### Common Properties ### Common Properties

View file

@ -2,28 +2,131 @@
layout: page layout: page
title: Themes title: Themes
--- ---
# Creating Your Own ## Themes
:warning: ***IMPORTANT!*** It is recommended you don't make any customisations to the included ENiGMA½ comes with an advanced theming system allowing system operators to highly customize the look and feel of their boards. A given installation can have as many themes as you like for your users to choose from.
`luciano_blocktronics' theme. Create your own and make changes to that instead:
## General Information
Themes live in `art/themes/`. Each theme (and thus it's *theme ID*) is a directory within the `themes` directory. The theme itself is simply a collection of art files, and a `theme.hjson` file that further defines layout, colors & formatting, etc. ENiGMA½ comes with a default theme by [Luciano Ayres](http://blocktronics.org/tag/luciano-ayres/) of [Blocktronics](http://blocktronics.org/) called Mystery Skull. This theme is in `art/themes/luciano_blocktronics`, and thus it's *theme ID* is `luciano_blocktronics`.
## Art
For information on art files, see [General Art Information](general.md). TL;DR: In general, to theme a piece of art, create a version of it in your themes directory.
:information_source: Remember that by default, the system will allow for randomly selecting art (in one of the directories mentioned above) by numbering it: `FOO1.ANS`, `FOO2.ANS`, etc.!
## Theme Sections
Themes are some important sections to be aware of:
| Config Item | Description |
|-------------|----------------------------------------------------------|
| `info` | This section describes the theme. |
| `customization` | The beef! |
### Info Block
The `info` configuration block describes the theme itself.
| Item | Required | Description |
|-------------|----------|----------------------------------------------------------|
| `name` | :+1: | Name of the theme. Be creative! |
| `author` | :+1: | Author of the theme/artwork. |
| `group` | :-1: | Group/affils of author. |
| `enabled` | :-1: | Boolean of enabled state. If set to `false`, this theme will not be available to your users. If a user currently has this theme selected, the system default will be selected for them at next login. |
### Customization Block
The `customization` block in is itself broken up into major parts:
| Item | Description |
|-------------|---------------------------------------------------|
| `defaults` | Default values to use when this theme is active. These values override system defaults, but can still be overridden themselves in specific areas of your theme. |
| `menus` | The bulk of what you theme in the system will be here. Any menu (that is, anything you find in `menu.hjson`) can be tweaked. |
| `prompts` | Similar to `menus`, this file themes prompts found in `prompts.hjson`. |
#### Defaults
| Item | Description |
|-------------|---------------------------------------------------|
| `passwordChar` | Character to display in password fields. Defaults to `*` |
| `dateFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for dates. |
| `timeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for times. |
| `dateTimeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for date/time combinations. |
Example:
```hjson
defaults: {
dateTimeFormat: {
short: MMM Do h:mm a
}
}
```
#### Menus Block
Each *key* in the `menus` block matches up with a key found in your `menu.hjson`. For example, consider a `matrix` menu defined in `menu.hjson`. In addition to perhaps providing a `MATRIX.ANS` in your themes directory, you can also theme other parts of the menu via a `matrix` entry in `theme.hjson`.
Major areas to override/theme:
* `config`: Override and/or provide additional theme information over that found in the `menu.hjson`'s entry. Common entries here are for further overriding date/time formats, and custom range info formats (`<someFormName>InfoFormat<num>`). See Entry Formatting in [MCI Codes](mci.md) and Custom Range Info Formatting below.
* `mci`: Set per-MCI code properties such as `height`, `width`, text styles, etc. See [MCI Codes](mci.md) for a more information.
Two formats for `mci` blocks are allowed:
* Verbose where a form ID(s) are supplied.
* Shorthand if only a single/first form is needed.
Example: Verbose `mci` with form IDs:
```hjson
newUserFeedbackToSysOp: {
0: {
mci: {
TL1: { width: 19, textOverflow: "..." }
ET2: { width: 19, textOverflow: "..." }
ET3: { width: 19, textOverflow: "..." }
}
}
1: {
mci: {
MT1: { height: 14 }
}
}
}
```
Example: Shorthand `mci` format:
```hjson
matrix: {
mci: {
VM1: {
itemFormat: "|03{text}"
focusItemFormat: "|11{text!styleFirstLower}"
}
}
}
```
##### Custom Range Info Formatting
Many modules support "custom range" MCI items. These are MCI codes that are left to the user to define using a format object specific to the module. For example, consider the `msg_area_list` module: This module sets MCI codes 10+ (`%TL10`, `%TL11`, etc.) as "custom range". When theming you can place these MCI codes in your artwork then define the format in `theme.hjson`:
```hjson
messageAreaChangeCurrentArea: {
config: {
areaListInfoFormat10: "|15{name}|07: |03{desc}"
}
}
```
## Creating Your Own
:warning: ***IMPORTANT!*** It is recommended you don't make any customisations to the included `luciano_blocktronics' theme. Instead, create your own and make changes to that instead:
1. Copy `/art/themes/luciano_blocktronics` to `art/themes/your_board_theme` 1. Copy `/art/themes/luciano_blocktronics` to `art/themes/your_board_theme`
2. Update the `info` block at the top of the theme.hjson file: 2. Update the `info` block at the top of the theme.hjson file:
``` hjson ``` hjson
info: { info: {
name: Awesome Theme name: Awesome Theme
author: Cool Artist author: Cool Artist
group: Sick Group group: Sick Group
enabled: true enabled: true // default
} }
``` ```
3. Specify it in the `defaults` section of `config.hjson`. The name supplied should match the name of the 3. If desired, you may make this the default system theme in `config.hjson` via `theme.default`. `theme.preLogin` may be set if you want this theme used for pre-authenticated users. Both of these values also accept `*` if you want the system to radomly pick.
directory you created in step 1:
``` hjson ``` hjson
defaults: { theme: {
theme: your_board_theme default: your_board_theme
} preLogin: *
}
``` ```
# General Theme Info

View file

@ -61,6 +61,7 @@ The following touch points exist in the system. Many more are planned:
* Message conferences and areas * Message conferences and areas
* File base areas * File base areas
* Menus within `menu.hjson`. See [menu.hjson](menu-hjson.md). * Menus within `menu.hjson`. See [Menu HJSON](menu-hjson.md).
See the specific areas documentation for information on available ACS checks. See the specific areas documentation for information on available ACS checks.

View file

@ -1,12 +1,14 @@
--- ---
layout: page layout: page
title: config.hjson title: System Configuration
--- ---
## System Configuration ## System Configuration
The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config/config.hjson` though you can override the `config.hjson` location with the `--config` parameter when invoking `main.js`. Values found in `core/config.js` may be overridden by simply providing the object members you wish replace. The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config/config.hjson` though you can override the `config.hjson` location with the `--config` parameter when invoking `main.js`. Values found in `core/config.js` may be overridden by simply providing the object members you wish replace.
See also [HJSON General Information](hjson.md) for more information on the HJSON format.
### Creating a Configuration ### Creating a Configuration
Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your enigma-bbs root directory: Your initial configuration skeleton should be created using the `oputil.js` command line utility. From your enigma-bbs root directory:
``` ```
./oputil.js config new ./oputil.js config new
``` ```
@ -28,10 +30,21 @@ general: {
} }
``` ```
(Note the very slightly different syntax. **You can use standard JSON if you wish**) (Note the very slightly [HJSON](hjson.md) different syntax. **You can use standard JSON if you wish!**)
While not everything that is available in your `config.hjson` file can be found defaulted in `core/config.js`, a lot is. [Poke around and see what you can find](https://github.com/NuSkooler/enigma-bbs/blob/master/core/config.js)! While not everything that is available in your `config.hjson` file can be found defaulted in `core/config.js`, a lot is. [Poke around and see what you can find](https://github.com/NuSkooler/enigma-bbs/blob/master/core/config.js)!
### Configuration Sections
Below is a list of various configuration sections. There are many more, but this should get you started:
* [ACS](acs.md)
* [Archivers](archivers.md): Set up external archive utilities for handling things like ZIP, ARJ, RAR, and so on.
* [Email](email.md): System email support.
* [Event Scheduler](event-scheduler.md): Set up events as you see fit!
* [File Base](/docs/filebase/index.md)
* [File Transfer Protocols](file-transfer-protocols.md): Oldschool file transfer protocols such as X/Y/Z-Modem!
* [Message Areas](/docs/messageareas/configuring-a-message-area.md), [Networks](/docs/messageareas/message-networks.md), [NetMail](/docs/messageareas/netmail.md), etc.
### A Sample Configuration ### A Sample Configuration
Below is a **sample** `config.hjson` illustrating various (but certainly not all!) elements that can be configured / tweaked. Below is a **sample** `config.hjson` illustrating various (but certainly not all!) elements that can be configured / tweaked.

View file

@ -2,26 +2,13 @@
layout: page layout: page
title: Creating Initial Config Files title: Creating Initial Config Files
--- ---
Configuration files in ENiGMA½ are simple UTF-8 encoded [HJSON](http://hjson.org/) files. HJSON is just Configuration files in ENiGMA½ are simple UTF-8 encoded [HJSON](http://hjson.org/) files. HJSON is just like JSON but simplified and much more resilient to human error.
like JSON but simplified and much more resilient to human error.
## config.hjson ## Initial Configuration
Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your enigma-bbs root directory:
enigma-bbs root directory: ```bash
```
./oputil.js config new ./oputil.js config new
``` ```
You will be asked a series of questions to create an initial configuration. You will be asked a series of questions to create an initial configuration, which will be saved to `/enigma-bbs-install-path/config/config.hjson`. This will also produce `config/<bbsName>-menu.hjson` and `config/<bbsName>-prompt.hjson` files (where `<bbsName>` is replaced by the name you provided in the steps above). See [Menu HJSON](menu-hjson.md) and [Prompt HJSON](prompt-hjson.md) for more information.
## menu.hjson and prompt.hjson
Create your own copy of `/config/menu.hjson` and `/config/prompt.hjson`, and specify it in the
`general` section of `config.hjson`:
````hjson
general: {
menuFile: my-menu.hjson
promptFile: my-prompt.hjson
}
````

View file

@ -2,16 +2,18 @@
layout: page layout: page
title: Email title: Email
--- ---
ENiGMA½ uses email to send password reset information to users. For it to work, you need to provide valid SMTP ## Email Support
config in your [config.hjson]({{ site.baseurl }}{% link configuration/config-hjson.md %}) ENiGMA½ uses email to send password reset information to users. For it to work, you need to provide valid [Nodemailer](https://nodemailer.com/about/) compatible `email` block in your [config.hjson]({{ site.baseurl }}{% link configuration/config-hjson.md %}). Nodemailer supports SMTP in addition to many pre-defined services for ease of use. The `transport` block within `email` must be Nodemailer compatible.
## SMTP Services Additional email support will come in the near future.
If you don't have an SMTP server to send from, [Sendgrid](https://sendgrid.com/) provide a reliable free ## Services
service.
## Example SMTP Configuration If you don't have an SMTP server to send from, [Sendgrid](https://sendgrid.com/) and [Zoho](https://www.zoho.com/mail/) both provide reliable and free services.
## Example Configurations
Example 1 - SMTP:
```hjson ```hjson
email: { email: {
defaultFrom: sysop@bbs.awesome.com defaultFrom: sysop@bbs.awesome.com
@ -27,3 +29,21 @@ email: {
} }
} }
``` ```
Example 2 - Zoho
```hjson
email: {
defaultFrom: sysop@bbs.awesome.com
transport: {
service: Zoho
auth: {
user: noreply@bbs.awesome.com
pass: yuspymypass
}
}
}
```
## Lockout Reset
If email is available on your system and you allow email-driven password resets, you may elect to allow unlocking accounts at the time of a password reset. This is controlled by the `users.unlockAtEmailPwReset` configuration option. If an account is locked due to too many failed login attempts, a user may reset their password to remedy the situation themselves.

View file

@ -0,0 +1,79 @@
---
layout: page
title: Event Scheduler
---
## Event Scheduler
The ENiGMA½ scheduler allows system operators to configure arbitrary events that can can fire based on date and/or time, or by watching for changes in a file. Events can kick off internal handlers, custom modules, or binaries & scripts.
## Scheduling Events
To create a scheduled event, create a new configuration block in `config.hjson` under `eventScheduler.events`.
Events can have the following members:
| Item | Required | Description |
|------|----------|-------------|
| `schedule` | :+1: | A [Later style](https://bunkat.github.io/later/parsers.html#text) parsable schedule string such as `at 4:00 am`, or `every 24 hours`. Can also be (or contain) an `@watch` clause. See **Schedules** below for details. |
| `action` | :+1: | Action to perform when the schedule is triggered. May be an `@method` or `@execute` spec. See **Actions** below. |
| `args` | :-1: | An array of arguments to pass along to the method or binary specified in `action`. |
### Schedules
As mentioned above, `schedule` may contain a [Later style](https://bunkat.github.io/later/parsers.html#text) parsable schedule string and/or an `@watch` clause.
`schedule` examples:
* `every 2 hours`
* `on the last day of the week`
* `after 12th hour`
An `@watch` clause monitors a specified file for changes and takes the following form: `@watch:<path>` where `<path>` is a fully qualified path.
:information_source: If you would like to have a schedule **and** watch a file for changes, place the `@watch` clause second and seperated with the word `or`. For example: `every 24 hours or @watch:/path/to/somefile.txt`.
### Actions
Events can kick off actions by calling a method (function) provided by the system or custom module in addition to executing arbritary binaries or scripts.
#### Methods
An action with a `@method` can take the following forms:
* `@method:/full/path/to/module.js:methodName`: Executes `methodName` at `/full/path/to/module.js`.
* `@method:rel/path/to/module.js:methodName`: Executes `methodName` using the *relative* path `rel/path/to/module.js`. Paths for `@method` are relative to the ENiGMA½ installation directory.
Methods are passed any supplied `args` in the order they are provided.
##### Method Signature
To create your own method, simply `export` a method with the following signature: `(args, callback)`. Methods are executed asynchronously.
Example:
```javascript
// my_custom_mod.js
exports.myCustomMethod = (args, cb) => {
console.log(`Hello, ${args[0]}!`);
return cb(null);
}
```
#### Executables
When using the `@execute` action, a binary or script can be executed. A full path or just the binary name is acceptable. If using the form without a path, the binary much be in ENiGMA½'s `PATH`.
Examples:
* `@execute:/usr/bin/foo`
* `@execute:foo`
Just like with methods, any supplied `args` will be passed along.
## Example Entries
Post a message to supplied networks every Monday night using the message post mod (see modding):
```hjson
eventScheduler: {
events: {
enigmaAdToNetworks: {
schedule: at 10:35 pm on Mon
action: @method:mods/message_post_evt/message_post_evt.js:messagePostEvent
args: [
"fsx_bot"
"/home/enigma-bbs/ad.asc"
]
}
}
}
```

View file

@ -0,0 +1,69 @@
---
layout: page
title: HJSON General Information
---
## JSON for Humans!
HJSON is the configuration file format used by ENiGMA½ for [System Configuration](config-hjson.md), [Menus](menu-hjson.md), [Prompts](prompt-hjson.md), etc. [HJSON](https://hjson.org/) is is [JSON](https://json.org/) for humans!
For those completely unfamiliar, JSON stands for JavaScript Object Notation. But don't let that scare you! JSON is simply a text file format with a bit of structure ― kind of like a fancier INI file. HJSON on the other hand as mentioned previously, is JSON for humans. That is, it has the following features and more:
* More resilient to syntax errors such as missing a comma
* Strings generally do not need to be quoted. Multi-line strings are also supported!
* Comments are supported (JSON doesn't allow this!): `#`, `//` and `/* ... */` style comments are allowed.
* Keys never need to be quoted
* ...much more! See [the official HJSON website](https://hjson.org/).
## Terminology
Through the documentation, some terms regarding HJSON and configuration files will be used:
* `config.hjson`: Refers to `/path/to/enigma-bbs/config/config.hjson`. See [System Configuration](config-hjson.md).
* `menu.hjson`: Refers to `/path/to/enigma-bbs/config/<yourBBSName>-menu.hjson`. See [Menus](menu-hjson.md).
* `prompt.hjson`: Refers to `/path/to/enigma-bbs/config/<yourBBSName>-prompt.hjson`. See [Prompts](prompt-hjson.md).
* Configuration *key*: Elements in HJSON are name-value pairs where the name is the *key*. For example, provided `foo: bar`, `foo` is the key.
* Configuration *section* or *block* (also commonly called an "Object" in code): This is referring to a section in a HJSON file that starts with a *key*. For example:
```hjson
someSection: {
foo: bar
}
```
Note that `someSection` is the configuration *section* (or *block*) and `foo: bar` is within it.
## Editing HJSON
HJSON is a text file format, and ENiGMA½ configuration files **should always be saved as UTF-8**.
It is **highly** recommended to use a text editor that has HJSON support. A few (but not all!) examples include:
* Sublime Text 3 via the `sublime-hjson` package.
* Visual Studio code via the `vscode-hjson` plugin.
* Notepad++ via the `npp-hjson` plugin.
See https://hjson.org/users.html for more more editors & plugins.
### Hot-Reload A.K.A. Live Editing
ENiGMA½'s configuration, menu, and theme files can edited while your BBS is running. When a file is saved, it is hot-reloaded into the running system. If users are currently connected and you change a menu for example, the next reload of that menu will show the changes.
### CaSe SeNsiTiVE
Configuration keys are **case sensitive**. That means if a configuration key is `boardName` for example, `boardname`, or `BOARDNAME` **will not work**.
### Escaping
Some values need escaped. This is especially important to remember on Windows machines where file paths contain backslashes (`\`). To specify a path to `C:\foo\bar\baz.exe` for example, an entry may look like this in your configuration file:
```hjson
something: {
path: "C:\\foo\\bar\\baz.exe" // note the extra \'s!
}
```
## Tips & Tricks
### JSON Compatibility
Remember that standard JSON is fully compatible with HJSON. If you are more comfortable with JSON (or have an editor that works with JSON that you prefer) simply convert your config file(s) to JSON and use that instead!
HJSON can be converted to JSON with the `hjson` CLI:
```bash
cd /path/to/enigma-bbs
cp ./config/config.hjson ./config/config.hjson.backup
./node_modules/hjson/bin/hjson ./config/config.hjson.backup -j > ./config/config.hjson
```
You can always convert back to HJSON by omitting `-j` in the command above.
### oputil
You can easily dump out your current configuration in a pretty-printed style using oputil: ```./oputil.js config cat```

View file

@ -1,20 +1,11 @@
--- ---
layout: page layout: page
title: menu.hjson title: Menus
--- ---
:warning: ***IMPORTANT!*** Before making any customisations, create your own copy of `/config/menu.hjson`, and specify it in the `general` section of `config.hjson`: ## Menus
The core of a ENiGMA½ based BBS is `menu.hjson`. Note that when `menu.hjson` is referenced, we're actually talking about `config/yourboardname-menu.hjson` or similar. This file determines the menus (or screens) a user can see, the order they come in and how they interact with each other, ACS configuration, etc. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. See [HJSON General Information](hjson.md) for more information.
````hjson Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to:
general: {
menuFile: yourboardname.hjson
}
````
This document and others will refer to `menu.hjson`. This should be seen as an alias to `yourboardname.hjson`
## The Basics
Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format.
Entries in `menu.hjson` are objects or _sections_ defining a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to:
* Classical Main, Messages, and File menus * Classical Main, Messages, and File menus
* Art file display * Art file display
@ -23,21 +14,47 @@ Entries in `menu.hjson` are objects or _sections_ defining a menu. A menu in thi
Menu entries live under the `menus` section of `menu.hjson`. The *key* for a menu is it's name that can be referenced by other menus and areas of the system. Menu entries live under the `menus` section of `menu.hjson`. The *key* for a menu is it's name that can be referenced by other menus and areas of the system.
## Common Menu Entry Members ## Common Menu Entry Members
* `desc`: A friendly description that can be found in places such as "Who's Online" or the `%MD` MCI code. Below is a table of **common** menu entry members. These members apply to most entries, though entries that are backed by a specialized module (ie: `module: bbs_list`) may differ. See documentation for the module in question for particulars.
* `art`: An art file specification.
* `next`: Specifies the next menu to go to next. Can be explicit or an array of possibilites dependent on ACS. See **Flow Control** in the **ACS Checks** section below. | Item | Description |
* `prompt`: Specifies a prompt, by name, to use along with this menu. |--------|--------------|
* `form`: Defines one or more forms available on this menu. | `desc` | A friendly description that can be found in places such as "Who's Online" or wherever the `%MD` MCI code is used. |
* `submit`: Defines a submit handler when using `prompt`. | `art` | An art file *spec*. See [General Art Information](/docs/art/general.md). |
* `config`: May contain any of the following standard configuration members in addition to per-module defined types (see appropriate module for more information): | `next` | Specifies the next menu entry to go to next. Can be explicit or an array of possibilities dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. |
* `cls`: If `true` the screen will be cleared before showing this menu. | `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in `prompt.hjson`. |
* `pause`: If `true` a pause will occur after showing this menu. Useful for simple menus such as displaying art or status screens. | `submit` | Defines a submit handler when using `prompt`.
* `nextTimeout`: Sets the number of **milliseconds** before the system will automatically advanced to the `next` menu. | `form` | An object defining one or more *forms* available on this menu. |
* `baudRate`: Sets the SyncTERM style emulated baud rate. May be `300`, `600`, `1200`, `2400`, `4800`, `9600`, `19200`, `38400`, `57600`, `76800`, or `115200`. A value of `ulimited`, `off`, or `0` resets (disables) the rate. See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. | `module` | Sets the module name to use for this menu. |
* `font`: Sets the SyncTERM style font. May be one of the following: `cp437`, `cp1251`, `koi8_r`, `iso8859_2`, `iso8859_4`, `cp866`, `iso8859_9`, `haik8`, `iso8859_8`, `koi8_u`, `iso8859_15`, `iso8859_4`, `koi8_r_b`, `iso8859_4`, `iso8859_5`, `ARMSCII_8`, `iso8859_15`, `cp850`, `cp850`, `cp885`, `cp1251`, `iso8859_7`, `koi8-r_c`, `iso8859_4`, `iso8859_1`, `cp866`, `cp437`, `cp866`, `cp885`, `cp866_u`, `iso8859_1`, `cp1131`, `c64_upper`, `c64_lower`, `c128_upper`, `c128_lower`, `atari`, `pot_noodle`, `mo_soul`, `microknight_plus`, `topaz_plus`, `microknight`, `topaz`. See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. | `config` | An object containing additional configuration. See **Config Block** below. |
### Config Block
The `config` block for a menu entry can contain common members as well as a per-module (when `module` is used) settings.
| Item | Description |
|------|-------------|
| `cls` | If `true` the screen will be cleared before showing this menu. |
| `pause` | If `true` a pause will occur after showing this menu. Useful for simple menus such as displaying art or status screens. |
| `nextTimeout` | Sets the number of **milliseconds** before the system will automatically advanced to the `next` menu. |
| `baudRate` | See baud rate information in [General Art Information](/docs/art/general.md). |
| `font` | Sets a SyncTERM style font to use when displaying this menus `art`. See font listing in [General Art Information](/docs/art/general.md). |
| `menuFlags` | An array of menu flag(s) controlling menu behavior. See **Menu Flags** below.
#### Menu Flags
The `menuFlags` field of a `config` block can change default behavior of a particular menu.
| Flag | Description |
|------|-------------|
| `noHistory` | Prevents the menu from remaining in the menu stack / history. When this flag is set, when the **next** menu falls back, this menu will be skipped and the previous menu again displayed instead. Example: menuA -> menuB(noHistory) -> menuC: Exiting menuC returns the user to menuA. |
| `popParent` | When *this* menu is exited, fall back beyond the parent as well. Often used in combination with `noHistory`. |
| `forwardArgs` | If set, when the next menu is entered, forward any `extraArgs` arguments to *this* menu on to it. |
## Forms ## Forms
TODO ENiGMA½ uses a concept of *forms* in menus. A form is a collection of associated *views*. Consider a New User Application using the `nua` module: The default implementation utilizes a single form with multiple EditTextView views, a submit button, etc. Forms are identified by number starting with `0`. A given menu may have mutiple forms (often associated with different states or screens within the menu).
Menus may also support more than one layout type by using a *MCI key*. A MCI key is a alpha-numerically sorted key made from 1:n MCI codes. This lets the system choose the appropriate set of form(s) based on theme or random art. An example of this may be a matrix menu: Perhaps one style of your matrix uses a vertical light bar (`VM` key) while another uses a horizontal (`HM` key). The system can discover the correct form to use by matching MCI codes found in the art to that of the available forms defined in `menu.hjson`.
For more information on views and associated MCI codes, see [MCI Codes](/docs/art/mci.md).
## Submit Handlers ## Submit Handlers
TODO TODO
@ -49,67 +66,69 @@ Let's look a couple basic menu entries:
telnetConnected: { telnetConnected: {
art: CONNECT art: CONNECT
next: matrix next: matrix
options: { nextTimeout: 1500 } config: { nextTimeout: 1500 }
} }
``` ```
The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in the Telnet server's config). The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in the Telnet server's config). The entry sets up a few things:
* A `art` spec of `CONNECT`. (See [General Art Information](/docs/art/general.md)).
An art pattern of `CONNECT` is set telling the system to look for `CONNECT<n>.*` where `<n>` represents a optional integer in art files to cause randomness, e.g. `CONNECT1.ANS`, `CONNECT2.ANS`, and so on. If desired, you can also be explicit by supplying a full filename with an extention such as `CONNECT.ANS`. * A `next` entry up the next menu, by name, in the stack (`matrix`) that we'll go to after `telnetConnected`.
* An `config` block containing a single `nextTimeout` field telling the system to proceed to the `next` (`matrix`) entry automatically after 1500ms.
The entry `next` sets up the next menu, by name, in the stack (`matrix`) that we'll go to after `telnetConnected`.
Finally, an `options` object may contain various common options for menus. In this case, `nextTimeout` tells the system to proceed to the `next` entry automatically after 1500ms.
Now let's look at `matrix`, the `next` entry from `telnetConnected`: Now let's look at `matrix`, the `next` entry from `telnetConnected`:
```hjson ```hjson
matrix: { matrix: {
art: matrix art: MATRIX
desc: Login Matrix desc: Login Matrix
form: { form: {
0: { 0: {
VM: { //
mci: { // Here we have a MCI key of "VM". In this case we could
VM1: { // omit this level since no other keys are present.
submit: true //
focus: true VM: {
items: [ "login", "apply", "log off" ] mci: {
argName: matrixSubmit VM1: {
submit: true
focus: true
items: [ "login", "apply", "log off" ]
argName: matrixSubmit
}
}
submit: {
*: [
{
value: { matrixSubmit: 0 }
action: @menu:login
}
{
value: { matrixSubmit: 1 },
action: @menu:newUserApplication
}
{
value: { matrixSubmit: 2 },
action: @menu:logoff
}
]
}
} }
//
// If we wanted, we could declare a "HM" MCI key block here.
// This would allow a horizontal matrix style when the matrix art
// loaded contained a %HM code.
//
} }
submit: {
*: [
{
value: { matrixSubmit: 0 }
action: @menu:login
}
{
value: { matrixSubmit: 1 },
action: @menu:newUserApplication
}
{
value: { matrixSubmit: 2 },
action: @menu:logoff
}
]
}
}
}
} }
} }
``` ```
In the above entry, you'll notice `form`. This defines a form(s) object. In this case, a single form In the above entry, you'll notice `form`. This defines a form(s) object. In this case, a single form by ID of `0`. The system is then told to use a block only when the resulting art provides a `VM` (*VerticalMenuView*) MCI entry. Some other bits about the form:
by ID of `0`. The system is then told to use a block only when the resulting art provides a `VM`
(*VerticalMenuView*) MCI entry. `VM1` is then setup to `submit` and start focused via `focus: true`
as well as have some menu entries ("login", "apply", ...) defined. We provide an `argName` for this
action as `matrixSubmit`.
The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`). * `VM1` is then setup to `submit` and start focused via `focus: true` as well as have some menu entries ("login", "apply", ...) defined. We provide an `argName` of `matrixSubmit` for this element view.
Upon submit, the first match will be executed. For example, if the user selects "login", the first entry * The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`).
with a value of `{ matrixSubmit: 0 }` will match causing `action` of `@menu:login` to be executed (go * Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ matrixSubmit: 0 }` will match (due to 0 being the first index in the list and `matrixSubmit` being the arg name in question) causing `action` of `@menu:login` to be executed (go to `login` menu).
to `login` menu).
## ACS Checks ## ACS Checks
Menu modules can check user ACS in order to restrict areas and perform flow control. See [ACS](acs.md) for available ACS syntax. Menu modules can check user ACS in order to restrict areas and perform flow control. See [ACS](acs.md) for available ACS syntax.
@ -141,4 +160,23 @@ login: {
} }
] ]
} }
``` ```
### Art Asset Selection
Another area in which you can apply ACS in a menu is art asset specs.
```hjson
someMenu: {
desc: Neato Dorito
art: [
{
acs: GM[couriers]
art: COURIERINFO
}
{
// show ie: EVERYONEELSE.ANS to everyone else
art: EVERYONEELSE
}
]
}
```

View file

@ -3,4 +3,6 @@ layout: page
title: prompt.hjson title: prompt.hjson
--- ---
:zap: This page is to describe general information the `prompt.hjson` file. It :zap: This page is to describe general information the `prompt.hjson` file. It
needs fleshing out, please submit a PR if you'd like to help! needs fleshing out, please submit a PR if you'd like to help!
See [HJSON General Information](hjson.md) for more information.

View file

@ -2,5 +2,4 @@
layout: page layout: page
title: SysOp Setup title: SysOp Setup
--- ---
SySop privileges will be granted to the first user to log into a fresh ENiGMA½ installation. SySop privileges will be granted to the first user to log into a fresh ENiGMA½ installation. +ops belong to the `sysop` user group by default.

View file

@ -2,8 +2,8 @@
layout: page layout: page
title: ACS title: ACS
--- ---
## File Base ACS
If no `acs` block is supplied in a file area definition, the following defaults apply to an area: [ACS Codes](/docs/configuration/acs.md) may be used to control acess to File Base areas by specifying an `acs` string in a file area's definition. If no `acs` is supplied in a file area definition, the following defaults apply to an area:
* `read` (list, download, etc.): `GM[users]` * `read` (list, download, etc.): `GM[users]`
* `write` (upload): `GM[sysops]` * `write` (upload): `GM[sysops]`

View file

@ -19,16 +19,21 @@ are OK) for Windows users. Note that you **should only need the Visual C++ compo
* [git](https://git-scm.com/downloads) to check out the ENiGMA source code. * [git](https://git-scm.com/downloads) to check out the ENiGMA source code.
## Node.js ## Node.js
If you're new to Node.js and/or do not care about Node itself and just want to get ENiGMA½ running ### With NVM
these steps should get you going on most \*nix type environments: Node Version Manager (NVM) is an excellent way to install and manage Node.js versions on most UNIX-like environments. [Get the latest version here](https://github.com/creationix/nvm). The install should look something like this:
```bash ```bash
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
nvm install 6
nvm use 6
``` ```
If the above completed without errors, you should now have `nvm`, `node`, and `npm` installed and in your environment. Next, install Node.js with NVM:
```bash
nvm install 10
nvm use 10
nvm alias default 10
```
If the above steps completed without errors, you should now have `nvm`, `node`, and `npm` installed and in your environment.
For Windows nvm-like systems exist ([nvm-windows](https://github.com/coreybutler/nvm-windows), ...) or [just download the installer](https://nodejs.org/en/download/). For Windows nvm-like systems exist ([nvm-windows](https://github.com/coreybutler/nvm-windows), ...) or [just download the installer](https://nodejs.org/en/download/).
@ -41,31 +46,25 @@ git clone https://github.com/NuSkooler/enigma-bbs.git
## Install Node Packages ## Install Node Packages
```bash ```bash
cd enigma-bbs cd enigma-bbs
npm install npm install # yarn also works
``` ```
## Other Recommended Packages ## Other Recommended Packages
ENiGMA BBS makes use of a few packages for archive and legacy protocol support. They're not pre-requisites for running ENiGMA, but without them you'll miss certain functionality. Once installed, they should be made available on your system path.
ENiGMA BBS makes use of a few packages for unarchiving and modem support. They're not pre-requisites for | Package | Description | Debian/Ubuntu Package (APT/DEP) | Red Hat Package (YUM/RPM) | Windows Package |
running ENiGMA, but without them you'll miss certain functionality. Once installed, they should be made
available on your system path.
| Package | Description | Debian/Ubuntu Package (APT/DEP) | Red Hat Package (YUM/RPM) | Windows Package |
|------------|-----------------------------------|--------------------------------------------|---------------------------------------------------|------------------------------------------------------------------| |------------|-----------------------------------|--------------------------------------------|---------------------------------------------------|------------------------------------------------------------------|
| arj | Unpacking arj archives | `arj` | n/a, binaries [here](http://arj.sourceforge.net/) | [ARJ](http://arj.sourceforge.net/) | | arj | Unpacking arj archives | `arj` | n/a, binaries [here](http://arj.sourceforge.net/) | [ARJ](http://arj.sourceforge.net/) |
| 7zip | Unpacking zip, rar, archives | `p7zip-full` | `p7zip-full` | [7-zip](http://www.7-zip.org/) | | 7zip | Unpacking zip, rar, archives | `p7zip-full` | `p7zip-full` | [7-zip](http://www.7-zip.org/) |
| lha | Unpacking lha archives | `lhasa` | n/a, source [here](http://www2m.biglobe.ne.jp/~dolphin/lha/lha.htm) | Unknown | | lha | Unpacking lha archives | `lhasa` | n/a, source [here](http://www2m.biglobe.ne.jp/~dolphin/lha/lha.htm) | Unknown |
| Rar | Unpacking rar archives | `unrar` | n/a, binaries [here](https://www.rarlab.com/download.htm) | Unknown | | Rar | Unpacking rar archives | `unrar` | n/a, binaries [here](https://www.rarlab.com/download.htm) | Unknown |
| lrzsz | sz/rz: X/Y/Z modem support | `lrzsz` | `lrzsz` | Unknown | | lrzsz | sz/rz: X/Y/Z protocol support | `lrzsz` | `lrzsz` | Unknown |
| sexyz | SexyZ modem support | [sexyz](https://l33t.codes/outgoing/sexyz) | [sexyz](https://l33t.codes/outgoing/sexyz) | Available with [Synchronet](http://wiki.synchro.net/install:win) | | sexyz | SexyZ protocol support | [sexyz](https://l33t.codes/outgoing/sexyz) | [sexyz](https://l33t.codes/outgoing/sexyz) | Available with [Synchronet](http://wiki.synchro.net/install:win) |
| exiftool | [ExifTool](https://www.sno.phy.queensu.ca/~phil/exiftool/) | libimage-exiftool-perl | perl-Image-ExifTool | Unknown | exiftool | [ExifTool](https://www.sno.phy.queensu.ca/~phil/exiftool/) | libimage-exiftool-perl | perl-Image-ExifTool | Unknown
| xdms | Unpack/view Amiga DMS | [xdms](http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html) | xdms | Unknown | xdms | Unpack/view Amiga DMS | [xdms](http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html) | xdms | Unknown
## Config Files ## Config Files
You'll need a basic configuration to get started. The main system configuration is handled via `config/config.hjson`. This is an [HJSON](http://hjson.org/) file (compiliant JSON is also OK). See [Configuration](../configuration/) for more information.
You'll need a basic configuration to get started. The main system configuration is handled via
`config/config.hjson`. This is an [HJSON](http://hjson.org/) file (compiliant JSON is also OK).
See [Configuration](../configuration/) for more information.
Use `oputil.js` to generate your **initial** configuration: Use `oputil.js` to generate your **initial** configuration:

View file

@ -2,10 +2,13 @@
layout: page layout: page
title: OS & Hardware Specific Information title: OS & Hardware Specific Information
--- ---
There are multiple ways of installing ENiGMA BBS, depending on your level of experience and desire to do There are multiple ways of installing ENiGMA BBS, depending on your level of experience and desire to do things manually versus have it automated for you.
things manually versus have it automated for you.
| Method | Notes | In general, please see [Installation Methods](installation-methods.md) and [Install Script](install-script.md).
|----------------------------------------|---------------------------------------------------------------------------------------------|
| [Raspberry Pi](rpi) | All Raspberry Pi models work great with ENiGMA½! | Below are some special cases:
| [Windows](windows) | Compatible with all Windows Operating Systems |
| Method | Notes |
|--------|-------|
| [Raspberry Pi](rpi.md) | All Raspberry Pi models work great with ENiGMA½! |
| [Windows](windows.md) | Compatible with all Windows Operating Systems |

View file

@ -36,11 +36,13 @@ ENiGMA½ will run on both 32bit and 64bit Windows. If you want to run 16bit door
*Add 7zip to your path so `7z` can be called from the console *Add 7zip to your path so `7z` can be called from the console
1. Right click `This PC` and Select `Properties` 1. Right click `This PC` and Select `Properties`
2. Go to the `Advanced` Tab and click on `Enviromental Varibles` 2. Go to the `Advanced` Tab and click on `Environment Variables`
3. Select `Path` under `System Varibles` and click `Edit` 3. Select `Path` under `System Variables` and click `Edit`
4. Click `New` and paste the path to 7zip 4. Click `New` and paste the path to 7zip
5. Close your console window and reopen. You can type `7z` to make sure it's working. 5. Close your console window and reopen. You can type `7z` to make sure it's working.
(Please see [Archivers](/docs/archivers.md) for additional archive utilities!)
3. Install [Git](https://git-scm.com/downloads) and optionally [TortoiseGit](https://tortoisegit.org/download/). 3. Install [Git](https://git-scm.com/downloads) and optionally [TortoiseGit](https://tortoisegit.org/download/).
4. Clone ENiGMA½ - browse to the directory you want and run 4. Clone ENiGMA½ - browse to the directory you want and run

View file

@ -4,7 +4,7 @@ title: BSO Import / Export
--- ---
The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss and scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers.ftn_bso`. The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss and scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers.ftn_bso`.
:information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts** to perfrom packet transport! An external utility such as Binkd is required for this! :information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts** to perfrom packet transport! An external [mailer](http://www.filegate.net/bbsmailers.htm) such as [Binkd](https://github.com/pgul/binkd) is required for this!
Let's look at some of the basic configuration: Let's look at some of the basic configuration:
@ -23,7 +23,7 @@ Schedules can be defined for importing and exporting via `import` and `export` u
* `@immediate`: A message will be immediately exported if this trigger is defined in a schedule. Only used for `export`. * `@immediate`: A message will be immediately exported if this trigger is defined in a schedule. Only used for `export`.
* `@watch:/path/to/file`: This trigger watches the path specified for changes and will trigger an import or export when such events occur. Only used for `import`. * `@watch:/path/to/file`: This trigger watches the path specified for changes and will trigger an import or export when such events occur. Only used for `import`.
* Free form text can be things like `at 5:00 pm` or `every 2 hours`. * Free form [Later style](https://bunkat.github.io/later/parsers.html#text) text — can be things like `at 5:00 pm` or `every 2 hours`.
See [Later text parsing documentation](http://bunkat.github.io/later/parsers.html#text) for more information. See [Later text parsing documentation](http://bunkat.github.io/later/parsers.html#text) for more information.
@ -45,14 +45,14 @@ See [Later text parsing documentation](http://bunkat.github.io/later/parsers.htm
## Nodes ## Nodes
The `nodes` section defines how to export messages for one or more uplinks. The `nodes` section defines how to export messages for one or more uplinks.
A node entry starts with a FTN style address (up to 5D) **as a key** in `config.hjson`. This key may contain wildcard(s) for net/zone/node/point/domain. A node entry starts with a [FTN address](http://ftsc.org/docs/old/fsp-1028.001) (up to 5D) **as a key** in `config.hjson`. This key may contain wildcard(s) for net/zone/node/point/domain.
| Config Item | Required | Description | | Config Item | Required | Description |
|------------------|----------|---------------------------------------------------------------------------------| |------------------|----------|---------------------------------------------------------------------------------|
| `packetType` | :-1: | `2`, `2.2`, or `2+`. Defaults to `2+` for modern mailer compatiability | | `packetType` | :-1: | `2`, `2.2`, or `2+`. Defaults to `2+` for modern mailer compatiability. |
| `packetPassword` | :-1: | Password for the packet | | `packetPassword` | :-1: | Optional password for the packet |
| `encoding` | :-1: | Encoding to use for message bodies; Defaults to `utf-8` | | `encoding` | :-1: | Encoding to use for message bodies; Defaults to `utf-8`. |
| `archiveType` | :-1: | Specifies the archive type for ArcMail bundles. Must be a valid archiver name such as `zip` (See archiver configuration) | | `archiveType` | :-1: | Specifies the archive type (by extension) for ArcMail bundles. This should be `zip` for most setups. Other valid examples include `arc`, `arj`, `lhz`, `pak`, `sqz`, or `zoo`. See docs on archiver configuration for more information. |
**Example**: **Example**:
```hjson ```hjson
@ -60,9 +60,9 @@ A node entry starts with a FTN style address (up to 5D) **as a key** in `config.
scannerTossers: { scannerTossers: {
ftn_bso: { ftn_bso: {
nodes: { nodes: {
"21:*": { "21:*": { // wildcard address
packetType: 2+ packetType: 2+
packetPassword: mypass packetPassword: D@TP4SS
encoding: cp437 encoding: cp437
archiveType: zip archiveType: zip
} }
@ -118,4 +118,36 @@ scannerTossers: {
} }
} }
} }
``` ```
## Binkd
Since Binkd is a very common mailer, a few tips on integrating it with ENiGMA½:
### Scheduling Polls
Binkd does not have it's own scheduler. Instead, you'll need to set up an Event Scheduler entry or perhaps a cron job:
First, create a script that runs through all of your uplinks. For example:
```bash
#!/bin/bash
UPLINKS=("21:1/100@fsxnet" "80:774/1@retronet" "10:101/0@araknet")
for uplink in "${UPLINKS[@]}"
do
/usr/local/sbin/binkd -p -P $uplink /home/enigma/xibalba/misc/binkd_xibalba.conf
done
```
Now, create an Event Scheuler entry in your `config.hjson`. As an example:
```hjson
eventScheduler: {
events: {
pollWithBink: {
// execute the script above very 1 hours
schedule: every 1 hours
action: @execute:/path/to/poll_bink.sh
}
}
}
```
## Additional Resources
* [Blog entry on setting up ENiGMA + Binkd on CentOS7](https://l33t.codes/enigma-12-binkd-on-centos-7/). Note that this references an **older version**, so be wary of the `config.hjson` refernces!

View file

@ -0,0 +1,23 @@
---
layout: page
title: File Base Download Manager
---
## File Base Download Manager Module
The `file_base_download_manager` module provides a download queue manager for "legacy" (X/Y/Z-Modem, etc.) downloads. Web (HTTP/HTTPS) download functionality can be optionally available when the web content server is enabled.
## Configuration
### Configuration Block
Available `config` block entries:
* `webDlExpireTimeFormat`: Sets the moment.js style format for web download expiration date/time.
* `fileTransferProtocolSelection`: Overrides the default `fileTransferProtocolSelection` target for a protocol selection menu.
* `emptyQueueMenu`: Overrides the default `fileBaseDownloadManagerEmptyQueue` target for menu to show when the users D/L queue is empty.
### Theming
The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) and MCI 10+ custom fields:
* `fileId`: File ID.
* `areaTag`: Area tag.
* `fileName`: Entry filename.
* `path`: Full file path.
* `byteSize`: Size in bytes of file.
* `webDlLink`: Web download link including [VTX style ANSI ESC sequences](https://raw.githubusercontent.com/codewar65/VTX_ClientServer/master/vtx.txt).
* `webDlExpire`: Expiration date/time for this link. Formatted using `webDlExpireTimeFormat`.

View file

@ -0,0 +1,26 @@
---
layout: page
title: File Base Web Download Manager
---
## File Base Web Download Manager Module
The `file_base_web_download_manager` module provides a download queue manager for web (HTTP/HTTPS) based downloads. This module relies on having the web server enabled at a minimum.
Web downloads can be a convienent way for users to download larger (100+ MiB) files where legacy protocols often have trouble. Additionally, batch downloads can be streamed to users in a single zip archive.
## Configuration
### Configuration Block
Available `config` block entries:
* `webDlExpireTimeFormat`: Sets the moment.js style format for web download expiration date/time.
* `emptyQueueMenu`: Overrides the default `fileBaseDownloadManagerEmptyQueue` target for menu to show when the users D/L queue is empty.
### Theming
The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) and custom range MCI 10+ custom fields:
* `fileId`: File ID.
* `areaTag`: Area tag.
* `fileName`: Entry filename.
* `path`: Full file path.
* `byteSize`: Size in bytes of file.
* `webDlLinkRaw`: Web download link.
* `webDlLink`: Web download link including [VTX style ANSI ESC sequences](https://raw.githubusercontent.com/codewar65/VTX_ClientServer/master/vtx.txt).
* `webDlExpire`: Expiration date/time for this link. Formatted using `webDlExpireTimeFormat`.

View file

@ -14,4 +14,4 @@ The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`):
The following additional MCIs are updated as the user changes selections in the main list: The following additional MCIs are updated as the user changes selections in the main list:
* MCI 2 (ie: `%TL2` or `%M%2`) is updated with the area description. * MCI 2 (ie: `%TL2` or `%M%2`) is updated with the area description.
* MCI 10+ (ie `%TL10`...) are custom ranges updated with the same information available above in `itemFormat`. * MCI 10+ (ie `%TL10`...) are custom ranges updated with the same information available above in `itemFormat`. Use `areaListItemFormat##`.

View file

@ -0,0 +1,29 @@
---
layout: page
title: Set Newscan Date Module
---
## Set Newscan Date Module
The `set_newscan_date` module allows setting newscan dates (aka pointers) for message conferences and areas as well as within the file base. Users can select specific conferences/areas or all (where applicable).
## Configuration
### Configuration Block
Available `config` block entries are as follows:
* `target`: Choose from `message` for message conferences & areas, or `file` for file base areas.
* `scanDateFormat`: Format for scan date. This format must align with the **output** of the MaskEditView (`%ME1`) MCI utilized for input. Defaults to `YYYYMMDD` (which matches mask of `####/##/##`).
### Theming
#### Message Conference & Areas
When `target` is `message`, the following `itemFormat` object is provided to MCI 2 (ie: `%SM2`):
* `conf`: An object containing:
* `confTag`: Conference tag.
* `name`: Conference name. Also available in `{text}`.
* `desc`: Conference description.
* `area`: An object containing:
* `areaTag`: Area tag.
* `name`: Area name. Also available in `{text}`.
* `desc`: Area description.
When dealing with the file base, ENiGMA½ does not currently have the ability to set newscan dates for specific areas. No `%SM2` is used in this case.
### Submit Actions
Submit action should map to `@method:scanDateSubmit` and provide `scanDate` in form data. For message conf/areas (`target` of `message`), `targetSelection` should be also be provided in form data: An index to the selected conf/area.

70
docs/modding/show-art.md Normal file
View file

@ -0,0 +1,70 @@
---
layout: page
title: User List
---
## The Show Art Module
The built in `show_art` module add some advanced ways in which you can configure your system to display art assets beyond what a standard menu entry can provide. For example, based on user selection of a file or message base area.
## Configuration
### Config Block
Available `config` block entries:
* `method`: Set the method in which to show art. See **Methods** below.
* `optional`: Is this art required or optional? If non-optional and we cannot show art based on `method`, it is an error.
* `key`: Used for some `method`s. See **Methods**
### Methods
#### Extra Args
When `method` is `extraArgs`, the module selects an *art spec* from a value found within `extraArgs` that were passed to `show_art` by `key`. Consider the following:
Given an `menu.hjson` entry:
```hjson
showWithExtraArgs: {
module: show_art
config: {
method: extraArgs
key: fooBaz
}
}
```
If the `showWithExtraArgs` menu was entered and passed `extraArgs` as the following:
```json
{
fizzBang : true,
fooBaz : "LOLART"
}
```
...then the system would use the *art spec* of `LOLART`.
#### Area & Conferences
Handy for inserting into File Base, Message Conferences, or Mesage Area selections selections. When `method` is `fileBaseArea`, `messageConf`, or `messageArea` the selected conf/area's associated *art spec* is utilized. Example:
Given a file base entry in `config.hjson`:
```hjson
areas: {
all_ur_base: {
name: All Your Base
desc: chown -r us ./base
art: ALLBASE
}
}
```
A menu entry may look like this:
```hjson
showFileBaseAreaArt: {
module: show_art
config: {
method: fileBaseArea
cls: true
pause: true
menuFlags: [ "popParent", "noHistory" ]
}
}
```
...if the user choose the "All Your Base" area, the *art spec* of `ALLBASE` would be selected and displayed.
The only difference for `messageConf` or `messageArea` methods are where the art is defined (which is always next to the conf or area declaration in `config.hjson`).
While `key` can be overridden, the system uses `areaTag` for message/file area selections, and `confTag` for conference selections by default.

View file

@ -1,17 +0,0 @@
---
layout: page
title: oputil
---
oputil is the ENiGMA½ command line utility for maintaining users, file areas and message areas, as well as
generating your initial ENiGMA½ config.
## File areas
The `oputil.js` +op utilty `fb` command has tools for managing file bases. For example, to import existing
files found within **all** storage locations tied to an area and set tags `tag1` and `tag2` to each import:
```bash
oputil.js fb scan some_area --tags tag1,tag2
```
See `oputil.js fb --help` for additional information.

View file

@ -2,14 +2,21 @@
layout: page layout: page
title: Monitoring Logs title: Monitoring Logs
--- ---
ENiGMA½ does not produce much to stdout. Logs are produced by Bunyan which outputs each entry as a ENiGMA½ does not produce much to stdout. Logs are produced by Bunyan which outputs each entry as a JSON object.
JSON object.
Start by installing bunyan and making it available on your path: Start by installing bunyan and making it available on your path:
npm install bunyan -g ```bash
npm install bunyan -g
```
or with Yarn:
```bash
yarn global add bunyan
```
To tail logs in a colorized and pretty format, issue the following command: To tail logs in a colorized and pretty format, issue the following command:
```bash
tail -F /path/to/enigma-bbs/logs/enigma-bbs.log | bunyan tail -F /path/to/enigma-bbs/logs/enigma-bbs.log | bunyan
```

View file

@ -3,6 +3,8 @@
const client = options.client; const client = options.client;
const user = options.client.user; const user = options.client.user;
const UserProps = require('./user_property.js');
const moment = require('moment'); const moment = require('moment');
function checkAccess(acsCode, value) { function checkAccess(acsCode, value) {
@ -19,7 +21,7 @@
value = [ value ]; value = [ value ];
} }
const userAccountStatus = parseInt(user.properties.account_status, 10); const userAccountStatus = user.getPropertyAsNumber(UserProps.AccountStatus);
return value.map(n => parseInt(n, 10)).includes(userAccountStatus); return value.map(n => parseInt(n, 10)).includes(userAccountStatus);
}, },
EC : function isEncoding() { EC : function isEncoding() {
@ -44,15 +46,15 @@
return value.map(n => parseInt(n, 10)).includes(client.node); return value.map(n => parseInt(n, 10)).includes(client.node);
}, },
NP : function numberOfPosts() { NP : function numberOfPosts() {
const postCount = parseInt(user.properties.post_count, 10) || 0; const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0;
return !isNaN(value) && postCount >= value; return !isNaN(value) && postCount >= value;
}, },
NC : function numberOfCalls() { NC : function numberOfCalls() {
const loginCount = parseInt(user.properties.login_count, 10); const loginCount = user.getPropertyAsNumber(UserProps.LoginCount);
return !isNaN(value) && loginCount >= value; return !isNaN(value) && loginCount >= value;
}, },
AA : function accountAge() { AA : function accountAge() {
const accountCreated = moment(user.properties.account_created); const accountCreated = moment(user.getProperty(UserProps.AccountCreated));
const now = moment(); const now = moment();
const daysOld = accountCreated.diff(moment(), 'days'); const daysOld = accountCreated.diff(moment(), 'days');
return !isNaN(value) && return !isNaN(value) &&
@ -61,36 +63,36 @@
daysOld >= value; daysOld >= value;
}, },
BU : function bytesUploaded() { BU : function bytesUploaded() {
const bytesUp = parseInt(user.properties.ul_total_bytes, 10) || 0; const bytesUp = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0;
return !isNaN(value) && bytesUp >= value; return !isNaN(value) && bytesUp >= value;
}, },
UP : function uploads() { UP : function uploads() {
const uls = parseInt(user.properties.ul_total_count, 10) || 0; const uls = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0;
return !isNaN(value) && uls >= value; return !isNaN(value) && uls >= value;
}, },
BD : function bytesDownloaded() { BD : function bytesDownloaded() {
const bytesDown = parseInt(user.properties.dl_total_bytes, 10) || 0; const bytesDown = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0;
return !isNaN(value) && bytesDown >= value; return !isNaN(value) && bytesDown >= value;
}, },
DL : function downloads() { DL : function downloads() {
const dls = parseInt(user.properties.dl_total_count, 10) || 0; const dls = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0;
return !isNaN(value) && dls >= value; return !isNaN(value) && dls >= value;
}, },
NR : function uploadDownloadRatioGreaterThan() { NR : function uploadDownloadRatioGreaterThan() {
const ulCount = parseInt(user.properties.ul_total_count, 10) || 0; const ulCount = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0;
const dlCount = parseInt(user.properties.dl_total_count, 10) || 0; const dlCount = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0;
const ratio = ~~((ulCount / dlCount) * 100); const ratio = ~~((ulCount / dlCount) * 100);
return !isNaN(value) && ratio >= value; return !isNaN(value) && ratio >= value;
}, },
KR : function uploadDownloadByteRatioGreaterThan() { KR : function uploadDownloadByteRatioGreaterThan() {
const ulBytes = parseInt(user.properties.ul_total_bytes, 10) || 0; const ulBytes = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0;
const dlBytes = parseInt(user.properties.dl_total_bytes, 10) || 0; const dlBytes = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0;
const ratio = ~~((ulBytes / dlBytes) * 100); const ratio = ~~((ulBytes / dlBytes) * 100);
return !isNaN(value) && ratio >= value; return !isNaN(value) && ratio >= value;
}, },
PC : function postCallRatio() { PC : function postCallRatio() {
const postCount = parseInt(user.properties.post_count, 10) || 0; const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0;
const loginCount = parseInt(user.properties.login_count, 10); const loginCount = user.getPropertyAsNumber(UserProps.LoginCount) || 0;
const ratio = ~~((postCount / loginCount) * 100); const ratio = ~~((postCount / loginCount) * 100);
return !isNaN(value) && ratio >= value; return !isNaN(value) && ratio >= value;
}, },

View file

@ -66,7 +66,8 @@
// See https://github.com/trentm/node-bunyan#streams // See https://github.com/trentm/node-bunyan#streams
// //
// Remember you can pipe logs through Bunyan to pretty-print: // Remember you can pipe logs through Bunyan to pretty-print:
// tail -F ./logs/enigma-bbs.log | bunyan // Linux : tail -F ./logs/enigma-bbs.log | bunyan
// PowerShell : Get-Content .\enigma-bbs.log -Tail 15 | bunyan.cmd
// //
// (npm install -g bunyan to get the binary) // (npm install -g bunyan to get the binary)
// //
@ -210,6 +211,9 @@
port: XXXXX port: XXXXX
enabled: false enabled: false
// bannerFile path in misc/ by default. Full paths allowed.
bannerFile: XXXXX
// //
// The Gopher Content Server can export message base // The Gopher Content Server can export message base
// conferences and areas via the "messageConferences" key. // conferences and areas via the "messageConferences" key.
@ -330,7 +334,7 @@
// ] // ]
// //
// //
// Set default group(s) new users should automatically be assigned to // Set default group(s) new users should automatically be assigned to:
// defaultGroups : [ // defaultGroups : [
// "lamerz" // "lamerz"
// ] // ]
@ -348,6 +352,23 @@
// Usernames reserved for applying to your system // Usernames reserved for applying to your system
newUserNames: [] newUserNames: []
// Handling of failed logins
failedLogin : {
// disconnect after N failed attempts. 0=disabled.
disconnect : XXXXX
// Lock the user out after N failed attempts. 0=disabled.
lockAccount : XXXXX
//
// If locked out, how long until the user can login again?
// Set to 0 to disable auto-unlock
//
autoUnlockMinutes : XXXXX
},
// Allow email driven password resets to unlock accounts?
unlockAtEmailPwReset : XXXXX
} }
// Archive files and related // Archive files and related
@ -378,10 +399,29 @@
// //
} }
//
// Use the Event Scheduler to set up arbitrary scheduled events
// using Later style syntax and/or @watch files.
// See docs/event-scheduler.md for more information.
//
eventScheduler: {
events: {
// Example:
//
// sampleEvent: {
// schedule: every 2 hours
// action: @execute:/path/to/some/script.sh
// args: [
// "--foo", "--bar"
// ]
// }
}
}
statLog: { statLog: {
systemEvents: { systemEvents: {
// Max login history event records kept. -1 = unlimited // Max login history event records kept. -1 = unlimited
loginHistoryMax: -1 loginHistoryMax: -1
} }
} }
} }

9
misc/gopher_banner.asc Normal file
View file

@ -0,0 +1,9 @@
_____________________ _____ ____________________ __________\_ /
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
// __|___// | \// |// | \// | | \// \ /___ /_____
/____ _____| __________ ___|__| ____| \ / _____ \
---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/
/__ _\
<*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/
-------------------------------------------------------------------------------

View file

@ -145,6 +145,9 @@
next: fullLoginSequenceLoginArt next: fullLoginSequenceLoginArt
config: { config: {
tooNodeMenu: loginAttemptTooNode tooNodeMenu: loginAttemptTooNode
inactive: loginAttemptAccountInactive
disabled: loginAttemptAccountDisabled
locked: loginAttemptAccountLocked
} }
form: { form: {
0: { 0: {
@ -185,6 +188,34 @@
cls: true cls: true
nextTimeout: 2000 nextTimeout: 2000
} }
next: logoff
}
loginAttemptAccountLocked: {
art: ACCOUNTLOCKED
config: {
cls: true
nextTimeout: 2000
}
next: logoff
}
loginAttemptAccountDisabled: {
art: ACCOUNTDISABLED
config: {
cls: true
nextTimeout: 2000
}
next: logoff
}
loginAttemptAccountInactive: {
art: ACCOUNTINACTIVE
config: {
cls: true
nextTimeout: 2000
}
next: logoff
} }
forgotPassword: { forgotPassword: {
@ -1921,7 +1952,6 @@
SM2: { SM2: {
argName: targetSelection argName: targetSelection
submit: false submit: false
justify: right
} }
} }
submit: { submit: {

69
package-lock.json generated
View file

@ -94,11 +94,6 @@
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg="
}, },
"arrify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0="
},
"asn1": { "asn1": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
@ -494,16 +489,15 @@
} }
}, },
"del": { "del": {
"version": "2.2.2", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz",
"integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=",
"requires": { "requires": {
"globby": "^5.0.0", "globby": "^6.1.0",
"is-path-cwd": "^1.0.0", "is-path-cwd": "^1.0.0",
"is-path-in-cwd": "^1.0.0", "is-path-in-cwd": "^1.0.0",
"object-assign": "^4.0.1", "p-map": "^1.1.1",
"pify": "^2.0.0", "pify": "^3.0.0",
"pinkie-promise": "^2.0.0",
"rimraf": "^2.2.8" "rimraf": "^2.2.8"
} }
}, },
@ -878,16 +872,22 @@
} }
}, },
"globby": { "globby": {
"version": "5.0.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
"requires": { "requires": {
"array-union": "^1.0.1", "array-union": "^1.0.1",
"arrify": "^1.0.0",
"glob": "^7.0.3", "glob": "^7.0.3",
"object-assign": "^4.0.1", "object-assign": "^4.0.1",
"pify": "^2.0.0", "pify": "^2.0.0",
"pinkie-promise": "^2.0.0" "pinkie-promise": "^2.0.0"
},
"dependencies": {
"pify": {
"version": "2.3.0",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
}
} }
}, },
"graceful-fs": { "graceful-fs": {
@ -954,9 +954,9 @@
"integrity": "sha512-U/fnTE3edW0AV92ZI/BfEluMZuVcu3MDOopsN7jS+HqDYcarQo8rXQiWlsBlm0uX48/taYSdxRsfzh2HRg5Z6w==" "integrity": "sha512-U/fnTE3edW0AV92ZI/BfEluMZuVcu3MDOopsN7jS+HqDYcarQo8rXQiWlsBlm0uX48/taYSdxRsfzh2HRg5Z6w=="
}, },
"hjson": { "hjson": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/hjson/-/hjson-3.1.1.tgz", "resolved": "https://registry.npmjs.org/hjson/-/hjson-3.1.2.tgz",
"integrity": "sha512-1oGkOq4sssz7HFZ8Is9HuTR47r8gSC46qAzQxVlAkj0lNKpS+W5Lv2eci+c5+fFqL+Idtj5EvprFreUwH29a8A==" "integrity": "sha512-2ILrho8eRl2Bniy61mDFiXRAloYqH2T6OwWkoF/8y55DPFgG2RcqQGNXIfBLp432dnAbLOpBJ4pJs63W3X27EA=="
}, },
"http-signature": { "http-signature": {
"version": "1.2.0", "version": "1.2.0",
@ -1649,6 +1649,11 @@
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
}, },
"p-map": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz",
"integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA=="
},
"pascalcase": { "pascalcase": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
@ -1675,9 +1680,9 @@
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
}, },
"pify": { "pify": {
"version": "2.3.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
}, },
"pinkie": { "pinkie": {
"version": "2.0.4", "version": "2.0.4",
@ -2233,11 +2238,11 @@
} }
}, },
"temptmp": { "temptmp": {
"version": "1.0.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/temptmp/-/temptmp-1.0.0.tgz", "resolved": "https://registry.npmjs.org/temptmp/-/temptmp-1.1.0.tgz",
"integrity": "sha1-M7Djbh8nMXyKKBIO6Wufj+tw2UM=", "integrity": "sha512-gHelQlePUzxRmodWL1uJ9LiwI+a7a3rkFGS9azTf4noPZgGOlx0dOPV9tZs5+QwGc4Nm8BfFxL9cfvV42GNxPQ==",
"requires": { "requires": {
"del": "^2.2.2" "del": "^3.0.0"
} }
}, },
"through": { "through": {
@ -2493,9 +2498,9 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}, },
"ws": { "ws": {
"version": "6.1.0", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz",
"integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==", "integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==",
"requires": { "requires": {
"async-limiter": "~1.0.0" "async-limiter": "~1.0.0"
} }
@ -2514,9 +2519,9 @@
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k="
}, },
"yazl": { "yazl": {
"version": "2.4.3", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/yazl/-/yazl-2.4.3.tgz", "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.0.tgz",
"integrity": "sha1-7CblzIfVYBud+EMtvdPNLlFzoHE=", "integrity": "sha512-rgptqKwX/f1/7bIRF1FHb4HGsP5k11QyxBpDl1etUDfNpTa7CNjDOYNPFnIaEzZ9dRq0c47IEJS+sy+T39JCLw==",
"requires": { "requires": {
"buffer-crc32": "~0.2.3" "buffer-crc32": "~0.2.3"
} }

View file

@ -31,7 +31,7 @@
"glob": "^7.1.2", "glob": "^7.1.2",
"graceful-fs": "^4.1.15", "graceful-fs": "^4.1.15",
"hashids": "^1.1.1", "hashids": "^1.1.1",
"hjson": "^3.1.1", "hjson": "^3.1.2",
"iconv-lite": "^0.4.23", "iconv-lite": "^0.4.23",
"inquirer": "^6.0.0", "inquirer": "^6.0.0",
"later": "1.2.0", "later": "1.2.0",
@ -47,12 +47,12 @@
"sqlite3": "^4.0.4", "sqlite3": "^4.0.4",
"sqlite3-trans": "^1.2.0", "sqlite3-trans": "^1.2.0",
"ssh2": "^0.6.1", "ssh2": "^0.6.1",
"temptmp": "^1.0.0", "temptmp": "^1.1.0",
"uuid": "^3.2.1", "uuid": "^3.2.1",
"uuid-parse": "^1.0.0", "uuid-parse": "^1.0.0",
"ws": "^6.1.0", "ws": "^6.1.2",
"xxhash": "^0.2.4", "xxhash": "^0.2.4",
"yazl": "^2.4.2" "yazl": "^2.5.0"
}, },
"devDependencies": {}, "devDependencies": {},
"engines": { "engines": {