diff --git a/packages/backend/src/routes/levels.ts b/packages/backend/src/routes/levels.ts index 0c55e9e32..e1c94b4d0 100644 --- a/packages/backend/src/routes/levels.ts +++ b/packages/backend/src/routes/levels.ts @@ -1,6 +1,10 @@ import type { Express, Response } from 'express' import { requireAuth, type AuthenticatedRequest } from '../middleware/auth' -import { validateBody, validateParams } from '../middleware/validate' +import { + validateBody, + validateParams, + validateQuery, +} from '../middleware/validate' import { writeLimiter } from '../middleware/rateLimit' import { asyncHandler } from '../middleware/asyncHandler' import { AppError } from '../errors/AppError' @@ -15,6 +19,10 @@ function p(val: string | string[]): string { return typeof val === 'string' ? val : val[0] } +const leaderboardQuery = z.object({ + limit: z.coerce.number().int().min(1).max(50).optional(), +}) + const rankParams = guildIdParam.merge(commonUserIdParam) const levelParam = guildIdParam.extend({ level: z.coerce.number().int().min(1), @@ -62,6 +70,7 @@ export function setupLevelsRoutes(app: Express): void { '/api/guilds/:guildId/levels/leaderboard', requireAuth, validateParams(guildIdParam), + validateQuery(leaderboardQuery), asyncHandler(async (req: AuthenticatedRequest, res: Response) => { const guildId = p(req.params.guildId) const limit = Number(req.query.limit) || 10 diff --git a/packages/backend/src/routes/starboard.ts b/packages/backend/src/routes/starboard.ts index b4999927d..196b4f7ea 100644 --- a/packages/backend/src/routes/starboard.ts +++ b/packages/backend/src/routes/starboard.ts @@ -1,6 +1,10 @@ import type { Express, Response } from 'express' import { requireAuth, type AuthenticatedRequest } from '../middleware/auth' -import { validateBody, validateParams } from '../middleware/validate' +import { + validateBody, + validateParams, + validateQuery, +} from '../middleware/validate' import { writeLimiter } from '../middleware/rateLimit' import { asyncHandler } from '../middleware/asyncHandler' import { AppError } from '../errors/AppError' @@ -19,6 +23,10 @@ const upsertConfigBody = z.object({ selfStar: z.boolean().optional(), }) +const entriesQuery = z.object({ + limit: z.coerce.number().int().min(1).max(50).optional(), +}) + export function setupStarboardRoutes(app: Express): void { app.get( '/api/guilds/:guildId/starboard/config', @@ -77,6 +85,7 @@ export function setupStarboardRoutes(app: Express): void { '/api/guilds/:guildId/starboard/entries', requireAuth, validateParams(guildIdParam), + validateQuery(entriesQuery), asyncHandler(async (req: AuthenticatedRequest, res: Response) => { const guildId = p(req.params.guildId) const limit = Number(req.query.limit) || 10 diff --git a/packages/backend/tests/unit/routes/snowflakeValidation.test.ts b/packages/backend/tests/unit/routes/snowflakeValidation.test.ts index 9e58aaf52..f44d871ed 100644 --- a/packages/backend/tests/unit/routes/snowflakeValidation.test.ts +++ b/packages/backend/tests/unit/routes/snowflakeValidation.test.ts @@ -357,6 +357,50 @@ describe('Guild ID Snowflake Validation', () => { ) }) + describe('Pagination limit bounds (#1183)', () => { + const validGuildId = '123456789012345678' + + test.each([ + ['9999', 400], + ['0', 400], + ['-5', 400], + ['abc', 400], + ['25', 200], + ])( + 'GET levels/leaderboard with limit=%s returns %s-class', + async (limit, expected) => { + const app = createApp(setupLevelsRoutes) + const res = await request(app).get( + `/api/guilds/${validGuildId}/levels/leaderboard?limit=${limit}`, + ) + if (expected === 400) { + expect(res.status).toBe(400) + } else { + expect(res.status).not.toBe(400) + } + }, + ) + + test.each([ + ['9999', 400], + ['0', 400], + ['25', 200], + ])( + 'GET starboard/entries with limit=%s returns %s-class', + async (limit, expected) => { + const app = createApp(setupStarboardRoutes) + const res = await request(app).get( + `/api/guilds/${validGuildId}/starboard/entries?limit=${limit}`, + ) + if (expected === 400) { + expect(res.status).toBe(400) + } else { + expect(res.status).not.toBe(400) + } + }, + ) + }) + describe('Music Routes (#1200)', () => { const validGuildId = '123456789012345678' const invalidGuildId = 'not-a-snowflake'