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

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

View file

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

View file

@ -2,13 +2,14 @@
'use strict';
// ENiGMA½
const logger = require('./logger.js');
const Events = require('./events.js');
const logger = require('./logger.js');
const Events = require('./events.js');
const UserProps = require('./user_property.js');
// deps
const _ = require('lodash');
const moment = require('moment');
const hashids = require('hashids');
const _ = require('lodash');
const moment = require('moment');
const hashids = require('hashids');
exports.getActiveConnections = getActiveConnections;
exports.getActiveConnectionList = getActiveConnectionList;
@ -47,11 +48,11 @@ function getActiveConnectionList(authUsersOnly) {
//
if(ac.user.isAuthenticated()) {
entry.userName = ac.user.username;
entry.realName = ac.user.properties.real_name;
entry.location = ac.user.properties.location;
entry.affils = entry.affiliation = ac.user.properties.affiliation;
entry.realName = ac.user.properties[UserProps.RealName];
entry.location = ac.user.properties[UserProps.Location];
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');
}
return entry;
@ -62,7 +63,7 @@ function addNewClient(client, clientSock) {
const id = client.session.id = clientConnections.push(client) - 1;
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() ]);
// Create a client specific logger

View file

@ -110,7 +110,8 @@ function renegadeToAnsi(s, client) {
result += s.substr(lastIndex, m.index - lastIndex) + attr;
} else if(m[4] || 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;
} else if(m[5]) {
// || -- literal '|', that is.

View file

@ -42,17 +42,53 @@ function hasMessageConferenceAndArea(config) {
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) {
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(
[
function mergeWithDefaultConfig(callback) {
const mergedConfig = _.mergeWith(
getDefaultConfig(),
config, (conf1, conf2) => {
// Arrays should always concat
if(_.isArray(conf1)) {
// :TODO: look for collisions & override dupes
return conf1.concat(conf2);
defaultConfig,
config,
(defConfig, userConfig, key) => {
if(Array.isArray(defConfig) && Array.isArray(userConfig)) {
//
// 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{}?
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
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
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 : {

View file

@ -67,7 +67,13 @@ function loadDatabaseForMod(modInfo, cb) {
function getISOTimestampString(ts) {
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) {

View file

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

View file

@ -4,6 +4,7 @@
// ENiGMA½
const Config = require('./config.js').get;
const StatLog = require('./stat_log.js');
const UserProps = require('./user_property.js');
// deps
const fs = require('graceful-fs');
@ -84,6 +85,8 @@ module.exports = class DropFile {
const prop = this.client.user.properties;
const now = moment();
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 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', // "Page Bell - 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"
prop.location || 'Anywhere', // "Calling From"
fullName, // "User Full Name"
prop[UserProps.Location]|| 'Anywhere', // "Calling From"
'123-456-7890', // "Home Phone"
'123-456-7890', // "Work/Data Phone"
'NOPE', // "Password" (Note: this is never given out or even stored plaintext)
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"
'15360', // "Seconds Remaining THIS call (for those that particular)"
'256', // "Minutes Remaining THIS call"
@ -120,7 +123,7 @@ module.exports = class DropFile {
'0', // "Total Downloads"
'0', // "Daily Download "K" Total"
'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:\\GEN\\', // "Path to the GEN directory"
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', // "Total "K" Bytes Uploaded"
'0', // "Total "K" Bytes Downloaded"
prop.user_comment || 'None', // "User Comment"
prop[UserProps.UserComment] || 'None', // "User Comment"
'0', // "Total Doors Opened"
'0', // "Total Messages Left"
@ -168,7 +171,7 @@ module.exports = class DropFile {
'115200',
Config().general.boardName,
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.getLegacySecurityLevel().toString(),
'546', // :TODO: Minutes left!
@ -189,21 +192,22 @@ module.exports = class DropFile {
const opUserName = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0];
const userName = /[^\s]*/.exec(this.client.user.username)[0];
const secLevel = this.client.user.getLegacySecurityLevel().toString();
const location = this.client.user.properties[UserProps.Location];
return iconv.encode( [
Config().general.boardName, // "The name of the system."
opUserName, // "The sysop's name up to 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."
'57600', // "The current port (DTE) rate."
'0', // "The number "0""
userName, // "The current user's name, up to 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."
'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."
'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."
Config().general.boardName, // "The name of the system."
opUserName, // "The sysop's name up to 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."
'57600', // "The current port (DTE) rate."
'0', // "The number "0""
userName, // "The current user's name, up to the first space."
userName, // "The current user's name, following the first space."
location || '', // "Where the user lives, or a blank line if unknown."
'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."
'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."
].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),
MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, 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),
BadLogin : (reason, reasonCode) => new EnigError('Bad login attempt', -32010, reason, reasonCode),
};
exports.ErrorReasons = {
@ -44,4 +45,9 @@ exports.ErrorReasons = {
NoPreviousMenu : 'NOPREV',
NoConditionMatch : 'NOCONDMATCH',
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 Config = require('./config.js').get;
const Log = require('./logger.js').log;
const { Errors } = require('./enig_error.js');
const _ = require('lodash');
const later = require('later');
@ -116,7 +117,7 @@ class ScheduledEvent {
methodModule[this.action.what](this.action.args, err => {
if(err) {
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');
}
@ -124,7 +125,7 @@ class ScheduledEvent {
});
} catch(e) {
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');
return cb(e);
@ -138,7 +139,22 @@ class ScheduledEvent {
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 => {
if(exitCode) {

View file

@ -7,6 +7,7 @@ const ViewController = require('./view_controller.js').ViewContro
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas;
const FileBaseFilters = require('./file_base_filter.js');
const stringFormat = require('./string_format.js');
const UserProps = require('./user_property.js');
// deps
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(filterUuid === this.client.user.properties.file_base_filter_active_uuid) {
if(filterUuid === this.client.user.properties[UserProps.FileBaseFilterActiveUuid]) {
const newActive = this.filtersArray[this.currentFilterIndex];
if(newActive) {
filters.setActive(newActive.uuid);

View file

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

View file

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

View file

@ -1,9 +1,11 @@
/* jslint node: true */
'use strict';
const UserProps = require('./user_property.js');
// deps
const _ = require('lodash');
const uuidV4 = require('uuid/v4');
const _ = require('lodash');
const uuidV4 = require('uuid/v4');
module.exports = class FileBaseFilters {
constructor(client) {
@ -64,7 +66,7 @@ module.exports = class FileBaseFilters {
}
load() {
let filtersProperty = this.client.user.properties.file_base_filters;
let filtersProperty = this.client.user.properties[UserProps.FileBaseFilters];
let defaulted;
if(!filtersProperty) {
filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters());
@ -90,7 +92,7 @@ module.exports = class FileBaseFilters {
}
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) {
@ -102,7 +104,7 @@ module.exports = class FileBaseFilters {
if(activeFilter) {
this.activeFilter = activeFilter;
this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid);
this.client.user.persistProperty(UserProps.FileBaseFilterActiveUuid, filterUuid);
return true;
}
@ -129,11 +131,11 @@ module.exports = class FileBaseFilters {
}
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) {
return parseInt((user.properties.user_file_base_last_viewed || 0));
return parseInt((user.properties[UserProps.FileBaseLastViewedId] || 0));
}
static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) {
@ -150,6 +152,6 @@ module.exports = class FileBaseFilters {
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'));
}
const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}';
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.setItems(this.dlQueue.items);
queueView.on('index update', idx => {
const fileEntry = this.dlQueue.items[idx];

View file

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

View file

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

View file

@ -6,6 +6,7 @@ const conf = require('./config.js');
const logger = require('./logger.js');
const ServerModule = require('./server_module.js').ServerModule;
const clientConns = require('./client_connections.js');
const UserProps = require('./user_property.js');
// deps
const _ = require('lodash');
@ -25,12 +26,12 @@ module.exports = class LoginServerModule extends ServerModule {
//
const preLoginTheme = _.get(conf.config, 'theme.preLogin');
if('*' === preLoginTheme) {
client.user.properties.theme_id = theme.getRandomTheme() || '';
client.user.properties[UserProps.ThemeId] = theme.getRandomTheme() || '';
} 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...
}

View file

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

View file

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

View file

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

View file

@ -2,8 +2,10 @@
'use strict';
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 {
@ -15,8 +17,8 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup {
if(recordPrevious) {
this.prevMessageConfAndArea = {
confTag : this.client.user.properties.message_conf_tag,
areaTag : this.client.user.properties.message_area_tag,
confTag : this.client.user.properties[UserProps.MessageConfTag],
areaTag : this.client.user.properties[UserProps.MessageAreaTag],
};
}
@ -27,8 +29,8 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup {
tempMessageConfAndAreaRestore() {
if(this.prevMessageConfAndArea) {
this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag;
this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag;
this.client.user.properties[UserProps.MessageConfTag] = this.prevMessageConfAndArea.confTag;
this.client.user.properties[UserProps.MessageAreaTag] = this.prevMessageConfAndArea.areaTag;
}
}
};

View file

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

View file

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

View file

@ -8,6 +8,7 @@ const messageArea = require('./message_area.js');
const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher;
const Errors = require('./enig_error.js').Errors;
const Message = require('./message.js');
const UserProps = require('./user_property.js');
// deps
const async = require('async');
@ -167,7 +168,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
if(this.config.messageAreaTag) {
this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag);
} 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 Config = require('./config.js').get;
const messageArea = require('./message_area.js');
const {
getISOTimestampString
} = require('./database.js');
const UserProps = require('./user_property.js');
// deps
const _ = require('lodash');
const moment = require('moment');
exports.moduleInfo = {
name : 'NUA',
@ -80,20 +85,20 @@ exports.getModule = class NewUserAppModule extends MenuModule {
areaTag = areaTag || '';
newUser.properties = {
real_name : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format
sex : formData.value.sex,
location : formData.value.location,
affiliation : formData.value.affils,
email_address : formData.value.email,
web_address : formData.value.web,
account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format
[ UserProps.RealName ] : formData.value.realName,
[ UserProps.Birthdate ] : getISOTimestampString(formData.value.birthdate),
[ UserProps.Sex ] : formData.value.sex,
[ UserProps.Location ] : formData.value.location,
[ UserProps.Affiliations ] : formData.value.affils,
[ UserProps.EmailAddress ] : formData.value.email,
[ UserProps.WebAddress ] : formData.value.web,
[ UserProps.AccountCreated ] : getISOTimestampString(),
message_conf_tag : confTag,
message_area_tag : areaTag,
[ UserProps.MessageConfTag ] : confTag,
[ UserProps.MessageAreaTag ] : areaTag,
term_height : self.client.term.termHeight,
term_width : self.client.term.termWidth,
[ UserProps.TermHeight ] : self.client.term.termHeight,
[ UserProps.TermWidth ] : self.client.term.termWidth,
// :TODO: Other defaults
// :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');
if('*' === defaultTheme) {
newUser.properties.theme_id = theme.getRandomTheme();
newUser.properties[UserProps.ThemeId] = theme.getRandomTheme();
} else {
newUser.properties.theme_id = defaultTheme;
newUser.properties[UserProps.ThemeId] = defaultTheme;
}
// :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);
} else {
//

View file

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

View file

@ -26,10 +26,11 @@ commands:
actions:
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
deactivate USERNAME sets USERNAME's status to deactive
deactivate USERNAME sets USERNAME's status to inactive
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
`,
@ -57,7 +58,7 @@ cat args:
actions:
scan AREA_TAG[@STORAGE_TAG] scan specified area
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
where CRITERIA is one of the following:

View file

@ -17,7 +17,7 @@ module.exports = function() {
process.exitCode = ExitCodes.SUCCESS;
if(true === argv.version) {
return console.info(require('../package.json').version);
return console.info(require('../../package.json').version);
}
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 getHelpFor = require('./oputil_help.js').getHelpFor;
const Errors = require('../enig_error.js').Errors;
const UserProps = require('../user_property.js');
const async = require('async');
const _ = require('lodash');
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) {
async.waterfall(
[
@ -34,12 +22,12 @@ function initAndGetUser(userName, cb) {
initConfigAndDatabases(callback);
},
function getUserObject(callback) {
getUser(userName, (err, user) => {
const User = require('../../core/user.js');
User.getUserIdAndName(userName, (err, userId) => {
if(err) {
process.exitCode = ExitCodes.BAD_ARGS;
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;
status = {
activate : AccountStatus.active,
deactivate : AccountStatus.inactive,
disable : AccountStatus.disabled,
lock : AccountStatus.locked,
}[status];
const statusDesc = _.invert(AccountStatus)[status];
user.persistProperty('account_status', status, err => {
if(err) {
process.exitCode = ExitCodes.ERROR;
console.error(err.message);
} else {
console.info(`User status set to ${statusDesc}`);
async.series(
[
(callback) => {
return user.persistProperty(UserProps.AccountStatus, status, callback);
},
(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) {
@ -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 errUsage() {
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
@ -195,11 +191,12 @@ function handleUserCommand() {
del : removeUser,
delete : removeUser,
activate : activateUser,
deactivate : deactivateUser,
disable : disableUser,
activate : setAccountStatus,
deactivate : setAccountStatus,
disable : setAccountStatus,
lock : setAccountStatus,
group : modUserGroups,
}[action] || errUsage)(user);
}[action] || errUsage)(user, action);
});
}

View file

@ -2,17 +2,18 @@
'use strict';
// ENiGMA½
const Config = require('./config.js').get;
const Log = require('./logger.js').log;
const Config = require('./config.js').get;
const Log = require('./logger.js').log;
const {
getMessageAreaByTag,
getMessageConferenceByTag
} = require('./message_area.js');
const clientConnections = require('./client_connections.js');
const StatLog = require('./stat_log.js');
const FileBaseFilters = require('./file_base_filter.js');
const { formatByteSize } = require('./string_util.js');
const ANSI = require('./ansi_term.js');
} = require('./message_area.js');
const clientConnections = require('./client_connections.js');
const StatLog = require('./stat_log.js');
const FileBaseFilters = require('./file_base_filter.js');
const { formatByteSize } = require('./string_util.js');
const ANSI = require('./ansi_term.js');
const UserProps = require('./user_property.js');
// deps
const packageJson = require('../package.json');
@ -80,62 +81,66 @@ const PREDEFINED_MCI_GENERATORS = {
UN : function userName(client) { return client.user.username; },
UI : function userId(client) { return client.user.userId.toString(); },
UG : function groups(client) { return _.values(client.user.groups).join(', '); },
UR : function realName(client) { return userStatAsString(client, 'real_name', ''); },
LO : function location(client) { return userStatAsString(client, 'location', ''); },
UR : function realName(client) { return userStatAsString(client, UserProps.RealName, ''); },
LO : function location(client) { return userStatAsString(client, UserProps.Location, ''); },
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
US : function sex(client) { return userStatAsString(client, 'sex', ''); },
UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); },
UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); },
UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); },
UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); },
UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); },
BD : function birthdate(client) { // iNiQUiTY
return moment(client.user.properties[UserProps.Birthdate]).format(client.currentTheme.helpers.getDateFormat());
},
US : function sex(client) { return userStatAsString(client, UserProps.Sex, ''); },
UE : function emailAddres(client) { return userStatAsString(client, UserProps.EmailAddress, ''); },
UW : function webAddress(client) { return userStatAsString(client, UserProps.WebAddress, ''); },
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(); },
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; },
FN : function activeFileBaseFilterName(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
const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes');
const byteSize = StatLog.getUserStatNum(client.user, UserProps.FileDlTotalBytes);
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
const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes');
const byteSize = StatLog.getUserStatNum(client.user, UserProps.FileUlTotalBytes);
return formatByteSize(byteSize, true); // true=withAbbr
},
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
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()); },
PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); },
PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); },
MS : function accountCreatedclient(client) {
return moment(client.user.properties[UserProps.AccountCreated]).format(client.currentTheme.helpers.getDateFormat());
},
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) {
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
},
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 : '';
},
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 : '';
},
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 : '';
},
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 : '';
},
@ -169,8 +174,9 @@ const PREDEFINED_MCI_GENERATORS = {
// Clean up CPU strings a bit for better display
//
return os.cpus()[0].model
.replace(/\(R\)|\(TM\)|processor|CPU/g, '')
.replace(/\s+(?= )/g, '');
.replace(/\(R\)|\(TM\)|processor|CPU/ig, '')
.replace(/\s+(?= )/g, '')
.trim();
},
// :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
});
} 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(', ')}])`));
} else {
return callback(null, localInfo);

View file

@ -17,6 +17,7 @@ const {
} = require('../../message_area.js');
const { sortAreasOrConfs } = require('../../conf_area_util.js');
const AnsiPrep = require('../../ansi_prep.js');
const { wordWrapText } = require('../../word_wrap.js');
// deps
const net = require('net');
@ -27,9 +28,10 @@ const moment = require('moment');
const ModuleInfo = exports.moduleInfo = {
name : 'Gopher',
desc : 'Gopher Server',
desc : 'A RFC-1436-ish Gopher Server',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.gopher.server',
notes : 'https://tools.ietf.org/html/rfc1436',
};
const Message = require('../../message.js');
@ -158,7 +160,7 @@ exports.getModule = class GopherModule extends ServerModule {
defaultGenerator(selectorMatch, cb) {
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);
fs.readFile(bannerFile, 'utf8', (err, banner) => {
if(err) {
@ -182,21 +184,43 @@ exports.getModule = class GopherModule extends ServerModule {
}
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)) {
AnsiPrep(
body,
{
cols : 79, // Gopher std. wants 70, but we'll have to deal with it.
forceLineTerm : true, // ensure each line is term'd
asciiMode : true, // export to ASCII
fillLines : false, // don't fill up to |cols|
cols : WordWrapColumn, // See notes above
forceLineTerm : true, // Ensure each line is term'd
asciiMode : true, // Export to ASCII
fillLines : false, // Don't fill up to |cols|
},
(err, prepped) => {
return cb(prepped || body);
}
);
} 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 => {
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);
}
@ -268,10 +292,17 @@ ${msgBody}
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 = [
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`),
this.makeItem(ItemTypes.InfoMessage, '(newest first)'),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
...msgList.map(msg => this.makeItem(
ItemTypes.TextFile,

View file

@ -10,6 +10,10 @@ const userLogin = require('../../user_login.js').userLogin;
const enigVersion = require('../../../package.json').version;
const theme = require('../../theme.js');
const stringFormat = require('../../string_format.js');
const {
Errors,
ErrorReasons
} = require('../../enig_error.js');
// deps
const ssh2 = require('ssh2');
@ -36,8 +40,6 @@ function SSHClient(clientConn) {
const self = this;
let loginAttempts = 0;
clientConn.on('authentication', function authAttempt(ctx) {
const username = ctx.username || '';
const password = ctx.password || '';
@ -52,26 +54,56 @@ function SSHClient(clientConn) {
return clientConn.end();
}
function alreadyLoggedIn(username) {
ctx.prompt(`${username} is already connected to the system. Terminating connection.\n(Press any key to continue)`);
function promptAndTerm(msg) {
if('keyboard-interactive' === ctx.method) {
ctx.prompt(msg);
}
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
// 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) {
return ctx.accept();
}
if(username.length > 0 && password.length > 0) {
loginAttempts += 1;
userLogin(self, ctx.username, ctx.password, function authResult(err) {
if(err) {
if(err.existingConn) {
return alreadyLoggedIn(username);
if(isSpecialHandleError(err)) {
return handleSpecialError(err, username);
}
return ctx.reject(SSHClient.ValidAuthMethods);
@ -92,15 +124,13 @@ function SSHClient(clientConn) {
const interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false };
ctx.prompt(interactivePrompt, function retryPrompt(answers) {
loginAttempts += 1;
userLogin(self, username, (answers[0] || ''), err => {
if(err) {
if(err.existingConn) {
return alreadyLoggedIn(username);
if(isSpecialHandleError(err)) {
return handleSpecialError(err, username);
}
if(loginAttempts >= config.general.loginAttempts) {
if(Errors.BadLogin().code === err.code) {
return terminateConnection();
}

View file

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

View file

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

View file

@ -2,10 +2,12 @@
'use strict';
const sysDb = require('./database.js').dbs.system;
const {
getISOTimestampString
} = require('./database.js');
// deps
const _ = require('lodash');
const moment = require('moment');
/*
System Event Log & Stats
@ -68,6 +70,7 @@ class StatLog {
};
}
// :TODO: fix spelling :)
setNonPeristentSystemStat(statName, statValue) {
this.systemStats[statName] = statValue;
}
@ -148,7 +151,9 @@ class StatLog {
}
// 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) {
sysDb.run(

View file

@ -2,10 +2,12 @@
'use strict';
// ENiGMA½
const removeClient = require('./client_connections.js').removeClient;
const { removeClient } = require('./client_connections.js');
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 { ErrorReasons } = require('./enig_error.js');
const UserProps = require('./user_property.js');
// deps
const _ = require('lodash');
@ -25,13 +27,23 @@ function login(callingMenu, formData, extraArgs, cb) {
userLogin(callingMenu.client, formData.value.username, formData.value.password, err => {
if(err) {
// login failure
if(err.existingConn && _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) {
// already logged in with this user?
if(ErrorReasons.AlreadyLoggedIn === err.reasonCode &&
_.has(callingMenu, 'menuConfig.config.tooNodeMenu'))
{
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!
@ -94,7 +106,7 @@ function reloadMenu(menu, cb) {
function prevConf(callingMenu, formData, extraArgs, cb) {
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 => {
if(err) {
@ -107,7 +119,7 @@ function prevConf(callingMenu, formData, extraArgs, cb) {
function nextConf(callingMenu, formData, extraArgs, cb) {
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) {
currIndex = -1;
@ -123,8 +135,8 @@ function nextConf(callingMenu, formData, extraArgs, cb) {
}
function prevArea(callingMenu, formData, extraArgs, cb) {
const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag);
const currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag) || areas.length;
const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties[UserProps.MessageConfTag]);
const currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties[UserProps.MessageAreaTag]) || areas.length;
messageArea.changeMessageArea(callingMenu.client, areas[currIndex - 1].areaTag, err => {
if(err) {
@ -136,8 +148,8 @@ function prevArea(callingMenu, formData, extraArgs, cb) {
}
function nextArea(callingMenu, formData, extraArgs, cb) {
const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag);
let currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag);
const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties[UserProps.MessageConfTag]);
let currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties[UserProps.MessageAreaTag]);
if(currIndex === areas.length - 1) {
currIndex = -1;

View file

@ -13,7 +13,9 @@ const Errors = require('./enig_error.js').Errors;
const ErrorReasons = require('./enig_error.js').ErrorReasons;
const Events = require('./events.js');
const AnsiPrep = require('./ansi_prep.js');
const UserProps = require('./user_property.js');
// deps
const fs = require('graceful-fs');
const paths = require('path');
const async = require('async');
@ -38,7 +40,7 @@ function refreshThemeHelpers(theme) {
getPasswordChar : function() {
let pwChar = _.get(
theme,
'customization.defaults.general.passwordChar',
'customization.defaults.passwordChar',
Config().theme.passwordChar
);
@ -427,8 +429,8 @@ function getThemeArt(options, cb) {
// random
//
const config = Config();
if(!options.themeId && _.has(options, 'client.user.properties.theme_id')) {
options.themeId = options.client.user.properties.theme_id;
if(!options.themeId && _.has(options, [ 'client', 'user', 'properties', UserProps.ThemeId ])) {
options.themeId = options.client.user.properties[UserProps.ThemeId];
} else {
options.themeId = config.theme.default;
}
@ -682,8 +684,9 @@ function displayThemedAsset(assetSpec, client, options, cb) {
options = {};
}
if(Array.isArray(assetSpec) && _.isString(options.acsCondMember)) {
assetSpec = client.acs.getConditionalValue(assetSpec, options.acsCondMember);
if(Array.isArray(assetSpec)) {
const acsCondMember = options.acsCondMember || 'art';
assetSpec = client.acs.getConditionalValue(assetSpec, acsCondMember);
}
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.
// Some File processors (Allfix) only insert a line with this
// 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.
// 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.
//
// Example: To 292/854

View file

@ -1,11 +1,18 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const userDb = require('./database.js').dbs.user;
const Config = require('./config.js').get;
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 UserProps = require('./user_property.js');
const Log = require('./logger.js').log;
const StatLog = require('./stat_log.js');
// deps
const crypto = require('crypto');
@ -39,18 +46,31 @@ module.exports = class User {
static get StandardPropertyGroups() {
return {
password : [ 'pw_pbkdf2_salt', 'pw_pbkdf2_dk' ],
password : [ UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk ],
};
}
static get AccountStatus() {
return {
disabled : 0,
inactive : 1,
active : 2,
disabled : 0, // +op disabled
inactive : 1, // inactive, aka requires +op approval/activation
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() {
return true === this.authenticated;
}
@ -60,16 +80,21 @@ module.exports = class User {
return false;
}
return this.hasValidPassword();
return this.hasValidPasswordProperties();
}
hasValidPassword() {
if(!this.properties || !this.properties.pw_pbkdf2_salt || !this.properties.pw_pbkdf2_dk) {
hasValidPasswordProperties() {
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 ((this.properties.pw_pbkdf2_salt.length === User.PBKDF2.saltLen * 2) &&
(this.properties.pw_pbkdf2_dk.length === User.PBKDF2.keyLen * 2));
return true;
}
isRoot() {
@ -101,31 +126,85 @@ module.exports = class User {
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) {
const self = this;
const cachedInfo = {};
const tempAuthInfo = {};
async.waterfall(
[
function fetchUserId(callback) {
// get user ID
User.getUserIdAndName(username, (err, uid, un) => {
cachedInfo.userId = uid;
cachedInfo.username = un;
tempAuthInfo.userId = uid;
tempAuthInfo.username = un;
return callback(err);
});
},
function getRequiredAuthProperties(callback) {
// 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);
});
},
function getDkWithSalt(props, callback) {
// get DK from stored salt and password provided
User.generatePasswordDerivedKey(password, props.pw_pbkdf2_salt, (err, dk) => {
return callback(err, dk, props.pw_pbkdf2_dk);
User.generatePasswordDerivedKey(password, props[UserProps.PassPbkdf2Salt], (err, dk) => {
return callback(err, dk, props[UserProps.PassPbkdf2Dk]);
});
},
function validateAuth(passDk, propsDk, callback) {
@ -135,30 +214,57 @@ module.exports = class User {
const passDkBuf = Buffer.from(passDk, 'hex');
const propsDkBuf = Buffer.from(propsDk, 'hex');
if(passDkBuf.length !== propsDkBuf.length) {
return callback(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'));
return callback(User.isSamePasswordSlowCompare(passDkBuf, propsDkBuf) ?
null :
Errors.AccessDenied('Invalid password')
);
},
function initProps(callback) {
User.loadProperties(cachedInfo.userId, (err, allProps) => {
User.loadProperties(tempAuthInfo.userId, (err, allProps) => {
if(!err) {
cachedInfo.properties = allProps;
tempAuthInfo.properties = allProps;
}
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) {
userGroup.getGroupsForUser(cachedInfo.userId, (err, groups) => {
userGroup.getGroupsForUser(tempAuthInfo.userId, (err, groups) => {
if(!err) {
cachedInfo.groups = groups;
tempAuthInfo.groups = groups;
}
return callback(err);
@ -166,15 +272,44 @@ module.exports = class User {
}
],
err => {
if(!err) {
self.userId = cachedInfo.userId;
self.username = cachedInfo.username;
self.properties = cachedInfo.properties;
self.groups = cachedInfo.groups;
if(err) {
//
// If we failed login due to something besides an inactive or disabled account,
// we need to update failure status and possibly lock the account.
//
// 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;
}
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;
// :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(
[
@ -211,7 +346,7 @@ module.exports = class User {
// Do not require activation for userId 1 (root/admin)
if(User.RootUserID === self.userId) {
self.properties.account_status = User.AccountStatus.active;
self.properties[UserProps.AccountStatus] = User.AccountStatus.active;
}
return callback(null, trans);
@ -224,8 +359,8 @@ module.exports = class User {
return callback(err);
}
self.properties.pw_pbkdf2_salt = info.salt;
self.properties.pw_pbkdf2_dk = info.dk;
self.properties[UserProps.PassPbkdf2Salt] = info.salt;
self.properties[UserProps.PassPbkdf2Dk] = info.dk;
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) {
// update live props
this.properties[propName] = propValue;
userDb.run(
`REPLACE INTO user_property (user_id, prop_name, prop_value)
VALUES (?, ?, ?);`,
[ this.userId, propName, propValue ],
err => {
if(cb) {
return cb(err);
}
}
);
return User.persistPropertyByUserId(this.userId, propName, propValue, 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) {
if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb;
@ -360,8 +516,8 @@ module.exports = class User {
}
const newProperties = {
pw_pbkdf2_salt : info.salt,
pw_pbkdf2_dk : info.dk,
[ UserProps.PassPbkdf2Salt ] : info.salt,
[ UserProps.PassPbkdf2Dk ] : info.dk,
};
this.persistProperties(newProperties, err => {
@ -371,8 +527,9 @@ module.exports = class User {
}
getAge() {
if(_.has(this.properties, 'birthdate')) {
return moment().diff(this.properties.birthdate, 'years');
const birthdate = this.getProperty(UserProps.Birthdate);
if(birthdate) {
return moment().diff(birthdate, 'years');
}
}
@ -439,7 +596,7 @@ module.exports = class User {
WHERE id = (
SELECT user_id
FROM user_property
WHERE prop_name='real_name' AND prop_value LIKE ?
WHERE prop_name='${UserProps.RealName}' AND prop_value LIKE ?
);`,
[ realName ],
(err, row) => {

View file

@ -1,11 +1,17 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController;
const theme = require('./theme.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 assert = require('assert');
const _ = require('lodash');
@ -49,7 +55,7 @@ exports.getModule = class UserConfigModule extends MenuModule {
//
// 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);
}
@ -101,15 +107,15 @@ exports.getModule = class UserConfigModule extends MenuModule {
assert(formData.value.password === formData.value.passwordConfirm);
const newProperties = {
real_name : formData.value.realName,
birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(),
sex : formData.value.sex,
location : formData.value.location,
affiliation : formData.value.affils,
email_address : formData.value.email,
web_address : formData.value.web,
term_height : formData.value.termHeight.toString(),
theme_id : self.availThemeInfo[formData.value.theme].themeId,
[ UserProps.RealName ] : formData.value.realName,
[ UserProps.Birthdate ] : getISOTimestampString(formData.value.birthdate),
[ UserProps.Sex ] : formData.value.sex,
[ UserProps.Location ] : formData.value.location,
[ UserProps.Affiliations ] : formData.value.affils,
[ UserProps.EmailAddress ] : formData.value.email,
[ UserProps.WebAddress ] : formData.value.web,
[ UserProps.TermHeight ] : formData.value.termHeight.toString(),
[ UserProps.ThemeId ] : self.availThemeInfo[formData.value.theme].themeId,
};
// runtime set theme
@ -176,22 +182,22 @@ exports.getModule = class UserConfigModule extends MenuModule {
}), 'name');
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);
},
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.BirthDate, moment(user.properties.birthdate).format('YYYYMMDD'));
self.setViewText('menu', MciCodeIds.Sex, user.properties.sex);
self.setViewText('menu', MciCodeIds.Loc, user.properties.location);
self.setViewText('menu', MciCodeIds.Affils, user.properties.affiliation);
self.setViewText('menu', MciCodeIds.Email, user.properties.email_address);
self.setViewText('menu', MciCodeIds.Web, user.properties.web_address);
self.setViewText('menu', MciCodeIds.TermHeight, user.properties.term_height.toString());
self.setViewText('menu', MciCodeIds.RealName, user.properties[UserProps.RealName]);
self.setViewText('menu', MciCodeIds.BirthDate, moment(user.properties[UserProps.Birthdate]).format('YYYYMMDD'));
self.setViewText('menu', MciCodeIds.Sex, user.properties[UserProps.Sex]);
self.setViewText('menu', MciCodeIds.Loc, user.properties[UserProps.Location]);
self.setViewText('menu', MciCodeIds.Affils, user.properties[UserProps.Affiliations]);
self.setViewText('menu', MciCodeIds.Email, user.properties[UserProps.EmailAddress]);
self.setViewText('menu', MciCodeIds.Web, user.properties[UserProps.WebAddress]);
self.setViewText('menu', MciCodeIds.TermHeight, user.properties[UserProps.TermHeight].toString());
var themeView = self.getView(MciCodeIds.Theme);

View file

@ -5,6 +5,7 @@
const { MenuModule } = require('./menu_module.js');
const { getUserList } = require('./user.js');
const { Errors } = require('./enig_error.js');
const UserProps = require('./user_property.js');
// deps
const moment = require('moment');
@ -44,7 +45,7 @@ exports.getModule = class UserListModule extends MenuModule {
}
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
};
getUserList(fetchOpts, (err, userList) => {

View file

@ -8,34 +8,44 @@ const StatLog = require('./stat_log.js');
const logger = require('./logger.js');
const Events = require('./events.js');
const Config = require('./config.js').get;
const {
Errors,
ErrorReasons
} = require('./enig_error.js');
const UserProps = require('./user_property.js');
// deps
const async = require('async');
const _ = require('lodash');
exports.userLogin = userLogin;
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) {
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');
// :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);
}
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.
// Loop through active connections -- which includes the current --
// and check for matching user ID. If the count is > 1, disallow.
//
let existingClientConnection;
clientConnections.forEach(function connEntry(cc) {
if(cc.user !== user && cc.user.userId === user.userId) {
existingClientConnection = cc;
}
const existingClientConnection = clientConnections.find(cc => {
return user !== cc.user && // not current connection
user.userId === cc.user.userId; // ...but same user
});
if(existingClientConnection) {
@ -48,12 +58,10 @@ function userLogin(client, username, password, cb) {
'Already logged in'
);
const existingConnError = new Error('Already logged in as supplied user');
existingConnError.existingConn = true;
// :TODO: We should use EnigError & pass existing connection as second param
return cb(existingConnError);
return cb(Errors.BadLogin(
`User ${user.username} already logged in.`,
ErrorReasons.AlreadyLoggedIn
));
}
// update client logger with addition of username
@ -67,24 +75,24 @@ function userLogin(client, username, password, cb) {
client.log.info('Successful login');
// 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 } );
async.parallel(
[
function setTheme(callback) {
setClientTheme(client, user.properties.theme_id);
setClientTheme(client, user.properties[UserProps.ThemeId]);
return callback(null);
},
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) {
return StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback);
return StatLog.setUserStat(user, UserProps.LastLoginTs, StatLog.now, callback);
},
function updateUserLoginCount(callback) {
return StatLog.incrementUserStat(user, 'login_count', 1, callback);
return StatLog.incrementUserStat(user, UserProps.LoginCount, 1, callback);
},
function recordLoginHistory(callback) {
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 getISOTimestampString = require('./database.js').getISOTimestampString;
const Log = require('./logger.js').log;
const UserProps = require('./user_property.js');
// deps
const async = require('async');
@ -17,6 +18,7 @@ const crypto = require('crypto');
const fs = require('graceful-fs');
const url = require('url');
const querystring = require('querystring');
const _ = require('lodash');
const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT =
`%USERNAME%:
@ -57,7 +59,7 @@ class WebPasswordReset {
}
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'));
}
@ -77,8 +79,8 @@ class WebPasswordReset {
token = token.toString('hex');
const newProperties = {
email_password_reset_token : token,
email_password_reset_token_ts : getISOTimestampString(),
[ UserProps.EmailPwResetToken ] : token,
[ UserProps.EmailPwResetTokenTs ] : getISOTimestampString(),
};
// we simply place the reset token in the user's properties
@ -103,13 +105,13 @@ class WebPasswordReset {
function buildAndSendEmail(user, textTemplate, htmlTemplate, callback) {
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) {
return s
.replace(/%BOARDNAME%/g, Config().general.boardName)
.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)
;
}
@ -120,7 +122,7 @@ class WebPasswordReset {
}
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
subject : 'Forgot Password',
text : textTemplate,
@ -283,8 +285,15 @@ class WebPasswordReset {
}
// delete assoc properties - no need to wait for completion
user.removeProperty('email_password_reset_token');
user.removeProperty('email_password_reset_token_ts');
user.removeProperties([ UserProps.EmailPwResetToken, UserProps.EmailPwResetTokenTs ]);
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);
return resp.end('Password changed successfully');