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
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"),
}
}

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>
)
}
161 changes: 132 additions & 29 deletions frontend/app/api/ai/explain/route.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,59 @@
export const runtime = 'nodejs';

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] NODE_ENV:', process.env.NODE_ENV);
}

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);
}
}
// =============================================================================

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 +80,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 };
}

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,
};
Comment on lines +83 to 110
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

Don’t bypass rate limits when IP is unknown.
Clients can omit/obfuscate forwarding headers and get unlimited access, which can drive cost and abuse. Use a shared “unknown” bucket instead of skipping limits.

🔧 Suggested fix (rate‑limit unknown IPs with a shared bucket)
-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 } {
+  const isUnknown = ip === 'unknown';
+  const key = isUnknown ? 'unknown' : ip;
 
   cleanupRateLimiter();
 
   const now = Date.now();
-  const entry = rateLimiter.get(ip);
+  const entry = rateLimiter.get(key);
 
   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, skipped: false };
+    rateLimiter.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
+    return {
+      allowed: true,
+      remaining: MAX_REQUESTS_PER_WINDOW - 1,
+      resetIn: RATE_LIMIT_WINDOW_MS,
+      skipped: isUnknown,
+    };
   }
 
   if (entry.count >= MAX_REQUESTS_PER_WINDOW) {
     const resetIn = entry.resetAt - now;
-    return { allowed: false, remaining: 0, resetIn, skipped: false };
+    return { allowed: false, remaining: 0, resetIn, skipped: isUnknown };
   }
 
   entry.count++;
   return {
     allowed: true,
     remaining: MAX_REQUESTS_PER_WINDOW - entry.count,
     resetIn: entry.resetAt - now,
-    skipped: false,
+    skipped: isUnknown,
   };
 }
📝 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 };
}
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,
};
function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } {
const isUnknown = ip === 'unknown';
const key = isUnknown ? 'unknown' : ip;
cleanupRateLimiter();
const now = Date.now();
const entry = rateLimiter.get(key);
if (!entry || now > entry.resetAt) {
rateLimiter.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
return {
allowed: true,
remaining: MAX_REQUESTS_PER_WINDOW - 1,
resetIn: RATE_LIMIT_WINDOW_MS,
skipped: isUnknown,
};
}
if (entry.count >= MAX_REQUESTS_PER_WINDOW) {
const resetIn = entry.resetAt - now;
return { allowed: false, remaining: 0, resetIn, skipped: isUnknown };
}
entry.count++;
return {
allowed: true,
remaining: MAX_REQUESTS_PER_WINDOW - entry.count,
resetIn: entry.resetAt - now,
skipped: isUnknown,
};
}
🤖 Prompt for AI Agents
In `@frontend/app/api/ai/explain/route.ts` around lines 83 - 110, The current
checkRateLimit function bypasses limits for ip === 'unknown', allowing abuse;
change it to treat unknown IPs as a shared bucket instead of skipping (e.g., map
'unknown' to a constant key like UNKNOWN_BUCKET and proceed through the normal
rateLimiter logic using rateLimiter, MAX_REQUESTS_PER_WINDOW and
RATE_LIMIT_WINDOW_MS), remove the early return that sets skipped: true, ensure
cleanupRateLimiter() still runs, and return skipped: false for the shared-bucket
behavior so unknown clients are rate-limited consistently with other IPs.

}

Expand Down Expand Up @@ -91,14 +137,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 +174,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 +209,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 }
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

logGroqApiCall('start');
const chatCompletion = await groq.chat.completions.create({
messages: [
{
Expand All @@ -166,10 +261,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 +281,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 }
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Expand All @@ -221,7 +313,6 @@ export async function POST(request: NextRequest) {
{
error: 'AI model not available',
code: 'MODEL_ERROR',
details: errorMessage,
},
{ status: 503 }
);
Expand All @@ -232,10 +323,22 @@ 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() {
return NextResponse.json(
{
status: 'ok',
service: 'ai-explain',
env: {
hasGroqKey: !!process.env.GROQ_API_KEY,
nodeEnv: process.env.NODE_ENV,
},
},
{ status: 200 }
);
}
29 changes: 16 additions & 13 deletions frontend/components/about/CommunitySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,34 @@

import { Github, ArrowRight, ExternalLink, MessageCircle } from "lucide-react"
import Link from "next/link"
import { useTranslations } from "next-intl"
import { TESTIMONIALS, type Testimonial } from "@/data/about"
import { GradientBadge } from "@/components/ui/gradient-badge"
import { SectionHeading } from "@/components/ui/section-heading"

export function CommunitySection() {
const t = useTranslations("about.community")

return (
<section className="w-full py-20 lg:py-28 relative overflow-hidden bg-gray-50 dark:bg-transparent">

<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[300px] h-[300px] bg-[#1e5eff]/5 dark:bg-[#ff2d55]/5 blur-[80px] rounded-full pointer-events-none" />

<div className="w-full relative z-10">

<div className="container-main mb-12 text-center">
<GradientBadge icon={MessageCircle} text="Community Love" className="mb-4" />
<SectionHeading
title="Approved by"
highlight="Survivors"
subtitle="Join thousands of developers who stopped guessing and started shipping. Real feedback from real engineers."
<GradientBadge icon={MessageCircle} text={t("badge")} className="mb-4" />

<SectionHeading
title={t("title")}
highlight={t("titleHighlight")}
subtitle={t("subtitle")}
className="mb-0"
/>
</div>

<div className="relative w-full pause-on-hover mb-8 md:mb-16">

<div className="pointer-events-none absolute left-0 top-0 z-10 h-full w-12 md:w-32 bg-gradient-to-r from-gray-50 dark:from-background to-transparent" />
<div className="pointer-events-none absolute right-0 top-0 z-10 h-full w-12 md:w-32 bg-gradient-to-l from-gray-50 dark:from-background to-transparent" />

Expand Down Expand Up @@ -60,7 +63,7 @@ export function CommunitySection() {
hover:text-[#1e5eff] dark:hover:text-[#ff2d55]"
>
<Github size={12} />
Join Discussion
{t("joinDiscussion")}
<ArrowRight size={10} className="group-hover:translate-x-0.5 transition-transform" />
</Link>
</div>
Expand All @@ -77,7 +80,7 @@ export function CommunitySection() {
hover:shadow-[0_0_30px_-5px_rgba(30,94,255,0.15)] dark:hover:shadow-[0_0_30px_-5px_rgba(255,45,85,0.15)]"
>
<span className="text-sm text-gray-700 dark:text-gray-200 font-medium">
Have a success story or feature request?
{t("successStory")}
</span>

<span className="flex items-center gap-2 px-5 py-2.5 rounded-full
Expand All @@ -88,13 +91,13 @@ export function CommunitySection() {
group-hover:text-white dark:group-hover:text-white"
>
<Github size={14} />
Join Discussion
{t("joinDiscussion")}
<ArrowRight size={12} className="group-hover:translate-x-1 transition-transform" />
</span>
</Link>

<p className="mt-3 md:mt-6 text-[8px] md:text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-widest md:font-bold md:opacity-60 opacity-70">
We read every <span className="hidden md:inline">single </span>thread
{t("readThreads")}
</p>
</div>

Expand Down
Loading