auth aftercallback changes and logical restructure (#1668)

Closes
https://linear.app/codeflash-ai/issue/CF-674/fix-login-flow-in-webapp
This commit is contained in:
Sarthak Agarwal 2025-07-07 11:10:43 +05:30 committed by GitHub
parent a2896e5680
commit 378412bd18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 3248 additions and 879 deletions

2
.gitignore vendored
View file

@ -15,7 +15,7 @@ cli/dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/

View file

@ -1,12 +1,27 @@
/** @type {import("next").NextConfig} */
let nextConfig = {
transpilePackages: ["@codeflash-ai/common"],
experimental: { serverActions: { allowedOrigins: ["app.codeflash.ai", "localhost:3000"] } },
experimental: {
serverActions: { allowedOrigins: ["app.codeflash.ai", "localhost:3000"] },
instrumentationHook: true,
},
typescript: {
ignoreBuildErrors: false,
},
// Optimize for production stability
poweredByHeader: false,
compress: true,
images: {
domains: ["avatars.githubusercontent.com", "github.com"],
remotePatterns: [
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
},
{
protocol: "https",
hostname: "github.com",
},
],
},
}
@ -32,9 +47,6 @@ export default withSentryConfig(
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Transpiles SDK to be compatible with IE11 (increases bundle size)
transpileClientSDK: true,
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
tunnelRoute: "/monitoring",
@ -44,10 +56,7 @@ export default withSentryConfig(
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors.
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
// Disable automatic instrumentation that might cause issues
automaticVercelMonitors: false,
},
)

File diff suppressed because it is too large Load diff

View file

@ -23,7 +23,7 @@
"@codeflash-ai/common": "^1.0.15",
"@hookform/resolvers": "^3.3.2",
"@monaco-editor/react": "^4.7.0",
"@prisma/client": "^6.2.1",
"@prisma/client": "^6.7.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
@ -35,7 +35,7 @@
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.1.4",
"@sentry/nextjs": "^8.55.0",
"@sentry/nextjs": "^9.34.0",
"@types/node": "^20",
"@types/pg": "^8.10.9",
"@types/react": "^18",
@ -60,6 +60,7 @@
"react-markdown": "^9.0.1",
"react-papaparse": "^4.4.0",
"remark-gfm": "^4.0.0",
"sharp": "^0.34.2",
"stripe": "^17.7.0",
"tailwind-merge": "^2.0.0",
"tailwindcss": "^3.3.0",
@ -83,7 +84,7 @@
"jsdom": "^24.1.0",
"lint-staged": "^15.4.3",
"prettier": "3.2.5",
"prisma": "^6.2.1",
"prisma": "^6.7.0",
"simple-git-hooks": "^2.9.0",
"typescript": "^5.4.5",
"vitest": "^3.0.8"

View file

@ -0,0 +1,52 @@
import { redirect } from "next/navigation"
import { getSession } from "@auth0/nextjs-auth0"
import { type JSX } from "react"
// Security function to validate returnTo URLs
function isValidReturnUrl(url: string): boolean {
if (url.startsWith("/")) {
return true
}
return false
}
export default async function AuthenticationPage({
searchParams,
}: {
searchParams: { returnTo?: string; error?: string }
}): Promise<JSX.Element> {
const session = await getSession()
if (session) {
// User is already logged in
console.log(`[Login] User ${session.user.sub} already authenticated`)
const returnTo =
searchParams.returnTo && isValidReturnUrl(searchParams.returnTo) ? searchParams.returnTo : "/"
redirect(returnTo)
}
// Show error if there was one
if (searchParams.error) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold">Login Error</h2>
<p className="mt-2">There was an error during login. Please try again.</p>
<a
href="/api/auth/login"
className="mt-4 inline-block rounded bg-blue-500 px-4 py-2 text-white"
>
Try Again
</a>
</div>
</div>
)
}
// Not logged in - redirect to Auth0
const returnTo =
searchParams.returnTo && isValidReturnUrl(searchParams.returnTo) ? searchParams.returnTo : "/"
const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(returnTo)}`
redirect(loginUrl)
}

View file

@ -4,8 +4,8 @@ import LogoText from "./assets/LogoText"
import { useEffect, useState } from "react"
import { SubmitSecondOnboardingPage } from "./SubmitSecondOnboardingPage"
import { useRouter } from "next/navigation"
import CfRadioButton from "@/app/onboarding/CfRadioButton"
import FinishSection from "@/app/onboarding/FinishSection"
import CfRadioButton from "./CfRadioButton"
import FinishSection from "./FinishSection"
import PythonLibrariesInput from "./PythonLibrariesInput"
export default function SecondPage({ className = "" }: SecondPageProps) {

View file

@ -2,7 +2,7 @@
import { getSession } from "@auth0/nextjs-auth0"
import { markUserCompletedOnboarding, submitOnboardingQuestions } from "@codeflash-ai/common"
import PostHogClient from "@/app/app/posthog"
import PostHogClient from "@/lib/posthog"
import { redirect } from "next/navigation"
import { cookies } from "next/headers"

View file

@ -1,7 +1,7 @@
"use server"
import { getSession } from "@auth0/nextjs-auth0"
import PostHogClient from "@/app/app/posthog"
import PostHogClient from "@/lib/posthog"
import { redirect } from "next/navigation"
export async function SubmitSecondOnboardingPage(

View file

@ -1,7 +1,7 @@
"use client"
import { useState, useEffect, useRef } from "react"
import { useRouter } from "next/navigation"
import { getUserId } from "../utils/auth"
import { getUserId } from "@/app/utils/auth"
import { checkUserOnboardingStatus, completeUserOnboarding } from "./onboarding-action"
import { upsertReferralSource, getUserSelectedReferral } from "./referral/actions"
import { AnimatePresence, motion } from "framer-motion"
@ -26,7 +26,7 @@ import {
} from "lucide-react"
import LogoBox from "@/components/dashboard/logo-box"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { generateToken } from "../app/apikeys/tokenfuncs"
import { generateToken } from "@/app/(dashboard)/apikeys/tokenfuncs"
import { ApiKeyCard, StepInstruction } from "@/components/onboarding/step-content-renderer"
// Type definitions to improve type safety

View file

@ -5,7 +5,7 @@ import {
isUserReferralComplete,
getUserReferralData,
} from "@codeflash-ai/common"
import PostHogClient from "@/app/app/posthog"
import PostHogClient from "@/lib/posthog"
import { getSession } from "@auth0/nextjs-auth0"
export async function upsertReferralSource(

View file

@ -1,4 +1,4 @@
import SecondPage from "@/app/onboarding/SecondPage"
import SecondPage from "../SecondPage"
import OnboardingWrapper from "../OnboardingWrapper"
import "../compiled-styles.css"

View file

@ -92,16 +92,30 @@ export async function getAllRepositories(
// New wrapper functions that call the imported functions
export async function getUserOptimizationCount(userId: string, username: string) {
return getOptimizationEventCountByUserId(userId, username)
try {
return await getOptimizationEventCountByUserId(userId, username)
} catch (error) {
console.error("Failed to get user optimization count:", error)
throw new Error(
`Failed to fetch optimization count: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getUserOptimizationSuccessfulCount(userId: string, username: string) {
return prisma.optimization_events.count({
where: {
is_optimization_found: true,
OR: [{ user_id: userId }, { current_username: username }],
},
})
try {
return await prisma.optimization_events.count({
where: {
is_optimization_found: true,
OR: [{ user_id: userId }, { current_username: username }],
},
})
} catch (error) {
console.error("Failed to get successful optimization count:", error)
throw new Error(
`Failed to fetch successful optimization count: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getRepoUserOptimizationCount(
@ -113,7 +127,14 @@ export async function getRepoUserOptimizationCount(
}
export async function getActiveRepositoriesLast30Days(userId: string, username: string) {
return getActiveRepoCountLast30Days(userId, username)
try {
return await getActiveRepoCountLast30Days(userId, username)
} catch (error) {
console.error("Failed to get active repositories count:", error)
throw new Error(
`Failed to fetch active repositories: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getOptimizationsTimeSeriesData(
@ -122,6 +143,9 @@ export async function getOptimizationsTimeSeriesData(
onlySuccessful?: boolean,
) {
try {
if (!userId || !username) {
throw new Error("Invalid user credentials")
}
const data = await prisma.optimization_events.findMany({
where: {
OR: [{ user_id: userId }, { current_username: username }],
@ -170,7 +194,9 @@ export async function getOptimizationsTimeSeriesData(
return completeData
} catch (error) {
console.error("Failed to fetch optimization time series data:", error)
return []
throw new Error(
`Failed to fetch time series data: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
@ -180,6 +206,9 @@ export async function getPullRequestEventTimeSeriesData(
year: number,
) {
try {
if (!userId || !username || !year) {
throw new Error("Invalid parameters provided")
}
const eventTypes = ["pr_created", "pr_merged", "pr_closed"]
const data = await prisma.optimization_events.findMany({
where: {
@ -222,7 +251,9 @@ export async function getPullRequestEventTimeSeriesData(
return completeData
} catch (error) {
console.error("Failed to fetch pull request event time series data:", error)
return []
throw new Error(
`Failed to fetch PR time series data: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
@ -230,76 +261,87 @@ export async function getActiveUserLeaderboardLast30Days(
userId: string,
username: string,
): Promise<{ username: string; eventCount: number; avatarUrl: string }[]> {
const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
try {
if (!userId || !username) {
throw new Error("Invalid user credentials")
}
// Get the repository IDs the user has access to or has optimization events in
const memberRepos = await prisma.repositories.findMany({
where: {
OR: [
{
repository_members: {
some: {
user_id: userId,
},
},
},
{
is_private: false,
optimization_events: {
some: {
current_username: username,
},
},
},
],
},
select: {
id: true,
},
})
const repoIds = memberRepos.map(r => r.id)
const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
if (repoIds.length === 0) return []
const groupedCounts = await prisma.optimization_events.groupBy({
by: ["current_username"],
where: {
AND: [
{
OR: [
{ user_id: userId },
{
repository_id: {
in: repoIds,
// Get the repository IDs the user has access to or has optimization events in
const memberRepos = await prisma.repositories.findMany({
where: {
OR: [
{
repository_members: {
some: {
user_id: userId,
},
},
],
},
{
created_at: {
gte: since,
},
},
{
current_username: {
not: null,
{
is_private: false,
optimization_events: {
some: {
current_username: username,
},
},
},
},
],
},
_count: {
id: true,
},
orderBy: {
_count: {
id: "desc",
],
},
},
})
select: {
id: true,
},
})
const repoIds = memberRepos.map(r => r.id)
return groupedCounts.map(entry => ({
username: entry.current_username!,
eventCount: entry._count.id,
avatarUrl: `https://github.com/${entry.current_username}.png`,
}))
if (repoIds.length === 0) return []
const groupedCounts = await prisma.optimization_events.groupBy({
by: ["current_username"],
where: {
AND: [
{
OR: [
{ user_id: userId },
{
repository_id: {
in: repoIds,
},
},
],
},
{
created_at: {
gte: since,
},
},
{
current_username: {
not: null,
},
},
],
},
_count: {
id: true,
},
orderBy: {
_count: {
id: "desc",
},
},
})
return groupedCounts.map(entry => ({
username: entry.current_username!,
eventCount: entry._count.id,
avatarUrl: `https://github.com/${entry.current_username}.png`,
}))
} catch (error) {
console.error("Failed to fetch active user leaderboard:", error)
throw new Error(
`Failed to fetch leaderboard data: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}

View file

@ -8,7 +8,7 @@ import {
TableRow,
} from "@/components/ui/table"
import ReactMarkdown from "react-markdown"
import { DeleteApiKeyButton } from "@/app/app/apikeys/delete-api-key-button"
import { DeleteApiKeyButton } from "./delete-api-key-button"
import React, { type JSX } from "react"
import {
Dialog,
@ -22,7 +22,7 @@ import {
import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button"
import { type cf_api_keys } from "@prisma/client"
import { deleteAPIKey } from "@/app/app/apikeys/tokenfuncs"
import { deleteAPIKey } from "./tokenfuncs"
import { Badge } from "@/components/ui/badge"
export function ApiKeyTable({ apiKeys }: { apiKeys: cf_api_keys[] }): JSX.Element {

View file

@ -19,7 +19,7 @@ import * as z from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { useToast } from "@/components/ui/use-toast"
import React from "react"
import { generateToken } from "@/app/app/apikeys/tokenfuncs"
import { generateToken } from "./tokenfuncs"
import { Plus } from "lucide-react"
const formSchema = z.object({

View file

@ -1,11 +1,11 @@
"use server"
import { getSession } from "@auth0/nextjs-auth0"
import { redirect } from "next/navigation"
import { CreateApiKeyDialog } from "@/app/app/apikeys/dialog-create-api-key"
import { CreateApiKeyDialog } from "./dialog-create-api-key"
import { Separator } from "@/components/ui/separator"
import { ApiKeyTable } from "@/app/app/apikeys/api-key-table"
import { ApiKeyTable } from "./api-key-table"
import { type cf_api_keys, PrismaClient } from "@prisma/client"
import PostHogClient from "@/app/app/posthog"
import PostHogClient from "@/lib/posthog"
import { ApiKeysClient } from "./api-keys-client"
const prisma = new PrismaClient()

View file

@ -3,7 +3,7 @@ import { getSession } from "@auth0/nextjs-auth0"
import { redirect } from "next/navigation"
import { PrismaClient } from "@prisma/client"
import { BillingView } from "./billing-view"
import PostHogClient from "@/app/app/posthog"
import PostHogClient from "@/lib/posthog"
import { SUBSCRIPTION_PLANS } from "@codeflash-ai/common"
const prisma = new PrismaClient()

View file

@ -2,7 +2,7 @@
import { useState, useRef, useEffect } from "react"
import OnboardingFlow from "@/components/onboarding/onboarding-flow"
import INTEGRATION_CONFIG from "@/app/onboarding/config"
import INTEGRATION_CONFIG from "@/app/(auth)/onboarding/config"
import { CompletionDialog } from "@/components/onboarding/completion-dialog"
import { ConfettiEffect } from "@/components/onboarding/confetti-effect"

View file

@ -0,0 +1,24 @@
import { getSession } from "@auth0/nextjs-auth0"
import { redirect } from "next/navigation"
import PostHogClient from "@/lib/posthog"
import GettingStartedClient from "./getting-started-client"
export default async function GettingStarted() {
const session = await getSession()
if (session == null) {
redirect("/login")
}
// Server-side tracking
const userId = session.user.sub
const posthog = PostHogClient()
posthog.capture({
distinctId: userId,
properties: { username: session.nickname },
event: "webapp-loaded-getting-started",
})
await posthog.shutdown()
return <GettingStartedClient />
}

View file

@ -0,0 +1,32 @@
import { getSession } from "@auth0/nextjs-auth0"
import { redirect } from "next/navigation"
import { ReactNode } from "react"
import { hasCompletedOnboarding } from "@codeflash-ai/common"
import { Sidebar } from "@/components/dashboard/sidebar"
/**
* Dashboard layout with authentication and sidebar
* Applied to all routes in the (dashboard) group
*/
export default async function DashboardLayout({ children }: { children: ReactNode }) {
const session = await getSession()
if (!session) {
redirect("/login")
}
const completedOnboarding = await hasCompletedOnboarding(session.user.sub)
if (!completedOnboarding) {
redirect("/onboarding")
}
return (
<div className="flex h-screen">
<Sidebar
className="h-screen border-r border-border/30 flex-shrink-0 w-60 bg-background"
user={session.user}
/>
<main className="flex-1 overflow-y-auto p-6">{children}</main>
</div>
)
}

View file

@ -15,7 +15,7 @@ import {
import Image from "next/image"
import { Card } from "@/components/ui/card"
import { getUserIdAndUsername } from "@/app/utils/auth"
import { RepositoryWithUsage, getAllRepositories } from "../home/action"
import { RepositoryWithUsage, getAllRepositories } from "../action"
// Error Boundary Component
class RepositoryErrorBoundary extends React.Component<
@ -646,7 +646,7 @@ function RepositoriesPage() {
// Update last auth check time
localStorage.setItem("lastAuthCheck", now.toString())
}, [])
}, [fetchRepositories])
// Refresh Button Component
const RefreshButton = () => (

View file

@ -1,5 +1,3 @@
// app/api/auth/[auth0]/route.js
import {
type AfterCallbackAppRoute,
type AppRouteHandlerFnContext,
@ -11,26 +9,89 @@ import {
type Session,
} from "@auth0/nextjs-auth0"
import { type NextRequest, NextResponse } from "next/server"
import { createOrUpdateUser, hasCompletedOnboarding } from "@codeflash-ai/common"
import { trackUserLogin } from "@/lib/analytics/tracking"
import { cookies } from "next/headers"
const afterCallback: AfterCallbackAppRoute = (req: NextRequest, session: Session) => {
if (session.user != null) {
// THIS IS THE KEY CHANGE - Your afterCallback was empty!
const afterCallback: AfterCallbackAppRoute = async (req: NextRequest, session: Session) => {
if (!session.user) {
return session
}
const user = session.user
console.log(`[Auth] Processing login for user: ${user.sub}`)
if (!user.sub || !user.nickname) {
console.error("[Auth] Missing required user fields")
return session
}
try {
// 1. SAVE TO DATABASE (moved from login page!)
console.log("[Auth] Saving user to database...")
await createOrUpdateUser(user.sub, user.nickname, user.email ?? null, user.name ?? null)
console.log("[Auth] User saved successfully")
// 2. TRACK LOGIN (moved from login page!)
await trackUserLogin({
userId: user.sub,
username: user.nickname,
email: user.email,
name: user.name,
})
// 3. CHECK ONBOARDING (moved from login page!)
const completedOnboarding = await hasCompletedOnboarding(user.sub)
console.log(`[Auth] Onboarding completed: ${completedOnboarding}`)
// 4. Decide where to redirect
let intendedDestination = "/dashboard"
// Try to get returnTo from state
const url = new URL(req.url)
const stateParam = url.searchParams.get("state")
if (stateParam) {
try {
const state = JSON.parse(Buffer.from(stateParam, "base64").toString("utf-8"))
if (state.returnTo) {
intendedDestination = state.returnTo
}
} catch (e) {
console.warn("[Auth] Failed to parse state")
}
}
// Handle onboarding redirect
if (!completedOnboarding) {
session.returnTo = "/onboarding"
} else {
session.returnTo = intendedDestination
}
} catch (error) {
console.error("[Auth] Error in afterCallback:", error)
// Don't fail login even if our processing fails
session.returnTo = "/dashboard"
}
return session
}
// Rest of your file stays mostly the same...
export const GET = handleAuth({
// eslint-disable-next-line
// Your existing login handler...
login: async (request: any, response: any) => {
console.log("Logging in")
try {
return await handleLogin(request as NextRequest, response as AppRouteHandlerFnContext, {
returnTo: "/app",
returnTo: "/dashboard",
})
} catch (error) {
console.log("Error logging in:", error)
}
},
// eslint-disable-next-line
// Your existing logout handler...
logout: async (request: any, response: any) => {
console.log("Logging out")
try {
@ -41,40 +102,29 @@ export const GET = handleAuth({
console.log("Error logging out:", error)
}
},
// eslint-disable-next-line
// Updated callback handler
callback: async (req: any, res: any) => {
try {
const response = (await handleCallback(req as NextRequest, res as AppRouteHandlerFnContext, {
afterCallback,
afterCallback, // NOW THIS DOES SOMETHING!
})) as NextResponse
const session = await getSession(req as NextRequest, response)
// Get returnTo URL from the state if available
const url = new URL(req.url)
const stateParam = url.searchParams.get("state")
let returnTo = "/app"
try {
if (stateParam) {
const state = JSON.parse(Buffer.from(stateParam, "base64").toString("utf-8"))
if (state.returnTo) {
returnTo = state.returnTo
console.log(returnTo)
}
}
} catch (e) {
console.warn("Failed to parse returnTo from state", e)
}
if (session != null) {
// Use the returnTo set by afterCallback
const returnTo = session.returnTo || "/dashboard"
const isAbsolute = returnTo.includes(process.env.AUTH0_BASE_URL ?? "")
const redirectUrl = isAbsolute ? returnTo : `${process.env.AUTH0_BASE_URL}${returnTo}`
return NextResponse.redirect(redirectUrl, response)
} else {
// TODO: Unsure where this should redirect to
return NextResponse.redirect(`${process.env.AUTH0_BASE_URL}/waitlist`, response)
} // eslint-disable-next-line
}
} catch (error: any) {
console.log("Error in callback:", error)
// Your existing error handling...
if (error.status === 400 && error.message.search("allowlist-fail") !== -1) {
// write regex parsing and group extraction for the following regex allowlist-fail\s(.*)\s(.*)\)
const re = /allowlist-fail\s(.*)\s(.*)\)/
const match = error.message.match(re)
if (match != null) {
@ -87,6 +137,4 @@ export const GET = handleAuth({
}
}
},
// TODO: Add callback for the waitlist if someone is not on the allowlist
})

View file

@ -0,0 +1,10 @@
import { redirect } from "next/navigation"
/**
* Catch-all route for legacy /app/* URLs
* Redirects to the corresponding route without the /app prefix
*/
export default function LegacyAppCatchAll({ params }: { params: { slug: string[] } }) {
const newPath = `/${params.slug.join("/")}`
redirect(newPath)
}

View file

@ -1,24 +1,9 @@
import { getSession } from "@auth0/nextjs-auth0"
import { redirect } from "next/navigation"
import PostHogClient from "@/app/app/posthog"
import GettingStartedClient from "./getting-started-client"
export default async function GettingStarted() {
const session = await getSession()
if (session == null) {
redirect("/login")
}
// Server-side tracking
const userId = session.user.sub
const posthog = PostHogClient()
posthog.capture({
distinctId: userId,
properties: { username: session.nickname },
event: "webapp-loaded-getting-started",
})
await posthog.shutdown()
return <GettingStartedClient />
/**
* Legacy /app/gettingstarted route for external links compatibility
* Redirects to the correct getting-started route
*/
export default function LegacyGettingStartedPage() {
redirect("/getting-started")
}

View file

@ -1,42 +0,0 @@
"use client"
import { Sidebar } from "@/components/dashboard/sidebar"
import { type JSX, useState, useEffect } from "react"
import { useUser } from "@auth0/nextjs-auth0/client"
export default function AppRootLayout({ children }: { children: React.ReactNode }): JSX.Element {
const { user, error, isLoading } = useUser()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [theme, setTheme] = useState<"light" | "dark">("light")
// Initialize theme from localStorage or system preference
useEffect(() => {
const savedTheme = localStorage.getItem("theme") as "light" | "dark" | null
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
if (savedTheme) {
setTheme(savedTheme)
document.documentElement.classList.toggle("dark", savedTheme === "dark")
} else if (prefersDark) {
setTheme("dark")
document.documentElement.classList.add("dark")
}
}, [])
return (
<>
<div className="flex bg-background text-foreground">
<div className="max-w-xs">
<Sidebar
className="h-screen border-r border-border/30 fixed top-0 left-0 w-60"
user={user!}
isLoading={isLoading}
error={error}
/>
</div>
{/* Page Content */}
<div className="px-6 py-6 relative left-60 w-[calc(100%-240px)]">{children}</div>
</div>
</>
)
}

View file

@ -1,29 +1,18 @@
"use client"
import { redirect } from "next/navigation"
import { useEffect } from "react"
import { usePostHog } from "posthog-js/react"
import { useUser } from "@auth0/nextjs-auth0/client"
import { getSession } from "@auth0/nextjs-auth0"
export default function AppHomePage(): void {
const posthog = usePostHog()
const session = useUser()
useEffect(() => {
if (session?.user?.sub) {
posthog?.identify(session.user.sub, {
username: session.user.nickname,
name: session.user.name,
email: session.user.email,
})
// posthog?.group("company", user.company_id)
redirect("/app/apikeys")
} else {
redirect("/login")
}
}, [
posthog,
session?.user?.email,
session?.user?.name,
session?.user?.nickname,
session?.user?.sub,
])
/**
* Legacy /app route for external links compatibility
* Redirects to the appropriate route based on authentication status
*/
export default async function LegacyAppPage() {
const session = await getSession()
if (session) {
// Authenticated users go to the dashboard (root)
redirect("/")
} else {
// Non-authenticated users go to login
redirect("/login")
}
}

View file

@ -0,0 +1,373 @@
"use server"
import {
prisma,
getActiveRepoCountLast30Days,
getOptimizationEventCountByRepoAndUser,
getOptimizationEventCountByUserId,
} from "@codeflash-ai/common"
import { eachDayOfInterval, startOfDay } from "date-fns"
export interface RepositoryWithUsage {
id: string
github_repo_id: string
name: string
full_name: string
is_private: boolean
is_active: boolean
has_github_action: boolean
created_at: Date
last_optimized: Date | null
optimizations_limit: number | null
optimizations_used: number
organization: string
membersCount?: number // Count from repository_members
avatarUrl?: string
}
export async function getAllRepositories(
userId: string,
username: string,
): Promise<RepositoryWithUsage[]> {
try {
// Validate inputs
if (!userId || !username) {
console.error("Invalid userId or username provided:", { userId, username })
throw new Error("Authentication required: Invalid user credentials")
}
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
const repositories = await prisma.repositories.findMany({
where: {
OR: [
{
repository_members: {
some: {
user_id: userId,
},
},
},
{
optimization_events: {
some: {
current_username: username,
},
},
},
],
},
})
// For each repo, check if it has any optimization event in the last 30 days
const reposWithActiveFlag = await Promise.all(
repositories.map(async repo => {
try {
const recentEventCount = await prisma.optimization_events.count({
where: {
repository_id: repo.id,
created_at: {
gte: thirtyDaysAgo,
},
},
})
return {
id: repo.id,
github_repo_id: repo.github_repo_id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.is_private,
is_active: recentEventCount > 0,
has_github_action: repo.has_github_action,
created_at: repo.created_at,
last_optimized: repo.last_optimized,
optimizations_limit: repo.optimizations_limit,
optimizations_used: repo.optimizations_used,
organization: repo.full_name.split("/")[0],
avatarUrl: `https://github.com/${repo.full_name.split("/")[0]}.png`,
}
} catch (repoError) {
console.error(`Failed to process repository ${repo.id}:`, repoError)
// Return basic repo data on individual repo processing error
return {
id: repo.id,
github_repo_id: repo.github_repo_id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.is_private,
is_active: false,
has_github_action: repo.has_github_action,
created_at: repo.created_at,
last_optimized: repo.last_optimized,
optimizations_limit: repo.optimizations_limit,
optimizations_used: repo.optimizations_used,
organization: repo.full_name.split("/")[0],
avatarUrl: `https://github.com/${repo.full_name.split("/")[0]}.png`,
}
}
}),
)
return reposWithActiveFlag
} catch (error) {
console.error("Failed to fetch repositories:", error)
throw new Error(`Database error: ${error instanceof Error ? error.message : "Unknown error"}`)
}
}
// New wrapper functions that call the imported functions
export async function getUserOptimizationCount(userId: string, username: string) {
try {
return await getOptimizationEventCountByUserId(userId, username)
} catch (error) {
console.error("Failed to get user optimization count:", error)
throw new Error(
`Failed to fetch optimization count: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getUserOptimizationSuccessfulCount(userId: string, username: string) {
try {
return await prisma.optimization_events.count({
where: {
is_optimization_found: true,
OR: [{ user_id: userId }, { current_username: username }],
},
})
} catch (error) {
console.error("Failed to get successful optimization count:", error)
throw new Error(
`Failed to fetch successful optimization count: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getRepoUserOptimizationCount(
repoId: string,
userId: string,
username: string,
) {
return getOptimizationEventCountByRepoAndUser(repoId, userId, username)
}
export async function getActiveRepositoriesLast30Days(userId: string, username: string) {
try {
return await getActiveRepoCountLast30Days(userId, username)
} catch (error) {
console.error("Failed to get active repositories count:", error)
throw new Error(
`Failed to fetch active repositories: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getOptimizationsTimeSeriesData(
userId: string,
username: string,
onlySuccessful?: boolean,
) {
try {
if (!userId || !username) {
throw new Error("Invalid user credentials")
}
const data = await prisma.optimization_events.findMany({
where: {
OR: [{ user_id: userId }, { current_username: username }],
...(onlySuccessful === true ? { is_optimization_found: true } : {}),
},
select: {
created_at: true,
},
})
const groupedByDay: Record<string, number> = {}
data.forEach(item => {
// Use the user's local time zone to format the date as YYYY-MM-DD
const day = item.created_at
.toLocaleDateString(undefined, {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
})
.replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$1-$2") // Convert MM/DD/YYYY to YYYY-MM-DD
groupedByDay[day] = (groupedByDay[day] || 0) + 1
})
const allDates = eachDayOfInterval({
start: new Date(Object.keys(groupedByDay).sort()[0]),
end: startOfDay(new Date()),
}).map(d =>
d
.toLocaleDateString(undefined, {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
})
.replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$1-$2"),
)
let cumulativeCount = 0
const completeData = allDates.map(date => {
cumulativeCount += groupedByDay[date] || 0
return { date, count: cumulativeCount }
})
return completeData
} catch (error) {
console.error("Failed to fetch optimization time series data:", error)
throw new Error(
`Failed to fetch time series data: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getPullRequestEventTimeSeriesData(
userId: string,
username: string,
year: number,
) {
try {
if (!userId || !username || !year) {
throw new Error("Invalid parameters provided")
}
const eventTypes = ["pr_created", "pr_merged", "pr_closed"]
const data = await prisma.optimization_events.findMany({
where: {
OR: [{ user_id: userId }, { current_username: username }],
event_type: { in: eventTypes },
created_at: {
gte: new Date(`${year}-01-01T00:00:00.000Z`),
lt: new Date(`${year + 1}-01-01T00:00:00.000Z`),
},
},
select: {
event_type: true,
created_at: true,
},
})
const groupedByMonth: Record<string, Record<string, number>> = {}
// Initialize the months of the year
for (let month = 1; month <= 12; month++) {
const monthKey = `${year}-${month.toString().padStart(2, "0")}`
groupedByMonth[monthKey] = { pr_created: 0, pr_merged: 0, pr_closed: 0 }
}
data.forEach(item => {
const month = item.created_at.getMonth() + 1 // JavaScript months are 0-indexed
const monthKey = `${year}-${month.toString().padStart(2, "0")}`
if (groupedByMonth[monthKey]) {
groupedByMonth[monthKey][item.event_type] += 1
}
})
const completeData = Object.keys(groupedByMonth).map(monthKey => ({
month: monthKey,
pr_created: groupedByMonth[monthKey].pr_created,
pr_merged: groupedByMonth[monthKey].pr_merged,
pr_closed: groupedByMonth[monthKey].pr_closed,
}))
return completeData
} catch (error) {
console.error("Failed to fetch pull request event time series data:", error)
throw new Error(
`Failed to fetch PR time series data: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getActiveUserLeaderboardLast30Days(
userId: string,
username: string,
): Promise<{ username: string; eventCount: number; avatarUrl: string }[]> {
try {
if (!userId || !username) {
throw new Error("Invalid user credentials")
}
const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
// Get the repository IDs the user has access to or has optimization events in
const memberRepos = await prisma.repositories.findMany({
where: {
OR: [
{
repository_members: {
some: {
user_id: userId,
},
},
},
{
is_private: false,
optimization_events: {
some: {
current_username: username,
},
},
},
],
},
select: {
id: true,
},
})
const repoIds = memberRepos.map(r => r.id)
if (repoIds.length === 0) return []
const groupedCounts = await prisma.optimization_events.groupBy({
by: ["current_username"],
where: {
AND: [
{
OR: [
{ user_id: userId },
{
repository_id: {
in: repoIds,
},
},
],
},
{
created_at: {
gte: since,
},
},
{
current_username: {
not: null,
},
},
],
},
_count: {
id: true,
},
orderBy: {
_count: {
id: "desc",
},
},
})
return groupedCounts.map(entry => ({
username: entry.current_username!,
eventCount: entry._count.id,
avatarUrl: `https://github.com/${entry.current_username}.png`,
}))
} catch (error) {
console.error("Failed to fetch active user leaderboard:", error)
throw new Error(
`Failed to fetch leaderboard data: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}

View file

@ -0,0 +1,32 @@
import { getSession } from "@auth0/nextjs-auth0"
import { redirect } from "next/navigation"
import { ReactNode } from "react"
import { hasCompletedOnboarding } from "@codeflash-ai/common"
import { Sidebar } from "@/components/dashboard/sidebar"
/**
* Dashboard layout with authentication and sidebar
* Applied to /dashboard route
*/
export default async function DashboardLayout({ children }: { children: ReactNode }) {
const session = await getSession()
if (!session) {
redirect("/login")
}
const completedOnboarding = await hasCompletedOnboarding(session.user.sub)
if (!completedOnboarding) {
redirect("/onboarding")
}
return (
<div className="flex h-screen">
<Sidebar
className="h-screen border-r border-border/30 flex-shrink-0 w-60 bg-background"
user={session.user}
/>
<main className="flex-1 overflow-y-auto p-6">{children}</main>
</div>
)
}

View file

@ -1106,28 +1106,8 @@ function Dashboard() {
}, [])
useEffect(() => {
// Check if user was recently authenticated
const lastAuthCheck = localStorage.getItem("lastAuthCheck")
const now = Date.now()
// If last auth check was less than 2 seconds ago, wait a bit
if (lastAuthCheck && now - parseInt(lastAuthCheck) < 2000) {
const delay = 2000 - (now - parseInt(lastAuthCheck))
setTimeout(() => {
fetchDashboardData()
}, delay)
} else {
// Add a small delay to prevent race conditions on rapid refreshes
const timeoutId = setTimeout(() => {
fetchDashboardData()
}, 100)
const cleanup = () => clearTimeout(timeoutId)
return cleanup
}
// Update last auth check time
localStorage.setItem("lastAuthCheck", now.toString())
// Simple initialization without localStorage timing checks
fetchDashboardData()
}, [fetchDashboardData])
// Calculate metrics for repository summary with safety checks

View file

@ -0,0 +1,42 @@
"use client"
import { useEffect } from "react"
import { RefreshCw } from "lucide-react"
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log the error to Sentry
console.error("Application Error:", error)
}, [error])
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="max-w-md w-full mx-auto p-6">
<div className="bg-red-50 border border-red-200 rounded-xl p-6 text-center">
<div className="w-12 h-12 mx-auto mb-4 bg-red-100 rounded-full flex items-center justify-center">
<RefreshCw className="w-6 h-6 text-red-600" />
</div>
<h2 className="text-lg font-semibold text-red-800 mb-2">Something went wrong!</h2>
<p className="text-red-600 mb-4 text-sm">
{process.env.NODE_ENV === "development"
? error.message
: "An unexpected error occurred. Please try again."}
</p>
{error.digest && <p className="text-xs text-red-500 mb-4">Error ID: {error.digest}</p>}
<button
onClick={reset}
className="w-full bg-red-100 hover:bg-red-200 text-red-800 font-medium py-2 px-4 rounded-lg transition-colors"
>
Try again
</button>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,48 @@
"use client"
import { useEffect } from "react"
import { RefreshCw } from "lucide-react"
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log the error to Sentry
console.error("Global Application Error:", error)
}, [error])
return (
<html>
<body>
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="max-w-md w-full mx-auto p-6">
<div className="bg-red-50 border border-red-200 rounded-xl p-6 text-center">
<div className="w-12 h-12 mx-auto mb-4 bg-red-100 rounded-full flex items-center justify-center">
<RefreshCw className="w-6 h-6 text-red-600" />
</div>
<h2 className="text-lg font-semibold text-red-800 mb-2">Application Error</h2>
<p className="text-red-600 mb-4 text-sm">
{process.env.NODE_ENV === "development"
? error.message
: "A critical error occurred. Please refresh the page."}
</p>
{error.digest && (
<p className="text-xs text-red-500 mb-4">Error ID: {error.digest}</p>
)}
<button
onClick={reset}
className="w-full bg-red-100 hover:bg-red-200 text-red-800 font-medium py-2 px-4 rounded-lg transition-colors"
>
Refresh Page
</button>
</div>
</div>
</div>
</body>
</html>
)
}

View file

@ -1,85 +0,0 @@
"use server"
import { redirect } from "next/navigation"
import { getSession } from "@auth0/nextjs-auth0"
import { type JSX } from "react"
import { createOrUpdateUser, hasCompletedOnboarding } from "@codeflash-ai/common"
import PostHogClient from "@/app/app/posthog"
import { cookies } from "next/headers"
// export const metadata: Metadata = {
// title: 'Codeflash login',
// description: 'The login page for Codeflash webapp'
// }
// Security function to validate returnTo URLs
function isValidReturnUrl(url: string): boolean {
// Only allow relative URLs or your domains
if (url.startsWith("/")) {
return true
}
return false // Reject external URLs
}
export default async function AuthenticationPage({
searchParams,
}: {
searchParams: { returnTo?: string }
}): Promise<JSX.Element> {
const session = await getSession()
if (session == null) {
const returnTo = searchParams.returnTo
if (returnTo && isValidReturnUrl(returnTo)) {
redirect(`/api/auth/login?returnTo=${encodeURIComponent(returnTo)}`)
} else {
redirect("/api/auth/login")
}
} else {
const user = session.user
console.log("user logged in!", user)
if (!(user.sub && user.nickname)) {
console.error("user.sub or user.nickname is null")
} else {
await createOrUpdateUser(user.sub, user.nickname, user.email ?? null, user.name ?? null)
const posthog = PostHogClient()
posthog.identify({
distinctId: user.sub,
properties: {
username: user.nickname,
email: user.email,
name: session?.user?.name,
},
})
posthog.capture({
distinctId: user.sub,
properties: { username: user.nickname, email: user.email },
event: "webapp-user-logged-in",
})
await posthog.shutdown()
}
// Get and validate returnTo parameter
const returnTo = searchParams.returnTo
const safeReturnTo = returnTo && isValidReturnUrl(returnTo) ? returnTo : null
const completedOnboarding = await hasCompletedOnboarding(user.sub)
if (!completedOnboarding) {
// Save the returnTo URL for after onboarding
if (safeReturnTo) {
cookies().set("returnAfterOnboarding", safeReturnTo, {
path: "/",
httpOnly: true,
sameSite: "lax",
})
}
console.log("user has not completed onboarding, redirecting")
redirect("/onboarding")
// redirect("https://vemx8ac1f86.typeform.com/to/GJhrpTZH")
} else if (safeReturnTo) {
// User has completed onboarding, redirect to original destination
console.log(`user has completed onboarding, redirecting to ${safeReturnTo}`)
redirect(safeReturnTo)
} else {
console.log("user has completed onboarding, redirecting to /app")
redirect("/app")
}
}
}

View file

@ -1,9 +1,35 @@
"use client"
import { getSession } from "@auth0/nextjs-auth0"
import { redirect } from "next/navigation"
import { useEffect } from "react"
import { hasCompletedOnboarding } from "@codeflash-ai/common"
import { Sidebar } from "@/components/dashboard/sidebar"
import DashboardPage from "./dashboard/page"
export default function TopRedirectPage(): void {
useEffect(() => {
/**
* Root page that serves dashboard with proper authentication and layout
* This ensures the sidebar shows up on the root page
*/
export default async function RootPage() {
const session = await getSession()
if (!session) {
redirect("/login")
}, [])
}
const completedOnboarding = await hasCompletedOnboarding(session.user.sub)
if (!completedOnboarding) {
redirect("/onboarding")
}
// Render dashboard with sidebar (same as dashboard layout)
return (
<div className="flex h-screen">
<Sidebar
className="h-screen border-r border-border/30 flex-shrink-0 w-60 bg-background"
user={session.user}
/>
<main className="flex-1 overflow-y-auto p-6">
<DashboardPage />
</main>
</div>
)
}

View file

@ -41,6 +41,6 @@ export default async function ProSubscribePage({ searchParams }: { searchParams:
redirect(checkoutUrl)
} catch (error) {
Sentry.captureException(error)
redirect("/app/billing?error=checkout_failed")
redirect("/billing?error=checkout_failed")
}
}

View file

@ -1,8 +1,10 @@
import { PrismaClient } from "@prisma/client"
import { notFound } from "next/navigation"
import { notFound, redirect } from "next/navigation"
import { ExperimentMetadata } from "@/lib/types" // Your defined types
import MonacoDiffViewer from "@/components/trace/monaco-diff-viewer" // The client component
import { Metadata } from "next" // For Next.js metadata API
import { getSession } from "@auth0/nextjs-auth0"
import { isTeamMember } from "@/app/utils/auth"
interface TraceDetailsPageProps {
params: {
@ -56,6 +58,58 @@ export default async function TraceDetailsPage({ params }: TraceDetailsPageProps
notFound()
}
// Check authentication - user must be logged in
const session = await getSession()
if (!session?.user) {
redirect(`/login?returnTo=${encodeURIComponent(`/trace/${trace_id}`)}`)
}
// Check team member access - only team members can view traces
const hasTeamAccess = await isTeamMember()
if (!hasTeamAccess) {
// Create a custom access denied page or redirect to a generic error
return (
<div className="flex flex-col items-center justify-center h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-200">
<div className="text-center max-w-md">
<div className="mb-6">
<svg
className="w-16 h-16 mx-auto text-red-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
<h1 className="text-2xl font-bold text-red-400 mb-4">Access Denied</h1>
<p className="text-slate-300 mb-6">
This trace is restricted to CodeFlash team members only.
</p>
<div className="text-sm text-slate-400 mb-6">
<p>
Logged in as:{" "}
<span className="font-mono">{session.user.email || session.user.nickname}</span>
</p>
<p>
Trace ID: <span className="font-mono">{trace_id}</span>
</p>
</div>
<a
href="/app"
className="inline-block bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition-colors"
>
Go to Dashboard
</a>
</div>
</div>
)
}
let optimizationFeature: {
experiment_metadata: unknown
metadata: unknown

View file

@ -5,7 +5,64 @@ export async function getUserId(): Promise<string | null> {
const session = await getSession()
return session?.user?.sub || null
}
export async function getUserIdAndUsername(): Promise<{ username: string; userId: string }> {
const session = await getSession()
return { userId: session?.user?.sub, username: session?.user?.nickname }
if (!session?.user?.sub || !session?.user?.nickname) {
throw new Error("User session not found or incomplete")
}
return {
userId: session.user.sub,
username: session.user.nickname,
}
}
// Define CodeFlash team members by email or GitHub username
const TEAM_MEMBERS = [
// Email addresses
"sarthak.saga@gmail.com",
"sarthak@codeflash.ai",
"team@codeflash.ai",
"saurabh@codeflash.ai",
"hesham@codeflash.ai",
"aseem@codeflash.ai",
"ali@codeflash.ai",
"turcioskevinr@gmail.com",
// GitHub usernames (Auth0 nickname field)
"saga4",
"sarthaksaga",
"codeflash-ai",
// Add more team members here as needed
]
export async function isTeamMember(): Promise<boolean> {
const session = await getSession()
if (!session?.user) {
return false
}
const email = session.user.email?.toLowerCase()
const nickname = session.user.nickname?.toLowerCase()
// Check if user's email or nickname matches team members list
return TEAM_MEMBERS.some(member => {
const memberLower = member.toLowerCase()
return (email && email === memberLower) || (nickname && nickname === memberLower)
})
}
export async function requireTeamMember(): Promise<void> {
const session = await getSession()
if (!session?.user) {
throw new Error("Authentication required")
}
const isTeam = await isTeamMember()
if (!isTeam) {
throw new Error("Access denied: Team member access required")
}
}

View file

@ -74,42 +74,42 @@ export function Sidebar({ className, user, isLoading, error }: SidebarProps): JS
}
return (
<div className={cn("flex flex-col h-screen pt-3 pb-6 max-w-xs", className)}>
<div className={cn("flex flex-col h-screen pt-3 pb-6 w-60 bg-background", className)}>
<LogoBox />
<div className="space-y-4 py-4 grow">
<div className="px-3 py-2">
<div className="space-y-2 grid gap-y-1">
<Link href="/app/home">
<Link href="/dashboard">
<Button
variant={currentRoute === "/app/home" ? "secondary" : "ghost"}
variant={currentRoute === "/dashboard" ? "secondary" : "ghost"}
className="w-full justify-start"
>
<Home size={16} className="mr-2 h-4 w-4" />
Home
Dashboard
</Button>
</Link>
<Link href="/app/repositories">
<Link href="/repositories">
<Button
variant={currentRoute === "/app/repositories" ? "secondary" : "ghost"}
variant={currentRoute === "/repositories" ? "secondary" : "ghost"}
className="w-full justify-start"
>
<FolderGit2 size={16} className="mr-2 h-4 w-4" />
Repositories
</Button>
</Link>
<Link href="/app/gettingstarted">
<Link href="/getting-started">
<Button
variant={currentRoute === "/app/gettingstarted" ? "secondary" : "ghost"}
variant={currentRoute === "/getting-started" ? "secondary" : "ghost"}
className="w-full justify-start"
>
<CirclePlay size={16} className="mr-2 h-4 w-4" />
Getting Started
</Button>
</Link>
<Link href="/app/apikeys">
<Link href="/apikeys">
<Button
variant={currentRoute === "/app/apikeys" ? "secondary" : "ghost"}
variant={currentRoute === "/apikeys" ? "secondary" : "ghost"}
className="w-full justify-start"
>
<KeyRound size={16} className="mr-2 h-4 w-4" />
@ -123,9 +123,9 @@ export function Sidebar({ className, user, isLoading, error }: SidebarProps): JS
Documentation
</Button>
</Link>
<Link href="/app/billing">
<Link href="/billing">
<Button
variant={currentRoute === "/app/billing" ? "secondary" : "ghost"}
variant={currentRoute === "/billing" ? "secondary" : "ghost"}
className="w-full justify-start"
>
<CreditCard size={16} className="mr-2 h-4 w-4" />

View file

@ -2,7 +2,7 @@
import { FC } from "react"
import { Card } from "@/components/ui/card"
import { IntegrationConfig, IntegrationMethod } from "@/app/onboarding/types"
import { IntegrationConfig, IntegrationMethod } from "@/app/(auth)/onboarding/types"
interface IntegrationCardProps {
method: IntegrationMethod

View file

@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"
import ProgressSteps from "@/components/onboarding/progress-steps"
import StepNavigation from "@/components/onboarding/step-navigation"
import LogoBox from "@/components/dashboard/logo-box"
import { ApiKeyState, StepConfig, StepsState } from "@/app/onboarding/types"
import { ApiKeyState, StepConfig, StepsState } from "@/app/(auth)/onboarding/types"
import { RenderStepContent } from "./step-content-renderer"
interface OnboardingFlowProps {

View file

@ -2,7 +2,7 @@
import { FC, useEffect, useRef, useState } from "react"
import { Check } from "lucide-react"
import { Step } from "@/app/onboarding/types"
import { Step } from "@/app/(auth)/onboarding/types"
interface ProgressStepsProps {
steps: Step[]

View file

@ -3,8 +3,8 @@ import { KeyRound, Copy, Check, Terminal, AlertCircle, ChevronLeft } from "lucid
import { Button } from "@/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useState } from "react"
import { ApiKeyState, StepConfig } from "@/app/onboarding/types"
import { generateToken } from "@/app/app/apikeys/tokenfuncs"
import { ApiKeyState, StepConfig } from "@/app/(auth)/onboarding/types"
import { generateToken } from "@/app/(dashboard)/apikeys/tokenfuncs"
// Custom API Key Card Component
export const ApiKeyCard = ({

View file

@ -16,6 +16,8 @@ import {
Save,
X,
Lock,
Monitor,
Smartphone,
} from "lucide-react"
// Ensure you have lucide-react installed as per your package.json
import { Loader2, FileText, AlertTriangle } from "lucide-react"
@ -38,6 +40,8 @@ const MonacoDiffViewer: React.FC<MonacoDiffViewerProps> = ({ metadata, repoFullN
const [currentEdit, setCurrentEdit] = useState<{ [key: string]: string }>({})
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle")
const [savedChanges, setSavedChanges] = useState<{ [key: string]: string }>({})
const [isMobile, setIsMobile] = useState(false)
const [useInlineView, setUseInlineView] = useState(false)
const isEditingRef = useRef(isEditing)
const activeFileKeyRef = useRef(activeFileKey)
@ -206,6 +210,19 @@ const MonacoDiffViewer: React.FC<MonacoDiffViewerProps> = ({ metadata, repoFullN
}
}, [fileKeys, activeFileKey])
// Mobile detection and responsive handler
useEffect(() => {
const checkMobile = () => {
const mobile = window.innerWidth < 768
setIsMobile(mobile)
setUseInlineView(mobile)
}
checkMobile()
window.addEventListener("resize", checkMobile)
return () => window.removeEventListener("resize", checkMobile)
}, [])
useEffect(() => {
if (monaco) {
// Define your custom dark theme for Monaco Editor
@ -443,33 +460,69 @@ const MonacoDiffViewer: React.FC<MonacoDiffViewerProps> = ({ metadata, repoFullN
{/* File Path/Tabs */}
{fileKeys.length === 1 ? (
// Single file - show full path
<div className="bg-[rgba(15,15,15,0.95)] border-b border-[rgba(255,255,255,0.05)] px-5 py-3 flex items-center gap-2">
<FileText size={14} className="text-sky-400" />
<span className="text-sm text-slate-300 font-mono">{fileKeys[0]}</span>
// Single file - show full path with view toggle
<div className="bg-[rgba(15,15,15,0.95)] border-b border-[rgba(255,255,255,0.05)] px-5 py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<FileText size={14} className="text-sky-400" />
<span className="text-sm text-slate-300 font-mono">{fileKeys[0]}</span>
</div>
{/* View Toggle for mobile compatibility */}
<div className="flex items-center gap-2">
<button
onClick={() => setUseInlineView(!useInlineView)}
className={`flex items-center gap-2 px-3 py-1 rounded-md text-xs transition-colors ${
useInlineView
? "bg-blue-600/20 text-blue-400 border border-blue-600/30"
: "bg-slate-700/50 text-slate-400 hover:text-slate-300"
}`}
title={useInlineView ? "Switch to side-by-side view" : "Switch to inline view"}
>
{useInlineView ? <Smartphone size={12} /> : <Monitor size={12} />}
{useInlineView ? "Inline" : "Side-by-side"}
</button>
</div>
</div>
) : (
// Multiple files - show tabs with full path on hover
<div className="bg-[rgba(15,15,15,0.95)] border-b border-[rgba(255,255,255,0.05)] flex overflow-x-auto whitespace-nowrap scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-slate-800">
{fileKeys.map(fileKey => (
<button
key={fileKey}
onClick={() => setActiveFileKey(fileKey)}
title={fileKey}
className={`px-5 py-3 text-sm transition-all duration-200 ease-in-out focus:outline-none flex items-center gap-2
${
activeFileKey === fileKey
? "text-white bg-[rgba(255,255,255,0.05)] border-b-2 border-sky-400"
: "text-slate-400 hover:text-slate-200 hover:bg-[rgba(255,255,255,0.03)] border-b-2 border-transparent"
<div className="bg-[rgba(15,15,15,0.95)] border-b border-[rgba(255,255,255,0.05)]">
<div className="flex items-center justify-between">
<div className="flex overflow-x-auto whitespace-nowrap scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-slate-800">
{fileKeys.map(fileKey => (
<button
key={fileKey}
onClick={() => setActiveFileKey(fileKey)}
title={fileKey}
className={`px-5 py-3 text-sm transition-all duration-200 ease-in-out focus:outline-none flex items-center gap-2
${
activeFileKey === fileKey
? "text-white bg-[rgba(255,255,255,0.05)] border-b-2 border-sky-400"
: "text-slate-400 hover:text-slate-200 hover:bg-[rgba(255,255,255,0.03)] border-b-2 border-transparent"
}`}
>
<FileText
size={14}
className={activeFileKey === fileKey ? "text-sky-400" : "text-slate-500"}
/>
{fileKey.split("/").pop()} {/* Show only filename */}
</button>
))}
</div>
{/* View Toggle for mobile compatibility */}
<div className="flex items-center gap-2 px-5">
<button
onClick={() => setUseInlineView(!useInlineView)}
className={`flex items-center gap-2 px-3 py-1 rounded-md text-xs transition-colors ${
useInlineView
? "bg-blue-600/20 text-blue-400 border border-blue-600/30"
: "bg-slate-700/50 text-slate-400 hover:text-slate-300"
}`}
>
<FileText
size={14}
className={activeFileKey === fileKey ? "text-sky-400" : "text-slate-500"}
/>
{fileKey.split("/").pop()} {/* Show only filename */}
</button>
))}
title={useInlineView ? "Switch to side-by-side view" : "Switch to inline view"}
>
{useInlineView ? <Smartphone size={12} /> : <Monitor size={12} />}
{useInlineView ? "Inline" : "Side-by-side"}
</button>
</div>
</div>
</div>
)}
@ -487,27 +540,34 @@ const MonacoDiffViewer: React.FC<MonacoDiffViewerProps> = ({ metadata, repoFullN
onMount={handleEditorOnMount}
options={{
automaticLayout: true,
renderSideBySide: true,
renderSideBySide: !useInlineView,
readOnly: false, // Allow editing on the modified side
scrollBeyondLastLine: false,
minimap: { enabled: true, scale: 1, size: "proportional" },
minimap: { enabled: !isMobile, scale: isMobile ? 0.5 : 1, size: "proportional" },
diffCodeLens: true,
renderIndicators: true,
ignoreTrimWhitespace: false,
renderWhitespace: "boundary",
fontSize: 13,
fontSize: isMobile ? 12 : 13,
fontFamily: "'Fira Code', 'Cascadia Code', Consolas, 'Courier New', monospace",
fontLigatures: true,
wordWrap: "off",
fontLigatures: !isMobile, // Disable font ligatures on mobile for better performance
wordWrap: isMobile ? "on" : "off", // Enable word wrap on mobile
scrollbar: {
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
verticalScrollbarSize: isMobile ? 14 : 10,
horizontalScrollbarSize: isMobile ? 14 : 10,
useShadows: false,
},
originalEditable: false, // Original side stays read-only
enableSplitViewResizing: true,
enableSplitViewResizing: !isMobile,
// Enable diff review mode for better editing experience
diffWordWrap: "off",
diffWordWrap: isMobile ? "on" : "off",
// Mobile-specific optimizations
mouseWheelZoom: !isMobile,
contextmenu: !isMobile,
quickSuggestions: !isMobile,
parameterHints: { enabled: !isMobile },
suggest: !isMobile ? {} : undefined,
hover: { enabled: !isMobile },
}}
loading={
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0A0A0A] text-slate-400">
@ -527,25 +587,32 @@ const MonacoDiffViewer: React.FC<MonacoDiffViewerProps> = ({ metadata, repoFullN
onMount={handleEditorOnMount}
options={{
automaticLayout: true,
renderSideBySide: true,
renderSideBySide: !useInlineView,
readOnly: true,
scrollBeyondLastLine: false,
minimap: { enabled: true, scale: 1, size: "proportional" },
minimap: { enabled: !isMobile, scale: isMobile ? 0.5 : 1, size: "proportional" },
diffCodeLens: true,
renderIndicators: true,
ignoreTrimWhitespace: false,
renderWhitespace: "boundary",
fontSize: 13,
fontSize: isMobile ? 12 : 13,
fontFamily: "'Fira Code', 'Cascadia Code', Consolas, 'Courier New', monospace",
fontLigatures: true,
wordWrap: "off",
fontLigatures: !isMobile, // Disable font ligatures on mobile for better performance
wordWrap: isMobile ? "on" : "off", // Enable word wrap on mobile
scrollbar: {
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
verticalScrollbarSize: isMobile ? 14 : 10,
horizontalScrollbarSize: isMobile ? 14 : 10,
useShadows: false,
},
originalEditable: false,
enableSplitViewResizing: true,
enableSplitViewResizing: !isMobile,
// Mobile-specific optimizations
mouseWheelZoom: !isMobile,
contextmenu: !isMobile,
quickSuggestions: false,
parameterHints: { enabled: false },
suggest: undefined,
hover: { enabled: !isMobile },
}}
loading={
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0A0A0A] text-slate-400">

View file

@ -2,7 +2,7 @@
import { FC } from "react"
import { Card } from "@/components/ui/card"
import { CopyButton } from "@/app/app/gettingstarted/copy-button"
import { CopyButton } from "@/app/(dashboard)/getting-started/copy-button"
interface TerminalCardProps {
command: string

View file

@ -1,13 +1,6 @@
import * as Sentry from "@sentry/nextjs"
export function register() {
Sentry.init({
dsn: "https://0fa0f40b2d709e4f1eb9aac76ff9e6be@o4506833230561280.ingest.us.sentry.io/4506833279582208",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
})
// Sentry initialization is now handled by dedicated config files:
// - sentry.server.config.ts for server-side
// - sentry.client.config.ts for client-side
// This prevents duplicate initialization issues with Sentry v9
}

View file

@ -0,0 +1,46 @@
import PostHogClient from "../posthog"
function getPostHogClient() {
return PostHogClient()
}
// Main tracking function for user login
export async function trackUserLogin(userData: {
userId: string
username: string
email?: string
name?: string
}) {
try {
const posthog = getPostHogClient()
// Identify the user
posthog.identify({
distinctId: userData.userId,
properties: {
username: userData.username,
email: userData.email,
name: userData.name,
},
})
// Track the login event
posthog.capture({
distinctId: userData.userId,
event: "webapp-user-logged-in",
properties: {
username: userData.username,
email: userData.email,
timestamp: new Date().toISOString(),
},
})
// Ensure events are sent
await posthog.shutdown()
console.log(`[Analytics] Tracked login for user ${userData.userId}`)
} catch (error) {
// Don't let tracking errors break login
console.error("[Analytics] Failed to track login:", error)
}
}

View file

@ -0,0 +1,22 @@
export async function fetchFromAPI(endpoint: string, options: RequestInit = {}) {
const apiKey = process.env.NEXT_PUBLIC_CF_API_KEY || localStorage.getItem("cf_api_key")
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"}${endpoint}`,
{
...options,
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey || "",
...options.headers,
},
},
)
if (!response.ok) {
const error = await response.json()
throw new Error(error.message || "API request failed")
}
return response.json()
}

View file

@ -0,0 +1,114 @@
import { PrismaClient } from "@prisma/client"
import { ExperimentMetadata } from "./types"
const prisma = new PrismaClient()
/**
* Get the modified code for a trace, falling back to original optimized code if no modifications exist
* @param traceId - The trace ID to get code for
* @param fileKey - Optional specific file key, if not provided returns all files
* @returns Modified code or original code if no modifications exist
*/
export async function getModifiedCodeForTrace(
traceId: string,
fileKey?: string,
): Promise<{ [fileKey: string]: string } | string | null> {
try {
const optimizationFeature = await prisma.optimization_features.findUnique({
where: { trace_id: traceId },
select: {
experiment_metadata: true,
metadata: true,
},
})
if (!optimizationFeature) {
return null
}
const experimentMetadata = optimizationFeature.experiment_metadata as ExperimentMetadata | null
const additionalMetadata = (optimizationFeature.metadata as any) || {}
// Get modified code from metadata if available
const modifiedCode = additionalMetadata.modifiedCode as { [key: string]: string } | undefined
if (fileKey) {
// Return specific file
if (modifiedCode && modifiedCode[fileKey]) {
return modifiedCode[fileKey]
}
// Fall back to original optimized code
if (experimentMetadata?.diffContents?.[fileKey]) {
return (
experimentMetadata.diffContents[fileKey].newContent ||
experimentMetadata.diffContents[fileKey].oldContent ||
""
)
}
return null
} else {
// Return all files
const result: { [fileKey: string]: string } = {}
// Start with original optimized code
if (experimentMetadata?.diffContents) {
Object.entries(experimentMetadata.diffContents).forEach(([key, diff]) => {
result[key] = diff.newContent || diff.oldContent || ""
})
}
// Override with modified code where available
if (modifiedCode) {
Object.entries(modifiedCode).forEach(([key, code]) => {
result[key] = code
})
}
return Object.keys(result).length > 0 ? result : null
}
} catch (error) {
console.error("Error getting modified code for trace:", error)
return null
}
}
/**
* Check if a trace has any modified code
* @param traceId - The trace ID to check
* @returns True if trace has modified code, false otherwise
*/
export async function hasModifiedCode(traceId: string): Promise<boolean> {
try {
const optimizationFeature = await prisma.optimization_features.findUnique({
where: { trace_id: traceId },
select: {
metadata: true,
},
})
if (!optimizationFeature) {
return false
}
const additionalMetadata = (optimizationFeature.metadata as any) || {}
const modifiedCode = additionalMetadata.modifiedCode as { [key: string]: string } | undefined
return !!(modifiedCode && Object.keys(modifiedCode).length > 0)
} catch (error) {
console.error("Error checking for modified code:", error)
return false
}
}
/**
* Get the most recent code for suggestions - modified code takes precedence over original
* This is the main function other services should use when they need code for suggestions
* @param traceId - The trace ID
* @returns Object with file paths as keys and code content as values
*/
export async function getCodeForSuggestions(
traceId: string,
): Promise<{ [fileKey: string]: string } | null> {
const code = await getModifiedCodeForTrace(traceId)
return typeof code === "object" ? code : null
}

View file

@ -7,4 +7,3 @@ export default function PostHogClient(): PostHog {
flushInterval: 0,
})
}
// "phc_aUO790jHd7z1SXwsYCz8dRApxueplZlZWeDSpKc5hol", { api_host: "https://app.posthog.com" }

View file

@ -0,0 +1,9 @@
import Stripe from "stripe"
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error("STRIPE_SECRET_KEY is not set in environment variables")
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2025-05-28.basil",
})

View file

@ -6,5 +6,5 @@ export default withMiddlewareAuthRequired({
})
export const config = {
matcher: "/app/:path*",
matcher: ["/app/:path*", "/trace/:path*"],
}