From 66d8212f7f407ecfa659690d3edb6cdaecd6f76d Mon Sep 17 00:00:00 2001 From: chavanGaneshDatta Date: Mon, 1 Jun 2026 06:53:32 +0530 Subject: [PATCH 1/2] feat: add client-side GitHub username validation --- app/customize/page.tsx | 43 +++++++++++++++++++++++++++++------------- lib/github.test.ts | 16 ++++++++++++++++ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/app/customize/page.tsx b/app/customize/page.tsx index d65dfceb..d8210bad 100644 --- a/app/customize/page.tsx +++ b/app/customize/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useCallback, useEffect, useRef, useState, type ReactElement } from 'react'; +import { validateGitHubUsername } from '@/lib/github'; import Link from 'next/link'; import { motion } from 'framer-motion'; import { ControlsPanel } from './components/ControlsPanel'; @@ -121,6 +122,12 @@ export default function CustomizePage(): ReactElement { setSvgState('idle'); return; } + if (!validateGitHubUsername(trimmedUsername)) { + setSvgContent(''); + setSvgState('error'); + setErrorMessage("That doesn't look like a valid GitHub username"); + return; + } setSvgState('loading'); const controller = new AbortController(); @@ -192,7 +199,7 @@ export default function CustomizePage(): ReactElement { }); return () => controller.abort(); - }, [previewSrc, hasUsername]); + }, [previewSrc, hasUsername, trimmedUsername]); const exportSnippet = getExportSnippet(exportFormat, queryString); @@ -395,6 +402,14 @@ export default function CustomizePage(): ReactElement { Loading preview... )} + {svgState === 'error' && + errorMessage === "That doesn't look like a valid GitHub username" && ( +
+

+ {errorMessage} +

+
+ )} {svgState === 'error' && errorMessage === 'GitHub user not found' && (
@@ -422,16 +437,18 @@ export default function CustomizePage(): ReactElement {
)} - {svgState === 'error' && errorMessage !== 'GitHub user not found' && ( -
-

- Failed to load badge -

-

- The API may be unavailable. Please try again. -

-
- )} + {svgState === 'error' && + errorMessage !== 'GitHub user not found' && + errorMessage !== "That doesn't look like a valid GitHub username" && ( +
+

+ Failed to load badge +

+

+ The API may be unavailable. Please try again. +

+
+ )} {svgState === 'loaded' && svgContent && ( ) : ( -
-
+
+
{ it('returns false for a username with underscore', () => { expect(validateGitHubUsername('invalid_username')).toBe(false); }); + + it('returns false for empty string', () => { + expect(validateGitHubUsername('')).toBe(false); + }); + + it('returns false for leading hyphen', () => { + expect(validateGitHubUsername('-octocat')).toBe(false); + }); + + it('returns false for trailing hyphen', () => { + expect(validateGitHubUsername('octocat-')).toBe(false); + }); + + it('returns false for consecutive hyphens', () => { + expect(validateGitHubUsername('octo--cat')).toBe(false); + }); }); describe('cacheKey', () => { it('creates key without year', () => { From 6554b8aa739f15e42783fbd54cc4dbdaa1d12874 Mon Sep 17 00:00:00 2001 From: chavanGaneshDatta Date: Mon, 1 Jun 2026 07:44:37 +0530 Subject: [PATCH 2/2] feat: add client-side GitHub username validation --- app/customize/page.tsx | 2 +- lib/github.test.ts | 30 ------------------------------ lib/github.ts | 4 ---- lib/validations.test.ts | 37 ++++++++++++++++++++++++++++++++++++- lib/validations.ts | 4 ++++ 5 files changed, 41 insertions(+), 36 deletions(-) diff --git a/app/customize/page.tsx b/app/customize/page.tsx index d8210bad..0c90f266 100644 --- a/app/customize/page.tsx +++ b/app/customize/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useCallback, useEffect, useRef, useState, type ReactElement } from 'react'; -import { validateGitHubUsername } from '@/lib/github'; +import { validateGitHubUsername } from '@/lib/validations'; import Link from 'next/link'; import { motion } from 'framer-motion'; import { ControlsPanel } from './components/ControlsPanel'; diff --git a/lib/github.test.ts b/lib/github.test.ts index a83f6606..4cf0984e 100644 --- a/lib/github.test.ts +++ b/lib/github.test.ts @@ -10,7 +10,6 @@ import { buildCommitClock, clearGitHubApiCacheForTests, GITHUB_CACHE_TTL_MS, - validateGitHubUsername, cacheKey, displayName, fetchOrgMembers, @@ -1690,35 +1689,6 @@ describe('displayName', () => { }); }); -describe('validateGitHubUsername', () => { - it('returns true for a valid username', () => { - expect(validateGitHubUsername('valid-username-123')).toBe(true); - }); - - it('returns false for a too long username', () => { - expect(validateGitHubUsername('a'.repeat(40))).toBe(false); - }); - - it('returns false for a username with underscore', () => { - expect(validateGitHubUsername('invalid_username')).toBe(false); - }); - - it('returns false for empty string', () => { - expect(validateGitHubUsername('')).toBe(false); - }); - - it('returns false for leading hyphen', () => { - expect(validateGitHubUsername('-octocat')).toBe(false); - }); - - it('returns false for trailing hyphen', () => { - expect(validateGitHubUsername('octocat-')).toBe(false); - }); - - it('returns false for consecutive hyphens', () => { - expect(validateGitHubUsername('octo--cat')).toBe(false); - }); -}); describe('cacheKey', () => { it('creates key without year', () => { expect(cacheKey('profile', 'DeepSikha')).toBe('profile:deepsikha'); diff --git a/lib/github.ts b/lib/github.ts index e73370fc..bc92aa63 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -315,10 +315,6 @@ const getHeaders = () => ({ 'Content-Type': 'application/json', }); -export function validateGitHubUsername(username: string): boolean { - return /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(username); -} - export function displayName(profile: GitHubUserProfile): string { if (typeof profile.name === 'string' && profile.name.trim() !== '') return profile.name; return profile.login; diff --git a/lib/validations.test.ts b/lib/validations.test.ts index 9b8f46f7..717ab244 100644 --- a/lib/validations.test.ts +++ b/lib/validations.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { githubParamsSchema, ogParamsSchema, streakParamsSchema } from './validations'; +import { + githubParamsSchema, + ogParamsSchema, + streakParamsSchema, + validateGitHubUsername, +} from './validations'; describe('streakParamsSchema — grace fallback behavior', () => { it('accepts "0" as a valid grace value', () => { @@ -40,6 +45,36 @@ describe('streakParamsSchema — grace fallback behavior', () => { }); }); +describe('validateGitHubUsername', () => { + it('returns true for a valid username', () => { + expect(validateGitHubUsername('valid-username-123')).toBe(true); + }); + + it('returns false for a too long username', () => { + expect(validateGitHubUsername('a'.repeat(40))).toBe(false); + }); + + it('returns false for a username with underscore', () => { + expect(validateGitHubUsername('invalid_username')).toBe(false); + }); + + it('returns false for empty string', () => { + expect(validateGitHubUsername('')).toBe(false); + }); + + it('returns false for leading hyphen', () => { + expect(validateGitHubUsername('-octocat')).toBe(false); + }); + + it('returns false for trailing hyphen', () => { + expect(validateGitHubUsername('octocat-')).toBe(false); + }); + + it('returns false for consecutive hyphens', () => { + expect(validateGitHubUsername('octo--cat')).toBe(false); + }); +}); + describe('githubParamsSchema', () => { it('should pass when username is valid', () => { const result = githubParamsSchema.safeParse({ diff --git a/lib/validations.ts b/lib/validations.ts index faa64cb7..99343b28 100644 --- a/lib/validations.ts +++ b/lib/validations.ts @@ -47,6 +47,10 @@ export function toDimensionValue(val?: string): number | undefined { return val === undefined ? undefined : Number(val); } +export function validateGitHubUsername(username: string): boolean { + return /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(username); +} + function dimensionParam(name: string, min: number, max: number) { return z .string()