mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(api): remove rate limiting (#58289)
This commit is contained in:
committed by
GitHub
parent
8ff5362118
commit
9429f52fd4
@@ -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' }]
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Generated
+13
-30
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user