aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web/app
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-02-12 14:52:00 +0000
committerMohamedBassem <me@mbassem.com>2024-02-12 14:55:00 +0000
commit6aacc0c7a86e36c52a3c2c1d26fe58cefcd3bec4 (patch)
treebad306e872d6bfcc2c67f00caa3880c8aa56070f /packages/web/app
parent230cafb6dfc8d3bad57d84ef13c3669f5bf5331a (diff)
downloadkarakeep-6aacc0c7a86e36c52a3c2c1d26fe58cefcd3bec4.tar.zst
feature: Add support for managing API keys
Diffstat (limited to 'packages/web/app')
-rw-r--r--packages/web/app/dashboard/components/Sidebar.tsx15
-rw-r--r--packages/web/app/dashboard/settings/components/AddApiKey.tsx148
-rw-r--r--packages/web/app/dashboard/settings/components/ApiKeySettings.tsx49
-rw-r--r--packages/web/app/dashboard/settings/components/DeleteApiKey.tsx65
-rw-r--r--packages/web/app/dashboard/settings/page.tsx10
5 files changed, 286 insertions, 1 deletions
diff --git a/packages/web/app/dashboard/components/Sidebar.tsx b/packages/web/app/dashboard/components/Sidebar.tsx
index 44892e81..d2ec14a6 100644
--- a/packages/web/app/dashboard/components/Sidebar.tsx
+++ b/packages/web/app/dashboard/components/Sidebar.tsx
@@ -1,5 +1,13 @@
import { Button } from "@/components/ui/button";
-import { Archive, MoreHorizontal, Star, Tag, Home, Brain } from "lucide-react";
+import {
+ Archive,
+ MoreHorizontal,
+ Star,
+ Tag,
+ Home,
+ Brain,
+ Settings,
+} from "lucide-react";
import { redirect } from "next/navigation";
import SidebarItem from "./SidebarItem";
import { getServerAuthSession } from "@/server/auth";
@@ -35,6 +43,11 @@ export default async function Sidebar() {
path="/dashboard/bookmarks/archive"
/>
<SidebarItem logo={<Tag />} name="Tags" path="#" />
+ <SidebarItem
+ logo={<Settings />}
+ name="Settings"
+ path="/dashboard/settings"
+ />
</ul>
</div>
<div className="mt-auto flex justify-between">
diff --git a/packages/web/app/dashboard/settings/components/AddApiKey.tsx b/packages/web/app/dashboard/settings/components/AddApiKey.tsx
new file mode 100644
index 00000000..f4f2894c
--- /dev/null
+++ b/packages/web/app/dashboard/settings/components/AddApiKey.tsx
@@ -0,0 +1,148 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { z } from "zod";
+import { useRouter } from "next/navigation";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm, SubmitErrorHandler } from "react-hook-form";
+import { toast } from "@/components/ui/use-toast";
+import { api } from "@/lib/trpc";
+import { useState } from "react";
+import { Copy } from "lucide-react";
+
+function ApiKeySuccess({ apiKey }: { apiKey: string }) {
+ return (
+ <div>
+ <div className="py-4">
+ Note: please copy the key and store it somewhere safe. Once you close
+ the dialog, you won&apos;t be able to access it again.
+ </div>
+ <div className="flex space-x-2 pt-2">
+ <Input value={apiKey} readOnly />
+ <Button onClick={() => navigator.clipboard.writeText(apiKey)}>
+ <Copy className="size-4" />
+ </Button>
+ </div>
+ </div>
+ );
+}
+
+function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) {
+ const formSchema = z.object({
+ name: z.string(),
+ });
+ const router = useRouter();
+
+ const form = useForm<z.infer<typeof formSchema>>({
+ resolver: zodResolver(formSchema),
+ });
+
+ async function onSubmit(value: z.infer<typeof formSchema>) {
+ try {
+ const resp = await api.apiKeys.create.mutate({ name: value.name });
+ onSuccess(resp.key);
+ } catch (e) {
+ toast({ description: "Something went wrong", variant: "destructive" });
+ return;
+ }
+ router.refresh();
+ }
+
+ const onError: SubmitErrorHandler<z.infer<typeof formSchema>> = (errors) => {
+ toast({
+ description: Object.values(errors)
+ .map((v) => v.message)
+ .join("\n"),
+ variant: "destructive",
+ });
+ };
+
+ return (
+ <Form {...form}>
+ <form
+ onSubmit={form.handleSubmit(onSubmit, onError)}
+ className="flex w-full space-x-3 space-y-8 pt-4"
+ >
+ <FormField
+ control={form.control}
+ name="name"
+ render={({ field }) => {
+ return (
+ <FormItem className="flex-1">
+ <FormLabel>Name</FormLabel>
+ <FormControl>
+ <Input type="text" placeholder="Name" {...field} />
+ </FormControl>
+ <FormDescription>
+ Give your API key a unique name
+ </FormDescription>
+ <FormMessage />
+ </FormItem>
+ );
+ }}
+ />
+ <Button className="h-full" type="submit">
+ Create
+ </Button>
+ </form>
+ </Form>
+ );
+}
+
+export default function AddApiKey() {
+ const [key, setKey] = useState<string | undefined>(undefined);
+ const [dialogOpen, setDialogOpen] = useState<boolean>(false);
+ return (
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
+ <DialogTrigger asChild>
+ <Button>New API Key</Button>
+ </DialogTrigger>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>
+ {key ? "Key was successfully created" : "Create API key"}
+ </DialogTitle>
+ <DialogDescription>
+ {key ? (
+ <ApiKeySuccess apiKey={key} />
+ ) : (
+ <AddApiKeyForm onSuccess={setKey} />
+ )}
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter className="sm:justify-end">
+ <DialogClose asChild>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => setKey(undefined)}
+ >
+ Close
+ </Button>
+ </DialogClose>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/packages/web/app/dashboard/settings/components/ApiKeySettings.tsx b/packages/web/app/dashboard/settings/components/ApiKeySettings.tsx
new file mode 100644
index 00000000..1598f25f
--- /dev/null
+++ b/packages/web/app/dashboard/settings/components/ApiKeySettings.tsx
@@ -0,0 +1,49 @@
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { api } from "@/server/api/client";
+import DeleteApiKey from "./DeleteApiKey";
+import AddApiKey from "./AddApiKey";
+
+export default async function ApiKeys() {
+ const keys = await api.apiKeys.list();
+ return (
+ <div className="pt-4">
+ <span className="text-xl">API Keys</span>
+ <hr className="my-2" />
+ <div className="flex flex-col space-y-3">
+ <div className="flex flex-1 justify-end">
+ <AddApiKey />
+ </div>
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>Name</TableHead>
+ <TableHead>Key</TableHead>
+ <TableHead>Created At</TableHead>
+ <TableHead>Action</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {keys.keys.map((k) => (
+ <TableRow key={k.id}>
+ <TableCell>{k.name}</TableCell>
+ <TableCell>**_{k.keyId}_**</TableCell>
+ <TableCell>{k.createdAt.toLocaleString()}</TableCell>
+ <TableCell>
+ <DeleteApiKey name={k.name} id={k.id} />
+ </TableCell>
+ </TableRow>
+ ))}
+ <TableRow></TableRow>
+ </TableBody>
+ </Table>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx b/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx
new file mode 100644
index 00000000..715b7a2c
--- /dev/null
+++ b/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { Trash } from "lucide-react";
+
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { api } from "@/lib/trpc";
+import { useRouter } from "next/navigation";
+import { toast } from "@/components/ui/use-toast";
+
+export default function DeleteApiKey({
+ name,
+ id,
+}: {
+ name: string;
+ id: string;
+}) {
+ const router = useRouter();
+ const deleteKey = async () => {
+ await api.apiKeys.revoke.mutate({ id });
+ toast({
+ description: "Key was successfully deleted",
+ });
+ router.refresh();
+ };
+ return (
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button variant="destructive">
+ <Trash className="size-5" />
+ </Button>
+ </DialogTrigger>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Delete API Key</DialogTitle>
+ <DialogDescription>
+ Are you sure you want to delete the API key &quot;{name}&quot;? Any
+ service using this API key will lose access.
+ </DialogDescription>
+ </DialogHeader>
+ <DialogFooter className="sm:justify-end">
+ <DialogClose asChild>
+ <Button type="button" variant="secondary">
+ Close
+ </Button>
+ </DialogClose>
+ <DialogClose asChild>
+ <Button type="button" variant="destructive" onClick={deleteKey}>
+ Delete
+ </Button>
+ </DialogClose>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/packages/web/app/dashboard/settings/page.tsx b/packages/web/app/dashboard/settings/page.tsx
new file mode 100644
index 00000000..e8799583
--- /dev/null
+++ b/packages/web/app/dashboard/settings/page.tsx
@@ -0,0 +1,10 @@
+import { Button } from "@/components/ui/button";
+import ApiKeySettings from "./components/ApiKeySettings";
+export default async function Settings() {
+ return (
+ <div className="m-4 flex flex-col space-y-2 rounded-md border bg-white p-4">
+ <p className="text-2xl">Settings</p>
+ <ApiKeySettings />
+ </div>
+ );
+}