Skip to content

[Feat]: Credential Management UI for providers#76

Merged
Ayush8923 merged 12 commits intomainfrom
feat/credential-management-ui
Mar 19, 2026
Merged

[Feat]: Credential Management UI for providers#76
Ayush8923 merged 12 commits intomainfrom
feat/credential-management-ui

Conversation

@Ayush8923
Copy link
Copy Markdown
Collaborator

@Ayush8923 Ayush8923 commented Mar 18, 2026

Issue: #74

Summary

PR introduces a Credential Management UI that allows users to add and update provider credentials directly from the frontend, removing the dependency on Swagger APIs.
Along with the UI implementation, this PR also includes API layer improvements to reduce duplication and improve code maintainability.

  • Added UI to add provider credentials
  • Enabled updating credentials directly from the interface
  • Supports multiple providers (e.g., OpenAI, Langfuse, Google)
  • Improves overall usability by removing the need to manually use API endpoints

Summary by CodeRabbit

  • New Features
    • Added "Settings" navigation with a Credentials management page.
    • Credentials UI to view/select providers and configure OpenAI, Langfuse, and Google credentials.
    • Per-provider form with field-level visibility toggles, Active on/off, Saved badge, Last updated timestamp, and Save/Update/Cancel actions.
    • Create, update, and delete credential flows with live list refresh and user feedback (toasts).

@Ayush8923 Ayush8923 self-assigned this Mar 18, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a credentials management feature: backend-proxy API routes, client/server API helpers, typed provider/credential models, utils, UI components (provider list and credential form), a settings page orchestrator, and a Sidebar navigation entry for Settings → Credentials.

Changes

Cohort / File(s) Summary
API Routes
app/api/credentials/route.ts, app/api/credentials/[provider]/route.ts
New route handlers that proxy GET/POST/PATCH for base credentials and GET/DELETE for provider-specific endpoints to the backend, preserving status and JSON error responses; special handling for 204 No Content.
API Client & Utilities
app/lib/apiClient.ts, app/lib/utils.ts
Adds apiClient (server passthrough) and apiFetch helpers, plus getExistingForProvider utility to locate saved credentials.
Types / Provider Catalog
app/lib/types/credentials.ts
Introduces FieldDef, ProviderDef, Credential interfaces and exported PROVIDERS array with provider field definitions.
Settings Page & Logic
app/settings/credentials/page.tsx
New Credentials settings page managing API key loading, fetching/refreshing credentials, form state, validation, save/update/delete flows, and toast feedback.
UI Components
app/components/settings/credentials/ProviderList.tsx, app/components/settings/credentials/CredentialForm.tsx
Adds ProviderList (selectable providers, configured indicator) and CredentialForm (dynamic fields, per-field visibility, active toggle, save/cancel UI).
Navigation
app/components/Sidebar.tsx
Appends a "Settings" nav item with a "Credentials" submenu route /settings/credentials.

Sequence Diagram

sequenceDiagram
    participant User
    participant Page as CredentialsPage
    participant List as ProviderList
    participant Form as CredentialForm
    participant APIRoute as /api/credentials/*
    participant Backend as Backend API

    User->>Page: open settings/credentials
    Page->>Page: load API key (localStorage)
    Page->>APIRoute: GET /api/credentials/
    APIRoute->>Backend: forward request with X-API-KEY
    Backend-->>APIRoute: return credentials list
    APIRoute-->>Page: { status, data }
    Page->>List: render providers + configured state
    User->>List: select provider
    List-->>Page: onSelect(provider)
    Page->>Form: render form for provider (values, visibility)
    User->>Form: modify fields / toggle visibility
    Form-->>Page: onValueChange / onToggle
    User->>Form: click Save
    Page->>Page: validate required fields
    Page->>APIRoute: POST or PATCH /api/credentials/ (body)
    APIRoute->>Backend: forward submission with X-API-KEY
    Backend-->>APIRoute: { status, data }
    APIRoute-->>Page: response
    Page->>APIRoute: GET /api/credentials/ (refresh)
    APIRoute->>Backend: fetch updated list
    Backend-->>APIRoute: updated credentials
    APIRoute-->>Page: { status, data }
    Page->>User: show success/failure toast
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested Labels

enhancement

Suggested Reviewers

  • Prajna1999
  • nishika26

Poem

🐇 I hopped through code to add a key,
Providers listed, fields set free,
Forms that hide or show with cheer,
API routes now bridge us near,
A gentle hop — credentials here! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title '[Feat]: Credential Management UI for providers' directly and clearly summarizes the main change—adding a UI for managing provider credentials.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/credential-management-ui
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (6)
app/lib/apiClient.ts (2)

3-3: Consider using a server-only environment variable.

NEXT_PUBLIC_BACKEND_URL exposes the backend URL to the client bundle. Since apiClient runs exclusively in server route handlers, a non-prefixed env var (e.g., BACKEND_URL) would keep the internal URL private.

Suggested change
-const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000";
+const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8000";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/apiClient.ts` at line 3, The constant BACKEND_URL in
app/lib/apiClient.ts uses NEXT_PUBLIC_BACKEND_URL which leaks the URL to the
client; change it to read a server-only env var (e.g., BACKEND_URL) instead of
the NEXT_PUBLIC_ prefixed one, update the declaration of BACKEND_URL to use
process.env.BACKEND_URL with the same fallback, and ensure any call sites that
import BACKEND_URL or rely on apiClient continue to work with the new name.

26-27: Guard against non-JSON error responses.

If the backend returns a non-JSON response (e.g., HTML error page on 502/504), response.json() will throw, masking the actual error. Consider defensive parsing:

Suggested approach
   // 204 No Content has no body
-  const data = response.status === 204 ? null : await response.json();
+  let data = null;
+  if (response.status !== 204) {
+    const text = await response.text();
+    try {
+      data = JSON.parse(text);
+    } catch {
+      data = { error: text || `Backend returned ${response.status}` };
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/apiClient.ts` around lines 26 - 27, The current parsing line "const
data = response.status === 204 ? null : await response.json()" can throw if the
response body is non-JSON (HTML error pages), so change it to defensively parse:
if status is 204 set data to null, otherwise attempt response.json() inside a
try/catch; on JSON parse failure fall back to await response.text() (or include
the raw text) and include that fallback along with response.status in the
error/return so callers see meaningful info. Update the parsing around the
"response" and "data" variables accordingly and ensure any thrown error includes
both status and the fallback text.
app/settings/credentials/page.tsx (4)

170-182: Add accessible label to sidebar toggle button.

The toggle button lacks an accessible name for screen reader users. Add an aria-label attribute to describe the button's action.

♻️ Proposed fix
             <button
               onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
               className="p-1.5 rounded-md"
               style={{ color: colors.text.secondary }}
+              aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/settings/credentials/page.tsx` around lines 170 - 182, The sidebar toggle
button lacks an accessible name; update the JSX button (the element using
onClick={() => setSidebarCollapsed(!sidebarCollapsed)} and referencing
sidebarCollapsed) to include an aria-label that describes the action (e.g.,
"Collapse sidebar" or "Expand sidebar" based on sidebarCollapsed) so screen
readers can announce its purpose; ensure the label text changes or use a single
descriptive label like "Toggle sidebar" to reflect the button's functionality.

49-65: Duplicated form population logic with handleCancel.

The logic in this effect (lines 49-65) is nearly identical to handleCancel (lines 132-146). Consider extracting a shared helper function to reduce duplication and ensure consistent behavior.

♻️ Suggested refactor
const populateForm = (provider: ProviderDef, existing: Credential | null) => {
  if (existing) {
    setExistingCredential(existing);
    setIsActive(existing.is_active);
    const populated: Record<string, string> = {};
    provider.fields.forEach((f) => { populated[f.key] = existing.credential[f.key] || ""; });
    setFormValues(populated);
  } else {
    setExistingCredential(null);
    setIsActive(true);
    const blank: Record<string, string> = {};
    provider.fields.forEach((f) => { blank[f.key] = ""; });
    setFormValues(blank);
  }
  setVisibleFields(new Set());
};

Then use populateForm(selectedProvider, existing) in both the effect and handleCancel.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/settings/credentials/page.tsx` around lines 49 - 65, The form population
logic in the useEffect duplicates the logic in handleCancel; extract a shared
helper (e.g., populateForm(provider: ProviderDef, existing: Credential | null))
that contains the setExistingCredential, setIsActive, building of
populated/blank Record<string,string> and setFormValues, and the
setVisibleFields(new Set()); call getExistingForProvider(selectedProvider,
credentials) in the effect and call populateForm(selectedProvider, existing)
there and replace the matching block in handleCancel to call
populateForm(selectedProvider, existing) as well so both use the same
implementation.

125-126: Avoid any type for error handling.

Using err: any bypasses TypeScript's type safety. Prefer unknown with type narrowing for safer error handling.

♻️ Proposed fix
-    } catch (err: any) {
-      toast.error(err.message || "Failed to save credentials");
+    } catch (err: unknown) {
+      const message = err instanceof Error ? err.message : "Failed to save credentials";
+      toast.error(message);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/settings/credentials/page.tsx` around lines 125 - 126, Replace the catch
clause that types the error as `any` with `unknown` and narrow it before using
its message: change `catch (err: any)` to `catch (err: unknown)` and then check
`if (err instanceof Error)` to call `toast.error(err.message)` otherwise call
`toast.error(String(err) || "Failed to save credentials")`; update the catch
block surrounding the `toast.error` call so all branches produce a safe string.

14-14: Consider moving shared exports to a dedicated module.

Importing APIKey and STORAGE_KEY from a page file creates tight coupling between pages. These should live in a shared location like app/lib/types/ or app/lib/constants.ts to improve maintainability and avoid circular dependency risks as the app grows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/settings/credentials/page.tsx` at line 14, APIKey and STORAGE_KEY are
exported from a page file which couples pages; extract these exports into a
dedicated shared module (e.g., a new "shared constants/types" module), export
APIKey and STORAGE_KEY from there, update the import sites (including the
current credentials page and the original keystore page) to import from the new
shared module instead of the page, and remove the original exports from the page
file so the page no longer serves as a shared module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/credentials/`[credential_id]/route.ts:
- Around line 17-32: The PATCH handler currently calls request.json() before the
try block which can throw on malformed JSON; move the await request.json() call
into the try block inside the PATCH function so JSON parsing errors are caught
by the catch, then use the parsed body in the apiClient call (keep
apiClient(request, `/api/v1/credentials/${credential_id}`, { method: 'PATCH',
body: JSON.stringify(body) }) and the existing NextResponse.json return logic)
and ensure the catch returns the 500 JSON error as before.
- Around line 34-45: The DELETE handler currently always returns 204; instead
call apiClient(request, `/api/v1/credentials/${credential_id}`, { method:
'DELETE' }) and capture its Response (from DELETE), then forward that response's
status and body to the client (e.g., return a NextResponse with the backend
response body and backend response.status; for 204/empty responses ensure you
return null body with the same status). Keep the try/catch around this call and
only return 500 with the error message when an exception is thrown. Ensure you
update references in the DELETE function and use the captured response's status
instead of hardcoding 204.

In `@app/api/credentials/route.ts`:
- Around line 13-24: In POST, move the await request.json() call inside the try
block so malformed JSON is caught; inside the try, first await request.json(),
then call apiClient(request, "/api/v1/credentials/", ...) and return
NextResponse.json(data, { status }); in the catch block detect JSON parse errors
and return a 400 with a clear error message (otherwise keep returning 500 for
other errors) so both request.json() and apiClient failures are handled; update
references to POST, request.json(), apiClient, and NextResponse accordingly.

In `@app/components/settings/credentials/CredentialForm.tsx`:
- Around line 104-121: The visibility toggle button (onToggleVisibility and
field.key) is missing an accessible name; update the button to include a dynamic
aria-label that reflects state (e.g., aria-label={`Hide ${field.key} value`}
when visible is true, otherwise aria-label={`Show ${field.key} value`}) so
screen readers can announce the control and its action; keep existing onClick,
visible logic, and other attributes but ensure the aria-label text includes the
field key or descriptive text to uniquely identify which credential is being
toggled.

In `@app/settings/credentials/page.tsx`:
- Around line 208-210: Replace the plain anchor used for "Keystore" with Next.js
client-side navigation by importing the Link component (import Link from
"next/link") and using the Link component instead of the <a> tag for the element
that renders "Keystore" (the current anchor in the credentials settings page).
Ensure the same className and inline style (colors.text.primary) are applied to
the Link so styling is preserved and remove the plain <a> to avoid a full page
reload.
- Around line 43-46: The useEffect that runs when apiKeys changes references
loadCredentials from outer scope but doesn't list it as a dependency, causing a
stale closure; fix by either (A) moving the loadCredentials function definition
inside the useEffect so it captures the latest apiKeys, or (B) memoizing
loadCredentials with useCallback and including it in the dependency array
(useCallback deps should include apiKeys and any other used state/props), then
update the useEffect dependencies to [apiKeys, loadCredentials]; ensure
consistent references to loadCredentials and apiKeys to avoid exhaustive-deps
warnings.

---

Nitpick comments:
In `@app/lib/apiClient.ts`:
- Line 3: The constant BACKEND_URL in app/lib/apiClient.ts uses
NEXT_PUBLIC_BACKEND_URL which leaks the URL to the client; change it to read a
server-only env var (e.g., BACKEND_URL) instead of the NEXT_PUBLIC_ prefixed
one, update the declaration of BACKEND_URL to use process.env.BACKEND_URL with
the same fallback, and ensure any call sites that import BACKEND_URL or rely on
apiClient continue to work with the new name.
- Around line 26-27: The current parsing line "const data = response.status ===
204 ? null : await response.json()" can throw if the response body is non-JSON
(HTML error pages), so change it to defensively parse: if status is 204 set data
to null, otherwise attempt response.json() inside a try/catch; on JSON parse
failure fall back to await response.text() (or include the raw text) and include
that fallback along with response.status in the error/return so callers see
meaningful info. Update the parsing around the "response" and "data" variables
accordingly and ensure any thrown error includes both status and the fallback
text.

In `@app/settings/credentials/page.tsx`:
- Around line 170-182: The sidebar toggle button lacks an accessible name;
update the JSX button (the element using onClick={() =>
setSidebarCollapsed(!sidebarCollapsed)} and referencing sidebarCollapsed) to
include an aria-label that describes the action (e.g., "Collapse sidebar" or
"Expand sidebar" based on sidebarCollapsed) so screen readers can announce its
purpose; ensure the label text changes or use a single descriptive label like
"Toggle sidebar" to reflect the button's functionality.
- Around line 49-65: The form population logic in the useEffect duplicates the
logic in handleCancel; extract a shared helper (e.g., populateForm(provider:
ProviderDef, existing: Credential | null)) that contains the
setExistingCredential, setIsActive, building of populated/blank
Record<string,string> and setFormValues, and the setVisibleFields(new Set());
call getExistingForProvider(selectedProvider, credentials) in the effect and
call populateForm(selectedProvider, existing) there and replace the matching
block in handleCancel to call populateForm(selectedProvider, existing) as well
so both use the same implementation.
- Around line 125-126: Replace the catch clause that types the error as `any`
with `unknown` and narrow it before using its message: change `catch (err: any)`
to `catch (err: unknown)` and then check `if (err instanceof Error)` to call
`toast.error(err.message)` otherwise call `toast.error(String(err) || "Failed to
save credentials")`; update the catch block surrounding the `toast.error` call
so all branches produce a safe string.
- Line 14: APIKey and STORAGE_KEY are exported from a page file which couples
pages; extract these exports into a dedicated shared module (e.g., a new "shared
constants/types" module), export APIKey and STORAGE_KEY from there, update the
import sites (including the current credentials page and the original keystore
page) to import from the new shared module instead of the page, and remove the
original exports from the page file so the page no longer serves as a shared
module.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 745c7718-27f1-4beb-811a-b80c652e455f

📥 Commits

Reviewing files that changed from the base of the PR and between 31b0489 and 1b6709e.

📒 Files selected for processing (9)
  • app/api/credentials/[credential_id]/route.ts
  • app/api/credentials/route.ts
  • app/components/Sidebar.tsx
  • app/components/settings/credentials/CredentialForm.tsx
  • app/components/settings/credentials/ProviderList.tsx
  • app/lib/apiClient.ts
  • app/lib/types/credentials.ts
  • app/lib/utils.ts
  • app/settings/credentials/page.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
app/settings/credentials/page.tsx (1)

54-58: ⚠️ Potential issue | 🟡 Minor

Missing dependency in useEffect causes stale closure.

loadCredentials references apiKeys from the outer scope but isn't listed as a dependency. Consider wrapping loadCredentials in useCallback with [apiKeys] as dependencies and adding it to this effect's dependency array, or inline the fetch logic within the effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/settings/credentials/page.tsx` around lines 54 - 58, The useEffect that
calls loadCredentials while depending on apiKeys can create a stale closure
because loadCredentials is not included in the dependency array; update the code
so the effect either inlines the fetch logic that references apiKeys or wrap
loadCredentials with useCallback([...]) using apiKeys as a dependency and then
add loadCredentials to the useEffect dependency list; ensure the symbols
mentioned (useEffect, loadCredentials, apiKeys, useCallback) are updated
accordingly so React sees correct dependencies and avoids stale closures.
🧹 Nitpick comments (1)
app/settings/credentials/page.tsx (1)

95-96: Consider adding user-facing error feedback on fetch failure.

Currently, errors are logged to the console but the user receives no notification when credentials fail to load. They may incorrectly assume no credentials exist.

♻️ Proposed fix
     } catch (err) {
       console.error("Failed to load credentials:", err);
+      toast.error("Failed to load credentials");
     } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/settings/credentials/page.tsx` around lines 95 - 96, The catch block in
app/settings/credentials/page.tsx only does console.error("Failed to load
credentials:", err) so users get no feedback; update the error handling in the
same async loader (the function that fetches credentials inside the Credentials
page component) to set a UI-visible error state (e.g., setLoadError or
setFetchError) or invoke the existing toast/notification API, include the err
message for context, and render a user-facing banner or error panel in the
Credentials page that explains the failure and offers a retry action; replace
the silent console.error with this state update/notification so the component
displays the error to the user.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/settings/credentials/page.tsx`:
- Around line 54-58: The useEffect that calls loadCredentials while depending on
apiKeys can create a stale closure because loadCredentials is not included in
the dependency array; update the code so the effect either inlines the fetch
logic that references apiKeys or wrap loadCredentials with useCallback([...])
using apiKeys as a dependency and then add loadCredentials to the useEffect
dependency list; ensure the symbols mentioned (useEffect, loadCredentials,
apiKeys, useCallback) are updated accordingly so React sees correct dependencies
and avoids stale closures.

---

Nitpick comments:
In `@app/settings/credentials/page.tsx`:
- Around line 95-96: The catch block in app/settings/credentials/page.tsx only
does console.error("Failed to load credentials:", err) so users get no feedback;
update the error handling in the same async loader (the function that fetches
credentials inside the Credentials page component) to set a UI-visible error
state (e.g., setLoadError or setFetchError) or invoke the existing
toast/notification API, include the err message for context, and render a
user-facing banner or error panel in the Credentials page that explains the
failure and offers a retry action; replace the silent console.error with this
state update/notification so the component displays the error to the user.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af99d9f9-c497-4095-87b3-8f06a3aefd1b

📥 Commits

Reviewing files that changed from the base of the PR and between 1b6709e and 0e53a8f.

📒 Files selected for processing (3)
  • app/api/credentials/[credential_id]/route.ts
  • app/api/credentials/route.ts
  • app/settings/credentials/page.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/api/credentials/route.ts
  • app/api/credentials/[credential_id]/route.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
app/settings/credentials/page.tsx (1)

55-58: ⚠️ Potential issue | 🟡 Minor

Stabilize loadCredentials usage in the effect dependency chain.

Line 57 calls loadCredentials() from outer scope, but the effect only depends on apiKeys. This is the same stale-closure/exhaustive-deps issue raised earlier.

♻️ Proposed fix
-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback } from "react";
@@
-  useEffect(() => {
-    if (apiKeys.length === 0) return;
-    loadCredentials();
-  }, [apiKeys]);
+  useEffect(() => {
+    if (apiKeys.length === 0) return;
+    loadCredentials();
+  }, [apiKeys, loadCredentials]);
@@
-  const loadCredentials = async () => {
+  const loadCredentials = useCallback(async () => {
     setIsLoading(true);
     try {
       const data = await apiFetch<{ data?: Credential[] } | Credential[]>(
         "/api/credentials",
         apiKeys[0].key,
       );
       setCredentials(Array.isArray(data) ? data : data.data || []);
     } catch (err) {
       console.error("Failed to load credentials:", err);
     } finally {
       setIsLoading(false);
     }
-  };
+  }, [apiKeys]);

Quick verification:

#!/bin/bash
rg -nU -C2 'useEffect\(\(\) => \{\s*if \(apiKeys.length === 0\) return;\s*loadCredentials\(\);\s*\}, \[apiKeys\]\);|const loadCredentials = async' app/settings/credentials/page.tsx

Also applies to: 83-96

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/settings/credentials/page.tsx` around lines 55 - 58, The effect uses
loadCredentials from outer scope but only lists apiKeys, causing a
stale-closure; fix by stabilizing loadCredentials (either wrap the existing
loadCredentials function in useCallback with dependencies it uses, or move its
async body into the useEffect) and then include loadCredentials in the effect
dependency array so the hook reads useEffect(() => { if (apiKeys.length === 0)
return; loadCredentials(); }, [apiKeys, loadCredentials]);; ensure the same
change is applied for the other occurrence around lines 83-96 so useEffect and
loadCredentials remain consistent.
app/api/credentials/route.ts (1)

13-24: ⚠️ Potential issue | 🟡 Minor

Return 400 for malformed JSON bodies instead of generic 500.

Line 15 and Line 28 can throw on invalid JSON, but both handlers currently return 500. That misclassifies client input errors.

♻️ Proposed fix
 export async function POST(request: NextRequest) {
   try {
     const body = await request.json();
@@
-  } catch (e: any) {
-    return NextResponse.json({ error: e.message }, { status: 500 });
+  } catch (e: unknown) {
+    const message = e instanceof Error ? e.message : "Unknown error";
+    const status = e instanceof SyntaxError ? 400 : 500;
+    return NextResponse.json({ error: message }, { status });
   }
 }
@@
 export async function PATCH(request: NextRequest) {
   try {
     const body = await request.json();
@@
-  } catch (e: any) {
-    return NextResponse.json({ error: e.message }, { status: 500 });
+  } catch (e: unknown) {
+    const message = e instanceof Error ? e.message : "Unknown error";
+    const status = e instanceof SyntaxError ? 400 : 500;
+    return NextResponse.json({ error: message }, { status });
   }
 }

Also applies to: 26-37

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/credentials/route.ts` around lines 13 - 24, The POST handler
currently treats any error from await request.json() as a 500; update POST (and
any other handler in this file that calls request.json) to detect malformed JSON
by catching a SyntaxError (or checking error.name === 'SyntaxError') thrown by
request.json and return NextResponse.json({ error: 'Malformed JSON' }, { status:
400 }) for that case, while preserving the existing 500 response for other
errors; locate the POST function and any other functions that call request.json
and implement this targeted error handling around the request.json call and in
the catch block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/credentials/`[provider]/route.ts:
- Around line 12-13: The dynamic provider value is interpolated raw into backend
paths (e.g. `/api/v1/credentials/provider/${provider}`) which can break routes
when provider contains reserved URL characters; update the code that builds
these backend URLs (where `provider` is used) to call
encodeURIComponent(provider) before interpolation so the proxied path is always
a valid URL-encoded segment (apply the same change for the second occurrence of
the backend path construction in this file).

In `@app/lib/apiClient.ts`:
- Around line 20-24: The current headers merge places ...(options.headers) last
which allows callers to override "X-API-KEY" and "Content-Type"; update the
merge so the fixed headers win (e.g. move ...(options.headers ?? {}) before the
fixed entries or explicitly assign 'X-API-KEY' and 'Content-Type' after the
spread) in the headers object in app/lib/apiClient.ts (apply the same change to
both occurrences around lines 20-24 and 45-49) so callers cannot override apiKey
or content-type.

In `@app/settings/credentials/page.tsx`:
- Around line 197-201: The sidebar toggle button (the element that calls
setSidebarCollapsed(!sidebarCollapsed) and reads sidebarCollapsed) is missing an
accessible name; add an accessible label (e.g., aria-label="Toggle sidebar" or
aria-label that reflects state like aria-label={sidebarCollapsed ? "Expand
sidebar" : "Collapse sidebar"}) to the button so screen readers can identify its
purpose; optionally also add aria-pressed or title to reflect the collapsed
state for additional accessibility.

---

Duplicate comments:
In `@app/api/credentials/route.ts`:
- Around line 13-24: The POST handler currently treats any error from await
request.json() as a 500; update POST (and any other handler in this file that
calls request.json) to detect malformed JSON by catching a SyntaxError (or
checking error.name === 'SyntaxError') thrown by request.json and return
NextResponse.json({ error: 'Malformed JSON' }, { status: 400 }) for that case,
while preserving the existing 500 response for other errors; locate the POST
function and any other functions that call request.json and implement this
targeted error handling around the request.json call and in the catch block.

In `@app/settings/credentials/page.tsx`:
- Around line 55-58: The effect uses loadCredentials from outer scope but only
lists apiKeys, causing a stale-closure; fix by stabilizing loadCredentials
(either wrap the existing loadCredentials function in useCallback with
dependencies it uses, or move its async body into the useEffect) and then
include loadCredentials in the effect dependency array so the hook reads
useEffect(() => { if (apiKeys.length === 0) return; loadCredentials(); },
[apiKeys, loadCredentials]);; ensure the same change is applied for the other
occurrence around lines 83-96 so useEffect and loadCredentials remain
consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 33ecfa72-6c8a-4f9a-b35c-99d9a8abed11

📥 Commits

Reviewing files that changed from the base of the PR and between 4c22b83 and dd1d6b7.

📒 Files selected for processing (4)
  • app/api/credentials/[provider]/route.ts
  • app/api/credentials/route.ts
  • app/lib/apiClient.ts
  • app/settings/credentials/page.tsx

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is the apiClient acts as a single source of truth for backend communication and centralizes API calls made from server components / route handlers. It's Helps avoid duplication of backend URLs and headers.

@Ayush8923 Ayush8923 requested a review from vprashrex March 19, 2026 11:01
@AkhileshNegi
Copy link
Copy Markdown
Contributor

  1. on the side bar we say credential and on middle view we say providers
image can we just have setting and remove the dropdown. So clicking on setting takes us there remove the providers heading too 2. make last updated at user friendly image 3. do we need saved label? since we if add anything it does not disappear image 4. can remove credentials and save in case i want to remove the API key being used 5. Add all the providers

@AkhileshNegi
Copy link
Copy Markdown
Contributor

Similar to this hamburger, heading and helper text
image
can we follow in this too for consistency
image

@Ayush8923 Ayush8923 requested a review from vprashrex March 19, 2026 12:27
@Ayush8923 Ayush8923 merged commit f5fa6ce into main Mar 19, 2026
1 check passed
@Ayush8923 Ayush8923 deleted the feat/credential-management-ui branch March 20, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants