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", "virtualApiKeysName": "Name",
"virtualApiKeysDescriptionOptional": "Description (optional)", "virtualApiKeysDescriptionOptional": "Description (optional)",
"virtualApiKeysAssociateUserOptional": "Associate User (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", "virtualApiKeysAllResources": "All public inference resources",
"virtualApiKeysAllResourcesDescription": "Allow this key to access every public inference resource in the organization", "virtualApiKeysAllResourcesDescription": "Allow this key to access every public inference resource in the organization",
"virtualApiKeysSelectResources": "Public Inference Resources", "virtualApiKeysSelectResources": "Public Inference Resources",
@@ -1666,7 +1666,7 @@
"virtualApiKeysSelectResourcesDescription": "Choose which public inference resources this key can access", "virtualApiKeysSelectResourcesDescription": "Choose which public inference resources this key can access",
"virtualApiKeysNoResources": "No resources", "virtualApiKeysNoResources": "No resources",
"virtualApiKeysSecret": "Key", "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]", "virtualApiKeysSecretHint": "Use this value as a Bearer token: vk-[id].[secret]",
"virtualApiKeysViewSecret": "View Secret", "virtualApiKeysViewSecret": "View Secret",
"virtualApiKeysViewSecretTitle": "Virtual API Key Secret", "virtualApiKeysViewSecretTitle": "Virtual API Key Secret",
+1 -1
View File
@@ -391,7 +391,7 @@ export default function CreateVirtualApiKeyForm({
)} )}
{credential && ( {credential && (
<div className="space-y-4"> <div className="space-y-4">
<p>{t("virtualApiKeysSeeOnce")}</p> <p>{t("virtualApiKeysCopyKey")}</p>
<CopyTextBox <CopyTextBox
text={credential} text={credential}
wrapText={false} wrapText={false}
+58 -5
View File
@@ -7,9 +7,9 @@ import {
FormDescription, FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel,
FormMessage FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Label } from "@app/components/ui/label";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
@@ -46,7 +46,9 @@ import {
} from "@app/components/multi-resource-selector"; } from "@app/components/multi-resource-selector";
import type { SelectedResource } from "@app/components/resource-selector"; import type { SelectedResource } from "@app/components/resource-selector";
import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import CopyTextBox from "@app/components/CopyTextBox";
import type { CreatedVirtualApiKey } from "@app/components/CreateVirtualApiKeyForm"; import type { CreatedVirtualApiKey } from "@app/components/CreateVirtualApiKeyForm";
import type { GetVirtualApiKeyResponse } from "@server/routers/virtualApiKey/types";
type FormProps = { type FormProps = {
open: boolean; open: boolean;
@@ -96,6 +98,8 @@ export default function EditVirtualApiKeyForm({
const [selectedResources, setSelectedResources] = useState< const [selectedResources, setSelectedResources] = useState<
SelectedResource[] SelectedResource[]
>([]); >([]);
const [credential, setCredential] = useState<string | null>(null);
const [credentialLoading, setCredentialLoading] = useState(false);
const formSchema = z const formSchema = z
.object({ .object({
@@ -132,6 +136,55 @@ export default function EditVirtualApiKeyForm({
form.reset({ form.reset({
allResources: virtualApiKey.allResources 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]); }, [open, virtualApiKey, form]);
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmit(values: z.infer<typeof formSchema>) {
@@ -228,11 +281,11 @@ export default function EditVirtualApiKeyForm({
id="edit-virtual-api-key-form" id="edit-virtual-api-key-form"
> >
<div className="space-y-2"> <div className="space-y-2">
<FormLabel> <Label>
{t( {t(
"virtualApiKeysAssociateUserOptional" "virtualApiKeysAssociateUserOptional"
)} )}
</FormLabel> </Label>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
@@ -316,11 +369,11 @@ export default function EditVirtualApiKeyForm({
{!allResources && ( {!allResources && (
<div className="space-y-2"> <div className="space-y-2">
<FormLabel> <Label>
{t( {t(
"virtualApiKeysSelectResources" "virtualApiKeysSelectResources"
)} )}
</FormLabel> </Label>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
+61 -3
View File
@@ -35,6 +35,7 @@ import CreateVirtualApiKeyForm, {
} from "@app/components/CreateVirtualApiKeyForm"; } from "@app/components/CreateVirtualApiKeyForm";
import EditVirtualApiKeyForm from "@app/components/EditVirtualApiKeyForm"; import EditVirtualApiKeyForm from "@app/components/EditVirtualApiKeyForm";
import ViewVirtualApiKeySecret from "@app/components/ViewVirtualApiKeySecret"; import ViewVirtualApiKeySecret from "@app/components/ViewVirtualApiKeySecret";
import CopyToClipboard from "@app/components/CopyToClipboard";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { UserSelector, type SelectedUser } from "@app/components/user-selector"; import { UserSelector, type SelectedUser } from "@app/components/user-selector";
@@ -44,6 +45,8 @@ import {
} from "@app/components/resource-selector"; } from "@app/components/resource-selector";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover"; import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover";
import type { GetVirtualApiKeyResponse } from "@server/routers/virtualApiKey/types";
import { AxiosResponse } from "axios";
export type VirtualApiKeyRow = CreatedVirtualApiKey; export type VirtualApiKeyRow = CreatedVirtualApiKey;
@@ -344,8 +347,12 @@ export default function VirtualApiKeysTable({
</Button> </Button>
); );
}, },
cell: ({ row }) => cell: ({ row }) => (
`vk-${row.original.virtualApiKeyId}.••••${row.original.lastChars}` <VirtualApiKeySecretCell
virtualApiKeyId={row.original.virtualApiKeyId}
lastChars={row.original.lastChars}
/>
)
}, },
{ {
accessorKey: "createdAt", accessorKey: "createdAt",
@@ -396,7 +403,7 @@ export default function VirtualApiKeysTable({
cell: ({ row }) => { cell: ({ row }) => {
const keyRow = row.original; const keyRow = row.original;
return ( return (
<div className="flex items-center justify-end"> <div className="flex items-center justify-end gap-2">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0"> <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 { Input } from "@app/components/ui/input";
import { DataTablePagination } from "@app/components/DataTablePagination"; import { DataTablePagination } from "@app/components/DataTablePagination";
import { dataTableFilterDropdownContentClassName } from "@app/lib/dataTableFilterPopover"; 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 { import {
Card, Card,
CardContent, CardContent,
@@ -523,29 +530,33 @@ export function DataTable<TData, TValue>({
addButtonText && ((addActions && addActions.length > 0) || onAdd) addButtonText && ((addActions && addActions.length > 0) || onAdd)
); );
const showAddActionInEmptyState = !hasRows && hasAddAction; const showAddActionInEmptyState = !hasRows && hasAddAction;
const addAction = addActions && addActions.length > 0 && addButtonText ? ( const addAction =
<DropdownMenu> addActions && addActions.length > 0 && addButtonText ? (
<DropdownMenuTrigger asChild> <DropdownMenu>
<Button disabled={addButtonDisabled}> <DropdownMenuTrigger asChild>
<Plus className="mr-2 h-4 w-4" /> <Button disabled={addButtonDisabled}>
{addButtonText} <Plus className="mr-2 h-4 w-4" />
<ChevronDown className="ml-2 h-4 w-4" /> {addButtonText}
</Button> <ChevronDown className="ml-2 h-4 w-4" />
</DropdownMenuTrigger> </Button>
<DropdownMenuContent align="end"> </DropdownMenuTrigger>
{addActions.map((action, i) => ( <DropdownMenuContent align="end">
<DropdownMenuItem key={i} onSelect={() => action.onSelect()}> {addActions.map((action, i) => (
{action.label} <DropdownMenuItem
</DropdownMenuItem> key={i}
))} onSelect={() => action.onSelect()}
</DropdownMenuContent> >
</DropdownMenu> {action.label}
) : onAdd && addButtonText ? ( </DropdownMenuItem>
<Button onClick={onAdd} disabled={addButtonDisabled}> ))}
<Plus className="mr-2 h-4 w-4" /> </DropdownMenuContent>
{addButtonText} </DropdownMenu>
</Button> ) : onAdd && addButtonText ? (
) : null; <Button onClick={onAdd} disabled={addButtonDisabled}>
<Plus className="mr-2 h-4 w-4" />
{addButtonText}
</Button>
) : null;
return ( return (
<div className="container mx-auto max-w-12xl"> <div className="container mx-auto max-w-12xl">
@@ -674,7 +685,9 @@ export function DataTable<TData, TValue>({
<Button <Button
variant="outline" variant="outline"
onClick={onRefresh} onClick={onRefresh}
disabled={isRefreshing || refreshButtonDisabled} disabled={
isRefreshing || refreshButtonDisabled
}
> >
<RefreshCw <RefreshCw
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
@@ -757,13 +770,6 @@ export function DataTable<TData, TValue>({
align="end" align="end"
className="w-48" className="w-48"
> >
<DropdownMenuLabel>
{t(
"toggleColumns"
) ||
"Toggle columns"}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{table {table
.getAllColumns() .getAllColumns()
.filter( .filter(
@@ -891,13 +897,11 @@ export function DataTable<TData, TValue>({
<DataTableEmptyState <DataTableEmptyState
colSpan={columns.length} colSpan={columns.length}
action={ action={
showAddActionInEmptyState showAddActionInEmptyState ? (
? ( <div className="hidden sm:block">
<div className="hidden sm:block"> {addAction}
{addAction} </div>
</div> ) : undefined
)
: undefined
} }
/> />
)} )}