security hardening
This commit is contained in:
@@ -7,6 +7,7 @@ const { mongoose, withRetry } = require('../db-connection');
|
||||
const { CONFIG } = require('../config');
|
||||
const { getPriorityEmoji } = require('../utils');
|
||||
const { logAutomation } = require('../services/debugLog');
|
||||
const { enqueueSend } = require('./channelQueue');
|
||||
|
||||
const Ticket = mongoose.model('Ticket');
|
||||
const TicketCounter = mongoose.model('TicketCounter');
|
||||
@@ -89,48 +90,51 @@ function makeTicketName(state, ticket, creatorNickname, claimerEmoji) {
|
||||
|
||||
async function canRename(ticket) {
|
||||
const now = Date.now();
|
||||
const fresh = await Ticket.findOne({ gmailThreadId: ticket.gmailThreadId })
|
||||
.select('renameCount renameWindowStart')
|
||||
.lean();
|
||||
if (!fresh) {
|
||||
return { ok: false, remaining: 0, waitMs: RENAME_WINDOW_MS };
|
||||
const windowCutoff = new Date(now - RENAME_WINDOW_MS);
|
||||
|
||||
// Atomic: reset the window if the stored start is older than the cutoff; count = 1.
|
||||
const resetDoc = await Ticket.findOneAndUpdate(
|
||||
{
|
||||
gmailThreadId: ticket.gmailThreadId,
|
||||
$or: [
|
||||
{ renameWindowStart: { $lt: windowCutoff } },
|
||||
{ renameWindowStart: null },
|
||||
{ renameWindowStart: { $exists: false } }
|
||||
]
|
||||
},
|
||||
{ $set: { renameWindowStart: new Date(now), renameCount: 1 } },
|
||||
{ new: true, projection: { renameCount: 1, renameWindowStart: 1 } }
|
||||
).lean();
|
||||
|
||||
if (resetDoc) {
|
||||
ticket.renameWindowStart = resetDoc.renameWindowStart;
|
||||
ticket.renameCount = resetDoc.renameCount;
|
||||
return { ok: true, remaining: RENAME_LIMIT - resetDoc.renameCount, waitMs: 0 };
|
||||
}
|
||||
|
||||
const windowStart = (fresh.renameWindowStart && new Date(fresh.renameWindowStart).getTime()) || 0;
|
||||
const count = fresh.renameCount || 0;
|
||||
|
||||
if (now - windowStart >= RENAME_WINDOW_MS) {
|
||||
await Ticket.updateOne(
|
||||
{ gmailThreadId: ticket.gmailThreadId },
|
||||
{ $set: { renameWindowStart: new Date(now), renameCount: 0 } }
|
||||
);
|
||||
ticket.renameWindowStart = new Date(now);
|
||||
ticket.renameCount = 0;
|
||||
return { ok: true, remaining: RENAME_LIMIT, waitMs: 0 };
|
||||
}
|
||||
|
||||
if (count >= RENAME_LIMIT) {
|
||||
const waitMs = RENAME_WINDOW_MS - (now - windowStart);
|
||||
return { ok: false, remaining: 0, waitMs };
|
||||
}
|
||||
|
||||
const updated = await Ticket.findOneAndUpdate(
|
||||
{ gmailThreadId: ticket.gmailThreadId },
|
||||
// Atomic: within window, only increment if count < limit.
|
||||
const incDoc = await Ticket.findOneAndUpdate(
|
||||
{
|
||||
gmailThreadId: ticket.gmailThreadId,
|
||||
renameCount: { $lt: RENAME_LIMIT }
|
||||
},
|
||||
{ $inc: { renameCount: 1 } },
|
||||
{ returnDocument: 'after' }
|
||||
)
|
||||
.select('renameCount renameWindowStart')
|
||||
.lean();
|
||||
{ new: true, projection: { renameCount: 1, renameWindowStart: 1 } }
|
||||
).lean();
|
||||
|
||||
if (!updated) {
|
||||
const waitMs = RENAME_WINDOW_MS - (now - windowStart);
|
||||
return { ok: false, remaining: 0, waitMs };
|
||||
if (incDoc) {
|
||||
ticket.renameWindowStart = incDoc.renameWindowStart;
|
||||
ticket.renameCount = incDoc.renameCount;
|
||||
return { ok: true, remaining: RENAME_LIMIT - incDoc.renameCount, waitMs: 0 };
|
||||
}
|
||||
|
||||
const newCount = updated.renameCount || 0;
|
||||
ticket.renameCount = newCount;
|
||||
ticket.renameWindowStart = updated.renameWindowStart;
|
||||
return { ok: true, remaining: RENAME_LIMIT - newCount, waitMs: 0 };
|
||||
// At limit — read the window start to compute waitMs.
|
||||
const fresh = await Ticket.findOne({ gmailThreadId: ticket.gmailThreadId })
|
||||
.select('renameWindowStart')
|
||||
.lean();
|
||||
const windowStart = (fresh?.renameWindowStart && new Date(fresh.renameWindowStart).getTime()) || now;
|
||||
const waitMs = Math.max(0, RENAME_WINDOW_MS - (now - windowStart));
|
||||
return { ok: false, remaining: 0, waitMs };
|
||||
}
|
||||
|
||||
function minutesFromMs(ms) {
|
||||
@@ -487,7 +491,7 @@ async function checkAutoClose(client, sendTicketClosedEmail) {
|
||||
|
||||
const channel = await guild.channels.fetch(ticket.discordThreadId).catch(() => null);
|
||||
if (channel) {
|
||||
await channel.send(CONFIG.DISCORD_AUTO_CLOSE_MESSAGE);
|
||||
await enqueueSend(channel, CONFIG.DISCORD_AUTO_CLOSE_MESSAGE);
|
||||
|
||||
await withRetry(() => Ticket.updateOne(
|
||||
{ gmailThreadId: ticket.gmailThreadId },
|
||||
@@ -531,7 +535,7 @@ async function checkReminders(client) {
|
||||
const message = CONFIG.REMINDER_MESSAGE
|
||||
.replace(/\{hours\}/g, String(CONFIG.REMINDER_AFTER_HOURS))
|
||||
.replace(/\{ping\}/g, ping);
|
||||
await channel.send(message);
|
||||
await enqueueSend(channel, message);
|
||||
|
||||
await withRetry(() => Ticket.updateOne(
|
||||
{ gmailThreadId: ticket.gmailThreadId },
|
||||
@@ -570,7 +574,7 @@ async function checkAutoUnclaim(client) {
|
||||
{ $set: { claimedBy: null } }
|
||||
));
|
||||
|
||||
await channel.send(
|
||||
await enqueueSend(channel,
|
||||
`This ticket has been auto-unclaimed due to inactivity (${CONFIG.AUTO_UNCLAIM_AFTER_HOURS} hours).`
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user