/**
* FedMCP API Keys Management Component
*
* Allows users to create, view, and manage API keys for the FedMCP
* (Canadian Parliamentary Data API) service.
*/
'use client';
import { useState, useEffect, useCallback } from 'react';
import { Key, Plus, Trash2, Copy, CheckCircle, Loader2, ExternalLink, AlertCircle } from 'lucide-react';
interface FedMCPApiKey {
id: string;
name: string;
prefix: string;
tier: string;
rate_limit_per_hour: number;
total_requests: number;
created_at: string;
last_used_at: string | null;
}
export function FedMCPApiKeys() {
const [keys, setKeys] = useState<FedMCPApiKey[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [creating, setCreating] = useState(false);
const [newKeyName, setNewKeyName] = useState('');
const [showNewKey, setShowNewKey] = useState<string | null>(null);
const [copiedId, setCopiedId] = useState<string | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const fetchKeys = useCallback(async () => {
setLoading(true);
setError('');
try {
const response = await fetch('/api/fedmcp/keys');
if (!response.ok) {
throw new Error('Failed to fetch API keys');
}
const data = await response.json();
setKeys(data);
} catch (err) {
console.error('Failed to fetch FedMCP API keys:', err);
setError('Failed to load API keys. Please try again.');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchKeys();
}, [fetchKeys]);
const createKey = async () => {
if (!newKeyName.trim()) return;
setCreating(true);
setError('');
try {
const response = await fetch('/api/fedmcp/keys', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: newKeyName.trim(),
}),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to create API key');
}
const data = await response.json();
// Show the new key (only shown once!)
setShowNewKey(data.key);
// Reset form
setNewKeyName('');
// Refresh keys list
await fetchKeys();
} catch (err: any) {
console.error('Failed to create FedMCP API key:', err);
setError(err.message || 'Failed to create API key. Please try again.');
} finally {
setCreating(false);
}
};
const deleteKey = async (keyId: string) => {
setDeletingId(keyId);
setError('');
try {
const response = await fetch(`/api/fedmcp/keys/${keyId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete API key');
}
// Refresh keys list
await fetchKeys();
} catch (err) {
console.error('Failed to delete FedMCP API key:', err);
setError('Failed to delete API key. Please try again.');
} finally {
setDeletingId(null);
}
};
const copyToClipboard = async (text: string, id: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const formatDate = (dateString: string | null) => {
if (!dateString) return 'Never';
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
const getTierBadgeColor = (tier: string) => {
switch (tier.toUpperCase()) {
case 'ENTERPRISE':
return 'bg-purple-100 text-purple-800';
case 'PRO':
return 'bg-blue-100 text-blue-800';
case 'BASIC':
return 'bg-green-100 text-green-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
return (
<div className="space-y-6">
{/* Header */}
<div>
<h3 className="text-lg font-medium text-gray-900 flex items-center gap-2">
<Key className="w-5 h-5" />
FedMCP API Keys
</h3>
<p className="mt-1 text-sm text-gray-600">
Access Canadian parliamentary data programmatically via the FedMCP API.
</p>
<a
href="https://fedmcp.canadagpt.ca/docs"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 mt-2 text-sm text-blue-600 hover:underline"
>
View API Documentation
<ExternalLink className="w-3 h-3" />
</a>
</div>
{/* Error Display */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-md flex items-start gap-2">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-800">{error}</p>
</div>
)}
{/* New Key Created Alert */}
{showNewKey && (
<div className="p-4 bg-green-50 border border-green-200 rounded-md">
<div className="flex items-start gap-2">
<CheckCircle className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-green-800">
API Key Created Successfully!
</p>
<p className="mt-1 text-sm text-green-700">
Copy this key now - you won't be able to see it again.
</p>
<div className="mt-2 flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-white border border-green-300 rounded text-sm font-mono break-all">
{showNewKey}
</code>
<button
onClick={() => copyToClipboard(showNewKey, 'new')}
className="px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700 flex items-center gap-1"
>
{copiedId === 'new' ? (
<>
<CheckCircle className="w-4 h-4" />
Copied!
</>
) : (
<>
<Copy className="w-4 h-4" />
Copy
</>
)}
</button>
</div>
<button
onClick={() => setShowNewKey(null)}
className="mt-3 text-sm text-green-700 hover:text-green-900"
>
Dismiss
</button>
</div>
</div>
</div>
)}
{/* Create New Key */}
<div className="p-4 bg-gray-50 border border-gray-200 rounded-md">
<h4 className="text-sm font-medium text-gray-900 mb-3">Create New API Key</h4>
<div className="flex gap-2">
<input
type="text"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="Key name (e.g., Production, Development)"
maxLength={100}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-gray-900 bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<button
onClick={createKey}
disabled={creating || !newKeyName.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center gap-2"
>
{creating ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Creating...
</>
) : (
<>
<Plus className="w-4 h-4" />
Create Key
</>
)}
</button>
</div>
<p className="mt-2 text-xs text-gray-500">
You can create up to 5 API keys per account.
</p>
</div>
{/* Keys List */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3">Your API Keys</h4>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
</div>
) : keys.length === 0 ? (
<div className="text-center py-8 bg-gray-50 rounded-md">
<Key className="w-12 h-12 mx-auto text-gray-300 mb-3" />
<p className="text-gray-500">No API keys yet</p>
<p className="text-sm text-gray-400">Create your first key to get started</p>
</div>
) : (
<div className="space-y-3">
{keys.map((key) => (
<div
key={key.id}
className="p-4 bg-white border border-gray-200 rounded-md hover:border-gray-300 transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900">{key.name}</span>
<span className={`px-2 py-0.5 text-xs font-medium rounded ${getTierBadgeColor(key.tier)}`}>
{key.tier}
</span>
</div>
<div className="mt-1 flex items-center gap-2">
<code className="text-sm text-gray-500 font-mono">{key.prefix}...</code>
<button
onClick={() => copyToClipboard(key.prefix, key.id)}
className="p-1 text-gray-400 hover:text-gray-600"
title="Copy key prefix"
>
{copiedId === key.id ? (
<CheckCircle className="w-4 h-4 text-green-600" />
) : (
<Copy className="w-4 h-4" />
)}
</button>
</div>
<div className="mt-2 flex items-center gap-4 text-xs text-gray-500">
<span>Created: {formatDate(key.created_at)}</span>
<span>Last used: {formatDate(key.last_used_at)}</span>
<span>Requests: {key.total_requests.toLocaleString()}</span>
<span>Limit: {key.rate_limit_per_hour.toLocaleString()}/hr</span>
</div>
</div>
<button
onClick={() => {
if (confirm('Are you sure you want to delete this API key? This action cannot be undone.')) {
deleteKey(key.id);
}
}}
disabled={deletingId === key.id}
className="p-2 text-gray-400 hover:text-red-600 disabled:opacity-50"
title="Delete key"
>
{deletingId === key.id ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Trash2 className="w-5 h-5" />
)}
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Rate Limit Info */}
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md">
<h4 className="text-sm font-medium text-blue-900 mb-2">Rate Limits by Tier</h4>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
<div>
<span className="font-medium text-blue-900">Free</span>
<p className="text-blue-700">100 requests/hour</p>
</div>
<div>
<span className="font-medium text-blue-900">Basic</span>
<p className="text-blue-700">1,000 requests/hour</p>
</div>
<div>
<span className="font-medium text-blue-900">Pro</span>
<p className="text-blue-700">10,000 requests/hour</p>
</div>
<div>
<span className="font-medium text-blue-900">Enterprise</span>
<p className="text-blue-700">100,000 requests/hour</p>
</div>
</div>
<p className="mt-3 text-xs text-blue-700">
Your API key tier is based on your subscription. Upgrade to increase your rate limits.
</p>
</div>
</div>
);
}