diff --git a/package.json b/package.json index f2bbf8c..11934a0 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@fastify/oauth2": "^5.0.0", "@prisma/client": "^4.0.0", "cryptr": "^6.0.3", - "discord.js": "^13.8.1", + "discord.js": "^13.9.0", "dotenv": "^16.0.1", "fastify": "^4.2.1", "figlet": "^1.5.2", diff --git a/src/client.js b/src/client.js index b2683aa..1b14547 100644 --- a/src/client.js +++ b/src/client.js @@ -36,7 +36,7 @@ module.exports = class Client extends FrameworkClient { async login(token) { /** @type {PrismaClient} */ this.prisma = new PrismaClient(); - this.prisma.$use(middleware); + this.prisma.$use(middleware(this.log)); this.keyv = new Keyv(); return super.login(token); } diff --git a/src/i18n/en-GB.yml b/src/i18n/en-GB.yml index 2657f6a..9998e1e 100644 --- a/src/i18n/en-GB.yml +++ b/src/i18n/en-GB.yml @@ -1,7 +1,9 @@ command: log: admin: - changes: 'Changes' + changes: + title: 'Changes' + description: 'These were the changes made:' description: joined: '{user} {verb} {targetType}' target: diff --git a/src/lib/logger.js b/src/lib/logger.js index 054e9ff..9ba59e7 100644 --- a/src/lib/logger.js +++ b/src/lib/logger.js @@ -37,7 +37,7 @@ module.exports = config => { } return new Logger({ - namespaces: ['commands', 'http', 'listeners'], + namespaces: ['commands', 'http', 'listeners', 'settings', 'tickets'], transports, }); }; diff --git a/src/lib/logging.js b/src/lib/logging.js index 1c57dd7..7bab016 100644 --- a/src/lib/logging.js +++ b/src/lib/logging.js @@ -32,21 +32,6 @@ async function getLogChannel(client, guildId) { return channelId && client.channels.cache.get(channelId); } -/** - * @param {import("client")} client - * @param {*} target - * @returns {string} target.type - * @returns {string} target.id -*/ -async function getTargetName(client, target) { - if (target.type === 'settings') { - return client.guilds.cache.get(target.id).name; - } else { - const row = await client.prisma[target.type].findUnique({ where: { id: target.id } }); - return row.name ?? target.id; - } -} - /** * @param {import("client")} client * @param {object} details @@ -58,7 +43,7 @@ async function logAdminEvent(client, { guildId, userId, action, target, diff, }) { const user = await client.users.fetch(userId); - client.log.info(`${user.tag} ${action}d ${target.type} ${target.id}`); + client.log.info.settings(`${user.tag} ${action}d ${target.type} ${target.id}`); const settings = await client.prisma.guild.findUnique({ select: { footer: true, @@ -75,7 +60,6 @@ async function logAdminEvent(client, { }; const channel = client.channels.cache.get(settings.logChannel); if (!channel) return; - const targetName = await getTargetName(client, target); return await channel.send({ embeds: [ @@ -95,7 +79,7 @@ async function logAdminEvent(client, { targetType: getMessage(`log.admin.description.target.${target.type}`), verb: getMessage(`log.admin.verb.${action}`), })) - .addField(getMessage(`log.admin.title.target.${target.type}`), targetName), + .addField(getMessage(`log.admin.title.target.${target.type}`), target.name ?? target.id), // .setFooter({ // iconURL: client.guilds.cache.get(guildId).iconURL(), // text: settings.footer, @@ -104,7 +88,8 @@ async function logAdminEvent(client, { diff?.original && new MessageEmbed() .setColor('ORANGE') - .setTitle(getMessage('log.admin.changes')) + .setTitle(getMessage('log.admin.changes.title')) + .setDescription(getMessage('log.admin.changes.description')) .setFields(makeDiff(diff)), ], ], diff --git a/src/lib/prisma.js b/src/lib/prisma.js index ad8112b..204b327 100644 --- a/src/lib/prisma.js +++ b/src/lib/prisma.js @@ -5,7 +5,7 @@ const fields = [ 'content', 'username', 'displayName', - 'channelName', + // 'channelName', 'openingMessage', 'description', 'value', @@ -19,23 +19,50 @@ const fields = [ const shouldEncrypt = ['create', 'createMany', 'update', 'updateMany', 'upsert']; const shouldDecrypt = ['findUnique', 'findFirst', 'findMany']; -module.exports = async (params, next) => { - if (params.args.data && shouldEncrypt.includes(params.action)) { - for (const field of fields) { - if (field in params.args.data && params.args.data[field] !== null && params.args.data[field] !== undefined) { - params.args.data[field] = cryptr.encrypt(params.args.data[field]); + + + +module.exports = log => { + const encrypt = obj => { + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + if (typeof obj[prop] === 'object') { + obj[prop] = encrypt(obj[prop]); + } else if (typeof obj[prop] === 'string' && obj[prop].length !== 0 && fields.includes(prop)) { + try { + obj[prop] = cryptr.encrypt(obj[prop]); + } catch (error) { + log.warn(`Failed to encrypt ${prop}`); + log.debug(error); + } + } } } - } + return obj; + }; - const result = await next(params); - - if (result && shouldDecrypt.includes(params.action)) { - for (const field of fields) { - if (field in result && result[field] !== null && result[field] !== undefined) { - result[field] = cryptr.decrypt(params.result[field]); + const decrypt = obj => { + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + if (typeof obj[prop] === 'object') { + obj[prop] = decrypt(obj[prop]); + } else if (typeof obj[prop] === 'string' && obj[prop].length !== 0 && fields.includes(prop)) { + try { + obj[prop] = cryptr.decrypt(obj[prop]); + } catch (error) { + log.warn(`Failed to decrypt ${prop}`); + log.debug(error); + } + } } } - } - return result; + return obj; + }; + + return async (params, next) => { + if (params.args.data && shouldEncrypt.includes(params.action)) params.args = encrypt(params.args); + let result = await next(params); + if (result && shouldDecrypt.includes(params.action)) result = decrypt(result); + return result; + }; }; \ No newline at end of file diff --git a/src/routes/api/admin/guilds/[guild]/categories/[category].js b/src/routes/api/admin/guilds/[guild]/categories/[category].js index e69de29..36854e7 100644 --- a/src/routes/api/admin/guilds/[guild]/categories/[category].js +++ b/src/routes/api/admin/guilds/[guild]/categories/[category].js @@ -0,0 +1,102 @@ +const { logAdminEvent } = require('../../../../../../lib/logging'); + +module.exports.get = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + + const category = await client.prisma.category.findUnique({ + include: { + questions: { + select: { + createdAt: true, + id: true, + label: true, + maxLength: true, + minLength: true, + order: true, + placeholder: true, + required: true, + style: true, + value: true, + }, + }, + }, + where: { id: Number(req.params.category) }, + }); + + return category; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); + +module.exports.patch = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + + const user = await client.users.fetch(req.user.payload.id); + const guild = client.guilds.cache.get(req.params.guild); + const data = req.body; + const allow = ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY', 'SEND_MESSAGES', 'EMBED_LINKS', 'ATTACH_FILES']; + const original = req.params.category && await client.prisma.category.findUnique({ where: { id: req.params.category } }); + if (!original) return res.status(404); + + if (!data.discordCategory) { + const channel = await guild.channels.create(data.name, { + permissionOverwrites: [ + ...[ + { + deny: ['VIEW_CHANNEL'], + id: guild.roles.everyone, + }, + { + allow: allow, + id: client.user.id, + }, + ], + ...data.staffRoles.map(id => ({ + allow: allow, + id, + })), + ], + position: 1, + reason: `Tickets category created by ${user.tag}`, + type: 'GUILD_CATEGORY', + }); + data.discordCategory = channel.id; + } + + const category = await client.prisma.category.update({ + data: { + guild: { connect: { id: guild.id } }, + ...data, + questions: { + upsert: data.questions?.map(q => ({ + create: q, + update: q, + where: { id: q.id }, + })), + }, + }, + }); + + logAdminEvent(client, { + action: 'update', + diff: { + original, + updated: category, + }, + guildId: guild.id, + target: { + id: category.id, + name: category.name, + type: 'category', + }, + userId: req.user.payload.id, + }); + + return category; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); \ No newline at end of file diff --git a/src/routes/api/admin/guilds/[guild]/categories/index.js b/src/routes/api/admin/guilds/[guild]/categories/index.js index ed0776e..e20f34b 100644 --- a/src/routes/api/admin/guilds/[guild]/categories/index.js +++ b/src/routes/api/admin/guilds/[guild]/categories/index.js @@ -1,11 +1,35 @@ +const { logAdminEvent } = require('../../../../../../lib/logging'); + module.exports.get = fastify => ({ handler: async (req, res) => { /** @type {import('client')} */ const client = res.context.config.client; - const categories = await client.prisma.guild.findUnique({ where: { id: req.params.guild } }).categories(); + const { categories } = await client.prisma.guild.findUnique({ + select: { + categories: { + include: { + questions: { + select: { + createdAt: true, + id: true, + label: true, + maxLength: true, + minLength: true, + order: true, + placeholder: true, + required: true, + style: true, + value: true, + }, + }, + }, + }, + }, + where: { id: req.params.guild }, + }); - res.send(categories); + return categories; }, onRequest: [fastify.authenticate, fastify.isAdmin], }); @@ -45,14 +69,28 @@ module.exports.post = fastify => ({ data.discordCategory = channel.id; } + if (data.channelName === null) data.channelName = undefined; + const category = await client.prisma.category.create({ data: { guild: { connect: { id: guild.id } }, ...data, + questions: { createMany: { data: data.questions ?? [] } }, }, }); - res.send(category); + logAdminEvent(client, { + action: 'create', + guildId: guild.id, + target: { + id: category.id, + name: category.name, + type: 'category', + }, + userId: req.user.payload.id, + }); + + return category; }, onRequest: [fastify.authenticate, fastify.isAdmin], -}); +}); \ No newline at end of file diff --git a/src/routes/api/admin/guilds/[guild]/settings.js b/src/routes/api/admin/guilds/[guild]/settings.js index ee3757a..e422142 100644 --- a/src/routes/api/admin/guilds/[guild]/settings.js +++ b/src/routes/api/admin/guilds/[guild]/settings.js @@ -12,6 +12,7 @@ module.exports.delete = fastify => ({ guildId: id, target: { id, + name: client.guilds.cache.get(id), type: 'settings', }, userId: req.user.payload.id, @@ -53,9 +54,9 @@ module.exports.patch = fastify => ({ updated: settings, }, guildId: id, - original, target: { id, + name: client.guilds.cache.get(id), type: 'settings', }, userId: req.user.payload.id,