diff --git a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/api-keys-screen.tsx b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/api-keys-screen.tsx index f07988161..fda7a6442 100644 --- a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/api-keys-screen.tsx +++ b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/api-keys-screen.tsx @@ -6,351 +6,443 @@ import { DashboardPageTemplate } from "../../../../_components/ui/dashboard-page import { DenButton } from "../../../../_components/ui/button"; import { DenInput } from "../../../../_components/ui/input"; import { getErrorMessage, requestJson } from "../../../../_lib/den-flow"; -import { getOrgAccessFlags, parseOrgApiKeysPayload, type DenOrgApiKey } from "../../../../_lib/den-org"; +import { + getOrgAccessFlags, + parseOrgApiKeysPayload, + type DenOrgApiKey, +} from "../../../../_lib/den-org"; import { useOrgDashboard } from "../_providers/org-dashboard-provider"; function formatDateTime(value: string | null) { - if (!value) { - return "Never"; - } + if (!value) { + return "Never"; + } - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return "Never"; - } + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return "Never"; + } - return date.toLocaleString(); + return date.toLocaleString(); } function formatKeyPreview(apiKey: DenOrgApiKey) { - if (apiKey.start) { - return `${apiKey.start}...`; - } + if (apiKey.start) { + return `${apiKey.start}...`; + } - if (apiKey.prefix) { - return `${apiKey.prefix}${apiKey.id.slice(0, 6)}...`; - } + if (apiKey.prefix) { + return `${apiKey.prefix}${apiKey.id.slice(0, 6)}...`; + } - return `${apiKey.id.slice(0, 6)}...`; + return `${apiKey.id.slice(0, 6)}...`; } function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; + return typeof value === "object" && value !== null; } function getCreatedKey(payload: unknown) { - if (!isRecord(payload) || typeof payload.key !== "string") { - return null; - } + if (!isRecord(payload) || typeof payload.key !== "string") { + return null; + } - return payload.key; + return payload.key; } export function ApiKeysScreen() { - const { orgId, orgContext } = useOrgDashboard(); - const [apiKeys, setApiKeys] = useState([]); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - const [name, setName] = useState(""); - const [creating, setCreating] = useState(false); - const [deletingId, setDeletingId] = useState(null); - const [showCreateForm, setShowCreateForm] = useState(false); - const [createdKey, setCreatedKey] = useState(null); - const [createdKeyName, setCreatedKeyName] = useState(null); - const [copied, setCopied] = useState(false); - - const access = useMemo( - () => getOrgAccessFlags(orgContext?.currentMember.role ?? "member", orgContext?.currentMember.isOwner ?? false), - [orgContext?.currentMember.isOwner, orgContext?.currentMember.role], - ); - - async function loadApiKeys() { - if (!orgId || !access.canManageApiKeys) { - setApiKeys([]); - return; - } + const { orgId, orgContext } = useOrgDashboard(); + const [apiKeys, setApiKeys] = useState([]); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [name, setName] = useState(""); + const [creating, setCreating] = useState(false); + const [deletingId, setDeletingId] = useState(null); + const [showCreateForm, setShowCreateForm] = useState(false); + const [createdKey, setCreatedKey] = useState(null); + const [createdKeyName, setCreatedKeyName] = useState(null); + const [copied, setCopied] = useState(false); + + const access = useMemo( + () => + getOrgAccessFlags( + orgContext?.currentMember.role ?? "member", + orgContext?.currentMember.isOwner ?? false, + ), + [orgContext?.currentMember.isOwner, orgContext?.currentMember.role], + ); - setBusy(true); - setError(null); - try { - const { response, payload } = await requestJson(`/v1/orgs/${encodeURIComponent(orgId)}/api-keys`, { method: "GET" }, 12000); - if (!response.ok) { - throw new Error(getErrorMessage(payload, `Failed to load API keys (${response.status}).`)); - } - - setApiKeys(parseOrgApiKeysPayload(payload)); - } catch (nextError) { - setError(nextError instanceof Error ? nextError.message : "Failed to load API keys."); - } finally { - setBusy(false); + async function loadApiKeys() { + if (!orgId || !access.canManageApiKeys) { + setApiKeys([]); + return; + } + + setBusy(true); + setError(null); + try { + const { response, payload } = await requestJson( + `/v1/orgs/${encodeURIComponent(orgId)}/api-keys`, + { method: "GET" }, + 12000, + ); + if (!response.ok) { + throw new Error( + getErrorMessage( + payload, + `Failed to load API keys (${response.status}).`, + ), + ); + } + + setApiKeys(parseOrgApiKeysPayload(payload)); + } catch (nextError) { + setError( + nextError instanceof Error + ? nextError.message + : "Failed to load API keys.", + ); + } finally { + setBusy(false); + } } - } - - useEffect(() => { - void loadApiKeys(); - }, [orgId, access.canManageApiKeys]); - useEffect(() => { - if (!copied) { - return; + useEffect(() => { + void loadApiKeys(); + }, [orgId, access.canManageApiKeys]); + + useEffect(() => { + if (!copied) { + return; + } + + const timeout = window.setTimeout(() => setCopied(false), 1500); + return () => window.clearTimeout(timeout); + }, [copied]); + + async function handleCreate(event: FormEvent) { + event.preventDefault(); + if (!orgId) { + setError("Organization not found."); + return; + } + + setCreating(true); + setError(null); + setCreatedKey(null); + setCreatedKeyName(null); + setCopied(false); + try { + const { response, payload } = await requestJson( + `/v1/orgs/${encodeURIComponent(orgId)}/api-keys`, + { + method: "POST", + body: JSON.stringify({ name }), + }, + 12000, + ); + + if (!response.ok) { + throw new Error( + getErrorMessage( + payload, + `Failed to create API key (${response.status}).`, + ), + ); + } + + const nextKey = getCreatedKey(payload); + if (!nextKey) { + throw new Error( + "API key was created, but the secret was not returned.", + ); + } + + setCreatedKey(nextKey); + setCreatedKeyName(name); + setName(""); + setShowCreateForm(false); + await loadApiKeys(); + } catch (nextError) { + setError( + nextError instanceof Error + ? nextError.message + : "Failed to create API key.", + ); + } finally { + setCreating(false); + } } - const timeout = window.setTimeout(() => setCopied(false), 1500); - return () => window.clearTimeout(timeout); - }, [copied]); - - async function handleCreate(event: FormEvent) { - event.preventDefault(); - if (!orgId) { - setError("Organization not found."); - return; + function openCreateForm() { + setError(null); + setCopied(false); + setCreatedKey(null); + setCreatedKeyName(null); + setName(""); + setShowCreateForm(true); } - setCreating(true); - setError(null); - setCreatedKey(null); - setCreatedKeyName(null); - setCopied(false); - try { - const { response, payload } = await requestJson( - `/v1/orgs/${encodeURIComponent(orgId)}/api-keys`, - { - method: "POST", - body: JSON.stringify({ name }), - }, - 12000, - ); - - if (!response.ok) { - throw new Error(getErrorMessage(payload, `Failed to create API key (${response.status}).`)); - } - - const nextKey = getCreatedKey(payload); - if (!nextKey) { - throw new Error("API key was created, but the secret was not returned."); - } - - setCreatedKey(nextKey); - setCreatedKeyName(name); - setName(""); - setShowCreateForm(false); - await loadApiKeys(); - } catch (nextError) { - setError(nextError instanceof Error ? nextError.message : "Failed to create API key."); - } finally { - setCreating(false); - } - } - - function openCreateForm() { - setError(null); - setCopied(false); - setCreatedKey(null); - setCreatedKeyName(null); - setName(""); - setShowCreateForm(true); - } - - function closeCreateForm() { - setName(""); - setShowCreateForm(false); - } - - async function handleDelete(apiKey: DenOrgApiKey) { - if (!orgId || !window.confirm(`Delete ${apiKey.name ?? apiKey.start ?? "this API key"}? This cannot be undone.`)) { - return; + function closeCreateForm() { + setName(""); + setShowCreateForm(false); } - setDeletingId(apiKey.id); - setError(null); - try { - const { response, payload } = await requestJson( - `/v1/orgs/${encodeURIComponent(orgId)}/api-keys/${encodeURIComponent(apiKey.id)}`, - { method: "DELETE" }, - 12000, - ); - - if (response.status !== 204 && !response.ok) { - throw new Error(getErrorMessage(payload, `Failed to delete API key (${response.status}).`)); - } - - await loadApiKeys(); - } catch (nextError) { - setError(nextError instanceof Error ? nextError.message : "Failed to delete API key."); - } finally { - setDeletingId(null); + async function handleDelete(apiKey: DenOrgApiKey) { + if ( + !orgId || + !window.confirm( + `Delete ${apiKey.name ?? apiKey.start ?? "this API key"}? This cannot be undone.`, + ) + ) { + return; + } + + setDeletingId(apiKey.id); + setError(null); + try { + const { response, payload } = await requestJson( + `/v1/orgs/${encodeURIComponent(orgId)}/api-keys/${encodeURIComponent(apiKey.id)}`, + { method: "DELETE" }, + 12000, + ); + + if (response.status !== 204 && !response.ok) { + throw new Error( + getErrorMessage( + payload, + `Failed to delete API key (${response.status}).`, + ), + ); + } + + await loadApiKeys(); + } catch (nextError) { + setError( + nextError instanceof Error + ? nextError.message + : "Failed to delete API key.", + ); + } finally { + setDeletingId(null); + } } - } - async function copyCreatedKey() { - if (!createdKey) { - return; + async function copyCreatedKey() { + if (!createdKey) { + return; + } + + try { + await navigator.clipboard.writeText(createdKey); + setCopied(true); + } catch { + setError( + "Could not copy the API key. Copy it manually before leaving this page.", + ); + } } - try { - await navigator.clipboard.writeText(createdKey); - setCopied(true); - } catch { - setError("Could not copy the API key. Copy it manually before leaving this page."); + if (!orgContext) { + return ( + +
+ Loading organization details... +
+
+ ); } - } - if (!orgContext) { return ( - -
- Loading organization details... -
-
- ); - } - - return ( - - {!access.canManageApiKeys ? ( -
- Only organization owners and admins can view or manage API keys. -
- ) : ( - <> - {error ? ( -
- {error} -
- ) : null} - -
- {createdKey ? ( -
-
-
-

- {createdKeyName ? `${createdKeyName} is ready` : "Your new API key is ready"} -

-

- Copy it now. After this state closes, only the name and leading characters remain visible in the table. -

-
+ + {!access.canManageApiKeys ? ( +
+ Only organization owners and admins can view or manage API + keys.
- -
- {createdKey} -
- -
- void copyCreatedKey()}> - {copied ? "Copied" : "Copy key"} - - Create another key -
-
- ) : showCreateForm ? ( -
-
-
-

Issue a new key

-

- Keys are always issued for your own membership in this workspace and inherit the built-in request limit. -

-
-
- - - -
- - Cancel - - - Create API key - -
-
- ) : ( -
-
-

Create a new API key

-

- Issue a named, rate-limited key for your own org membership when you need one. -

-
- New key -
- )} -
- -
-
- Key - Owner - Last used - -
- - {busy ? ( -
Loading API keys...
- ) : apiKeys.length === 0 ? ( -
No API keys for this workspace yet.
) : ( - apiKeys.map((apiKey) => ( -
-
-

- {apiKey.name ?? apiKey.start ?? "Untitled key"} -

-

- {formatKeyPreview(apiKey)} {formatDateTime(apiKey.createdAt)} -

-
- -
-

{apiKey.owner.name}

-

{apiKey.owner.email}

-
- - {formatDateTime(apiKey.lastRequest)} - -
- void handleDelete(apiKey)} - disabled={deletingId === apiKey.id} - > - {deletingId === apiKey.id ? "Deleting..." : "Delete"} - -
-
- )) + <> + {error ? ( +
+ {error} +
+ ) : null} + +
+ {createdKey ? ( +
+
+
+

+ {createdKeyName + ? `${createdKeyName} is ready` + : "Your new API key is ready"} +

+

+ The key will only be shown once. +

+
+
+ +
+ + {createdKey} + +
+ +
+ void copyCreatedKey()} + > + {copied ? "Copied" : "Copy key"} + + + Create another key + +
+
+ ) : showCreateForm ? ( +
+
+
+

+ Issue a new key +

+

+ Keys are issued to you for this + organization only. +

+
+
+ + + +
+ + Cancel + + + Create API key + +
+
+ ) : ( +
+
+

+ Create a new API key +

+

+ Create a new API key for this + organization. +

+
+ + New key + +
+ )} +
+ +
+
+ Key + Owner + Last used + +
+ + {busy ? ( +
+ Loading API keys... +
+ ) : apiKeys.length === 0 ? ( +
+ No API keys for this workspace yet. +
+ ) : ( + apiKeys.map((apiKey) => ( +
+
+

+ {apiKey.name ?? + apiKey.start ?? + "Untitled key"} +

+

+ {formatKeyPreview(apiKey)}{" "} + {formatDateTime(apiKey.createdAt)} +

+
+ +
+

+ {apiKey.owner.name} +

+

+ {apiKey.owner.email} +

+
+ + + {formatDateTime(apiKey.lastRequest)} + + +
+ + void handleDelete(apiKey) + } + disabled={deletingId === apiKey.id} + > + {deletingId === apiKey.id + ? "Deleting..." + : "Delete"} + +
+
+ )) + )} +
+ )} -
- - )} - - ); + + ); }