allow retrieving api key secret

This commit is contained in:
miloschwartz
2026-08-11 10:46:22 -04:00
parent 495c772d6b
commit 732fd4eba1
5 changed files with 165 additions and 50 deletions
+2 -2
View File
@@ -1658,7 +1658,7 @@
"virtualApiKeysName": "Name",
"virtualApiKeysDescriptionOptional": "Description (optional)",
"virtualApiKeysAssociateUserOptional": "Associate User (optional)",
"virtualApiKeysAssociateUserDescription": "Attribution only. Does not grant access by itself.",
"virtualApiKeysAssociateUserDescription": "Associate this key with a user to track usage.",
"virtualApiKeysAllResources": "All public inference resources",
"virtualApiKeysAllResourcesDescription": "Allow this key to access every public inference resource in the organization",
"virtualApiKeysSelectResources": "Public Inference Resources",
@@ -1666,7 +1666,7 @@
"virtualApiKeysSelectResourcesDescription": "Choose which public inference resources this key can access",
"virtualApiKeysNoResources": "No resources",
"virtualApiKeysSecret": "Key",
"virtualApiKeysSeeOnce": "Copy this key now. You can also view it again later from the table.",
"virtualApiKeysCopyKey": "Copy this key. You can view it again later from the table or when editing.",
"virtualApiKeysSecretHint": "Use this value as a Bearer token: vk-[id].[secret]",
"virtualApiKeysViewSecret": "View Secret",
"virtualApiKeysViewSecretTitle": "Virtual API Key Secret",
+1 -1
View File
@@ -391,7 +391,7 @@ export default function CreateVirtualApiKeyForm({
)}
{credential && (
<div className="space-y-4">
<p>{t("virtualApiKeysSeeOnce")}</p>
<p>{t("virtualApiKeysCopyKey")}</p>
<CopyTextBox
text={credential}
wrapText={false}
+58 -5
View File
@@ -7,9 +7,9 @@ import {
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Label } from "@app/components/ui/label";
import { toast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod";
import { AxiosResponse } from "axios";
@@ -46,7 +46,9 @@ import {
} from "@app/components/multi-resource-selector";
import type { SelectedResource } from "@app/components/resource-selector";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import CopyTextBox from "@app/components/CopyTextBox";
import type { CreatedVirtualApiKey } from "@app/components/CreateVirtualApiKeyForm";
import type { GetVirtualApiKeyResponse } from "@server/routers/virtualApiKey/types";
type FormProps = {
open: boolean;
@@ -96,6 +98,8 @@ export default function EditVirtualApiKeyForm({
const [selectedResources, setSelectedResources] = useState<
SelectedResource[]
>([]);
const [credential, setCredential] = useState<string | null>(null);
const [credentialLoading, setCredentialLoading] = useState(false);
const formSchema = z
.object({
@@ -132,6 +136,55 @@ export default function EditVirtualApiKeyForm({
form.reset({
allResources: virtualApiKey.allResources
});
let cancelled = false;
setCredentialLoading(true);
setCredential(null);
api.get<AxiosResponse<GetVirtualApiKeyResponse>>(
`/virtual-api-key/${virtualApiKey.virtualApiKeyId}`
)
.then((res) => {
if (cancelled) {
return;
}
const secret = res.data.data.virtualApiKey.secret;
if (secret) {
setCredential(
`vk-${virtualApiKey.virtualApiKeyId}.${secret}`
);
} else {
toast({
variant: "destructive",
title: t("virtualApiKeysErrorFetchSecret"),
description: t(
"virtualApiKeysErrorFetchSecretDescription"
)
});
}
})
.catch((e) => {
if (cancelled) {
return;
}
toast({
variant: "destructive",
title: t("virtualApiKeysErrorFetchSecret"),
description: formatAxiosError(
e,
t("virtualApiKeysErrorFetchSecretDescription")
)
});
})
.finally(() => {
if (!cancelled) {
setCredentialLoading(false);
}
});
return () => {
cancelled = true;
};
}, [open, virtualApiKey, form]);
async function onSubmit(values: z.infer<typeof formSchema>) {
@@ -228,11 +281,11 @@ export default function EditVirtualApiKeyForm({
id="edit-virtual-api-key-form"
>
<div className="space-y-2">
<FormLabel>
<Label>
{t(
"virtualApiKeysAssociateUserOptional"
)}
</FormLabel>
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
@@ -316,11 +369,11 @@ export default function EditVirtualApiKeyForm({
{!allResources && (
<div className="space-y-2">
<FormLabel>
<Label>
{t(
"virtualApiKeysSelectResources"
)}
</FormLabel>
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
+61 -3
View File
@@ -35,6 +35,7 @@ import CreateVirtualApiKeyForm, {
} from "@app/components/CreateVirtualApiKeyForm";
import EditVirtualApiKeyForm from "@app/components/EditVirtualApiKeyForm";
import ViewVirtualApiKeySecret from "@app/components/ViewVirtualApiKeySecret";
import CopyToClipboard from "@app/components/CopyToClipboard";
import { useTranslations } from "next-intl";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { UserSelector, type SelectedUser } from "@app/components/user-selector";
@@ -44,6 +45,8 @@ import {
} from "@app/components/resource-selector";
import { cn } from "@app/lib/cn";
import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover";
import type { GetVirtualApiKeyResponse } from "@server/routers/virtualApiKey/types";
import { AxiosResponse } from "axios";
export type VirtualApiKeyRow = CreatedVirtualApiKey;
@@ -344,8 +347,12 @@ export default function VirtualApiKeysTable({
</Button>
);
},
cell: ({ row }) =>
`vk-${row.original.virtualApiKeyId}.••••${row.original.lastChars}`
cell: ({ row }) => (
<VirtualApiKeySecretCell
virtualApiKeyId={row.original.virtualApiKeyId}
lastChars={row.original.lastChars}
/>
)
},
{
accessorKey: "createdAt",
@@ -396,7 +403,7 @@ export default function VirtualApiKeysTable({
cell: ({ row }) => {
const keyRow = row.original;
return (
<div className="flex items-center justify-end">
<div className="flex items-center justify-end gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
@@ -514,3 +521,54 @@ export default function VirtualApiKeysTable({
</>
);
}
function VirtualApiKeySecretCell({
virtualApiKeyId,
lastChars
}: {
virtualApiKeyId: string;
lastChars: string;
}) {
const t = useTranslations();
const api = createApiClient(useEnvContext());
const preview = `vk-${virtualApiKeyId}••••${lastChars}`;
const [credential, setCredential] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
api.get<AxiosResponse<GetVirtualApiKeyResponse>>(
`/virtual-api-key/${virtualApiKeyId}`
)
.then((res) => {
if (cancelled) {
return;
}
const secret = res.data.data.virtualApiKey.secret;
if (secret) {
setCredential(`vk-${virtualApiKeyId}.${secret}`);
}
})
.catch((e) => {
if (cancelled) {
return;
}
toast({
variant: "destructive",
title: t("virtualApiKeysErrorFetchSecret"),
description: formatAxiosError(
e,
t("virtualApiKeysErrorFetchSecretDescription")
)
});
});
return () => {
cancelled = true;
};
}, [virtualApiKeyId]);
return (
<CopyToClipboard text={credential ?? preview} displayText={preview} />
);
}
+43 -39
View File
@@ -35,7 +35,14 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { Input } from "@app/components/ui/input";
import { DataTablePagination } from "@app/components/DataTablePagination";
import { dataTableFilterDropdownContentClassName } from "@app/lib/dataTableFilterPopover";
import { ChevronDown, Plus, Search, RefreshCw, Columns, Filter } from "lucide-react";
import {
ChevronDown,
Plus,
Search,
RefreshCw,
Columns,
Filter
} from "lucide-react";
import {
Card,
CardContent,
@@ -523,29 +530,33 @@ export function DataTable<TData, TValue>({
addButtonText && ((addActions && addActions.length > 0) || onAdd)
);
const showAddActionInEmptyState = !hasRows && hasAddAction;
const addAction = addActions && addActions.length > 0 && addButtonText ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button disabled={addButtonDisabled}>
<Plus className="mr-2 h-4 w-4" />
{addButtonText}
<ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{addActions.map((action, i) => (
<DropdownMenuItem key={i} onSelect={() => action.onSelect()}>
{action.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : onAdd && addButtonText ? (
<Button onClick={onAdd} disabled={addButtonDisabled}>
<Plus className="mr-2 h-4 w-4" />
{addButtonText}
</Button>
) : null;
const addAction =
addActions && addActions.length > 0 && addButtonText ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button disabled={addButtonDisabled}>
<Plus className="mr-2 h-4 w-4" />
{addButtonText}
<ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{addActions.map((action, i) => (
<DropdownMenuItem
key={i}
onSelect={() => action.onSelect()}
>
{action.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : onAdd && addButtonText ? (
<Button onClick={onAdd} disabled={addButtonDisabled}>
<Plus className="mr-2 h-4 w-4" />
{addButtonText}
</Button>
) : null;
return (
<div className="container mx-auto max-w-12xl">
@@ -674,7 +685,9 @@ export function DataTable<TData, TValue>({
<Button
variant="outline"
onClick={onRefresh}
disabled={isRefreshing || refreshButtonDisabled}
disabled={
isRefreshing || refreshButtonDisabled
}
>
<RefreshCw
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
@@ -757,13 +770,6 @@ export function DataTable<TData, TValue>({
align="end"
className="w-48"
>
<DropdownMenuLabel>
{t(
"toggleColumns"
) ||
"Toggle columns"}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{table
.getAllColumns()
.filter(
@@ -891,13 +897,11 @@ export function DataTable<TData, TValue>({
<DataTableEmptyState
colSpan={columns.length}
action={
showAddActionInEmptyState
? (
<div className="hidden sm:block">
{addAction}
</div>
)
: undefined
showAddActionInEmptyState ? (
<div className="hidden sm:block">
{addAction}
</div>
) : undefined
}
/>
)}