Skip to content

Commit 9ac9d4c

Browse files
committed
fix(api): validate usernames in JSON routes
1 parent b124f6b commit 9ac9d4c

3 files changed

Lines changed: 31 additions & 3 deletions

File tree

app/api/github/route.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ describe('GET /api/github', () => {
2929
expect(body.error).toContain('Invalid parameters');
3030
});
3131

32+
it('returns 400 and skips GitHub when username format is invalid', async () => {
33+
const response = await GET(makeRequest({ username: 'bad user' }));
34+
const body = await response.json();
35+
36+
expect(response.status).toBe(400);
37+
expect(body.error).toContain('Invalid parameters');
38+
expect(getFullDashboardData).not.toHaveBeenCalled();
39+
});
40+
3241
// Test 2 — valid username → 200
3342
it('returns 200 with JSON body for a valid username', async () => {
3443
vi.mocked(getFullDashboardData).mockResolvedValue({ profile: 'octocat' } as never);

app/api/stats/route.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ describe('GET /api/stats', () => {
5555
expect(fetchGitHubContributions).not.toHaveBeenCalled();
5656
});
5757

58+
it('returns 400 and skips GitHub when the username format is invalid', async () => {
59+
const response = await GET(makeRequest({ user: 'octo/cat' }));
60+
61+
expect(response.status).toBe(400);
62+
expect(fetchGitHubContributions).not.toHaveBeenCalled();
63+
});
64+
5865
it('returns 400 for an unknown timezone', async () => {
5966
const response = await GET(makeRequest({ user: 'testuser', tz: 'Not/ATimezone' }));
6067
expect(response.status).toBe(400);

lib/validations.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,15 @@ function dimensionParam(name: string, min: number, max: number) {
5858
.transform(toDimensionValue);
5959
}
6060

61+
const GITHUB_USERNAME_REGEX = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9]))*$/;
62+
6163
export const streakParamsSchema = z.object({
6264
// Required — missing user surfaces as "Missing" to match existing tests
6365
user: z
6466
.string({ error: 'Missing user parameter' })
6567
.min(1, { message: 'Missing user parameter' })
6668
.max(39, { message: 'GitHub username cannot exceed 39 characters' })
67-
.regex(/^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9]))*$/, {
69+
.regex(GITHUB_USERNAME_REGEX, {
6870
message: 'Invalid GitHub username',
6971
}),
7072

@@ -218,7 +220,11 @@ export const streakParamsSchema = z.object({
218220
export const githubParamsSchema = z.object({
219221
username: z
220222
.string({ error: 'Missing "username" parameter' })
221-
.min(1, { message: 'Username is required' }),
223+
.min(1, { message: 'Username is required' })
224+
.max(39, { message: 'GitHub username cannot exceed 39 characters' })
225+
.regex(GITHUB_USERNAME_REGEX, {
226+
message: 'Invalid GitHub username',
227+
}),
222228
refresh: z.string().optional().transform(toRefreshFlag),
223229
});
224230

@@ -258,7 +264,13 @@ export const ogParamsSchema = z
258264
}));
259265

260266
export const statsParamsSchema = z.object({
261-
user: z.string({ error: 'Missing user parameter' }).min(1, { message: 'Missing user parameter' }),
267+
user: z
268+
.string({ error: 'Missing user parameter' })
269+
.min(1, { message: 'Missing user parameter' })
270+
.max(39, { message: 'GitHub username cannot exceed 39 characters' })
271+
.regex(GITHUB_USERNAME_REGEX, {
272+
message: 'Invalid GitHub username',
273+
}),
262274
refresh: z.string().optional().transform(toRefreshFlag),
263275
tz: z.string().optional(),
264276
});

0 commit comments

Comments
 (0)