Skip to content

Commit fa7da73

Browse files
authored
fix(github): validate username format before API call (JhaSourav07#426)
## Description Added GitHub username validation before making API calls. - Added validateGitHubUsername helper - Added early validation before API request - Throw clear error for invalid usernames - Added tests for valid username, too long username, underscore, and spaces. Fixes JhaSourav07#407 ## Pillar - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview N/A ## 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 (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started 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 "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
1 parent 10651a2 commit fa7da73

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

lib/github.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
generateAchievements,
99
clearGitHubApiCacheForTests,
1010
GITHUB_CACHE_TTL_MS,
11+
validateGitHubUsername,
1112
} from './github';
1213
import type { ContributionCalendar } from '../types';
1314

@@ -393,3 +394,33 @@ describe('generateAchievements', () => {
393394
expect(unlocked.some((a) => a.title === '100 Day Streak')).toBe(false);
394395
});
395396
});
397+
398+
describe('validateGitHubUsername', () => {
399+
it('returns true for a valid username', () => {
400+
expect(validateGitHubUsername('valid-username-123')).toBe(true);
401+
});
402+
403+
it('returns false for a too long username', () => {
404+
expect(validateGitHubUsername('a'.repeat(40))).toBe(false);
405+
});
406+
407+
it('returns false for a username with underscore', () => {
408+
expect(validateGitHubUsername('invalid_username')).toBe(false);
409+
});
410+
411+
it('returns false for a username with spaces', () => {
412+
expect(validateGitHubUsername('invalid username')).toBe(false);
413+
});
414+
415+
it('returns false for a leading hyphen', () => {
416+
expect(validateGitHubUsername('-invalid')).toBe(false);
417+
});
418+
419+
it('returns false for a trailing hyphen', () => {
420+
expect(validateGitHubUsername('invalid-')).toBe(false);
421+
});
422+
423+
it('returns false for consecutive hyphens', () => {
424+
expect(validateGitHubUsername('in--valid')).toBe(false);
425+
});
426+
});

lib/github.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,18 @@ const getHeaders = () => ({
118118
'Content-Type': 'application/json',
119119
});
120120

121+
export function validateGitHubUsername(username: string): boolean {
122+
return /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(username);
123+
}
124+
121125
export async function fetchGitHubContributions(
122126
username: string,
123127
options: FetchOptions = {}
124128
): Promise<ContributionCalendar> {
129+
if (!validateGitHubUsername(username)) {
130+
throw new Error('Invalid GitHub username format');
131+
}
132+
125133
const key = cacheKey('contributions', username, options.from?.substring(0, 4));
126134

127135
if (!options.bypassCache) {
@@ -186,6 +194,10 @@ export async function fetchUserProfile(
186194
username: string,
187195
options: FetchOptions = {}
188196
): Promise<GitHubUserProfile> {
197+
if (!validateGitHubUsername(username)) {
198+
throw new Error('Invalid GitHub username format');
199+
}
200+
189201
const key = cacheKey('profile', username);
190202

191203
if (!options.bypassCache) {
@@ -216,6 +228,10 @@ export async function fetchUserRepos(
216228
username: string,
217229
options: FetchOptions = {}
218230
): Promise<GitHubRepo[]> {
231+
if (!validateGitHubUsername(username)) {
232+
throw new Error('Invalid GitHub username format');
233+
}
234+
219235
const key = cacheKey('repos', username);
220236

221237
if (!options.bypassCache) {
@@ -289,6 +305,9 @@ export function generateAchievements(totalContributions: number, currentStreak:
289305
}
290306

291307
export async function getFullDashboardData(username: string, options: FetchOptions = {}) {
308+
if (!validateGitHubUsername(username)) {
309+
throw new Error('Invalid GitHub username format');
310+
}
292311
try {
293312
const [profileData, reposData, calendarData] = await Promise.all([
294313
fetchUserProfile(username, options),

0 commit comments

Comments
 (0)