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',
|
collection: 'Survey',
|
||||||
indexes: [{ key: { userId: 1 }, name: 'userId_1' }]
|
indexes: [{ key: { userId: 1 }, name: 'userId_1' }]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
collection: 'UserRateLimit',
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
key: { expirationDate: 1 },
|
|
||||||
name: 'expirationDate_1',
|
|
||||||
expireAfterSeconds: 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
collection: 'UserToken',
|
collection: 'UserToken',
|
||||||
indexes: [{ key: { userId: 1 }, name: 'userId_1' }]
|
indexes: [{ key: { userId: 1 }, name: 'userId_1' }]
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"@fastify/cookie": "9.4.0",
|
"@fastify/cookie": "9.4.0",
|
||||||
"@fastify/csrf-protection": "6.4.1",
|
"@fastify/csrf-protection": "6.4.1",
|
||||||
"@fastify/oauth2": "7.8.1",
|
"@fastify/oauth2": "7.8.1",
|
||||||
"@fastify/rate-limit": "9.1.0",
|
|
||||||
"@fastify/swagger": "8.14.0",
|
"@fastify/swagger": "8.14.0",
|
||||||
"@fastify/swagger-ui": "1.10.2",
|
"@fastify/swagger-ui": "1.10.2",
|
||||||
"@fastify/type-provider-typebox": "3.6.0",
|
"@fastify/type-provider-typebox": "3.6.0",
|
||||||
@@ -33,7 +32,6 @@
|
|||||||
"nodemon": "2.0.22",
|
"nodemon": "2.0.22",
|
||||||
"pino-pretty": "10.2.3",
|
"pino-pretty": "10.2.3",
|
||||||
"query-string": "7.1.3",
|
"query-string": "7.1.3",
|
||||||
"rate-limit-mongo": "^2.3.2",
|
|
||||||
"stripe": "16.0.0",
|
"stripe": "16.0.0",
|
||||||
"validator": "13.11.0"
|
"validator": "13.11.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -365,14 +365,6 @@ model Donation {
|
|||||||
@@index([userId], map: "userId_1")
|
@@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 {
|
model UserToken {
|
||||||
id String @id @map("_id")
|
id String @id @map("_id")
|
||||||
created DateTime @db.Date
|
created DateTime @db.Date
|
||||||
|
|||||||
@@ -54,26 +54,11 @@ describe('auth0 routes', () => {
|
|||||||
superGet = createSuperRequest({ method: 'GET' });
|
superGet = createSuperRequest({ method: 'GET' });
|
||||||
});
|
});
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await fastifyTestInstance.prisma.userRateLimit.deleteMany({});
|
|
||||||
await fastifyTestInstance.prisma.user.deleteMany({
|
await fastifyTestInstance.prisma.user.deleteMany({
|
||||||
where: { email: newUserEmail }
|
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 () => {
|
it('should return 401 if the authorization header is invalid', async () => {
|
||||||
mockedFetch.mockResolvedValueOnce(mockAuth0NotOk());
|
mockedFetch.mockResolvedValueOnce(mockAuth0NotOk());
|
||||||
const res = await superGet('/mobile-login').set(
|
const res = await superGet('/mobile-login').set(
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
import type {
|
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||||
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 isEmail from 'validator/lib/isEmail';
|
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 { auth0Client } from '../../plugins/auth0';
|
||||||
import { createAccessToken } from '../../utils/tokens';
|
import { createAccessToken } from '../../utils/tokens';
|
||||||
import { findOrCreateUser } from '../helpers/auth-helpers';
|
import { findOrCreateUser } from '../helpers/auth-helpers';
|
||||||
@@ -31,75 +23,19 @@ const getEmailFromAuth0 = async (
|
|||||||
return typeof email === 'string' ? email : null;
|
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.
|
* Route handler for Mobile authentication.
|
||||||
*
|
*
|
||||||
* @param fastify The Fastify instance.
|
* @param fastify The Fastify instance.
|
||||||
* @param _options Options passed to the plugin via `fastify.register(plugin, options)`.
|
* @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,
|
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
|
// TODO(Post-MVP): move this into the app, so that we add this hook once for
|
||||||
// all auth routes.
|
// all auth routes.
|
||||||
fastify.addHook('onRequest', fastify.redirectIfSignedIn);
|
fastify.addHook('onRequest', fastify.redirectIfSignedIn);
|
||||||
@@ -124,6 +60,8 @@ export const mobileAuth0Routes: FastifyPluginAsync = async (
|
|||||||
|
|
||||||
reply.setAccessTokenCookie(createAccessToken(id));
|
reply.setAccessTokenCookie(createAccessToken(id));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Generated
+13
-30
@@ -147,9 +147,6 @@ importers:
|
|||||||
'@fastify/oauth2':
|
'@fastify/oauth2':
|
||||||
specifier: 7.8.1
|
specifier: 7.8.1
|
||||||
version: 7.8.1
|
version: 7.8.1
|
||||||
'@fastify/rate-limit':
|
|
||||||
specifier: 9.1.0
|
|
||||||
version: 9.1.0
|
|
||||||
'@fastify/swagger':
|
'@fastify/swagger':
|
||||||
specifier: 8.14.0
|
specifier: 8.14.0
|
||||||
version: 8.14.0
|
version: 8.14.0
|
||||||
@@ -219,9 +216,6 @@ importers:
|
|||||||
query-string:
|
query-string:
|
||||||
specifier: 7.1.3
|
specifier: 7.1.3
|
||||||
version: 7.1.3
|
version: 7.1.3
|
||||||
rate-limit-mongo:
|
|
||||||
specifier: ^2.3.2
|
|
||||||
version: 2.3.2
|
|
||||||
stripe:
|
stripe:
|
||||||
specifier: 16.0.0
|
specifier: 16.0.0
|
||||||
version: 16.0.0
|
version: 16.0.0
|
||||||
@@ -3038,9 +3032,6 @@ packages:
|
|||||||
'@fastify/oauth2@7.8.1':
|
'@fastify/oauth2@7.8.1':
|
||||||
resolution: {integrity: sha512-PBIMizzgEOcUcttyfX1hC6CR9vESoI1lfNucBywgcqrxvknVg+zvBCgH2+oU8NvrpSDMtlY6nyuEYYZtVhDT7Q==}
|
resolution: {integrity: sha512-PBIMizzgEOcUcttyfX1hC6CR9vESoI1lfNucBywgcqrxvknVg+zvBCgH2+oU8NvrpSDMtlY6nyuEYYZtVhDT7Q==}
|
||||||
|
|
||||||
'@fastify/rate-limit@9.1.0':
|
|
||||||
resolution: {integrity: sha512-h5dZWCkuZXN0PxwqaFQLxeln8/LNwQwH9popywmDCFdKfgpi4b/HoMH1lluy6P+30CG9yzzpSpwTCIPNB9T1JA==}
|
|
||||||
|
|
||||||
'@fastify/send@2.1.0':
|
'@fastify/send@2.1.0':
|
||||||
resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==}
|
resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==}
|
||||||
|
|
||||||
@@ -12749,10 +12740,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==}
|
resolution: {integrity: sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
toad-cache@3.7.0:
|
|
||||||
resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
toidentifier@1.0.0:
|
toidentifier@1.0.0:
|
||||||
resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==}
|
resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
@@ -14489,7 +14476,7 @@ snapshots:
|
|||||||
'@babel/traverse': 7.23.7
|
'@babel/traverse': 7.23.7
|
||||||
'@babel/types': 7.23.9
|
'@babel/types': 7.23.9
|
||||||
convert-source-map: 2.0.0
|
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
|
gensync: 1.0.0-beta.2
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
@@ -16694,7 +16681,7 @@ snapshots:
|
|||||||
'@babel/helper-split-export-declaration': 7.22.6
|
'@babel/helper-split-export-declaration': 7.22.6
|
||||||
'@babel/parser': 7.23.6
|
'@babel/parser': 7.23.6
|
||||||
'@babel/types': 7.23.9
|
'@babel/types': 7.23.9
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
globals: 11.12.0
|
globals: 11.12.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -17033,12 +17020,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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':
|
'@fastify/send@2.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lukeed/ms': 2.0.2
|
'@lukeed/ms': 2.0.2
|
||||||
@@ -19265,7 +19246,7 @@ snapshots:
|
|||||||
|
|
||||||
agent-base@6.0.2:
|
agent-base@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -19592,7 +19573,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@fastify/error': 3.4.1
|
'@fastify/error': 3.4.1
|
||||||
archy: 1.0.0
|
archy: 1.0.0
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
fastq: 1.17.1
|
fastq: 1.17.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -21172,6 +21153,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
supports-color: 5.5.0
|
supports-color: 5.5.0
|
||||||
|
|
||||||
|
debug@4.3.4:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.2
|
||||||
|
|
||||||
debug@4.3.4(supports-color@8.1.1):
|
debug@4.3.4(supports-color@8.1.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
@@ -24106,7 +24091,7 @@ snapshots:
|
|||||||
https-proxy-agent@5.0.1:
|
https-proxy-agent@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 6.0.2
|
agent-base: 6.0.2
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -24652,7 +24637,7 @@ snapshots:
|
|||||||
|
|
||||||
istanbul-lib-source-maps@4.0.1:
|
istanbul-lib-source-maps@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
istanbul-lib-coverage: 3.2.0
|
istanbul-lib-coverage: 3.2.0
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -25198,7 +25183,7 @@ snapshots:
|
|||||||
|
|
||||||
json-schema-resolver@2.0.0:
|
json-schema-resolver@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
rfdc: 1.3.0
|
rfdc: 1.3.0
|
||||||
uri-js: 4.4.1
|
uri-js: 4.4.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -28999,7 +28984,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@hapi/hoek': 11.0.4
|
'@hapi/hoek': 11.0.4
|
||||||
'@hapi/wreck': 18.1.0
|
'@hapi/wreck': 18.1.0
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
joi: 17.12.2
|
joi: 17.12.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -29621,7 +29606,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
component-emitter: 1.3.0
|
component-emitter: 1.3.0
|
||||||
cookiejar: 2.1.4
|
cookiejar: 2.1.4
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
fast-safe-stringify: 2.1.1
|
fast-safe-stringify: 2.1.1
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
formidable: 2.1.2
|
formidable: 2.1.2
|
||||||
@@ -29873,8 +29858,6 @@ snapshots:
|
|||||||
|
|
||||||
toad-cache@3.3.0: {}
|
toad-cache@3.3.0: {}
|
||||||
|
|
||||||
toad-cache@3.7.0: {}
|
|
||||||
|
|
||||||
toidentifier@1.0.0: {}
|
toidentifier@1.0.0: {}
|
||||||
|
|
||||||
toidentifier@1.0.1: {}
|
toidentifier@1.0.1: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user