From 058ff3f3676fb0ad8ba5cb8f688b6af5915dba91 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 18 Feb 2017 13:21:18 -0700 Subject: [PATCH] * Rework user.js and User object to ES6 * Update download stats for user when web download is completed --- core/bbs.js | 7 +- core/client.js | 4 +- core/file_area_web.js | 39 +- core/fse.js | 4 +- core/oputil/oputil_help.js | 5 +- core/oputil/oputil_main.js | 1 - core/oputil/oputil_user.js | 10 +- core/system_view_validate.js | 8 +- core/user.js | 971 ++++++++++++++++++----------------- mods/bbs_list.js | 4 +- mods/last_callers.js | 13 +- mods/msg_area_reply_fse.js | 7 - mods/nua.js | 8 +- mods/user_list.js | 4 +- 14 files changed, 569 insertions(+), 516 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 2995dabc..3bf53391 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -196,19 +196,18 @@ function initialize(cb) { // * We do this every time as the op is free to change this information just // like any other user // - const user = require('./user.js'); + const User = require('./user.js'); async.waterfall( [ function getOpUserName(next) { - return user.getUserName(1, next); + return User.getUserName(1, next); }, function getOpProps(opUserName, next) { const propLoadOpts = { - userId : 1, names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ], }; - user.loadProperties(propLoadOpts, (err, opProps) => { + User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => { return next(err, opUserName, opProps); }); } diff --git a/core/client.js b/core/client.js index 6815768d..02b225b8 100644 --- a/core/client.js +++ b/core/client.js @@ -34,7 +34,7 @@ // ENiGMA½ const term = require('./client_term.js'); const ansi = require('./ansi_term.js'); -const user = require('./user.js'); +const User = require('./user.js'); const Config = require('./config.js').config; const MenuStack = require('./menu_stack.js'); const ACS = require('./acs.js'); @@ -77,7 +77,7 @@ function Client(input, output) { const self = this; - this.user = new user.User(); + this.user = new User(); this.currentTheme = { info : { name : 'N/A', description : 'None' } }; this.lastKeyPressMs = Date.now(); this.menuStack = new MenuStack(this); diff --git a/core/file_area_web.js b/core/file_area_web.js index b610fd06..ce62f83d 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -9,6 +9,10 @@ const FileEntry = require('./file_entry.js'); const getServer = require('./listening_server.js').getServer; const Errors = require('./enig_error.js').Errors; const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; +const StatLog = require('./stat_log.js'); +const User = require('./user.js'); +const Log = require('./logger.js').log; +const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId; // deps const hashids = require('hashids'); @@ -219,7 +223,7 @@ class FileAreaWebAccess { if(!this.isEnabled()) { return cb(notEnabledError()); } - + const hashId = this.getHashId(client, fileEntry); const url = this.buildTempDownloadLink(client, fileEntry, hashId); options.expireTime = options.expireTime || moment().add(2, 'days'); @@ -277,7 +281,7 @@ class FileAreaWebAccess { resp.on('finish', () => { // transfer completed fully - // :TODO: we need to update the users stats - bytes xferred, credit stuff, etc. + this.updateDownloadStatsForUserId(servedItem.userId, stats.size); }); const headers = { @@ -293,6 +297,37 @@ class FileAreaWebAccess { }); }); } + + updateDownloadStatsForUserId(userId, dlBytes, cb) { + async.waterfall( + [ + function fetchActiveUser(callback) { + const clientForUserId = getConnectionByUserId(userId); + if(clientForUserId) { + return callback(null, clientForUserId.user); + } + + // not online now - look 'em up + User.getUser(userId, (err, assocUser) => { + return callback(err, assocUser); + }); + }, + function updateStats(user, callback) { + StatLog.incrementUserStat(user, 'dl_total_count', 1); + StatLog.incrementUserStat(user, 'dl_total_bytes', dlBytes); + StatLog.incrementSystemStat('dl_total_count', 1); + StatLog.incrementSystemStat('dl_total_bytes', dlBytes); + + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } } module.exports = new FileAreaWebAccess(); \ No newline at end of file diff --git a/core/fse.js b/core/fse.js index 8a7e57f4..7b93e800 100644 --- a/core/fse.js +++ b/core/fse.js @@ -9,7 +9,7 @@ const theme = require('./theme.js'); const Message = require('./message.js'); const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId; const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; -const getUserIdAndName = require('./user.js').getUserIdAndName; +const User = require('./user.js'); const cleanControlCodes = require('./string_util.js').cleanControlCodes; const StatLog = require('./stat_log.js'); const stringFormat = require('./string_format.js'); @@ -373,7 +373,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul callback(null); } else { // we need to look it up - getUserIdAndName(self.message.toUserName, function userInfo(err, toUserId) { + User.getUserIdAndName(self.message.toUserName, function userInfo(err, toUserId) { if(err) { callback(err); } else { diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 25237680..60c59595 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -17,7 +17,6 @@ global args: where is one of: user : user utilities config : config file management - file-base fb : file base management `, @@ -39,7 +38,7 @@ valid args: --new : generate a new/initial configuration `, FileBase : -`usage: oputil.js file-base [] [] +`usage: oputil.js fb [] [] where is one of: scan AREA_TAG : (re)scan area specified by AREA_TAG for new files @@ -47,6 +46,8 @@ where is one of: valid scan : --tags TAG1,TAG2,... : specify tag(s) to assign to discovered entries + +ARE_TAG can optionally contain @STORAGE_TAG; for example: retro_pc@bbs ` }; diff --git a/core/oputil/oputil_main.js b/core/oputil/oputil_main.js index 78dae8d2..d27a4976 100644 --- a/core/oputil/oputil_main.js +++ b/core/oputil/oputil_main.js @@ -34,7 +34,6 @@ module.exports = function() { handleConfigCommand(); break; - case 'file-base' : case 'fb' : handleFileBaseCommand(); break; diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index feb712f6..90d811f5 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -7,8 +7,8 @@ const ExitCodes = require('./oputil_common.js').ExitCodes; const argv = require('./oputil_common.js').argv; const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; - const async = require('async'); +const _ = require('lodash'); exports.handleUserCommand = handleUserCommand; @@ -55,13 +55,13 @@ function handleUserCommand() { } function getUser(userName, cb) { - const user = require('./core/user.js'); - user.getUserIdAndName(argv.user, function userNameAndId(err, userId) { + const User = require('../../core/user.js'); + User.getUserIdAndName(argv.user, function userNameAndId(err, userId) { if(err) { process.exitCode = ExitCodes.BAD_ARGS; return cb(new Error('Failed to retrieve user')); } else { - let u = new user.User(); + let u = new User(); u.userId = userId; return cb(null, u); } @@ -97,7 +97,7 @@ function setAccountStatus(userName, active) { initAndGetUser(argv.user, callback); }, function activateUser(user, callback) { - const AccountStatus = require('./core/user.js').User.AccountStatus; + const AccountStatus = require('../../core/user.js').AccountStatus; user.persistProperty('account_status', active ? AccountStatus.active : AccountStatus.inactive, callback); } ], diff --git a/core/system_view_validate.js b/core/system_view_validate.js index 27975bf9..ac550d82 100644 --- a/core/system_view_validate.js +++ b/core/system_view_validate.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const user = require('./user.js'); +const User = require('./user.js'); const Config = require('./config.js').config; exports.validateNonEmpty = validateNonEmpty; @@ -38,7 +38,7 @@ function validateUserNameAvail(data, cb) { } else if(/^[0-9]+$/.test(data)) { return cb(new Error('Username cannot be a number')); } else { - user.getUserIdAndName(data, function userIdAndName(err) { + User.getUserIdAndName(data, function userIdAndName(err) { if(!err) { // err is null if we succeeded -- meaning this user exists already return cb(new Error('Username unavailable')); } @@ -56,7 +56,7 @@ function validateUserNameExists(data, cb) { return cb(invalidUserNameError); } - user.getUserIdAndName(data, (err) => { + User.getUserIdAndName(data, (err) => { return cb(err ? invalidUserNameError : null); }); } @@ -80,7 +80,7 @@ function validateEmailAvail(data, cb) { return cb(new Error('Invalid email address')); } - user.getUserIdsWithProperty('email_address', data, function userIdsWithEmail(err, uids) { + User.getUserIdsWithProperty('email_address', data, function userIdsWithEmail(err, uids) { if(err) { return cb(new Error('Internal system error')); } else if(uids.length > 0) { diff --git a/core/user.js b/core/user.js index 53272fff..5d85c5b4 100644 --- a/core/user.js +++ b/core/user.js @@ -1,569 +1,598 @@ /* jslint node: true */ 'use strict'; -var userDb = require('./database.js').dbs.user; -var Config = require('./config.js').config; -var userGroup = require('./user_group.js'); +const userDb = require('./database.js').dbs.user; +const Config = require('./config.js').config; +const userGroup = require('./user_group.js'); +const Errors = require('./enig_error.js').Errors; -var crypto = require('crypto'); -var assert = require('assert'); -var async = require('async'); -var _ = require('lodash'); -var moment = require('moment'); - -exports.User = User; -exports.getUserIdAndName = getUserIdAndName; -exports.getUserName = getUserName; -exports.loadProperties = loadProperties; -exports.getUserIdsWithProperty = getUserIdsWithProperty; -exports.getUserList = getUserList; +// deps +const crypto = require('crypto'); +const assert = require('assert'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); exports.isRootUserId = function(id) { return 1 === id; }; -function User() { - var self = this; +module.exports = class User { + constructor() { + this.userId = 0; + this.username = ''; + this.properties = {}; // name:value + this.groups = []; // group membership(s) + } - this.userId = 0; - this.username = ''; - this.properties = {}; // name:value - this.groups = []; // group membership(s) + // static property accessors + static get RootUserID() { + return 1; + } - this.isAuthenticated = function() { - return true === self.authenticated; - }; + static get PBKDF2() { + return { + iterations : 1000, + keyLen : 128, + saltLen : 32, + }; + } - this.isValid = function() { - if(self.userId <= 0 || self.username.length < Config.users.usernameMin) { + static get StandardPropertyGroups() { + return { + password : [ 'pw_pbkdf2_salt', 'pw_pbkdf2_dk' ], + }; + } + + static get AccountStatus() { + return { + disabled : 0, + inactive : 1, + active : 2, + }; + } + + isAuthenticated() { + return true === this.authenticated; + } + + isValid() { + if(this.userId <= 0 || this.username.length < Config.users.usernameMin) { return false; } return this.hasValidPassword(); - }; + } - this.hasValidPassword = function() { + hasValidPassword() { if(!this.properties || !this.properties.pw_pbkdf2_salt || !this.properties.pw_pbkdf2_dk) { return false; } - return this.properties.pw_pbkdf2_salt.length === User.PBKDF2.saltLen * 2 && - this.prop_name.pw_pbkdf2_dk.length === User.PBKDF2.keyLen * 2; - }; + return this.properties.pw_pbkdf2_salt.length === User.PBKDF2.saltLen * 2 && this.prop_name.pw_pbkdf2_dk.length === User.PBKDF2.keyLen * 2; + } - this.isRoot = function() { - return 1 === this.userId; - }; + isRoot() { + return User.isRootUserId(this.userId); + } - this.isSysOp = this.isRoot; // alias + isSysOp() { // alias to isRoot() + return this.isRoot(); + } - this.isGroupMember = function(groupNames) { + isGroupMember(groupNames) { if(_.isString(groupNames)) { groupNames = [ groupNames ]; } - const isMember = groupNames.some(gn => (-1 !== self.groups.indexOf(gn))); + const isMember = groupNames.some(gn => (-1 !== this.groups.indexOf(gn))); return isMember; - }; + } - this.getLegacySecurityLevel = function() { - if(self.isRoot() || self.isGroupMember('sysops')) { + getLegacySecurityLevel() { + if(this.isRoot() || this.isGroupMember('sysops')) { return 100; } - if(self.isGroupMember('users')) { + if(this.isGroupMember('users')) { return 30; } return 10; // :TODO: Is this what we want? - }; + } -} + authenticate(username, password, cb) { + const self = this; + const cachedInfo = {}; -User.PBKDF2 = { - iterations : 1000, - keyLen : 128, - saltLen : 32, -}; + async.waterfall( + [ + function fetchUserId(callback) { + // get user ID + User.getUserIdAndName(username, (err, uid, un) => { + cachedInfo.userId = uid; + cachedInfo.username = un; -User.StandardPropertyGroups = { - password : [ 'pw_pbkdf2_salt', 'pw_pbkdf2_dk' ], -}; + return callback(err); + }); + }, + function getRequiredAuthProperties(callback) { + // fetch properties required for authentication + User.loadProperties(cachedInfo.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); + }); + }, + function validateAuth(passDk, propsDk, callback) { + // + // Use constant time comparison here for security feel-goods + // + const passDkBuf = new Buffer(passDk, 'hex'); + const propsDkBuf = new Buffer(propsDk, 'hex'); -User.AccountStatus = { - disabled : 0, - inactive : 1, - active : 2, -}; - -User.prototype.load = function(userId, cb) { - -}; - -User.prototype.authenticate = function(username, password, cb) { - const self = this; - - const cachedInfo = {}; - - async.waterfall( - [ - function fetchUserId(callback) { - // get user ID - getUserIdAndName(username, function onUserId(err, uid, un) { - cachedInfo.userId = uid; - cachedInfo.username = un; - - callback(err); - }); - }, - - function getRequiredAuthProperties(callback) { - // fetch properties required for authentication - loadProperties( { userId : cachedInfo.userId, names : User.StandardPropertyGroups.password }, function onProps(err, props) { - callback(err, props); - }); - }, - function getDkWithSalt(props, callback) { - // get DK from stored salt and password provided - generatePasswordDerivedKey(password, props.pw_pbkdf2_salt, function onDk(err, dk) { - callback(err, dk, props.pw_pbkdf2_dk); - }); - }, - function validateAuth(passDk, propsDk, callback) { - // - // Use constant time comparison here for security feel-goods - // - var passDkBuf = new Buffer(passDk, 'hex'); - var propsDkBuf = new Buffer(propsDk, 'hex'); - - if(passDkBuf.length !== propsDkBuf.length) { - callback(new Error('Invalid password')); - return; - } - - var c = 0; - for(var i = 0; i < passDkBuf.length; i++) { - c |= passDkBuf[i] ^ propsDkBuf[i]; - } - - callback(0 === c ? null : new Error('Invalid password')); - }, - function initProps(callback) { - loadProperties( { userId : cachedInfo.userId }, function onProps(err, allProps) { - if(!err) { - cachedInfo.properties = allProps; + if(passDkBuf.length !== propsDkBuf.length) { + return callback(Errors.AccessDenied('Invalid password')); } - callback(err); - }); - }, - function initGroups(callback) { - userGroup.getGroupsForUser(cachedInfo.userId, function groupsLoaded(err, groups) { - if(!err) { - cachedInfo.groups = groups; + let c = 0; + for(let i = 0; i < passDkBuf.length; i++) { + c |= passDkBuf[i] ^ propsDkBuf[i]; } - callback(err); - }); - } - ], - function complete(err) { - if(!err) { - self.userId = cachedInfo.userId; - self.username = cachedInfo.username; - self.properties = cachedInfo.properties; - self.groups = cachedInfo.groups; - self.authenticated = true; - } + return callback(0 === c ? null : Errors.AccessDenied('Invalid password')); + }, + function initProps(callback) { + User.loadProperties(cachedInfo.userId, (err, allProps) => { + if(!err) { + cachedInfo.properties = allProps; + } - return cb(err); + return callback(err); + }); + }, + function initGroups(callback) { + userGroup.getGroupsForUser(cachedInfo.userId, (err, groups) => { + if(!err) { + cachedInfo.groups = groups; + } + + return callback(err); + }); + } + ], + err => { + if(!err) { + self.userId = cachedInfo.userId; + self.username = cachedInfo.username; + self.properties = cachedInfo.properties; + self.groups = cachedInfo.groups; + self.authenticated = true; + } + + return cb(err); + } + ); + } + + create(password, cb) { + assert(0 === this.userId); + + if(this.username.length < Config.users.usernameMin || this.username.length > Config.users.usernameMax) { + return cb(Errors.Invalid('Invalid username length')); } - ); -}; -User.prototype.create = function(options, cb) { - assert(0 === this.userId); - assert(this.username.length > 0); // :TODO: Min username length? Max? - assert(_.isObject(options)); - assert(_.isString(options.password)); + const self = this; - var 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; - // :TODO: set various defaults, e.g. default activation status, etc. - self.properties.account_status = Config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; - - async.series( - [ - function beginTransaction(callback) { - userDb.run('BEGIN;', function transBegin(err) { - callback(err); - }); - }, - function createUserRec(callback) { - userDb.run( - 'INSERT INTO user (user_name) ' + - 'VALUES (?);', - [ self.username ], - function userInsert(err) { - if(err) { - callback(err); - } else { + async.series( + [ + function beginTransaction(callback) { + userDb.run('BEGIN;', err => { + return callback(err); + }); + }, + function createUserRec(callback) { + userDb.run( + `INSERT INTO user (user_name) + VALUES (?);`, + [ self.username ], + function inserted(err) { // use classic function for |this| + if(err) { + return callback(err); + } + self.userId = this.lastID; // Do not require activation for userId 1 (root/admin) - if(1 === self.userId) { + if(User.RootUserID === self.userId) { self.properties.account_status = User.AccountStatus.active; } - callback(null); + return callback(null); } - } - ); - }, - function genAuthCredentials(callback) { - generatePasswordDerivedKeyAndSalt(options.password, function dkAndSalt(err, info) { - if(err) { - callback(err); - } else { + ); + }, + function genAuthCredentials(callback) { + User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { + if(err) { + return callback(err); + } + self.properties.pw_pbkdf2_salt = info.salt; self.properties.pw_pbkdf2_dk = info.dk; - callback(null); + return callback(null); + }); + }, + function setInitialGroupMembership(callback) { + self.groups = Config.users.defaultGroups; + + if(User.RootUserID === self.userId) { // root/SysOp? + self.groups.push('sysops'); } - }); - }, - function setInitialGroupMembership(callback) { - self.groups = Config.users.defaultGroups; - if(1 === self.userId) { // root/SysOp? - self.groups.push('sysops'); + return callback(null); + }, + function saveAll(callback) { + self.persist(false, err => { + return callback(err); + }); } - - callback(null); - }, - function saveAll(callback) { - self.persist(false, function persisted(err) { - callback(err); - }); - } - ], - function complete(err) { - if(err) { - var originalError = err; - userDb.run('ROLLBACK;', function rollback(err) { - assert(!err); - cb(originalError); - }); - } else { - userDb.run('COMMIT;', function commited(err) { - cb(err); - }); - } - } - ); -}; - -User.prototype.persist = function(useTransaction, cb) { - assert(this.userId > 0); - - var self = this; - - async.series( - [ - function beginTransaction(callback) { - if(useTransaction) { - userDb.run('BEGIN;', function transBegin(err) { - callback(err); + ], + err => { + if(err) { + const originalError = err; + userDb.run('ROLLBACK;', err => { + assert(!err); + return cb(originalError); }); } else { - callback(null); - } - }, - function saveProps(callback) { - self.persistAllProperties(function persisted(err) { - callback(err); - }); - }, - function saveGroups(callback) { - userGroup.addUserToGroups(self.userId, self.groups, function groupsSaved(err) { - callback(err); - }); - } - ], - function complete(err) { - if(err) { - if(useTransaction) { - userDb.run('ROLLBACK;', function rollback(err) { - cb(err); + userDb.run('COMMIT;', err => { + return cb(err); }); - } else { - cb(err); } - } else { - if(useTransaction) { - userDb.run('COMMIT;', function commited(err) { - cb(err); + } + ); + } + + persist(useTransaction, cb) { + assert(this.userId > 0); + + const self = this; + + async.series( + [ + function beginTransaction(callback) { + if(useTransaction) { + userDb.run('BEGIN;', err => { + return callback(err); + }); + } else { + return callback(null); + } + }, + function saveProps(callback) { + self.persistProperties(self.properties, err => { + return callback(err); }); + }, + function saveGroups(callback) { + userGroup.addUserToGroups(self.userId, self.groups, err => { + return callback(err); + }); + } + ], + err => { + if(err) { + if(useTransaction) { + userDb.run('ROLLBACK;', err => { + return cb(err); + }); + } else { + return cb(err); + } } else { - cb(null); + if(useTransaction) { + userDb.run('COMMIT;', err => { + return cb(err); + }); + } else { + return cb(null); + } } } - } - ); -}; + ); + } -User.prototype.persistProperty = function(propName, propValue, cb) { - // update live props - this.properties[propName] = propValue; + 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 ], - function ran(err) { - if(cb) { - cb(err); + userDb.run( + `REPLACE INTO user_property (user_id, prop_name, prop_value) + VALUES (?, ?, ?);`, + [ this.userId, propName, propValue ], + err => { + if(cb) { + return cb(err); + } } - } - ); -}; + ); + } -User.prototype.removeProperty = function(propName, cb) { - // update live - delete this.properties[propName]; + removeProperty(propName, cb) { + // update live + delete this.properties[propName]; - userDb.run( - `DELETE FROM user_property - WHERE user_id = ? AND prop_name = ?;`, - [ this.userId, propName ], + userDb.run( + `DELETE FROM user_property + WHERE user_id = ? AND prop_name = ?;`, + [ this.userId, propName ], + err => { + if(cb) { + return cb(err); + } + } + ); + } + + persistProperties(properties, cb) { + const self = this; + + // update live props + _.merge(this.properties, properties); + + const stmt = userDb.prepare( + `REPLACE INTO user_property (user_id, prop_name, prop_value) + VALUES (?, ?, ?);` + ); + + async.each(Object.keys(properties), (propName, nextProp) => { + stmt.run(self.userId, propName, properties[propName], err => { + return nextProp(err); + }); + }, err => { - if(cb) { + if(err) { return cb(err); } - } - ); -}; - -User.prototype.persistProperties = function(properties, cb) { - var self = this; - - // update live props - _.merge(this.properties, properties); - - var stmt = userDb.prepare( - 'REPLACE INTO user_property (user_id, prop_name, prop_value) ' + - 'VALUES (?, ?, ?);'); - - async.each(Object.keys(properties), function property(propName, callback) { - stmt.run(self.userId, propName, properties[propName], function onRun(err) { - callback(err); - }); - }, function complete(err) { - if(err) { - cb(err); - } else { - stmt.finalize(function finalized() { - cb(null); + + stmt.finalize( () => { + return cb(null); }); - } - }); -}; + }); + } -User.prototype.persistAllProperties = function(cb) { - assert(this.userId > 0); - - this.persistProperties(this.properties, cb); -}; - -User.prototype.setNewAuthCredentials = function(password, cb) { - var self = this; - - generatePasswordDerivedKeyAndSalt(password, function dkAndSalt(err, info) { - if(err) { - cb(err); - } else { - var newProperties = { + setNewAuthCredentials(password, cb) { + User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { + if(err) { + return cb(err); + } + + const newProperties = { pw_pbkdf2_salt : info.salt, pw_pbkdf2_dk : info.dk, }; - self.persistProperties(newProperties, function persisted(err) { - cb(err); + this.persistProperties(newProperties, err => { + return cb(err); }); - } - }); -}; - -User.prototype.getAge = function() { - if(_.has(this.properties, 'birthdate')) { - return moment().diff(this.properties.birthdate, 'years'); + }); } -}; -/////////////////////////////////////////////////////////////////////////////// -// Exported methods -/////////////////////////////////////////////////////////////////////////////// -function getUserIdAndName(username, cb) { - userDb.get( - 'SELECT id, user_name ' + - 'FROM user ' + - 'WHERE user_name LIKE ?;', - [ username ], - function onResults(err, row) { - if(err) { - cb(err); - } else { - if(row) { - cb(null, row.id, row.user_name); - } else { - cb(new Error('No matching username')); - } - } + getAge() { + if(_.has(this.properties, 'birthdate')) { + return moment().diff(this.properties.birthdate, 'years'); } - ); -} + } -function getUserName(userId, cb) { - userDb.get( - 'SELECT user_name ' + - 'FROM user ' + - 'WHERE id=?;', [ userId ], - function got(err, row) { - if(err) { - cb(err); - } else { - if(row) { - cb(null, row.user_name); - } else { - cb(new Error('No matching user ID')); + static getUser(userId, cb) { + async.waterfall( + [ + function fetchUserId(callback) { + User.getUserName(userId, (err, userName) => { + return callback(null, userName); + }); + }, + function initProps(userName, callback) { + User.loadProperties(userId, (err, properties) => { + return callback(err, userName, properties); + }); + }, + function initGroups(userName, properties, callback) { + userGroup.getGroupsForUser(userId, (err, groups) => { + return callback(null, userName, properties, groups); + }); } - } - } - ); -} + ], + (err, userName, properties, groups) => { + const user = new User(); + user.userId = userId; + user.username = userName; + user.properties = properties; + user.groups = groups; + user.authenticated = false; // this is NOT an authenticated user! -/////////////////////////////////////////////////////////////////////////////// -// Internal utility methods -/////////////////////////////////////////////////////////////////////////////// -function generatePasswordDerivedKeyAndSalt(password, cb) { - async.waterfall( - [ - function getSalt(callback) { - generatePasswordDerivedKeySalt(function onSalt(err, salt) { - callback(err, salt); - }); + return cb(err, user); + } + ); + } + + static isRootUserId(userId) { + return (User.RootUserID === userId); + } + + static getUserIdAndName(username, cb) { + userDb.get( + `SELECT id, user_name + FROM user + WHERE user_name LIKE ?;`, + [ username ], + (err, row) => { + if(err) { + return cb(err); + } + + if(row) { + return cb(null, row.id, row.user_name); + } + + return cb(Errors.DoesNotExist('No matching username')); + } + ); + } + + static getUserName(userId, cb) { + userDb.get( + `SELECT user_name + FROM user + WHERE id = ?;`, + [ userId ], + (err, row) => { + if(err) { + return cb(err); + } + + if(row) { + return cb(null, row.user_name); + } + + return cb(Errors.DoesNotExist('No matching user ID')); + } + ); + } + + static loadProperties(userId, options, cb) { + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } + + let sql = + `SELECT prop_name, prop_value + FROM user_property + WHERE user_id = ?`; + + if(options.names) { + sql += ` AND prop_name IN("${options.names.join('","')}");`; + } else { + sql += ';'; + } + + let properties = {}; + userDb.each(sql, [ userId ], (err, row) => { + if(err) { + return cb(err); + } + properties[row.prop_name] = row.prop_value; + }, () => { + return cb(null, properties); + }); + } + + // :TODO: make this much more flexible - propValue should allow for case-insensitive compare, etc. + static getUserIdsWithProperty(propName, propValue, cb) { + let userIds = []; + + userDb.each( + `SELECT user_id + FROM user_property + WHERE prop_name = ? AND prop_value = ?;`, + [ propName, propValue ], + (err, row) => { + if(row) { + userIds.push(row.user_id); + } + }, + () => { + return cb(null, userIds); + } + ); + } + + static getUserList(options, cb) { + let userList = []; + let orderClause = 'ORDER BY ' + (options.order || 'user_name'); + + userDb.each( + `SELECT id, user_name + FROM user + ${orderClause};`, + (err, row) => { + if(row) { + userList.push({ + userId : row.id, + userName : row.user_name, + }); + } }, - function getDk(salt, callback) { - generatePasswordDerivedKey(password, salt, function onDk(err, dk) { - callback(err, salt, dk); + () => { + options.properties = options.properties || []; + async.map(userList, (user, nextUser) => { + userDb.each( + `SELECT prop_name, prop_value + FROM user_property + WHERE user_id = ? AND prop_name IN ("${options.properties.join('","')}");`, + [ user.userId ], + (err, row) => { + if(row) { + user[row.prop_name] = row.prop_value; + } + }, + err => { + return nextUser(err, user); + } + ); + }, + (err, transformed) => { + return cb(err, transformed); }); } - ], - function onComplete(err, salt, dk) { - cb(err, { salt : salt, dk : dk }); - } - ); -} - -function generatePasswordDerivedKeySalt(cb) { - crypto.randomBytes(User.PBKDF2.saltLen, function onRandSalt(err, salt) { - if(err) { - cb(err); - } else { - cb(null, salt.toString('hex')); - } - }); -} - -function generatePasswordDerivedKey(password, salt, cb) { - password = new Buffer(password).toString('hex'); - crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, 'sha1', function onDerivedKey(err, dk) { - if(err) { - cb(err); - } else { - cb(null, dk.toString('hex')); - } - }); -} - -function loadProperties(options, cb) { - assert(options.userId); - - var sql = - 'SELECT prop_name, prop_value ' + - 'FROM user_property ' + - 'WHERE user_id = ?'; - - if(options.names) { - sql +=' AND prop_name IN("' + options.names.join('","') + '");'; - } else { - sql += ';'; + ); } - var properties = {}; - - userDb.each(sql, [ options.userId ], function onRow(err, row) { - if(err) { - cb(err); - return; - } else { - properties[row.prop_name] = row.prop_value; - } - }, function complete() { - cb(null, properties); - }); -} - -// :TODO: make this much more flexible - propValue should allow for case-insensitive compare, etc. -function getUserIdsWithProperty(propName, propValue, cb) { - var userIds = []; - - userDb.each( - 'SELECT user_id ' + - 'FROM user_property ' + - 'WHERE prop_name = ? AND prop_value = ?;', - [ propName, propValue ], - function rowEntry(err, row) { - if(!err) { - userIds.push(row.user_id); + static generatePasswordDerivedKeyAndSalt(password, cb) { + async.waterfall( + [ + function getSalt(callback) { + User.generatePasswordDerivedKeySalt( (err, salt) => { + return callback(err, salt); + }); + }, + function getDk(salt, callback) { + User.generatePasswordDerivedKey(password, salt, (err, dk) => { + return callback(err, salt, dk); + }); + } + ], + (err, salt, dk) => { + return cb(err, { salt : salt, dk : dk } ); } - }, - function complete() { - cb(null, userIds); - } - ); -} + ); + } -function getUserList(options, cb) { - var userList = []; + static generatePasswordDerivedKeySalt(cb) { + crypto.randomBytes(User.PBKDF2.saltLen, (err, salt) => { + if(err) { + return cb(err); + } + return cb(null, salt.toString('hex')); + }); + } - var orderClause = 'ORDER BY ' + (options.order || 'user_name'); + static generatePasswordDerivedKey(password, salt, cb) { + password = new Buffer(password).toString('hex'); - userDb.each( - 'SELECT id, user_name ' + - 'FROM user ' + - orderClause + ';', - function userRow(err, row) { - userList.push({ - userId : row.id, - userName : row.user_name, - }); - }, - function usersComplete(err) { - options.properties = options.properties || []; - async.map(userList, function iter(user, callback) { - userDb.each( - 'SELECT prop_name, prop_value ' + - 'FROM user_property ' + - 'WHERE user_id=? AND prop_name IN ("' + options.properties.join('","') + '");', - [ user.userId ], - function propRow(err, row) { - user[row.prop_name] = row.prop_value; - }, - function complete(err) { - callback(err, user); - } - ); - }, function propsComplete(err, transformed) { - cb(err, transformed); - }); - } - ); -} + crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, 'sha1', (err, dk) => { + if(err) { + return cb(err); + } + + return cb(null, dk.toString('hex')); + }); + } +}; diff --git a/mods/bbs_list.js b/mods/bbs_list.js index 43ff3135..e24beba6 100644 --- a/mods/bbs_list.js +++ b/mods/bbs_list.js @@ -7,7 +7,7 @@ const getModDatabasePath = require('../core/database.js').getModDatabasePath; const ViewController = require('../core/view_controller.js').ViewController; const ansi = require('../core/ansi_term.js'); const theme = require('../core/theme.js'); -const getUserName = require('../core/user.js').getUserName; +const User = require('../core/user.js'); const stringFormat = require('../core/string_format.js'); // deps @@ -284,7 +284,7 @@ exports.getModule = class BBSListModule extends MenuModule { }, function getUserNames(entriesView, callback) { async.each(self.entries, (entry, next) => { - getUserName(entry.submitterUserId, (err, username) => { + User.getUserName(entry.submitterUserId, (err, username) => { if(username) { entry.submitter = username; } else { diff --git a/mods/last_callers.js b/mods/last_callers.js index bf05fc35..27326970 100644 --- a/mods/last_callers.js +++ b/mods/last_callers.js @@ -5,9 +5,7 @@ const MenuModule = require('../core/menu_module.js').MenuModule; const ViewController = require('../core/view_controller.js').ViewController; const StatLog = require('../core/stat_log.js'); -const getUserName = require('../core/user.js').getUserName; -const loadProperties = require('../core/user.js').loadProperties; -const isRootUserId = require('../core/user.js').isRootUserId; +const User = require('../core/user.js'); const stringFormat = require('../core/string_format.js'); // deps @@ -73,7 +71,7 @@ exports.getModule = class LastCallersModule extends MenuModule { if(self.menuConfig.config.hideSysOpLogin) { const noOpLoginHistory = loginHistory.filter(lh => { - return false === isRootUserId(parseInt(lh.log_value)); // log_value=userId + return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId }); // @@ -106,11 +104,10 @@ exports.getModule = class LastCallersModule extends MenuModule { item.userId = parseInt(item.log_value); item.ts = moment(item.timestamp).format(dateTimeFormat); - getUserName(item.userId, (err, userName) => { - item.userName = userName; - getPropOpts.userId = item.userId; + User.getUserName(item.userId, (err, userName) => { + item.userName = userName; - loadProperties(getPropOpts, (err, props) => { + User.loadProperties(item.userId, getPropOpts, (err, props) => { if(!err) { item.location = props.location; item.affiliation = item.affils = props.affiliation; diff --git a/mods/msg_area_reply_fse.js b/mods/msg_area_reply_fse.js index 6bac09d1..d1cb5faa 100644 --- a/mods/msg_area_reply_fse.js +++ b/mods/msg_area_reply_fse.js @@ -2,13 +2,6 @@ 'use strict'; var FullScreenEditorModule = require('../core/fse.js').FullScreenEditorModule; -var Message = require('../core/message.js'); -var messageArea = require('../core/message_area.js'); -var user = require('../core/user.js'); - -var _ = require('lodash'); -var async = require('async'); -var assert = require('assert'); exports.getModule = AreaReplyFSEModule; diff --git a/mods/nua.js b/mods/nua.js index 31658eb3..878e0581 100644 --- a/mods/nua.js +++ b/mods/nua.js @@ -3,7 +3,7 @@ // ENiGMA½ const MenuModule = require('../core/menu_module.js').MenuModule; -const user = require('../core/user.js'); +const User = require('../core/user.js'); const theme = require('../core/theme.js'); const login = require('../core/system_menu_method.js').login; const Config = require('../core/config.js').config; @@ -61,7 +61,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { // Submit handlers // submitApplication : function(formData, extraArgs, cb) { - const newUser = new user.User(); + const newUser = new User(); newUser.username = formData.value.username; @@ -102,7 +102,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { } // :TODO: User.create() should validate email uniqueness! - newUser.create( { password : formData.value.password }, err => { + newUser.create(formData.value.password, err => { if(err) { self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); @@ -124,7 +124,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { }; } - if(user.User.AccountStatus.inactive === self.client.user.properties.account_status) { + if(User.AccountStatus.inactive === self.client.user.properties.account_status) { return self.gotoMenu(extraArgs.inactive, cb); } else { // diff --git a/mods/user_list.js b/mods/user_list.js index 07c27965..b2a88e79 100644 --- a/mods/user_list.js +++ b/mods/user_list.js @@ -2,7 +2,7 @@ 'use strict'; const MenuModule = require('../core/menu_module.js').MenuModule; -const getUserList = require('../core/user.js').getUserList; +const User = require('../core/user.js'); const ViewController = require('../core/view_controller.js').ViewController; const stringFormat = require('../core/string_format.js'); @@ -64,7 +64,7 @@ exports.getModule = class UserListModule extends MenuModule { }, function fetchUserList(callback) { // :TODO: Currently fetching all users - probably always OK, but this could be paged - getUserList(USER_LIST_OPTS, function got(err, ul) { + User.getUserList(USER_LIST_OPTS, function got(err, ul) { userList = ul; callback(err); });