diff --git a/src/interface/web/app/settings/page.tsx b/src/interface/web/app/settings/page.tsx index 53bf1f86..8f1825c8 100644 --- a/src/interface/web/app/settings/page.tsx +++ b/src/interface/web/app/settings/page.tsx @@ -20,8 +20,16 @@ import { DropdownMenuRadioItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + } from "@/components/ui/table" -import { ArrowRight, ChatCircleText, Key, Palette, SpeakerHigh, UserCircle, FileMagnifyingGlass } from "@phosphor-icons/react"; +import { ArrowRight, ChatCircleText, Key, Palette, SpeakerHigh, UserCircle, FileMagnifyingGlass, Trash, Copy, PlusCircle } from "@phosphor-icons/react"; import NavMenu from "../components/navMenu/navMenu"; import SidePanel from "../components/sidePanel/chatHistorySidePanel"; @@ -62,9 +70,82 @@ const DropdownComponent: React.FC = ({ items, selected, ); } +interface TokenObject { + token: string; + name: string; +} + +export const useApiKeys = () => { + const [apiKeys, setApiKeys] = useState([]); + const { toast } = useToast(); + + const generateAPIKey = async () => { + try { + const response = await fetch(`/auth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + const tokenObj = await response.json(); + setApiKeys(prevKeys => [...prevKeys, tokenObj]); + } catch (error) { + console.error('Error generating API key:', error); + } + }; + + const copyAPIKey = async (token: string) => { + try { + await navigator.clipboard.writeText(token); + toast({ + title: "🔑 API Key", + description: "Copied to clipboard", + }); + } catch (error) { + console.error('Error copying API key:', error); + } + }; + + const deleteAPIKey = async (token: string) => { + try { + const response = await fetch(`/auth/token?token=${token}`, { method: 'DELETE' }); + if (response.ok) { + setApiKeys(prevKeys => prevKeys.filter(key => key.token !== token)); + } + } catch (error) { + console.error('Error deleting API key:', error); + } + }; + + const listApiKeys = async () => { + try { + const response = await fetch(`/auth/token`); + const tokens = await response.json(); + if (tokens?.length > 0) { + setApiKeys(tokens); + } + } catch (error) { + console.error('Error listing API keys:', error); + } + }; + + useEffect(() => { + listApiKeys(); + }, []); + + return { + apiKeys, + generateAPIKey, + copyAPIKey, + deleteAPIKey, + }; +}; + + export default function SettingsView() { const [title, setTitle] = useState("Settings"); const [isMobileWidth, setIsMobileWidth] = useState(false); + const { apiKeys, generateAPIKey, copyAPIKey, deleteAPIKey } = useApiKeys(); const userConfig = useUserConfig(true); const cardClassName = "w-1/3 grid grid-flow-column border border-gray-300 shadow-md rounded-lg"; const { toast } = useToast(); @@ -219,6 +300,49 @@ export default function SettingsView() { +
+
Clients
+
+ + API Keys + +

+ Access Khoj from your Desktop App, Obsidian plugin, and more. +

+ + + + Name + Token + Actions + + + + {apiKeys.map((key) => ( + + {key.name} + {`${key.token.slice(0, 4)}...${key.token.slice(-4)}`} + + + + + + ))} + +
+
+ + + +
+
+
diff --git a/src/interface/web/components/ui/table.tsx b/src/interface/web/components/ui/table.tsx new file mode 100644 index 00000000..7f3502f8 --- /dev/null +++ b/src/interface/web/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/src/interface/web/next.config.mjs b/src/interface/web/next.config.mjs index b22a8010..0feefdcf 100644 --- a/src/interface/web/next.config.mjs +++ b/src/interface/web/next.config.mjs @@ -10,6 +10,10 @@ const nextConfig = { source: '/api/:path*', destination: 'http://localhost:42110/api/:path*', }, + { + source: '/auth/:path*', + destination: 'http://localhost:42110/auth/:path*', + }, ]; }, trailingSlash: true,