-
Notifications
You must be signed in to change notification settings - Fork 204
fix(contributor-profile): add GitHub response validation #708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,31 +11,96 @@ type PR = { | |
| type Profile = { | ||
| avatar_url: string; | ||
| login: string; | ||
| bio: string; | ||
| bio: string | null; | ||
| }; | ||
|
|
||
| const isProfile = (data: unknown): data is Profile => { | ||
| return ( | ||
| typeof data === "object" && | ||
| data !== null && | ||
| typeof (data as any).avatar_url === "string" && | ||
| typeof (data as any).login === "string" && | ||
| (typeof (data as any).bio === "string" || (data as any).bio === null) | ||
| ); | ||
| }; | ||
|
Tanayajadhav1 marked this conversation as resolved.
|
||
|
|
||
| const isPrArray = (data: unknown): data is PR[] => { | ||
| return ( | ||
| Array.isArray(data) && | ||
| data.every( | ||
| (item) => | ||
| typeof item === "object" && | ||
| item !== null && | ||
| typeof (item as any).title === "string" && | ||
| typeof (item as any).html_url === "string" && | ||
| typeof (item as any).repository_url === "string" | ||
| ) | ||
| ); | ||
| }; | ||
|
|
||
| export default function ContributorProfile() { | ||
| const { username } = useParams(); | ||
| const [profile, setProfile] = useState<Profile | null>(null); | ||
| const [prs, setPRs] = useState<PR[]>([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| async function fetchData() { | ||
| if (!username) return; | ||
| if (!username) { | ||
| setError("No username provided."); | ||
| setLoading(false); | ||
| return; | ||
| } | ||
|
Tanayajadhav1 marked this conversation as resolved.
|
||
|
|
||
| setLoading(true); | ||
| setError(null); | ||
|
|
||
| try { | ||
| const userRes = await fetch(`https://api.github.com/users/${username}`); | ||
|
Tanayajadhav1 marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the file and print the relevant section around the cited line(s).
FILE="src/pages/ContributorProfile/ContributorProfile.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE"
exit 1
fi
echo "== File: $FILE =="
wc -l "$FILE"
sed -n '1,140p' "$FILE" | nl -ba | sed -n '40,100p'
# Also search for usages of `username` and GitHub URL construction within the file.
echo "== rg: username in ContributorProfile.tsx =="
rg -n "username" "$FILE"
echo "== rg: github.com in ContributorProfile.tsx =="
rg -n "github\.com" "$FILE"Repository: GitMetricsLab/github_tracker Length of output: 254 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="src/pages/ContributorProfile/ContributorProfile.tsx"
echo "== File: $FILE =="
wc -l "$FILE"
echo "== Lines 40-100 =="
awk 'NR>=40 && NR<=100 {printf "%d\t%s\n", NR, $0}' "$FILE"
echo "== rg: username in ContributorProfile.tsx =="
rg -n "username" "$FILE" || true
echo "== rg: github.com in ContributorProfile.tsx =="
rg -n "github\.com" "$FILE" || true
echo "== Print fetch/search URL constructions =="
rg -n "fetch\\(|search/issues" "$FILE" || trueRepository: GitMetricsLab/github_tracker Length of output: 3230 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="src/pages/ContributorProfile/ContributorProfile.tsx"
rg -n "encodeURIComponent" "$FILE" || trueRepository: GitMetricsLab/github_tracker Length of output: 54 Encode Suggested fix- const userRes = await fetch(`https://api.github.com/users/${username}`);
+ const encodedUsername = encodeURIComponent(username);
+ const userRes = await fetch(`https://api.github.com/users/${encodedUsername}`);
...
- `https://api.github.com/search/issues?q=author:${username}+type:pr`
+ `https://api.github.com/search/issues?q=author:${encodedUsername}+type:pr`🤖 Prompt for AI Agents |
||
| if (!userRes.ok) { | ||
| if (userRes.status === 404) { | ||
| throw new Error("User not found."); | ||
| } | ||
| if (userRes.status === 403) { | ||
| throw new Error("GitHub API rate limit exceeded. Please try again later."); | ||
| } | ||
| throw new Error(`GitHub user fetch failed with status ${userRes.status}.`); | ||
| } | ||
|
|
||
| const userData = await userRes.json(); | ||
| if (!isProfile(userData)) { | ||
| throw new Error("Invalid GitHub profile response."); | ||
| } | ||
| setProfile(userData); | ||
|
|
||
| const prsRes = await fetch( | ||
| `https://api.github.com/search/issues?q=author:${username}+type:pr` | ||
| ); | ||
| if (!prsRes.ok) { | ||
| if (prsRes.status === 403) { | ||
| throw new Error("GitHub API rate limit exceeded. Please try again later."); | ||
| } | ||
| throw new Error(`GitHub PR fetch failed with status ${prsRes.status}.`); | ||
| } | ||
|
|
||
| const prsData = await prsRes.json(); | ||
| setPRs(prsData.items); | ||
| } catch { | ||
| toast.error("Failed to fetch user data."); | ||
| if ( | ||
| typeof prsData !== "object" || | ||
| prsData === null || | ||
| !Array.isArray((prsData as any).items) || | ||
| !isPrArray((prsData as any).items) | ||
| ) { | ||
| throw new Error("Invalid GitHub PR response."); | ||
| } | ||
|
|
||
| setPRs((prsData as any).items); | ||
| } catch (err) { | ||
| const message = err instanceof Error ? err.message : "Failed to fetch user data."; | ||
| setError(message); | ||
| toast.error(message); | ||
| setProfile(null); | ||
| setPRs([]); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
|
|
@@ -51,6 +116,11 @@ export default function ContributorProfile() { | |
|
|
||
| if (loading) return <div className="text-center mt-10">Loading...</div>; | ||
|
|
||
| if (error) | ||
| return ( | ||
| <div className="text-center mt-10 text-red-600">{error}</div> | ||
| ); | ||
|
|
||
| if (!profile) | ||
| return ( | ||
| <div className="text-center mt-10 text-red-600">User not found.</div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: GitMetricsLab/github_tracker
Length of output: 100
🏁 Script executed:
Repository: GitMetricsLab/github_tracker
Length of output: 719
🏁 Script executed:
Repository: GitMetricsLab/github_tracker
Length of output: 3996
Remove
as anycasts in runtime guards/PR parsing to satisfyno-explicit-anyand keep validation type-safe.src/pages/ContributorProfile/ContributorProfile.tsxstill usesas anyat lines 21-23, 34-36, 91-92, and 97. Replace these casts withRecord<string, unknown>-based narrowing and a smallhasItemsguard.Suggested fix
🧰 Tools
🪛 ESLint
[error] 21-21: Unexpected any. Specify a different type.
(
@typescript-eslint/no-explicit-any)[error] 22-22: Unexpected any. Specify a different type.
(
@typescript-eslint/no-explicit-any)[error] 23-23: Unexpected any. Specify a different type.
(
@typescript-eslint/no-explicit-any)[error] 23-23: Unexpected any. Specify a different type.
(
@typescript-eslint/no-explicit-any)🤖 Prompt for AI Agents