683 lines
24 KiB
JavaScript
683 lines
24 KiB
JavaScript
const modulename = 'AdminStore';
|
|
import fs from 'node:fs';
|
|
import fsp from 'node:fs/promises';
|
|
import { cloneDeep } from 'lodash-es';
|
|
import { nanoid } from 'nanoid';
|
|
import { txHostConfig } from '@core/globalData';
|
|
import CfxProvider from './providers/CitizenFX.js';
|
|
import { createHash } from 'node:crypto';
|
|
import consoleFactory from '@lib/console.js';
|
|
import fatalError from '@lib/fatalError.js';
|
|
import { chalkInversePad } from '@lib/misc.js';
|
|
const console = consoleFactory(modulename);
|
|
|
|
//NOTE: The way I'm doing versioning right now is horrible but for now it's the best I can do
|
|
//NOTE: I do not need to version every admin, just the file itself
|
|
const ADMIN_SCHEMA_VERSION = 1;
|
|
|
|
|
|
//Helpers
|
|
const migrateProviderIdentifiers = (providerName, providerData) => {
|
|
if (providerName === 'citizenfx') {
|
|
// data may be empty, or nameid may be invalid
|
|
try {
|
|
const res = /\/user\/(\d{1,8})/.exec(providerData.data.nameid);
|
|
providerData.identifier = `fivem:${res[1]}`;
|
|
} catch (error) {
|
|
providerData.identifier = 'fivem:00000000';
|
|
}
|
|
} else if (providerName === 'discord') {
|
|
providerData.identifier = `discord:${providerData.id}`;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Module responsible for storing, retrieving and validating admins data.
|
|
*/
|
|
export default class AdminStore {
|
|
constructor() {
|
|
this.adminsFile = txHostConfig.dataSubPath('admins.json');
|
|
this.adminsFileHash = null;
|
|
this.admins = null;
|
|
this.refreshRoutine = null;
|
|
|
|
//Not alphabetical order, but that's fine
|
|
//FIXME: move to a separate file
|
|
//TODO: maybe put in @shared so the frontend's UnauthorizedPage can use it
|
|
//TODO: when migrating the admins page to react, definitely put this in @shared so the front rendering doesn't depend on the backend response - lessons learned from the settings page.
|
|
//FIXME: if not using enums, definitely use so other type of type safety
|
|
//FIXME: maybe rename all_permissions to `administrator` (just like discord) or `super_admin` and rename the `Admins` page to `Users`. This fits better with how people use txAdmin as "mods" are not really admins
|
|
this.registeredPermissions = {
|
|
'all_permissions': 'All Permissions',
|
|
'manage.admins': 'Manage Admins', //will enable the "set admin" button in the player modal
|
|
'settings.view': 'Settings: View (no tokens)',
|
|
'settings.write': 'Settings: Change',
|
|
'console.view': 'Console: View',
|
|
'console.write': 'Console: Write',
|
|
'control.server': 'Start/Stop Server + Scheduler', //FIXME: horrible name
|
|
'announcement': 'Send Announcements',
|
|
'commands.resources': 'Start/Stop Resources',
|
|
'server.cfg.editor': 'Read/Write server.cfg', //FIXME: rename to server.cfg_editor
|
|
'txadmin.log.view': 'View System Logs', //FIXME: rename to system.log.view
|
|
'server.log.view': 'View Server Logs',
|
|
|
|
'menu.vehicle': 'Spawn / Fix Vehicles',
|
|
'menu.clear_area': 'Reset world area',
|
|
'menu.viewids': 'View Player IDs in-game', //be able to see the ID of the players
|
|
'players.direct_message': 'Direct Message',
|
|
'players.whitelist': 'Whitelist',
|
|
'players.warn': 'Warn',
|
|
'players.kick': 'Kick',
|
|
'players.ban': 'Ban',
|
|
'players.freeze': 'Freeze Players',
|
|
'players.heal': 'Heal', //self, everyone, and the "heal" button in player modal
|
|
'players.playermode': 'NoClip / God Mode', //self playermode, and also the player spectate option
|
|
'players.spectate': 'Spectate', //self playermode, and also the player spectate option
|
|
'players.teleport': 'Teleport', //self teleport, and the bring/go to on player modal
|
|
'players.troll': 'Troll Actions', //all the troll options in the player modal
|
|
};
|
|
//FIXME: pode remover, hardcode na cron function
|
|
this.hardConfigs = {
|
|
refreshInterval: 15e3,
|
|
};
|
|
|
|
|
|
//Load providers
|
|
//FIXME: pode virar um top-level singleton , não precisa estar na classe
|
|
try {
|
|
this.providers = {
|
|
discord: false,
|
|
citizenfx: new CfxProvider(),
|
|
};
|
|
} catch (error) {
|
|
throw new Error(`Failed to load providers with error: ${error.message}`);
|
|
}
|
|
|
|
//Check if admins file exists
|
|
let adminFileExists;
|
|
try {
|
|
fs.statSync(this.adminsFile, fs.constants.F_OK);
|
|
adminFileExists = true;
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
adminFileExists = false;
|
|
} else {
|
|
throw new Error(`Failed to check presence of admin file with error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
//Printing PIN or starting loop
|
|
if (!adminFileExists) {
|
|
if (!txHostConfig.defaults.account) {
|
|
this.addMasterPin = (Math.random() * 10000).toFixed().padStart(4, '0');
|
|
this.admins = false;
|
|
} else {
|
|
const { username, fivemId, password } = txHostConfig.defaults.account;
|
|
this.createAdminsFile(
|
|
username,
|
|
fivemId ? `fivem:${fivemId}` : undefined,
|
|
undefined,
|
|
password,
|
|
password ? false : undefined,
|
|
);
|
|
console.ok(`Created master account ${chalkInversePad(username)} with credentials provided by ${txHostConfig.sourceName}.`);
|
|
}
|
|
} else {
|
|
this.loadAdminsFile();
|
|
this.setupRefreshRoutine();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* sets the admins file refresh routine
|
|
*/
|
|
setupRefreshRoutine() {
|
|
this.refreshRoutine = setInterval(() => {
|
|
this.checkAdminsFile();
|
|
}, this.hardConfigs.refreshInterval);
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates a admins.json file based on the first account
|
|
* @param {string} username
|
|
* @param {string|undefined} fivemId with the fivem: prefix
|
|
* @param {string|undefined} discordId with the discord: prefix
|
|
* @param {string|undefined} password backup password
|
|
* @param {boolean|undefined} isPlainTextPassword
|
|
* @returns {(boolean)} true or throws an error
|
|
*/
|
|
createAdminsFile(username, fivemId, discordId, password, isPlainTextPassword) {
|
|
//Sanity check
|
|
if (this.admins !== false && this.admins !== null) throw new Error('Admins file already exists.');
|
|
if (typeof username !== 'string' || username.length < 3) throw new Error('Invalid username parameter.');
|
|
|
|
//Handling password
|
|
let password_hash, password_temporary;
|
|
if(password){
|
|
password_hash = isPlainTextPassword ? GetPasswordHash(password) : password;
|
|
// password_temporary = false; //undefined will do the same
|
|
} else {
|
|
const veryRandomString = `${username}-password-not-meant-to-be-used-${nanoid()}`;
|
|
password_hash = GetPasswordHash(veryRandomString);
|
|
password_temporary = true;
|
|
}
|
|
|
|
//Handling third party providers
|
|
const providers = {};
|
|
if (fivemId) {
|
|
providers.citizenfx = {
|
|
id: username,
|
|
identifier: fivemId,
|
|
data: {},
|
|
};
|
|
}
|
|
if (discordId) {
|
|
providers.discord = {
|
|
id: discordId,
|
|
identifier: `discord:${discordId}`,
|
|
data: {},
|
|
};
|
|
}
|
|
|
|
//Creating new admin
|
|
const newAdmin = {
|
|
$schema: ADMIN_SCHEMA_VERSION,
|
|
name: username,
|
|
master: true,
|
|
password_hash,
|
|
password_temporary,
|
|
providers,
|
|
permissions: [],
|
|
};
|
|
this.admins = [newAdmin];
|
|
this.addMasterPin = undefined;
|
|
|
|
//Saving admin file
|
|
try {
|
|
const jsonData = JSON.stringify(this.admins);
|
|
this.adminsFileHash = createHash('sha1').update(jsonData).digest('hex');
|
|
fs.writeFileSync(this.adminsFile, jsonData, { encoding: 'utf8', flag: 'wx' });
|
|
this.setupRefreshRoutine();
|
|
return newAdmin;
|
|
} catch (error) {
|
|
let message = `Failed to create '${this.adminsFile}' with error: ${error.message}`;
|
|
console.verbose.error(message);
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a list of admins and permissions
|
|
*/
|
|
getAdminsList() {
|
|
if (this.admins == false) return [];
|
|
return this.admins.map((user) => {
|
|
return {
|
|
name: user.name,
|
|
master: user.master,
|
|
providers: Object.keys(user.providers),
|
|
permissions: user.permissions,
|
|
};
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the raw array of admins, except for the hash
|
|
*/
|
|
getRawAdminsList() {
|
|
if (this.admins === false) return [];
|
|
return cloneDeep(this.admins);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns all data from an admin by provider user id (ex discord id), or false
|
|
* @param {string} uid
|
|
*/
|
|
getAdminByProviderUID(uid) {
|
|
if (this.admins == false) return false;
|
|
let id = uid.trim().toLowerCase();
|
|
if (!id.length) return false;
|
|
let admin = this.admins.find((user) => {
|
|
return Object.keys(user.providers).find((provider) => {
|
|
return (id === user.providers[provider].id.toLowerCase());
|
|
});
|
|
});
|
|
return (admin) ? cloneDeep(admin) : false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns an array with all identifiers of the admins (fivem/discord)
|
|
*/
|
|
getAdminsIdentifiers() {
|
|
if (this.admins === false) return [];
|
|
const ids = [];
|
|
for (const admin of this.admins) {
|
|
admin.providers.citizenfx && ids.push(admin.providers.citizenfx.identifier);
|
|
admin.providers.discord && ids.push(admin.providers.discord.identifier);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns all data from an admin by their name, or false
|
|
* @param {string} uname
|
|
*/
|
|
getAdminByName(uname) {
|
|
if (!this.admins) return false;
|
|
const username = uname.trim().toLowerCase();
|
|
if (!username.length) return false;
|
|
const admin = this.admins.find((user) => {
|
|
return (username === user.name.toLowerCase());
|
|
});
|
|
return (admin) ? cloneDeep(admin) : false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns all data from an admin by game identifier, or false
|
|
* @param {string[]} identifiers
|
|
*/
|
|
getAdminByIdentifiers(identifiers) {
|
|
if (!this.admins) return false;
|
|
identifiers = identifiers
|
|
.map((i) => i.trim().toLowerCase())
|
|
.filter((i) => i.length);
|
|
if (!identifiers.length) return false;
|
|
const admin = this.admins.find((user) =>
|
|
identifiers.find((identifier) =>
|
|
Object.keys(user.providers).find((provider) =>
|
|
(identifier === user.providers[provider].identifier.toLowerCase()))));
|
|
return (admin) ? cloneDeep(admin) : false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a list with all registered permissions
|
|
*/
|
|
getPermissionsList() {
|
|
return cloneDeep(this.registeredPermissions);
|
|
}
|
|
|
|
|
|
/**
|
|
* Writes to storage the admins file
|
|
*/
|
|
async writeAdminsFile() {
|
|
const jsonData = JSON.stringify(this.admins, null, 2);
|
|
this.adminsFileHash = createHash('sha1').update(jsonData).digest('hex');
|
|
await fsp.writeFile(this.adminsFile, jsonData, 'utf8');
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Writes to storage the admins file
|
|
*/
|
|
async checkAdminsFile() {
|
|
const restore = async () => {
|
|
try {
|
|
await this.writeAdminsFile();
|
|
console.ok('Restored admins.json file.');
|
|
} catch (error) {
|
|
console.error(`Failed to restore admins.json file: ${error.message}`);
|
|
console.verbose.dir(error);
|
|
}
|
|
};
|
|
try {
|
|
const jsonData = await fsp.readFile(this.adminsFile, 'utf8');
|
|
const inboundHash = createHash('sha1').update(jsonData).digest('hex');
|
|
if (this.adminsFileHash !== inboundHash) {
|
|
console.warn('The admins.json file was modified or deleted by an external source, txAdmin will try to restore it.');
|
|
restore();
|
|
}
|
|
} catch (error) {
|
|
console.error(`Cannot check admins file integrity: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Add a new admin to the admins file
|
|
* NOTE: I'm fully aware this coud be optimized. Leaving this way to improve readability and error verbosity
|
|
* @param {string} name
|
|
* @param {object|undefined} citizenfxData or false
|
|
* @param {object|undefined} discordData or false
|
|
* @param {string} password
|
|
* @param {array} permissions
|
|
*/
|
|
async addAdmin(name, citizenfxData, discordData, password, permissions) {
|
|
if (this.admins == false) throw new Error('Admins not set');
|
|
|
|
//Check if username is already taken
|
|
if (this.getAdminByName(name)) throw new Error('Username already taken');
|
|
|
|
//Preparing admin
|
|
const admin = {
|
|
$schema: ADMIN_SCHEMA_VERSION,
|
|
name,
|
|
master: false,
|
|
password_hash: GetPasswordHash(password),
|
|
password_temporary: true,
|
|
providers: {},
|
|
permissions,
|
|
};
|
|
|
|
//Check if provider uid already taken and inserting into admin object
|
|
if (citizenfxData) {
|
|
const existingCitizenFX = this.getAdminByProviderUID(citizenfxData.id);
|
|
if (existingCitizenFX) throw new Error('CitizenFX ID already taken');
|
|
admin.providers.citizenfx = {
|
|
id: citizenfxData.id,
|
|
identifier: citizenfxData.identifier,
|
|
data: {},
|
|
};
|
|
}
|
|
if (discordData) {
|
|
const existingDiscord = this.getAdminByProviderUID(discordData.id);
|
|
if (existingDiscord) throw new Error('Discord ID already taken');
|
|
admin.providers.discord = {
|
|
id: discordData.id,
|
|
identifier: discordData.identifier,
|
|
data: {},
|
|
};
|
|
}
|
|
|
|
//Saving admin file
|
|
this.admins.push(admin);
|
|
this.refreshOnlineAdmins().catch((e) => { });
|
|
try {
|
|
return await this.writeAdminsFile();
|
|
} catch (error) {
|
|
throw new Error(`Failed to save admins.json with error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Edit admin and save to the admins file
|
|
* @param {string} name
|
|
* @param {string|null} password
|
|
* @param {object|false} [citizenfxData] or false
|
|
* @param {object|false} [discordData] or false
|
|
* @param {string[]} [permissions]
|
|
*/
|
|
async editAdmin(name, password, citizenfxData, discordData, permissions) {
|
|
if (this.admins == false) throw new Error('Admins not set');
|
|
|
|
//Find admin index
|
|
let username = name.toLowerCase();
|
|
let adminIndex = this.admins.findIndex((user) => {
|
|
return (username === user.name.toLowerCase());
|
|
});
|
|
if (adminIndex == -1) throw new Error('Admin not found');
|
|
|
|
//Editing admin
|
|
if (password !== null) {
|
|
this.admins[adminIndex].password_hash = GetPasswordHash(password);
|
|
delete this.admins[adminIndex].password_temporary;
|
|
}
|
|
if (typeof citizenfxData !== 'undefined') {
|
|
if (!citizenfxData) {
|
|
delete this.admins[adminIndex].providers.citizenfx;
|
|
} else {
|
|
this.admins[adminIndex].providers.citizenfx = {
|
|
id: citizenfxData.id,
|
|
identifier: citizenfxData.identifier,
|
|
data: {},
|
|
};
|
|
}
|
|
}
|
|
if (typeof discordData !== 'undefined') {
|
|
if (!discordData) {
|
|
delete this.admins[adminIndex].providers.discord;
|
|
} else {
|
|
this.admins[adminIndex].providers.discord = {
|
|
id: discordData.id,
|
|
identifier: discordData.identifier,
|
|
data: {},
|
|
};
|
|
}
|
|
}
|
|
if (typeof permissions !== 'undefined') this.admins[adminIndex].permissions = permissions;
|
|
|
|
//Prevent race condition, will allow the session to be updated before refreshing socket.io
|
|
//sessions which will cause reauth and closing of the temp password modal on first access
|
|
setTimeout(() => {
|
|
this.refreshOnlineAdmins().catch((e) => { });
|
|
}, 250);
|
|
|
|
//Saving admin file
|
|
try {
|
|
await this.writeAdminsFile();
|
|
return (password !== null) ? this.admins[adminIndex].password_hash : true;
|
|
} catch (error) {
|
|
throw new Error(`Failed to save admins.json with error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Delete admin and save to the admins file
|
|
* @param {string} name
|
|
*/
|
|
async deleteAdmin(name) {
|
|
if (this.admins == false) throw new Error('Admins not set');
|
|
|
|
//Delete admin
|
|
let username = name.toLowerCase();
|
|
let found = false;
|
|
this.admins = this.admins.filter((user) => {
|
|
if (username !== user.name.toLowerCase()) {
|
|
return true;
|
|
} else {
|
|
found = true;
|
|
return false;
|
|
}
|
|
});
|
|
if (!found) throw new Error('Admin not found');
|
|
|
|
//Saving admin file
|
|
this.refreshOnlineAdmins().catch((e) => { });
|
|
try {
|
|
return await this.writeAdminsFile();
|
|
} catch (error) {
|
|
throw new Error(`Failed to save admins.json with error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads the admins.json file into the admins list
|
|
* NOTE: The verbosity here is driving me insane.
|
|
* But still seems not to be enough for people that don't read the README.
|
|
*/
|
|
async loadAdminsFile() {
|
|
let raw = null;
|
|
let jsonData = null;
|
|
let hasMigration = false;
|
|
|
|
const callError = (reason) => {
|
|
let details;
|
|
if (reason === 'cannot read file') {
|
|
details = ['This means the file doesn\'t exist or txAdmin doesn\'t have permission to read it.'];
|
|
} else {
|
|
details = [
|
|
'This likely means the file got somehow corrupted.',
|
|
'You can try restoring it or you can delete it and let txAdmin create a new one.',
|
|
];
|
|
}
|
|
fatalError.AdminStore(0, [
|
|
['Unable to load admins.json', reason],
|
|
...details,
|
|
['Admin File Path', this.adminsFile],
|
|
]);
|
|
};
|
|
|
|
try {
|
|
raw = await fsp.readFile(this.adminsFile, 'utf8');
|
|
this.adminsFileHash = createHash('sha1').update(raw).digest('hex');
|
|
} catch (error) {
|
|
return callError('cannot read file');
|
|
}
|
|
|
|
if (!raw.length) {
|
|
return callError('empty file');
|
|
}
|
|
|
|
try {
|
|
jsonData = JSON.parse(raw);
|
|
} catch (error) {
|
|
return callError('json parse error');
|
|
}
|
|
|
|
if (!Array.isArray(jsonData)) {
|
|
return callError('not an array');
|
|
}
|
|
|
|
if (!jsonData.length) {
|
|
return callError('no admins');
|
|
}
|
|
|
|
const structureIntegrityTest = jsonData.some((x) => {
|
|
if (typeof x.name !== 'string' || x.name.length < 3) return true;
|
|
if (typeof x.master !== 'boolean') return true;
|
|
if (typeof x.password_hash !== 'string' || !x.password_hash.startsWith('$2')) return true;
|
|
if (typeof x.providers !== 'object') return true;
|
|
const providersTest = Object.keys(x.providers).some((y) => {
|
|
if (!Object.keys(this.providers).includes(y)) return true;
|
|
if (typeof x.providers[y].id !== 'string' || x.providers[y].id.length < 3) return true;
|
|
if (typeof x.providers[y].data !== 'object') return true;
|
|
if (typeof x.providers[y].identifier === 'string') {
|
|
if (x.providers[y].identifier.length < 3) return true;
|
|
} else {
|
|
migrateProviderIdentifiers(y, x.providers[y]);
|
|
hasMigration = true;
|
|
}
|
|
});
|
|
if (providersTest) return true;
|
|
if (!Array.isArray(x.permissions)) return true;
|
|
return false;
|
|
});
|
|
if (structureIntegrityTest) {
|
|
return callError('invalid data in the admins file');
|
|
}
|
|
|
|
const masters = jsonData.filter((x) => x.master);
|
|
if (masters.length !== 1) {
|
|
return callError('must have exactly 1 master account');
|
|
}
|
|
|
|
//Migrate admin stuff
|
|
jsonData.forEach((admin) => {
|
|
//Migration (tx v7.3.0)
|
|
if (admin.$schema === undefined) {
|
|
//adding schema version
|
|
admin.$schema = ADMIN_SCHEMA_VERSION;
|
|
hasMigration = true;
|
|
|
|
//separate DM and Announcement permissions
|
|
if (admin.permissions.includes('players.message')) {
|
|
hasMigration = true;
|
|
admin.permissions = admin.permissions.filter((perm) => perm !== 'players.message');
|
|
admin.permissions.push('players.direct_message');
|
|
admin.permissions.push('announcement');
|
|
}
|
|
|
|
//Adding the new permission, except if they have no permissions or all of them
|
|
if (admin.permissions.length && !admin.permissions.includes('all_permissions')) {
|
|
admin.permissions.push('server.log.view');
|
|
}
|
|
}
|
|
});
|
|
|
|
this.admins = jsonData;
|
|
if (hasMigration) {
|
|
try {
|
|
await this.writeAdminsFile();
|
|
console.ok('The admins.json file was migrated to a new version.');
|
|
} catch (error) {
|
|
console.error(`Failed to migrate admins.json with error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Notify game server about admin changes
|
|
*/
|
|
async refreshOnlineAdmins() {
|
|
//Refresh auth of all admins connected to socket.io
|
|
txCore.webServer.webSocket.reCheckAdminAuths().catch((e) => { });
|
|
|
|
try {
|
|
//Getting all admin identifiers
|
|
const adminIDs = this.admins.reduce((ids, adm) => {
|
|
const adminIDs = Object.keys(adm.providers).map((pName) => adm.providers[pName].identifier);
|
|
return ids.concat(adminIDs);
|
|
}, []);
|
|
|
|
//Finding online admins
|
|
const playerList = txCore.fxPlayerlist.getPlayerList();
|
|
const onlineIDs = playerList.filter((p) => {
|
|
return p.ids.some((i) => adminIDs.includes(i));
|
|
}).map((p) => p.netid);
|
|
|
|
txCore.fxRunner.sendEvent('adminsUpdated', onlineIDs);
|
|
} catch (error) {
|
|
console.verbose.error('Failed to refreshOnlineAdmins() with error:');
|
|
console.verbose.dir(error);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a random token to be used as CSRF Token.
|
|
*/
|
|
genCsrfToken() {
|
|
return nanoid();
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks if there are admins configured or not.
|
|
* Optionally, prints the master PIN on the console.
|
|
*/
|
|
hasAdmins(printPin = false) {
|
|
if (Array.isArray(this.admins) && this.admins.length) {
|
|
return true;
|
|
} else {
|
|
if (printPin) {
|
|
console.warn('Use this PIN to add a new master account: ' + chalkInversePad(this.addMasterPin));
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the public name to display for that particular purpose
|
|
* TODO: maybe use enums for the purpose
|
|
*/
|
|
getAdminPublicName(name, purpose) {
|
|
if (!name || !purpose) throw new Error('Invalid parameters');
|
|
const replacer = txConfig.general.serverName ?? 'txAdmin';
|
|
|
|
if (purpose === 'punishment') {
|
|
return txConfig.gameFeatures.hideAdminInPunishments ? replacer : name;
|
|
} else if (purpose === 'message') {
|
|
return txConfig.gameFeatures.hideAdminInMessages ? replacer : name;
|
|
} else {
|
|
throw new Error(`Invalid purpose: ${purpose}`);
|
|
}
|
|
}
|
|
};
|