Revert "Regenerate Key State Management and Authentication Issues (#12729)"

This reverts commit 663abbe275.
This commit is contained in:
Ishaan Jaff
2025-07-19 12:42:20 -07:00
parent e03bc3ec7e
commit 5c7e5d4324
2 changed files with 114 additions and 288 deletions
@@ -13,37 +13,19 @@ import {
Title,
Badge,
TextInput,
Select as TremorSelect,
Select as TremorSelect
} from "@tremor/react";
import {
ArrowLeftIcon,
TrashIcon,
RefreshIcon,
} from "@heroicons/react/outline";
import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline";
import { keyDeleteCall, keyUpdateCall } from "./networking";
import { KeyResponse } from "./key_team_helpers/key_list";
import {
Form,
Input,
InputNumber,
message,
Select,
Tooltip,
Button as AntdButton,
} from "antd";
import { Form, Input, InputNumber, message, Select, Tooltip, Button as AntdButton } from "antd";
import { KeyEditView } from "./key_edit_view";
import { RegenerateKeyModal } from "./regenerate_key_modal";
import { rolesWithWriteAccess } from "../utils/roles";
import { rolesWithWriteAccess } from '../utils/roles';
import ObjectPermissionsView from "./object_permissions_view";
import LoggingSettingsView from "./logging_settings_view";
import {
copyToClipboard as utilCopyToClipboard,
formatNumberWithCommas,
} from "@/utils/dataUtils";
import {
extractLoggingSettings,
formatMetadataForDisplay,
} from "./key_info_utils";
import { copyToClipboard as utilCopyToClipboard, formatNumberWithCommas } from "@/utils/dataUtils";
import { extractLoggingSettings, formatMetadataForDisplay } from "./key_info_utils";
import { CopyIcon, CheckIcon } from "lucide-react";
interface KeyInfoViewProps {
@@ -57,38 +39,20 @@ interface KeyInfoViewProps {
userRole: string | null;
teams: any[] | null;
premiumUser: boolean;
setAccessToken: (token: string) => void;
}
export default function KeyInfoView({
keyId,
onClose,
keyData,
accessToken,
userID,
userRole,
teams,
onKeyDataUpdate,
onDelete,
premiumUser,
setAccessToken,
}: KeyInfoViewProps) {
export default function KeyInfoView({ keyId, onClose, keyData, accessToken, userID, userRole, teams, onKeyDataUpdate, onDelete, premiumUser }: KeyInfoViewProps) {
const [isEditing, setIsEditing] = useState(false);
const [form] = Form.useForm();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false);
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
const [currentKeyData, setCurrentKeyData] = useState(keyData);
React.useEffect(() => {
setCurrentKeyData(keyData);
}, [keyData]);
if (!currentKeyData) {
if (!keyData) {
return (
<div className="p-4">
<Button
icon={ArrowLeftIcon}
<Button
icon={ArrowLeftIcon}
variant="light"
onClick={onClose}
className="mb-4"
@@ -110,22 +74,19 @@ export default function KeyInfoView({
// Handle object_permission updates
if (formValues.vector_stores !== undefined) {
formValues.object_permission = {
...currentKeyData.object_permission,
vector_stores: formValues.vector_stores || [],
...keyData.object_permission,
vector_stores: formValues.vector_stores || []
};
// Remove vector_stores from the top level as it should be in object_permission
delete formValues.vector_stores;
}
if (formValues.mcp_servers_and_groups !== undefined) {
const { servers, accessGroups } = formValues.mcp_servers_and_groups || {
servers: [],
accessGroups: [],
};
const { servers, accessGroups } = formValues.mcp_servers_and_groups || { servers: [], accessGroups: [] };
formValues.object_permission = {
...currentKeyData.object_permission,
...keyData.object_permission,
mcp_servers: servers || [],
mcp_access_groups: accessGroups || [],
mcp_access_groups: accessGroups || []
};
// Remove mcp_servers_and_groups from the top level as it should be in object_permission
delete formValues.mcp_servers_and_groups;
@@ -137,12 +98,8 @@ export default function KeyInfoView({
const parsedMetadata = JSON.parse(formValues.metadata);
formValues.metadata = {
...parsedMetadata,
...(formValues.guardrails?.length > 0
? { guardrails: formValues.guardrails }
: {}),
...(formValues.logging_settings
? { logging: formValues.logging_settings }
: {}),
...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}),
...(formValues.logging_settings ? { logging: formValues.logging_settings } : {})
};
} catch (error) {
console.error("Error parsing metadata JSON:", error);
@@ -152,12 +109,8 @@ export default function KeyInfoView({
} else {
formValues.metadata = {
...(formValues.metadata || {}),
...(formValues.guardrails?.length > 0
? { guardrails: formValues.guardrails }
: {}),
...(formValues.logging_settings
? { logging: formValues.logging_settings }
: {}),
...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}),
...(formValues.logging_settings ? { logging: formValues.logging_settings } : {})
};
}
@@ -168,46 +121,31 @@ export default function KeyInfoView({
const durationMap: Record<string, string> = {
daily: "24h",
weekly: "7d",
monthly: "30d",
monthly: "30d"
};
formValues.budget_duration = durationMap[formValues.budget_duration];
}
const newKeyValues = await keyUpdateCall(accessToken, formValues);
setCurrentKeyData((prev) =>
prev ? { ...prev, ...newKeyValues } : newKeyValues,
);
if (onKeyDataUpdate) {
onKeyDataUpdate(newKeyValues);
onKeyDataUpdate(newKeyValues)
}
message.success("Key updated successfully");
setIsEditing(false);
// Refresh key data here if needed
} catch (error) {
message.error("Failed to update key");
console.error("Error updating key:", error);
}
};
const handleRegenerateKeyUpdate = (updatedKeyData: Partial<KeyResponse>) => {
setCurrentKeyData((prev) =>
prev ? { ...prev, ...updatedKeyData } : (updatedKeyData as KeyResponse),
);
if (onKeyDataUpdate) {
onKeyDataUpdate(updatedKeyData);
}
};
const handleDelete = async () => {
try {
if (!accessToken) return;
await keyDeleteCall(accessToken as string, currentKeyData.token);
await keyDeleteCall(accessToken as string, keyData.token);
message.success("Key deleted successfully");
if (onDelete) {
onDelete();
onDelete()
}
onClose();
} catch (error) {
@@ -230,47 +168,34 @@ export default function KeyInfoView({
<div className="w-full h-screen p-4">
<div className="flex justify-between items-center mb-6">
<div>
<Button
icon={ArrowLeftIcon}
<Button
icon={ArrowLeftIcon}
variant="light"
onClick={onClose}
className="mb-4"
>
Back to Keys
</Button>
<Title>{currentKeyData.key_alias || "API Key"}</Title>
<div className="flex items-center cursor-pointer">
<Text className="text-gray-500 font-mono">
{currentKeyData.token}
</Text>
<Title>{keyData.key_alias || "API Key"}</Title>
<div className="flex items-center cursor-pointer"
>
<Text className="text-gray-500 font-mono">{keyData.token}</Text>
<AntdButton
type="text"
size="small"
icon={
copiedStates["key-id"] ? (
<CheckIcon size={12} />
) : (
<CopyIcon size={12} />
)
}
onClick={() => copyToClipboard(currentKeyData.token, "key-id")}
icon={copiedStates["key-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(keyData.token, "key-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["key-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
copiedStates["key-id"]
? 'text-green-600 bg-green-50 border-green-200'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
/>
</div>
</div>
{userRole && rolesWithWriteAccess.includes(userRole) && (
<div className="flex gap-2">
<Tooltip
title={
!premiumUser
? "This is a LiteLLM Enterprise feature, and requires a valid key to use."
: ""
}
>
<Tooltip title={!premiumUser ? "This is a LiteLLM Enterprise feature, and requires a valid key to use." : ""}>
<span className="inline-block">
<Button
icon={RefreshIcon}
@@ -297,32 +222,22 @@ export default function KeyInfoView({
{/* Add RegenerateKeyModal */}
<RegenerateKeyModal
selectedToken={currentKeyData}
selectedToken={keyData}
visible={isRegenerateModalOpen}
onClose={() => setIsRegenerateModalOpen(false)}
accessToken={accessToken}
setAccessToken={setAccessToken}
premiumUser={premiumUser}
onKeyUpdate={handleRegenerateKeyUpdate}
/>
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 transition-opacity"
aria-hidden="true"
>
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
&#8203;
</span>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
@@ -340,7 +255,11 @@ export default function KeyInfoView({
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<Button onClick={handleDelete} color="red" className="ml-2">
<Button
onClick={handleDelete}
color="red"
className="ml-2"
>
Delete
</Button>
<Button onClick={() => setIsDeleteModalOpen(false)}>
@@ -365,41 +284,24 @@ export default function KeyInfoView({
<Card>
<Text>Spend</Text>
<div className="mt-2">
<Title>
${formatNumberWithCommas(currentKeyData.spend, 4)}
</Title>
<Text>
of{" "}
{currentKeyData.max_budget !== null
? `$${formatNumberWithCommas(currentKeyData.max_budget)}`
: "Unlimited"}
</Text>
<Title>${formatNumberWithCommas(keyData.spend, 4)}</Title>
<Text>of {keyData.max_budget !== null ? `$${formatNumberWithCommas(keyData.max_budget)}` : "Unlimited"}</Text>
</div>
</Card>
<Card>
<Text>Rate Limits</Text>
<div className="mt-2">
<Text>
TPM:{" "}
{currentKeyData.tpm_limit !== null
? currentKeyData.tpm_limit
: "Unlimited"}
</Text>
<Text>
RPM:{" "}
{currentKeyData.rpm_limit !== null
? currentKeyData.rpm_limit
: "Unlimited"}
</Text>
<Text>TPM: {keyData.tpm_limit !== null ? keyData.tpm_limit : "Unlimited"}</Text>
<Text>RPM: {keyData.rpm_limit !== null ? keyData.rpm_limit : "Unlimited"}</Text>
</div>
</Card>
<Card>
<Text>Models</Text>
<div className="mt-2 flex flex-wrap gap-2">
{currentKeyData.models && currentKeyData.models.length > 0 ? (
currentKeyData.models.map((model, index) => (
{keyData.models && keyData.models.length > 0 ? (
keyData.models.map((model, index) => (
<Badge key={index} color="red">
{model}
</Badge>
@@ -412,14 +314,14 @@ export default function KeyInfoView({
<Card>
<ObjectPermissionsView
objectPermission={currentKeyData.object_permission}
objectPermission={keyData.object_permission}
variant="inline"
accessToken={accessToken}
/>
</Card>
<LoggingSettingsView
loggingConfigs={extractLoggingSettings(currentKeyData.metadata)}
loggingConfigs={extractLoggingSettings(keyData.metadata)}
variant="card"
/>
</Grid>
@@ -430,18 +332,16 @@ export default function KeyInfoView({
<Card className="overflow-y-auto max-h-[65vh]">
<div className="flex justify-between items-center mb-4">
<Title>Key Settings</Title>
{!isEditing &&
userRole &&
rolesWithWriteAccess.includes(userRole) && (
<Button variant="light" onClick={() => setIsEditing(true)}>
Edit Settings
</Button>
)}
{!isEditing && userRole && rolesWithWriteAccess.includes(userRole) && (
<Button variant="light" onClick={() => setIsEditing(true)}>
Edit Settings
</Button>
)}
</div>
{isEditing ? (
<KeyEditView
keyData={currentKeyData}
keyData={keyData}
onCancel={() => setIsEditing(false)}
onSubmit={handleKeyUpdate}
teams={teams}
@@ -453,57 +353,49 @@ export default function KeyInfoView({
<div className="space-y-4">
<div>
<Text className="font-medium">Key ID</Text>
<Text className="font-mono">{currentKeyData.token}</Text>
<Text className="font-mono">{keyData.token}</Text>
</div>
<div>
<Text className="font-medium">Key Alias</Text>
<Text>{currentKeyData.key_alias || "Not Set"}</Text>
<Text>{keyData.key_alias || "Not Set"}</Text>
</div>
<div>
<Text className="font-medium">Secret Key</Text>
<Text className="font-mono">{currentKeyData.key_name}</Text>
<Text className="font-mono">{keyData.key_name}</Text>
</div>
<div>
<Text className="font-medium">Team ID</Text>
<Text>{currentKeyData.team_id || "Not Set"}</Text>
<Text>{keyData.team_id || "Not Set"}</Text>
</div>
<div>
<Text className="font-medium">Organization</Text>
<Text>{currentKeyData.organization_id || "Not Set"}</Text>
<Text>{keyData.organization_id || "Not Set"}</Text>
</div>
<div>
<Text className="font-medium">Created</Text>
<Text>
{new Date(currentKeyData.created_at).toLocaleString()}
</Text>
<Text>{new Date(keyData.created_at).toLocaleString()}</Text>
</div>
<div>
<Text className="font-medium">Expires</Text>
<Text>
{currentKeyData.expires
? new Date(currentKeyData.expires).toLocaleString()
: "Never"}
</Text>
<Text>{keyData.expires ? new Date(keyData.expires).toLocaleString() : "Never"}</Text>
</div>
<div>
<Text className="font-medium">Spend</Text>
<Text>
${formatNumberWithCommas(currentKeyData.spend, 4)} USD
</Text>
<Text>${formatNumberWithCommas(keyData.spend, 4)} USD</Text>
</div>
<div>
<Text className="font-medium">Budget</Text>
<Text>
{currentKeyData.max_budget !== null
? `$${formatNumberWithCommas(currentKeyData.max_budget, 2)}`
{keyData.max_budget !== null
? `$${formatNumberWithCommas(keyData.max_budget, 2)}`
: "Unlimited"}
</Text>
</div>
@@ -511,9 +403,8 @@ export default function KeyInfoView({
<div>
<Text className="font-medium">Models</Text>
<div className="flex flex-wrap gap-2 mt-1">
{currentKeyData.models &&
currentKeyData.models.length > 0 ? (
currentKeyData.models.map((model, index) => (
{keyData.models && keyData.models.length > 0 ? (
keyData.models.map((model, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 rounded text-xs"
@@ -529,60 +420,29 @@ export default function KeyInfoView({
<div>
<Text className="font-medium">Rate Limits</Text>
<Text>
TPM:{" "}
{currentKeyData.tpm_limit !== null
? currentKeyData.tpm_limit
: "Unlimited"}
</Text>
<Text>
RPM:{" "}
{currentKeyData.rpm_limit !== null
? currentKeyData.rpm_limit
: "Unlimited"}
</Text>
<Text>
Max Parallel Requests:{" "}
{currentKeyData.max_parallel_requests !== null
? currentKeyData.max_parallel_requests
: "Unlimited"}
</Text>
<Text>
Model TPM Limits:{" "}
{currentKeyData.metadata?.model_tpm_limit
? JSON.stringify(
currentKeyData.metadata.model_tpm_limit,
)
: "Unlimited"}
</Text>
<Text>
Model RPM Limits:{" "}
{currentKeyData.metadata?.model_rpm_limit
? JSON.stringify(
currentKeyData.metadata.model_rpm_limit,
)
: "Unlimited"}
</Text>
<Text>TPM: {keyData.tpm_limit !== null ? keyData.tpm_limit : "Unlimited"}</Text>
<Text>RPM: {keyData.rpm_limit !== null ? keyData.rpm_limit : "Unlimited"}</Text>
<Text>Max Parallel Requests: {keyData.max_parallel_requests !== null ? keyData.max_parallel_requests : "Unlimited"}</Text>
<Text>Model TPM Limits: {keyData.metadata?.model_tpm_limit ? JSON.stringify(keyData.metadata.model_tpm_limit) : "Unlimited"}</Text>
<Text>Model RPM Limits: {keyData.metadata?.model_rpm_limit ? JSON.stringify(keyData.metadata.model_rpm_limit) : "Unlimited"}</Text>
</div>
<div>
<Text className="font-medium">Metadata</Text>
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
{formatMetadataForDisplay(currentKeyData.metadata)}
{formatMetadataForDisplay(keyData.metadata)}
</pre>
</div>
<ObjectPermissionsView
objectPermission={currentKeyData.object_permission}
objectPermission={keyData.object_permission}
variant="inline"
className="pt-4 border-t border-gray-200"
accessToken={accessToken}
/>
<LoggingSettingsView
loggingConfigs={extractLoggingSettings(
currentKeyData.metadata,
)}
loggingConfigs={extractLoggingSettings(keyData.metadata)}
variant="inline"
className="pt-4 border-t border-gray-200"
/>
@@ -594,4 +454,4 @@ export default function KeyInfoView({
</TabGroup>
</div>
);
}
}
@@ -11,9 +11,7 @@ interface RegenerateKeyModalProps {
visible: boolean;
onClose: () => void;
accessToken: string | null;
setAccessToken: (token: string) => void;
premiumUser: boolean;
onKeyUpdate?: (updatedKeyData: Partial<KeyResponse>) => void;
}
export function RegenerateKeyModal({
@@ -21,9 +19,7 @@ export function RegenerateKeyModal({
visible,
onClose,
accessToken,
setAccessToken,
premiumUser,
onKeyUpdate,
}: RegenerateKeyModalProps) {
const [form] = Form.useForm();
const [regeneratedKey, setRegeneratedKey] = useState<string | null>(null);
@@ -86,59 +82,19 @@ export function RegenerateKeyModal({
const handleRegenerateKey = async () => {
if (!selectedToken || !accessToken) return;
const isOwnKey = selectedToken.token === accessToken;
setIsRegenerating(true);
try {
const formValues = await form.validateFields();
const response = await regenerateKeyCall(accessToken, selectedToken.token, formValues);
setRegeneratedKey(response.key);
if (onKeyUpdate) {
const updatedKeyData = {
...selectedToken,
token: response.key,
key_name: response.key,
max_budget: formValues.max_budget,
tpm_limit: formValues.tpm_limit,
rpm_limit: formValues.rpm_limit,
expires: formValues.duration
? calculateNewExpiryTime(formValues.duration) || undefined
: selectedToken.expires,
};
onKeyUpdate(updatedKeyData);
}
// If user regenerated their own authentication key, update accessToken
if (isOwnKey) {
setAccessToken(response.key);
}
message.success("API Key regenerated successfully");
} catch (error) {
console.error("Error regenerating key:", error);
message.error("Failed to regenerate API Key");
setIsRegenerating(false);
setIsRegenerating(false); // Reset regenerating state on error
}
};
const calculateNewExpiryTime = (duration: string) => {
const now = new Date();
let newExpiry: Date;
if (duration.endsWith("s")) {
newExpiry = add(now, { seconds: parseInt(duration) });
} else if (duration.endsWith("h")) {
newExpiry = add(now, { hours: parseInt(duration) });
} else if (duration.endsWith("d")) {
newExpiry = add(now, { days: parseInt(duration) });
} else {
return null;
}
return newExpiry.toISOString();
};
const handleClose = () => {
setRegeneratedKey(null);
setIsRegenerating(false);
@@ -151,43 +107,49 @@ export function RegenerateKeyModal({
title="Regenerate API Key"
open={visible}
onCancel={handleClose}
footer={
regeneratedKey
? [
<Button key="close" onClick={handleClose}>
Close
</Button>,
]
: [
<Button key="cancel" onClick={handleClose} className="mr-2">
Cancel
</Button>,
<Button key="regenerate" onClick={handleRegenerateKey} disabled={isRegenerating}>
{isRegenerating ? "Regenerating..." : "Regenerate"}
</Button>,
]
}
footer={regeneratedKey ? [
<Button key="close" onClick={handleClose}>
Close
</Button>,
] : [
<Button key="cancel" onClick={handleClose} className="mr-2">
Cancel
</Button>,
<Button
key="regenerate"
onClick={handleRegenerateKey}
disabled={isRegenerating}
>
{isRegenerating ? "Regenerating..." : "Regenerate"}
</Button>,
]}
>
{regeneratedKey ? (
<Grid numItems={1} className="gap-2 w-full">
<Title>Regenerated Key</Title>
<Col numColSpan={1}>
<p>
Please replace your old key with the new key generated. For security reasons,{" "}
<b>you will not be able to view it again</b> through your LiteLLM account. If you lose this secret key,
you will need to generate a new one.
Please replace your old key with the new key generated. For
security reasons, <b>you will not be able to view it again</b>{" "}
through your LiteLLM account. If you lose this secret key, you
will need to generate a new one.
</p>
</Col>
<Col numColSpan={1}>
<Text className="mt-3">Key Alias:</Text>
<div className="bg-gray-100 p-2 rounded mb-2">
<pre className="break-words whitespace-normal">{selectedToken?.key_alias || "No alias set"}</pre>
<pre className="break-words whitespace-normal">
{selectedToken?.key_alias || "No alias set"}
</pre>
</div>
<Text className="mt-3">New API Key:</Text>
<div className="bg-gray-100 p-2 rounded mb-2">
<pre className="break-words whitespace-normal">{regeneratedKey}</pre>
</div>
<CopyToClipboard text={regeneratedKey} onCopy={() => message.success("API Key copied to clipboard")}>
<CopyToClipboard
text={regeneratedKey}
onCopy={() => message.success("API Key copied to clipboard")}
>
<Button className="mt-3">Copy API Key</Button>
</CopyToClipboard>
</Col>
@@ -220,9 +182,13 @@ export function RegenerateKeyModal({
<div className="mt-2 text-sm text-gray-500">
Current expiry: {selectedToken?.expires ? new Date(selectedToken.expires).toLocaleString() : "Never"}
</div>
{newExpiryTime && <div className="mt-2 text-sm text-green-600">New expiry: {newExpiryTime}</div>}
{newExpiryTime && (
<div className="mt-2 text-sm text-green-600">
New expiry: {newExpiryTime}
</div>
)}
</Form>
)}
</Modal>
);
}
}