diff --git a/server/reflector/db/user_api_keys.py b/server/reflector/db/user_api_keys.py
index b4fe7538..8e0ab928 100644
--- a/server/reflector/db/user_api_keys.py
+++ b/server/reflector/db/user_api_keys.py
@@ -84,7 +84,8 @@ class UserApiKeyController:
(user_api_keys.c.id == key_id) & (user_api_keys.c.user_id == user_id)
)
result = await get_database().execute(query)
- return result > 0
+ # asyncpg returns None for DELETE, consider it success if no exception
+ return result is None or result > 0
user_api_keys_controller = UserApiKeyController()
diff --git a/www/app/(app)/layout.tsx b/www/app/(app)/layout.tsx
index 8bca1df6..7d9f1c84 100644
--- a/www/app/(app)/layout.tsx
+++ b/www/app/(app)/layout.tsx
@@ -78,6 +78,14 @@ export default async function AppLayout({
)}
{featureEnabled("requireLogin") ? (
<>
+ ·
+
+ Settings
+
·
>
diff --git a/www/app/(app)/settings/api-keys/page.tsx b/www/app/(app)/settings/api-keys/page.tsx
new file mode 100644
index 00000000..e63ed07a
--- /dev/null
+++ b/www/app/(app)/settings/api-keys/page.tsx
@@ -0,0 +1,341 @@
+"use client";
+import React, { useState, useRef } from "react";
+import {
+ Box,
+ Button,
+ Heading,
+ Stack,
+ Text,
+ Input,
+ Table,
+ Flex,
+ IconButton,
+ Code,
+ Dialog,
+} from "@chakra-ui/react";
+import { LuTrash2, LuCopy, LuPlus } from "react-icons/lu";
+import { useQueryClient } from "@tanstack/react-query";
+import { $api } from "../../../lib/apiClient";
+import { toaster } from "../../../components/ui/toaster";
+
+interface CreateApiKeyResponse {
+ id: string;
+ user_id: string;
+ name: string | null;
+ created_at: string;
+ key: string;
+}
+
+export default function ApiKeysPage() {
+ const [newKeyName, setNewKeyName] = useState("");
+ const [isCreating, setIsCreating] = useState(false);
+ const [createdKey, setCreatedKey] = useState(
+ null,
+ );
+ const [keyToDelete, setKeyToDelete] = useState(null);
+ const queryClient = useQueryClient();
+ const cancelRef = useRef(null);
+
+ const { data: apiKeys, isLoading } = $api.useQuery(
+ "get",
+ "/v1/user/api-keys",
+ );
+
+ const createKeyMutation = $api.useMutation("post", "/v1/user/api-keys", {
+ onSuccess: (data) => {
+ setCreatedKey(data);
+ setNewKeyName("");
+ setIsCreating(false);
+ queryClient.invalidateQueries({ queryKey: ["get", "/v1/user/api-keys"] });
+ toaster.create({
+ duration: 5000,
+ render: () => (
+
+ API key created
+
+ Make sure to copy it now - you won't see it again!
+
+
+ ),
+ });
+
+ setTimeout(() => {
+ const keyElement = document.querySelector(".api-key-code");
+ if (keyElement) {
+ const range = document.createRange();
+ range.selectNodeContents(keyElement);
+ const selection = window.getSelection();
+ selection?.removeAllRanges();
+ selection?.addRange(range);
+ }
+ }, 100);
+ },
+ onError: () => {
+ toaster.create({
+ duration: 3000,
+ render: () => (
+
+ Error
+ Failed to create API key
+
+ ),
+ });
+ },
+ });
+
+ const deleteKeyMutation = $api.useMutation(
+ "delete",
+ "/v1/user/api-keys/{key_id}",
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: ["get", "/v1/user/api-keys"],
+ });
+ toaster.create({
+ duration: 3000,
+ render: () => (
+
+ API key deleted
+
+ ),
+ });
+ },
+ onError: () => {
+ toaster.create({
+ duration: 3000,
+ render: () => (
+
+ Error
+ Failed to delete API key
+
+ ),
+ });
+ },
+ },
+ );
+
+ const handleCreateKey = () => {
+ createKeyMutation.mutate({
+ body: { name: newKeyName || null },
+ });
+ };
+
+ const handleCopyKey = (key: string) => {
+ navigator.clipboard.writeText(key);
+ toaster.create({
+ duration: 2000,
+ render: () => (
+
+ Copied to clipboard
+
+ ),
+ });
+ };
+
+ const handleDeleteRequest = (keyId: string) => {
+ setKeyToDelete(keyId);
+ };
+
+ const confirmDelete = () => {
+ if (keyToDelete) {
+ deleteKeyMutation.mutate({
+ params: { path: { key_id: keyToDelete } },
+ });
+ setKeyToDelete(null);
+ }
+ };
+
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ };
+
+ return (
+
+ API Keys
+
+ Manage your API keys for programmatic access to Reflector
+
+
+ {/* Show newly created key */}
+ {createdKey && (
+
+
+
+ API Key Created
+
+
+
+
+ Make sure to copy your API key now. You won't be able to see it
+ again!
+
+
+
+ {createdKey.key}
+
+ handleCopyKey(createdKey.key)}
+ >
+
+
+
+
+ )}
+
+ {/* Create new key */}
+
+
+ Create New API Key
+
+ {!isCreating ? (
+
+ ) : (
+
+
+ Name (optional)
+ setNewKeyName(e.target.value)}
+ />
+
+
+
+
+
+
+ )}
+
+
+ {/* List of API keys */}
+
+
+ Your API Keys
+
+ {isLoading ? (
+ Loading...
+ ) : !apiKeys || apiKeys.length === 0 ? (
+
+ No API keys yet. Create one to get started.
+
+ ) : (
+
+
+
+ Name
+ Created
+ Actions
+
+
+
+ {apiKeys.map((key) => (
+
+
+ {key.name || Unnamed}
+
+ {formatDate(key.created_at)}
+
+ handleDeleteRequest(key.id)}
+ loading={
+ deleteKeyMutation.isPending &&
+ deleteKeyMutation.variables?.params?.path?.key_id ===
+ key.id
+ }
+ >
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ {/* Delete confirmation dialog */}
+ {
+ if (!e.open) setKeyToDelete(null);
+ }}
+ initialFocusEl={() => cancelRef.current}
+ >
+
+
+
+
+ Delete API Key
+
+
+
+ Are you sure you want to delete this API key? This action cannot
+ be undone.
+
+
+
+
+
+
+
+
+
+
+ );
+}