feat: cli user management commands (#9848)
* feat: cli user management commands * fix: consistent nomenclatureisekai-main
parent
9215c7d1e1
commit
d1ff3d6298
@ -0,0 +1,310 @@
|
||||
'use strict';
|
||||
|
||||
const { Command, Option } = require('commander');
|
||||
|
||||
module.exports = () => {
|
||||
const userCmd = new Command('user')
|
||||
.description('Manage users')
|
||||
.arguments('[command]');
|
||||
|
||||
userCmd.configureHelp(require('./colors'));
|
||||
const userCommands = UserCommands();
|
||||
|
||||
userCmd
|
||||
.command('info')
|
||||
.description('Display user info by uid/username/userslug.')
|
||||
.option('-i, --uid <uid>', 'Retrieve user by uid')
|
||||
.option('-u, --username <username>', 'Retrieve user by username')
|
||||
.option('-s, --userslug <userslug>', 'Retrieve user by userslug')
|
||||
.action((...args) => execute(userCommands.info, args));
|
||||
userCmd
|
||||
.command('create')
|
||||
.description('Create a new user.')
|
||||
.arguments('<username>')
|
||||
.option('-p, --password <password>', 'Set a new password. (Auto-generates if omitted)')
|
||||
.option('-e, --email <email>', 'Associate with an email.')
|
||||
.action((...args) => execute(userCommands.create, args));
|
||||
userCmd
|
||||
.command('reset')
|
||||
.description('Reset a user\'s password or send a password reset email.')
|
||||
.arguments('<uid>')
|
||||
.option('-p, --password <password>', 'Set a new password. (Auto-generates if passed empty)', false)
|
||||
.option('-s, --send-reset-email', 'Send a password reset email.', false)
|
||||
.action((...args) => execute(userCommands.reset, args));
|
||||
userCmd
|
||||
.command('delete')
|
||||
.description('Delete user(s) and/or their content')
|
||||
.arguments('<uids...>')
|
||||
.addOption(
|
||||
new Option('-c, --content [operation]', 'Delete user content ([purge]), leave content ([account]), or delete content only ([content])')
|
||||
.choices(['purge', 'account', 'content']).default('purge')
|
||||
)
|
||||
.action((...args) => execute(userCommands.deleteUser, args));
|
||||
|
||||
const make = userCmd.command('make')
|
||||
.description('Make user(s) admin, global mod, moderator or a regular user.')
|
||||
.arguments('[command]');
|
||||
|
||||
make.command('admin')
|
||||
.description('Make user(s) an admin')
|
||||
.arguments('<uids...>')
|
||||
.action((...args) => execute(userCommands.makeAdmin, args));
|
||||
make.command('global-mod')
|
||||
.description('Make user(s) a global moderator')
|
||||
.arguments('<uids...>')
|
||||
.action((...args) => execute(userCommands.makeGlobalMod, args));
|
||||
make.command('mod')
|
||||
.description('Make uid(s) of user(s) moderator of given category IDs (cids)')
|
||||
.arguments('<uids...>')
|
||||
.requiredOption('-c, --cid <cids...>', 'ID(s) of categories to make the user a moderator of')
|
||||
.action((...args) => execute(userCommands.makeMod, args));
|
||||
make.command('regular')
|
||||
.description('Make user(s) a non-privileged user')
|
||||
.arguments('<uids...>')
|
||||
.action((...args) => execute(userCommands.makeRegular, args));
|
||||
|
||||
return userCmd;
|
||||
};
|
||||
|
||||
let db;
|
||||
let user;
|
||||
let groups;
|
||||
let privileges;
|
||||
let privHelpers;
|
||||
let utils;
|
||||
let winston;
|
||||
|
||||
async function init() {
|
||||
db = require('../database');
|
||||
await db.init();
|
||||
|
||||
user = require('../user');
|
||||
groups = require('../groups');
|
||||
privileges = require('../privileges');
|
||||
privHelpers = require('../privileges/helpers');
|
||||
utils = require('../utils');
|
||||
winston = require('winston');
|
||||
}
|
||||
|
||||
async function execute(cmd, args) {
|
||||
await init();
|
||||
try {
|
||||
await cmd(...args);
|
||||
} catch (err) {
|
||||
const userError = err.name === 'UserError';
|
||||
winston.error(`[userCmd/${cmd.name}] ${userError ? `${err.message}` : 'Command failed.'}`, userError ? '' : err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
function UserCmdHelpers() {
|
||||
async function getAdminUidOrFail() {
|
||||
const adminUid = (await db.getSortedSetMembers('group:administrators:members')).reverse()[0];
|
||||
if (!adminUid) {
|
||||
const err = new Error('An admin account does not exists to execute the operation.');
|
||||
err.name = 'UserError';
|
||||
throw err;
|
||||
}
|
||||
return adminUid;
|
||||
}
|
||||
|
||||
async function setupApp() {
|
||||
const nconf = require('nconf');
|
||||
const Benchpress = require('benchpressjs');
|
||||
|
||||
const meta = require('../meta');
|
||||
await meta.configs.init();
|
||||
|
||||
const webserver = require('../webserver');
|
||||
const viewsDir = nconf.get('views_dir');
|
||||
|
||||
webserver.app.engine('tpl', (filepath, data, next) => {
|
||||
filepath = filepath.replace(/\.tpl$/, '.js');
|
||||
|
||||
Benchpress.__express(filepath, data, next);
|
||||
});
|
||||
webserver.app.set('view engine', 'tpl');
|
||||
webserver.app.set('views', viewsDir);
|
||||
|
||||
const emailer = require('../emailer');
|
||||
emailer.registerApp(webserver.app);
|
||||
}
|
||||
|
||||
const argParsers = {
|
||||
intParse: (value, varName) => {
|
||||
const parsedValue = parseInt(value, 10);
|
||||
if (isNaN(parsedValue)) {
|
||||
const err = new Error(`"${varName}" expected to be a number.`);
|
||||
err.name = 'UserError';
|
||||
throw err;
|
||||
}
|
||||
return parsedValue;
|
||||
},
|
||||
intArrayParse: (values, varName) => values.map(value => argParsers.intParse(value, varName)),
|
||||
};
|
||||
|
||||
return {
|
||||
argParsers,
|
||||
getAdminUidOrFail,
|
||||
setupApp,
|
||||
};
|
||||
}
|
||||
|
||||
function UserCommands() {
|
||||
const { argParsers, getAdminUidOrFail, setupApp } = UserCmdHelpers();
|
||||
|
||||
async function info({ uid, username, userslug }) {
|
||||
if (!uid && !username && !userslug) {
|
||||
return winston.error('[userCmd/info] At least one option has to be passed (--uid, --username or --userslug).');
|
||||
}
|
||||
|
||||
if (uid) {
|
||||
uid = argParsers.intParse(uid, 'uid');
|
||||
} else if (username) {
|
||||
uid = await user.getUidByUsername(username);
|
||||
} else {
|
||||
uid = await user.getUidByUserslug(userslug);
|
||||
}
|
||||
|
||||
const userData = await user.getUserData(uid);
|
||||
winston.info('[userCmd/info] User info retrieved:');
|
||||
console.log(userData);
|
||||
}
|
||||
|
||||
async function create(username, { password, email }) {
|
||||
let pwGenerated = false;
|
||||
if (password === undefined) {
|
||||
password = utils.generateUUID().slice(0, 8);
|
||||
pwGenerated = true;
|
||||
}
|
||||
|
||||
const userExists = await user.getUidByUsername(username);
|
||||
if (userExists) {
|
||||
return winston.error(`[userCmd/create] A user with username '${username}' already exists`);
|
||||
}
|
||||
|
||||
const uid = await user.create({
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
});
|
||||
|
||||
winston.info(`[userCmd/create] User '${username}'${password ? '' : ' without a password'} has been created with uid: ${uid}.\
|
||||
${pwGenerated ? ` Generated password: ${password}` : ''}`);
|
||||
}
|
||||
|
||||
async function reset(uid, { password, sendResetEmail }) {
|
||||
uid = argParsers.intParse(uid, 'uid');
|
||||
|
||||
if (password === false && sendResetEmail === false) {
|
||||
return winston.error('[userCmd/reset] At least one option has to be passed (--password or --send-reset-email).');
|
||||
}
|
||||
|
||||
const userExists = await user.exists(uid);
|
||||
if (!userExists) {
|
||||
return winston.error(`[userCmd/reset] A user with given uid does not exists.`);
|
||||
}
|
||||
|
||||
let pwGenerated = false;
|
||||
if (password === '') {
|
||||
password = utils.generateUUID().slice(0, 8);
|
||||
pwGenerated = true;
|
||||
}
|
||||
|
||||
const adminUid = await getAdminUidOrFail();
|
||||
|
||||
if (password) {
|
||||
await user.changePassword(adminUid, {
|
||||
newPassword: password,
|
||||
uid,
|
||||
});
|
||||
winston.info(`[userCmd/reset] ${password ? 'User password changed.' : ''}${pwGenerated ? ` Generated password: ${password}` : ''}`);
|
||||
}
|
||||
|
||||
if (sendResetEmail) {
|
||||
const userEmail = await user.getUserField(uid, 'email');
|
||||
if (!userEmail) {
|
||||
return winston.error('User doesn\'t have an email address to send reset email.');
|
||||
}
|
||||
await setupApp();
|
||||
await user.reset.send(userEmail);
|
||||
winston.info('[userCmd/reset] Password reset email has been sent.');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(uids, { content }) {
|
||||
uids = argParsers.intArrayParse(uids, 'uids');
|
||||
|
||||
const userExists = await user.exists(uids);
|
||||
if (!userExists || userExists.some(r => r === false)) {
|
||||
return winston.error(`[userCmd/reset] A user with given uid does not exists.`);
|
||||
}
|
||||
|
||||
await db.initSessionStore();
|
||||
const adminUid = await getAdminUidOrFail();
|
||||
|
||||
switch (content) {
|
||||
case 'purge':
|
||||
await Promise.all(uids.map(uid => user.delete(adminUid, uid)));
|
||||
winston.info(`[userCmd/delete] User(s) with their content has been deleted.`);
|
||||
break;
|
||||
case 'account':
|
||||
await Promise.all(uids.map(uid => user.deleteAccount(uid)));
|
||||
winston.info(`[userCmd/delete] User(s) has been deleted, their content left intact.`);
|
||||
break;
|
||||
case 'content':
|
||||
await Promise.all(uids.map(uid => user.deleteContent(adminUid, uid)));
|
||||
winston.info(`[userCmd/delete] User(s)' content has been deleted.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function makeAdmin(uids) {
|
||||
uids = argParsers.intArrayParse(uids, 'uids');
|
||||
await Promise.all(uids.map(uid => groups.join('administrators', uid)));
|
||||
|
||||
winston.info('[userCmd/make/admin] User(s) added as administrators.');
|
||||
}
|
||||
|
||||
async function makeGlobalMod(uids) {
|
||||
uids = argParsers.intArrayParse(uids, 'uids');
|
||||
await Promise.all(uids.map(uid => groups.join('Global Moderators', uid)));
|
||||
|
||||
winston.info('[userCmd/make/globalMod] User(s) added as global moderators.');
|
||||
}
|
||||
|
||||
async function makeMod(uids, { cid: cids }) {
|
||||
uids = argParsers.intArrayParse(uids, 'uids');
|
||||
cids = argParsers.intArrayParse(cids, 'cids');
|
||||
|
||||
const categoryPrivList = await privileges.categories.getPrivilegeList();
|
||||
await privHelpers.giveOrRescind(groups.join, categoryPrivList, cids, uids);
|
||||
|
||||
winston.info('[userCmd/make/mod] User(s) added as moderators to given categories.');
|
||||
}
|
||||
|
||||
async function makeRegular(uids) {
|
||||
uids = argParsers.intArrayParse(uids, 'uids');
|
||||
|
||||
await Promise.all(uids.map(uid => groups.leave(['administrators', 'Global Moderators'], uid)));
|
||||
|
||||
const categoryPrivList = await privileges.categories.getPrivilegeList();
|
||||
const cids = await db.getSortedSetRevRange('categories:cid', 0, -1);
|
||||
await privHelpers.giveOrRescind(groups.leave, categoryPrivList, cids, uids);
|
||||
|
||||
winston.info('[userCmd/make/regular] User(s) made regular/non-privileged.');
|
||||
}
|
||||
|
||||
return {
|
||||
info,
|
||||
create,
|
||||
reset,
|
||||
deleteUser,
|
||||
makeAdmin,
|
||||
makeGlobalMod,
|
||||
makeMod,
|
||||
makeRegular,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue