diff options
| -rw-r--r-- | apps/browser-extension/src/CustomHeadersPage.tsx | 155 | ||||
| -rw-r--r-- | apps/browser-extension/src/NotConfiguredPage.tsx | 13 | ||||
| -rw-r--r-- | apps/browser-extension/src/SignInPage.tsx | 15 | ||||
| -rw-r--r-- | apps/browser-extension/src/main.tsx | 2 | ||||
| -rw-r--r-- | apps/browser-extension/src/utils/settings.ts | 6 | ||||
| -rw-r--r-- | apps/browser-extension/src/utils/trpc.ts | 20 |
6 files changed, 204 insertions, 7 deletions
diff --git a/apps/browser-extension/src/CustomHeadersPage.tsx b/apps/browser-extension/src/CustomHeadersPage.tsx new file mode 100644 index 00000000..1b3fd8df --- /dev/null +++ b/apps/browser-extension/src/CustomHeadersPage.tsx @@ -0,0 +1,155 @@ +import { useEffect, useState } from "react"; +import { Plus, Trash2 } from "lucide-react"; +import { useNavigate } from "react-router-dom"; + +import { Button } from "./components/ui/button"; +import { Input } from "./components/ui/input"; +import Logo from "./Logo"; +import usePluginSettings from "./utils/settings"; + +export default function CustomHeadersPage() { + const navigate = useNavigate(); + const { settings, setSettings } = usePluginSettings(); + + // Convert headers object to array of entries for easier manipulation + const [headers, setHeaders] = useState<{ key: string; value: string }[]>([]); + const [newHeaderKey, setNewHeaderKey] = useState(""); + const [newHeaderValue, setNewHeaderValue] = useState(""); + + // Update headers when settings change (e.g., when loaded from storage) + useEffect(() => { + setHeaders( + Object.entries(settings.customHeaders || {}).map(([key, value]) => ({ + key, + value, + })), + ); + }, [settings.customHeaders]); + + const handleAddHeader = () => { + if (!newHeaderKey.trim() || !newHeaderValue.trim()) { + return; + } + + // Check if header already exists + const existingIndex = headers.findIndex((h) => h.key === newHeaderKey); + if (existingIndex >= 0) { + // Update existing header + const updatedHeaders = [...headers]; + updatedHeaders[existingIndex].value = newHeaderValue; + setHeaders(updatedHeaders); + } else { + // Add new header + setHeaders([...headers, { key: newHeaderKey, value: newHeaderValue }]); + } + + setNewHeaderKey(""); + setNewHeaderValue(""); + }; + + const handleRemoveHeader = (index: number) => { + setHeaders(headers.filter((_, i) => i !== index)); + }; + + const handleSave = () => { + // Convert array back to object + const headersObject = headers.reduce( + (acc, { key, value }) => { + if (key.trim() && value.trim()) { + acc[key] = value; + } + return acc; + }, + {} as Record<string, string>, + ); + + setSettings((s) => ({ ...s, customHeaders: headersObject })); + navigate(-1); + }; + + const handleCancel = () => { + navigate(-1); + }; + + return ( + <div className="flex flex-col space-y-2"> + <Logo /> + <span className="text-lg">Custom Headers</span> + <p className="text-sm text-muted-foreground"> + Add custom HTTP headers that will be sent with every API request. + </p> + <hr /> + + {/* Existing Headers List */} + <div className="max-h-64 space-y-2 overflow-y-auto"> + {headers.length === 0 ? ( + <p className="py-4 text-center text-sm text-muted-foreground"> + No custom headers configured + </p> + ) : ( + headers.map((header, index) => ( + <div + key={index} + className="flex items-center gap-2 rounded-lg border bg-background p-3" + > + <div className="flex-1 space-y-1"> + <p className="text-sm font-semibold">{header.key}</p> + <p className="truncate text-xs text-muted-foreground"> + {header.value} + </p> + </div> + <Button + variant="ghost" + size="sm" + onClick={() => handleRemoveHeader(index)} + className="h-8 w-8 p-0 text-destructive hover:text-destructive" + > + <Trash2 className="h-4 w-4" /> + </Button> + </div> + )) + )} + </div> + + <hr /> + + {/* Add New Header */} + <div className="space-y-2"> + <p className="text-sm font-semibold">Add New Header</p> + <Input + placeholder="Header Name (e.g., X-Custom-Header)" + value={newHeaderKey} + onChange={(e) => setNewHeaderKey(e.target.value)} + autoCapitalize="none" + /> + <Input + placeholder="Header Value" + value={newHeaderValue} + onChange={(e) => setNewHeaderValue(e.target.value)} + autoCapitalize="none" + /> + <Button + variant="secondary" + onClick={handleAddHeader} + disabled={!newHeaderKey.trim() || !newHeaderValue.trim()} + className="w-full" + > + <Plus className="mr-2 h-4 w-4" /> + Add Header + </Button> + </div> + + <hr /> + + {/* Action Buttons */} + <div className="flex gap-2"> + <Button variant="outline" onClick={handleCancel} className="flex-1"> + Cancel + </Button> + <Button onClick={handleSave} className="flex-1"> + Save + </Button> + </div> + </div> + ); +} diff --git a/apps/browser-extension/src/NotConfiguredPage.tsx b/apps/browser-extension/src/NotConfiguredPage.tsx index ed8cbeb4..94b95ed4 100644 --- a/apps/browser-extension/src/NotConfiguredPage.tsx +++ b/apps/browser-extension/src/NotConfiguredPage.tsx @@ -14,6 +14,7 @@ export default function NotConfiguredPage() { const [error, setError] = useState(""); const [serverAddress, setServerAddress] = useState(settings.address); + useEffect(() => { setServerAddress(settings.address); }, [settings.address]); @@ -51,6 +52,18 @@ export default function NotConfiguredPage() { onChange={(e) => setServerAddress(e.target.value)} /> </div> + <div className="flex justify-start"> + <button + type="button" + onClick={() => navigate("/customheaders")} + className="text-xs text-muted-foreground underline hover:text-foreground" + > + Configure Custom Headers + {settings.customHeaders && + Object.keys(settings.customHeaders).length > 0 && + ` (${Object.keys(settings.customHeaders).length})`} + </button> + </div> <Button onClick={onSave}>Configure</Button> </div> ); diff --git a/apps/browser-extension/src/SignInPage.tsx b/apps/browser-extension/src/SignInPage.tsx index 1d849028..6cf8b35d 100644 --- a/apps/browser-extension/src/SignInPage.tsx +++ b/apps/browser-extension/src/SignInPage.tsx @@ -15,7 +15,7 @@ const enum LoginState { export default function SignInPage() { const navigate = useNavigate(); - const { setSettings } = usePluginSettings(); + const { settings, setSettings } = usePluginSettings(); const { mutate: login, @@ -156,6 +156,19 @@ export default function SignInPage() { Login with API key </Button> </form> + + <div className="flex justify-center pt-2"> + <button + type="button" + onClick={() => navigate("/customheaders")} + className="text-xs text-muted-foreground underline hover:text-foreground" + > + Configure Custom Headers + {settings.customHeaders && + Object.keys(settings.customHeaders).length > 0 && + ` (${Object.keys(settings.customHeaders).length})`} + </button> + </div> </div> ); } diff --git a/apps/browser-extension/src/main.tsx b/apps/browser-extension/src/main.tsx index 65456012..123e505e 100644 --- a/apps/browser-extension/src/main.tsx +++ b/apps/browser-extension/src/main.tsx @@ -6,6 +6,7 @@ import { HashRouter, Route, Routes } from "react-router-dom"; import BookmarkDeletedPage from "./BookmarkDeletedPage.tsx"; import BookmarkSavedPage from "./BookmarkSavedPage.tsx"; +import CustomHeadersPage from "./CustomHeadersPage.tsx"; import Layout from "./Layout.tsx"; import NotConfiguredPage from "./NotConfiguredPage.tsx"; import OptionsPage from "./OptionsPage.tsx"; @@ -33,6 +34,7 @@ function App() { <Route path="/notconfigured" element={<NotConfiguredPage />} /> <Route path="/options" element={<OptionsPage />} /> <Route path="/signin" element={<SignInPage />} /> + <Route path="/customheaders" element={<CustomHeadersPage />} /> </Routes> </HashRouter> </Providers> diff --git a/apps/browser-extension/src/utils/settings.ts b/apps/browser-extension/src/utils/settings.ts index c3ac50d2..463efd2b 100644 --- a/apps/browser-extension/src/utils/settings.ts +++ b/apps/browser-extension/src/utils/settings.ts @@ -7,20 +7,22 @@ export const DEFAULT_SHOW_COUNT_BADGE = false; const zSettingsSchema = z.object({ apiKey: z.string(), apiKeyId: z.string().optional(), - address: z.string(), + address: z.string().optional().default("https://cloud.karakeep.app"), theme: z.enum(["light", "dark", "system"]).optional().default("system"), showCountBadge: z.boolean().default(DEFAULT_SHOW_COUNT_BADGE), useBadgeCache: z.boolean().default(true), badgeCacheExpireMs: z.number().min(0).default(DEFAULT_BADGE_CACHE_EXPIRE_MS), + customHeaders: z.record(z.string(), z.string()).optional().default({}), }); const DEFAULT_SETTINGS: Settings = { apiKey: "", - address: "", + address: "https://cloud.karakeep.app", theme: "system", showCountBadge: DEFAULT_SHOW_COUNT_BADGE, useBadgeCache: true, badgeCacheExpireMs: DEFAULT_BADGE_CACHE_EXPIRE_MS, + customHeaders: {}, }; export type Settings = z.infer<typeof zSettingsSchema>; diff --git a/apps/browser-extension/src/utils/trpc.ts b/apps/browser-extension/src/utils/trpc.ts index 76534bcb..b3215d9d 100644 --- a/apps/browser-extension/src/utils/trpc.ts +++ b/apps/browser-extension/src/utils/trpc.ts @@ -18,10 +18,11 @@ let currentSettings: { apiKey: string; badgeCacheExpireMs: number; useBadgeCache: boolean; + customHeaders: Record<string, string>; } | null = null; export async function initializeClients() { - const { address, apiKey, badgeCacheExpireMs, useBadgeCache } = + const { address, apiKey, badgeCacheExpireMs, useBadgeCache, customHeaders } = await getPluginSettings(); if (currentSettings) { @@ -31,6 +32,9 @@ export async function initializeClients() { currentSettings.badgeCacheExpireMs !== badgeCacheExpireMs; const useBadgeCacheChanged = currentSettings.useBadgeCache !== useBadgeCache; + const customHeadersChanged = + JSON.stringify(currentSettings.customHeaders) !== + JSON.stringify(customHeaders); if (!address || !apiKey) { // Invalid configuration, clean @@ -40,7 +44,7 @@ export async function initializeClients() { return; } - if (addressChanged || apiKeyChanged) { + if (addressChanged || apiKeyChanged || customHeadersChanged) { // Switch context completely → discard the old instance and wipe persisted cache const persisterForCleanup = createChromeStorage(); await persisterForCleanup.removeClient(); @@ -58,7 +62,8 @@ export async function initializeClients() { !addressChanged && !apiKeyChanged && !cacheTimeChanged && - !useBadgeCacheChanged + !useBadgeCacheChanged && + !customHeadersChanged ) { return; } @@ -66,7 +71,13 @@ export async function initializeClients() { if (address && apiKey) { // Store current settings - currentSettings = { address, apiKey, badgeCacheExpireMs, useBadgeCache }; + currentSettings = { + address, + apiKey, + badgeCacheExpireMs, + useBadgeCache, + customHeaders, + }; // Create new QueryClient with updated settings queryClient = new QueryClient(); @@ -92,6 +103,7 @@ export async function initializeClients() { headers() { return { Authorization: `Bearer ${apiKey}`, + ...customHeaders, }; }, transformer: superjson, |
