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.
This commit is contained in:
Anish Sarkar 2026-07-08 10:43:11 +05:30
parent 9a18c7771e
commit 719c2da525
5 changed files with 158 additions and 45 deletions

View file

@ -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 (
<div className="mx-auto w-full max-w-5xl space-y-6">
<div className="space-y-1">
<h2 className="text-xl font-semibold tracking-tight">API keys</h2>
<p className="text-sm text-muted-foreground">
Create user API keys and choose whether they can access this workspace.
</p>
</div>
<Alert>
<Info />
<AlertDescription>
External API calls need both a user API key and workspace API key access enabled.
</AlertDescription>
</Alert>
<section>
<WorkspaceApiAccessControl workspaceId={workspaceId} />
</section>
<Separator className="bg-border" />
<section>
<ApiKeyContent />
</section>
</div>
);
}

View file

@ -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 }) {
<p>
Manually run SurfSense's platform-native APIs and inspect their output. To use these APIs outside SurfSense,{" "}
<Link
href={`/dashboard/${workspaceId}/user-settings/api-key`}
href={`${base}/api-keys`}
className="font-medium text-foreground underline-offset-4 hover:underline"
>
create an API key
@ -51,6 +51,19 @@ export function PlaygroundIndex({ workspaceId }: { workspaceId: number }) {
</div>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
</Link>
<Link
href={`${base}/api-keys`}
className="flex items-center justify-between rounded-lg border border-border/60 bg-accent/40 px-4 py-3 transition-colors hover:bg-accent"
>
<div className="flex items-center gap-3">
<KeyRound className="h-5 w-5 text-muted-foreground" />
<div>
<p className="text-sm font-medium">API Keys</p>
<p className="text-xs text-muted-foreground">Manage keys and workspace API access</p>
</div>
</div>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
</Link>
</div>
<div className="space-y-6">

View file

@ -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: <History className="h-4 w-4" />,
},
{
value: "api-keys",
label: "API Keys",
href: `${base}/api-keys`,
icon: <KeyRound className="h-4 w-4" />,
},
];
}

View file

@ -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 (
<div className="space-y-4 md:space-y-6">
@ -206,18 +180,7 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
</div>
</form>
<div className="border-t pt-6 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<Label htmlFor="api-access-enabled">API key access</Label>
<p className="text-xs text-muted-foreground">Allow API keys to access this workspace.</p>
</div>
<Switch
id="api-access-enabled"
checked={!!workspace?.api_access_enabled}
disabled={savingApiAccess}
onCheckedChange={handleApiAccessToggle}
/>
</div>
<WorkspaceApiAccessControl workspaceId={workspaceId} className="border-t pt-6" />
<div className="border-t pt-6 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">

View file

@ -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 (
<div className={cn("flex flex-col gap-3 md:flex-row md:items-center md:justify-between", className)}>
<div className="space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-56" />
</div>
<Skeleton className="h-6 w-11 rounded-full" />
</div>
);
}
if (isError) {
return (
<div className={cn("flex flex-col gap-3 md:flex-row md:items-center md:justify-between", className)}>
<div className="space-y-1">
<Label>API key access</Label>
<p className="text-xs text-destructive">Failed to load workspace API access.</p>
</div>
<Button variant="outline" size="sm" onClick={() => refetch()}>
Retry
</Button>
</div>
);
}
return (
<div className={cn("flex flex-col gap-3 md:flex-row md:items-center md:justify-between", className)}>
<div className="space-y-1">
<Label htmlFor="api-access-enabled">API key access</Label>
<p className="text-xs text-muted-foreground">Allow API keys to access this workspace.</p>
</div>
<Switch
id="api-access-enabled"
checked={!!workspace?.api_access_enabled}
disabled={savingApiAccess}
onCheckedChange={handleApiAccessToggle}
/>
</div>
);
}