mirror of
https://github.com/codeflash-ai/codeflash-internal.git
synced 2026-05-04 18:25:18 +00:00
Merge pull request #2606 from codeflash-ai/fix/cf-webapp-security-hardening
fix: harden cf-webapp security across auth, XSS, and headers
This commit is contained in:
commit
e6cec80c9d
16 changed files with 226 additions and 194 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -384,10 +384,6 @@ export async function createPullRequest({
|
|||
const authorizedRepository = (
|
||||
authorizedEvent.event as { repository?: { full_name?: string | null } | null }
|
||||
).repository
|
||||
if (
|
||||
full_repo_name &&
|
||||
authorizedRepository?.full_name &&
|
||||
authorizedRepository.full_name !== full_repo_name
|
||||
if (full_repo_name && !authorizedRepository?.full_name) {
|
||||
return createErrorResponse("Repository not found for this optimization event")
|
||||
}
|
||||
|
|
@ -398,8 +394,6 @@ export async function createPullRequest({
|
|||
) {
|
||||
return createErrorResponse("Repository mismatch for optimization event")
|
||||
}
|
||||
return createErrorResponse("Repository mismatch for optimization event")
|
||||
}
|
||||
|
||||
const cfapiUrl = process.env.CODEFLASH_CFAPI_URL
|
||||
const session = await auth0.getAccessToken()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" });`
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue