Skip to content
Closed
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
13 changes: 11 additions & 2 deletions frontend/app/[locale]/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getTranslations } from "next-intl/server"
import { getPlatformStats } from "@/lib/about/stats"
import { getSponsors } from "@/lib/about/github-sponsors"

Expand All @@ -7,6 +8,14 @@ import { FeaturesSection } from "@/components/about/FeaturesSection"
import { PricingSection } from "@/components/about/PricingSection"
import { CommunitySection } from "@/components/about/CommunitySection"

export async function generateMetadata() {
const t = await getTranslations("about")
return {
title: t("metaTitle"),
description: t("metaDescription"),
}
}
Comment on lines +11 to +17
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.

⚠️ Potential issue | 🟠 Major

Missing locale parameter in generateMetadata.

Other pages in this codebase (e.g., q&a/page.tsx, blog/page.tsx, dashboard/page.tsx) explicitly extract the locale from params and pass it to getTranslations. Without this, the metadata may not be generated in the correct locale.

Proposed fix to match existing patterns
-export async function generateMetadata() {
-  const t = await getTranslations("about")
+export async function generateMetadata({
+  params,
+}: {
+  params: Promise<{ locale: string }>;
+}) {
+  const { locale } = await params;
+  const t = await getTranslations({ locale, namespace: "about" });
   return {
     title: t("metaTitle"),
     description: t("metaDescription"),
   }
 }
🤖 Prompt for AI Agents
In `@frontend/app/`[locale]/about/page.tsx around lines 11 - 17, The
generateMetadata function currently calls getTranslations() with no locale;
update its signature to accept params and pass the locale to getTranslations
(e.g., change to async function generateMetadata({ params }) and call
getTranslations(params.locale, "about") or equivalent), keeping the returned
object with title: t("metaTitle") and description: t("metaDescription"); this
mirrors other pages (q&a/page.tsx, blog/page.tsx, dashboard/page.tsx) and
ensures metadata is generated in the correct locale.


export default async function AboutPage() {
const [stats, sponsors] = await Promise.all([
getPlatformStats(),
Expand All @@ -17,13 +26,13 @@ export default async function AboutPage() {
<main className="min-h-screen bg-gray-50 dark:bg-black overflow-hidden text-gray-900 dark:text-white
w-[100vw] relative left-[50%] right-[50%] -ml-[50vw] -mr-[50vw]"
>

<HeroSection stats={stats} />
<TopicsSection />
<FeaturesSection />
<PricingSection sponsors={sponsors} />
<CommunitySection />

</main>
)
}
176 changes: 147 additions & 29 deletions frontend/app/api/ai/explain/route.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,69 @@
export const runtime = 'nodejs';
export const maxDuration = 25;

import { NextRequest, NextResponse } from 'next/server';
import Groq from 'groq-sdk';
import { z } from 'zod';
import {
createExplainPrompt,
type ExplanationResponse,
} from '@/lib/ai/prompts';
import { getClientIp } from '@/lib/security/client-ip';

// =============================================================================
// SERVER-SIDE LOGGING (sanitized - no sensitive data exposed)
// =============================================================================
function logEnvironmentDiagnostics() {
const apiKey = process.env.GROQ_API_KEY;
console.log('[ENV] GROQ_API_KEY configured:', !!apiKey);
console.log('[ENV] GROQ_API_KEY length:', apiKey ? apiKey.length : 0);
console.log('[ENV] NODE_ENV:', process.env.NODE_ENV);
console.log('[ENV] NETLIFY:', process.env.NETLIFY ?? 'false');
console.log('[ENV] CONTEXT:', process.env.CONTEXT ?? 'unknown');
}

function logRequestDiagnostics(request: NextRequest) {
console.log('[REQ] Method:', request.method);
console.log('[REQ] URL path:', new URL(request.url).pathname);
}

function logBodyParsingResult(success: boolean, error?: unknown) {
console.log('[BODY] Parse success:', success);
if (error) {
console.log('[BODY] Parse error:', error instanceof Error ? error.message : 'Unknown error');
}
}

function logGroqInitialization(success: boolean, error?: unknown) {
console.log('[GROQ] Init success:', success);
if (error) {
const err = error as Error & { status?: number; code?: string };
console.log('[GROQ] Init error:', err.name, err.message);
}
}

function logGroqApiCall(phase: 'start' | 'success' | 'error', details?: unknown) {
if (phase === 'start') {
console.log('[GROQ] Starting API call');
} else if (phase === 'success') {
console.log('[GROQ] API call successful');
} else if (phase === 'error') {
const err = details as Error & { status?: number; code?: string };
console.log('[GROQ] API error:', err?.name, err?.message);
}
}
// =============================================================================

// =============================================================================
// RATE LIMITER (In-memory - limited effectiveness in serverless)
// Note: This Map only persists within a single warm function instance.
// For production, consider Upstash Redis or Netlify Blobs for true rate limiting.
// Current behavior: works during warm instance, resets on cold start.
// =============================================================================
const rateLimiter = new Map<string, { count: number; resetAt: number }>();
const MAX_REQUESTS_PER_WINDOW = 10;
const RATE_LIMIT_WINDOW_MS = 20 * 60 * 1000;
const RATE_LIMIT_WINDOW_MS = 20 * 60 * 1000;

const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
let lastCleanup = Date.now();

function cleanupRateLimiter() {
Expand All @@ -40,27 +90,33 @@ const requestSchema = z.object({
});


function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number } {
function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } {
// Bypass rate limiting for unknown IPs (serverless safety)
if (ip === 'unknown') {
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW, resetIn: RATE_LIMIT_WINDOW_MS, skipped: true };
}
Comment on lines +93 to +97
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.

⚠️ Potential issue | 🟡 Minor

Rate limit bypass for unknown IPs may be exploitable.

Bypassing rate limiting when ip === 'unknown' could allow abuse if getClientIp fails to extract IPs in certain environments (e.g., misconfigured proxies). Consider logging when rate limiting is skipped to monitor for abuse, or applying a stricter fallback.

Suggested improvement
 function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } {
   // Bypass rate limiting for unknown IPs (serverless safety)
   if (ip === 'unknown') {
+    console.warn('[RATE_LIMIT] Skipped - unknown IP');
     return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW, resetIn: RATE_LIMIT_WINDOW_MS, skipped: true };
   }
📝 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
function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } {
// Bypass rate limiting for unknown IPs (serverless safety)
if (ip === 'unknown') {
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW, resetIn: RATE_LIMIT_WINDOW_MS, skipped: true };
}
function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } {
// Bypass rate limiting for unknown IPs (serverless safety)
if (ip === 'unknown') {
console.warn('[RATE_LIMIT] Skipped - unknown IP');
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW, resetIn: RATE_LIMIT_WINDOW_MS, skipped: true };
}
🤖 Prompt for AI Agents
In `@frontend/app/api/ai/explain/route.ts` around lines 93 - 97, The
checkRateLimit function currently bypasses limits for ip === 'unknown', which is
exploitable; update checkRateLimit to not silently allow unlimited requests:
when ip === 'unknown' either apply a conservative fallback rate (e.g., much
lower MAX_REQUESTS_PER_WINDOW) or still enforce the normal limits but keyed to a
special "unknown" bucket, and emit a monitored log/metric indicating the
fallback was used (use the existing logger/processLogger or telemetry). Also
update callers (e.g., getClientIp) to surface when IP extraction failed so the
log contains context, and ensure the returned object sets skipped to true only
when a safe monitoring path is enabled.


cleanupRateLimiter();

const now = Date.now();
const entry = rateLimiter.get(ip);

if (!entry || now > entry.resetAt) {
rateLimiter.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW - 1, resetIn: RATE_LIMIT_WINDOW_MS };
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW - 1, resetIn: RATE_LIMIT_WINDOW_MS, skipped: false };
}

if (entry.count >= MAX_REQUESTS_PER_WINDOW) {
const resetIn = entry.resetAt - now;
return { allowed: false, remaining: 0, resetIn };
return { allowed: false, remaining: 0, resetIn, skipped: false };
}

entry.count++;
return {
allowed: true,
remaining: MAX_REQUESTS_PER_WINDOW - entry.count,
resetIn: entry.resetAt - now,
skipped: false,
};
}

Expand Down Expand Up @@ -91,14 +147,17 @@ function parseExplanationResponse(content: string): ExplanationResponse {
}

export async function POST(request: NextRequest) {
logEnvironmentDiagnostics();
logRequestDiagnostics(request);

const apiKey = process.env.GROQ_API_KEY;
if (!apiKey) {
console.error('GROQ_API_KEY is not configured');
console.error('Available env vars starting with GROQ:',
Object.keys(process.env).filter(k => k.startsWith('GROQ'))
);
console.error('[FATAL] GROQ_API_KEY is not configured. Check environment variables.');
return NextResponse.json(
{ error: 'AI service not configured', code: 'SERVICE_UNAVAILABLE' },
{
error: 'AI service not configured',
code: 'SERVICE_UNAVAILABLE',
},
{ status: 503 }
);
}
Expand All @@ -125,10 +184,21 @@ export async function POST(request: NextRequest) {
);
}

// Safe JSON body parsing for Netlify
let body: unknown;
try {
body = await request.json();
} catch {
const text = await request.text();
if (!text || text.trim() === '') {
console.log('[BODY] Empty request body received');
return NextResponse.json(
{ error: 'Request body is empty', code: 'EMPTY_BODY' },
{ status: 400 }
);
}
body = JSON.parse(text);
logBodyParsingResult(true);
} catch (parseError) {
logBodyParsingResult(false, parseError);
return NextResponse.json(
{ error: 'Invalid JSON body', code: 'INVALID_JSON' },
{ status: 400 }
Expand All @@ -149,11 +219,46 @@ export async function POST(request: NextRequest) {

const { term, context } = validationResult.data;

const groq = new Groq({ apiKey });
// Dynamic import for Netlify compatibility
let Groq: typeof import('groq-sdk').default;
try {
const groqModule = await import('groq-sdk');
Groq = groqModule.default;
} catch (importError) {
console.error('[SDK_IMPORT_ERROR] Failed to import groq-sdk:',
importError instanceof Error ? importError.message : String(importError)
);
return NextResponse.json(
{
error: 'Failed to load AI client',
code: 'SDK_IMPORT_ERROR',
},
{ status: 503 }
);
}

let groq: InstanceType<typeof Groq>;
try {
groq = new Groq({ apiKey });
logGroqInitialization(true);
} catch (initError) {
logGroqInitialization(false, initError);
console.error('[SDK_INIT_ERROR] Failed to initialize Groq client:',
initError instanceof Error ? initError.message : String(initError)
);
return NextResponse.json(
{
error: 'Failed to initialize AI client',
code: 'SDK_INIT_ERROR',
},
{ status: 503 }
);
}

try {
const prompt = createExplainPrompt({ term, context });

logGroqApiCall('start');
const chatCompletion = await groq.chat.completions.create({
messages: [
{
Expand All @@ -166,10 +271,12 @@ export async function POST(request: NextRequest) {
max_tokens: 1500,
top_p: 1,
});
logGroqApiCall('success', chatCompletion);

const content = chatCompletion.choices[0]?.message?.content;

if (!content) {
console.error('[ERROR] No content in Groq response');
throw new Error('No content in response');
}

Expand All @@ -184,26 +291,21 @@ export async function POST(request: NextRequest) {
},
});
} catch (error) {
console.error('Groq API error:', error);

const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
const errorName = error instanceof Error ? error.name : 'UnknownError';

console.error('Error details:', {
name: errorName,
message: errorMessage,
stack: error instanceof Error ? error.stack : undefined,
});
logGroqApiCall('error', error);
console.error('[GROQ_ERROR]', error instanceof Error ? error.message : 'Unknown error');

if (error instanceof Error) {
if (
error.message.includes('401') ||
error.message.includes('authentication') ||
error.message.includes('Invalid API Key')
) {
console.error('[AUTH_ERROR] API key authentication failed');
return NextResponse.json(
{ error: 'AI service authentication failed', code: 'AUTH_ERROR' },
{
error: 'AI service authentication failed',
code: 'AUTH_ERROR',
},
{ status: 503 }
);
}
Expand All @@ -221,7 +323,6 @@ export async function POST(request: NextRequest) {
{
error: 'AI model not available',
code: 'MODEL_ERROR',
details: errorMessage,
},
{ status: 503 }
);
Expand All @@ -232,10 +333,27 @@ export async function POST(request: NextRequest) {
{
error: 'Failed to generate explanation',
code: 'AI_ERROR',
details:
process.env.NODE_ENV === 'development' ? errorMessage : undefined,
},
{ status: 500 }
);
}
}

export async function GET() {
const apiKey = process.env.GROQ_API_KEY;
return NextResponse.json(
{
status: apiKey ? 'ok' : 'misconfigured',
service: 'ai-explain',
timestamp: new Date().toISOString(),
env: {
hasGroqKey: !!apiKey,
groqKeyLength: apiKey ? apiKey.length : 0,
nodeEnv: process.env.NODE_ENV,
isNetlify: !!process.env.NETLIFY,
context: process.env.CONTEXT ?? 'unknown',
},
},
{ status: apiKey ? 200 : 503 }
);
}
Comment on lines +342 to +359
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.

⚠️ Potential issue | 🟡 Minor

Health endpoint exposes potentially sensitive environment details.

The GET endpoint publicly exposes groqKeyLength, nodeEnv, isNetlify, and deployment context. This information could aid attackers in fingerprinting the environment. Consider restricting access or reducing the information exposed.

Suggested: Reduce public exposure
 export async function GET() {
   const apiKey = process.env.GROQ_API_KEY;
   return NextResponse.json(
     {
       status: apiKey ? 'ok' : 'misconfigured',
       service: 'ai-explain',
       timestamp: new Date().toISOString(),
-      env: {
-        hasGroqKey: !!apiKey,
-        groqKeyLength: apiKey ? apiKey.length : 0,
-        nodeEnv: process.env.NODE_ENV,
-        isNetlify: !!process.env.NETLIFY,
-        context: process.env.CONTEXT ?? 'unknown',
-      },
     },
     { status: apiKey ? 200 : 503 }
   );
 }

If detailed diagnostics are needed, consider protecting this endpoint with authentication or moving it to a separate internal route.

📝 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
export async function GET() {
const apiKey = process.env.GROQ_API_KEY;
return NextResponse.json(
{
status: apiKey ? 'ok' : 'misconfigured',
service: 'ai-explain',
timestamp: new Date().toISOString(),
env: {
hasGroqKey: !!apiKey,
groqKeyLength: apiKey ? apiKey.length : 0,
nodeEnv: process.env.NODE_ENV,
isNetlify: !!process.env.NETLIFY,
context: process.env.CONTEXT ?? 'unknown',
},
},
{ status: apiKey ? 200 : 503 }
);
}
export async function GET() {
const apiKey = process.env.GROQ_API_KEY;
return NextResponse.json(
{
status: apiKey ? 'ok' : 'misconfigured',
service: 'ai-explain',
timestamp: new Date().toISOString(),
},
{ status: apiKey ? 200 : 503 }
);
}
🤖 Prompt for AI Agents
In `@frontend/app/api/ai/explain/route.ts` around lines 342 - 359, The GET health
handler exposes sensitive environment details; update the GET function to stop
returning groqKeyLength, nodeEnv, isNetlify, and context (or move them behind
auth) — keep only non-sensitive status/service/timestamp and a boolean
hasGroqKey, or protect the route with authentication/ACL so only internal users
can fetch full diagnostics; locate the GET export in route.ts and remove the
sensitive fields from the NextResponse.json payload (or wrap the detailed
payload behind an auth check) and ensure the response status logic using apiKey
remains unchanged.

Loading