Skip to content

Commit db3161a

Browse files
authored
feat: add client-side GitHub username validation (JhaSourav07#2209)
## Description Fixes JhaSourav07#2158 Adds client-side GitHub username validation before triggering badge preview requests. ### Changes - Uses `validateGitHubUsername()` before making API requests. - Prevents requests for invalid GitHub usernames. - Displays a validation error message for invalid input. - Adds test cases for: - Empty username - Leading hyphen - Trailing hyphen - Consecutive hyphens ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview N/A (client-side validation change) ## 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): ...`). - [ ] 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 "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.
2 parents 088544f + 6554b8a commit db3161a

5 files changed

Lines changed: 70 additions & 32 deletions

File tree

app/customize/page.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { useCallback, useEffect, useRef, useState, type ReactElement } from 'react';
4+
import { validateGitHubUsername } from '@/lib/validations';
45
import Link from 'next/link';
56
import { motion } from 'framer-motion';
67
import { ControlsPanel } from './components/ControlsPanel';
@@ -121,6 +122,12 @@ export default function CustomizePage(): ReactElement {
121122
setSvgState('idle');
122123
return;
123124
}
125+
if (!validateGitHubUsername(trimmedUsername)) {
126+
setSvgContent('');
127+
setSvgState('error');
128+
setErrorMessage("That doesn't look like a valid GitHub username");
129+
return;
130+
}
124131

125132
setSvgState('loading');
126133
const controller = new AbortController();
@@ -192,7 +199,7 @@ export default function CustomizePage(): ReactElement {
192199
});
193200

194201
return () => controller.abort();
195-
}, [previewSrc, hasUsername]);
202+
}, [previewSrc, hasUsername, trimmedUsername]);
196203

197204
const exportSnippet = getExportSnippet(exportFormat, queryString);
198205

@@ -395,6 +402,14 @@ export default function CustomizePage(): ReactElement {
395402
Loading preview...
396403
</div>
397404
)}
405+
{svgState === 'error' &&
406+
errorMessage === "That doesn't look like a valid GitHub username" && (
407+
<div className="flex flex-col items-center justify-center gap-2 text-center py-8">
408+
<p className="text-sm font-semibold text-red-500 dark:text-red-400">
409+
{errorMessage}
410+
</p>
411+
</div>
412+
)}
398413
{svgState === 'error' && errorMessage === 'GitHub user not found' && (
399414
<div className="flex flex-col items-center justify-center gap-4 py-12 text-center">
400415
<div className="flex h-16 w-16 items-center justify-center rounded-3xl border border-red-500/20 bg-red-500/10 shadow-inner">
@@ -422,16 +437,18 @@ export default function CustomizePage(): ReactElement {
422437
</div>
423438
</div>
424439
)}
425-
{svgState === 'error' && errorMessage !== 'GitHub user not found' && (
426-
<div className="flex flex-col items-center justify-center gap-2 text-center py-8">
427-
<p className="text-sm font-semibold text-red-500 dark:text-red-400">
428-
Failed to load badge
429-
</p>
430-
<p className="text-xs text-gray-500 dark:text-white/45">
431-
The API may be unavailable. Please try again.
432-
</p>
433-
</div>
434-
)}
440+
{svgState === 'error' &&
441+
errorMessage !== 'GitHub user not found' &&
442+
errorMessage !== "That doesn't look like a valid GitHub username" && (
443+
<div className="flex flex-col items-center justify-center gap-2 text-center py-8">
444+
<p className="text-sm font-semibold text-red-500 dark:text-red-400">
445+
Failed to load badge
446+
</p>
447+
<p className="text-xs text-gray-500 dark:text-white/45">
448+
The API may be unavailable. Please try again.
449+
</p>
450+
</div>
451+
)}
435452
{svgState === 'loaded' && svgContent && (
436453
<motion.div
437454
initial={{ opacity: 0, scale: 0.95 }}
@@ -446,8 +463,8 @@ export default function CustomizePage(): ReactElement {
446463
)}
447464
</div>
448465
) : (
449-
<div className="relative z-10 flex w-full max-w-xl flex-col items-center justify-center rounded-[1.25rem] border border-dashed border-black/10 bg-gray-100/80 backdrop-blur-md dark:border-white/10 dark:bg-white/[0.03] px-6 py-12 text-center">
450-
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-black/10 bg-gray-100/80 dark:border-white/10 dark:bg-white/[0.04] text-gray-500 dark:text-emerald-300/70">
466+
<div className="relative z-10 flex w-full max-w-xl flex-col items-center justify-center rounded-[1.25rem] border border-dashed border-black/10 bg-gray-100/80 backdrop-blur-md dark:border-white/10 dark:bg-white/3 px-6 py-12 text-center">
467+
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-black/10 bg-gray-100/80 dark:border-white/10 dark:bg-white/4 text-gray-500 dark:text-emerald-300/70">
451468
<svg
452469
xmlns="http://www.w3.org/2000/svg"
453470
className="h-6 w-6"

lib/github.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
buildCommitClock,
1111
clearGitHubApiCacheForTests,
1212
GITHUB_CACHE_TTL_MS,
13-
validateGitHubUsername,
1413
cacheKey,
1514
displayName,
1615
fetchOrgMembers,
@@ -1690,19 +1689,6 @@ describe('displayName', () => {
16901689
});
16911690
});
16921691

1693-
describe('validateGitHubUsername', () => {
1694-
it('returns true for a valid username', () => {
1695-
expect(validateGitHubUsername('valid-username-123')).toBe(true);
1696-
});
1697-
1698-
it('returns false for a too long username', () => {
1699-
expect(validateGitHubUsername('a'.repeat(40))).toBe(false);
1700-
});
1701-
1702-
it('returns false for a username with underscore', () => {
1703-
expect(validateGitHubUsername('invalid_username')).toBe(false);
1704-
});
1705-
});
17061692
describe('cacheKey', () => {
17071693
it('creates key without year', () => {
17081694
expect(cacheKey('profile', 'DeepSikha')).toBe('profile:deepsikha');

lib/github.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,6 @@ const getHeaders = () => ({
315315
'Content-Type': 'application/json',
316316
});
317317

318-
export function validateGitHubUsername(username: string): boolean {
319-
return /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(username);
320-
}
321-
322318
export function displayName(profile: GitHubUserProfile): string {
323319
if (typeof profile.name === 'string' && profile.name.trim() !== '') return profile.name;
324320
return profile.login;

lib/validations.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { describe, expect, it } from 'vitest';
2-
import { githubParamsSchema, ogParamsSchema, streakParamsSchema } from './validations';
2+
import {
3+
githubParamsSchema,
4+
ogParamsSchema,
5+
streakParamsSchema,
6+
validateGitHubUsername,
7+
} from './validations';
38

49
describe('streakParamsSchema — grace fallback behavior', () => {
510
it('accepts "0" as a valid grace value', () => {
@@ -40,6 +45,36 @@ describe('streakParamsSchema — grace fallback behavior', () => {
4045
});
4146
});
4247

48+
describe('validateGitHubUsername', () => {
49+
it('returns true for a valid username', () => {
50+
expect(validateGitHubUsername('valid-username-123')).toBe(true);
51+
});
52+
53+
it('returns false for a too long username', () => {
54+
expect(validateGitHubUsername('a'.repeat(40))).toBe(false);
55+
});
56+
57+
it('returns false for a username with underscore', () => {
58+
expect(validateGitHubUsername('invalid_username')).toBe(false);
59+
});
60+
61+
it('returns false for empty string', () => {
62+
expect(validateGitHubUsername('')).toBe(false);
63+
});
64+
65+
it('returns false for leading hyphen', () => {
66+
expect(validateGitHubUsername('-octocat')).toBe(false);
67+
});
68+
69+
it('returns false for trailing hyphen', () => {
70+
expect(validateGitHubUsername('octocat-')).toBe(false);
71+
});
72+
73+
it('returns false for consecutive hyphens', () => {
74+
expect(validateGitHubUsername('octo--cat')).toBe(false);
75+
});
76+
});
77+
4378
describe('githubParamsSchema', () => {
4479
it('should pass when username is valid', () => {
4580
const result = githubParamsSchema.safeParse({

lib/validations.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export function toDimensionValue(val?: string): number | undefined {
4747
return val === undefined ? undefined : Number(val);
4848
}
4949

50+
export function validateGitHubUsername(username: string): boolean {
51+
return /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(username);
52+
}
53+
5054
function dimensionParam(name: string, min: number, max: number) {
5155
return z
5256
.string()

0 commit comments

Comments
 (0)