feat(api): remove rate limiting (#58289)

This commit is contained in:
Oliver Eyton-Williams
2025-01-23 22:35:50 +01:00
committed by GitHub
parent 8ff5362118
commit 9429f52fd4
6 changed files with 21 additions and 135 deletions
-10
View File
@@ -127,16 +127,6 @@ const indexData: IndexData[] = [
collection: 'Survey',
indexes: [{ key: { userId: 1 }, name: 'userId_1' }]
},
{
collection: 'UserRateLimit',
indexes: [
{
key: { expirationDate: 1 },
name: 'expirationDate_1',
expireAfterSeconds: 0
}
]
},
{
collection: 'UserToken',
indexes: [{ key: { userId: 1 }, name: 'userId_1' }]
-2
View File
@@ -9,7 +9,6 @@
"@fastify/cookie": "9.4.0",
"@fastify/csrf-protection": "6.4.1",
"@fastify/oauth2": "7.8.1",
"@fastify/rate-limit": "9.1.0",
"@fastify/swagger": "8.14.0",
"@fastify/swagger-ui": "1.10.2",
"@fastify/type-provider-typebox": "3.6.0",
@@ -33,7 +32,6 @@
"nodemon": "2.0.22",
"pino-pretty": "10.2.3",
"query-string": "7.1.3",
"rate-limit-mongo": "^2.3.2",
"stripe": "16.0.0",
"validator": "13.11.0"
},
-8
View File
@@ -365,14 +365,6 @@ model Donation {
@@index([userId], map: "userId_1")
}
model UserRateLimit {
id String @id @map("_id")
counter Int
expirationDate DateTime @db.Date
@@index([expirationDate], map: "expirationDate_1")
}
model UserToken {
id String @id @map("_id")
created DateTime @db.Date
-15
View File
@@ -54,26 +54,11 @@ describe('auth0 routes', () => {
superGet = createSuperRequest({ method: 'GET' });
});
beforeEach(async () => {
await fastifyTestInstance.prisma.userRateLimit.deleteMany({});
await fastifyTestInstance.prisma.user.deleteMany({
where: { email: newUserEmail }
});
});
it('should be rate-limited', async () => {
// Rather than spamming the endpoint, we can check the headers.
const res = await superGet('/mobile-login');
// These headers are semi-official
// https://www.ietf.org/archive/id/draft-polli-ratelimit-headers-02.html
// so should not depend on the details of the rate-limiting library
expect(res.headers['ratelimit-limit']).toBe('10');
expect(res.headers['ratelimit-remaining']).toBe('9');
expect(res.headers['ratelimit-reset']).toMatch(/^\d+$/);
const res2 = await superGet('/mobile-login');
expect(res2.headers['ratelimit-remaining']).toBe('8');
});
it('should return 401 if the authorization header is invalid', async () => {
mockedFetch.mockResolvedValueOnce(mockAuth0NotOk());
const res = await superGet('/mobile-login').set(
+8 -70
View File
@@ -1,15 +1,7 @@
import type {
FastifyPluginCallback,
FastifyPluginAsync,
FastifyRequest,
RouteOptions
} from 'fastify';
import rateLimit, { type FastifyRateLimitStore } from '@fastify/rate-limit';
// @ts-expect-error - no types
import MongoStoreRL from 'rate-limit-mongo';
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
import isEmail from 'validator/lib/isEmail';
import { AUTH0_DOMAIN, MONGOHQ_URL } from '../../utils/env';
import { AUTH0_DOMAIN } from '../../utils/env';
import { auth0Client } from '../../plugins/auth0';
import { createAccessToken } from '../../utils/tokens';
import { findOrCreateUser } from '../helpers/auth-helpers';
@@ -31,75 +23,19 @@ const getEmailFromAuth0 = async (
return typeof email === 'string' ? email : null;
};
// TODO: Use Redis! Then we don't need to maintain this store.
class Store implements FastifyRateLimitStore {
mongoStore: MongoStoreRL;
// We don't really need this.options, but it's here for consistency with the
// custom store in the fastify-rate-limit docs.
options: { timeWindow: number };
constructor({ timeWindow }: { timeWindow: number }) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
this.mongoStore = new MongoStoreRL({
collectionName: 'UserRateLimit',
uri: MONGOHQ_URL,
expireTimeMs: timeWindow // timeWindow is Fastify's equivalent of express-rate-limit's expireTimeMs
});
this.options = { timeWindow };
}
incr(
key: string,
cb: (err: Error | null, result?: { current: number; ttl: number }) => void
) {
// This converts between what rate-limit-mongo calls and what
// fastify-rate-limit expects
const callbackConverted = (
err: Error | null,
current: number,
expires: Date
) => {
const ttl = expires.getTime() - Date.now();
cb(err, { current, ttl });
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
this.mongoStore.incr(key, callbackConverted);
}
// routeOptions are ignored for now, but this is the signature we need to implement
child(
routeOptions: RouteOptions & { path: string; prefix: string }
): FastifyRateLimitStore {
const childParams = { ...this.options, ...routeOptions };
const store = new Store(childParams);
return store;
}
}
/**
* Route handler for Mobile authentication.
*
* @param fastify The Fastify instance.
* @param _options Options passed to the plugin via `fastify.register(plugin, options)`.
* @param done Callback to signal that the logic has completed.
*
*/
export const mobileAuth0Routes: FastifyPluginAsync = async (
export const mobileAuth0Routes: FastifyPluginCallback = (
fastify,
_options
_options,
done
) => {
// Rate limit for mobile login
// 10 requests per 15 minute windows
// @ts-expect-error - no types
await fastify.register(rateLimit, {
timeWindow: 15 * 60 * 1000,
max: 10,
enableDraftSpec: true, // ratelimit-* instead of x-ratelimit-*
keyGenerator: req => {
return (req.headers['x-forwarded-for'] as string) || 'localhost';
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
store: Store
});
// TODO(Post-MVP): move this into the app, so that we add this hook once for
// all auth routes.
fastify.addHook('onRequest', fastify.redirectIfSignedIn);
@@ -124,6 +60,8 @@ export const mobileAuth0Routes: FastifyPluginAsync = async (
reply.setAccessTokenCookie(createAccessToken(id));
});
done();
};
/**
+13 -30
View File
@@ -147,9 +147,6 @@ importers:
'@fastify/oauth2':
specifier: 7.8.1
version: 7.8.1
'@fastify/rate-limit':
specifier: 9.1.0
version: 9.1.0
'@fastify/swagger':
specifier: 8.14.0
version: 8.14.0
@@ -219,9 +216,6 @@ importers:
query-string:
specifier: 7.1.3
version: 7.1.3
rate-limit-mongo:
specifier: ^2.3.2
version: 2.3.2
stripe:
specifier: 16.0.0
version: 16.0.0
@@ -3038,9 +3032,6 @@ packages:
'@fastify/oauth2@7.8.1':
resolution: {integrity: sha512-PBIMizzgEOcUcttyfX1hC6CR9vESoI1lfNucBywgcqrxvknVg+zvBCgH2+oU8NvrpSDMtlY6nyuEYYZtVhDT7Q==}
'@fastify/rate-limit@9.1.0':
resolution: {integrity: sha512-h5dZWCkuZXN0PxwqaFQLxeln8/LNwQwH9popywmDCFdKfgpi4b/HoMH1lluy6P+30CG9yzzpSpwTCIPNB9T1JA==}
'@fastify/send@2.1.0':
resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==}
@@ -12749,10 +12740,6 @@ packages:
resolution: {integrity: sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==}
engines: {node: '>=12'}
toad-cache@3.7.0:
resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
engines: {node: '>=12'}
toidentifier@1.0.0:
resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==}
engines: {node: '>=0.6'}
@@ -14489,7 +14476,7 @@ snapshots:
'@babel/traverse': 7.23.7
'@babel/types': 7.23.9
convert-source-map: 2.0.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -16694,7 +16681,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.23.6
'@babel/types': 7.23.9
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -17033,12 +17020,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@fastify/rate-limit@9.1.0':
dependencies:
'@lukeed/ms': 2.0.2
fastify-plugin: 4.5.1
toad-cache: 3.7.0
'@fastify/send@2.1.0':
dependencies:
'@lukeed/ms': 2.0.2
@@ -19265,7 +19246,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
transitivePeerDependencies:
- supports-color
@@ -19592,7 +19573,7 @@ snapshots:
dependencies:
'@fastify/error': 3.4.1
archy: 1.0.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
fastq: 1.17.1
transitivePeerDependencies:
- supports-color
@@ -21172,6 +21153,10 @@ snapshots:
optionalDependencies:
supports-color: 5.5.0
debug@4.3.4:
dependencies:
ms: 2.1.2
debug@4.3.4(supports-color@8.1.1):
dependencies:
ms: 2.1.2
@@ -24106,7 +24091,7 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
transitivePeerDependencies:
- supports-color
@@ -24652,7 +24637,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1:
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
istanbul-lib-coverage: 3.2.0
source-map: 0.6.1
transitivePeerDependencies:
@@ -25198,7 +25183,7 @@ snapshots:
json-schema-resolver@2.0.0:
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
rfdc: 1.3.0
uri-js: 4.4.1
transitivePeerDependencies:
@@ -28999,7 +28984,7 @@ snapshots:
dependencies:
'@hapi/hoek': 11.0.4
'@hapi/wreck': 18.1.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
joi: 17.12.2
transitivePeerDependencies:
- supports-color
@@ -29621,7 +29606,7 @@ snapshots:
dependencies:
component-emitter: 1.3.0
cookiejar: 2.1.4
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
fast-safe-stringify: 2.1.1
form-data: 4.0.0
formidable: 2.1.2
@@ -29873,8 +29858,6 @@ snapshots:
toad-cache@3.3.0: {}
toad-cache@3.7.0: {}
toidentifier@1.0.0: {}
toidentifier@1.0.1: {}