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

@@ -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).`
);