From 2b633cbc0dac027ac4e831db6e9b1b45198517eb Mon Sep 17 00:00:00 2001 From: Aljaz Ceru Date: Tue, 26 Aug 2025 10:32:21 +0200 Subject: [PATCH] cleanup of api key generation --- backend/app/api/v1/chatbot.py | 127 --------- frontend/src/app/api-keys/page.tsx | 244 ++++++++++-------- .../src/app/api/chatbot/[id]/api-key/route.ts | 73 ------ frontend/src/app/llm/page.tsx | 189 +------------- .../src/components/chatbot/ChatbotManager.tsx | 242 +---------------- 5 files changed, 149 insertions(+), 726 deletions(-) delete mode 100644 frontend/src/app/api/chatbot/[id]/api-key/route.ts diff --git a/backend/app/api/v1/chatbot.py b/backend/app/api/v1/chatbot.py index afc66e9..1f2f579 100644 --- a/backend/app/api/v1/chatbot.py +++ b/backend/app/api/v1/chatbot.py @@ -957,130 +957,3 @@ async def external_chatbot_chat_completions( await db.rollback() log_api_request("external_chatbot_chat_completions_error", {"error": str(e), "chatbot_id": chatbot_id}) raise HTTPException(status_code=500, detail=f"Failed to process chat completions: {str(e)}") - - -@router.post("/{chatbot_id}/api-key") -async def create_chatbot_api_key( - chatbot_id: str, - current_user: User = Depends(get_current_user), - db: AsyncSession = Depends(get_db) -): - """Create an API key for a specific chatbot""" - user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id - log_api_request("create_chatbot_api_key", { - "user_id": user_id, - "chatbot_id": chatbot_id - }) - - try: - # Get existing chatbot and verify ownership - result = await db.execute( - select(ChatbotInstance) - .where(ChatbotInstance.id == chatbot_id) - .where(ChatbotInstance.created_by == str(user_id)) - ) - chatbot = result.scalar_one_or_none() - - if not chatbot: - raise HTTPException(status_code=404, detail="Chatbot not found or access denied") - - # Generate API key - from app.api.v1.api_keys import generate_api_key - full_key, key_hash = generate_api_key() - key_prefix = full_key[:8] - - # Create chatbot-specific API key - new_api_key = APIKey.create_chatbot_key( - user_id=user_id, - name=f"{chatbot.name} API Key", - key_hash=key_hash, - key_prefix=key_prefix, - chatbot_id=chatbot_id, - chatbot_name=chatbot.name - ) - - db.add(new_api_key) - await db.commit() - await db.refresh(new_api_key) - - return { - "api_key_id": new_api_key.id, - "name": new_api_key.name, - "key_prefix": new_api_key.key_prefix + "...", - "secret_key": full_key, # Only returned on creation - "chatbot_id": chatbot_id, - "chatbot_name": chatbot.name, - "endpoint": f"/api/v1/chatbot/external/{chatbot_id}/chat/completions", - "scopes": new_api_key.scopes, - "rate_limit_per_minute": new_api_key.rate_limit_per_minute, - "created_at": new_api_key.created_at.isoformat() - } - - except HTTPException: - raise - except Exception as e: - await db.rollback() - log_api_request("create_chatbot_api_key_error", {"error": str(e), "user_id": user_id}) - raise HTTPException(status_code=500, detail=f"Failed to create chatbot API key: {str(e)}") - - -@router.get("/{chatbot_id}/api-keys") -async def list_chatbot_api_keys( - chatbot_id: str, - current_user: User = Depends(get_current_user), - db: AsyncSession = Depends(get_db) -): - """List API keys for a specific chatbot""" - user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id - log_api_request("list_chatbot_api_keys", { - "user_id": user_id, - "chatbot_id": chatbot_id - }) - - try: - # Get existing chatbot and verify ownership - result = await db.execute( - select(ChatbotInstance) - .where(ChatbotInstance.id == chatbot_id) - .where(ChatbotInstance.created_by == str(user_id)) - ) - chatbot = result.scalar_one_or_none() - - if not chatbot: - raise HTTPException(status_code=404, detail="Chatbot not found or access denied") - - # Get API keys that can access this chatbot - api_keys_result = await db.execute( - select(APIKey) - .where(APIKey.user_id == user_id) - .where(APIKey.allowed_chatbots.contains([chatbot_id])) - .order_by(APIKey.created_at.desc()) - ) - api_keys = api_keys_result.scalars().all() - - api_key_list = [] - for api_key in api_keys: - api_key_list.append({ - "id": api_key.id, - "name": api_key.name, - "key_prefix": api_key.key_prefix + "...", - "is_active": api_key.is_active, - "created_at": api_key.created_at.isoformat(), - "last_used_at": api_key.last_used_at.isoformat() if api_key.last_used_at else None, - "total_requests": api_key.total_requests, - "rate_limit_per_minute": api_key.rate_limit_per_minute, - "scopes": api_key.scopes - }) - - return { - "chatbot_id": chatbot_id, - "chatbot_name": chatbot.name, - "api_keys": api_key_list, - "total": len(api_key_list) - } - - except HTTPException: - raise - except Exception as e: - log_api_request("list_chatbot_api_keys_error", {"error": str(e), "user_id": user_id}) - raise HTTPException(status_code=500, detail=f"Failed to list chatbot API keys: {str(e)}") \ No newline at end of file diff --git a/frontend/src/app/api-keys/page.tsx b/frontend/src/app/api-keys/page.tsx index e4ca1b8..0d824c3 100644 --- a/frontend/src/app/api-keys/page.tsx +++ b/frontend/src/app/api-keys/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import { useSearchParams } from "next/navigation"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -31,7 +32,8 @@ import { AlertTriangle, CheckCircle, Clock, - MoreHorizontal + MoreHorizontal, + Bot } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { apiClient } from "@/lib/api-client"; @@ -42,9 +44,6 @@ interface ApiKey { description: string; key_prefix: string; scopes: string[]; - rate_limit_per_minute: number; - rate_limit_per_hour: number; - rate_limit_per_day: number; is_active: boolean; expires_at: string | null; last_used_at: string | null; @@ -56,35 +55,30 @@ interface ApiKey { budget_limit?: number; budget_type?: "total" | "monthly"; is_unlimited: boolean; + allowed_models: string[]; + allowed_chatbots: string[]; } interface NewApiKeyData { name: string; description: string; scopes: string[]; - rate_limit_per_minute: number; - rate_limit_per_hour: number; - rate_limit_per_day: number; expires_at: string | null; is_unlimited: boolean; budget_limit_cents?: number; budget_type?: "total" | "monthly"; + allowed_models: string[]; + allowed_chatbots: string[]; } const PERMISSION_OPTIONS = [ { value: "llm:chat", label: "LLM Chat Completions" }, { value: "llm:embeddings", label: "LLM Embeddings" }, - { value: "modules:read", label: "Read Modules" }, - { value: "modules:write", label: "Manage Modules" }, - { value: "users:read", label: "Read Users" }, - { value: "users:write", label: "Manage Users" }, - { value: "audit:read", label: "Read Audit Logs" }, - { value: "settings:read", label: "Read Settings" }, - { value: "settings:write", label: "Manage Settings" }, ]; export default function ApiKeysPage() { const { toast } = useToast(); + const searchParams = useSearchParams(); const [apiKeys, setApiKeys] = useState([]); const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(null); @@ -94,23 +88,48 @@ export default function ApiKeysPage() { const [newKeyVisible, setNewKeyVisible] = useState(null); const [visibleKeys, setVisibleKeys] = useState>({}); const [editKeyData, setEditKeyData] = useState>({}); + const [availableModels, setAvailableModels] = useState([]); + const [availableChatbots, setAvailableChatbots] = useState([]); const [newKeyData, setNewKeyData] = useState({ name: "", description: "", scopes: [], - rate_limit_per_minute: 100, - rate_limit_per_hour: 1000, - rate_limit_per_day: 10000, expires_at: null, - is_unlimited: true, + is_unlimited: false, budget_limit_cents: 1000, // $10.00 default budget_type: "monthly", + allowed_models: [], + allowed_chatbots: [], }); useEffect(() => { fetchApiKeys(); - }, []); + fetchAvailableModels(); + fetchAvailableChatbots(); + + // Check URL parameters for chatbot pre-selection + const chatbotId = searchParams.get('chatbot'); + const chatbotName = searchParams.get('chatbot_name'); + + if (chatbotId && chatbotName) { + // Pre-populate the form with the chatbot selected and required permissions + setNewKeyData(prev => ({ + ...prev, + name: `${decodeURIComponent(chatbotName)} API Key`, + allowed_chatbots: [chatbotId], + scopes: ["llm:chat"] // Chatbots need chat completion permission + })); + + // Automatically open the create dialog + setShowCreateDialog(true); + + toast({ + title: "Chatbot Selected", + description: `Creating API key for ${decodeURIComponent(chatbotName)}` + }); + } + }, [searchParams, toast]); const fetchApiKeys = async () => { try { @@ -129,6 +148,26 @@ export default function ApiKeysPage() { } }; + const fetchAvailableModels = async () => { + try { + const result = await apiClient.get("/api-internal/v1/llm/models") as any; + setAvailableModels(result.data || []); + } catch (error) { + console.error("Failed to fetch models:", error); + setAvailableModels([]); + } + }; + + const fetchAvailableChatbots = async () => { + try { + const result = await apiClient.get("/api-internal/v1/chatbot/list") as any; + setAvailableChatbots(result || []); + } catch (error) { + console.error("Failed to fetch chatbots:", error); + setAvailableChatbots([]); + } + }; + const handleCreateApiKey = async () => { try { setActionLoading("create"); @@ -145,13 +184,12 @@ export default function ApiKeysPage() { name: "", description: "", scopes: [], - rate_limit_per_minute: 100, - rate_limit_per_hour: 1000, - rate_limit_per_day: 10000, expires_at: null, - is_unlimited: true, + is_unlimited: false, budget_limit_cents: 1000, // $10.00 default budget_type: "monthly", + allowed_models: [], + allowed_chatbots: [], }); await fetchApiKeys(); @@ -248,9 +286,6 @@ export default function ApiKeysPage() { await apiClient.put(`/api-internal/v1/api-keys/${keyId}`, { name: editKeyData.name, description: editKeyData.description, - rate_limit_per_minute: editKeyData.rate_limit_per_minute, - rate_limit_per_hour: editKeyData.rate_limit_per_hour, - rate_limit_per_day: editKeyData.rate_limit_per_day, is_unlimited: editKeyData.is_unlimited, budget_limit_cents: editKeyData.is_unlimited ? null : editKeyData.budget_limit, budget_type: editKeyData.is_unlimited ? null : editKeyData.budget_type, @@ -281,9 +316,6 @@ export default function ApiKeysPage() { setEditKeyData({ name: apiKey.name, description: apiKey.description, - rate_limit_per_minute: apiKey.rate_limit_per_minute, - rate_limit_per_hour: apiKey.rate_limit_per_hour, - rate_limit_per_day: apiKey.rate_limit_per_day, is_unlimited: apiKey.is_unlimited, budget_limit: apiKey.budget_limit, budget_type: apiKey.budget_type || "monthly", @@ -419,6 +451,79 @@ export default function ApiKeysPage() { + {/* Model Restrictions - Hidden for chatbot API keys since model is already selected by chatbot */} + {newKeyData.allowed_chatbots.length === 0 && ( +
+ +

+ Leave empty to allow all models, or select specific models to restrict access. +

+
+ {availableModels.map((model) => ( +
+ { + const checked = e.target.checked; + setNewKeyData(prev => ({ + ...prev, + allowed_models: checked + ? [...prev.allowed_models, model.id] + : prev.allowed_models.filter(m => m !== model.id) + })); + }} + className="rounded" + /> + +
+ ))} + {availableModels.length === 0 && ( +

No models available

+ )} +
+
+ )} + + + {/* Chatbot Restrictions */} +
+ +

+ Leave empty to allow all chatbots, or select specific chatbots to restrict access. +

+
+ {availableChatbots.map((chatbot) => ( +
+ { + const checked = e.target.checked; + setNewKeyData(prev => ({ + ...prev, + allowed_chatbots: checked + ? [...prev.allowed_chatbots, chatbot.id] + : prev.allowed_chatbots.filter(c => c !== chatbot.id) + })); + }} + className="rounded" + /> + +
+ ))} + {availableChatbots.length === 0 && ( +

No chatbots available

+ )} +
+
+ {/* Budget Configuration */}
@@ -429,10 +534,10 @@ export default function ApiKeysPage() { onChange={(e) => setNewKeyData(prev => ({ ...prev, is_unlimited: e.target.checked }))} className="rounded" /> - +
- {!newKeyData.is_unlimited && ( + {newKeyData.is_unlimited && (
@@ -468,35 +573,6 @@ export default function ApiKeysPage() { )}
-
-
- - setNewKeyData(prev => ({ ...prev, rate_limit_per_minute: parseInt(e.target.value) }))} - /> -
-
- - setNewKeyData(prev => ({ ...prev, rate_limit_per_hour: parseInt(e.target.value) }))} - /> -
-
- - setNewKeyData(prev => ({ ...prev, rate_limit_per_day: parseInt(e.target.value) }))} - /> -
-
@@ -641,20 +717,6 @@ export default function ApiKeysPage() {
-
- Rate Limits: -
-
- Per Minute: {apiKey.rate_limit_per_minute} -
-
- Per Hour: {apiKey.rate_limit_per_hour} -
-
- Per Day: {apiKey.rate_limit_per_day} -
-
-
- {!editKeyData.is_unlimited && ( + {editKeyData.is_unlimited && (
@@ -799,36 +861,6 @@ export default function ApiKeysPage() { )}
- {/* Rate Limits */} -
-
- - setEditKeyData(prev => ({ ...prev, rate_limit_per_minute: parseInt(e.target.value) }))} - /> -
-
- - setEditKeyData(prev => ({ ...prev, rate_limit_per_hour: parseInt(e.target.value) }))} - /> -
-
- - setEditKeyData(prev => ({ ...prev, rate_limit_per_day: parseInt(e.target.value) }))} - /> -
-
{/* Expiration */}
diff --git a/frontend/src/app/api/chatbot/[id]/api-key/route.ts b/frontend/src/app/api/chatbot/[id]/api-key/route.ts deleted file mode 100644 index 519f468..0000000 --- a/frontend/src/app/api/chatbot/[id]/api-key/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -const BACKEND_URL = process.env.INTERNAL_API_URL || 'http://enclava-backend:8000' - -export async function POST( - request: NextRequest, - { params }: { params: { id: string } } -) { - try { - const token = request.headers.get('authorization') - - if (!token) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const response = await fetch(`${BACKEND_URL}/api/chatbot/${params.id}/api-key`, { - method: 'POST', - headers: { - 'Authorization': token, - 'Content-Type': 'application/json', - }, - }) - - if (!response.ok) { - const error = await response.json() - return NextResponse.json(error, { status: response.status }) - } - - const data = await response.json() - return NextResponse.json(data) - } catch (error) { - console.error('Error creating chatbot API key:', error) - return NextResponse.json( - { error: 'Failed to create chatbot API key' }, - { status: 500 } - ) - } -} - -export async function GET( - request: NextRequest, - { params }: { params: { id: string } } -) { - try { - const token = request.headers.get('authorization') - - if (!token) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const response = await fetch(`${BACKEND_URL}/api/chatbot/${params.id}/api-keys`, { - method: 'GET', - headers: { - 'Authorization': token, - 'Content-Type': 'application/json', - }, - }) - - if (!response.ok) { - const error = await response.json() - return NextResponse.json(error, { status: response.status }) - } - - const data = await response.json() - return NextResponse.json(data) - } catch (error) { - console.error('Error listing chatbot API keys:', error) - return NextResponse.json( - { error: 'Failed to list chatbot API keys' }, - { status: 500 } - ) - } -} \ No newline at end of file diff --git a/frontend/src/app/llm/page.tsx b/frontend/src/app/llm/page.tsx index 3f8beba..9847740 100644 --- a/frontend/src/app/llm/page.tsx +++ b/frontend/src/app/llm/page.tsx @@ -28,6 +28,7 @@ import { import { useToast } from '@/hooks/use-toast' import { apiClient } from '@/lib/api-client' import { ProtectedRoute } from '@/components/auth/ProtectedRoute' +import { useRouter } from 'next/navigation' interface APIKey { id: number @@ -69,20 +70,11 @@ function LLMPageContent() { const [apiKeys, setApiKeys] = useState([]) const [models, setModels] = useState([]) const [loading, setLoading] = useState(true) - const [showCreateDialog, setShowCreateDialog] = useState(false) const [showEditDialog, setShowEditDialog] = useState(false) const [editingKey, setEditingKey] = useState(null) - const [showSecretKeyDialog, setShowSecretKeyDialog] = useState(false) - const [newSecretKey, setNewSecretKey] = useState('') const { toast } = useToast() + const router = useRouter() - // New API Key form state - const [newKey, setNewKey] = useState({ - name: '', - model: '', - expires_at: '', - description: '' - }) // Edit API Key form state const [editKey, setEditKey] = useState({ @@ -134,39 +126,6 @@ function LLMPageContent() { } } - const createAPIKey = async () => { - try { - // Clean the data before sending - remove empty optional fields - const cleanedKey = { ...newKey } - if (!cleanedKey.expires_at || cleanedKey.expires_at.trim() === '') { - delete cleanedKey.expires_at - } - if (!cleanedKey.description || cleanedKey.description.trim() === '') { - delete cleanedKey.description - } - if (!cleanedKey.model || cleanedKey.model === 'all') { - delete cleanedKey.model - } - - const result = await apiClient.post('/api-internal/v1/api-keys', cleanedKey) - setNewSecretKey(result.secret_key) - setShowCreateDialog(false) - setShowSecretKeyDialog(true) - setNewKey({ - name: '', - model: '', - expires_at: '', - description: '' - }) - fetchData() - } catch (error) { - toast({ - title: "Error", - description: "Failed to create API key", - variant: "destructive" - }) - } - } const openEditDialog = (apiKey: APIKey) => { setEditingKey(apiKey) @@ -237,14 +196,6 @@ function LLMPageContent() { }) } - const handleSecretKeyAcknowledged = () => { - setShowSecretKeyDialog(false) - setNewSecretKey('') - toast({ - title: "API Key Created", - description: "Your API key has been created successfully" - }) - } const formatCurrency = (cents: number) => { return `$${(cents / 100).toFixed(4)}` @@ -364,79 +315,10 @@ function LLMPageContent() { API Keys - - - - - - - Create New API Key - - Create a new API key with optional model restrictions. - - -
-
- - setNewKey(prev => ({ ...prev, name: e.target.value }))} - placeholder="e.g., Frontend Application" - /> -
- -
- -