Feat: Auth , Landing page , Api Key insertion functionality#7
Feat: Auth , Landing page , Api Key insertion functionality#7Abhishek-Sonje wants to merge 3 commits into
Conversation
Review Summary by QodoImplement authentication, API key management, and comprehensive landing page with Chalk AI design system
WalkthroughsDescription• **Authentication & Authorization**: Implemented Clerk authentication middleware protecting /board route, with public landing page and authenticated board access • **API Key Management**: Added Gemini API key validation endpoint, user-provided key support in diagram completion, and persistent localStorage management via context provider • **Landing Page**: Converted home page to comprehensive landing page with Hero, Features, How It Works, Testimonials, Founders, CTA, and Footer sections with sketched design aesthetic • **Board Refactoring**: Extracted board functionality into dedicated BoardClient component with integrated API key modal, voice input, and fullscreen controls • **Design System**: Implemented "Chalk AI" design tokens with purple/gray color palette, custom fonts (Kalam for sketch style, Inter for body), and marquee animations • **UI Components**: Added reusable components including API key modal with validation, video modal, fade-in animations, decorative icons, and responsive header with auth integration • **Dependencies**: Added @clerk/nextjs for authentication infrastructure Diagramflowchart LR
A["Landing Page<br/>Hero, Features,<br/>Testimonials"] -->|"Authenticated"| B["Board Page<br/>BoardClient<br/>Component"]
A -->|"Sign In"| C["Clerk Auth<br/>Middleware"]
C -->|"Protected"| B
B -->|"API Key Input"| D["API Key Modal<br/>& Provider"]
D -->|"Validate"| E["Validate Key<br/>Endpoint"]
E -->|"User Key"| F["Complete Diagram<br/>Endpoint"]
F -->|"Generate"| G["Gemini API<br/>Response"]
H["Design System<br/>Chalk AI Tokens"] -.->|"Styles"| A
H -.->|"Styles"| B
File Changes1. frontend/app/api/validate-key/route.ts
|
Code Review by Qodo
1. Env key race leak
|
There was a problem hiding this comment.
Pull request overview
This PR layers a Clerk-based auth flow, a marketing landing page, and an in-app "Bring Your Own Gemini Key" experience onto the existing whiteboard app. The board UI (formerly app/page.tsx) is moved behind /board, the public root now renders a multi-section landing page, and a new modal + provider lets users save a Gemini key in localStorage that is forwarded to the diagram API via header.
Changes:
- Add Clerk auth (provider in root layout, sign-in/up buttons,
Showhelper,proxy.tsmatcher config, and a server-side guard on/board). - Replace the home page with a landing experience (
hero,features,how-it-works,testimonials,founders,cta,footer, decorative-icon, fade-in) and introduceHeader,UIProvider,LayoutWrappershells; refactor board logic intoBoardClient. - Introduce user-supplied Gemini key handling:
ApiKeyProvider/ApiKeyModal, a/api/validate-keyroute, and header-based key forwarding in/api/complete-diagram.
Reviewed changes
Copilot reviewed 27 out of 38 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/proxy.ts | New Clerk middleware — but placed at a non-recognized path so it doesn't run. |
| frontend/package.json / package-lock.json | Adds @clerk/nextjs dependency. |
| frontend/bun.lock | Adds a Windows-only lightningcss native binary as a direct dep. |
| frontend/app/layout.tsx | Wraps app in ClerkProvider/ApiKeyProvider/UIProvider, adds header + fonts. |
| frontend/app/page.tsx | Replaces whiteboard with the new landing-page composition. |
| frontend/app/board/page.tsx | New server-side auth-guarded entry that renders BoardClient. |
| frontend/app/api/complete-diagram/route.ts | Accepts per-request Gemini key via header; mutates process.env to forward it. |
| frontend/app/api/validate-key/route.ts | New unauthenticated endpoint that probes Gemini to validate a user key. |
| frontend/app/globals.css | Adds chalk-themed CSS variables, marquee keyframes, font tokens. |
| frontend/components/board-client.tsx | Extracts whiteboard UI; integrates API-key gating and modal. |
| frontend/components/api-key-provider.tsx / api-key-modal.tsx | New context + modal for managing Gemini key in localStorage. |
| frontend/components/header.tsx / layout-wrapper.tsx / ui-provider.tsx | New chrome/layout primitives shared across pages. |
| frontend/components/clerk-helpers.tsx | Async Show helper to conditionally render by auth state. |
| frontend/components/landing/*.tsx | New landing sections, icons, doodles, decorative icon, fade-in animator. |
Files not reviewed (1)
- frontend/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; | ||
|
|
||
| // Define which routes are public. All other routes will require authentication. | ||
| // / is public, /board is protected. | ||
| const isPublicRoute = createRouteMatcher(["/"]); | ||
|
|
||
| export default clerkMiddleware(async (auth, request) => { | ||
| if (!isPublicRoute(request)) { | ||
| // Protect all non-public routes | ||
| await auth.protect(); | ||
| } | ||
| }); | ||
|
|
||
| export const config = { | ||
| matcher: [ | ||
| // Skip Next.js internals and all static files, unless found in search params | ||
| "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", | ||
| // Always run for API routes | ||
| "/(api|trpc)(.*)", | ||
| ], | ||
| }; |
| // Inject the key into the environment for the @ai-sdk/google provider | ||
| process.env.GOOGLE_GENERATIVE_AI_API_KEY = apiKey; |
| style="color:#0a0000;" | ||
| width="64" | ||
| height="64" | ||
| preserveAspectRatio="xMidYMid meet" | ||
| > | ||
| <mask id="path-1-inside-1_103_6790" fill="currentColor"> | ||
| <path | ||
| fill-rule="evenodd" | ||
| clip-rule="evenodd" |
| export const PlayIcon = () => ( | ||
| <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor"> | ||
| <path d="M3 2l7 4-7 4z" /> | ||
| </svg> | ||
| ); |
| import { PlayIcon, SparkleIcon } from "./icons"; | ||
| import { ArrowRight, Sparkles } from "lucide-react"; | ||
| import { VideoModal } from "./video-modal"; | ||
| import { SignInButton, SignUpButton, useAuth } from "@clerk/nextjs"; |
| import Link from "next/link"; | ||
| import { FadeIn } from "./fade-in"; | ||
| import { LogoIcon, ArrowRightIcon } from "./icons"; | ||
| import { SignInButton, SignUpButton, useAuth } from "@clerk/nextjs"; |
| 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"; |
| 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" }] }], | ||
| }), | ||
| } | ||
| ); |
| 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 } | ||
| ); | ||
| } |
| useEffect(() => { | ||
| try { | ||
| const stored = localStorage.getItem(STORAGE_KEY); | ||
| if (stored) setApiKeyState(stored); | ||
| } catch {} | ||
| setIsLoaded(true); | ||
| }, []); | ||
|
|
||
| const setApiKey = useCallback((key: string) => { | ||
| try { | ||
| localStorage.setItem(STORAGE_KEY, key); | ||
| } catch {} | ||
| setApiKeyState(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 } | ||
| ); | ||
| } | ||
|
|
||
| // Inject the key into the environment for the @ai-sdk/google provider | ||
| process.env.GOOGLE_GENERATIVE_AI_API_KEY = apiKey; | ||
|
|
There was a problem hiding this comment.
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
| import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; | ||
|
|
||
| // Define which routes are public. All other routes will require authentication. | ||
| // / is public, /board is protected. | ||
| const isPublicRoute = createRouteMatcher(["/"]); | ||
|
|
||
| export default clerkMiddleware(async (auth, request) => { | ||
| if (!isPublicRoute(request)) { | ||
| // Protect all non-public routes | ||
| await auth.protect(); | ||
| } | ||
| }); | ||
|
|
||
| export const config = { | ||
| matcher: [ | ||
| // Skip Next.js internals and all static files, unless found in search params | ||
| "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", | ||
| // Always run for API routes | ||
| "/(api|trpc)(.*)", | ||
| ], | ||
| }; |
There was a problem hiding this comment.
2. Middleware file misnamed 🐞 Bug ⛨ Security
Clerk route protection is implemented in frontend/proxy.ts, but Next.js only executes middleware from middleware.ts/middleware.js at the app root. As a result, /board and /api/* protections may not run, leaving API routes unauthenticated.
Agent Prompt
## Issue description
The Clerk middleware is defined in `frontend/proxy.ts`, which Next.js will not automatically run as middleware. This likely disables intended auth protection for non-public routes and API endpoints.
## Issue Context
`BoardPage` explicitly references `middleware.ts` as the primary guard, but the repo contains `proxy.ts` instead.
## Fix Focus Areas
- Rename/move `frontend/proxy.ts` to `frontend/middleware.ts` (or the correct root per your Next.js layout).
- Ensure the matcher config remains intact and that the middleware actually runs for `/board` and `/api/*`.
- Update any comments/docs to reflect the real filename.
## Fix Focus Areas (references)
- frontend/proxy.ts[1-21]
- frontend/app/board/page.tsx[9-17]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| export const Sparkle = () => ( | ||
| <svg | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| viewBox="0 0 87 87" | ||
| fill="currentColor" | ||
| style="color:#0a0000;" | ||
| width="64" | ||
| height="64" | ||
| preserveAspectRatio="xMidYMid meet" | ||
| > | ||
| <mask id="path-1-inside-1_103_6790" fill="currentColor"> | ||
| <path | ||
| fill-rule="evenodd" | ||
| clip-rule="evenodd" | ||
| d="M48.956 32.7515L43.007 0.632324L37.0728 32.6721L23.227 24.0134L31.7425 37.7602L0 43.6394L31.9899 49.5643L23.1425 63.712L37.1519 55.0337L43.007 86.6464L48.8587 55.0524L62.8411 63.7964L54.0248 49.5642L86.0141 43.6394L54.3703 37.7785L62.9256 24.0979L48.956 32.7515Z" | ||
| fill="#000000" | ||
| stroke="#000000" | ||
| /> | ||
| </mask> | ||
| <path | ||
| d="M43.007 0.632324L45.3499 0.198402H40.6642L43.007 0.632324ZM48.956 32.7515L46.6131 33.1854L47.2479 36.6123L50.2107 34.777L48.956 32.7515ZM37.0728 32.6721L35.8095 34.6922L38.778 36.5486L39.4156 33.106L37.0728 32.6721ZM23.227 24.0134L24.4903 21.9932L21.2015 25.2681L23.227 24.0134ZM31.7425 37.7602L32.1765 40.103L35.6034 39.4683L33.7681 36.5055L31.7425 37.7602ZM0 43.6394L-0.433923 41.2966L-0.433923 45.9822L0 43.6394ZM31.9899 49.5643L34.0101 50.8277L35.8665 47.8592L32.4238 47.2215L31.9899 49.5643ZM23.1425 63.712L21.1224 62.4486L24.3972 65.7375L23.1425 63.712ZM37.1519 55.0337L39.4947 54.5998L38.86 51.1729L35.8972 53.0082L37.1519 55.0337ZM43.007 86.6464L40.6642 87.0803H45.3499L43.007 86.6464ZM48.8587 55.0524L50.122 53.0322L47.1535 51.1758L46.5159 54.6185L48.8587 55.0524ZM62.8411 63.7964L61.5777 65.8166L64.8666 62.5417L62.8411 63.7964ZM54.0248 49.5642L53.5909 47.2214L50.164 47.8561L51.9993 50.819L54.0248 49.5642ZM86.0141 43.6394L86.448 45.9822V41.2966L86.0141 43.6394ZM54.3703 37.7785L52.3501 36.5151L50.4937 39.4837L53.9363 40.1213L54.3703 37.7785ZM62.9256 24.0979L64.9457 25.3612L61.6708 22.0724L62.9256 24.0979ZM40.6642 1.06625L46.6131 33.1854L51.2988 32.3175L45.3499 0.198402L40.6642 1.06625ZM39.4156 33.106L45.3499 1.06625L40.6642 0.198402L34.73 32.2381L39.4156 33.106ZM21.9637 26.0336L35.8095 34.6922L38.3362 30.6519L24.4903 21.9932L21.9637 26.0336ZM33.7681 36.5055L25.2525 22.7587L21.2015 25.2681L29.717 39.0149L33.7681 36.5055ZM0.433923 45.9822L32.1765 40.103L31.3086 35.4174L-0.433923 41.2966L0.433923 45.9822ZM32.4238 47.2215L0.433923 41.2966L-0.433923 45.9822L31.556 51.9072L32.4238 47.2215ZM25.1627 64.9753L34.0101 50.8277L29.9697 48.301L21.1224 62.4486L25.1627 64.9753ZM35.8972 53.0082L21.8878 61.6864L24.3972 65.7375L38.4067 57.0593L35.8972 53.0082ZM45.3499 86.2125L39.4947 54.5998L34.8091 55.4677L40.6642 87.0803L45.3499 86.2125ZM46.5159 54.6185L40.6642 86.2125L45.3499 87.0803L51.2015 55.4863L46.5159 54.6185ZM64.1044 61.7763L50.122 53.0322L47.5954 57.0726L61.5777 65.8166L64.1044 61.7763ZM51.9993 50.819L60.8155 65.0512L64.8666 62.5417L56.0504 48.3095L51.9993 50.819ZM85.5802 41.2966L53.5909 47.2214L54.4588 51.907L86.448 45.9822L85.5802 41.2966ZM53.9363 40.1213L85.5802 45.9822L86.448 41.2966L54.8042 35.4357L53.9363 40.1213ZM60.9054 22.8346L52.3501 36.5151L56.3904 39.0418L64.9457 25.3612L60.9054 22.8346ZM50.2107 34.777L64.1803 26.1234L61.6708 22.0724L47.7012 30.7259L50.2107 34.777Z" | ||
| fill="#000000" | ||
| mask="url(#path-1-inside-1_103_6790)" | ||
| stroke="#000000" | ||
| /> | ||
| </svg> |
There was a problem hiding this comment.
3. Invalid svg jsx props 🐞 Bug ≡ Correctness
frontend/components/landing/doodles.tsx uses invalid TSX/SVG props (e.g., style="...", fill-rule, clip-rule) that do not type-check in React/TypeScript. This will break next build type-checking even if the component is not imported.
Agent Prompt
## Issue description
`doodles.tsx` is TSX and is included in TypeScript checking. It uses invalid JSX attribute names (`fill-rule`, `clip-rule`) and assigns `style` as a string, which violates React/TS types.
## Issue Context
TypeScript checking in Next.js covers `**/*.ts` and `**/*.tsx` (per tsconfig include), so this can fail CI/build even if unused.
## Fix Focus Areas
- Convert SVG attributes to React’s camelCase equivalents (`fillRule`, `clipRule`, etc.).
- Replace `style="color:#0a0000;"` with `style={{ color: "#0a0000" }}`.
- If the component is not needed, delete the file instead.
## Fix Focus Areas (references)
- frontend/components/landing/doodles.tsx[1-26]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| --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%); } | ||
| } | ||
| } |
There was a problem hiding this comment.
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
No description provided.