From 211cddb8a766a0ba364f95727a5fcbd06401d0a8 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:21:32 +0530 Subject: [PATCH 1/5] fix(security): move api key localstorage to cookies --- CLAUDE.md | 2 +- app/(main)/keystore/page.tsx | 72 ++++++-------------------- app/api/apikeys/route.ts | 74 +++++++++++++++++++++++++++ app/api/auth/logout/route.ts | 7 ++- app/api/evaluations/stt/runs/route.ts | 4 +- app/components/keystore/KeysCard.tsx | 48 ++++------------- app/lib/apiClient.ts | 24 +++++++-- app/lib/authCookie.ts | 37 ++++++++++++-- app/lib/constants.ts | 5 +- app/lib/context/AuthContext.tsx | 54 ++++++++++--------- app/lib/guardrailsClient.ts | 3 +- app/lib/types/auth.ts | 15 ++++-- app/lib/types/credentials.ts | 17 +++++- app/lib/utils.ts | 20 +++----- 14 files changed, 235 insertions(+), 147 deletions(-) create mode 100644 app/api/apikeys/route.ts diff --git a/CLAUDE.md b/CLAUDE.md index 5990c8bc..b88b90bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,7 +118,7 @@ function MyComponent() { const { isAuthenticated, isHydrated, session, currentUser, googleProfile, - apiKeys, activeKey, addKey, removeKey, setKeys, + apiKeys, activeKey, addKey, removeKey, loginWithToken, logout, } = useAuth(); } diff --git a/app/(main)/keystore/page.tsx b/app/(main)/keystore/page.tsx index cafc6ef7..8e97a4dc 100644 --- a/app/(main)/keystore/page.tsx +++ b/app/(main)/keystore/page.tsx @@ -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(); @@ -25,8 +23,6 @@ export default function KaapiKeystore() { const [newKeyLabel, setNewKeyLabel] = useState(""); const [newKeyValue, setNewKeyValue] = useState(""); const [newKeyProvider, setNewKeyProvider] = useState("Kaapi"); - const [visibleKeys, setVisibleKeys] = useState>(new Set()); - const [copiedKeyId, setCopiedKeyId] = useState(null); const [isValidating, setIsValidating] = useState(false); const resetForm = () => { @@ -41,63 +37,33 @@ export default function KaapiKeystore() { return; } - const trimmedKey = newKeyValue.trim(); setIsValidating(true); - + const newKey = { + key: newKeyValue.trim(), + label: newKeyLabel.trim(), + provider: newKeyProvider, + }; try { - await apiFetch("/api/apikeys/verify", trimmedKey); + await addKey(newKey); + 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); 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(); @@ -118,10 +84,6 @@ export default function KaapiKeystore() {
setIsModalOpen(true)} /> diff --git a/app/api/apikeys/route.ts b/app/api/apikeys/route.ts new file mode 100644 index 00000000..3ce2fb9a --- /dev/null +++ b/app/api/apikeys/route.ts @@ -0,0 +1,74 @@ +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(); + const provider = body.provider?.trim() || "Kaapi"; + + 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 }, + ); + } + + const meta: ApiKeyMeta = { + id: crypto.randomUUID(), + label, + provider, + masked: maskKey(key), + createdAt: new Date().toISOString(), + }; + + const res = NextResponse.json({ data: meta }, { status: 200 }); + setApiKeyCookies(res, key, meta); + return res; +} + +export async function DELETE() { + const res = NextResponse.json({ success: true }, { status: 200 }); + clearApiKeyCookies(res); + return res; +} diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts index 8461a4fe..ec14accc 100644 --- a/app/api/auth/logout/route.ts +++ b/app/api/auth/logout/route.ts @@ -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( @@ -18,6 +22,7 @@ export async function POST(request: NextRequest) { clearRoleCookie(res); clearFeaturesCookie(res); + clearApiKeyCookies(res); return res; } diff --git a/app/api/evaluations/stt/runs/route.ts b/app/api/evaluations/stt/runs/route.ts index 1c21d4b6..f045f5f7 100644 --- a/app/api/evaluations/stt/runs/route.ts +++ b/app/api/evaluations/stt/runs/route.ts @@ -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 { @@ -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( { diff --git a/app/components/keystore/KeysCard.tsx b/app/components/keystore/KeysCard.tsx index df765f15..5390600c 100644 --- a/app/components/keystore/KeysCard.tsx +++ b/app/components/keystore/KeysCard.tsx @@ -1,34 +1,17 @@ "use client"; import { Button } from "@/app/components/ui"; -import { - EyeIcon, - EyeOffIcon, - CopyIcon, - TrashIcon, - KeyIcon, - InfoIcon, - PlusIcon, - CheckLineIcon, -} from "@/app/components/icons"; +import { TrashIcon, KeyIcon, InfoIcon, PlusIcon } from "@/app/components/icons"; import { APIKey } from "@/app/lib/types/credentials"; interface KeysCardProps { apiKeys: APIKey[]; - visibleKeys: Set; - copiedKeyId: string | null; - onToggleVisibility: (id: string) => void; - onCopy: (text: string, id: string) => void; onDelete: (id: string) => void; onAddNew: () => void; } export default function KeysCard({ apiKeys, - visibleKeys, - copiedKeyId, - onToggleVisibility, - onCopy, onDelete, onAddNew, }: KeysCardProps) { @@ -60,7 +43,8 @@ export default function KeysCard({
Only one API key can be stored at a time. Delete this key to add a - different one. + different one. For your security, the key is stored server-side and + cannot be viewed again after it is added. {apiKeys.map((apiKey) => ( @@ -76,29 +60,15 @@ export default function KeysCard({
- {visibleKeys.has(apiKey.id) ? apiKey.key : "•".repeat(32)} + {apiKey.masked ?? "•".repeat(32)} -

- Added {new Date(apiKey.createdAt).toLocaleDateString()} -

+ {apiKey.createdAt && ( +

+ Added {new Date(apiKey.createdAt).toLocaleDateString()} +

+ )}
- onToggleVisibility(apiKey.id)} - title={visibleKeys.has(apiKey.id) ? "Hide" : "Show"} - > - {visibleKeys.has(apiKey.id) ? : } - - onCopy(apiKey.key, apiKey.id)} - title={copiedKeyId === apiKey.id ? "Copied" : "Copy"} - > - {copiedKeyId === apiKey.id ? ( - - ) : ( - - )} - onDelete(apiKey.id)} tone="danger" diff --git a/app/lib/apiClient.ts b/app/lib/apiClient.ts index 05aa7664..219dd435 100644 --- a/app/lib/apiClient.ts +++ b/app/lib/apiClient.ts @@ -1,7 +1,25 @@ import { NextRequest } from "next/server"; -import { AUTH_EXPIRED_EVENT } from "@/app/lib/constants"; +import { AUTH_EXPIRED_EVENT, COOKIE_KEYS } from "@/app/lib/constants"; const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8000"; + +function readCookieHeader(cookieHeader: string, name: string): string { + for (const part of cookieHeader.split(";")) { + const eq = part.indexOf("="); + if (eq === -1) continue; + if (part.slice(0, eq).trim() === name) { + return decodeURIComponent(part.slice(eq + 1).trim()); + } + } + return ""; +} + +export function getRequestApiKey(request: NextRequest | Request): string { + const header = request.headers.get("X-API-KEY"); + if (header) return header; + const cookieHeader = request.headers.get("Cookie") || ""; + return readCookieHeader(cookieHeader, COOKIE_KEYS.API_KEY); +} export type UploadPhase = "uploading" | "processing" | "done"; type ApiClientOptions = RequestInit & { responseType?: TResponseType }; @@ -30,7 +48,7 @@ export async function apiClient< options: ApiClientOptions = {} as ApiClientOptions, ): Promise> { const { responseType = "json", ...requestOptions } = options; - const apiKey = request.headers.get("X-API-KEY") || ""; + const apiKey = getRequestApiKey(request); const cookie = request.headers.get("Cookie") || ""; const headers = new Headers(requestOptions.headers); if ( @@ -244,7 +262,7 @@ export async function apiFetch( if (!(fetchOptions.body instanceof FormData)) { headers.set("Content-Type", "application/json"); } - headers.set("X-API-KEY", apiKey); + if (apiKey) headers.set("X-API-KEY", apiKey); return headers; }; diff --git a/app/lib/authCookie.ts b/app/lib/authCookie.ts index a56a82fa..eebb91d4 100644 --- a/app/lib/authCookie.ts +++ b/app/lib/authCookie.ts @@ -1,9 +1,38 @@ import type { NextResponse } from "next/server"; -import { COOKIE_KEYS, type FeatureFlagKey } from "@/app/lib/constants"; +import { AUTH_COOKIE_MAX_AGE, COOKIE_KEYS } from "@/app/lib/constants"; +import type { ApiKeyMeta } from "@/app/lib/types/credentials"; +import type { UserLike } from "@/app/lib/types/auth"; -interface UserLike { - is_superuser?: boolean; - features?: FeatureFlagKey[]; +export function setApiKeyCookies( + response: NextResponse, + key: string, + meta: ApiKeyMeta, +): void { + const secure = process.env.NODE_ENV === "production" ? "; Secure" : ""; + const attrs = `Path=/; Max-Age=${AUTH_COOKIE_MAX_AGE}; SameSite=Lax${secure}`; + + response.headers.append( + "Set-Cookie", + `${COOKIE_KEYS.API_KEY}=${encodeURIComponent(key)}; ${attrs}; HttpOnly`, + ); + response.headers.append( + "Set-Cookie", + `${COOKIE_KEYS.API_KEY_META}=${encodeURIComponent(JSON.stringify(meta))}; ${attrs}`, + ); +} + +export function clearApiKeyCookies(response: NextResponse): void { + const secure = process.env.NODE_ENV === "production" ? "; Secure" : ""; + const attrs = `Path=/; Max-Age=0; SameSite=Lax${secure}`; + + response.headers.append( + "Set-Cookie", + `${COOKIE_KEYS.API_KEY}=; ${attrs}; HttpOnly`, + ); + response.headers.append( + "Set-Cookie", + `${COOKIE_KEYS.API_KEY_META}=; ${attrs}`, + ); } /** Set the role cookie by appending a raw Set-Cookie header (won't overwrite existing cookies). */ diff --git a/app/lib/constants.ts b/app/lib/constants.ts index 53d17407..03e240f7 100644 --- a/app/lib/constants.ts +++ b/app/lib/constants.ts @@ -16,7 +16,6 @@ export const FeatureFlag = { export type FeatureFlagKey = (typeof FeatureFlag)[keyof typeof FeatureFlag]; export const STORAGE_KEYS = { - API_KEYS: "kaapi_api_keys", SESSION: "kaapi_session", CONFIGS_CACHE: "kaapi_configs_cache", COLLECTION_CACHE: "collection_job_cache", @@ -28,8 +27,12 @@ export const STORAGE_KEYS = { export const COOKIE_KEYS = { ROLE: "kaapi_role", FEATURES: "kaapi_features", + API_KEY: "kaapi_api_key", + API_KEY_META: "kaapi_api_key_meta", } as const; +export const AUTH_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; + /** localStorage key for the config cache */ export const CACHE_KEY = STORAGE_KEYS.CONFIGS_CACHE; diff --git a/app/lib/context/AuthContext.tsx b/app/lib/context/AuthContext.tsx index 00b2a86f..e007ac3d 100644 --- a/app/lib/context/AuthContext.tsx +++ b/app/lib/context/AuthContext.tsx @@ -17,11 +17,12 @@ import { import { apiFetch } from "@/app/lib/apiClient"; import { AUTH_EXPIRED_EVENT, + COOKIE_KEYS, FEATURES_UPDATED_EVENT, STORAGE_KEYS, } from "@/app/lib/constants"; import { useChatStore } from "@/app/lib/store/chat"; -import { clearAllStorage } from "@/app/lib/utils"; +import { clearAllStorage, readClientCookie } from "@/app/lib/utils"; export type { User, GoogleProfile, Session } from "@/app/lib/types/auth"; const AuthContext = createContext(null); @@ -32,11 +33,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const [currentUser, setCurrentUser] = useState(null); const [session, setSession] = useState(null); - // Initialize from localStorage after hydration useEffect(() => { try { - const storedKeys = localStorage.getItem(STORAGE_KEYS.API_KEYS); - if (storedKeys) setApiKeys(JSON.parse(storedKeys)); + const rawMeta = readClientCookie(COOKIE_KEYS.API_KEY_META); + if (rawMeta) setApiKeys([JSON.parse(rawMeta) as APIKey]); const storedSession = localStorage.getItem(STORAGE_KEYS.SESSION); if (storedSession) { @@ -53,7 +53,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Always fetch the latest user profile on hydration. useEffect(() => { if (!isHydrated) return; - const hasApiKey = !!apiKeys[0]?.key; + const hasApiKey = apiKeys.length > 0; const hasSession = !!session; if (!hasApiKey && !hasSession) return; @@ -93,25 +93,32 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }; }, [apiKeys, session, isHydrated]); - const persist = useCallback((keys: APIKey[]) => { - setApiKeys(keys); - if (keys.length > 0) { - localStorage.setItem(STORAGE_KEYS.API_KEYS, JSON.stringify(keys)); - } else { - localStorage.removeItem(STORAGE_KEYS.API_KEYS); - } - window.dispatchEvent(new Event("kaapi-auth-changed")); - }, []); - const addKey = useCallback( - (key: APIKey) => persist([...apiKeys, key]), - [apiKeys, persist], - ); - const removeKey = useCallback( - (id: string) => persist(apiKeys.filter((k) => k.id !== id)), - [apiKeys, persist], + async (input: { key: string; label: string; provider?: string }) => { + const res = await fetch("/api/apikeys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify(input), + }); + const body = (await res.json().catch(() => ({}))) as { + data?: APIKey; + error?: string; + }; + if (!res.ok || !body.data) { + throw new Error(body.error || "Failed to add API key"); + } + setApiKeys([body.data]); + window.dispatchEvent(new Event("kaapi-auth-changed")); + }, + [], ); - const setKeys = useCallback((keys: APIKey[]) => persist(keys), [persist]); + + const removeKey = useCallback(async () => { + await fetch("/api/apikeys", { method: "DELETE", credentials: "include" }); + setApiKeys([]); + window.dispatchEvent(new Event("kaapi-auth-changed")); + }, []); const loginWithToken = useCallback( (accessToken: string, user?: User, googleProfile?: GoogleProfile) => { @@ -143,7 +150,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { useChatStore.getState().reset(); setApiKeys([]); window.dispatchEvent(new Event("kaapi-auth-changed")); - window.location.replace("/evaluations"); + window.location.replace("/chat"); }, []); // logout when both access + refresh tokens are expired @@ -206,7 +213,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { hasFeature, addKey, removeKey, - setKeys, loginWithToken, logout, }} diff --git a/app/lib/guardrailsClient.ts b/app/lib/guardrailsClient.ts index c7d90eac..919bdcc2 100644 --- a/app/lib/guardrailsClient.ts +++ b/app/lib/guardrailsClient.ts @@ -1,4 +1,5 @@ import { NextRequest } from "next/server"; +import { getRequestApiKey } from "@/app/lib/apiClient"; const GUARDRAILS_URL = process.env.GUARDRAILS_URL || "http://localhost:8001"; @@ -22,7 +23,7 @@ export async function guardrailsClient( if (token) { headers.set("Authorization", `Bearer ${token}`); } else { - const apiKey = request.headers.get("X-API-KEY") || ""; + const apiKey = getRequestApiKey(request); const cookie = request.headers.get("Cookie") || ""; if (apiKey) headers.set("X-API-KEY", apiKey); if (cookie) headers.set("Cookie", cookie); diff --git a/app/lib/types/auth.ts b/app/lib/types/auth.ts index cfba47da..cb4287b7 100644 --- a/app/lib/types/auth.ts +++ b/app/lib/types/auth.ts @@ -1,4 +1,5 @@ import { APIKey } from "./credentials"; +import type { FeatureFlagKey } from "@/app/lib/constants"; export interface User { id: number; @@ -9,6 +10,11 @@ export interface User { features?: string[]; } +export interface UserLike { + is_superuser?: boolean; + features?: FeatureFlagKey[]; +} + export interface GoogleProfile { email?: string; name?: string; @@ -71,9 +77,12 @@ export interface AuthContextValue { isAuthenticated: boolean; features: string[]; hasFeature: (flag: string) => boolean; - addKey: (key: APIKey) => void; - removeKey: (id: string) => void; - setKeys: (keys: APIKey[]) => void; + addKey: (input: { + key: string; + label: string; + provider?: string; + }) => Promise; + removeKey: (id?: string) => Promise; loginWithToken: ( accessToken: string, user?: User, diff --git a/app/lib/types/credentials.ts b/app/lib/types/credentials.ts index 997b998e..00f4b7b3 100644 --- a/app/lib/types/credentials.ts +++ b/app/lib/types/credentials.ts @@ -106,7 +106,22 @@ export const PROVIDERS: ProviderDef[] = [ export interface APIKey { id: string; label: string; - key: string; provider: string; + masked?: string; createdAt?: string; + key?: string; +} + +export interface ApiKeyMeta { + id: string; + label: string; + provider: string; + masked: string; + createdAt: string; +} + +export interface AddApiKeyRequest { + key?: string; + label?: string; + provider?: string; } diff --git a/app/lib/utils.ts b/app/lib/utils.ts index a3a9e1ed..1c3aad16 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -69,18 +69,14 @@ export const invalidateConfigCache = (): void => { clearConfigCache(); }; -export const getApiKey = (): string | null => { - if (typeof window === "undefined") return null; - try { - const stored = localStorage.getItem(STORAGE_KEYS.API_KEYS); - if (stored) { - const keys = JSON.parse(stored); - return keys.length > 0 ? keys[0].key : null; - } - } catch (e) { - console.error("Failed to get API key:", e); - } - return null; +export const readClientCookie = (name: string): string | undefined => { + if (typeof document === "undefined") return undefined; + const prefix = `${name}=`; + const entry = document.cookie + .split(";") + .map((part) => part.trim()) + .find((part) => part.startsWith(prefix)); + return entry ? decodeURIComponent(entry.slice(prefix.length)) : undefined; }; /** From ce5a5f9d06ef3ed5a2c149cdf4b67f4b286845a1 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:40:49 +0530 Subject: [PATCH 2/5] fix(*): fix the keystore routing if the user is not logged in --- middleware.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index a49a5eae..34dec534 100644 --- a/middleware.ts +++ b/middleware.ts @@ -46,9 +46,10 @@ export function middleware(request: NextRequest) { ); const isAuthenticated = role === "superuser" || role === "user"; const isSuperuser = role === "superuser"; + const hasApiKey = !!request.cookies.get(COOKIE_KEYS.API_KEY)?.value; if (GUEST_ONLY_ROUTES.has(pathname)) { - if (isAuthenticated) { + if (hasApiKey) { return NextResponse.redirect(new URL(HOME_ROUTE, request.url)); } return NextResponse.next(); From adee97ea452d59d512f0c5cd63d4aded872867e9 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:01:12 +0530 Subject: [PATCH 3/5] fix(*): handle few edge cases --- app/components/keystore/AddKeyModal.tsx | 10 ---------- app/components/keystore/KeysCard.tsx | 2 +- middleware.ts | 3 +-- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/app/components/keystore/AddKeyModal.tsx b/app/components/keystore/AddKeyModal.tsx index c6ae1b04..9ac698bc 100644 --- a/app/components/keystore/AddKeyModal.tsx +++ b/app/components/keystore/AddKeyModal.tsx @@ -1,7 +1,6 @@ "use client"; import { Button, Field, Modal, Select } from "@/app/components/ui"; -import { InfoIcon } from "@/app/components/icons"; const PROVIDERS = [{ value: "Kaapi", label: "Kaapi" }]; @@ -73,15 +72,6 @@ export default function AddKeyModal({ placeholder="Paste your API key here" />
- -
-
- -

- API keys are stored in your browser's local storage. -

-
-
diff --git a/app/components/keystore/KeysCard.tsx b/app/components/keystore/KeysCard.tsx index 5390600c..59e386ed 100644 --- a/app/components/keystore/KeysCard.tsx +++ b/app/components/keystore/KeysCard.tsx @@ -88,7 +88,7 @@ export default function KeysCard({ function InlineNotice({ children }: { children: React.ReactNode }) { return ( -
+

{children}

diff --git a/middleware.ts b/middleware.ts index 34dec534..a49a5eae 100644 --- a/middleware.ts +++ b/middleware.ts @@ -46,10 +46,9 @@ export function middleware(request: NextRequest) { ); const isAuthenticated = role === "superuser" || role === "user"; const isSuperuser = role === "superuser"; - const hasApiKey = !!request.cookies.get(COOKIE_KEYS.API_KEY)?.value; if (GUEST_ONLY_ROUTES.has(pathname)) { - if (hasApiKey) { + if (isAuthenticated) { return NextResponse.redirect(new URL(HOME_ROUTE, request.url)); } return NextResponse.next(); From 195d137dbbeb823e4eae6f6d6a19ce84e6986c81 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:06:15 +0530 Subject: [PATCH 4/5] fix(security): code clenaps and coderabbit suggestions --- app/components/keystore/KeysCard.tsx | 4 ++-- app/lib/authCookie.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/keystore/KeysCard.tsx b/app/components/keystore/KeysCard.tsx index 59e386ed..79aa2404 100644 --- a/app/components/keystore/KeysCard.tsx +++ b/app/components/keystore/KeysCard.tsx @@ -43,8 +43,8 @@ export default function KeysCard({
Only one API key can be stored at a time. Delete this key to add a - different one. For your security, the key is stored server-side and - cannot be viewed again after it is added. + different one. For your security, the raw key is kept in an HttpOnly + cookie and cannot be viewed again after it is added. {apiKeys.map((apiKey) => ( diff --git a/app/lib/authCookie.ts b/app/lib/authCookie.ts index eebb91d4..7255da77 100644 --- a/app/lib/authCookie.ts +++ b/app/lib/authCookie.ts @@ -47,7 +47,7 @@ export function setRoleCookieFromBody( const value = user.is_superuser ? "superuser" : "user"; const secure = process.env.NODE_ENV === "production" ? "; Secure" : ""; - const cookie = `${COOKIE_KEYS.ROLE}=${value}; Path=/; Max-Age=${60 * 60 * 24 * 7}; SameSite=Lax${secure}`; + const cookie = `${COOKIE_KEYS.ROLE}=${value}; Path=/; Max-Age=${AUTH_COOKIE_MAX_AGE}; SameSite=Lax${secure}`; response.headers.append("Set-Cookie", cookie); } @@ -71,7 +71,7 @@ export function setFeaturesCookieFromBody( const features = user.features.filter((f) => typeof f === "string"); const value = encodeURIComponent(JSON.stringify(features)); const secure = process.env.NODE_ENV === "production" ? "; Secure" : ""; - const cookie = `${COOKIE_KEYS.FEATURES}=${value}; Path=/; Max-Age=${60 * 60 * 24 * 7}; SameSite=Lax${secure}`; + const cookie = `${COOKIE_KEYS.FEATURES}=${value}; Path=/; Max-Age=${AUTH_COOKIE_MAX_AGE}; SameSite=Lax${secure}`; response.headers.append("Set-Cookie", cookie); } From 8dac10ba12decae9f09e36c253a971b23107cd1c Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:34:21 +0530 Subject: [PATCH 5/5] fix(security): code clenaps and reviewers suggestions --- app/(main)/keystore/page.tsx | 11 +------- app/api/apikeys/route.ts | 2 -- app/components/keystore/AddKeyModal.tsx | 27 ++++++------------ app/components/keystore/KeysCard.tsx | 6 ++-- app/lib/context/AuthContext.tsx | 37 ++++++++++++------------- app/lib/types/auth.ts | 6 +--- app/lib/types/credentials.ts | 3 -- 7 files changed, 31 insertions(+), 61 deletions(-) diff --git a/app/(main)/keystore/page.tsx b/app/(main)/keystore/page.tsx index 8e97a4dc..dcf330ee 100644 --- a/app/(main)/keystore/page.tsx +++ b/app/(main)/keystore/page.tsx @@ -22,13 +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 [isValidating, setIsValidating] = useState(false); const resetForm = () => { setNewKeyLabel(""); setNewKeyValue(""); - setNewKeyProvider("Kaapi"); }; const handleAddKey = async () => { @@ -38,13 +36,8 @@ export default function KaapiKeystore() { } setIsValidating(true); - const newKey = { - key: newKeyValue.trim(), - label: newKeyLabel.trim(), - provider: newKeyProvider, - }; try { - await addKey(newKey); + await addKey({ key: newKeyValue.trim(), label: newKeyLabel.trim() }); resetForm(); setIsModalOpen(false); toast.success("API key added successfully"); @@ -96,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} /> diff --git a/app/api/apikeys/route.ts b/app/api/apikeys/route.ts index 3ce2fb9a..8b161257 100644 --- a/app/api/apikeys/route.ts +++ b/app/api/apikeys/route.ts @@ -21,7 +21,6 @@ export async function POST(request: NextRequest) { const key = body.key?.trim(); const label = body.label?.trim(); - const provider = body.provider?.trim() || "Kaapi"; if (!key || !label) { return NextResponse.json( @@ -57,7 +56,6 @@ export async function POST(request: NextRequest) { const meta: ApiKeyMeta = { id: crypto.randomUUID(), label, - provider, masked: maskKey(key), createdAt: new Date().toISOString(), }; diff --git a/app/components/keystore/AddKeyModal.tsx b/app/components/keystore/AddKeyModal.tsx index 9ac698bc..c994216f 100644 --- a/app/components/keystore/AddKeyModal.tsx +++ b/app/components/keystore/AddKeyModal.tsx @@ -1,18 +1,15 @@ "use client"; -import { Button, Field, Modal, Select } from "@/app/components/ui"; - -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; } @@ -21,11 +18,9 @@ export default function AddKeyModal({ open, newKeyLabel, newKeyValue, - newKeyProvider, isValidating, onLabelChange, onValueChange, - onProviderChange, onAddKey, onClose, }: AddKeyModalProps) { @@ -42,20 +37,16 @@ export default function AddKeyModal({

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.

-
- -