diff --git a/backend/src/api/guilds/importExport.ts b/backend/src/api/guilds/importExport.ts index a6f3eb59..0da4f1d2 100644 --- a/backend/src/api/guilds/importExport.ts +++ b/backend/src/api/guilds/importExport.ts @@ -5,6 +5,8 @@ import { clientError, ok } from "../responses"; import { GuildCases } from "../../data/GuildCases"; import { z } from "zod"; import { Case } from "../../data/entities/Case"; +import { rateLimit } from "../rateLimits"; +import { MINUTES } from "../../utils"; const caseHandlingModeSchema = z.union([ z.literal("replace"), @@ -62,6 +64,11 @@ export function initGuildsImportExportAPI(guildRouter: express.Router) { importExportRouter.post( "/:guildId/import", requireGuildPermission(ApiPermissions.ManageAccess), + rateLimit( + (req) => `import-${req.params.guildId}`, + 5 * MINUTES, + "A single server can only import data once every 5 minutes", + ), async (req: Request, res: Response) => { let data: TImportExportData; try { @@ -119,6 +126,11 @@ export function initGuildsImportExportAPI(guildRouter: express.Router) { importExportRouter.post( "/:guildId/export", requireGuildPermission(ApiPermissions.ManageAccess), + rateLimit( + (req) => `export-${req.params.guildId}`, + 5 * MINUTES, + "A single server can only export data once every 5 minutes", + ), async (req: Request, res: Response) => { const guildCases = GuildCases.getGuildInstance(req.params.guildId); diff --git a/backend/src/api/rateLimits.ts b/backend/src/api/rateLimits.ts new file mode 100644 index 00000000..90b344d5 --- /dev/null +++ b/backend/src/api/rateLimits.ts @@ -0,0 +1,18 @@ +import { Request, Response } from "express"; +import { error } from "./responses"; + +const lastRequestsByKey: Map = new Map(); + +export function rateLimit(getKey: (req: Request) => string, limitMs: number, message = "Rate limited") { + return async (req: Request, res: Response, next) => { + const key = getKey(req); + if (lastRequestsByKey.has(key)) { + if (lastRequestsByKey.get(key)! > Date.now() - limitMs) { + return error(res, message, 429); + } + } + + lastRequestsByKey.set(key, Date.now()); + next(); + }; +} diff --git a/dashboard/src/components/dashboard/GuildImportExport.vue b/dashboard/src/components/dashboard/GuildImportExport.vue index 31716f45..a71c287b 100644 --- a/dashboard/src/components/dashboard/GuildImportExport.vue +++ b/dashboard/src/components/dashboard/GuildImportExport.vue @@ -108,7 +108,7 @@ export default { caseHandlingMode: this.importCaseMode, }); } catch (err) { - this.importError = String(err); + this.importError = err.body?.error ?? String(err); return; } finally { this.importing = false; @@ -134,7 +134,7 @@ export default { guildId: this.$route.params.guildId, }); } catch (err) { - this.exportError = String(err); + this.exportError = err.body?.error ?? String(err); return; } finally { this.exporting = false;