LibreChat/client/src/components/Nav/SettingsTabs/APIKeys/APIKeyRow.tsx
Danny Avila 2c898b030b 🔑 feat: Surface User-Provided API Keys In Settings
Add a dedicated API Keys tab in user settings that lists endpoints
configured with `apiKey: "user_provided"` and reuses SetKeyDialog for
key entry and revocation. The tab is only shown when at least one
endpoint requires a user key.

Restores access to user-provided key entry when `modelSpecs.list` is
configured, since the sidebar endpoints dropdown is hidden by default
in that case (closes #12743).
2026-05-25 08:56:13 -04:00

94 lines
3.3 KiB
TypeScript

import React, { useMemo, useState } from 'react';
import { Button } from '@librechat/client';
import { alternateName, getEndpointField } from 'librechat-data-provider';
import type { EModelEndpoint, TEndpointsConfig } from 'librechat-data-provider';
import { SetKeyDialog } from '~/components/Input/SetKeyDialog';
import { icons } from '~/hooks/Endpoint/Icons';
import { useUserKey, useLocalize } from '~/hooks';
import { getIconKey } from '~/utils';
interface APIKeyRowProps {
endpoint: string;
endpointsConfig: TEndpointsConfig;
}
const APIKeyRow = ({ endpoint, endpointsConfig }: APIKeyRowProps) => {
const localize = useLocalize();
const [dialogOpen, setDialogOpen] = useState(false);
const { getExpiry, checkExpiry } = useUserKey(endpoint);
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
const iconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointType });
const Icon = icons[iconKey];
const label = useMemo(() => alternateName[endpoint] || endpoint, [endpoint]);
const expiry = getExpiry();
const hasKey = !!expiry && checkExpiry();
const expiryLabel = useMemo(() => {
if (!expiry) {
return localize('com_nav_api_keys_not_set');
}
if (expiry === 'never') {
return localize('com_endpoint_config_key_never_expires');
}
return `${localize('com_endpoint_config_key_encryption')} ${new Date(expiry).toLocaleString()}`;
}, [expiry, localize]);
return (
<>
<div className="flex items-center justify-between gap-3 py-2">
<div className="flex min-w-0 items-center gap-3">
{Icon && (
<div className="flex shrink-0 items-center justify-center" aria-hidden="true">
{React.createElement(Icon, {
size: 20,
className: 'text-text-primary shrink-0 icon-md',
iconURL,
endpoint,
})}
</div>
)}
<div className="min-w-0">
<div className="truncate font-medium text-text-primary">{label}</div>
<div className="truncate text-xs text-text-secondary">{expiryLabel}</div>
</div>
</div>
<Button variant="outline" onClick={() => setDialogOpen(true)}>
{hasKey ? localize('com_ui_update') : localize('com_endpoint_config_key')}
</Button>
</div>
{dialogOpen && (
<SetKeyDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
endpoint={endpoint as EModelEndpoint}
endpointType={endpointType}
userProvideURL={getEndpointField(endpointsConfig, endpoint, 'userProvideURL')}
userProvideAccessKeyId={getEndpointField(
endpointsConfig,
endpoint,
'userProvideAccessKeyId',
)}
userProvideSecretAccessKey={getEndpointField(
endpointsConfig,
endpoint,
'userProvideSecretAccessKey',
)}
userProvideSessionToken={getEndpointField(
endpointsConfig,
endpoint,
'userProvideSessionToken',
)}
userProvideBearerToken={getEndpointField(
endpointsConfig,
endpoint,
'userProvideBearerToken',
)}
/>
)}
</>
);
};
export default APIKeyRow;