/* jslint node: true */ /* eslint-disable no-console */ 'use strict'; const { printUsageAndSetExitCode, getAnswers, ExitCodes, argv, initConfigAndDatabases } = require('./oputil_common.js'); 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'); const moment = require('moment'); const fs = require('fs-extra'); exports.handleUserCommand = handleUserCommand; function initAndGetUser(userName, cb) { async.waterfall( [ function init(callback) { initConfigAndDatabases(callback); }, function getUserObject(callback) { const User = require('../../core/user.js'); User.getUserIdAndName(userName, (err, userId) => { if(err) { // try user ID if number was supplied if (_.isNumber(userName)) { return User.getUser(parseInt(userName), callback); } return callback(err); } return User.getUser(userId, callback); }); } ], (err, user) => { return cb(err, user); } ); } function setAccountStatus(user, status) { if(argv._.length < 3) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } 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]; 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) { if(argv._.length < 4) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } async.waterfall( [ function validate(callback) { // :TODO: prompt if no password provided (more secure, no history, etc.) const password = argv._[argv._.length - 1]; if(0 === password.length) { return callback(Errors.Invalid('Invalid password')); } return callback(null, password); }, function set(password, callback) { user.setNewAuthCredentials(password, err => { if(err) { process.exitCode = ExitCodes.BAD_ARGS; } return callback(err); }); } ], err => { if(err) { console.error(err.message); } else { console.info('New password set'); } } ); } function removeUserRecordsFromDbAndTable(dbName, tableName, userId, col, cb) { const db = require('../../core/database.js').dbs[dbName]; db.run( `DELETE FROM ${tableName} WHERE ${col} = ?;`, [ userId ], err => { return cb(err); } ); } function removeUser(user) { async.series( [ (callback) => { if(user.isRoot()) { return callback(Errors.Invalid('Cannot delete root/SysOp user!')); } return callback(null); }, (callback) => { if(false === argv.prompt) { return callback(null); } console.info('About to permanently delete the following user:'); console.info(`Username : ${user.username}`); console.info(`Real name: ${user.properties[UserProps.RealName] || 'N/A'}`); console.info(`User ID : ${user.userId}`); console.info('WARNING: This cannot be undone!'); getAnswers([ { name : 'proceed', message : `Proceed in deleting ${user.username}?`, type : 'confirm', } ], answers => { if(answers.proceed) { return callback(null); } return callback(Errors.General('User canceled')); }); }, (callback) => { // op has confirmed they are wanting ready to proceed (or passed --no-prompt) const DeleteFrom = { message : [ 'user_message_area_last_read' ], system : [ 'user_event_log', ], user : [ 'user_group_member', 'user' ], }; async.eachSeries(Object.keys(DeleteFrom), (dbName, nextDbName) => { const tables = DeleteFrom[dbName]; async.eachSeries(tables, (tableName, nextTableName) => { const col = ('user' === dbName && 'user' === tableName) ? 'id' : 'user_id'; removeUserRecordsFromDbAndTable(dbName, tableName, user.userId, col, err => { return nextTableName(err); }); }, err => { return nextDbName(err); }); }, err => { return callback(err); }); }, (callback) => { // // Clean up *private* messages *to* this user // const Message = require('../../core/message.js'); const MsgDb = require('../../core/database.js').dbs.message; const filter = { resultType : 'id', privateTagUserId : user.userId, }; Message.findMessages(filter, (err, ids) => { if(err) { return callback(err); } async.eachSeries(ids, (messageId, nextMessageId) => { MsgDb.run( `DELETE FROM message WHERE message_id = ?;`, [ messageId ], err => { return nextMessageId(err); } ); }, err => { return callback(err); }); }); } ], err => { if(err) { return console.error(err.reason ? err.reason : err.message); } console.info('User has been deleted.'); } ); } function renameUser(user) { if(argv._.length < 3) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } const newUserName = argv._[argv._.length - 1]; async.series( [ (callback) => { const { validateUserNameAvail } = require('../../core/system_view_validate.js'); return validateUserNameAvail(newUserName, callback); }, (callback) => { const userDb = require('../../core/database.js').dbs.user; userDb.run( `UPDATE user SET user_name = ? WHERE id = ?;`, [ newUserName, user.userId, ], err => { return callback(err); } ); } ], err => { if(err) { return console.error(err.reason ? err.reason : err.message); } return console.info(`User "${user.username}" renamed to "${newUserName}"`); } ); } function modUserGroups(user) { if(argv._.length < 3) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } let groupName = argv._[argv._.length - 1].toString().replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo" let action = groupName[0]; // + or - if('-' === action || '+' === action) { groupName = groupName.substr(1); } action = action || '+'; if(0 === groupName.length) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } // // Groups are currently arbritary, so do a slight validation // if(!/[A-Za-z0-9]+/.test(groupName)) { process.exitCode = ExitCodes.BAD_ARGS; return console.error('Bad group name'); } function done(err) { if(err) { process.exitCode = ExitCodes.BAD_ARGS; console.error(err.message); } else { console.info('User groups modified'); } } const UserGroup = require('../../core/user_group.js'); if('-' === action) { UserGroup.removeUserFromGroup(user.userId, groupName, done); } else { UserGroup.addUserToGroup(user.userId, groupName, done); } } function showUserInfo(user) { const User = require('../../core/user.js'); const statusDesc = () => { const status = user.properties[UserProps.AccountStatus]; return _.invert(User.AccountStatus)[status] || 'unknown'; }; const created = () => { const ac = user.properties[UserProps.AccountCreated]; return ac ? moment(ac).format() : 'N/A'; }; const lastLogin = () => { const ll = user.properties[UserProps.LastLoginTs]; return ll ? moment(ll).format() : 'N/A'; }; const propOrNA = p => { return user.properties[p] || 'N/A'; }; const currentTheme = () => { return user.properties[UserProps.ThemeId]; }; const stdInfo = `User information: Username : ${user.username}${user.isRoot() ? ' (root/SysOp)' : ''} Real name : ${propOrNA(UserProps.RealName)} ID : ${user.userId} Status : ${statusDesc()} Groups : ${user.groups.join(', ')} Theme ID : ${currentTheme()} Created : ${created()} Last login : ${lastLogin()} Login count : ${propOrNA(UserProps.LoginCount)} Email : ${propOrNA(UserProps.EmailAddress)} Location : ${propOrNA(UserProps.Location)} Affiliations : ${propOrNA(UserProps.Affiliations)}`; let secInfo = ''; if(argv.security) { const otp = user.getProperty(UserProps.AuthFactor2OTP); if(otp) { const backupCodesOrNa = () => { try { return JSON.parse(user.getProperty(UserProps.AuthFactor2OTPBackupCodes)).join(', '); } catch(e) { return 'N/A'; } }; secInfo = `\n2FA OTP : ${otp} OTP secret : ${user.getProperty(UserProps.AuthFactor2OTPSecret) || 'N/A'} OTP Backup : ${backupCodesOrNa()}`; } } console.info(`${stdInfo}${secInfo}`); } function twoFactorAuthOTP(user) { if(argv._.length < 4) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } const { OTPTypes, prepareOTP, createBackupCodes, } = require('../../core/user_2fa_otp.js'); let otpType = argv._[argv._.length - 1]; // shortcut for removal if('disable' === otpType) { const props = [ UserProps.AuthFactor2OTP, UserProps.AuthFactor2OTPSecret, UserProps.AuthFactor2OTPBackupCodes, ]; return user.removeProperties(props, err => { if(err) { console.error(err.message); } else { console.info(`2FA OTP disabled for ${user.username}`); } }); } async.waterfall( [ function validate(callback) { // :TODO: Prompt for if not supplied // allow aliases for OTP types otpType = { google : OTPTypes.GoogleAuthenticator, hotp : OTPTypes.RFC4266_HOTP, totp : OTPTypes.RFC6238_TOTP, }[otpType] || otpType; otpType = _.find(OTPTypes, t => { return t.toLowerCase() === otpType.toLowerCase(); }); if(!otpType) { return callback(Errors.Invalid('Invalid OTP type')); } return callback(null, otpType); }, function prepare(otpType, callback) { const otpOpts = { username : user.username, qrType : argv['qr-type'] || 'ascii', }; prepareOTP(otpType, otpOpts, (err, otpInfo) => { return callback(err, Object.assign(otpInfo, { otpType, backupCodes : createBackupCodes() })); }); }, function storeOrDisplayQR(otpInfo, callback) { if(!argv.out || !otpInfo.qr) { return callback(null, otpInfo); } fs.writeFile(argv.out, otpInfo.qr, 'utf8', err => { return callback(err, otpInfo); }); }, function persist(otpInfo, callback) { const props = { [ UserProps.AuthFactor2OTP ] : otpInfo.otpType, [ UserProps.AuthFactor2OTPSecret ] : otpInfo.secret, [ UserProps.AuthFactor2OTPBackupCodes ] : JSON.stringify(otpInfo.backupCodes), }; user.persistProperties(props, err => { return callback(err, otpInfo); }); } ], (err, otpInfo) => { if(err) { console.error(err.message); } else { console.info(`OTP enabled for : ${user.username}`); console.info(`Secret : ${otpInfo.secret}`); console.info(`Backup codes : ${otpInfo.backupCodes.join(', ')}`); if(otpInfo.qr) { if(!argv.out) { console.info('--- Begin QR ---'); console.info(otpInfo.qr); console.info('--- End QR ---'); } else { console.info(`QR code saved to ${argv.out}`); } } } } ); } function listUsers() { // oputil user list [disabled|inactive|active|locked|all] // :TODO: --created-since SPEC and --last-called SPEC // --created-since SPEC // SPEC can be TIMESTAMP or e.g. "-1hour" or "-90days" // :TODO: --sort name|id let listWhat; if (argv._.length > 2) { listWhat = argv._[argv._.length - 1]; } else { listWhat = 'all'; } const User = require('../../core/user'); if (![ 'all' ].concat(Object.keys(User.AccountStatus)).includes(listWhat)) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } async.waterfall( [ (callback) => { const UserProps = require('../../core/user_property'); const userListOpts = { properties : [ UserProps.AccountStatus, ], }; User.getUserList(userListOpts, (err, userList) => { if (err) { return callback(err); } if ('all' === listWhat) { return callback(null, userList); } const accountStatusFilter = User.AccountStatus[listWhat].toString(); return callback(null, userList.filter(user => { return user[UserProps.AccountStatus] === accountStatusFilter; })); }); }, (userList, callback) => { userList.forEach(user => { console.info(`${user.userId}: ${user.userName}`); }); return callback(null); }, ], err => { if(err) { return console.error(err.reason ? err.reason : err.message); } } ); } function handleUserCommand() { function errUsage() { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } if(true === argv.help) { return errUsage(); } const action = argv._[1]; const userRequired = ![ 'list' ].includes(action); let userName; if (userRequired) { const usernameIdx = [ 'pw', 'pass', 'passwd', 'password', 'group', 'mv', 'rename', '2fa-otp', 'otp' ].includes(action) ? argv._.length - 2 : argv._.length - 1; userName = argv._[usernameIdx]; } if(!userName && userRequired) { return errUsage(); } initAndGetUser(userName, (err, user) => { if(userName && err) { process.exitCode = ExitCodes.ERROR; return console.error(err.message); } return ({ pw : setUserPassword, passwd : setUserPassword, password : setUserPassword, rm : removeUser, remove : removeUser, del : removeUser, delete : removeUser, mv : renameUser, rename : renameUser, activate : setAccountStatus, deactivate : setAccountStatus, disable : setAccountStatus, lock : setAccountStatus, group : modUserGroups, info : showUserInfo, '2fa-otp' : twoFactorAuthOTP, otp : twoFactorAuthOTP, list : listUsers, }[action] || errUsage)(user, action); }); }