Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/(main)/datasets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { useState, useEffect } from "react";

import { useAuth } from "@/app/lib/context/AuthContext";
import { useApp } from "@/app/lib/context/AppContext";
import { Dataset } from "@/app/lib/types/datasets";
import { apiFetch } from "@/app/lib/apiClient";
import Sidebar from "@/app/components/Sidebar";
import PageHeader from "@/app/components/PageHeader";
import { useToast } from "@/app/components/Toast";
import { Dataset } from "@/app/lib/types/dataset";

export const DATASETS_STORAGE_KEY = "kaapi_datasets";

Expand Down
2 changes: 0 additions & 2 deletions app/(main)/keystore/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import { useAuth } from "@/app/lib/context/AuthContext";
import { useApp } from "@/app/lib/context/AppContext";
import { APIKey } from "@/app/lib/types/credentials";

export const STORAGE_KEY = "kaapi_api_keys";

export default function KaapiKeystore() {
const { sidebarCollapsed } = useApp();
const [isModalOpen, setIsModalOpen] = useState(false);
Expand Down
39 changes: 39 additions & 0 deletions app/api/assessment/assessments/[assessment_id]/results/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from "next/server";
import {
assessmentApiFetch,
safeParseJson,
toDownloadResponse,
} from "@/app/api/assessment/utils";

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ assessment_id: string }> },
) {
try {
const { assessment_id } = await params;
const queryParams = new URLSearchParams(request.nextUrl.searchParams);
queryParams.set("get_trace_info", "true");

const response = await assessmentApiFetch(
request,
`/api/v1/assessment/assessments/${assessment_id}/results?${queryParams.toString()}`,
{ method: "GET" },
);

const downloadResponse = await toDownloadResponse(response);
if (downloadResponse) {
return downloadResponse;
}

const data = await safeParseJson(response);
return NextResponse.json(data, { status: response.status });
} catch (error: unknown) {
console.error("Assessment results proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward request",
},
{ status: 500 },
);
}
}
27 changes: 27 additions & 0 deletions app/api/assessment/assessments/[assessment_id]/retry/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from "next/server";
import { apiClient } from "@/app/lib/apiClient";

interface RouteContext {
params: Promise<{ assessment_id: string }>;
}

export async function POST(request: NextRequest, context: RouteContext) {
try {
const { assessment_id } = await context.params;
const { status, data } = await apiClient(
request,
`/api/v1/assessment/assessments/${assessment_id}/retry`,
{ method: "POST" },
);

return NextResponse.json(data, { status });
} catch (error: unknown) {
console.error("Assessment retry proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward assessment retry request",
},
{ status: 500 },
);
}
}
25 changes: 25 additions & 0 deletions app/api/assessment/assessments/route.ts
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from "next/server";
import { apiClient } from "@/app/lib/apiClient";

export async function GET(request: NextRequest) {
try {
const queryString = request.nextUrl.searchParams.toString();
const endpoint = `/api/v1/assessment/assessments${
queryString ? `?${queryString}` : ""
}`;

const { status, data } = await apiClient(request, endpoint, {
method: "GET",
});

return NextResponse.json(data, { status });
} catch (error: unknown) {
console.error("Assessment list proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward request to backend",
},
{ status: 500 },
);
}
}
95 changes: 95 additions & 0 deletions app/api/assessment/datasets/[dataset_id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { NextRequest, NextResponse } from "next/server";
import { apiClient } from "@/app/lib/apiClient";

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ dataset_id: string }> },
) {
try {
const { dataset_id } = await params;
const fetchContent =
request.nextUrl.searchParams.get("fetch_content") === "true";

// Always request signed URL when fetch_content is needed
const backendParams = new URLSearchParams(request.nextUrl.searchParams);
if (fetchContent) {
backendParams.set("include_signed_url", "true");
}
const endpoint = `/api/v1/assessment/datasets/${dataset_id}${
backendParams.toString() ? `?${backendParams.toString()}` : ""
}`;

const { status, data } = await apiClient(request, endpoint, {
method: "GET",
});

if (status >= 400) {
return NextResponse.json(data, { status });
}

// Download file from S3 server-side and return as base64
if (fetchContent) {
const signedUrl =
(data as { data?: { signed_url?: string }; signed_url?: string })?.data
?.signed_url ||
(data as { data?: { signed_url?: string }; signed_url?: string })
?.signed_url;

if (!signedUrl) {
return NextResponse.json(
{ error: "No signed URL available" },
{ status: 404 },
);
}

const fileResponse = await fetch(signedUrl);
if (!fileResponse.ok) {
return NextResponse.json(
{ error: "Failed to fetch file from storage" },
{ status: 502 },
);
}

const arrayBuffer = await fileResponse.arrayBuffer();
const base64 = Buffer.from(arrayBuffer).toString("base64");
return NextResponse.json(
Comment on lines +45 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Cap fetch_content before base64-encoding the whole file.

This path buffers the entire object into memory and then expands it again as base64. A single large dataset can spike memory hard enough to take the route down. Either return the signed URL to the client or enforce a strict size cap before arrayBuffer().

Possible guardrail
       const fileResponse = await fetch(signedUrl);
       if (!fileResponse.ok) {
         return NextResponse.json(
           { error: "Failed to fetch file from storage" },
           { status: 502 },
         );
       }

+      const contentLength = Number(
+        fileResponse.headers.get("content-length") || "0",
+      );
+      const MAX_EMBEDDED_BYTES = 5 * 1024 * 1024;
+      if (contentLength > MAX_EMBEDDED_BYTES) {
+        return NextResponse.json(
+          { error: "Dataset file is too large to embed in the response" },
+          { status: 413 },
+        );
+      }
+
       const arrayBuffer = await fileResponse.arrayBuffer();
       const base64 = Buffer.from(arrayBuffer).toString("base64");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/datasets/`[dataset_id]/route.ts around lines 45 - 55, The
route currently calls fetch(signedUrl) then awaits fileResponse.arrayBuffer()
and base64-encodes it (using Buffer.from(...).toString("base64")), which can OOM
on large objects; update the logic in the handler in route.ts to first inspect
fileResponse.headers.get('content-length') (or otherwise determine size) and
enforce a strict max size (e.g., reject with NextResponse.json({ error: "File
too large" }, { status: 413 })) before calling arrayBuffer(), or alternatively
return the signedUrl to the client instead of downloading and encoding
server-side; ensure you change the branch that currently performs arrayBuffer()
and Buffer.from(...) to use the size check or return the signed URL so the
server never buffers large files into memory.

{ ...(data as Record<string, unknown>), file_content: base64 },
{ status: 200 },
);
}

return NextResponse.json(data, { status });
} catch (error: unknown) {
console.error("Assessment dataset details proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward request to backend",
},
{ status: 500 },
);
}
}

export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ dataset_id: string }> },
) {
try {
const { dataset_id } = await params;
const { status, data } = await apiClient(
request,
`/api/v1/assessment/datasets/${dataset_id}`,
{ method: "DELETE" },
);

return NextResponse.json(data, { status });
Comment on lines +79 to +85
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle 204 deletes without NextResponse.json().

If the backend returns 204 No Content, NextResponse.json(data, { status: 204 }) still tries to build a body, which is invalid for 204 responses and can throw. Return an empty NextResponse for that status instead.

Suggested fix
     const { status, data } = await apiClient(
       request,
       `/api/v1/assessment/datasets/${dataset_id}`,
       { method: "DELETE" },
     );

+    if (status === 204) {
+      return new NextResponse(null, { status });
+    }
+
     return NextResponse.json(data, { status });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { status, data } = await apiClient(
request,
`/api/v1/assessment/datasets/${dataset_id}`,
{ method: "DELETE" },
);
return NextResponse.json(data, { status });
const { status, data } = await apiClient(
request,
`/api/v1/assessment/datasets/${dataset_id}`,
{ method: "DELETE" },
);
if (status === 204) {
return new NextResponse(null, { status });
}
return NextResponse.json(data, { status });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assessment/datasets/`[dataset_id]/route.ts around lines 79 - 85, The
DELETE response handling should special-case HTTP 204: after calling
apiClient(request, `/api/v1/assessment/datasets/${dataset_id}`, { method:
"DELETE" }) inspect the returned status and if it equals 204 return an empty
NextResponse with that status instead of calling NextResponse.json(data, {
status }), otherwise continue to return NextResponse.json(data, { status });
update the route handler in route.ts where apiClient and dataset_id are used to
implement this conditional.

} catch (error: unknown) {
console.error("Assessment dataset delete proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward request to backend",
},
{ status: 500 },
);
}
}
47 changes: 47 additions & 0 deletions app/api/assessment/datasets/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from "next/server";
import { apiClient } from "@/app/lib/apiClient";

export async function GET(request: NextRequest) {
try {
const { status, data } = await apiClient(
request,
"/api/v1/assessment/datasets",
{ method: "GET" },
);

return NextResponse.json(data, { status });
} catch (error: unknown) {
console.error("Assessment datasets list proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward request to backend",
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{ status: 500 },
);
}
}

export async function POST(request: NextRequest) {
try {
const formData = await request.formData();

const { status, data } = await apiClient(
request,
"/api/v1/assessment/datasets",
{
method: "POST",
body: formData,
},
);

return NextResponse.json(data, { status });
} catch (error: unknown) {
console.error("Assessment datasets create proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward request to backend",
},
{ status: 500 },
);
}
}
39 changes: 39 additions & 0 deletions app/api/assessment/evaluations/[evaluation_id]/results/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from "next/server";
import {
assessmentApiFetch,
safeParseJson,
toDownloadResponse,
} from "@/app/api/assessment/utils";

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ evaluation_id: string }> },
) {
try {
const { evaluation_id } = await params;
const queryString = request.nextUrl.searchParams.toString();
const endpoint = `/api/v1/assessment/evaluations/${evaluation_id}/results${
queryString ? `?${queryString}` : ""
}`;

const response = await assessmentApiFetch(request, endpoint, {
method: "GET",
});

const downloadResponse = await toDownloadResponse(response);
if (downloadResponse) {
return downloadResponse;
}

const data = await safeParseJson(response);
return NextResponse.json(data, { status: response.status });
} catch (error: unknown) {
console.error("Assessment evaluation results proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward request",
},
{ status: 500 },
);
}
}
27 changes: 27 additions & 0 deletions app/api/assessment/evaluations/[evaluation_id]/retry/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from "next/server";
import { apiClient } from "@/app/lib/apiClient";

interface RouteContext {
params: Promise<{ evaluation_id: string }>;
}

export async function POST(request: NextRequest, context: RouteContext) {
try {
const { evaluation_id } = await context.params;
const { status, data } = await apiClient(
request,
`/api/v1/assessment/evaluations/${evaluation_id}/retry`,
{ method: "POST" },
);

return NextResponse.json(data, { status });
} catch (error: unknown) {
console.error("Assessment evaluation retry proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward evaluation retry request",
},
{ status: 500 },
);
}
}
50 changes: 50 additions & 0 deletions app/api/assessment/evaluations/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { NextRequest, NextResponse } from "next/server";
import { apiClient } from "@/app/lib/apiClient";

export async function GET(request: NextRequest) {
try {
const queryString = request.nextUrl.searchParams.toString();
const endpoint = `/api/v1/assessment/evaluations${
queryString ? `?${queryString}` : ""
}`;

const { status, data } = await apiClient(request, endpoint, {
method: "GET",
});

return NextResponse.json(data, { status });
} catch (error: unknown) {
console.error("Assessment evaluations list proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward request to backend",
},
{ status: 500 },
);
}
}

export async function POST(request: NextRequest) {
try {
const body = await request.json();

const { status, data } = await apiClient(
request,
"/api/v1/assessment/evaluations",
{
method: "POST",
body: JSON.stringify(body),
},
);

return NextResponse.json(data, { status });
} catch (error: unknown) {
console.error("Assessment evaluations create proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward request to backend",
},
{ status: 500 },
);
}
}
Loading
Loading