Skip to content

Commit fc6fcbf

Browse files
committed
implement rate limiting on specific routes
1 parent f90e61e commit fc6fcbf

File tree

7 files changed

+52
-14
lines changed

7 files changed

+52
-14
lines changed

src/api/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint import/no-nodejs-modules: ["error", {"allow": ["crypto"]}] */
22
import { randomUUID } from "crypto";
3-
import fastify, { FastifyInstance } from "fastify";
3+
import fastify, { FastifyInstance, FastifyRequest } from "fastify";
44
import FastifyAuthProvider from "@fastify/auth";
55
import fastifyAuthPlugin from "./plugins/auth.js";
66
import protectedRoute from "./routes/protected.js";
@@ -72,10 +72,6 @@ async function init() {
7272
await app.register(fastifyZodValidationPlugin);
7373
await app.register(FastifyAuthProvider);
7474
await app.register(errorHandlerPlugin);
75-
await app.register(rateLimiterPlugin, {
76-
limit: 50,
77-
duration: 20,
78-
});
7975
if (!process.env.RunEnvironment) {
8076
process.env.RunEnvironment = "dev";
8177
}

src/api/plugins/rateLimiter.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import { genericConfig } from "common/config.js";
88
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from "fastify";
99

1010
interface RateLimiterOptions {
11-
limit?: number;
11+
limit?: number | ((request: FastifyRequest) => number);
1212
duration?: number;
13+
rateLimitIdentifier?: string;
1314
}
1415

1516
interface RateLimitParams {
@@ -76,26 +77,32 @@ const rateLimiterPlugin: FastifyPluginAsync<RateLimiterOptions> = async (
7677
fastify,
7778
options,
7879
) => {
79-
const { limit = 10, duration = 60 } = options;
80-
80+
const {
81+
limit = 10,
82+
duration = 60,
83+
rateLimitIdentifier = "api-request",
84+
} = options;
8185
fastify.addHook(
82-
"onRequest",
86+
"preHandler",
8387
async (request: FastifyRequest, reply: FastifyReply) => {
8488
const userIdentifier = request.ip;
85-
const rateLimitIdentifier = "api-request";
86-
89+
console.log(request.username);
90+
let computedLimit = limit;
91+
if (typeof computedLimit === "function") {
92+
computedLimit = computedLimit(request);
93+
}
8794
const { limited, resetTime, used } = await isAtLimit({
8895
ddbClient: fastify.dynamoClient,
8996
rateLimitIdentifier,
9097
duration,
91-
limit,
98+
limit: computedLimit,
9299
userIdentifier,
93100
});
94-
reply.header("X-RateLimit-Limit", limit.toString());
101+
reply.header("X-RateLimit-Limit", computedLimit.toString());
95102
reply.header("X-RateLimit-Reset", resetTime?.toString() || "0");
96103
reply.header(
97104
"X-RateLimit-Remaining",
98-
limited ? 0 : used ? limit - used : limit - 1,
105+
limited ? 0 : used ? computedLimit - used : computedLimit - 1,
99106
);
100107
if (limited) {
101108
const retryAfter = resetTime

src/api/routes/events.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { randomUUID } from "crypto";
2424
import moment from "moment-timezone";
2525
import { IUpdateDiscord, updateDiscord } from "../functions/discord.js";
26+
import rateLimiter from "api/plugins/rateLimiter.js";
2627

2728
// POST
2829

@@ -88,6 +89,16 @@ type EventsGetQueryParams = {
8889
};
8990

9091
const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
92+
fastify.register(rateLimiter, {
93+
limit: (request) => {
94+
if (request.method === "GET") {
95+
return 30;
96+
}
97+
return 15;
98+
},
99+
duration: 60,
100+
rateLimitIdentifier: "events",
101+
});
91102
fastify.post<{ Body: EventPostRequest }>(
92103
"/:id?",
93104
{

src/api/routes/ics.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import moment from "moment";
1616
import { getVtimezoneComponent } from "@touch4it/ical-timezones";
1717
import { OrganizationList } from "../../common/orgs.js";
1818
import { EventRepeatOptions } from "./events.js";
19+
import rateLimiter from "api/plugins/rateLimiter.js";
1920

2021
const repeatingIcalMap: Record<EventRepeatOptions, ICalEventJSONRepeatingData> =
2122
{
@@ -34,6 +35,11 @@ function generateHostName(host: string) {
3435
}
3536

3637
const icalPlugin: FastifyPluginAsync = async (fastify, _options) => {
38+
fastify.register(rateLimiter, {
39+
limit: 10,
40+
duration: 30,
41+
rateLimitIdentifier: "ical",
42+
});
3743
fastify.get("/:host?", async (request, reply) => {
3844
const host = (request.params as Record<string, string>).host;
3945
let queryParams: QueryCommandInput = {

src/api/routes/membership.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ import { genericConfig, roleArns } from "common/config.js";
1111
import { getRoleCredentials } from "api/functions/sts.js";
1212
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
1313
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
14+
import rateLimiter from "api/plugins/rateLimiter.js";
1415

1516
const NONMEMBER_CACHE_SECONDS = 1800; // 30 minutes
1617
const MEMBER_CACHE_SECONDS = 43200; // 12 hours
1718

1819
const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
20+
fastify.register(rateLimiter, {
21+
limit: 20,
22+
duration: 30,
23+
rateLimitIdentifier: "membership",
24+
});
1925
const getAuthorizedClients = async () => {
2026
if (roleArns.Entra) {
2127
fastify.log.info(

src/api/routes/mobileWallet.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
1414
import { genericConfig } from "../../common/config.js";
1515
import { zodToJsonSchema } from "zod-to-json-schema";
16+
import rateLimiter from "api/plugins/rateLimiter.js";
1617

1718
const queuedResponseJsonSchema = zodToJsonSchema(
1819
z.object({
@@ -21,6 +22,11 @@ const queuedResponseJsonSchema = zodToJsonSchema(
2122
);
2223

2324
const mobileWalletRoute: FastifyPluginAsync = async (fastify, _options) => {
25+
fastify.register(rateLimiter, {
26+
limit: 5,
27+
duration: 30,
28+
rateLimitIdentifier: "mobileWallet",
29+
});
2430
fastify.post<{ Querystring: { email: string } }>(
2531
"/membership",
2632
{

src/api/routes/organizations.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { FastifyPluginAsync } from "fastify";
22
import { OrganizationList } from "../../common/orgs.js";
33
import fastifyCaching from "@fastify/caching";
4+
import rateLimiter from "api/plugins/rateLimiter.js";
45

56
const organizationsPlugin: FastifyPluginAsync = async (fastify, _options) => {
67
fastify.register(fastifyCaching, {
78
privacy: fastifyCaching.privacy.PUBLIC,
89
serverExpiresIn: 60 * 60 * 4,
910
expiresIn: 60 * 60 * 4,
1011
});
12+
fastify.register(rateLimiter, {
13+
limit: 60,
14+
duration: 60,
15+
rateLimitIdentifier: "organizations",
16+
});
1117
fastify.get("/", {}, async (request, reply) => {
1218
reply.send(OrganizationList);
1319
});

0 commit comments

Comments
 (0)