Skip to content
This repository was archived by the owner on Apr 8, 2026. It is now read-only.

Commit 351eb8d

Browse files
committed
feat(rate-limiting): implement rate limiting for various controllers to enhance API stability
1 parent 6e01406 commit 351eb8d

7 files changed

Lines changed: 317 additions & 33 deletions

File tree

src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,5 @@ server.setErrorConfig(app => {
4343
});
4444

4545
export const app = server.build();
46+
47+

src/controllers/GameController.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Request, Response } from 'express';
2+
import rateLimit from 'express-rate-limit';
23
import { inject } from 'inversify';
34
import { controller, httpGet, httpPost, httpPut } from 'inversify-express-utils';
45
import fetch from 'node-fetch';
@@ -30,6 +31,46 @@ async function validateOr400(schema: yup.Schema<unknown>, data: unknown, res: Re
3031
}
3132
}
3233

34+
const createGameRateLimit = rateLimit({
35+
windowMs: 60 * 60 * 1000,
36+
max: 5,
37+
message: 'Too many game creations, please try again later.',
38+
standardHeaders: true,
39+
legacyHeaders: false,
40+
});
41+
42+
const updateGameRateLimit = rateLimit({
43+
windowMs: 60 * 60 * 1000,
44+
max: 10,
45+
message: 'Too many game updates, please try again later.',
46+
standardHeaders: true,
47+
legacyHeaders: false,
48+
});
49+
50+
const buyGameRateLimit = rateLimit({
51+
windowMs: 60 * 60 * 1000,
52+
max: 20,
53+
message: 'Too many game purchases, please try again later.',
54+
standardHeaders: true,
55+
legacyHeaders: false,
56+
});
57+
58+
const transferOwnershipRateLimit = rateLimit({
59+
windowMs: 60 * 60 * 1000,
60+
max: 5,
61+
message: 'Too many ownership transfers, please try again later.',
62+
standardHeaders: true,
63+
legacyHeaders: false,
64+
});
65+
66+
const transferGameRateLimit = rateLimit({
67+
windowMs: 60 * 60 * 1000,
68+
max: 10,
69+
message: 'Too many game transfers, please try again later.',
70+
standardHeaders: true,
71+
legacyHeaders: false,
72+
});
73+
3374
@controller('/games')
3475
export class Games {
3576
constructor(
@@ -157,7 +198,7 @@ export class Games {
157198
}
158199
}
159200

160-
@httpPost('/', LoggedCheck.middleware)
201+
@httpPost('/', LoggedCheck.middleware, createGameRateLimit)
161202
public async createGame(req: AuthenticatedRequest, res: Response) {
162203
if (!(await validateOr400(createGameBodySchema, req.body, res))) {
163204
await this.createLog(req, 'createGame', 'games', 400, req.user?.user_id);
@@ -176,7 +217,7 @@ export class Games {
176217
}
177218
}
178219

179-
@httpPut(':gameId', LoggedCheck.middleware)
220+
@httpPut(':gameId', LoggedCheck.middleware, updateGameRateLimit)
180221
public async updateGame(req: AuthenticatedRequest, res: Response) {
181222
if (!(await validateOr400(gameIdParamSchema, req.params, res))) {
182223
await this.createLog(req, 'updateGame', 'games', 400, req.user?.user_id);
@@ -206,7 +247,7 @@ export class Games {
206247
}
207248
}
208249

209-
@httpPost(':gameId/buy', LoggedCheck.middleware)
250+
@httpPost(':gameId/buy', LoggedCheck.middleware, buyGameRateLimit)
210251
public async buyGame(req: AuthenticatedRequest, res: Response) {
211252
const { gameId } = req.params;
212253
const userId = req.user.user_id;
@@ -251,7 +292,7 @@ export class Games {
251292
}
252293
}
253294

254-
@httpPost('/transfer-ownership/:gameId', LoggedCheck.middleware)
295+
@httpPost('/transfer-ownership/:gameId', LoggedCheck.middleware, transferOwnershipRateLimit)
255296
public async transferOwnership(req: AuthenticatedRequest, res: Response) {
256297
const { gameId } = req.params;
257298
const { newOwnerId } = req.body;
@@ -285,7 +326,7 @@ export class Games {
285326
}
286327
}
287328

288-
@httpPost(':gameId/transfer', LoggedCheck.middleware)
329+
@httpPost(':gameId/transfer', LoggedCheck.middleware, transferGameRateLimit)
289330
public async transferGame(req: AuthenticatedRequest, res: Response) {
290331
if (!(await validateOr400(gameIdParamSchema, req.params, res))) {
291332
await this.createLog(req, 'transferGame', 'games', 400, req.user?.user_id);
@@ -378,3 +419,5 @@ export class Games {
378419
}
379420
}
380421
}
422+
423+

src/controllers/ItemController.ts

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Request, Response } from 'express';
2+
import rateLimit from 'express-rate-limit';
23
import { inject } from 'inversify';
34
import { controller, httpDelete, httpGet, httpPost, httpPut } from 'inversify-express-utils';
45
import { v4 } from 'uuid';
@@ -30,6 +31,70 @@ async function validateOr400(schema: Schema<unknown>, data: unknown, res: Respon
3031
}
3132
}
3233

34+
const createItemRateLimit = rateLimit({
35+
windowMs: 60 * 60 * 1000,
36+
max: 500,
37+
message: 'Too many item creations, please try again later.',
38+
standardHeaders: true,
39+
legacyHeaders: false,
40+
});
41+
42+
const updateItemRateLimit = rateLimit({
43+
windowMs: 60 * 60 * 1000,
44+
max: 1000,
45+
message: 'Too many item updates, please try again later.',
46+
standardHeaders: true,
47+
legacyHeaders: false,
48+
});
49+
50+
const deleteItemRateLimit = rateLimit({
51+
windowMs: 60 * 60 * 1000,
52+
max: 500,
53+
message: 'Too many item deletions, please try again later.',
54+
standardHeaders: true,
55+
legacyHeaders: false,
56+
});
57+
58+
const buyItemRateLimit = rateLimit({
59+
windowMs: 60 * 60 * 1000,
60+
max: 2000,
61+
message: 'Too many item purchases, please try again later.',
62+
standardHeaders: true,
63+
legacyHeaders: false,
64+
});
65+
66+
const sellItemRateLimit = rateLimit({
67+
windowMs: 60 * 60 * 1000,
68+
max: 2000,
69+
message: 'Too many item sales, please try again later.',
70+
standardHeaders: true,
71+
legacyHeaders: false,
72+
});
73+
74+
const consumeItemRateLimit = rateLimit({
75+
windowMs: 60 * 60 * 1000,
76+
max: 1000,
77+
message: 'Too many item consumptions, please try again later.',
78+
standardHeaders: true,
79+
legacyHeaders: false,
80+
});
81+
82+
const dropItemRateLimit = rateLimit({
83+
windowMs: 60 * 60 * 1000,
84+
max: 1000,
85+
message: 'Too many item drops, please try again later.',
86+
standardHeaders: true,
87+
legacyHeaders: false,
88+
});
89+
90+
const transferOwnershipRateLimit = rateLimit({
91+
windowMs: 60 * 60 * 1000,
92+
max: 500,
93+
message: 'Too many ownership transfers, please try again later.',
94+
standardHeaders: true,
95+
legacyHeaders: false,
96+
});
97+
3398
@controller('/items')
3499
export class Items {
35100
constructor(
@@ -216,7 +281,7 @@ export class Items {
216281
example: 'POST /api/items/create {"name": "Apple", "description": "A fruit", "price": 100, "iconHash": "abc123", "showInStore": true}',
217282
requiresAuth: true,
218283
})
219-
@httpPost('/create', LoggedCheck.middleware)
284+
@httpPost('/create', LoggedCheck.middleware, createItemRateLimit)
220285
public async createItem(req: AuthenticatedRequest, res: Response) {
221286
if (!(await validateOr400(createItemValidator, req.body, res, 'Invalid item data'))) {
222287
await this.createLog(req, 'items', 400);
@@ -259,7 +324,7 @@ export class Items {
259324
example: 'PUT /api/items/update/123 {"name": "Apple", "description": "A fruit", "price": 100, "iconHash": "abc123", "showInStore": true}',
260325
requiresAuth: true,
261326
})
262-
@httpPut('/update/:itemId', OwnerCheck.middleware)
327+
@httpPut('/update/:itemId', OwnerCheck.middleware, updateItemRateLimit)
263328
public async updateItem(req: AuthenticatedRequestWithOwner, res: Response) {
264329
if (!(await validateOr400(itemIdParamValidator, req.params, res, 'Invalid itemId'))) {
265330
await this.createLog(req, 'items', 400);
@@ -305,7 +370,7 @@ export class Items {
305370
example: 'DELETE /api/items/delete/123',
306371
requiresAuth: true,
307372
})
308-
@httpDelete('/delete/:itemId', OwnerCheck.middleware)
373+
@httpDelete('/delete/:itemId', OwnerCheck.middleware, deleteItemRateLimit)
309374
public async deleteItem(req: AuthenticatedRequestWithOwner, res: Response) {
310375
if (!(await validateOr400(itemIdParamValidator, req.params, res, 'Invalid itemId'))) {
311376
await this.createLog(req, 'items', 400);
@@ -322,7 +387,7 @@ export class Items {
322387
}
323388
}
324389

325-
@httpPost('/buy/:itemId', LoggedCheck.middleware)
390+
@httpPost('/buy/:itemId', LoggedCheck.middleware, buyItemRateLimit)
326391
public async buyItem(req: AuthenticatedRequest, res: Response) {
327392
const { itemId } = req.params;
328393
const { amount } = req.body;
@@ -362,7 +427,7 @@ export class Items {
362427
}
363428
}
364429

365-
@httpPost('/sell/:itemId', LoggedCheck.middleware)
430+
@httpPost('/sell/:itemId', LoggedCheck.middleware, sellItemRateLimit)
366431
public async sellItem(req: AuthenticatedRequest, res: Response) {
367432
const { itemId } = req.params;
368433
const { amount, purchasePrice, dataItemIndex } = req.body;
@@ -418,7 +483,7 @@ export class Items {
418483
}
419484
}
420485

421-
@httpPost('/consume/:itemId', OwnerCheck.middleware)
486+
@httpPost('/consume/:itemId', OwnerCheck.middleware, consumeItemRateLimit)
422487
public async consumeItem(req: AuthenticatedRequestWithOwner, res: Response) {
423488
const { itemId } = req.params;
424489
const { amount, uniqueId, userId } = req.body;
@@ -468,7 +533,7 @@ export class Items {
468533
}
469534
}
470535

471-
@httpPost('/drop/:itemId', LoggedCheck.middleware)
536+
@httpPost('/drop/:itemId', LoggedCheck.middleware, dropItemRateLimit)
472537
public async dropItem(req: AuthenticatedRequest, res: Response) {
473538
const { itemId } = req.params;
474539
const { amount, uniqueId, dataItemIndex } = req.body;
@@ -513,7 +578,7 @@ export class Items {
513578
}
514579
}
515580

516-
@httpPost('/transfer-ownership/:itemId', OwnerCheck.middleware)
581+
@httpPost('/transfer-ownership/:itemId', OwnerCheck.middleware, transferOwnershipRateLimit)
517582
public async transferOwnership(req: AuthenticatedRequestWithOwner, res: Response) {
518583
const { itemId } = req.params;
519584
const { newOwnerId } = req.body;
@@ -541,3 +606,5 @@ export class Items {
541606
}
542607
}
543608
}
609+
610+

src/controllers/LobbyController.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Request, Response } from 'express';
2+
import rateLimit from 'express-rate-limit';
23
import { inject } from 'inversify';
34
import { controller, httpGet, httpPost } from 'inversify-express-utils';
45
import { v4 } from 'uuid';
@@ -27,6 +28,30 @@ async function validateOr400(schema: Schema<unknown>, data: unknown, res: Respon
2728
}
2829
}
2930

31+
const createLobbyRateLimit = rateLimit({
32+
windowMs: 60 * 60 * 1000,
33+
max: 30,
34+
message: 'Too many lobby creations, please try again later.',
35+
standardHeaders: true,
36+
legacyHeaders: false,
37+
});
38+
39+
const joinLobbyRateLimit = rateLimit({
40+
windowMs: 60 * 60 * 1000,
41+
max: 100,
42+
message: 'Too many lobby joins, please try again later.',
43+
standardHeaders: true,
44+
legacyHeaders: false,
45+
});
46+
47+
const leaveLobbyRateLimit = rateLimit({
48+
windowMs: 60 * 60 * 1000,
49+
max: 100,
50+
message: 'Too many lobby leaves, please try again later.',
51+
standardHeaders: true,
52+
legacyHeaders: false,
53+
});
54+
3055
@controller('/lobbies')
3156
export class Lobbies {
3257
constructor(
@@ -59,7 +84,7 @@ export class Lobbies {
5984
example: 'POST /api/lobbies',
6085
requiresAuth: true,
6186
})
62-
@httpPost('/', LoggedCheck.middleware)
87+
@httpPost('/', LoggedCheck.middleware, createLobbyRateLimit)
6388
public async createLobby(req: AuthenticatedRequest, res: Response) {
6489
try {
6590
const lobbyId = v4();
@@ -179,7 +204,7 @@ export class Lobbies {
179204
example: 'POST /api/lobbies/123/join',
180205
requiresAuth: true,
181206
})
182-
@httpPost('/:lobbyId/join', LoggedCheck.middleware)
207+
@httpPost('/:lobbyId/join', LoggedCheck.middleware, joinLobbyRateLimit)
183208
public async joinLobby(req: AuthenticatedRequest, res: Response) {
184209
if (!(await validateOr400(lobbyIdParamSchema, req.params, res))) {
185210
await this.createLog(req, 'joinLobby', 'lobbies', 400, req.user.user_id);
@@ -205,7 +230,7 @@ export class Lobbies {
205230
example: 'POST /api/lobbies/123/leave',
206231
requiresAuth: true,
207232
})
208-
@httpPost('/:lobbyId/leave', LoggedCheck.middleware)
233+
@httpPost('/:lobbyId/leave', LoggedCheck.middleware, leaveLobbyRateLimit)
209234
public async leaveLobby(req: AuthenticatedRequest, res: Response) {
210235
if (!(await validateOr400(lobbyIdParamSchema, req.params, res))) {
211236
await this.createLog(req, 'leaveLobby', 'lobbies', 400, req.user.user_id);
@@ -221,3 +246,5 @@ export class Lobbies {
221246
}
222247
}
223248
}
249+
250+

0 commit comments

Comments
 (0)