fix: harden cf-webapp security across auth, XSS, and headers

- Add auth0.getSession() to unauthenticated observability endpoints
  (llm-call-debug, llm-export, observability chat)
- Remove hardcoded JWT_SECRET fallback; require env var
- Sanitize markdown HTML with DOMPurify before innerHTML assignment
- Escape user data in Intercom boot snippet via JSON.stringify
- Add security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options,
  Referrer-Policy, Permissions-Policy) via next.config.mjs
- Move OAuth params from sessionStorage to signed HttpOnly cookie
- Add input validation: clamp page/pageSize bounds, allowlist sort fields
- Stop leaking error.message to clients in API responses
- Remove ~40 console.log/error statements that logged user IDs, org IDs,
  PKCE params, and OAuth flow details
- Delete unused api-client.ts (NEXT_PUBLIC_CF_API_KEY never imported)
This commit is contained in:
Kevin Turcios 2026-04-13 19:25:19 -05:00
parent 80d10762ff
commit 91b692c1a0
15 changed files with 226 additions and 188 deletions

View file

@ -21,8 +21,8 @@ STRIPE_PRO_PRICE_YEARLY_ID=
STRIPE_WEBHOOK_SECRET=
# Codeflash
NEXT_PUBLIC_CF_API_KEY=
API_TOKEN_LIMIT=4000
JWT_SECRET=
# Sentry (omit NEXT_PUBLIC_SENTRY_DISABLED to enable)
NEXT_PUBLIC_SENTRY_DISABLED=true

View file

@ -10,6 +10,37 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
/** @type {import("next").NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "Content-Security-Policy",
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://widget.intercom.io https://js.intercomcdn.com https://client.crisp.chat https://settings.crisp.chat",
"style-src 'self' 'unsafe-inline' https://client.crisp.chat",
"img-src 'self' data: blob: https://avatars.githubusercontent.com https://github.com https://*.intercomcdn.com https://*.crisp.chat https://image.crisp.chat",
"font-src 'self' data: https://client.crisp.chat",
"connect-src 'self' https://*.intercom.io https://api-iam.intercom.io wss://*.intercom.io https://*.crisp.chat wss://*.crisp.chat https://*.sentry.io https://*.ingest.us.sentry.io https://us.i.posthog.com https://us.posthog.com",
"frame-src 'self' https://intercom-sheets.com https://game.crisp.chat",
"media-src 'self' https://*.intercomcdn.com",
"worker-src 'self' blob:",
].join("; "),
},
],
},
]
},
cacheComponents: true,
cacheLife: {
dashboard: {

View file

@ -51,6 +51,7 @@
"clsx": "^2.0.0",
"date-fns": "^4.1.0",
"diff": "^8.0.2",
"dompurify": "^3.3.3",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^1.8.0",
"marked": "^18.0.0",
@ -83,6 +84,7 @@
"devDependencies": {
"@next/bundle-analyzer": "^16.2.2",
"@testing-library/react": "^16.0.0",
"@types/dompurify": "^3.2.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/papaparse": "^5.5.2",
"@vitejs/plugin-react": "^4.3.1",

View file

@ -7,16 +7,19 @@ import jwt from "jsonwebtoken"
import { CacheContainer } from "node-ts-cache"
import { MemoryStorage } from "node-ts-cache-storage-memory"
import { organizationMemberRepository } from "@codeflash-ai/common"
import { cookies } from "next/headers"
const RATE_LIMIT = 5
const RATE_LIMIT_WINDOW_MS = 60 * 1000
const rateLimitCache = new CacheContainer(new MemoryStorage())
// TODO:: Find a way to save it in Session
const JWT_SECRET = process.env.JWT_SECRET || "abrakadabra-codeflash-jwt-secret"
if (!JWT_SECRET) {
throw new Error("JWT_SECRET is not defined in environment variables")
function getJwtSecret(): string {
const secret = process.env.JWT_SECRET
if (!secret) {
throw new Error("JWT_SECRET environment variable is required")
}
return secret
}
const JWT_SECRET: string = getJwtSecret()
interface OAuthStatePayload {
userId: string
@ -72,8 +75,7 @@ export async function fetchUserInfo(): Promise<{
avatarUrl: session.user.picture,
},
}
} catch (error) {
console.error("Error fetching user info:", error)
} catch {
return { error: "Failed to fetch user info" }
}
}
@ -94,12 +96,84 @@ export async function fetchUserOrganizations(): Promise<{
}
return { organizations: result.organizations }
} catch (error) {
console.error("Error fetching user organizations:", error)
} catch {
return { error: "Failed to fetch organizations" }
}
}
const OAUTH_COOKIE_NAME = "oauth_params"
interface OAuthParams {
redirectUri: string
codeChallenge: string
codeChallengeMethod: string
clientId: string
vscodeState: string
}
export async function storeOAuthParams(params: OAuthParams): Promise<{ error?: string }> {
try {
const userId = await getUserId()
if (!userId) {
return { error: "Unauthorized" }
}
const signed = jwt.sign({ ...params, type: "oauth_params" }, JWT_SECRET, {
expiresIn: "10m",
})
const cookieStore = await cookies()
cookieStore.set(OAUTH_COOKIE_NAME, signed, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/codeflash/auth",
maxAge: 600,
})
return {}
} catch {
return { error: "Failed to store OAuth parameters" }
}
}
export async function getStoredOAuthParams(): Promise<{
params?: OAuthParams
error?: string
}> {
try {
const cookieStore = await cookies()
const cookie = cookieStore.get(OAUTH_COOKIE_NAME)
if (!cookie?.value) {
return { error: "Session expired. Please refresh the page and try again." }
}
const payload = jwt.verify(cookie.value, JWT_SECRET) as unknown as OAuthParams & {
type: string
}
if (payload.type !== "oauth_params") {
return { error: "Invalid session" }
}
return {
params: {
redirectUri: payload.redirectUri,
codeChallenge: payload.codeChallenge,
codeChallengeMethod: payload.codeChallengeMethod,
clientId: payload.clientId,
vscodeState: payload.vscodeState,
},
}
} catch {
return { error: "Session expired. Please refresh the page and try again." }
}
}
async function clearOAuthCookie() {
const cookieStore = await cookies()
cookieStore.delete(OAUTH_COOKIE_NAME)
}
export async function isRateLimited(userId: string): Promise<boolean> {
const cacheKey = `rate_limit_vsc_signin_${userId}`
const record = await rateLimitCache.getItem<{ count: number; startTime: number }>(cacheKey)
@ -111,12 +185,10 @@ export async function isRateLimited(userId: string): Promise<boolean> {
{ count: 1, startTime: now },
{ ttl: RATE_LIMIT_WINDOW_MS / 1000 },
)
console.log(`Rate limit initialized for user: ${userId}`)
return false
}
if (record.count >= RATE_LIMIT) {
console.warn(`Rate limit exceeded for user: ${userId}, count: ${record.count}`)
return true
}
@ -124,7 +196,6 @@ export async function isRateLimited(userId: string): Promise<boolean> {
await rateLimitCache.setItem(cacheKey, record, {
ttl: (RATE_LIMIT_WINDOW_MS - (now - record.startTime)) / 1000,
})
console.log(`Rate limit check passed for user: ${userId}, count: ${record.count}`)
return false
}
@ -136,19 +207,9 @@ export async function createOAuthState(params: {
clientId: string
orgId?: string
}): Promise<{ state: string; error?: string }> {
console.log("=== Creating OAuth State (JWT) ===")
console.log("Params:", {
redirectUri: params.redirectUri,
codeChallenge: params.codeChallenge.substring(0, 10) + "...",
codeChallengeMethod: params.codeChallengeMethod,
clientId: params.clientId,
orgId: params.orgId,
})
try {
const userId = await getUserId()
if (!userId) {
console.error("No user ID found - unauthorized")
return { state: "", error: "Unauthorized" }
}
if (params.orgId) {
@ -158,11 +219,8 @@ export async function createOAuthState(params: {
}
}
console.log("User ID:", userId)
const limited = await isRateLimited(userId)
if (limited) {
console.error("Rate limit exceeded for user:", userId)
return { state: "", error: "Rate limit exceeded" }
}
@ -181,11 +239,8 @@ export async function createOAuthState(params: {
jwtid: crypto.randomBytes(16).toString("hex"),
})
console.log("OAuth state JWT created successfully")
return { state }
} catch (error) {
console.error("Error creating OAuth state:", error)
} catch {
return { state: "", error: "Internal server error" }
}
}
@ -195,34 +250,24 @@ export async function authorizeOAuth(state: string): Promise<{
redirectUri?: string
error?: string
}> {
console.log("=== Authorizing OAuth (JWT) ===")
try {
const userId = await getUserId()
if (!userId) {
console.error("No user ID found - unauthorized")
return { error: "Unauthorized" }
}
console.log("User ID:", userId)
let oauthState: OAuthStatePayload
try {
oauthState = jwt.verify(state, JWT_SECRET) as OAuthStatePayload
} catch (error) {
console.error("JWT verification failed:", error instanceof Error ? error.message : error)
oauthState = jwt.verify(state, JWT_SECRET) as unknown as OAuthStatePayload
} catch {
return { error: "Invalid or expired state" }
}
if (oauthState.type !== "oauth_state") {
console.error("Invalid token type:", oauthState.type)
return { error: "Invalid state token" }
}
console.log("OAuth state JWT verified successfully")
if (oauthState.userId !== userId) {
console.error("User mismatch:", { expected: oauthState.userId, actual: userId })
return { error: "User mismatch" }
}
@ -241,14 +286,13 @@ export async function authorizeOAuth(state: string): Promise<{
jwtid: crypto.randomBytes(16).toString("hex"),
})
console.log("Authorization code JWT created successfully")
await clearOAuthCookie()
return {
code,
redirectUri: oauthState.redirectUri,
}
} catch (error) {
console.error("Error authorizing OAuth:", error)
} catch {
return { error: "Internal server error" }
}
}
@ -263,80 +307,45 @@ interface TokenExchangeParams {
export async function exchangeCodeForToken(
params: TokenExchangeParams,
): Promise<{ accessToken?: string; error?: string }> {
console.log("=== Exchanging Code for Token (JWT) ===")
console.log("Params:", {
codeVerifier: params.codeVerifier.substring(0, 10) + "...",
redirectUri: params.redirectUri,
clientId: params.clientId,
})
try {
let codeData: AuthCodePayload
try {
codeData = jwt.verify(params.code, JWT_SECRET) as AuthCodePayload
} catch (error) {
console.error("JWT verification failed:", error instanceof Error ? error.message : error)
codeData = jwt.verify(params.code, JWT_SECRET) as unknown as AuthCodePayload
} catch {
return { error: "Invalid or expired authorization code" }
}
if (codeData.type !== "auth_code") {
console.error("Invalid token type:", codeData.type)
return { error: "Invalid authorization code" }
}
console.log("✓ Authorization code JWT verified successfully!")
console.log("Code data:", {
userId: codeData.userId,
redirectUri: codeData.redirectUri,
clientId: codeData.clientId,
})
if (codeData.clientId !== params.clientId) {
console.error("Client ID mismatch:", { expected: codeData.clientId, actual: params.clientId })
return { error: "Client ID mismatch" }
}
if (codeData.redirectUri !== params.redirectUri) {
console.error("Redirect URI mismatch:", {
expected: codeData.redirectUri,
actual: params.redirectUri,
})
return { error: "Redirect URI mismatch" }
}
console.log("Computing code challenge...")
const computedChallenge = crypto
.createHash(codeData.codeChallengeMethod)
.update(params.codeVerifier)
.digest("base64url")
if (computedChallenge !== codeData.codeChallenge) {
console.error("Code verifier validation failed")
return { error: "Code verifier validation failed" }
}
console.log("✓ PKCE validation successful")
console.log("Generating API token for userId:", codeData.userId, "orgId:", codeData.orgId)
try {
const apiKey = await generateTokenForVsCode(codeData.userId, codeData.orgId)
console.log("API token generated successfully")
console.log("=== Token Exchange Completed Successfully ===")
return { accessToken: apiKey.token }
} catch (tokenError: unknown) {
if (tokenError instanceof Error && tokenError.message === "NEXT_REDIRECT") {
console.error("Caught redirect error during token generation")
return { error: "Authentication required" }
}
console.error("Error generating token:", tokenError)
return { error: "Failed to generate API token" }
}
} catch (error) {
console.error("=== Token Exchange Failed ===")
console.error("Error:", error)
} catch {
return { error: "Internal server error" }
}
}

View file

@ -10,6 +10,8 @@ import {
createOAuthState,
fetchUserOrganizations,
fetchUserInfo,
storeOAuthParams,
getStoredOAuthParams,
Organization,
UserInfo,
} from "./action"
@ -59,15 +61,6 @@ export default function CodeFlashAuthContent() {
const hasCheckedAuth = useRef(false)
useEffect(() => {
// Check if user already authenticated in this session
const authenticated = sessionStorage.getItem("oauth_authenticated")
if (authenticated === "true") {
setHasAuthenticated(true)
setStep("waiting")
setIsCheckingAuth(false)
return
}
// Guard against duplicate runs (React Strict Mode, Suspense remounts)
if (hasCheckedAuth.current) return
hasCheckedAuth.current = true
@ -121,16 +114,22 @@ export default function CodeFlashAuthContent() {
setOrganizations(orgsResult.organizations)
}
// Store OAuth params for later use when user clicks authenticate
sessionStorage.setItem("oauth_redirect_uri", redirectUri)
sessionStorage.setItem("oauth_code_challenge", codeChallenge)
sessionStorage.setItem("oauth_code_challenge_method", codeChallengeMethod)
sessionStorage.setItem("oauth_client_id", clientId)
sessionStorage.setItem("oauth_vscode_state", state)
// Store OAuth params in server-side HttpOnly cookie
const storeResult = await storeOAuthParams({
redirectUri,
codeChallenge,
codeChallengeMethod,
clientId,
vscodeState: state,
})
if (storeResult.error) {
setError(storeResult.error)
return
}
setStep("ready")
} catch (err) {
console.error("Error checking authentication:", err)
} catch {
setError("An unexpected error occurred. Please try again.")
} finally {
setIsCheckingAuth(false)
@ -152,19 +151,18 @@ export default function CodeFlashAuthContent() {
setStep("authorizing")
try {
const redirectUri = sessionStorage.getItem("oauth_redirect_uri")
const codeChallenge = sessionStorage.getItem("oauth_code_challenge")
const codeChallengeMethod = sessionStorage.getItem("oauth_code_challenge_method")
const clientId = sessionStorage.getItem("oauth_client_id")
const vscodeState = sessionStorage.getItem("oauth_vscode_state")
if (!redirectUri || !codeChallenge || !codeChallengeMethod || !clientId || !vscodeState) {
setError("Session expired. Please refresh the page and try again.")
// Retrieve OAuth params from server-side HttpOnly cookie
const stored = await getStoredOAuthParams()
if (stored.error || !stored.params) {
setError(stored.error || "Session expired. Please refresh the page and try again.")
setIsLoading(false)
setStep("ready")
return
}
const { redirectUri, codeChallenge, codeChallengeMethod, clientId, vscodeState } =
stored.params
// Create OAuth state with selected org
const stateResult = await createOAuthState({
redirectUri,
@ -205,30 +203,19 @@ export default function CodeFlashAuthContent() {
return
}
// Mark as authenticated
sessionStorage.setItem("oauth_authenticated", "true")
setHasAuthenticated(true)
// Clean up OAuth state
sessionStorage.removeItem("oauth_redirect_uri")
sessionStorage.removeItem("oauth_code_challenge")
sessionStorage.removeItem("oauth_code_challenge_method")
sessionStorage.removeItem("oauth_client_id")
sessionStorage.removeItem("oauth_vscode_state")
// Redirect back to VS Code with code, state, and theme
const redirectUrl = new URL(result.redirectUri)
redirectUrl.searchParams.set("code", result.code)
redirectUrl.searchParams.set("state", vscodeState)
redirectUrl.searchParams.set("theme", theme) // Add theme parameter
redirectUrl.searchParams.set("theme", theme)
setStep("waiting")
setIsLoading(false)
// Redirect immediately
window.location.href = redirectUrl.toString()
} catch (err) {
console.error("Error authorizing:", err)
} catch {
setError("An error occurred. Please try again.")
setIsLoading(false)
setStep("ready")

View file

@ -132,6 +132,12 @@ function baseParams(systemPrompt: string, conversationMessages: Anthropic.Messag
}
export async function POST(request: NextRequest): Promise<Response> {
const { auth0 } = await import("@/lib/auth0")
const session = await auth0.getSession()
if (!session?.user?.sub) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
let client: Anthropic
try {
client = getClient()

View file

@ -1,7 +1,13 @@
import { NextRequest, NextResponse } from "next/server"
import { prisma } from "@/lib/prisma"
import { auth0 } from "@/lib/auth0"
export async function GET(request: NextRequest): Promise<NextResponse> {
const session = await auth0.getSession()
if (!session?.user?.sub) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const callId = request.nextUrl.searchParams.get("callId")
if (!callId) {

View file

@ -12,8 +12,11 @@ export async function GET(request: NextRequest) {
}
const { searchParams } = request.nextUrl
const page = parseInt(searchParams.get("page") || "1", 10)
const pageSize = parseInt(searchParams.get("pageSize") || "10", 10)
const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1)
const pageSize = Math.min(
100,
Math.max(1, parseInt(searchParams.get("pageSize") || "10", 10) || 10),
)
const search = searchParams.get("search") || ""
const repositoryId = searchParams.get("repositoryId") || undefined
const status = searchParams.get("status") || "all"
@ -38,8 +41,16 @@ export async function GET(request: NextRequest) {
filter.review_quality = reviewQuality
}
// Build sort object
const [sortField, sortDirection] = sortBy.split("_").reduce(
// Build sort object with allowlisted fields
const ALLOWED_SORT_FIELDS = new Set([
"created_at",
"updated_at",
"status",
"event_type",
"function_name",
"repository_id",
])
const [parsedField, parsedDirection] = sortBy.split("_").reduce(
(acc, part, index, arr) => {
if (index === arr.length - 1 && (part === "asc" || part === "desc")) {
return [acc[0], part]
@ -48,8 +59,11 @@ export async function GET(request: NextRequest) {
},
["", "desc"] as [string, string],
)
const sortField = ALLOWED_SORT_FIELDS.has(parsedField) ? parsedField : "created_at"
const sortDirection =
parsedDirection === "asc" || parsedDirection === "desc" ? parsedDirection : "desc"
const sort: Record<string, "asc" | "desc"> = {
[sortField]: sortDirection as "asc" | "desc",
[sortField]: sortDirection,
}
const data = await getAllOptimizationEvents({
@ -66,9 +80,6 @@ export async function GET(request: NextRequest) {
})
} catch (error) {
console.error("Failed to fetch optimization events:", error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Internal server error" },
{ status: 500 },
)
return NextResponse.json({ error: "Internal server error" }, { status: 500 })
}
}

View file

@ -31,8 +31,11 @@ export async function GET(request: NextRequest) {
}
const { searchParams } = request.nextUrl
const page = parseInt(searchParams.get("page") || "1", 10)
const pageSize = parseInt(searchParams.get("pageSize") || "10", 10)
const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1)
const pageSize = Math.min(
100,
Math.max(1, parseInt(searchParams.get("pageSize") || "10", 10) || 10),
)
const eventTypeFilter = searchParams.get("eventTypeFilter") || "all"
const repositoryId = searchParams.get("repositoryId") || undefined
@ -42,9 +45,6 @@ export async function GET(request: NextRequest) {
})
} catch (error) {
console.error("Failed to fetch optimization PRs:", error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Internal server error" },
{ status: 500 },
)
return NextResponse.json({ error: "Internal server error" }, { status: 500 })
}
}

View file

@ -58,10 +58,10 @@ async function IntercomScript() {
const bootSnippet = session
? `window.Intercom('boot', {
app_id: "ljxo1nzr",
name: "${session.user.name}",
nickname: "${session.user.nickname}",
picture: "${session.user.picture}",
user_id: "${session.user.sub}",
name: ${JSON.stringify(session.user.name ?? "")},
nickname: ${JSON.stringify(session.user.nickname ?? "")},
picture: ${JSON.stringify(session.user.picture ?? "")},
user_id: ${JSON.stringify(session.user.sub ?? "")},
email: null,
});`
: `window.Intercom('boot', { app_id: "ljxo1nzr" });`

View file

@ -1,4 +1,5 @@
import { type NextRequest, NextResponse } from "next/server"
import { auth0 } from "@/lib/auth0"
import { getTraceData, type TraceData } from "@/app/observability/lib/get-trace-data"
import { transformToTimelineSections } from "@/app/observability/components/timeline-types"
import { formatTimelineForLLM } from "@/app/observability/components/format-llm-export"
@ -21,6 +22,11 @@ function getFilePath(
}
export async function GET(request: NextRequest) {
const session = await auth0.getSession()
if (!session?.user?.sub) {
return new NextResponse("Unauthorized", { status: 401 })
}
const traceId = request.nextUrl.searchParams.get("trace_id")?.trim()
if (!traceId) {

View file

@ -3,6 +3,7 @@
import { useEffect, useRef } from "react"
import { marked } from "marked"
import DOMPurify from "dompurify"
interface MarkdownViewerProps {
content: string
@ -20,9 +21,9 @@ export function MarkdownViewer({ content, className = "" }: MarkdownViewerProps)
gfm: true,
})
const html = await marked(content)
const rawHtml = await marked(content)
if (containerRef.current) {
containerRef.current.innerHTML = html
containerRef.current.innerHTML = DOMPurify.sanitize(rawHtml)
const codeBlocks = containerRef.current.querySelectorAll("pre code")
codeBlocks.forEach(block => {
block.classList.add("bg-muted", "p-2", "rounded", "text-sm")

View file

@ -1,22 +0,0 @@
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

@ -57,16 +57,14 @@ function getAuth0Client(): Auth0Client {
},
async onCallback(error, context, session) {
if (error) {
console.error("[Auth] Error in callback:", error)
const errorMessage = error.message || ""
if (errorMessage.includes("allowlist-fail")) {
const re = /allowlist-fail\s(.*)\s(.*)\)/
const match = errorMessage.match(re)
if (match != null) {
const userId = match[1]
const userNickname = match[2]
return redirectTo(`/waitlist?username=${userNickname}&userid=${userId}`)
return redirectTo(`/waitlist?username=${userNickname}`)
}
}
@ -78,22 +76,14 @@ function getAuth0Client(): Auth0Client {
}
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 redirectTo(context.returnTo || APP_ROUTES.BASE)
}
try {
// Save user to database (must complete before checking onboarding)
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")
// Track login and check onboarding in parallel — both only need the
// user to exist (which createOrUpdateUser ensures), and neither
// depends on the other's result.
const [, completedOnboarding] = await Promise.all([
trackUserLogin({
userId: user.sub,
@ -103,11 +93,9 @@ function getAuth0Client(): Auth0Client {
}),
hasCompletedOnboarding(user.sub),
])
console.log(`[Auth] Onboarding completed: ${completedOnboarding}`)
const intendedDestination = context.returnTo || APP_ROUTES.BASE
// Check if the path is codeflash/auth/[token]
const isAuthPath =
intendedDestination.startsWith("/codeflash/auth") ||
intendedDestination.includes("/codeflash/auth")
@ -117,8 +105,7 @@ function getAuth0Client(): Auth0Client {
}
return redirectTo(intendedDestination)
} catch (err) {
console.error("[Auth] Error in onCallback:", err)
} catch {
return redirectTo(context.returnTo || APP_ROUTES.BASE)
}
},

View file

@ -58,7 +58,7 @@ importers:
version: 2.6.1(@opentelemetry/api@1.9.1)
'@prisma/client':
specifier: ^7.7.0
version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)
version: 7.7.0(prisma@7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)
'@sentry/node':
specifier: ^10.48.0
version: 10.48.0(@opentelemetry/exporter-trace-otlp-http@0.214.0(@opentelemetry/api@1.9.1))
@ -200,7 +200,7 @@ importers:
version: 0.214.0(@opentelemetry/api@1.9.1)
'@prisma/client':
specifier: ^7.7.0
version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)
version: 7.7.0(prisma@7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)
'@prisma/instrumentation':
specifier: ^7.6.0
version: 7.7.0(@opentelemetry/api@1.9.1)
@ -270,6 +270,9 @@ importers:
diff:
specifier: ^8.0.2
version: 8.0.4
dompurify:
specifier: ^3.3.3
version: 3.3.3
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.3
@ -361,6 +364,9 @@ importers:
'@testing-library/react':
specifier: ^16.0.0
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@types/dompurify':
specifier: ^3.2.0
version: 3.2.0
'@types/jsonwebtoken':
specifier: ^9.0.10
version: 9.0.10
@ -430,7 +436,7 @@ importers:
version: 7.7.0
'@prisma/client':
specifier: ^7.7.0
version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)
version: 7.7.0(prisma@7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)
'@prisma/sqlcommenter-query-insights':
specifier: ^7.7.0
version: 7.7.0
@ -3178,6 +3184,10 @@ packages:
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/dompurify@3.2.0':
resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==}
deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.
'@types/eslint-scope@3.7.7':
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
@ -9839,7 +9849,7 @@ snapshots:
'@prisma/client-runtime-utils@7.7.0': {}
'@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)':
'@prisma/client@7.7.0(prisma@7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)':
dependencies:
'@prisma/client-runtime-utils': 7.7.0
optionalDependencies:
@ -10816,6 +10826,10 @@ snapshots:
'@types/deep-eql@4.0.2': {}
'@types/dompurify@3.2.0':
dependencies:
dompurify: 3.3.3
'@types/eslint-scope@3.7.7':
dependencies:
'@types/eslint': 9.6.1
@ -12341,7 +12355,7 @@ snapshots:
'@next/eslint-plugin-next': 16.2.3
eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7))
@ -12376,7 +12390,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@ -12391,13 +12405,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)):
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)):
dependencies:
debug: 3.2.7
optionalDependencies:
eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
transitivePeerDependencies:
- supports-color
@ -12428,7 +12442,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7))
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3