diff --git a/handlers/buttons.js b/handlers/buttons.js index 8979e80..a597cee 100644 --- a/handlers/buttons.js +++ b/handlers/buttons.js @@ -34,6 +34,7 @@ const { addMemberToStaffThread, createStaffThread } = require('../services/staff const { pinMessage } = require('../services/pinMessage'); const { logError, logTicketEvent } = require('../services/debugLog'); const { findTicketForChannel, runDeferred } = require('./sharedHelpers'); +const { requireStaffRole } = require('./commands/helpers'); const Ticket = mongoose.model('Ticket'); const Transcript = mongoose.model('Transcript'); @@ -732,16 +733,28 @@ async function handleButton(interaction) { const { customId } = interaction; // Tag-delete confirm has a dynamic id (`confirm_delete_tag::`). + // Mutates the Tag collection — staff only. if (customId.startsWith('confirm_delete_tag::')) { + if (await requireStaffRole(interaction)) return; 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). const freeHandler = FREE_BUTTON_HANDLERS[customId]; if (freeHandler) return freeHandler(interaction); const ticketHandler = TICKET_BUTTON_HANDLERS[customId]; if (!ticketHandler) return; + // Every TICKET_BUTTON_HANDLERS entry mutates ticket state + // (claim/close/confirm_close*/cancel_close/escalate*/deescalate). The slash + // command dispatcher in handlers/commands/index.js gates these via + // requireStaffRole; the button dispatcher must do the same — non-staff + // members with view access to the ticket channel (creator, /add'd users) + // could otherwise click Claim, Escalate, Close, etc. + if (await requireStaffRole(interaction)) return; + const ticket = await findTicketForChannel( interaction, 'This channel is not linked to a ticket, or the ticket could not be found.'