Skip to content

Commit a9effc4

Browse files
authored
Merge pull request #95 from BeyteFlow/copilot/fix-multi-language-readme-support
fix: Validate and allowlist language input to prevent prompt injection in multi-language README generation
2 parents 820c80e + 8f1a8c9 commit a9effc4

4 files changed

Lines changed: 87 additions & 25 deletions

File tree

src/app/api/generate/route.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
import { NextResponse } from "next/server";
22
import { getGeminiModel } from "@/lib/gemini";
33
import { getRepoData, getRepoContents } from "@/lib/octokit";
4+
import { SUPPORTED_LANGUAGES } from "@/constants/languages";
45

56
export const dynamic = "force-dynamic";
67

78
/**
89
* AI README Generation Endpoint
9-
* Optimized for data accuracy and clean prompt interpolation.
10+
* Optimized for data accuracy, clean prompt interpolation, and multi-language support.
11+
*
12+
* @param {Request} req - The incoming request object containing the repo URL and optional language.
13+
* @returns {Promise<NextResponse>} A JSON response containing the generated Markdown or an error message.
1014
*/
1115
export async function POST(req: Request) {
1216
let rawUrl: string;
17+
let language: string;
1318
try {
1419
const body = await req.json();
1520
rawUrl = body.url;
21+
const rawLanguage =
22+
typeof body.language === "string" ? body.language.trim() : "";
23+
const normalized =
24+
rawLanguage.charAt(0).toUpperCase() + rawLanguage.slice(1).toLowerCase();
25+
language = (SUPPORTED_LANGUAGES as readonly string[]).includes(normalized)
26+
? normalized
27+
: "English";
1628
} catch {
1729
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
1830
}
@@ -96,7 +108,7 @@ export async function POST(req: Request) {
96108
// Fix: Prompt updated with neutral fallbacks and dynamic license
97109
const prompt = `
98110
**Role**: You are a Principal Solutions Architect and World-Class Technical Writer.
99-
**Task**: Generate a professional, high-conversion README.md for the GitHub repository: "${repo}".
111+
**Task**: Generate a professional, high-conversion README.md for the GitHub repository: "${repo}" in the following language: **${language}**.
100112
101113
---
102114
### 1. PROJECT CONTEXT (VERIFIED DATA)

src/app/generate/GeneratePageClient.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,17 @@ export default function GeneratePageClient({ repoSlug }: GeneratePageProps) {
2626
}
2727
}, [repoSlug]);
2828

29-
const handleGenerate = async (githubUrl: string) => {
29+
const handleGenerate = async (
30+
githubUrl: string,
31+
language: string = "English",
32+
) => {
3033
setIsLoading(true);
3134
setMarkdown("");
3235
try {
3336
const response = await fetch("/api/generate", {
3437
method: "POST",
3538
headers: { "Content-Type": "application/json" },
36-
body: JSON.stringify({ url: githubUrl }),
39+
body: JSON.stringify({ url: githubUrl, language }),
3740
});
3841

3942
if (!response.ok) {

src/components/Generator/SearchInput.tsx

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@
22
import React, { useState } from "react";
33
import { Loader2, Github, AlertCircle } from "lucide-react";
44
import { Button } from "../ui/Button";
5+
import { SUPPORTED_LANGUAGES } from "@/constants/languages";
56

67
interface SearchInputProps {
7-
onGenerate: (url: string) => void;
8+
onGenerate: (url: string, language: string) => void;
89
isLoading: boolean;
910
initialValue?: string; // optional initial value
1011
ariaLabel?: string; // optional aria-label for accessibility
1112
}
1213

14+
/**
15+
* SearchInput Component
16+
* Renders a responsive form for GitHub URL input and language selection.
17+
*
18+
* @param {SearchInputProps} props - The component props.
19+
* @returns {JSX.Element} The rendered search input form.
20+
*/
1321
export const SearchInput = ({
1422
onGenerate,
1523
isLoading,
@@ -18,6 +26,7 @@ export const SearchInput = ({
1826
}: SearchInputProps) => {
1927
// Initialize state directly from initialValue once
2028
const [url, setUrl] = useState(initialValue || "");
29+
const [language, setLanguage] = useState("English");
2130
const [error, setError] = useState<string | null>(null);
2231

2332
const handleSubmit = (e: React.FormEvent) => {
@@ -28,36 +37,59 @@ export const SearchInput = ({
2837
/^https?:\/\/(www\.)?github\.com\/[\w.-]+\/[\w.-]+\/?$/;
2938

3039
if (githubUrlPattern.test(url.trim())) {
31-
onGenerate(url.trim());
40+
onGenerate(url.trim(), language);
3241
} else {
3342
setError("Please enter a valid GitHub repository URL.");
3443
}
3544
};
3645

3746
return (
38-
<div className="w-full max-w-4xl mx-auto">
39-
<form onSubmit={handleSubmit} className="relative group">
40-
<div className="absolute inset-y-0 left-5 flex items-center pointer-events-none text-gray-500 group-focus-within:text-blue-500 transition-colors">
41-
<Github size={20} />
47+
<div className="w-full max-w-4xl mx-auto space-y-4">
48+
<form
49+
onSubmit={handleSubmit}
50+
className="relative group flex flex-col md:flex-row gap-4"
51+
>
52+
<div className="relative flex-grow">
53+
<div className="absolute inset-y-0 left-5 flex items-center pointer-events-none text-gray-500 group-focus-within:text-blue-500 transition-colors">
54+
<Github size={20} />
55+
</div>
56+
<input
57+
type="text"
58+
value={url}
59+
onChange={(e) => {
60+
setUrl(e.target.value);
61+
if (error) setError(null);
62+
}}
63+
placeholder="https://github.com/username/repo"
64+
aria-label={ariaLabel}
65+
className={`w-full bg-zinc-900/50 border ${
66+
error ? "border-red-500/50" : "border-white/10"
67+
} rounded-2xl py-6 pl-14 pr-4 text-white placeholder:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all backdrop-blur-xl`}
68+
/>
4269
</div>
43-
<input
44-
type="text"
45-
value={url}
46-
onChange={(e) => {
47-
setUrl(e.target.value);
48-
if (error) setError(null);
49-
}}
50-
placeholder="https://github.com/username/repo"
51-
aria-label={ariaLabel}
52-
className={`w-full bg-zinc-900/50 border ${
53-
error ? "border-red-500/50" : "border-white/10"
54-
} rounded-2xl py-6 pl-14 pr-40 text-white placeholder:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all backdrop-blur-xl`}
55-
/>
56-
<div className="absolute inset-y-2 right-2 flex items-center">
70+
71+
<div className="flex gap-4">
72+
<select
73+
value={language}
74+
onChange={(e) => setLanguage(e.target.value)}
75+
aria-label="Select language for README generation"
76+
className="bg-zinc-900/50 border border-white/10 rounded-2xl px-6 py-6 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all backdrop-blur-xl appearance-none cursor-pointer min-w-[140px]"
77+
>
78+
{SUPPORTED_LANGUAGES.map((lang) => (
79+
<option
80+
key={lang}
81+
value={lang}
82+
className="bg-zinc-900 text-white"
83+
>
84+
{lang}
85+
</option>
86+
))}
87+
</select>
88+
5789
<Button
5890
type="submit"
5991
disabled={isLoading || !url}
60-
className="h-full px-8 shadow-lg shadow-blue-500/20"
92+
className="h-full px-8 shadow-lg shadow-blue-500/20 whitespace-nowrap"
6193
>
6294
{isLoading ? (
6395
<Loader2 className="animate-spin" size={18} />

src/constants/languages.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const SUPPORTED_LANGUAGES = [
2+
"English",
3+
"Spanish",
4+
"French",
5+
"German",
6+
"Chinese",
7+
"Japanese",
8+
"Korean",
9+
"Portuguese",
10+
"Russian",
11+
"Arabic",
12+
"Turkish",
13+
] as const;
14+
15+
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];

0 commit comments

Comments
 (0)