mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
Merge branch 'dev' into twitter-integration
This commit is contained in:
@@ -56,15 +56,24 @@ class SendWebRequestBlock(Block):
|
||||
)
|
||||
|
||||
def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
||||
if isinstance(input_data.body, str):
|
||||
input_data.body = json.loads(input_data.body)
|
||||
body = input_data.body
|
||||
|
||||
if input_data.json_format:
|
||||
if isinstance(body, str):
|
||||
try:
|
||||
# Try to parse as JSON first
|
||||
body = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
# If it's not valid JSON and just plain text,
|
||||
# we should send it as plain text instead
|
||||
input_data.json_format = False
|
||||
|
||||
response = requests.request(
|
||||
input_data.method.value,
|
||||
input_data.url,
|
||||
headers=input_data.headers,
|
||||
json=input_data.body if input_data.json_format else None,
|
||||
data=input_data.body if not input_data.json_format else None,
|
||||
json=body if input_data.json_format else None,
|
||||
data=body if not input_data.json_format else None,
|
||||
)
|
||||
result = response.json() if input_data.json_format else response.text
|
||||
|
||||
|
||||
@@ -541,7 +541,7 @@ def get_execution_schedules(
|
||||
|
||||
@v1_router.post(
|
||||
"/api-keys",
|
||||
response_model=list[CreateAPIKeyResponse] | dict[str, str],
|
||||
response_model=CreateAPIKeyResponse,
|
||||
tags=["api-keys"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
@@ -583,7 +583,7 @@ async def get_api_keys(
|
||||
|
||||
@v1_router.get(
|
||||
"/api-keys/{key_id}",
|
||||
response_model=list[APIKeyWithoutHash] | dict[str, str],
|
||||
response_model=APIKeyWithoutHash,
|
||||
tags=["api-keys"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
@@ -604,7 +604,7 @@ async def get_api_key(
|
||||
|
||||
@v1_router.delete(
|
||||
"/api-keys/{key_id}",
|
||||
response_model=list[APIKeyWithoutHash] | dict[str, str],
|
||||
response_model=APIKeyWithoutHash,
|
||||
tags=["api-keys"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
@@ -626,7 +626,7 @@ async def delete_api_key(
|
||||
|
||||
@v1_router.post(
|
||||
"/api-keys/{key_id}/suspend",
|
||||
response_model=list[APIKeyWithoutHash] | dict[str, str],
|
||||
response_model=APIKeyWithoutHash,
|
||||
tags=["api-keys"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
@@ -648,7 +648,7 @@ async def suspend_key(
|
||||
|
||||
@v1_router.put(
|
||||
"/api-keys/{key_id}/permissions",
|
||||
response_model=list[APIKeyWithoutHash] | dict[str, str],
|
||||
response_model=APIKeyWithoutHash,
|
||||
tags=["api-keys"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Inter, Poppins } from "next/font/google";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Navbar } from "@/components/agptui/Navbar";
|
||||
@@ -10,8 +10,16 @@ import TallyPopupSimple from "@/components/TallyPopup";
|
||||
import { GoogleAnalytics } from "@next/third-parties/google";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { IconType } from "@/components/ui/icons";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
// Fonts
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
const poppins = Poppins({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
variable: "--font-poppins",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "NextGen AutoGPT",
|
||||
@@ -24,7 +32,10 @@ export default async function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html
|
||||
lang="en"
|
||||
className={`${GeistSans.variable} ${GeistMono.variable} ${poppins.variable} ${inter.variable}`}
|
||||
>
|
||||
<body className={cn("antialiased transition-colors", inter.className)}>
|
||||
<Providers
|
||||
attribute="class"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { APIKeysSection } from "@/components/agptui/composite/APIKeySection";
|
||||
|
||||
const ApiKeysPage = () => {
|
||||
return (
|
||||
<div className="w-full pr-4 pt-24 md:pt-0">
|
||||
<APIKeysSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeysPage;
|
||||
@@ -8,6 +8,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
{ text: "Creator Dashboard", href: "/store/dashboard" },
|
||||
{ text: "Agent dashboard", href: "/store/agent-dashboard" },
|
||||
{ text: "Integrations", href: "/store/integrations" },
|
||||
{ text: "API Keys", href: "/store/api_keys" },
|
||||
{ text: "Profile", href: "/store/profile" },
|
||||
{ text: "Settings", href: "/store/settings" },
|
||||
],
|
||||
@@ -17,7 +18,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-screen max-w-[1360px] flex-col lg:flex-row">
|
||||
<Sidebar linkGroups={sidebarLinkGroups} />
|
||||
<div className="pl-4">{children}</div>
|
||||
<div className="flex-1 pl-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ const TallyPopupSimple = () => {
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={resetTutorial}
|
||||
className="font-inter mb-0 h-14 w-28 rounded-2xl bg-[rgba(65,65,64,1)] text-left text-lg font-medium leading-6"
|
||||
className="mb-0 h-14 w-28 rounded-2xl bg-[rgba(65,65,64,1)] text-left font-inter text-lg font-medium leading-6"
|
||||
>
|
||||
Tutorial
|
||||
</Button>
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "./Button";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Menu } from "lucide-react";
|
||||
import { KeyIcon, Menu } from "lucide-react";
|
||||
import {
|
||||
IconDashboardLayout,
|
||||
IconIntegrations,
|
||||
@@ -58,6 +58,15 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
|
||||
Integrations
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/store/api_keys"
|
||||
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||
>
|
||||
<KeyIcon className="h-6 w-6" />
|
||||
<div className="p-ui-medium text-base font-medium leading-normal">
|
||||
API Keys
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/store/profile"
|
||||
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||
@@ -102,6 +111,15 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
|
||||
Integrations
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/store/api_keys"
|
||||
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||
>
|
||||
<KeyIcon className="h-6 w-6" strokeWidth={1} />
|
||||
<div className="p-ui-medium text-base font-medium leading-normal">
|
||||
API Keys
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/store/profile"
|
||||
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { APIKey, APIKeyPermission } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
import { LuCopy } from "react-icons/lu";
|
||||
import { Loader2, MoreVertical } from "lucide-react";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function APIKeysSection() {
|
||||
const [apiKeys, setApiKeys] = useState<APIKey[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [isKeyDialogOpen, setIsKeyDialogOpen] = useState(false);
|
||||
const [newKeyName, setNewKeyName] = useState("");
|
||||
const [newKeyDescription, setNewKeyDescription] = useState("");
|
||||
const [newApiKey, setNewApiKey] = useState("");
|
||||
const [selectedPermissions, setSelectedPermissions] = useState<
|
||||
APIKeyPermission[]
|
||||
>([]);
|
||||
const { toast } = useToast();
|
||||
const api = useBackendAPI();
|
||||
|
||||
useEffect(() => {
|
||||
loadAPIKeys();
|
||||
}, []);
|
||||
|
||||
const loadAPIKeys = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const keys = await api.listAPIKeys();
|
||||
setApiKeys(keys.filter((key) => key.status === "ACTIVE"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateKey = async () => {
|
||||
try {
|
||||
const response = await api.createAPIKey(
|
||||
newKeyName,
|
||||
selectedPermissions,
|
||||
newKeyDescription,
|
||||
);
|
||||
|
||||
setNewApiKey(response.plain_text_key);
|
||||
setIsCreateOpen(false);
|
||||
setIsKeyDialogOpen(true);
|
||||
loadAPIKeys();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to create AutoGPT Platform API key",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyKey = () => {
|
||||
navigator.clipboard.writeText(newApiKey);
|
||||
toast({
|
||||
title: "Copied",
|
||||
description: "AutoGPT Platform API key copied to clipboard",
|
||||
});
|
||||
};
|
||||
|
||||
const handleRevokeKey = async (keyId: string) => {
|
||||
try {
|
||||
await api.revokeAPIKey(keyId);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "AutoGPT Platform API key revoked successfully",
|
||||
});
|
||||
loadAPIKeys();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to revoke AutoGPT Platform API key",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>AutoGPT Platform API Keys</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your AutoGPT Platform API keys for programmatic access
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Create Key</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New API Key</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new AutoGPT Platform API key
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
placeholder="My AutoGPT Platform API Key"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description (Optional)</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={newKeyDescription}
|
||||
onChange={(e) => setNewKeyDescription(e.target.value)}
|
||||
placeholder="Used for..."
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Permissions</Label>
|
||||
{Object.values(APIKeyPermission).map((permission) => (
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
key={permission}
|
||||
>
|
||||
<Checkbox
|
||||
id={permission}
|
||||
checked={selectedPermissions.includes(permission)}
|
||||
onCheckedChange={(checked) => {
|
||||
setSelectedPermissions(
|
||||
checked
|
||||
? [...selectedPermissions, permission]
|
||||
: selectedPermissions.filter(
|
||||
(p) => p !== permission,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={permission}>{permission}</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsCreateOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateKey}>Create</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isKeyDialogOpen} onOpenChange={setIsKeyDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>AutoGPT Platform API Key Created</DialogTitle>
|
||||
<DialogDescription>
|
||||
Please copy your AutoGPT API key now. You won't be able
|
||||
to see it again!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center space-x-2">
|
||||
<code className="flex-1 rounded-md bg-secondary p-2 text-sm">
|
||||
{newApiKey}
|
||||
</code>
|
||||
<Button size="icon" variant="outline" onClick={handleCopyKey}>
|
||||
<LuCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setIsKeyDialogOpen(false)}>Close</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center p-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
apiKeys.length > 0 && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>API Key</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Last Used</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeys.map((key) => (
|
||||
<TableRow key={key.id}>
|
||||
<TableCell>{key.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="rounded-md border p-1 px-2 text-xs">
|
||||
{`${key.prefix}******************${key.postfix}`}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
key.status === "ACTIVE" ? "default" : "destructive"
|
||||
}
|
||||
className={
|
||||
key.status === "ACTIVE"
|
||||
? "border-green-600 bg-green-100 text-green-800"
|
||||
: "border-red-600 bg-red-100 text-red-800"
|
||||
}
|
||||
>
|
||||
{key.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(key.created_at).toLocaleDateString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{key.last_used_at
|
||||
? new Date(key.last_used_at).toLocaleDateString()
|
||||
: "Never"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => handleRevokeKey(key.id)}
|
||||
>
|
||||
Revoke
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
StoreReview,
|
||||
ScheduleCreatable,
|
||||
Schedule,
|
||||
APIKeyPermission,
|
||||
CreateAPIKeyResponse,
|
||||
APIKey,
|
||||
} from "./types";
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import getServerSupabase from "../supabase/getServerSupabase";
|
||||
@@ -221,6 +224,36 @@ export default class BackendAPI {
|
||||
);
|
||||
}
|
||||
|
||||
// API Key related requests
|
||||
async createAPIKey(
|
||||
name: string,
|
||||
permissions: APIKeyPermission[],
|
||||
description?: string,
|
||||
): Promise<CreateAPIKeyResponse> {
|
||||
return this._request("POST", "/api-keys", {
|
||||
name,
|
||||
permissions,
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
async listAPIKeys(): Promise<APIKey[]> {
|
||||
return this._get("/api-keys");
|
||||
}
|
||||
|
||||
async revokeAPIKey(keyId: string): Promise<APIKey> {
|
||||
return this._request("DELETE", `/api-keys/${keyId}`);
|
||||
}
|
||||
|
||||
async updateAPIKeyPermissions(
|
||||
keyId: string,
|
||||
permissions: APIKeyPermission[],
|
||||
): Promise<APIKey> {
|
||||
return this._request("PUT", `/api-keys/${keyId}/permissions`, {
|
||||
permissions,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if a ping event was received, `false` if provider doesn't support pinging but the webhook exists.
|
||||
* @throws `Error` if the webhook does not exist.
|
||||
|
||||
@@ -514,3 +514,36 @@ export type StoreReviewCreate = {
|
||||
score: number;
|
||||
comments?: string;
|
||||
};
|
||||
|
||||
// API Key Types
|
||||
|
||||
export enum APIKeyPermission {
|
||||
EXECUTE_GRAPH = "EXECUTE_GRAPH",
|
||||
READ_GRAPH = "READ_GRAPH",
|
||||
EXECUTE_BLOCK = "EXECUTE_BLOCK",
|
||||
READ_BLOCK = "READ_BLOCK",
|
||||
}
|
||||
|
||||
export enum APIKeyStatus {
|
||||
ACTIVE = "ACTIVE",
|
||||
REVOKED = "REVOKED",
|
||||
SUSPENDED = "SUSPENDED",
|
||||
}
|
||||
|
||||
export interface APIKey {
|
||||
id: string;
|
||||
name: string;
|
||||
prefix: string;
|
||||
postfix: string;
|
||||
status: APIKeyStatus;
|
||||
permissions: APIKeyPermission[];
|
||||
created_at: string;
|
||||
last_used_at?: string;
|
||||
revoked_at?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CreateAPIKeyResponse {
|
||||
api_key: APIKey;
|
||||
plain_text_key: string;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ const config = {
|
||||
mono: ["var(--font-geist-mono)"],
|
||||
// Include the custom font family
|
||||
neue: ['"PP Neue Montreal TT"', "sans-serif"],
|
||||
poppin: ["var(--font-poppins)"],
|
||||
inter: ["var(--font-inter)"],
|
||||
},
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
|
||||
Reference in New Issue
Block a user