aboutsummaryrefslogtreecommitdiffstats
path: root/apps/browser-extension/src
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-11-09 12:21:54 +0000
committerGitHub <noreply@github.com>2025-11-09 12:21:54 +0000
commitec87813a257e63f8a161e7bc04679e9fab6fbcf6 (patch)
treea6f20a7b036ea35656e0f3ca9dc9f3d63a1b7793 /apps/browser-extension/src
parent725b5218ea03d677cebbe62aadd2d227f8b6e214 (diff)
downloadkarakeep-ec87813a257e63f8a161e7bc04679e9fab6fbcf6.tar.zst
feat(extension): Add custom header support for extension (#2111)
Fixes #1287
Diffstat (limited to 'apps/browser-extension/src')
-rw-r--r--apps/browser-extension/src/CustomHeadersPage.tsx155
-rw-r--r--apps/browser-extension/src/NotConfiguredPage.tsx13
-rw-r--r--apps/browser-extension/src/SignInPage.tsx15
-rw-r--r--apps/browser-extension/src/main.tsx2
-rw-r--r--apps/browser-extension/src/utils/settings.ts6
-rw-r--r--apps/browser-extension/src/utils/trpc.ts20
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,