From e0e1baaefcf6fd38625562d0af028cd54f66c2e9 Mon Sep 17 00:00:00 2001 From: Trezy Date: Fri, 06 Mar 2026 17:07:27 +0000 Subject: [PATCH] feat: move API keys into settings --- web/src/app/(dashboard)/api-keys/page.tsx | 278 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- web/src/app/(dashboard)/settings/page.tsx | 400 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------- web/src/components/app-sidebar.tsx | 2 -- 3 file(s) changed, 330 insertion(s)(+), 350 deletion(s)(-) diff --git a/web/src/app/(dashboard)/api-keys/page.tsx b/web/src/app/(dashboard)/api-keys/page.tsx deleted file mode 100644 --- a/web/src/app/(dashboard)/api-keys/page.tsx +++ /dev/null @@ -1,278 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useState } from "react"; -import { Copy, Check } from "lucide-react"; - -import { useAuth } from "@/lib/auth-context"; -import { getApiKeys, createApiKey, revokeApiKey } from "@/lib/api"; -import type { ApiKeySummary, CreateApiKeyResponse } from "@/types/api-keys"; -import { SiteHeader } from "@/components/site-header"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; - -export default function ApiKeysPage() { - const { getToken } = useAuth(); - const [keys, setKeys] = useState([]); - const [error, setError] = useState(null); - - const load = useCallback(() => { - getApiKeys(getToken) - .then(setKeys) - .catch((e) => setError(e.message)); - }, [getToken]); - - useEffect(() => { - load(); - }, [load]); - - async function handleRevoke(id: string) { - try { - await revokeApiKey(getToken, id); - load(); - } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); - } - } - - return ( - <> - -
- {error &&

{error}

} - -
-

API Keys

- -
- -
- - - - Name - Key - Created - Last Used - - - - - {keys.length === 0 && ( - - - No API keys yet. - - - )} - {keys.map((key) => ( - - - {key.name} - - - {key.key_prefix}... - - - {new Date(key.created_at).toLocaleString()} - - - {key.last_used_at - ? new Date(key.last_used_at).toLocaleString() - : "Never"} - - - {!key.revoked_at && ( - - - - - - - Revoke API Key - - This will permanently revoke the key “{key.name}”. - Any services using this key will lose access. - - - - Cancel - handleRevoke(key.id)}> - Revoke - - - - - )} - - - ))} - -
-
-
- - ); -} - -function CreateApiKeyDialog({ - getToken, - onSuccess, -}: { - getToken: () => Promise; - onSuccess: () => void; -}) { - const [name, setName] = useState(""); - const [error, setError] = useState(null); - const [open, setOpen] = useState(false); - const [createdKey, setCreatedKey] = useState(null); - const [copied, setCopied] = useState(false); - - function handleOpenChange(nextOpen: boolean) { - setOpen(nextOpen); - if (!nextOpen) { - setName(""); - setError(null); - if (createdKey) { - setCreatedKey(null); - onSuccess(); - } - } - } - - async function handleCreate() { - setError(null); - try { - const result = await createApiKey(getToken, { name }); - setCreatedKey(result); - setCopied(false); - } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); - } - } - - async function handleCopy() { - if (!createdKey) return; - await navigator.clipboard.writeText(createdKey.key); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } - - return ( - - - - - - - - {createdKey ? "API Key Created" : "Create API Key"} - - - {createdKey - ? "Copy your API key now. It won\u2019t be shown again." - : "Give this key a name to identify its purpose."} - - - - {createdKey ? ( -
-
- -
- - -
-

- Store this key securely. You will not be able to see it again. -

-
-
- ) : ( -
- {error &&

{error}

} -
- - setName(e.target.value)} - placeholder="e.g., CI Deploy" - /> -
-
- )} - - - - - - {!createdKey && ( - - )} - -
-
- ); -} diff --git a/web/src/app/(dashboard)/settings/page.tsx b/web/src/app/(dashboard)/settings/page.tsx --- a/web/src/app/(dashboard)/settings/page.tsx +++ b/web/src/app/(dashboard)/settings/page.tsx @@ -1,17 +1,21 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { Copy, Check, Trash2, Pencil } from "lucide-react"; import { useAuth } from "@/lib/auth-context"; import { getScriptVariables, upsertScriptVariable, deleteScriptVariable, + getApiKeys, + createApiKey, + revokeApiKey, } from "@/lib/api"; import type { ScriptVariableSummary } from "@/types/script-variables"; +import type { ApiKeySummary, CreateApiKeyResponse } from "@/types/api-keys"; import { SiteHeader } from "@/components/site-header"; import { Button } from "@/components/ui/button"; -import { Trash2, Pencil } from "lucide-react"; import { Dialog, DialogClose, @@ -22,6 +26,17 @@ DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; @@ -33,26 +48,49 @@ TableHead, TableHeader, TableRow, } from "@/components/ui/table"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; export default function SettingsPage() { const { getToken } = useAuth(); const [vars, setVars] = useState([]); + const [keys, setKeys] = useState([]); const [error, setError] = useState(null); - const load = useCallback(() => { + const loadVars = useCallback(() => { getScriptVariables(getToken) .then(setVars) + .catch((e) => setError(e.message)); + }, [getToken]); + + const loadKeys = useCallback(() => { + getApiKeys(getToken) + .then(setKeys) .catch((e) => setError(e.message)); }, [getToken]); useEffect(() => { - load(); - }, [load]); + loadVars(); + loadKeys(); + }, [loadVars, loadKeys]); - async function handleDelete(key: string) { + async function handleDeleteVar(key: string) { try { await deleteScriptVariable(getToken, key); - load(); + loadVars(); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : String(e)); + } + } + + async function handleRevoke(id: string) { + try { + await revokeApiKey(getToken, id); + loadKeys(); } catch (e: unknown) { setError(e instanceof Error ? e.message : String(e)); } @@ -64,71 +102,169 @@
{error &&

{error}

} -
-
-

Script Variables

-

- Define variables that Lua scripts can access via the{" "} - env global table. -

-
- -
+ + + ENV Variables + API Keys + -
- - - - Key - Preview - Updated - - - - - {vars.length === 0 && ( - - - No script variables yet. - - - )} - {vars.map((v) => ( - - {v.key} - - {v.preview} - - - {new Date(v.updated_at).toLocaleString()} - - -
- -
+ + + Key + Preview + Updated + + + + + {vars.length === 0 && ( + + - - - - - - ))} - -
-
+ No script variables yet. + + + )} + {vars.map((v) => ( + + + {v.key} + + + {v.preview} + + + {new Date(v.updated_at).toLocaleString()} + + +
+ + +
+
+
+ ))} + + +
+ + + +
+

API Keys

+ +
+ +
+ + + + Name + Key + Created + Last Used + + + + + {keys.length === 0 && ( + + + No API keys yet. + + + )} + {keys.map((key) => ( + + + {key.name} + + + {key.key_prefix}... + + + {new Date(key.created_at).toLocaleString()} + + + {key.last_used_at + ? new Date(key.last_used_at).toLocaleString() + : "Never"} + + + {!key.revoked_at && ( + + + + + + + + Revoke API Key + + + This will permanently revoke the key “ + {key.name}”. Any services using this key + will lose access. + + + + Cancel + handleRevoke(key.id)} + > + Revoke + + + + + )} + + + ))} + +
+
+
+ ); @@ -195,7 +331,9 @@ )} - {isEdit ? "Edit Variable" : "Add Variable"} + + {isEdit ? "Edit Variable" : "Add Variable"} + {isEdit ? "Update the value for this script variable." @@ -236,3 +374,125 @@ ); } + +function CreateApiKeyDialog({ + getToken, + onSuccess, +}: { + getToken: () => Promise; + onSuccess: () => void; +}) { + const [name, setName] = useState(""); + const [error, setError] = useState(null); + const [open, setOpen] = useState(false); + const [createdKey, setCreatedKey] = useState( + null + ); + const [copied, setCopied] = useState(false); + + function handleOpenChange(nextOpen: boolean) { + setOpen(nextOpen); + if (!nextOpen) { + setName(""); + setError(null); + if (createdKey) { + setCreatedKey(null); + onSuccess(); + } + } + } + + async function handleCreate() { + setError(null); + try { + const result = await createApiKey(getToken, { name }); + setCreatedKey(result); + setCopied(false); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : String(e)); + } + } + + async function handleCopy() { + if (!createdKey) return; + await navigator.clipboard.writeText(createdKey.key); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + + return ( + + + + + + + + {createdKey ? "API Key Created" : "Create API Key"} + + + {createdKey + ? "Copy your API key now. It won\u2019t be shown again." + : "Give this key a name to identify its purpose."} + + + + {createdKey ? ( +
+
+ +
+ + +
+

+ Store this key securely. You will not be able to see it again. +

+
+
+ ) : ( +
+ {error &&

{error}

} +
+ + setName(e.target.value)} + placeholder="e.g., CI Deploy" + /> +
+
+ )} + + + + + + {!createdKey && ( + + )} + +
+
+ ); +} diff --git a/web/src/components/app-sidebar.tsx b/web/src/components/app-sidebar.tsx --- a/web/src/components/app-sidebar.tsx +++ b/web/src/components/app-sidebar.tsx @@ -7,7 +7,6 @@ IconDatabase, IconTable, IconClipboardList, IconUsers, - IconKey, IconSettings, IconLogout, } from "@tabler/icons-react" @@ -35,7 +34,6 @@ { title: "Backfill", url: "/backfill", icon: IconDatabase }, { title: "Records", url: "/records", icon: IconTable }, { title: "Event Logs", url: "/events", icon: IconClipboardList }, { title: "Admins", url: "/admins", icon: IconUsers }, - { title: "API Keys", url: "/api-keys", icon: IconKey }, { title: "Settings", url: "/settings", icon: IconSettings }, ] -- tangled.sh