add configurable extension backend url and login support

This commit is contained in:
Aleksas Pielikis 2026-03-03 12:50:26 +02:00
parent 026e653c7a
commit eb8a9dd1f0
6 changed files with 228 additions and 20 deletions

View file

@ -2,7 +2,7 @@ import { Route, Routes } from "react-router-dom";
import ApiKeyForm from "./pages/ApiKeyForm";
import HomePage from "./pages/HomePage";
import "../tailwind.css";
import "~tailwind.css";
export const Routing = () => (
<Routes>

View file

@ -4,6 +4,8 @@ import { ReloadIcon } from "@radix-ui/react-icons";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "~/routes/ui/button";
import { ConnectionSettingsButton } from "~/routes/ui/connection-settings-button";
import { buildBackendUrl } from "~utils/backend-url";
const ApiKeyForm = () => {
const navigation = useNavigate();
@ -27,8 +29,7 @@ const ApiKeyForm = () => {
setLoading(true);
try {
// Verify token is valid by making a request to the API
const response = await fetch(`${process.env.PLASMO_PUBLIC_BACKEND_URL}/verify-token`, {
const response = await fetch(await buildBackendUrl("/verify-token"), {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
@ -53,6 +54,10 @@ const ApiKeyForm = () => {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 flex flex-col items-center justify-center p-6">
<div className="w-full max-w-md mx-auto space-y-8">
<div className="flex justify-end">
<ConnectionSettingsButton />
</div>
<div className="flex flex-col items-center space-y-2">
<div className="bg-gray-800 p-3 rounded-full ring-2 ring-gray-700 shadow-lg">
<img className="w-12 h-12" src={icon} alt="SurfSense" />

View file

@ -16,6 +16,7 @@ import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { cn } from "~/lib/utils";
import { Button } from "~/routes/ui/button";
import { ConnectionSettingsButton } from "~/routes/ui/connection-settings-button";
import {
Command,
CommandEmpty,
@ -27,6 +28,7 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from "~/routes/ui/popover";
import { Label } from "~routes/ui/label";
import { useToast } from "~routes/ui/use-toast";
import { buildBackendUrl } from "~utils/backend-url";
import { getRenderedHtml } from "~utils/commons";
import type { WebHistory } from "~utils/interfaces";
import Loading from "./Loading";
@ -45,15 +47,19 @@ const HomePage = () => {
const checkSearchSpaces = async () => {
const storage = new Storage({ area: "local" });
const token = await storage.get("token");
if (!token) {
setLoading(false);
navigation("/login");
return;
}
try {
const response = await fetch(
`${process.env.PLASMO_PUBLIC_BACKEND_URL}/api/v1/searchspaces`,
{
headers: {
Authorization: `Bearer ${token}`,
},
const response = await fetch(await buildBackendUrl("/api/v1/searchspaces"), {
headers: {
Authorization: `Bearer ${token}`,
}
);
});
if (!response.ok) {
throw new Error("Token verification failed");
@ -66,11 +72,12 @@ const HomePage = () => {
await storage.remove("token");
await storage.remove("showShadowDom");
navigation("/login");
} finally {
setLoading(false);
}
};
checkSearchSpaces();
setLoading(false);
}, []);
useEffect(() => {
@ -304,6 +311,19 @@ const HomePage = () => {
navigation("/login");
}
async function handleConnectionSaved(changed: boolean): Promise<void> {
if (!changed) {
return;
}
const storage = new Storage({ area: "local" });
await storage.remove("token");
await storage.remove("showShadowDom");
await storage.remove("search_space");
await storage.remove("search_space_id");
navigation("/login");
}
if (loading) {
return <Loading />;
} else {
@ -344,15 +364,18 @@ const HomePage = () => {
</div>
<h1 className="text-xl font-semibold text-white">SurfSense</h1>
</div>
<Button
variant="ghost"
size="icon"
onClick={logOut}
className="rounded-full text-gray-400 hover:bg-gray-800 hover:text-white"
>
<ExitIcon className="h-4 w-4" />
<span className="sr-only">Log out</span>
</Button>
<div className="flex items-center gap-1">
<ConnectionSettingsButton onSaved={handleConnectionSaved} />
<Button
variant="ghost"
size="icon"
onClick={logOut}
className="rounded-full text-gray-400 hover:bg-gray-800 hover:text-white"
>
<ExitIcon className="h-4 w-4" />
<span className="sr-only">Log out</span>
</Button>
</div>
</div>
<div className="space-y-3 py-4">

View file

@ -0,0 +1,114 @@
import { GearIcon } from "@radix-ui/react-icons";
import { useEffect, useState } from "react";
import { Button } from "~/routes/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/routes/ui/dialog";
import { Label } from "~/routes/ui/label";
import {
DEFAULT_BACKEND_BASE_URL,
getCustomBackendBaseUrl,
normalizeBackendBaseUrl,
setCustomBackendBaseUrl,
} from "~utils/backend-url";
type ConnectionSettingsButtonProps = {
onSaved?: (changed: boolean) => void | Promise<void>;
};
export function ConnectionSettingsButton({ onSaved }: ConnectionSettingsButtonProps) {
const [open, setOpen] = useState(false);
const [customUrl, setCustomUrl] = useState("");
const [savedUrl, setSavedUrl] = useState("");
useEffect(() => {
if (!open) {
return;
}
const loadSettings = async () => {
const normalized = await getCustomBackendBaseUrl();
setCustomUrl(normalized || DEFAULT_BACKEND_BASE_URL);
setSavedUrl(normalized);
};
loadSettings();
}, [open]);
const handleSave = async () => {
const normalizedUrl = normalizeBackendBaseUrl(customUrl);
const nextUrl = await setCustomBackendBaseUrl(
normalizedUrl === DEFAULT_BACKEND_BASE_URL ? "" : normalizedUrl
);
const changed = nextUrl !== savedUrl;
setSavedUrl(nextUrl);
setCustomUrl(nextUrl || DEFAULT_BACKEND_BASE_URL);
setOpen(false);
if (onSaved) {
await onSaved(changed);
}
};
return (
<>
<Button
variant="ghost"
size="icon"
onClick={() => setOpen(true)}
className="rounded-full text-gray-400 hover:bg-gray-800 hover:text-white"
>
<GearIcon className="h-4 w-4" />
<span className="sr-only">Connection settings</span>
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-md border-gray-700 bg-gray-800 text-white">
<DialogHeader>
<DialogTitle>Connection Settings</DialogTitle>
<DialogDescription className="text-gray-400">
Leave blank to use the default SurfSense backend URL.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="backendBaseUrl" className="text-gray-300">
Custom Backend URL
</Label>
<input
id="backendBaseUrl"
type="url"
value={customUrl}
onChange={(event) => setCustomUrl(event.target.value)}
placeholder={DEFAULT_BACKEND_BASE_URL}
className="w-full rounded-md border border-gray-700 bg-gray-900 px-3 py-2 text-white placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
<p className="text-xs text-gray-500">Default: {DEFAULT_BACKEND_BASE_URL}</p>
</div>
<DialogFooter className="gap-2">
<Button
type="button"
variant="outline"
onClick={() => setCustomUrl(DEFAULT_BACKEND_BASE_URL)}
className="border-gray-700 bg-gray-900 text-gray-200 hover:bg-gray-700"
>
Use Default
</Button>
<Button
type="button"
onClick={handleSave}
className="bg-teal-600 text-white hover:bg-teal-500"
>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View file

@ -0,0 +1,41 @@
import { Storage } from "@plasmohq/storage";
export const BACKEND_URL_STORAGE_KEY = "backend_base_url";
export const FALLBACK_BACKEND_BASE_URL = "https://www.surfsense.com";
const storage = new Storage({ area: "local" });
export function normalizeBackendBaseUrl(url: string) {
return url.trim().replace(/\/+$/, "");
}
export const DEFAULT_BACKEND_BASE_URL = normalizeBackendBaseUrl(
process.env.PLASMO_PUBLIC_BACKEND_URL || FALLBACK_BACKEND_BASE_URL
);
export async function getCustomBackendBaseUrl() {
const value = await storage.get(BACKEND_URL_STORAGE_KEY);
return typeof value === "string" ? normalizeBackendBaseUrl(value) : "";
}
export async function setCustomBackendBaseUrl(url: string) {
const normalized = normalizeBackendBaseUrl(url);
if (normalized) {
await storage.set(BACKEND_URL_STORAGE_KEY, normalized);
return normalized;
}
await storage.remove(BACKEND_URL_STORAGE_KEY);
return "";
}
export async function getBackendBaseUrl() {
return (await getCustomBackendBaseUrl()) || DEFAULT_BACKEND_BASE_URL;
}
export async function buildBackendUrl(path: string) {
const baseUrl = await getBackendBaseUrl();
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
return `${baseUrl}${normalizedPath}`;
}

View file

@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from "next/server";
const backendBaseUrl = (process.env.INTERNAL_FASTAPI_BACKEND_URL || "http://backend:8000").replace(
/\/+$/,
""
);
export async function GET(request: NextRequest) {
const response = await fetch(`${backendBaseUrl}/verify-token`, {
method: "GET",
headers: {
Authorization: request.headers.get("authorization") || "",
"X-API-Key": request.headers.get("x-api-key") || "",
},
cache: "no-store",
});
return new NextResponse(response.body, {
status: response.status,
headers: {
"content-type": response.headers.get("content-type") || "application/json",
"cache-control": "no-store",
},
});
}