252 lines
8.5 KiB
TypeScript
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});
|
|
}
|
|
}
|