import { sendMail } from '../mailer'; import { getSettings } from '../adminSettings'; /** * Abuse alerts, sent directly rather than through the editable templates. * * An abuse alert is not copy anyone will want to reword, and making it editable * means it can be broken — a required placeholder removed from an alert nobody * reads until an incident is a poor way to discover the validation. */ /** At most one of each kind per hour. */ const ALERT_INTERVAL_MS = 60 * 60 * 1000; /** * Throttled in memory rather than in the database. * * A restart loses it, so a deploy during an incident can send one extra alert. * That is a far better trade than writing to admin_settings from the request * path on every refused submission. This is a single-container deployment; were * it ever replicated, each replica would alert once per window and this would * have to move. */ const lastSent = new Map(); function shouldSend(key: string, now: number): boolean { const previous = lastSent.get(key); if (previous !== undefined && now - previous < ALERT_INTERVAL_MS) return false; lastSent.set(key, now); return true; } /** Exported for tests, which need each case to start from silence. */ export function resetAlertThrottleForTests(): void { lastSent.clear(); } async function send(key: string, subject: string, html: string): Promise { const { intakeNotifyEmail } = await getSettings(); const to = intakeNotifyEmail?.trim(); if (!to) return; if (!shouldSend(key, Date.now())) return; await sendMail(to, subject, html); } export async function alertCeilingReached(used: number, ceiling: number): Promise { await send( 'ceiling', 'Intake submissions are being refused', `

The intake surface has taken ${used} submissions in the last 24 hours, which is at or ` + `over the ceiling of ${ceiling}. Further submissions are being refused until the window ` + `rolls.

` + `

The storefront, checkout and admin are unaffected. If this is legitimate, raise the ` + `ceiling or reset the window. If it is not, revoke the link being used.

` ); } export async function alertLinkThreshold( linkId: number, label: string, used: number, threshold: number ): Promise { await send( `link:${linkId}`, `An upload link is being used heavily: ${label}`, `

The link ${label} has taken ${used} submissions in the last 24 hours, ` + `past the alert threshold of ${threshold}.

` + `

This is the signal that a link has been shared further than intended. If that is what ` + `has happened, revoke it from the Upload links screen — the submissions already received ` + `are kept, and are in the review queue.

` ); }