2026-02-06 23:01:30 -05:00

229 lines
8.5 KiB
TypeScript

'use client';
import React, { useState, useCallback } from 'react';
import { Key, Globe, Shield, XCircle, X } from 'lucide-react';
import type { AuthConfig } from '@mcpengine/ai-pipeline/types';
interface AuthConfigPanelProps {
config?: AuthConfig;
onChange: (config: AuthConfig | undefined) => void;
}
type AuthType = 'none' | 'api_key' | 'oauth2' | 'bearer';
const AUTH_OPTIONS: { value: AuthType; label: string; icon: React.ReactNode }[] = [
{ value: 'none', label: 'None', icon: <XCircle className="w-3.5 h-3.5" /> },
{ value: 'api_key', label: 'API Key', icon: <Key className="w-3.5 h-3.5" /> },
{ value: 'oauth2', label: 'OAuth2', icon: <Globe className="w-3.5 h-3.5" /> },
{ value: 'bearer', label: 'Bearer', icon: <Shield className="w-3.5 h-3.5" /> },
];
export function AuthConfigPanel({ config, onChange }: AuthConfigPanelProps) {
const currentType: AuthType = (['api_key', 'oauth2', 'bearer'].includes(config?.type ?? '') ? config!.type : 'none') as AuthType;
const [keyName, setKeyName] = useState(config?.keyName ?? '');
const [keyLocation, setKeyLocation] = useState<'header' | 'query'>(
config?.keyLocation ?? 'header'
);
const [authUrl, setAuthUrl] = useState(config?.oauthConfig?.authUrl ?? '');
const [tokenUrl, setTokenUrl] = useState(config?.oauthConfig?.tokenUrl ?? '');
const [scopes, setScopes] = useState<string[]>(config?.oauthConfig?.scopes ?? []);
const [scopeInput, setScopeInput] = useState('');
const [bearerInstructions, setBearerInstructions] = useState('');
const selectType = useCallback(
(type: AuthType) => {
if (type === 'none') {
onChange(undefined);
return;
}
const base: AuthConfig = { type };
if (type === 'api_key') {
base.keyName = keyName;
base.keyLocation = keyLocation;
} else if (type === 'oauth2') {
base.oauthConfig = { authUrl, tokenUrl, scopes };
}
onChange(base);
},
[onChange, keyName, keyLocation, authUrl, tokenUrl, scopes]
);
const addScope = useCallback(() => {
const trimmed = scopeInput.trim();
if (trimmed && !scopes.includes(trimmed)) {
const newScopes = [...scopes, trimmed];
setScopes(newScopes);
setScopeInput('');
onChange({
type: 'oauth2',
oauthConfig: { authUrl, tokenUrl, scopes: newScopes },
});
}
}, [scopeInput, scopes, onChange, authUrl, tokenUrl]);
const removeScope = useCallback(
(scope: string) => {
const newScopes = scopes.filter((s) => s !== scope);
setScopes(newScopes);
onChange({
type: 'oauth2',
oauthConfig: { authUrl, tokenUrl, scopes: newScopes },
});
},
[scopes, onChange, authUrl, tokenUrl]
);
return (
<div className="space-y-3">
{/* Auth type radio buttons */}
<div className="grid grid-cols-4 gap-1.5">
{AUTH_OPTIONS.map((opt) => (
<button
key={opt.value}
onClick={() => selectType(opt.value)}
className={`
flex flex-col items-center gap-1 py-2 px-1 rounded-lg border text-xs transition-colors
${currentType === opt.value
? 'border-indigo-500 bg-indigo-500/10 text-indigo-400'
: 'border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600'
}
`}
>
{opt.icon}
<span>{opt.label}</span>
</button>
))}
</div>
{/* API Key config */}
{currentType === 'api_key' && (
<div className="space-y-2 pt-1">
<div>
<label className="block text-[10px] text-gray-500 mb-1">Key Name</label>
<input
type="text"
value={keyName}
onChange={(e) => {
setKeyName(e.target.value);
onChange({
type: 'api_key',
keyName: e.target.value,
keyLocation,
});
}}
placeholder="e.g. X-API-Key"
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none focus:border-indigo-500 transition-colors"
/>
</div>
<div>
<label className="block text-[10px] text-gray-500 mb-1">Location</label>
<select
value={keyLocation}
onChange={(e) => {
const loc = e.target.value as 'header' | 'query';
setKeyLocation(loc);
onChange({ type: 'api_key', keyName, keyLocation: loc });
}}
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none"
>
<option value="header">Header</option>
<option value="query">Query Parameter</option>
</select>
</div>
</div>
)}
{/* OAuth2 config */}
{currentType === 'oauth2' && (
<div className="space-y-2 pt-1">
<div>
<label className="block text-[10px] text-gray-500 mb-1">Authorization URL</label>
<input
type="text"
value={authUrl}
onChange={(e) => {
setAuthUrl(e.target.value);
onChange({
type: 'oauth2',
oauthConfig: { authUrl: e.target.value, tokenUrl, scopes },
});
}}
placeholder="https://provider.com/oauth/authorize"
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none focus:border-indigo-500 transition-colors"
/>
</div>
<div>
<label className="block text-[10px] text-gray-500 mb-1">Token URL</label>
<input
type="text"
value={tokenUrl}
onChange={(e) => {
setTokenUrl(e.target.value);
onChange({
type: 'oauth2',
oauthConfig: { authUrl, tokenUrl: e.target.value, scopes },
});
}}
placeholder="https://provider.com/oauth/token"
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none focus:border-indigo-500 transition-colors"
/>
</div>
<div>
<label className="block text-[10px] text-gray-500 mb-1">Scopes</label>
<div className="flex flex-wrap gap-1.5 mb-1.5">
{scopes.map((scope) => (
<span
key={scope}
className="inline-flex items-center gap-1 bg-indigo-500/15 text-indigo-400 text-[10px] px-2 py-0.5 rounded-full"
>
{scope}
<button onClick={() => removeScope(scope)}>
<X className="w-2.5 h-2.5" />
</button>
</span>
))}
</div>
<div className="flex gap-1.5">
<input
type="text"
value={scopeInput}
onChange={(e) => setScopeInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addScope()}
placeholder="Add scope..."
className="flex-1 bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none focus:border-indigo-500 transition-colors"
/>
<button
onClick={addScope}
className="px-2 py-1.5 bg-gray-700 text-gray-300 text-xs rounded hover:bg-gray-600 transition-colors"
>
Add
</button>
</div>
</div>
</div>
)}
{/* Bearer config */}
{currentType === 'bearer' && (
<div className="space-y-2 pt-1">
<div>
<label className="block text-[10px] text-gray-500 mb-1">Instructions</label>
<textarea
value={bearerInstructions}
onChange={(e) => setBearerInstructions(e.target.value)}
rows={3}
placeholder="Provide instructions for obtaining a bearer token..."
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none focus:border-indigo-500 transition-colors resize-none"
/>
</div>
<p className="text-[10px] text-gray-500">
Users will supply a Bearer token at runtime via environment variable.
</p>
</div>
)}
</div>
);
}