mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 06:18:55 +00:00
fix(cliproxy): add management_key support for remote proxy auth separation
- Add management_key field to CliproxyServerConfig for separate management API auth - Update proxy-target-resolver with buildManagementHeaders() for /v0/management/* endpoints - Update stats-fetcher and remote-auth-fetcher to use management headers - Add management_key input field to dashboard remote proxy settings - Disable quota UI in remote mode (tokens on remote server) - Fix preset application to use dynamic API key from config instead of hardcoded placeholder
This commit is contained in:
@@ -22,8 +22,10 @@ export interface ProxyTarget {
|
|||||||
port: number;
|
port: number;
|
||||||
/** Protocol (http/https) */
|
/** Protocol (http/https) */
|
||||||
protocol: 'http' | 'https';
|
protocol: 'http' | 'https';
|
||||||
/** Optional auth token - only send header if defined and non-empty */
|
/** Optional auth token for API endpoints - only send header if defined and non-empty */
|
||||||
authToken?: string;
|
authToken?: string;
|
||||||
|
/** Optional management key for management API endpoints (/v0/management/*) */
|
||||||
|
managementKey?: string;
|
||||||
/** True if targeting remote server, false if local */
|
/** True if targeting remote server, false if local */
|
||||||
isRemote: boolean;
|
isRemote: boolean;
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,7 @@ export function getProxyTarget(): ProxyTarget {
|
|||||||
port,
|
port,
|
||||||
protocol,
|
protocol,
|
||||||
authToken: config.remote.auth_token || undefined, // Empty string -> undefined
|
authToken: config.remote.auth_token || undefined, // Empty string -> undefined
|
||||||
|
managementKey: config.remote.management_key || undefined, // Empty string -> undefined
|
||||||
isRemote: true,
|
isRemote: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -102,3 +105,29 @@ export function buildProxyHeaders(
|
|||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build request headers for management API endpoints (/v0/management/*).
|
||||||
|
* Uses management_key if configured, otherwise falls back to authToken.
|
||||||
|
*
|
||||||
|
* @param target Resolved proxy target
|
||||||
|
* @param additionalHeaders Extra headers to merge
|
||||||
|
*/
|
||||||
|
export function buildManagementHeaders(
|
||||||
|
target: ProxyTarget,
|
||||||
|
additionalHeaders: Record<string, string> = {}
|
||||||
|
): Record<string, string> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
...additionalHeaders,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use management key for management API, fallback to authToken
|
||||||
|
const authKey = target.managementKey ?? target.authToken;
|
||||||
|
|
||||||
|
if (authKey) {
|
||||||
|
headers['Authorization'] = `Bearer ${authKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import {
|
import {
|
||||||
getProxyTarget,
|
getProxyTarget,
|
||||||
buildProxyUrl,
|
buildProxyUrl,
|
||||||
buildProxyHeaders,
|
buildManagementHeaders,
|
||||||
ProxyTarget,
|
ProxyTarget,
|
||||||
} from './proxy-target-resolver';
|
} from './proxy-target-resolver';
|
||||||
|
|
||||||
@@ -81,6 +81,7 @@ export async function fetchRemoteAuthStatus(target?: ProxyTarget): Promise<Remot
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = buildProxyUrl(proxyTarget, '/v0/management/auth-files');
|
const url = buildProxyUrl(proxyTarget, '/v0/management/auth-files');
|
||||||
|
const headers = buildManagementHeaders(proxyTarget);
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), REMOTE_FETCH_TIMEOUT_MS);
|
const timeoutId = setTimeout(() => controller.abort(), REMOTE_FETCH_TIMEOUT_MS);
|
||||||
@@ -88,7 +89,7 @@ export async function fetchRemoteAuthStatus(target?: ProxyTarget): Promise<Remot
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
headers: buildProxyHeaders(proxyTarget),
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|||||||
@@ -6,7 +6,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getEffectiveApiKey, getEffectiveManagementSecret } from './auth-token-manager';
|
import { getEffectiveApiKey, getEffectiveManagementSecret } from './auth-token-manager';
|
||||||
import { getProxyTarget, buildProxyUrl, buildProxyHeaders } from './proxy-target-resolver';
|
import {
|
||||||
|
getProxyTarget,
|
||||||
|
buildProxyUrl,
|
||||||
|
buildProxyHeaders,
|
||||||
|
buildManagementHeaders,
|
||||||
|
} from './proxy-target-resolver';
|
||||||
|
|
||||||
/** Per-account usage statistics */
|
/** Per-account usage statistics */
|
||||||
export interface AccountUsageStats {
|
export interface AccountUsageStats {
|
||||||
@@ -109,9 +114,9 @@ export async function fetchCliproxyStats(port?: number): Promise<CliproxyStats |
|
|||||||
}
|
}
|
||||||
const url = buildProxyUrl(target, '/v0/management/usage');
|
const url = buildProxyUrl(target, '/v0/management/usage');
|
||||||
|
|
||||||
// For management endpoints, use CCS control panel secret for local, remote auth for remote
|
// For management endpoints, use management key for remote, local management secret for local
|
||||||
const headers = target.isRemote
|
const headers = target.isRemote
|
||||||
? buildProxyHeaders(target)
|
? buildManagementHeaders(target)
|
||||||
: { Accept: 'application/json', Authorization: `Bearer ${getEffectiveManagementSecret()}` };
|
: { Accept: 'application/json', Authorization: `Bearer ${getEffectiveManagementSecret()}` };
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -326,9 +331,9 @@ export async function fetchCliproxyErrorLogs(port?: number): Promise<CliproxyErr
|
|||||||
}
|
}
|
||||||
const url = buildProxyUrl(target, '/v0/management/request-error-logs');
|
const url = buildProxyUrl(target, '/v0/management/request-error-logs');
|
||||||
|
|
||||||
// For management endpoints, use CCS control panel secret for local, remote auth for remote
|
// For management endpoints, use management key for remote, local management secret for local
|
||||||
const headers = target.isRemote
|
const headers = target.isRemote
|
||||||
? buildProxyHeaders(target)
|
? buildManagementHeaders(target)
|
||||||
: { Accept: 'application/json', Authorization: `Bearer ${getEffectiveManagementSecret()}` };
|
: { Accept: 'application/json', Authorization: `Bearer ${getEffectiveManagementSecret()}` };
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -374,9 +379,9 @@ export async function fetchCliproxyErrorLogContent(
|
|||||||
`/v0/management/request-error-logs/${encodeURIComponent(name)}`
|
`/v0/management/request-error-logs/${encodeURIComponent(name)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// For management endpoints, use CCS control panel secret for local, remote auth for remote
|
// For management endpoints, use management key for remote, local management secret for local
|
||||||
const headers = target.isRemote
|
const headers = target.isRemote
|
||||||
? buildProxyHeaders(target)
|
? buildManagementHeaders(target)
|
||||||
: { Authorization: `Bearer ${getEffectiveManagementSecret()}` };
|
: { Authorization: `Bearer ${getEffectiveManagementSecret()}` };
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
|
|||||||
auth_token:
|
auth_token:
|
||||||
partial.cliproxy_server?.remote?.auth_token ??
|
partial.cliproxy_server?.remote?.auth_token ??
|
||||||
DEFAULT_CLIPROXY_SERVER_CONFIG.remote.auth_token,
|
DEFAULT_CLIPROXY_SERVER_CONFIG.remote.auth_token,
|
||||||
|
// management_key is optional - falls back to auth_token when not set
|
||||||
|
management_key: partial.cliproxy_server?.remote?.management_key,
|
||||||
},
|
},
|
||||||
fallback: {
|
fallback: {
|
||||||
enabled:
|
enabled:
|
||||||
|
|||||||
@@ -225,8 +225,15 @@ export interface ProxyRemoteConfig {
|
|||||||
port?: number;
|
port?: number;
|
||||||
/** Protocol for remote connection */
|
/** Protocol for remote connection */
|
||||||
protocol: 'http' | 'https';
|
protocol: 'http' | 'https';
|
||||||
/** Auth token for remote proxy (optional, sent as header) */
|
/** Auth token for remote proxy API endpoints (optional, sent as header) */
|
||||||
auth_token: string;
|
auth_token: string;
|
||||||
|
/**
|
||||||
|
* Management key for remote proxy management API endpoints.
|
||||||
|
* CLIProxyAPI uses separate authentication for management endpoints
|
||||||
|
* (/v0/management/*) via 'secret-key' config.
|
||||||
|
* If not set, falls back to auth_token for backwards compatibility.
|
||||||
|
*/
|
||||||
|
management_key?: string;
|
||||||
/** Connection timeout in milliseconds (default: 2000) */
|
/** Connection timeout in milliseconds (default: 2000) */
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
/* eslint-disable react-refresh/only-export-components */
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { ConfirmDialog } from '@/components/shared/confirm-dialog';
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog';
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||||
import { Loader2, Code2 } from 'lucide-react';
|
import { Loader2, Code2 } from 'lucide-react';
|
||||||
@@ -93,11 +94,23 @@ export function ProviderEditor({
|
|||||||
|
|
||||||
const accounts = authStatus.accounts || [];
|
const accounts = authStatus.accounts || [];
|
||||||
|
|
||||||
|
// Fetch effective API key for presets (uses configured value, not hardcoded)
|
||||||
|
const { data: authTokens } = useQuery<{ apiKey: { value: string } }>({
|
||||||
|
queryKey: ['auth-tokens-raw'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch('/api/settings/auth/tokens/raw');
|
||||||
|
if (!response.ok) return { apiKey: { value: 'ccs-internal-managed' } };
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
staleTime: 60000, // Cache for 1 minute
|
||||||
|
});
|
||||||
|
const effectiveApiKey = authTokens?.apiKey?.value ?? 'ccs-internal-managed';
|
||||||
|
|
||||||
const handleApplyPreset = (updates: Record<string, string>) => {
|
const handleApplyPreset = (updates: Record<string, string>) => {
|
||||||
const effectivePort = port ?? CLIPROXY_PORT;
|
const effectivePort = port ?? CLIPROXY_PORT;
|
||||||
updateEnvValues({
|
updateEnvValues({
|
||||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${effectivePort}/api/provider/${provider}`,
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${effectivePort}/api/provider/${provider}`,
|
||||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
ANTHROPIC_AUTH_TOKEN: effectiveApiKey,
|
||||||
...updates,
|
...updates,
|
||||||
});
|
});
|
||||||
toast.success(`Applied "${updates.ANTHROPIC_MODEL?.split('/').pop() || 'preset'}" preset`);
|
toast.success(`Applied "${updates.ANTHROPIC_MODEL?.split('/').pop() || 'preset'}" preset`);
|
||||||
@@ -107,7 +120,7 @@ export function ProviderEditor({
|
|||||||
const effectivePort = port ?? CLIPROXY_PORT;
|
const effectivePort = port ?? CLIPROXY_PORT;
|
||||||
updateEnvValues({
|
updateEnvValues({
|
||||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${effectivePort}/api/provider/${provider}`,
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${effectivePort}/api/provider/${provider}`,
|
||||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
ANTHROPIC_AUTH_TOKEN: effectiveApiKey,
|
||||||
ANTHROPIC_MODEL: values.default,
|
ANTHROPIC_MODEL: values.default,
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: values.opus,
|
ANTHROPIC_DEFAULT_OPUS_MODEL: values.opus,
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: values.sonnet,
|
ANTHROPIC_DEFAULT_SONNET_MODEL: values.sonnet,
|
||||||
@@ -189,6 +202,7 @@ export function ProviderEditor({
|
|||||||
onRemoveAccount={onRemoveAccount}
|
onRemoveAccount={onRemoveAccount}
|
||||||
isRemovingAccount={isRemovingAccount}
|
isRemovingAccount={isRemovingAccount}
|
||||||
privacyMode={privacyMode}
|
privacyMode={privacyMode}
|
||||||
|
isRemoteMode={isRemoteMode}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ interface ModelConfigTabProps {
|
|||||||
onRemoveAccount: (accountId: string) => void;
|
onRemoveAccount: (accountId: string) => void;
|
||||||
isRemovingAccount?: boolean;
|
isRemovingAccount?: boolean;
|
||||||
privacyMode?: boolean;
|
privacyMode?: boolean;
|
||||||
|
/** True if connected to remote CLIProxy (quota not available) */
|
||||||
|
isRemoteMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModelConfigTab({
|
export function ModelConfigTab({
|
||||||
@@ -60,6 +62,7 @@ export function ModelConfigTab({
|
|||||||
onRemoveAccount,
|
onRemoveAccount,
|
||||||
isRemovingAccount,
|
isRemovingAccount,
|
||||||
privacyMode,
|
privacyMode,
|
||||||
|
isRemoteMode,
|
||||||
}: ModelConfigTabProps) {
|
}: ModelConfigTabProps) {
|
||||||
// Kiro-specific: no-incognito setting (defaults to true = normal browser)
|
// Kiro-specific: no-incognito setting (defaults to true = normal browser)
|
||||||
const isKiro = provider === 'kiro';
|
const isKiro = provider === 'kiro';
|
||||||
@@ -133,7 +136,7 @@ export function ModelConfigTab({
|
|||||||
onRemoveAccount={onRemoveAccount}
|
onRemoveAccount={onRemoveAccount}
|
||||||
isRemovingAccount={isRemovingAccount}
|
isRemovingAccount={isRemovingAccount}
|
||||||
privacyMode={privacyMode}
|
privacyMode={privacyMode}
|
||||||
showQuota={provider === 'agy'}
|
showQuota={provider === 'agy' && !isRemoteMode}
|
||||||
isKiro={isKiro}
|
isKiro={isKiro}
|
||||||
kiroNoIncognito={kiroNoIncognito}
|
kiroNoIncognito={kiroNoIncognito}
|
||||||
onKiroNoIncognitoChange={saveKiroNoIncognito}
|
onKiroNoIncognitoChange={saveKiroNoIncognito}
|
||||||
|
|||||||
@@ -193,6 +193,8 @@ export interface ProxyRemoteConfig {
|
|||||||
port?: number;
|
port?: number;
|
||||||
protocol: 'http' | 'https';
|
protocol: 'http' | 'https';
|
||||||
auth_token: string;
|
auth_token: string;
|
||||||
|
/** Management key for /v0/management/* endpoints (optional, falls back to auth_token) */
|
||||||
|
management_key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fallback configuration */
|
/** Fallback configuration */
|
||||||
@@ -278,7 +280,9 @@ export const api = {
|
|||||||
cliproxy: {
|
cliproxy: {
|
||||||
list: () => request<{ variants: Variant[] }>('/cliproxy'),
|
list: () => request<{ variants: Variant[] }>('/cliproxy'),
|
||||||
getAuthStatus: () =>
|
getAuthStatus: () =>
|
||||||
request<{ authStatus: AuthStatus[]; source?: 'remote' | 'local' }>('/cliproxy/auth'),
|
request<{ authStatus: AuthStatus[]; source?: 'remote' | 'local'; error?: string }>(
|
||||||
|
'/cliproxy/auth'
|
||||||
|
),
|
||||||
create: (data: CreateVariant) =>
|
create: (data: CreateVariant) =>
|
||||||
request('/cliproxy', {
|
request('/cliproxy', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -8,6 +8,24 @@ import { MODEL_CATALOGS } from './model-catalogs';
|
|||||||
/** CLIProxy port - should match the backend configuration */
|
/** CLIProxy port - should match the backend configuration */
|
||||||
export const CLIPROXY_PORT = 8317;
|
export const CLIPROXY_PORT = 8317;
|
||||||
|
|
||||||
|
/** Default fallback API key if fetch fails */
|
||||||
|
const DEFAULT_API_KEY = 'ccs-internal-managed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch effective API key from backend
|
||||||
|
* Falls back to default if fetch fails
|
||||||
|
*/
|
||||||
|
async function fetchEffectiveApiKey(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/auth/tokens/raw');
|
||||||
|
if (!response.ok) return DEFAULT_API_KEY;
|
||||||
|
const data = await response.json();
|
||||||
|
return data?.apiKey?.value ?? DEFAULT_API_KEY;
|
||||||
|
} catch {
|
||||||
|
return DEFAULT_API_KEY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply default preset for a provider to its settings
|
* Apply default preset for a provider to its settings
|
||||||
* Uses the first model's presetMapping or falls back to using defaultModel for all tiers
|
* Uses the first model's presetMapping or falls back to using defaultModel for all tiers
|
||||||
@@ -32,11 +50,14 @@ export async function applyDefaultPreset(
|
|||||||
haiku: catalog.defaultModel,
|
haiku: catalog.defaultModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch effective API key (respects user customization)
|
||||||
|
const effectiveApiKey = await fetchEffectiveApiKey();
|
||||||
|
|
||||||
const effectivePort = port ?? CLIPROXY_PORT;
|
const effectivePort = port ?? CLIPROXY_PORT;
|
||||||
const settings = {
|
const settings = {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${effectivePort}/api/provider/${provider}`,
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${effectivePort}/api/provider/${provider}`,
|
||||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
ANTHROPIC_AUTH_TOKEN: effectiveApiKey,
|
||||||
ANTHROPIC_MODEL: mapping.default,
|
ANTHROPIC_MODEL: mapping.default,
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: mapping.opus,
|
ANTHROPIC_DEFAULT_OPUS_MODEL: mapping.opus,
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: mapping.sonnet,
|
ANTHROPIC_DEFAULT_SONNET_MODEL: mapping.sonnet,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export function useProxyConfig() {
|
|||||||
const [editedHost, setEditedHost] = useState<string | null>(null);
|
const [editedHost, setEditedHost] = useState<string | null>(null);
|
||||||
const [editedPort, setEditedPort] = useState<string | null>(null);
|
const [editedPort, setEditedPort] = useState<string | null>(null);
|
||||||
const [editedAuthToken, setEditedAuthToken] = useState<string | null>(null);
|
const [editedAuthToken, setEditedAuthToken] = useState<string | null>(null);
|
||||||
|
const [editedManagementKey, setEditedManagementKey] = useState<string | null>(null);
|
||||||
const [editedLocalPort, setEditedLocalPort] = useState<string | null>(null);
|
const [editedLocalPort, setEditedLocalPort] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchConfig = useCallback(async () => {
|
const fetchConfig = useCallback(async () => {
|
||||||
@@ -108,6 +109,8 @@ export function useProxyConfig() {
|
|||||||
setEditedPort,
|
setEditedPort,
|
||||||
editedAuthToken,
|
editedAuthToken,
|
||||||
setEditedAuthToken,
|
setEditedAuthToken,
|
||||||
|
editedManagementKey,
|
||||||
|
setEditedManagementKey,
|
||||||
editedLocalPort,
|
editedLocalPort,
|
||||||
setEditedLocalPort,
|
setEditedLocalPort,
|
||||||
fetchConfig,
|
fetchConfig,
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export default function ProxySection() {
|
|||||||
setEditedPort,
|
setEditedPort,
|
||||||
editedAuthToken,
|
editedAuthToken,
|
||||||
setEditedAuthToken,
|
setEditedAuthToken,
|
||||||
|
editedManagementKey,
|
||||||
|
setEditedManagementKey,
|
||||||
editedLocalPort,
|
editedLocalPort,
|
||||||
setEditedLocalPort,
|
setEditedLocalPort,
|
||||||
fetchConfig,
|
fetchConfig,
|
||||||
@@ -62,11 +64,13 @@ export default function ProxySection() {
|
|||||||
const hostInput = config.remote.host ?? '';
|
const hostInput = config.remote.host ?? '';
|
||||||
const portInput = config.remote.port !== undefined ? config.remote.port.toString() : '';
|
const portInput = config.remote.port !== undefined ? config.remote.port.toString() : '';
|
||||||
const authTokenInput = config.remote.auth_token ?? '';
|
const authTokenInput = config.remote.auth_token ?? '';
|
||||||
|
const managementKeyInput = config.remote.management_key ?? '';
|
||||||
const localPortInput = (config.local.port ?? 8317).toString();
|
const localPortInput = (config.local.port ?? 8317).toString();
|
||||||
|
|
||||||
const displayHost = editedHost ?? hostInput;
|
const displayHost = editedHost ?? hostInput;
|
||||||
const displayPort = editedPort ?? portInput;
|
const displayPort = editedPort ?? portInput;
|
||||||
const displayAuthToken = editedAuthToken ?? authTokenInput;
|
const displayAuthToken = editedAuthToken ?? authTokenInput;
|
||||||
|
const displayManagementKey = editedManagementKey ?? managementKeyInput;
|
||||||
const displayLocalPort = editedLocalPort ?? localPortInput;
|
const displayLocalPort = editedLocalPort ?? localPortInput;
|
||||||
|
|
||||||
// Save functions for blur events
|
// Save functions for blur events
|
||||||
@@ -97,6 +101,14 @@ export default function ProxySection() {
|
|||||||
setEditedAuthToken(null);
|
setEditedAuthToken(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveManagementKey = () => {
|
||||||
|
const value = editedManagementKey ?? displayManagementKey;
|
||||||
|
if (value !== config.remote.management_key) {
|
||||||
|
saveConfig({ remote: { ...remoteConfig, management_key: value || undefined } });
|
||||||
|
}
|
||||||
|
setEditedManagementKey(null);
|
||||||
|
};
|
||||||
|
|
||||||
const saveLocalPort = () => {
|
const saveLocalPort = () => {
|
||||||
const port = parseInt(editedLocalPort ?? displayLocalPort, 10);
|
const port = parseInt(editedLocalPort ?? displayLocalPort, 10);
|
||||||
if (!isNaN(port) && port !== config.local.port) {
|
if (!isNaN(port) && port !== config.local.port) {
|
||||||
@@ -203,12 +215,15 @@ export default function ProxySection() {
|
|||||||
displayHost={displayHost}
|
displayHost={displayHost}
|
||||||
displayPort={displayPort}
|
displayPort={displayPort}
|
||||||
displayAuthToken={displayAuthToken}
|
displayAuthToken={displayAuthToken}
|
||||||
|
displayManagementKey={displayManagementKey}
|
||||||
setEditedHost={setEditedHost}
|
setEditedHost={setEditedHost}
|
||||||
setEditedPort={setEditedPort}
|
setEditedPort={setEditedPort}
|
||||||
setEditedAuthToken={setEditedAuthToken}
|
setEditedAuthToken={setEditedAuthToken}
|
||||||
|
setEditedManagementKey={setEditedManagementKey}
|
||||||
onSaveHost={saveHost}
|
onSaveHost={saveHost}
|
||||||
onSavePort={savePort}
|
onSavePort={savePort}
|
||||||
onSaveAuthToken={saveAuthToken}
|
onSaveAuthToken={saveAuthToken}
|
||||||
|
onSaveManagementKey={saveManagementKey}
|
||||||
onSaveConfig={saveConfig}
|
onSaveConfig={saveConfig}
|
||||||
onTestConnection={handleTestConnection}
|
onTestConnection={handleTestConnection}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ interface RemoteProxyCardProps {
|
|||||||
displayHost: string;
|
displayHost: string;
|
||||||
displayPort: string;
|
displayPort: string;
|
||||||
displayAuthToken: string;
|
displayAuthToken: string;
|
||||||
|
displayManagementKey: string;
|
||||||
setEditedHost: (value: string | null) => void;
|
setEditedHost: (value: string | null) => void;
|
||||||
setEditedPort: (value: string | null) => void;
|
setEditedPort: (value: string | null) => void;
|
||||||
setEditedAuthToken: (value: string | null) => void;
|
setEditedAuthToken: (value: string | null) => void;
|
||||||
|
setEditedManagementKey: (value: string | null) => void;
|
||||||
onSaveHost: () => void;
|
onSaveHost: () => void;
|
||||||
onSavePort: () => void;
|
onSavePort: () => void;
|
||||||
onSaveAuthToken: () => void;
|
onSaveAuthToken: () => void;
|
||||||
|
onSaveManagementKey: () => void;
|
||||||
onSaveConfig: (updates: Partial<CliproxyServerConfig>) => void;
|
onSaveConfig: (updates: Partial<CliproxyServerConfig>) => void;
|
||||||
onTestConnection: () => void;
|
onTestConnection: () => void;
|
||||||
}
|
}
|
||||||
@@ -41,12 +44,15 @@ export function RemoteProxyCard({
|
|||||||
displayHost,
|
displayHost,
|
||||||
displayPort,
|
displayPort,
|
||||||
displayAuthToken,
|
displayAuthToken,
|
||||||
|
displayManagementKey,
|
||||||
setEditedHost,
|
setEditedHost,
|
||||||
setEditedPort,
|
setEditedPort,
|
||||||
setEditedAuthToken,
|
setEditedAuthToken,
|
||||||
|
setEditedManagementKey,
|
||||||
onSaveHost,
|
onSaveHost,
|
||||||
onSavePort,
|
onSavePort,
|
||||||
onSaveAuthToken,
|
onSaveAuthToken,
|
||||||
|
onSaveManagementKey,
|
||||||
onSaveConfig,
|
onSaveConfig,
|
||||||
onTestConnection,
|
onTestConnection,
|
||||||
}: RemoteProxyCardProps) {
|
}: RemoteProxyCardProps) {
|
||||||
@@ -115,18 +121,38 @@ export function RemoteProxyCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Auth Token */}
|
{/* Auth Token (API Key) */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-sm text-muted-foreground">Auth Token (optional)</label>
|
<label className="text-sm text-muted-foreground">API Key (optional)</label>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
value={displayAuthToken}
|
value={displayAuthToken}
|
||||||
onChange={(e) => setEditedAuthToken(e.target.value)}
|
onChange={(e) => setEditedAuthToken(e.target.value)}
|
||||||
onBlur={onSaveAuthToken}
|
onBlur={onSaveAuthToken}
|
||||||
placeholder="Bearer token for authentication"
|
placeholder="For /v1/* API endpoints"
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Used for API requests to /v1/chat/completions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Management Key */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm text-muted-foreground">Management Key (optional)</label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={displayManagementKey}
|
||||||
|
onChange={(e) => setEditedManagementKey(e.target.value)}
|
||||||
|
onBlur={onSaveManagementKey}
|
||||||
|
placeholder="For /v0/management/* endpoints"
|
||||||
|
className="font-mono"
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Used for dashboard management APIs. Falls back to API Key if not set.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Test Connection */}
|
{/* Test Connection */}
|
||||||
|
|||||||
Reference in New Issue
Block a user