mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
add configurable extension backend url and login support
This commit is contained in:
parent
026e653c7a
commit
eb8a9dd1f0
6 changed files with 228 additions and 20 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
41
surfsense_browser_extension/utils/backend-url.ts
Normal file
41
surfsense_browser_extension/utils/backend-url.ts
Normal 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}`;
|
||||
}
|
||||
25
surfsense_web/app/verify-token/route.ts
Normal file
25
surfsense_web/app/verify-token/route.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue