From 719c2da525003b7e9dfe8e3b6c94d5a9f421b38d Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:43:11 +0530 Subject: [PATCH] feat(api-keys): implement API keys management interface and access control - Added a new WorkspaceApiAccessControl component for managing API key access within workspaces. - Enhanced the PlaygroundApiKeysPage to display API key creation options and access information. - Updated PlaygroundIndex and PlaygroundSidebar to include links to the new API keys management section. - Improved user feedback with alerts and descriptions regarding API key usage and requirements. --- .../playground/api-keys/page.tsx | 35 +++++- .../components/playground-index.tsx | 17 ++- .../ui/playground/PlaygroundSidebar.tsx | 8 +- .../settings/general-settings-manager.tsx | 43 +------- .../settings/workspace-api-access-control.tsx | 100 ++++++++++++++++++ 5 files changed, 158 insertions(+), 45 deletions(-) create mode 100644 surfsense_web/components/settings/workspace-api-access-control.tsx diff --git a/surfsense_web/app/dashboard/[workspace_id]/playground/api-keys/page.tsx b/surfsense_web/app/dashboard/[workspace_id]/playground/api-keys/page.tsx index 2df3ec58e..28a532e92 100644 --- a/surfsense_web/app/dashboard/[workspace_id]/playground/api-keys/page.tsx +++ b/surfsense_web/app/dashboard/[workspace_id]/playground/api-keys/page.tsx @@ -1,4 +1,8 @@ -import { redirect } from "next/navigation"; +import { Info } from "lucide-react"; +import { WorkspaceApiAccessControl } from "@/components/settings/workspace-api-access-control"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Separator } from "@/components/ui/separator"; +import { ApiKeyContent } from "../../user-settings/components/ApiKeyContent"; export default async function PlaygroundApiKeysPage({ params, @@ -6,6 +10,33 @@ export default async function PlaygroundApiKeysPage({ params: Promise<{ workspace_id: string }>; }) { const { workspace_id } = await params; + const workspaceId = Number(workspace_id); - redirect(`/dashboard/${workspace_id}/user-settings/api-key`); + return ( +
+
+

API keys

+

+ Create user API keys and choose whether they can access this workspace. +

+
+ + + + + External API calls need both a user API key and workspace API key access enabled. + + + +
+ +
+ + + +
+ +
+
+ ); } diff --git a/surfsense_web/app/dashboard/[workspace_id]/playground/components/playground-index.tsx b/surfsense_web/app/dashboard/[workspace_id]/playground/components/playground-index.tsx index 7e9d45373..58d56e146 100644 --- a/surfsense_web/app/dashboard/[workspace_id]/playground/components/playground-index.tsx +++ b/surfsense_web/app/dashboard/[workspace_id]/playground/components/playground-index.tsx @@ -1,6 +1,6 @@ "use client"; -import { ArrowRight, History, Info } from "lucide-react"; +import { ArrowRight, History, Info, KeyRound } from "lucide-react"; import Link from "next/link"; import { useMemo } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -27,7 +27,7 @@ export function PlaygroundIndex({ workspaceId }: { workspaceId: number }) {

Manually run SurfSense's platform-native APIs and inspect their output. To use these APIs outside SurfSense,{" "} create an API key @@ -51,6 +51,19 @@ export function PlaygroundIndex({ workspaceId }: { workspaceId: number }) { + +

+ +
+

API Keys

+

Manage keys and workspace API access

+
+
+ +
diff --git a/surfsense_web/components/layout/ui/playground/PlaygroundSidebar.tsx b/surfsense_web/components/layout/ui/playground/PlaygroundSidebar.tsx index 60e7b9846..f3fdcded3 100644 --- a/surfsense_web/components/layout/ui/playground/PlaygroundSidebar.tsx +++ b/surfsense_web/components/layout/ui/playground/PlaygroundSidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronRight, History, LayoutGrid } from "lucide-react"; +import { ChevronRight, History, KeyRound, LayoutGrid } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; @@ -28,6 +28,12 @@ export function getPlaygroundNavItems(base: string): RoutedSectionItem[] { href: `${base}/runs`, icon: , }, + { + value: "api-keys", + label: "API Keys", + href: `${base}/api-keys`, + icon: , + }, ]; } diff --git a/surfsense_web/components/settings/general-settings-manager.tsx b/surfsense_web/components/settings/general-settings-manager.tsx index 2db1b4def..c8cd906c0 100644 --- a/surfsense_web/components/settings/general-settings-manager.tsx +++ b/surfsense_web/components/settings/general-settings-manager.tsx @@ -5,20 +5,17 @@ import { useAtomValue } from "jotai"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; -import { - updateWorkspaceApiAccessMutationAtom, - updateWorkspaceMutationAtom, -} from "@/atoms/workspaces/workspace-mutation.atoms"; +import { updateWorkspaceMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; -import { Switch } from "@/components/ui/switch"; import { workspacesApiService } from "@/lib/apis/workspaces-api.service"; import { authenticatedFetch } from "@/lib/auth-fetch"; import { buildBackendUrl } from "@/lib/env-config"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { Spinner } from "../ui/spinner"; +import { WorkspaceApiAccessControl } from "./workspace-api-access-control"; interface GeneralSettingsManagerProps { workspaceId: number; @@ -39,14 +36,10 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr }); const { mutateAsync: updateWorkspace } = useAtomValue(updateWorkspaceMutationAtom); - const { mutateAsync: updateWorkspaceApiAccess } = useAtomValue( - updateWorkspaceApiAccessMutationAtom - ); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [saving, setSaving] = useState(false); - const [savingApiAccess, setSavingApiAccess] = useState(false); const [isExporting, setIsExporting] = useState(false); const hasWorkspace = !!workspace; const workspaceName = workspace?.name; @@ -121,25 +114,6 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr handleSave(); }; - const handleApiAccessToggle = useCallback( - async (enabled: boolean) => { - try { - setSavingApiAccess(true); - await updateWorkspaceApiAccess({ - id: workspaceId, - api_access_enabled: enabled, - }); - await fetchWorkspace(); - } catch (error) { - console.error("Error updating API access:", error); - toast.error(error instanceof Error ? error.message : "Failed to update API access"); - } finally { - setSavingApiAccess(false); - } - }, - [fetchWorkspace, workspaceId, updateWorkspaceApiAccess] - ); - if (loading) { return (
@@ -206,18 +180,7 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
-
-
- -

Allow API keys to access this workspace.

-
- -
+
diff --git a/surfsense_web/components/settings/workspace-api-access-control.tsx b/surfsense_web/components/settings/workspace-api-access-control.tsx new file mode 100644 index 000000000..de74d103a --- /dev/null +++ b/surfsense_web/components/settings/workspace-api-access-control.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useAtomValue } from "jotai"; +import { useCallback, useState } from "react"; +import { toast } from "sonner"; +import { updateWorkspaceApiAccessMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Switch } from "@/components/ui/switch"; +import { workspacesApiService } from "@/lib/apis/workspaces-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; +import { cn } from "@/lib/utils"; + +interface WorkspaceApiAccessControlProps { + workspaceId: number; + className?: string; +} + +export function WorkspaceApiAccessControl({ + workspaceId, + className, +}: WorkspaceApiAccessControlProps) { + const { + data: workspace, + isLoading, + isError, + refetch, + } = useQuery({ + queryKey: cacheKeys.workspaces.detail(workspaceId.toString()), + queryFn: () => workspacesApiService.getWorkspace({ id: workspaceId }), + enabled: !!workspaceId, + }); + + const { mutateAsync: updateWorkspaceApiAccess } = useAtomValue( + updateWorkspaceApiAccessMutationAtom + ); + const [savingApiAccess, setSavingApiAccess] = useState(false); + + const handleApiAccessToggle = useCallback( + async (enabled: boolean) => { + try { + setSavingApiAccess(true); + await updateWorkspaceApiAccess({ + id: workspaceId, + api_access_enabled: enabled, + }); + await refetch(); + } catch (error) { + console.error("Error updating API access:", error); + toast.error(error instanceof Error ? error.message : "Failed to update API access"); + } finally { + setSavingApiAccess(false); + } + }, + [refetch, workspaceId, updateWorkspaceApiAccess] + ); + + if (isLoading) { + return ( +
+
+ + +
+ +
+ ); + } + + if (isError) { + return ( +
+
+ +

Failed to load workspace API access.

+
+ +
+ ); + } + + return ( +
+
+ +

Allow API keys to access this workspace.

+
+ +
+ ); +}