Skip to content

Commit 31e4d9d

Browse files
authored
fix(api): validate monthly badge dimensions (JhaSourav07#813)
## Description Fixes JhaSourav07#806 ## Pillar - [x] 🛠️ Other (Bug fix, refactoring, docs) ## What changed The monthly badge accepted `width` and `height` as raw strings, then parsed them in the route with `parseInt`. That allowed values like `width=abc`, `height=-20`, or very large numbers to reach the SVG renderer and produce broken or unreasonable SVG dimensions. This PR moves dimension validation into `streakParamsSchema` so invalid input is rejected before the GitHub fetch or SVG render path runs. The accepted ranges are: - `width`: `100` to `1200` - `height`: `80` to `800` The route now receives already-parsed numbers and no longer needs to call `parseInt` for these params. ## Visual Preview N/A — this is input validation for the monthly SVG route. ## Testing ```bash npm run format npm run test -- app/api/streak/route.test.ts npm run lint npm run typecheck npm run test npm run build ``` ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors. - [x] My commits follow the Conventional Commits format. - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have starred the repo. - [x] I have made sure that I have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse quality standard.
2 parents ef6f3f0 + 05b77c6 commit 31e4d9d

3 files changed

Lines changed: 52 additions & 4 deletions

File tree

app/api/streak/route.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,25 @@ describe('GET /api/streak', () => {
9090

9191
expect(fetchGitHubContributions).not.toHaveBeenCalled();
9292
});
93+
94+
it('returns 400 for invalid monthly badge dimensions', async () => {
95+
const invalidDimensionParams: Array<Record<string, string>> = [
96+
{ width: 'abc' },
97+
{ width: '-50' },
98+
{ width: '1201' },
99+
{ height: 'abc' },
100+
{ height: '0' },
101+
{ height: '801' },
102+
];
103+
104+
for (const params of invalidDimensionParams) {
105+
const response = await GET(makeRequest({ user: 'octocat', view: 'monthly', ...params }));
106+
107+
expect(response.status).toBe(400);
108+
}
109+
110+
expect(fetchGitHubContributions).not.toHaveBeenCalled();
111+
});
93112
});
94113

95114
describe('successful response', () => {
@@ -615,6 +634,18 @@ describe('GET /api/streak', () => {
615634
expect(body).toContain('COMMITS THIS MONTH');
616635
});
617636

637+
it('uses valid custom width and height in monthly SVG output', async () => {
638+
const response = await GET(
639+
makeRequest({ user: 'octocat', view: 'monthly', width: '400', height: '200' })
640+
);
641+
const body = await response.text();
642+
643+
expect(response.status).toBe(200);
644+
expect(body).toContain('width="400"');
645+
expect(body).toContain('height="200"');
646+
expect(body).toContain('viewBox="0 0 400 200"');
647+
});
648+
618649
it('defaults to default view when an unknown view is given', async () => {
619650
const response = await GET(makeRequest({ user: 'octocat', view: 'invalid' }));
620651

app/api/streak/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ export async function GET(request: Request) {
129129
lang,
130130
view,
131131
delta_format,
132-
width: width ? parseInt(width, 10) : undefined,
133-
height: height ? parseInt(height, 10) : undefined,
132+
width,
133+
height,
134134
size,
135135
grace,
136136
};

lib/validations.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
import { z } from 'zod';
22
import { sanitizeHexColor, sanitizeSpeed, sanitizeRadius, sanitizeFont } from './svg/sanitizer';
33

4+
function dimensionParam(name: string, min: number, max: number) {
5+
return z
6+
.string()
7+
.optional()
8+
.refine(
9+
(val) => {
10+
if (val === undefined) return true;
11+
if (!/^\d+$/.test(val)) return false;
12+
13+
const parsed = Number(val);
14+
return parsed >= min && parsed <= max;
15+
},
16+
{ message: `${name} must be an integer between ${min} and ${max}` }
17+
)
18+
.transform((val) => (val === undefined ? undefined : Number(val)));
19+
}
20+
421
export const streakParamsSchema = z.object({
522
// Required — missing user surfaces as "Missing" to match existing tests
623
user: z
@@ -83,8 +100,8 @@ export const streakParamsSchema = z.object({
83100
view: z.enum(['default', 'monthly']).catch('default').default('default'),
84101
// Invalid delta formats fall back to percentage mode.
85102
delta_format: z.enum(['percent', 'absolute', 'both']).catch('percent').default('percent'),
86-
width: z.string().optional(),
87-
height: z.string().optional(),
103+
width: dimensionParam('width', 100, 1200),
104+
height: dimensionParam('height', 80, 800),
88105
grace: z
89106
.string()
90107
.optional()

0 commit comments

Comments
 (0)