Skip to content
Open
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: 9 additions & 4 deletions frontend/app/api/complete-diagram/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ export const maxDuration = 60;

export async function POST(request: NextRequest) {
try {
// Server-side only API key (no NEXT_PUBLIC prefix)
const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
// Prefer user-provided key from header, fall back to server env var
const userKey = request.headers.get("x-gemini-api-key");
const apiKey = userKey?.trim() || process.env.GOOGLE_GENERATIVE_AI_API_KEY;

if (!apiKey) {
return NextResponse.json(
{ error: "Missing GOOGLE_GENERATIVE_AI_API_KEY" },
{ status: 500 }
{ error: "NO_API_KEY", message: "No Gemini API key configured. Please add your API key in the board settings." },
{ status: 401 }
);
}
Comment on lines 9 to 19

// Inject the key into the environment for the @ai-sdk/google provider
process.env.GOOGLE_GENERATIVE_AI_API_KEY = apiKey;
Comment on lines +21 to +22

Comment on lines +10 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Env key race leak 🐞 Bug ⛨ Security

POST /api/complete-diagram assigns process.env.GOOGLE_GENERATIVE_AI_API_KEY from a request
header, which is global and shared across concurrent requests. This can cause one user’s Gemini key
to be used for another user’s request and can persist beyond the request when the header is absent.
Agent Prompt
## Issue description
`frontend/app/api/complete-diagram/route.ts` sets `process.env.GOOGLE_GENERATIVE_AI_API_KEY` per request based on `x-gemini-api-key`. Because `process.env` is process-global, concurrent requests can race and later requests can unintentionally reuse a previous user's key.

## Issue Context
The code calls `generateText({ model: google("gemini-2.5-flash-image"), ... })`, and the provider reads credentials from environment/global config.

## Fix Focus Areas
- Prefer a per-request API key injection mechanism (provider option / client instance / explicit header) rather than global env.
- If the SDK truly only supports env-based configuration, do **not** use a shared mutable global; refactor to a direct HTTP call where the key can be set per request.

## Fix Focus Areas (references)
- frontend/app/api/complete-diagram/route.ts[8-23]
- frontend/app/api/complete-diagram/route.ts[79-83]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

const { prompt, image_data } = (await request.json()) as {
prompt: string;
image_data?: string;
Expand Down
73 changes: 73 additions & 0 deletions frontend/app/api/validate-key/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from "next/server";

export const runtime = "nodejs";

export async function POST(request: NextRequest) {
try {
const { api_key } = (await request.json()) as { api_key: string };

if (!api_key?.trim()) {
return NextResponse.json(
{ valid: false, reason: "API key is required." },
{ status: 400 }
);
}

if (!api_key.startsWith("AIza")) {
return NextResponse.json({
valid: false,
reason: "That doesn't look like a valid Gemini API key. Keys start with 'AIza'.",
});
}

// Make a real but minimal call to Gemini to validate the key
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${api_key}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contents: [{ parts: [{ text: "Hi" }] }],
}),
}
);
Comment on lines +5 to +33

if (response.ok) {
return NextResponse.json({ valid: true });
}

const data = await response.json().catch(() => ({}));
const errorCode = data?.error?.status ?? "";
const errorMessage = data?.error?.message ?? "";

if (response.status === 400 && errorMessage.includes("API_KEY_INVALID")) {
return NextResponse.json({
valid: false,
reason: "Invalid API key. Please check and try again.",
});
}

if (response.status === 403 || errorCode === "PERMISSION_DENIED") {
return NextResponse.json({
valid: false,
reason: "API key is valid but doesn't have permission to use Gemini. Enable the Generative Language API in your Google Cloud project.",
});
}

if (response.status === 429 || errorCode === "RESOURCE_EXHAUSTED") {
// Key works, just rate-limited — treat as valid
return NextResponse.json({ valid: true });
}

return NextResponse.json({
valid: false,
reason: `Gemini returned an error: ${errorMessage || response.statusText}`,
});
} catch (error) {
console.error("Key validation error:", error);
return NextResponse.json(
{ valid: false, reason: "Could not reach Gemini API. Check your internet connection." },
{ status: 500 }
);
}
}
20 changes: 20 additions & 0 deletions frontend/app/board/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
import { BoardClient } from "@/components/board-client";

/**
* BoardPage is a server component that handles authentication checks
* before rendering the whiteboard client application.
*/
export default async function BoardPage() {
// Use the latest Clerk auth() API with async/await
const { userId } = await auth();

// If no user is authenticated, redirect to the landing page
// Note: middleware.ts also handles this, but this is a secondary safety check
if (!userId) {
redirect("/");
}

return <BoardClient />;
}
50 changes: 38 additions & 12 deletions frontend/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
@import "tailwindcss";



:root {
/* Minimal Monochrome Palette */
--background: #ffffff;
--foreground: #000000;
--background: #fafaf7;
--foreground: #1a1a1a;

/* Chalk AI Design Tokens */
--chalk-ink: #1a1a1a;
--chalk-ink-soft: #3d3d3d;
--chalk-gray: #888;
--chalk-bg: #fafaf7;
--chalk-white: #ffffff;
--chalk-purple: #7b5ea7;
--chalk-purple-light: #ede8f5;
--chalk-blue-soft: #e8eef8;
--chalk-border: #d0cfc7;
--chalk-border-soft: #e8e7e0;

/* Modern Accents */
--muted: #f4f4f5;
--muted-foreground: #737373;
--border: #eaeaea;
--input: #f4f4f5;
--primary: #000000;
--primary-foreground: #ffffff;
--font-sketch: var(--font-sketch);
--font-body: var(--font-body);

--ring: #000000;
--radius: 0.75rem;
}

Expand All @@ -28,8 +35,27 @@
--color-primary-foreground: var(--primary-foreground);
--color-ring: var(--ring);
--radius-lg: var(--radius);
--font-sans: var(--font-geist-sans), system-ui, sans-serif;
--color-chalk-ink: var(--chalk-ink);
--color-chalk-ink-soft: var(--chalk-ink-soft);
--color-chalk-gray: var(--chalk-gray);
--color-chalk-bg: var(--chalk-bg);
--color-chalk-white: var(--chalk-white);
--color-chalk-purple: var(--chalk-purple);
--color-chalk-purple-light: var(--chalk-purple-light);
--color-chalk-blue-soft: var(--chalk-blue-soft);
--color-chalk-border: var(--chalk-border);
--color-chalk-border-soft: var(--chalk-border-soft);

--font-sans: var(--font-body), system-ui, sans-serif;
--font-mono: var(--font-geist-mono), monospace;
--font-sketch: var(--font-sketch);

--animate-marquee: marquee 28s linear infinite;

@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
}
Comment on lines 35 to 59
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Broken theme css tokens 🐞 Bug ≡ Correctness

globals.css defines @keyframes inside @theme inline and maps theme variables to undefined
custom properties like --muted/--primary, making utilities such as
bg-primary/text-muted-foreground resolve to invalid values. This can break global styling and
animations across the app.
Agent Prompt
## Issue description
In `globals.css`, the Tailwind `@theme inline` block contains a nested `@keyframes` rule and references CSS variables (`--muted`, `--primary`, etc.) that are not defined anywhere. This can break global token resolution and utility styles.

## Issue Context
Many components use classes like `bg-primary`, `text-muted-foreground`, etc. With `--primary`/`--muted` undefined, the mapped `--color-*` variables become invalid.

## Fix Focus Areas
- Move `@keyframes marquee` to the top level (outside `@theme`).
- Define the missing base tokens in `:root` (e.g., `--muted`, `--primary`, `--border`, `--ring`, `--primary-foreground`, etc.), or remove the mappings if you no longer use those utilities.

## Fix Focus Areas (references)
- frontend/app/globals.css[5-25]
- frontend/app/globals.css[27-59]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


body {
Expand Down
67 changes: 65 additions & 2 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Link from "next/link";
import { ClerkProvider, SignInButton, SignUpButton, UserButton } from "@clerk/nextjs";
import { Show } from "@/components/clerk-helpers";
import { UIProvider } from "@/components/ui-provider";
import { ApiKeyProvider } from "@/components/api-key-provider";
import { Header } from "@/components/header";
import { LayoutWrapper } from "@/components/layout-wrapper";
import { Caveat, Nunito, Kalam, Inter } from "next/font/google";
Comment on lines 2 to +10
import "./globals.css";

const kalam = Kalam({
subsets: ["latin"],
weight: ["300", "400", "700"],
style: ["normal"],
variable: "--font-sketch",
});

const inter = Inter({
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700"],
style: ["normal", "italic"],
variable: "--font-body",
});

// const nunito = Nunito({
// subsets: ["latin"],
// weight: ["300", "400", "500", "600", "700"],
// style: ["normal", "italic"],
// variable: "--font-body",
// });


const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
Expand Down Expand Up @@ -61,9 +91,42 @@ export default function RootLayout({
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} ${inter.variable} ${kalam.variable} antialiased`}
>
{children}
<ClerkProvider>
<ApiKeyProvider>
<UIProvider>
<Header>
<Show when="signed-out">
<SignInButton mode="modal">
<button className="text-sm font-medium text-chalk-ink-soft hover:text-chalk-ink transition-colors">
Log in
</button>
</SignInButton>
<SignUpButton mode="modal">
<button className="px-5 py-2 text-sm font-semibold bg-chalk-ink text-white rounded-lg hover:-translate-y-0.5 hover:shadow-lg transition-all active:scale-95">
Get Started
</button>
</SignUpButton>
</Show>
<Show when="signed-in">
<div className="flex items-center gap-4">
<Link
href="/board"
className="text-sm font-semibold text-chalk-ink hover:text-chalk-purple transition-colors"
>
Board
</Link>
<UserButton appearance={{ elements: { userButtonAvatarBox: "w-9 h-9" } }} />
</div>
</Show>
</Header>
<LayoutWrapper>
{children}
</LayoutWrapper>
</UIProvider>
</ApiKeyProvider>
</ClerkProvider>
</body>
</html>
);
Expand Down
Loading