Merge pull request #857 from aleksas/extension-login-minimal-pr

add configurable extension backend url and login support
This commit is contained in:
Rohan Verma 2026-03-03 23:41:55 -08:00 committed by GitHub
commit 7df64b5c9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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 ApiKeyForm from "./pages/ApiKeyForm";
import HomePage from "./pages/HomePage"; import HomePage from "./pages/HomePage";
import "../tailwind.css"; import "~tailwind.css";
export const Routing = () => ( export const Routing = () => (
<Routes> <Routes>

View file

@ -4,6 +4,8 @@ import { ReloadIcon } from "@radix-ui/react-icons";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Button } from "~/routes/ui/button"; import { Button } from "~/routes/ui/button";
import { ConnectionSettingsButton } from "~/routes/ui/connection-settings-button";
import { buildBackendUrl } from "~utils/backend-url";
const ApiKeyForm = () => { const ApiKeyForm = () => {
const navigation = useNavigate(); const navigation = useNavigate();
@ -27,8 +29,7 @@ const ApiKeyForm = () => {
setLoading(true); setLoading(true);
try { try {
// Verify token is valid by making a request to the API const response = await fetch(await buildBackendUrl("/verify-token"), {
const response = await fetch(`${process.env.PLASMO_PUBLIC_BACKEND_URL}/verify-token`, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: `Bearer ${apiKey}`, Authorization: `Bearer ${apiKey}`,
@ -53,6 +54,10 @@ const ApiKeyForm = () => {
return ( 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="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="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="flex flex-col items-center space-y-2">
<div className="bg-gray-800 p-3 rounded-full ring-2 ring-gray-700 shadow-lg"> <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" /> <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 { useNavigate } from "react-router-dom";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { Button } from "~/routes/ui/button"; import { Button } from "~/routes/ui/button";
import { ConnectionSettingsButton } from "~/routes/ui/connection-settings-button";
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
@ -27,6 +28,7 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from "~/routes/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "~/routes/ui/popover";
import { Label } from "~routes/ui/label"; import { Label } from "~routes/ui/label";
import { useToast } from "~routes/ui/use-toast"; import { useToast } from "~routes/ui/use-toast";
import { buildBackendUrl } from "~utils/backend-url";
import { getRenderedHtml } from "~utils/commons"; import { getRenderedHtml } from "~utils/commons";
import type { WebHistory } from "~utils/interfaces"; import type { WebHistory } from "~utils/interfaces";
import Loading from "./Loading"; import Loading from "./Loading";
@ -45,15 +47,19 @@ const HomePage = () => {
const checkSearchSpaces = async () => { const checkSearchSpaces = async () => {
const storage = new Storage({ area: "local" }); const storage = new Storage({ area: "local" });
const token = await storage.get("token"); const token = await storage.get("token");
if (!token) {
setLoading(false);
navigation("/login");
return;
}
try { try {
const response = await fetch( const response = await fetch(await buildBackendUrl("/api/v1/searchspaces"), {
`${process.env.PLASMO_PUBLIC_BACKEND_URL}/api/v1/searchspaces`, headers: {
{ Authorization: `Bearer ${token}`,
headers: {
Authorization: `Bearer ${token}`,
},
} }
); });
if (!response.ok) { if (!response.ok) {
throw new Error("Token verification failed"); throw new Error("Token verification failed");
@ -66,11 +72,12 @@ const HomePage = () => {
await storage.remove("token"); await storage.remove("token");
await storage.remove("showShadowDom"); await storage.remove("showShadowDom");
navigation("/login"); navigation("/login");
} finally {
setLoading(false);
} }
}; };
checkSearchSpaces(); checkSearchSpaces();
setLoading(false);
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -304,6 +311,19 @@ const HomePage = () => {
navigation("/login"); 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) { if (loading) {
return <Loading />; return <Loading />;
} else { } else {
@ -344,15 +364,18 @@ const HomePage = () => {
</div> </div>
<h1 className="text-xl font-semibold text-white">SurfSense</h1> <h1 className="text-xl font-semibold text-white">SurfSense</h1>
</div> </div>
<Button <div className="flex items-center gap-1">
variant="ghost" <ConnectionSettingsButton onSaved={handleConnectionSaved} />
size="icon" <Button
onClick={logOut} variant="ghost"
className="rounded-full text-gray-400 hover:bg-gray-800 hover:text-white" size="icon"
> onClick={logOut}
<ExitIcon className="h-4 w-4" /> className="rounded-full text-gray-400 hover:bg-gray-800 hover:text-white"
<span className="sr-only">Log out</span> >
</Button> <ExitIcon className="h-4 w-4" />
<span className="sr-only">Log out</span>
</Button>
</div>
</div> </div>
<div className="space-y-3 py-4"> <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",
},
});
}