security hardening

This commit is contained in:
2026-04-18 11:10:41 +00:00
parent a409203025
commit 21618efbad
36 changed files with 1455 additions and 283 deletions

View File

@@ -6,6 +6,7 @@ const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('
const { CONFIG } = require('../config');
const { mongoose } = require('../db-connection');
const { logSecurity } = require('../services/debugLog');
const { enqueueSend } = require('../services/channelQueue');
const User = mongoose.model('User');
@@ -167,7 +168,7 @@ async function handleSendAccountInfoToChannel(interaction) {
}
const embed = buildAccountInfoEmbed(user, `${interaction.user.tag} (from ticket)`);
await channel.send({ embeds: [embed] });
await enqueueSend(channel, { embeds: [embed] });
await interaction.update({
content: 'Account info sent to account transcript channel.',

View File

@@ -21,7 +21,7 @@ const { sendTicketClosedEmail } = require('../services/gmail');
const { getTicketActionRow } = require('../utils/ticketComponents');
const { sanitizeEmbedText, truncateEmbedDescription, truncateEmbedField, enforceEmbedLimit } = require('../utils');
const { setEmailRouting } = require('../services/guildSettings');
const { enqueueRename } = require('../services/channelQueue');
const { enqueueRename, enqueueSend } = require('../services/channelQueue');
const { runEscalation, runDeescalation } = require('./commands');
const { trackInteraction, trackError } = require('./analytics');
const { pendingCloses } = require('./pendingCloses');
@@ -356,7 +356,7 @@ async function handleClaim(interaction, ticket) {
} else {
const unlockAtMs = Date.now() + renameInfo.waitMs;
const unlockAtUnix = Math.floor(unlockAtMs / 1000);
await interaction.channel.send(
await enqueueSend(interaction.channel,
`Channel renamed too quickly. Try again <t:${unlockAtUnix}:R>.`
);
}
@@ -410,7 +410,7 @@ async function handleClaim(interaction, ticket) {
} else {
const unlockAtMs = Date.now() + renameInfo.waitMs;
const unlockAtUnix = Math.floor(unlockAtMs / 1000);
await interaction.channel.send(
await enqueueSend(interaction.channel,
`Channel renamed too quickly. Try again <t:${unlockAtUnix}:R>.`
);
}
@@ -496,7 +496,7 @@ async function handleConfirmClose(interaction, ticket) {
// In-ticket message before transcript is posted (Discord close message)
const discordCloseContent = CONFIG.DISCORD_CLOSE_MESSAGE;
await interaction.channel.send(discordCloseContent);
await enqueueSend(interaction.channel, discordCloseContent);
const transcriptChan = await interaction.client.channels
.fetch(CONFIG.TRANSCRIPT_CHAN)
@@ -511,7 +511,7 @@ async function handleConfirmClose(interaction, ticket) {
+ `\n\nDate Opened: ${openedStr}\nDate Closed: ${closedStr}`;
if (transcriptChan) {
transcriptMsg = await transcriptChan.send({
transcriptMsg = await enqueueSend(transcriptChan, {
content: transcriptContent,
files: [file]
});
@@ -559,7 +559,7 @@ async function handleConfirmClose(interaction, ticket) {
} else {
logMsg = `Closed **${channelName}** (${ticket.senderEmail}) by ${closerMention} (${closerDisplayName})`;
}
await logChan.send(logMsg);
await enqueueSend(logChan, logMsg);
}
const closerDisplayName =
@@ -732,7 +732,7 @@ async function handleTicketModal(interaction) {
enforceEmbedLimit([welcomeEmbed, infoEmbed, resourcesEmbed]);
try {
const welcomeMsg = await channel.send({
const welcomeMsg = await enqueueSend(channel, {
content: `Hey There ${interaction.user} 🥦`,
embeds: [welcomeEmbed, infoEmbed, resourcesEmbed],
components: [actionRow]
@@ -765,7 +765,7 @@ async function handleTicketModal(interaction) {
const logChan = await interaction.client.channels.fetch(CONFIG.LOG_CHAN).catch(() => null);
if (logChan) {
await logChan.send(
await enqueueSend(logChan,
`📝 ${channel.name} created by ${interaction.user.tag}`
);
}

View File

@@ -17,7 +17,7 @@ const { canRename, makeTicketName, resolveCreatorNickname, getSenderLocal, toDis
const { sendTicketNotificationEmail } = require('../services/gmail');
const { getTicketActionRow } = require('../utils/ticketComponents');
const { getEmailRouting } = require('../services/guildSettings');
const { enqueueRename, enqueueMove } = require('../services/channelQueue');
const { enqueueRename, enqueueMove, enqueueSend } = require('../services/channelQueue');
const { setNotifyDm } = require('../services/staffSettings');
const { trackInteraction, trackError, getAnalyticsSummary } = require('./analytics');
const { logTicketEvent, logSecurity } = require('../services/debugLog');
@@ -74,7 +74,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
// Clear claim on escalation
await Ticket.updateOne(
{ gmailThreadId: ticket.gmailThreadId },
{ $set: { escalated: true, escalationTier: nextTier, claimedBy: null, claimerId: null, unclaimedReminderssent: [] } }
{ $set: { escalated: true, escalationTier: nextTier, claimedBy: null, claimerId: null, unclaimedRemindersSent: [] } }
);
ticket.escalated = true;
ticket.escalationTier = nextTier;
@@ -94,7 +94,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
} else {
const unlockAtMs = Date.now() + renameInfo.waitMs;
const unlockAtUnix = Math.floor(unlockAtMs / 1000);
await interaction.channel.send(
await enqueueSend(interaction.channel,
`Channel renamed too quickly. Try again <t:${unlockAtUnix}:R>.`
);
}
@@ -116,7 +116,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
const heyLine = creatorMention
? `Hey There ${creatorMention} 🥦`
: 'Hey There 🥦';
await interaction.channel.send(
await enqueueSend(interaction.channel,
`${heyLine}\n**Getting the senior ${roleMention} for you.**`
);
@@ -130,7 +130,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
.setFooter({ text: `Escalated by ${interaction.member?.displayName || interaction.user.username}` });
const updatedTicketForRow = { ...ticket, escalationTier: nextTier, escalated: true };
const escalationRow = getTicketActionRow(updatedTicketForRow);
const escalationMsg = await interaction.channel.send({
const escalationMsg = await enqueueSend(interaction.channel, {
content: null,
embeds: [escalatedEmbed],
components: [escalationRow]
@@ -174,7 +174,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
if (logChan) {
const ticketType = isDiscordTicket ? 'Discord' : 'Email';
const tierLabel = nextTier === 1 ? 'tier 2' : 'tier 3';
await logChan.send(
await enqueueSend(logChan,
`${ticketType} ticket ${interaction.channel} escalated to ${tierLabel} by ${interaction.user.tag}.\nReason: ${reason}`
);
}
@@ -204,7 +204,7 @@ async function runDeescalation(interaction, ticket) {
} else {
const unlockAtMs = Date.now() + renameInfo.waitMs;
const unlockAtUnix = Math.floor(unlockAtMs / 1000);
await interaction.channel.send(
await enqueueSend(interaction.channel,
`Channel renamed too quickly. Try again <t:${unlockAtUnix}:R>.`
);
}
@@ -235,7 +235,7 @@ async function runDeescalation(interaction, ticket) {
const logChan = await interaction.client.channels.fetch(CONFIG.LOG_CHAN).catch(() => null);
if (logChan) {
const ticketType = isDiscordTicket ? 'Discord' : 'Email';
await logChan.send(
await enqueueSend(logChan,
`${ticketType} ticket ${interaction.channel} deescalated to ${tierLabel} by ${interaction.user.tag}.`
);
}
@@ -517,7 +517,7 @@ async function handleCommand(interaction) {
const logChan = await interaction.client.channels.fetch(CONFIG.LOG_CHAN).catch(() => null);
if (logChan) {
await logChan.send(
await enqueueSend(logChan,
`Ticket ${interaction.channel} transferred from ${interaction.user.tag} to ${member.tag}.\nReason: ${reason}`
);
}
@@ -542,7 +542,7 @@ async function handleCommand(interaction) {
const logChan = await interaction.client.channels.fetch(CONFIG.LOG_CHAN).catch(() => null);
if (logChan) {
await logChan.send(
await enqueueSend(logChan,
`Ticket ${interaction.channel} moved to category **${category.name}** by ${interaction.user.tag}`
);
}
@@ -652,10 +652,10 @@ async function handleCommand(interaction) {
{ $set: { status: 'closed' } }
);
await channelRef.send('Ticket force-closed. Archiving...');
await enqueueSend(channelRef, 'Ticket force-closed. Archiving...');
try {
await channelRef.send(CONFIG.DISCORD_CLOSE_MESSAGE);
await enqueueSend(channelRef, CONFIG.DISCORD_CLOSE_MESSAGE);
const messages = await channelRef.messages.fetch({ limit: 100 });
const log =
@@ -691,7 +691,7 @@ async function handleCommand(interaction) {
.replace(/\{date_opened\}/g, openedStr)
.replace(/\{date_closed\}/g, closedStr)
+ `\n\nDate Opened: ${openedStr}\nDate Closed: ${closedStr}`;
await transcriptChan.send({
await enqueueSend(transcriptChan, {
content: transcriptContent,
files: [file]
});
@@ -1080,7 +1080,7 @@ async function handleCommand(interaction) {
}
try {
await channel.send({ embeds: [embed], components: [row] });
await enqueueSend(channel, { embeds: [embed], components: [row] });
await interaction.reply({ content: `Panel created in ${channel}!`, ephemeral: true });
} catch (err) {
console.error('Panel creation error:', err);
@@ -1104,7 +1104,7 @@ async function handleCommand(interaction) {
}
const buf = Buffer.from(lines.join('\n'), 'utf8');
const channel = await interaction.client.channels.fetch(CONFIG.BACKUP_EXPORT_CHANNEL_ID);
await channel.send({
await enqueueSend(channel, {
content: `Ticket backup by ${interaction.user.tag} (${tickets.length} tickets)`,
files: [new AttachmentBuilder(buf, { name: `ticket-backup-${Date.now()}.txt` })]
});
@@ -1134,7 +1134,7 @@ async function handleCommand(interaction) {
}
const buf = Buffer.from(lines.join('\n'), 'utf8');
const channel = await interaction.client.channels.fetch(CONFIG.BACKUP_EXPORT_CHANNEL_ID);
await channel.send({
await enqueueSend(channel, {
content: `Ticket export by ${interaction.user.tag} (${tickets.length} tickets${status ? ` status=${status}` : ''})`,
files: [new AttachmentBuilder(buf, { name: `ticket-export-${Date.now()}.txt` })]
});
@@ -1362,7 +1362,7 @@ async function handleContextMenu(interaction) {
const row = getTicketActionRow({ escalationTier: 0 });
try {
const welcomeMsg = await channel.send({
const welcomeMsg = await enqueueSend(channel, {
content: `<@&${CONFIG.ROLE_ID_TO_PING}>\nHey There ${message.author} 🥦`,
embeds: [welcomeEmbed, infoEmbed],
components: [row]

View File

@@ -15,6 +15,7 @@ const {
ChannelSelectMenuBuilder
} = require('discord.js');
const { CONFIG } = require('../config');
const { enqueueSend } = require('../services/channelQueue');
const TOTAL_STEPS = 5;
const WIZARD_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
@@ -505,7 +506,7 @@ async function handleSetupButton(interaction) {
);
}
await channel.send({ embeds: [embed], components: [row] });
await enqueueSend(channel, { embeds: [embed], components: [row] });
const envLines = state.ticketType === 'both'
? [`DISCORD_THREAD_CHANNEL_ID=${state.threadChannelId}`, `DISCORD_TICKET_CATEGORY_ID=${state.categoryId}`]