diff --git a/surfsense_browser_extension/routes/index.tsx b/surfsense_browser_extension/routes/index.tsx index 8df110be1..39aed5854 100644 --- a/surfsense_browser_extension/routes/index.tsx +++ b/surfsense_browser_extension/routes/index.tsx @@ -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 = () => ( diff --git a/surfsense_browser_extension/routes/pages/ApiKeyForm.tsx b/surfsense_browser_extension/routes/pages/ApiKeyForm.tsx index b6deb1c05..537eba3da 100644 --- a/surfsense_browser_extension/routes/pages/ApiKeyForm.tsx +++ b/surfsense_browser_extension/routes/pages/ApiKeyForm.tsx @@ -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 (
+
+ +
+
SurfSense diff --git a/surfsense_browser_extension/routes/pages/HomePage.tsx b/surfsense_browser_extension/routes/pages/HomePage.tsx index 362c64056..9d8787d29 100644 --- a/surfsense_browser_extension/routes/pages/HomePage.tsx +++ b/surfsense_browser_extension/routes/pages/HomePage.tsx @@ -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 { + 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 ; } else { @@ -344,15 +364,18 @@ const HomePage = () => {

SurfSense

- +
+ + +
diff --git a/surfsense_browser_extension/routes/ui/connection-settings-button.tsx b/surfsense_browser_extension/routes/ui/connection-settings-button.tsx new file mode 100644 index 000000000..f68f252a5 --- /dev/null +++ b/surfsense_browser_extension/routes/ui/connection-settings-button.tsx @@ -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; +}; + +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 ( + <> + + + + + Connection Settings + + Leave blank to use the default SurfSense backend URL. + + + +
+ + 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" + /> +

Default: {DEFAULT_BACKEND_BASE_URL}

+
+ + + + + +
+
+ + ); +} diff --git a/surfsense_browser_extension/utils/backend-url.ts b/surfsense_browser_extension/utils/backend-url.ts new file mode 100644 index 000000000..b295bf963 --- /dev/null +++ b/surfsense_browser_extension/utils/backend-url.ts @@ -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}`; +} diff --git a/surfsense_web/app/verify-token/route.ts b/surfsense_web/app/verify-token/route.ts new file mode 100644 index 000000000..1c11d6ce0 --- /dev/null +++ b/surfsense_web/app/verify-token/route.ts @@ -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", + }, + }); +}