Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions app/api/home/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// app/api/home/route.ts
import { NextResponse } from "next/server";

export const runtime = "edge";

export interface HomeAction {
key: string;
label: string;
url: string;
isVisible: boolean;
}

export interface GalleryItem {
imageUrl: string;
caption: string;
altText: string;
}

export interface HomeResponse {
success: boolean;
data: {
actions: HomeAction[];
hero: {
imageUrl: string;
caption: string;
altText: string;
};
gallery: GalleryItem[];
};
}

export async function GET() {
const apiUrl = process.env.API_BASE_URL;

if (!apiUrl) {
return NextResponse.json(
{ success: false, message: "API base URL is not defined", data: { actions: [] } },
{ status: 500 }
);
}

try {
const res = await fetch(`${apiUrl}/api/v1/site-content`, {
cache: "no-store",
signal: AbortSignal.timeout(30000), // 30 second timeout
});

if (!res.ok) {
console.error(`Upstream API returned status ${res.status}`);
return NextResponse.json(
{ success: false, message: `Upstream API error: ${res.status}`, data: { actions: [] } },
{ status: 502 }
);
}

const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch (err: unknown) {
if (err instanceof Error) {
console.error("Invalid JSON from upstream API:", text, err.message);
} else {
console.error("Invalid JSON from upstream API:", text);
}

return NextResponse.json(
{ success: false, message: "Upstream API returned invalid JSON", data: { actions: [] } },
{ status: 500 }
);
}

if (!data || !data.success || !data.data || !Array.isArray(data.data.actions)) {
console.error("Unexpected API response structure", data);
return NextResponse.json(
{ success: false, message: "Invalid response from upstream API", data: { actions: [] } },
{ status: 500 }
);
}
Comment on lines +73 to +79

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate individual action objects before passing through to the client.

The route checks that data.data.actions is an array but does not validate the shape of each element. If the upstream returns actions with missing or malformed fields (e.g., url as a non-string, isVisible as the string "false" instead of boolean false), the client will render broken UI or show buttons that should be hidden.

🛡️ Proposed fix: add per-action validation
     if (!data || !data.success || !data.data || !Array.isArray(data.data.actions)) {
       console.error("Unexpected API response structure", data);
       return NextResponse.json(
         { success: false, message: "Invalid response from upstream API", data: { actions: [] } },
         { status: 500 }
       );
     }

+    const validActions = data.data.actions.filter(
+      (a): a is HomeAction =>
+        a != null &&
+        typeof a === "object" &&
+        typeof a.key === "string" &&
+        typeof a.label === "string" &&
+        typeof a.url === "string" &&
+        typeof a.isVisible === "boolean"
+    );
+
+    if (validActions.length !== data.data.actions.length) {
+      console.error("Some actions failed validation");
+    }

-    return NextResponse.json({
+    return NextResponse.json({
       success: true,
-      data: data.data,
+      data: { ...data.data, actions: validActions },
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!data || !data.success || !data.data || !Array.isArray(data.data.actions)) {
console.error("Unexpected API response structure", data);
return NextResponse.json(
{ success: false, message: "Invalid response from upstream API", data: { actions: [] } },
{ status: 500 }
);
}
if (!data || !data.success || !data.data || !Array.isArray(data.data.actions)) {
console.error("Unexpected API response structure", data);
return NextResponse.json(
{ success: false, message: "Invalid response from upstream API", data: { actions: [] } },
{ status: 500 }
);
}
const validActions = data.data.actions.filter(
(a): a is HomeAction =>
a != null &&
typeof a === "object" &&
typeof a.key === "string" &&
typeof a.label === "string" &&
typeof a.url === "string" &&
typeof a.isVisible === "boolean"
);
if (validActions.length !== data.data.actions.length) {
console.error("Some actions failed validation");
}
return NextResponse.json({
success: true,
data: { ...data.data, actions: validActions },
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/home/route.ts` around lines 73 - 79, Extend the response validation
in the home route around the existing data.data.actions check to validate every
action object and its required fields, including enforcing string types such as
url and boolean types such as isVisible. Reject the entire upstream response
with the existing 500 Invalid response result when any action is malformed,
while preserving valid action arrays unchanged.


return NextResponse.json({
success: true,
data: data.data,
});
} catch (error: unknown) {
if (error instanceof Error) {
console.error("Error in /api/home route:", error.message);
} else {
console.error("Error in /api/home route:", error);
}

return NextResponse.json(
{ success: false, message: "Internal error", data: { actions: [] } },
{ status: 500 }
);
}
}
76 changes: 63 additions & 13 deletions components/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ import Ferris from "./ui/ferris-eyes";
import { Button } from 'pixel-retroui';
import localFont from "next/font/local";

interface HomeAction {
key: string;
label: string;
url: string;
isVisible: boolean;
}

interface GalleryItem {
imageUrl: string;
caption: string;
altText: string;
}

interface HomeResponse {
success: boolean;
data: {
actions: HomeAction[];
hero: {
imageUrl: string;
caption: string;
altText: string;
};
gallery: GalleryItem[];
};
}

const pressStart2P = localFont({
src: "../app/fonts/PressStart2P-Regular.ttf",
display: "swap",
Expand All @@ -18,7 +44,24 @@ const pressStart2P = localFont({
export default function HeroSection() {
const [color, setColor] = useState("#000000");
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [showButton ] = useState(false);
const [actions, setActions] = useState<HomeAction[]>([]);

useEffect(() => {
async function fetchHomeData() {
try {
const res = await fetch("/api/home");
if (res.ok) {
const json: HomeResponse = await res.json();
if (json.success && json.data && Array.isArray(json.data.actions)) {
setActions(json.data.actions);
}
}
} catch (err) {
console.error("Failed to fetch home page data:", err);
}
}
fetchHomeData();
}, []);

useEffect(() => {
const updateColor = () => {
Expand Down Expand Up @@ -68,7 +111,7 @@ export default function HeroSection() {
shadow-[6px_6px_0px_rgba(0,0,0,1)] dark:shadow-[6px_6px_0px_rgba(255,255,255,1)]
hover:shadow-[8px_8px_0px_rgba(0,0,0,1)] dark:hover:shadow-[8px_8px_0px_rgba(255,255,255,1)]
active:translate-x-0.5 active:translate-y-0.5
transition-all duration-200 mb-32
transition-all duration-200
`.replace(/\s+/g, ' ').trim();


Expand Down Expand Up @@ -183,17 +226,24 @@ export default function HeroSection() {
</motion.svg>


<Button
onClick={() => {
window.open("https://forms.gle/qME3Qh1Skj7JvsBr9", "_blank");
}}
className={buttonStyles}
aria-label="Register for Call of Code event"
style={{display : showButton ? "inline-block" : "none"}}
>
Register Now!
</Button>

{actions.filter(action => action.isVisible).length > 0 && (
<div className="flex flex-wrap justify-center items-center gap-6 mb-32 z-20">
{actions.filter(action => action.isVisible).map((action) => (
<a
key={action.key}
href={action.url}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate action.url protocol before rendering as href.

The URL from the API response is used directly in href without protocol validation. A compromised or buggy upstream could return javascript: or data: URLs that execute arbitrary code. While target="_blank" blocks javascript: in modern browsers, data: URLs remain exploitable.

🛡️ Proposed fix: validate URL protocol before rendering
           <a
             key={action.key}
-            href={action.url}
+            href={action.url.startsWith("http://") || action.url.startsWith("https://") ? action.url : "#"}
             target="_blank"
             rel="noopener noreferrer"
             className="inline-block"
           >

Alternatively, perform this validation in the API route when filtering actions.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
href={action.url}
href={action.url.startsWith("http://") || action.url.startsWith("https://") ? action.url : "#"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/Home.tsx` at line 234, Validate action.url before assigning it to
href in the action-rendering flow of Home, allowing only safe protocols such as
http: and https:. Reject or omit actions with javascript:, data:, or other
unsupported protocols, while preserving valid URL rendering; apply the same
validation at the API filtering boundary if that is the established
action-sanitization path.

target="_blank"
rel="noopener noreferrer"
className="inline-block"
>
<Button className={buttonStyles} aria-label={action.label}>
{action.label}
</Button>
</a>
))}
</div>
)}


<h2 className="bg-clip-text text-transparent text-center bg-gradient-to-r from-red-600 via-purple-600 to-blue-500 dark:from-red-600 dark:via-purple-600 dark:to-blue-500 text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-sans relative z-20 font-bold tracking-tight leading-tight mb-8 px-4">
&lt;&gt; CALL OF CODE &lt;/&gt;
Expand Down
Loading