From c5acc6312921fdd9009796113488400c2ac1d14c Mon Sep 17 00:00:00 2001 From: "jeetul.samaiya" Date: Tue, 26 Aug 2025 18:51:06 +0530 Subject: [PATCH 1/6] Add support for custom headers and migrate from legacy auth - Introduced CustomHeaders component for managing custom headers. - Updated Sidebar to include CustomHeaders component. - Enhanced useConnection hook to handle custom headers and legacy auth migration. - Added tests for custom headers functionality. - Updated package.json and package-lock.json for new dependencies. --- client/package.json | 2 + client/src/App.tsx | 43 ++++ client/src/components/CustomHeaders.tsx | 215 ++++++++++++++++++ client/src/components/Sidebar.tsx | 54 ++--- .../src/components/__tests__/Sidebar.test.tsx | 4 + client/src/components/ui/switch.tsx | 27 +++ .../hooks/__tests__/useConnection.test.tsx | 130 +++++++++++ client/src/lib/hooks/useConnection.ts | 60 ++++- client/src/lib/types/customHeaders.ts | 59 +++++ package-lock.json | 40 ++++ server/package.json | 3 + server/src/index.ts | 17 ++ 12 files changed, 606 insertions(+), 48 deletions(-) create mode 100644 client/src/components/CustomHeaders.tsx create mode 100644 client/src/components/ui/switch.tsx create mode 100644 client/src/lib/types/customHeaders.ts diff --git a/client/package.json b/client/package.json index 61f65b623..6b84b1062 100644 --- a/client/package.json +++ b/client/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-popover": "^1.1.3", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-tooltip": "^1.1.8", @@ -69,6 +70,7 @@ "globals": "^15.9.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-fixed-jsdom": "^0.0.9", "postcss": "^8.5.6", "tailwindcss": "^3.4.13", "tailwindcss-animate": "^1.0.7", diff --git a/client/src/App.tsx b/client/src/App.tsx index 60ce90c51..ff44759eb 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -74,6 +74,7 @@ import ElicitationTab, { PendingElicitationRequest, ElicitationResponse, } from "./components/ElicitationTab"; +import { CustomHeaders, migrateFromLegacyAuth } from "./lib/types/customHeaders"; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; @@ -127,6 +128,28 @@ const App = () => { return localStorage.getItem("lastOauthScope") || ""; }); + // Custom headers state with migration from legacy auth + const [customHeaders, setCustomHeaders] = useState(() => { + const savedHeaders = localStorage.getItem("lastCustomHeaders"); + if (savedHeaders) { + try { + return JSON.parse(savedHeaders); + } catch { + // Fall back to migration if JSON parsing fails + } + } + + // Migrate from legacy auth if available + const legacyToken = localStorage.getItem("lastBearerToken") || ""; + const legacyHeaderName = localStorage.getItem("lastHeaderName") || ""; + + if (legacyToken) { + return migrateFromLegacyAuth(legacyToken, legacyHeaderName); + } + + return []; + }); + const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< PendingRequest & { @@ -215,6 +238,7 @@ const App = () => { env, bearerToken, headerName, + customHeaders, oauthClientId, oauthScope, config, @@ -310,6 +334,23 @@ const App = () => { localStorage.setItem("lastHeaderName", headerName); }, [headerName]); + useEffect(() => { + localStorage.setItem("lastCustomHeaders", JSON.stringify(customHeaders)); + }, [customHeaders]); + + // Auto-migrate from legacy auth when custom headers are empty but legacy auth exists + useEffect(() => { + if (customHeaders.length === 0 && (bearerToken || headerName)) { + const migratedHeaders = migrateFromLegacyAuth(bearerToken, headerName); + if (migratedHeaders.length > 0) { + setCustomHeaders(migratedHeaders); + // Clear legacy auth after migration + setBearerToken(""); + setHeaderName(""); + } + } + }, [bearerToken, headerName, customHeaders, setCustomHeaders]); + useEffect(() => { localStorage.setItem("lastOauthClientId", oauthClientId); }, [oauthClientId]); @@ -814,6 +855,8 @@ const App = () => { setBearerToken={setBearerToken} headerName={headerName} setHeaderName={setHeaderName} + customHeaders={customHeaders} + setCustomHeaders={setCustomHeaders} oauthClientId={oauthClientId} setOauthClientId={setOauthClientId} oauthScope={oauthScope} diff --git a/client/src/components/CustomHeaders.tsx b/client/src/components/CustomHeaders.tsx new file mode 100644 index 000000000..9196480ef --- /dev/null +++ b/client/src/components/CustomHeaders.tsx @@ -0,0 +1,215 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { Plus, Trash2, Eye, EyeOff } from "lucide-react"; +import { CustomHeaders as CustomHeadersType, CustomHeader, createEmptyHeader } from "@/lib/types/customHeaders"; + +interface CustomHeadersProps { + headers: CustomHeadersType; + onChange: (headers: CustomHeadersType) => void; + className?: string; +} + +const CustomHeaders = ({ headers, onChange, className }: CustomHeadersProps) => { + const [isJsonMode, setIsJsonMode] = useState(false); + const [jsonValue, setJsonValue] = useState(""); + const [jsonError, setJsonError] = useState(null); + const [visibleValues, setVisibleValues] = useState>(new Set()); + + const updateHeader = (index: number, field: keyof CustomHeader, value: string | boolean) => { + const newHeaders = [...headers]; + newHeaders[index] = { ...newHeaders[index], [field]: value }; + onChange(newHeaders); + }; + + const addHeader = () => { + onChange([...headers, createEmptyHeader()]); + }; + + const removeHeader = (index: number) => { + const newHeaders = headers.filter((_, i) => i !== index); + onChange(newHeaders); + }; + + const toggleValueVisibility = (index: number) => { + const newVisible = new Set(visibleValues); + if (newVisible.has(index)) { + newVisible.delete(index); + } else { + newVisible.add(index); + } + setVisibleValues(newVisible); + }; + + const switchToJsonMode = () => { + const jsonObject: Record = {}; + headers.forEach((header) => { + if (header.enabled && header.name.trim() && header.value.trim()) { + jsonObject[header.name.trim()] = header.value.trim(); + } + }); + setJsonValue(JSON.stringify(jsonObject, null, 2)); + setJsonError(null); + setIsJsonMode(true); + }; + + const switchToFormMode = () => { + try { + const parsed = JSON.parse(jsonValue); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + setJsonError("JSON must be an object with string key-value pairs"); + return; + } + + const newHeaders: CustomHeadersType = Object.entries(parsed).map(([name, value]) => ({ + name, + value: String(value), + enabled: true, + })); + + onChange(newHeaders); + setJsonError(null); + setIsJsonMode(false); + } catch (error) { + setJsonError("Invalid JSON format"); + } + }; + + const handleJsonChange = (value: string) => { + setJsonValue(value); + setJsonError(null); + }; + + if (isJsonMode) { + return ( +
+
+

Custom Headers (JSON)

+ +
+
+