diff --git a/broccolini-discord.js b/broccolini-discord.js index e000e93..4cbd02f 100644 --- a/broccolini-discord.js +++ b/broccolini-discord.js @@ -11,6 +11,7 @@ const { mongoose } = require('./db-connection'); // Handlers const { handleButton, handleTicketModal } = require('./handlers/buttons'); const { handleCommand, handleContextMenu, handleAutocomplete } = require('./handlers/commands'); +const { requireStaffRole } = require('./handlers/commands/helpers'); const { handleDiscordReply } = require('./handlers/messages'); // Services & jobs @@ -110,6 +111,9 @@ client.on('interactionCreate', async interaction => { } if (interaction.isModalSubmit() && interaction.customId.startsWith('signature_modal_')) { + // Staff-only: /signature shows this modal, which is gated; double-gate the + // submit path in case an attacker crafts the submission directly. + if (await requireStaffRole(interaction)) return; // Handle signature modal submit try { const valediction = interaction.fields.getTextInputValue('valediction'); diff --git a/handlers/buttons.js b/handlers/buttons.js index a597cee..fda8d95 100644 --- a/handlers/buttons.js +++ b/handlers/buttons.js @@ -707,12 +707,15 @@ async function postTicketWelcomeEmbeds(channel, interaction, email, game, descri // Dispatch tables // ============================================================ -/** Buttons that don't depend on a ticket-bound channel. */ +/** + * Public-facing buttons that don't require a staff role: the panel buttons + * that any member uses to open a ticket. Customer-facing entry points stay + * here. cancel_delete_tag is staff-only and gated separately in handleButton. + */ const FREE_BUTTON_HANDLERS = { open_ticket: handleOpenTicketModal, open_ticket_thread: handleOpenTicketModal, - open_ticket_channel: handleOpenTicketModal, - cancel_delete_tag: handleTagDeleteCancel + open_ticket_channel: handleOpenTicketModal }; /** Buttons that fire inside a ticket channel. The dispatcher does the lookup. */ @@ -739,8 +742,15 @@ async function handleButton(interaction) { return handleTagDeleteConfirm(interaction); } - // FREE_BUTTON_HANDLERS are public-facing: open_ticket* (panel buttons, - // anyone can open a ticket) and cancel_delete_tag (no-op cancel). + // Tag-delete cancel: paired with the staff-only delete flow; gate to keep + // the button surface consistent (non-staff can't reach the dialog anyway). + if (customId === 'cancel_delete_tag') { + if (await requireStaffRole(interaction)) return; + return handleTagDeleteCancel(interaction); + } + + // FREE_BUTTON_HANDLERS are the public-facing panel buttons (open_ticket*). + // Customers/members must be able to click these to open a ticket. const freeHandler = FREE_BUTTON_HANDLERS[customId]; if (freeHandler) return freeHandler(interaction); diff --git a/handlers/commands/index.js b/handlers/commands/index.js index 3743681..5546932 100644 --- a/handlers/commands/index.js +++ b/handlers/commands/index.js @@ -274,11 +274,11 @@ const CONTEXT_MENU_HANDLERS = { }; /** - * Slash-command dispatcher. /help is open to everyone; everything else - * requires the staff role. + * Slash-command dispatcher. Every command is staff-only — including /help, + * which previously bypassed the role check. */ async function handleCommand(interaction) { - if (interaction.commandName !== 'help' && (await requireStaffRole(interaction))) return; + if (await requireStaffRole(interaction)) return; const handler = COMMAND_HANDLERS[interaction.commandName]; if (handler) await handler(interaction); }