Skip to content
Merged
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 CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function MyComponent() {
const {
isAuthenticated, isHydrated,
session, currentUser, googleProfile,
apiKeys, activeKey, addKey, removeKey, setKeys,
apiKeys, activeKey, addKey, removeKey,
loginWithToken, logout,
} = useAuth();
}
Expand Down
71 changes: 12 additions & 59 deletions app/(main)/keystore/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import AddKeyModal from "@/app/components/keystore/AddKeyModal";
import { useAuth } from "@/app/lib/context/AuthContext";
import { useApp } from "@/app/lib/context/AppContext";
import { useToast } from "@/app/hooks/useToast";
import { apiFetch } from "@/app/lib/apiClient";
import { APIKey } from "@/app/lib/types/credentials";

export default function KaapiKeystore() {
const { sidebarCollapsed } = useApp();
Expand All @@ -24,15 +22,11 @@ export default function KaapiKeystore() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [newKeyLabel, setNewKeyLabel] = useState("");
const [newKeyValue, setNewKeyValue] = useState("");
const [newKeyProvider, setNewKeyProvider] = useState("Kaapi");
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set());
const [copiedKeyId, setCopiedKeyId] = useState<string | null>(null);
const [isValidating, setIsValidating] = useState(false);

const resetForm = () => {
setNewKeyLabel("");
setNewKeyValue("");
setNewKeyProvider("Kaapi");
};

const handleAddKey = async () => {
Expand All @@ -41,63 +35,28 @@ export default function KaapiKeystore() {
return;
}

const trimmedKey = newKeyValue.trim();
setIsValidating(true);

try {
await apiFetch("/api/apikeys/verify", trimmedKey);
await addKey({ key: newKeyValue.trim(), label: newKeyLabel.trim() });
resetForm();
setIsModalOpen(false);
toast.success("API key added successfully");
} catch (err) {
console.error("API key validation failed:", err);
toast.error("Invalid API key. Please check the key and try again.");
toast.error(
err instanceof Error
? err.message
: "Invalid API key. Please check the key and try again.",
);
} finally {
setIsValidating(false);
return;
}

const newKey: APIKey = {
id: Date.now().toString(),
label: newKeyLabel.trim(),
key: trimmedKey,
provider: newKeyProvider,
createdAt: new Date().toISOString(),
};

addKey(newKey);
resetForm();
setIsModalOpen(false);
setIsValidating(false);
toast.success("API key added successfully");
};

const handleDeleteKey = (id: string) => {
removeApiKey(id);
setVisibleKeys((prev) => {
const next = new Set(prev);
next.delete(id);
return next;
});
const handleDeleteKey = async (id: string) => {
await removeApiKey(id);
Comment thread
Ayush8923 marked this conversation as resolved.
toast.success("API key removed");
};

const toggleKeyVisibility = (id: string) => {
setVisibleKeys((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};

const copyToClipboard = async (text: string, id: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedKeyId(id);
toast.success("API key copied to clipboard");
setTimeout(() => setCopiedKeyId(null), 1500);
} catch {
toast.error("Failed to copy API key");
}
};

const closeAddModal = () => {
setIsModalOpen(false);
resetForm();
Expand All @@ -118,10 +77,6 @@ export default function KaapiKeystore() {
<div className="max-w-3xl mx-auto">
<KeysCard
apiKeys={apiKeys}
visibleKeys={visibleKeys}
copiedKeyId={copiedKeyId}
onToggleVisibility={toggleKeyVisibility}
onCopy={copyToClipboard}
onDelete={handleDeleteKey}
onAddNew={() => setIsModalOpen(true)}
/>
Expand All @@ -134,11 +89,9 @@ export default function KaapiKeystore() {
open={isModalOpen}
newKeyLabel={newKeyLabel}
newKeyValue={newKeyValue}
newKeyProvider={newKeyProvider}
isValidating={isValidating}
onLabelChange={setNewKeyLabel}
onValueChange={setNewKeyValue}
onProviderChange={setNewKeyProvider}
onAddKey={handleAddKey}
onClose={closeAddModal}
/>
Expand Down
72 changes: 72 additions & 0 deletions app/api/apikeys/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from "next/server";
import { apiClient } from "@/app/lib/apiClient";
import { clearApiKeyCookies, setApiKeyCookies } from "@/app/lib/authCookie";
import type { AddApiKeyRequest, ApiKeyMeta } from "@/app/lib/types/credentials";

function maskKey(key: string): string {
const tail = key.slice(-4);
return `${"•".repeat(8)}${tail}`;
}

export async function POST(request: NextRequest) {
let body: AddApiKeyRequest;
try {
body = (await request.json()) as AddApiKeyRequest;
} catch {
return NextResponse.json(
{ error: "Invalid request body" },
{ status: 400 },
);
}

const key = body.key?.trim();
const label = body.label?.trim();

if (!key || !label) {
return NextResponse.json(
{ error: "Both a label and an API key are required" },
{ status: 400 },
);
}

const verifyRequest = new Request(request.url, {
headers: {
"X-API-KEY": key,
Cookie: request.headers.get("Cookie") || "",
},
});

let status: number;
try {
({ status } = await apiClient(verifyRequest, "/api/v1/apikeys/verify"));
} catch {
return NextResponse.json(
{ error: "Failed to reach backend for API key verification" },
{ status: 502 },
);
}

if (status < 200 || status >= 300) {
return NextResponse.json(
{ error: "Invalid API key. Please check the key and try again." },
{ status: 401 },
);
}
Comment thread
Ayush8923 marked this conversation as resolved.

const meta: ApiKeyMeta = {
id: crypto.randomUUID(),
label,
masked: maskKey(key),
createdAt: new Date().toISOString(),
};

const res = NextResponse.json({ data: meta }, { status: 200 });
setApiKeyCookies(res, key, meta);
return res;
}
Comment thread
Ayush8923 marked this conversation as resolved.

export async function DELETE() {
const res = NextResponse.json({ success: true }, { status: 200 });
clearApiKeyCookies(res);
return res;
}
7 changes: 6 additions & 1 deletion app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { NextRequest, NextResponse } from "next/server";
import { apiClient } from "@/app/lib/apiClient";
import { clearFeaturesCookie, clearRoleCookie } from "@/app/lib/authCookie";
import {
clearApiKeyCookies,
clearFeaturesCookie,
clearRoleCookie,
} from "@/app/lib/authCookie";

export async function POST(request: NextRequest) {
const { status, data, headers } = await apiClient(
Expand All @@ -18,6 +22,7 @@ export async function POST(request: NextRequest) {

clearRoleCookie(res);
clearFeaturesCookie(res);
clearApiKeyCookies(res);

return res;
}
4 changes: 2 additions & 2 deletions app/api/evaluations/stt/runs/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { apiClient } from "@/app/lib/apiClient";
import { apiClient, getRequestApiKey } from "@/app/lib/apiClient";

export async function GET(request: Request) {
try {
Expand All @@ -18,7 +18,7 @@ export async function GET(request: Request) {

export async function POST(request: Request) {
try {
const apiKey = request.headers.get("X-API-KEY");
const apiKey = getRequestApiKey(request);
if (!apiKey) {
return NextResponse.json(
{
Expand Down
37 changes: 9 additions & 28 deletions app/components/keystore/AddKeyModal.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
"use client";

import { Button, Field, Modal, Select } from "@/app/components/ui";
import { InfoIcon } from "@/app/components/icons";

const PROVIDERS = [{ value: "Kaapi", label: "Kaapi" }];
import { Button, Field, Modal } from "@/app/components/ui";
import { APP_NAME } from "@/app/lib/constants";

interface AddKeyModalProps {
open: boolean;
newKeyLabel: string;
newKeyValue: string;
newKeyProvider: string;
isValidating?: boolean;
onLabelChange: (value: string) => void;
onValueChange: (value: string) => void;
onProviderChange: (value: string) => void;
onAddKey: () => void;
onClose: () => void;
}
Expand All @@ -22,11 +18,9 @@ export default function AddKeyModal({
open,
newKeyLabel,
newKeyValue,
newKeyProvider,
isValidating,
onLabelChange,
onValueChange,
onProviderChange,
onAddKey,
onClose,
}: AddKeyModalProps) {
Expand All @@ -43,20 +37,16 @@ export default function AddKeyModal({
<div className="px-6 pb-2">
<p className="text-sm mb-5 text-text-secondary">
Add a new API key to use in your evaluation workflows. Keys are stored
locally in your browser.
securely server-side and cannot be viewed again after they are added.
</p>
Comment thread
Ayush8923 marked this conversation as resolved.

<div className="space-y-4">
<div>
<label className="block text-xs font-medium text-text-secondary mb-1">
Provider
</label>
<Select
value={newKeyProvider}
onChange={(e) => onProviderChange(e.target.value)}
options={PROVIDERS}
/>
</div>
<Field
label="Provider"
value={APP_NAME}
onChange={() => {}}
disabled
/>

<Field
label="Label"
Expand All @@ -73,15 +63,6 @@ export default function AddKeyModal({
placeholder="Paste your API key here"
/>
</div>

<div className="mt-5 rounded-md p-3 bg-accent-primary/5 border border-accent-primary/20">
<div className="flex gap-2">
<InfoIcon className="w-4 h-4 shrink-0 mt-0.5 text-accent-primary" />
<p className="text-xs text-text-secondary">
API keys are stored in your browser&apos;s local storage.
</p>
</div>
</div>
</div>

<div className="border-t border-border px-6 py-4 flex items-center justify-end gap-3 shrink-0">
Expand Down
Loading
Loading