monitor/core/routes/adminManager/actions.ts
2025-04-16 22:30:27 +07:00

252 lines
8.5 KiB
TypeScript

const modulename = 'WebServer:AdminManagerActions';
import { customAlphabet } from 'nanoid';
import dict49 from 'nanoid-dictionary/nolookalikes';
import got from '@lib/got';
import consts from '@shared/consts';
import consoleFactory from '@lib/console';
import { AuthedCtx } from '@modules/WebServer/ctxTypes';
const console = consoleFactory(modulename);
//Helpers
const nanoid = customAlphabet(dict49, 20);
//NOTE: this desc misses that it should start and end with alphanum or _, and cannot have repeated -_.
const nameRegexDesc = 'up to 20 characters containing only letters, numbers and the characters \`_.-\`';
const cfxHttpReqOptions = {
timeout: { request: 6000 },
};
type ProviderDataType = {id: string, identifier: string};
/**
* Returns the output page containing the admins.
*/
export default async function AdminManagerActions(ctx: AuthedCtx) {
//Sanity check
if (typeof ctx.params?.action !== 'string') {
return ctx.utils.error(400, 'Invalid Request');
}
const action = ctx.params.action;
//Check permissions
if (!ctx.admin.testPermission('manage.admins', modulename)) {
return ctx.send({
type: 'danger',
message: 'You don\'t have permission to execute this action.',
});
}
//Delegate to the specific action handler
if (action == 'add') {
return await handleAdd(ctx);
} else if (action == 'edit') {
return await handleEdit(ctx);
} else if (action == 'delete') {
return await handleDelete(ctx);
} else {
return ctx.send({
type: 'danger',
message: 'Unknown action.',
});
}
};
/**
* Handle Add
*/
async function handleAdd(ctx: AuthedCtx) {
//Sanity check
if (
typeof ctx.request.body.name !== 'string'
|| typeof ctx.request.body.citizenfxID !== 'string'
|| typeof ctx.request.body.discordID !== 'string'
|| ctx.request.body.permissions === undefined
) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
//Prepare and filter variables
const name = ctx.request.body.name.trim();
const password = nanoid();
const citizenfxID = ctx.request.body.citizenfxID.trim();
const discordID = ctx.request.body.discordID.trim();
let permissions = (Array.isArray(ctx.request.body.permissions)) ? ctx.request.body.permissions : [];
permissions = permissions.filter((x: unknown) => typeof x === 'string');
if (permissions.includes('all_permissions')) permissions = ['all_permissions'];
//Validate name
if (!consts.regexValidFivemUsername.test(name)) {
return ctx.send({type: 'danger', markdown: true, message: `**Invalid username, it must follow the rule:**\n${nameRegexDesc}`});
}
//Validate & translate FiveM ID
let citizenfxData: ProviderDataType | undefined;
if (citizenfxID.length) {
try {
citizenfxData = {
id: citizenfxID,
identifier: citizenfxID,
};
} catch (error) {
console.error(`Failed to resolve CitizenFX ID to game identifier with error: ${(error as Error).message}`);
}
}
//Validate Discord ID
let discordData: ProviderDataType | undefined;
if (discordID.length) {
if (!consts.validIdentifierParts.discord.test(discordID)) {
return ctx.send({type: 'danger', message: 'Invalid Discord ID'});
}
discordData = {
id: discordID,
identifier: `discord:${discordID}`,
};
}
//Check for privilege escalation
if (!ctx.admin.isMaster && !ctx.admin.permissions.includes('all_permissions')) {
const deniedPerms = permissions.filter((x: string) => !ctx.admin.permissions.includes(x));
if (deniedPerms.length) {
return ctx.send({
type: 'danger',
message: `You cannot give permissions you do not have:<br>${deniedPerms.join(', ')}`,
});
}
}
//Add admin and give output
try {
await txCore.adminStore.addAdmin(name, citizenfxData, discordData, password, permissions);
ctx.admin.logAction(`Adding user '${name}'.`);
return ctx.send({type: 'showPassword', password});
} catch (error) {
return ctx.send({type: 'danger', message: (error as Error).message});
}
}
/**
* Handle Edit
*/
async function handleEdit(ctx: AuthedCtx) {
//Sanity check
if (
typeof ctx.request.body.name !== 'string'
|| typeof ctx.request.body.citizenfxID !== 'string'
|| typeof ctx.request.body.discordID !== 'string'
|| ctx.request.body.permissions === undefined
) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
//Prepare and filter variables
const name = ctx.request.body.name.trim();
const citizenfxID = ctx.request.body.citizenfxID.trim();
const discordID = ctx.request.body.discordID.trim();
//Check if editing himself
if (ctx.admin.name.toLowerCase() === name.toLowerCase()) {
return ctx.send({type: 'danger', message: '(ERR0) You cannot edit yourself.'});
}
//Validate & translate permissions
let permissions;
if (Array.isArray(ctx.request.body.permissions)) {
permissions = ctx.request.body.permissions.filter((x: unknown) => typeof x === 'string');
if (permissions.includes('all_permissions')) permissions = ['all_permissions'];
} else {
permissions = [];
}
//Validate & translate FiveM ID
let citizenfxData: ProviderDataType | undefined;
if (citizenfxID.length) {
try {
citizenfxData = {
id: citizenfxID,
identifier: citizenfxID,
};
} catch (error) {
console.error(`Failed to resolve CitizenFX ID to game identifier with error: ${(error as Error).message}`);
}
}
//Validate Discord ID
//FIXME: you cannot remove a discord id by erasing from the field
let discordData: ProviderDataType | undefined;
if (discordID.length) {
if (!consts.validIdentifierParts.discord.test(discordID)) {
return ctx.send({type: 'danger', message: 'Invalid Discord ID'});
}
discordData = {
id: discordID,
identifier: `discord:${discordID}`,
};
}
//Check if admin exists
const admin = txCore.adminStore.getAdminByName(name);
if (!admin) return ctx.send({type: 'danger', message: 'Admin not found.'});
//Check if editing an master admin
if (!ctx.admin.isMaster && admin.master) {
return ctx.send({type: 'danger', message: 'You cannot edit an admin master.'});
}
//Check for privilege escalation
if (permissions && !ctx.admin.isMaster && !ctx.admin.permissions.includes('all_permissions')) {
const deniedPerms = permissions.filter((x: string) => !ctx.admin.permissions.includes(x));
if (deniedPerms.length) {
return ctx.send({
type: 'danger',
message: `You cannot give permissions you do not have:<br>${deniedPerms.join(', ')}`,
});
}
}
//Add admin and give output
try {
await txCore.adminStore.editAdmin(name, null, citizenfxData, discordData, permissions);
ctx.admin.logAction(`Editing user '${name}'.`);
return ctx.send({type: 'success', refresh: true});
} catch (error) {
return ctx.send({type: 'danger', message: (error as Error).message});
}
}
/**
* Handle Delete
*/
async function handleDelete(ctx: AuthedCtx) {
//Sanity check
if (typeof ctx.request.body.name !== 'string') {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
const name = ctx.request.body.name.trim();
//Check if deleting himself
if (ctx.admin.name.toLowerCase() === name.toLowerCase()) {
return ctx.send({type: 'danger', message: "You can't delete yourself."});
}
//Check if admin exists
const admin = txCore.adminStore.getAdminByName(name);
if (!admin) return ctx.send({type: 'danger', message: 'Admin not found.'});
//Check if editing an master admin
if (admin.master) {
return ctx.send({type: 'danger', message: 'You cannot delete an admin master.'});
}
//Delete admin and give output
try {
await txCore.adminStore.deleteAdmin(name);
ctx.admin.logAction(`Deleting user '${name}'.`);
return ctx.send({type: 'success', refresh: true});
} catch (error) {
return ctx.send({type: 'danger', message: (error as Error).message});
}
}