Merge branch 'dev' into twitter-integration

This commit is contained in:
Aarushi
2025-01-08 13:29:54 +00:00
committed by GitHub
11 changed files with 429 additions and 15 deletions

View File

@@ -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

View File

@@ -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)],
)

View File

@@ -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"

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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&apos;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>
);
}

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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))",