Create client API keys section on settings page

- Add table shadcn component to use in API keys settings section
- In dev mode, route requests to auth to khoj server at localhost:42110
This commit is contained in:
Debanjum Singh Solanky 2024-07-24 18:17:21 +05:30
parent 00fa4fa0fa
commit 2e165a0e0a
3 changed files with 246 additions and 1 deletions

View file

@ -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<DropdownComponentProps> = ({ items, selected,
);
}
interface TokenObject {
token: string;
name: string;
}
export const useApiKeys = () => {
const [apiKeys, setApiKeys] = useState<TokenObject[]>([]);
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() {
</Card>
</div>
</div>
<div className="section grid gap-8">
<div className="text-4xl">Clients</div>
<div className="cards flex flex-wrap gap-16">
<Card className="grid grid-flow-column border border-gray-300 shadow-md rounded-lg">
<CardHeader className="text-xl flex flex-row"><Key className="h-7 w-7 mr-2"/>API Keys</CardHeader>
<CardContent className="overflow-hidden">
<p className="text-md text-gray-400">
Access Khoj from your Desktop App, Obsidian plugin, and more.
</p>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Token</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiKeys.map((key) => (
<TableRow key={key.token}>
<TableCell><b>{key.name}</b></TableCell>
<TableCell>{`${key.token.slice(0, 4)}...${key.token.slice(-4)}`}</TableCell>
<TableCell>
<Button variant="outline" className="border border-green-400" onClick={() => copyAPIKey(key.token)}>
<Copy className="h-4 w-4 mr-2" /><span className="hidden md:inline">Copy</span>
</Button>
<Button variant="outline" className="ml-4 border border-red-400" onClick={() => deleteAPIKey(key.token)}>
<Trash className='h-4 w-4 mr-2' /><span className="hidden md:inline">Delete</span>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
<CardFooter className="flex flex-wrap gap-4">
<Button variant="outline" className="border border-green-300" onClick={generateAPIKey}>
<PlusCircle className='h-4 w-4 mr-2' />Generate Key
</Button>
</CardFooter>
</Card>
</div>
</div>
</div>
</Suspense>
</div>

View file

@ -0,0 +1,117 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View file

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