cleanup of api key generation

This commit is contained in:
2025-08-26 10:32:21 +02:00
parent 10cdf06ae1
commit 2b633cbc0d
5 changed files with 149 additions and 726 deletions

View File

@@ -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)}")

View File

@@ -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<ApiKey[]>([]);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState<string | null>(null);
@@ -94,23 +88,48 @@ export default function ApiKeysPage() {
const [newKeyVisible, setNewKeyVisible] = useState<string | null>(null);
const [visibleKeys, setVisibleKeys] = useState<Record<string, boolean>>({});
const [editKeyData, setEditKeyData] = useState<Partial<ApiKey>>({});
const [availableModels, setAvailableModels] = useState<any[]>([]);
const [availableChatbots, setAvailableChatbots] = useState<any[]>([]);
const [newKeyData, setNewKeyData] = useState<NewApiKeyData>({
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() {
</div>
</div>
{/* Model Restrictions - Hidden for chatbot API keys since model is already selected by chatbot */}
{newKeyData.allowed_chatbots.length === 0 && (
<div className="space-y-2">
<Label>Model Restrictions (Optional)</Label>
<p className="text-sm text-muted-foreground mb-2">
Leave empty to allow all models, or select specific models to restrict access.
</p>
<div className="grid grid-cols-1 gap-2 max-h-32 overflow-y-auto border rounded-md p-2">
{availableModels.map((model) => (
<div key={model.id} className="flex items-center space-x-2">
<input
type="checkbox"
id={`model-${model.id}`}
checked={newKeyData.allowed_models.includes(model.id)}
onChange={(e) => {
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"
/>
<Label htmlFor={`model-${model.id}`} className="text-sm">
{model.name}
</Label>
</div>
))}
{availableModels.length === 0 && (
<p className="text-sm text-muted-foreground">No models available</p>
)}
</div>
</div>
)}
{/* Chatbot Restrictions */}
<div className="space-y-2">
<Label>Chatbot Restrictions (Optional)</Label>
<p className="text-sm text-muted-foreground mb-2">
Leave empty to allow all chatbots, or select specific chatbots to restrict access.
</p>
<div className="grid grid-cols-1 gap-2 max-h-32 overflow-y-auto border rounded-md p-2">
{availableChatbots.map((chatbot) => (
<div key={chatbot.id} className="flex items-center space-x-2">
<input
type="checkbox"
id={`chatbot-${chatbot.id}`}
checked={newKeyData.allowed_chatbots.includes(chatbot.id)}
onChange={(e) => {
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"
/>
<Label htmlFor={`chatbot-${chatbot.id}`} className="text-sm">
{chatbot.name}
</Label>
</div>
))}
{availableChatbots.length === 0 && (
<p className="text-sm text-muted-foreground">No chatbots available</p>
)}
</div>
</div>
{/* Budget Configuration */}
<div className="space-y-4">
<div className="flex items-center space-x-2">
@@ -429,10 +534,10 @@ export default function ApiKeysPage() {
onChange={(e) => setNewKeyData(prev => ({ ...prev, is_unlimited: e.target.checked }))}
className="rounded"
/>
<Label htmlFor="unlimited-budget">Unlimited budget</Label>
<Label htmlFor="unlimited-budget">Set budget</Label>
</div>
{!newKeyData.is_unlimited && (
{newKeyData.is_unlimited && (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="budget-type">Budget Type</Label>
@@ -468,35 +573,6 @@ export default function ApiKeysPage() {
)}
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="minute-limit">Per Minute</Label>
<Input
id="minute-limit"
type="number"
value={newKeyData.rate_limit_per_minute}
onChange={(e) => setNewKeyData(prev => ({ ...prev, rate_limit_per_minute: parseInt(e.target.value) }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="hour-limit">Per Hour</Label>
<Input
id="hour-limit"
type="number"
value={newKeyData.rate_limit_per_hour}
onChange={(e) => setNewKeyData(prev => ({ ...prev, rate_limit_per_hour: parseInt(e.target.value) }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="day-limit">Per Day</Label>
<Input
id="day-limit"
type="number"
value={newKeyData.rate_limit_per_day}
onChange={(e) => setNewKeyData(prev => ({ ...prev, rate_limit_per_day: parseInt(e.target.value) }))}
/>
</div>
</div>
</div>
<DialogFooter>
@@ -641,20 +717,6 @@ export default function ApiKeysPage() {
</div>
</div>
<div className="space-y-2 mb-4">
<span className="text-sm font-medium">Rate Limits:</span>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Per Minute:</span> {apiKey.rate_limit_per_minute}
</div>
<div>
<span className="text-muted-foreground">Per Hour:</span> {apiKey.rate_limit_per_hour}
</div>
<div>
<span className="text-muted-foreground">Per Day:</span> {apiKey.rate_limit_per_day}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
@@ -760,10 +822,10 @@ export default function ApiKeysPage() {
onChange={(e) => setEditKeyData(prev => ({ ...prev, is_unlimited: e.target.checked }))}
className="rounded"
/>
<Label htmlFor="edit-unlimited-budget">Unlimited budget</Label>
<Label htmlFor="edit-unlimited-budget">Set budget</Label>
</div>
{!editKeyData.is_unlimited && (
{editKeyData.is_unlimited && (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="edit-budget-type">Budget Type</Label>
@@ -799,36 +861,6 @@ export default function ApiKeysPage() {
)}
</div>
{/* Rate Limits */}
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="edit-minute-limit">Per Minute</Label>
<Input
id="edit-minute-limit"
type="number"
value={editKeyData.rate_limit_per_minute || 0}
onChange={(e) => setEditKeyData(prev => ({ ...prev, rate_limit_per_minute: parseInt(e.target.value) }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-hour-limit">Per Hour</Label>
<Input
id="edit-hour-limit"
type="number"
value={editKeyData.rate_limit_per_hour || 0}
onChange={(e) => setEditKeyData(prev => ({ ...prev, rate_limit_per_hour: parseInt(e.target.value) }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-day-limit">Per Day</Label>
<Input
id="edit-day-limit"
type="number"
value={editKeyData.rate_limit_per_day || 0}
onChange={(e) => setEditKeyData(prev => ({ ...prev, rate_limit_per_day: parseInt(e.target.value) }))}
/>
</div>
</div>
{/* Expiration */}
<div className="space-y-2">

View File

@@ -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 }
)
}
}

View File

@@ -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<APIKey[]>([])
const [models, setModels] = useState<Model[]>([])
const [loading, setLoading] = useState(true)
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [showEditDialog, setShowEditDialog] = useState(false)
const [editingKey, setEditingKey] = useState<APIKey | null>(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() {
<Key className="h-5 w-5" />
API Keys
</CardTitle>
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogTrigger asChild>
<Button>
<Button onClick={() => router.push('/api-keys')}>
<Plus className="h-4 w-4 mr-2" />
Create API Key
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Create New API Key</DialogTitle>
<DialogDescription>
Create a new API key with optional model restrictions.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={newKey.name}
onChange={(e) => setNewKey(prev => ({ ...prev, name: e.target.value }))}
placeholder="e.g., Frontend Application"
/>
</div>
<div>
<Label htmlFor="description">Description (Optional)</Label>
<Textarea
id="description"
value={newKey.description}
onChange={(e) => setNewKey(prev => ({ ...prev, description: e.target.value }))}
placeholder="Brief description of what this key is for"
/>
</div>
<div>
<Label htmlFor="model">Model Restriction (Optional)</Label>
<Select value={newKey.model || "all"} onValueChange={(value) => setNewKey(prev => ({ ...prev, model: value === "all" ? "" : value }))}>
<SelectTrigger>
<SelectValue placeholder="Allow all models" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Models</SelectItem>
{models.map(model => (
<SelectItem key={model.id} value={model.id}>
{model.id}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="expires">Expiration Date (Optional)</Label>
<Input
id="expires"
type="date"
value={newKey.expires_at}
onChange={(e) => setNewKey(prev => ({ ...prev, expires_at: e.target.value }))}
/>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={() => setShowCreateDialog(false)}>
Cancel
</Button>
<Button onClick={createAPIKey} disabled={!newKey.name}>
Create API Key
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
<CardDescription>
OpenAI-compatible API keys for accessing your LLM endpoints.
@@ -643,65 +525,6 @@ function LLMPageContent() {
</DialogContent>
</Dialog>
{/* Secret Key Display Dialog */}
<Dialog open={showSecretKeyDialog} onOpenChange={() => {}}>
<DialogContent className="max-w-2xl" onPointerDownOutside={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-orange-500" />
Your API Key - Copy It Now!
</DialogTitle>
<DialogDescription className="text-orange-600 font-medium">
This is the only time you'll see your complete API key. Make sure to copy it and store it securely.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="h-4 w-4 text-orange-500" />
<span className="text-sm font-medium text-orange-800">Important Security Notice</span>
</div>
<p className="text-sm text-orange-700">
• This API key will never be shown again after you close this dialog
<br />
• Store it in a secure location (password manager, secure notes, etc.)
<br />
• Anyone with this key can access your API - keep it confidential
<br />
• If you lose it, you'll need to regenerate a new one
</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-700">Your API Key</Label>
<div className="mt-1 flex items-center gap-2">
<code className="flex-1 p-3 bg-gray-100 border rounded-md text-sm font-mono break-all">
{newSecretKey}
</code>
<Button
onClick={() => copyToClipboard(newSecretKey, "API key")}
variant="outline"
size="sm"
className="flex items-center gap-1"
>
<Copy className="h-4 w-4" />
Copy
</Button>
</div>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button
onClick={handleSecretKeyAcknowledged}
className="bg-orange-600 hover:bg-orange-700"
>
I've Copied My API Key
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -1,6 +1,7 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
@@ -123,6 +124,7 @@ interface PromptTemplate {
}
export function ChatbotManager() {
const router = useRouter()
const [chatbots, setChatbots] = useState<ChatbotInstance[]>([])
const [ragCollections, setRagCollections] = useState<RagCollection[]>([])
const [promptTemplates, setPromptTemplates] = useState<PromptTemplate[]>([])
@@ -135,11 +137,6 @@ export function ChatbotManager() {
const [editingChatbot, setEditingChatbot] = useState<ChatbotInstance | null>(null)
const [showChatInterface, setShowChatInterface] = useState(false)
const [testingChatbot, setTestingChatbot] = useState<ChatbotInstance | null>(null)
const [showApiKeyDialog, setShowApiKeyDialog] = useState(false)
const [apiKeyChatbot, setApiKeyChatbot] = useState<ChatbotInstance | null>(null)
const [apiKeys, setApiKeys] = useState<any[]>([])
const [loadingApiKeys, setLoadingApiKeys] = useState(false)
const [newApiKey, setNewApiKey] = useState<string | null>(null)
const { toast } = useToast()
// New chatbot form state
@@ -356,44 +353,9 @@ export function ChatbotManager() {
}
}
const handleManageApiKeys = async (chatbot: ChatbotInstance) => {
setApiKeyChatbot(chatbot)
setShowApiKeyDialog(true)
await loadApiKeys(chatbot.id)
}
const loadApiKeys = async (chatbotId: string) => {
setLoadingApiKeys(true)
try {
const data = await apiClient.get(`/api-internal/v1/chatbot/${chatbotId}/api-keys`)
setApiKeys(data.api_keys || [])
} catch (error) {
console.error('Failed to load API keys:', error)
setApiKeys([])
} finally {
setLoadingApiKeys(false)
}
}
const createApiKey = async () => {
if (!apiKeyChatbot) return
try {
const data = await apiClient.post(`/api-internal/v1/chatbot/${apiKeyChatbot.id}/api-key`, {})
setNewApiKey(data.secret_key)
await loadApiKeys(apiKeyChatbot.id)
toast({
title: "Success",
description: "API key created successfully"
})
} catch (error) {
console.error('Failed to create API key:', error)
toast({
title: "Error",
description: error instanceof Error ? error.message : "Failed to create API key",
variant: "destructive"
})
}
const handleManageApiKeys = (chatbot: ChatbotInstance) => {
// Navigate to unified API keys page with chatbot context
router.push(`/api-keys?chatbot=${chatbot.id}&chatbot_name=${encodeURIComponent(chatbot.name)}`)
}
const getChatbotTypeInfo = (type: string) => {
@@ -1082,200 +1044,6 @@ export function ChatbotManager() {
</Dialog>
)}
{/* API Key Management Dialog */}
<Dialog open={showApiKeyDialog} onOpenChange={(open) => {
setShowApiKeyDialog(open)
if (!open) {
setApiKeyChatbot(null)
setApiKeys([])
setNewApiKey(null)
}
}}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Key className="h-5 w-5" />
API Access - {apiKeyChatbot?.name}
</DialogTitle>
<DialogDescription>
Generate API keys to integrate this chatbot into external applications.
Each API key is restricted to this specific chatbot only.
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* API Endpoint Info */}
<div className="bg-muted/50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Globe className="h-4 w-4" />
<span className="font-medium">API Endpoint (Direct HTTP/curl)</span>
</div>
<div className="bg-background p-3 rounded border overflow-x-auto">
<code className="text-sm whitespace-nowrap">
POST {config.getPublicApiUrl()}/chatbot/external/{apiKeyChatbot?.id}/chat/completions
</code>
</div>
<p className="text-sm text-muted-foreground mt-2">
Send POST requests with your API key in the Authorization header: Bearer YOUR_API_KEY
</p>
<div className="mt-3 p-2 bg-blue-50 border border-blue-200 rounded">
<p className="text-xs text-blue-700">
🔗 Unified API endpoint at <strong>{config.getAppUrl()}</strong> via nginx reverse proxy
</p>
</div>
</div>
{/* Create API Key Section */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium">API Keys</h3>
<p className="text-sm text-muted-foreground">
Create and manage API keys for external access
</p>
</div>
<Button onClick={createApiKey} disabled={loadingApiKeys}>
<Plus className="h-4 w-4 mr-2" />
Create API Key
</Button>
</div>
{/* New API Key Display */}
{newApiKey && (
<div className="bg-green-50 border border-green-200 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Key className="h-4 w-4 text-green-600" />
<span className="font-medium text-green-800">New API Key Created</span>
</div>
<div className="flex items-center gap-2">
<Input
value={newApiKey}
readOnly
className="font-mono text-sm flex-1 min-w-0"
/>
<Button
size="sm"
onClick={() => {
navigator.clipboard.writeText(newApiKey)
toast({
title: "Copied",
description: "API key copied to clipboard"
})
}}
>
<Copy className="h-4 w-4" />
</Button>
</div>
<p className="text-sm text-green-700 mt-2">
Save this API key now - it won't be shown again!
</p>
<Button
variant="outline"
size="sm"
className="mt-2"
onClick={() => setNewApiKey(null)}
>
Dismiss
</Button>
</div>
)}
{/* API Keys List */}
{loadingApiKeys ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
<p className="mt-2 text-muted-foreground">Loading API keys...</p>
</div>
) : apiKeys.length > 0 ? (
<div className="space-y-3">
{apiKeys.map((apiKey) => (
<Card key={apiKey.id}>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">{apiKey.name}</span>
<Badge variant={apiKey.is_active ? "default" : "secondary"}>
{apiKey.is_active ? "Active" : "Inactive"}
</Badge>
</div>
<div className="text-sm text-muted-foreground mt-1 space-y-1">
<div className="font-mono text-xs bg-muted px-2 py-1 rounded w-fit">
{apiKey.key_prefix}
</div>
<div>
Created {new Date(apiKey.created_at).toLocaleDateString()} •
{apiKey.total_requests} requests •
Limit: {apiKey.rate_limit_per_minute}/min
</div>
</div>
{apiKey.last_used_at && (
<div className="text-xs text-muted-foreground">
Last used: {new Date(apiKey.last_used_at).toLocaleString()}
</div>
)}
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
disabled
>
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
) : (
<div className="text-center py-8">
<Key className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">No API Keys</h3>
<p className="text-muted-foreground mb-4">
Create your first API key to enable external access to this chatbot.
</p>
<Button onClick={createApiKey}>
<Plus className="h-4 w-4 mr-2" />
Create API Key
</Button>
</div>
)}
{/* Usage Example */}
<div className="bg-muted/50 p-4 rounded-lg">
<h4 className="font-medium mb-2">Usage Example</h4>
<div className="bg-background p-4 rounded border overflow-x-auto">
<pre className="text-sm whitespace-pre-wrap break-all">
{`curl -X POST "${config.getPublicApiUrl()}/chatbot/external/${apiKeyChatbot?.id}/chat/completions" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"messages": [
{"role": "user", "content": "Hello, how can you help me?"}
],
"max_tokens": 1000,
"temperature": 0.7
}'`}
</pre>
</div>
<div className="mt-3 p-3 bg-blue-50 border border-blue-200 rounded">
<p className="text-sm text-blue-800">
<strong>💡 OpenAI Library Usage:</strong> For OpenAI Python/JavaScript libraries, use base_url without /chat/completions:
</p>
<code className="block mt-1 text-xs bg-blue-100 p-2 rounded">
base_url="{config.getPublicApiUrl()}/chatbot/external/{apiKeyChatbot?.id}"
</code>
<p className="text-xs text-blue-700 mt-1">
The OpenAI client automatically appends /chat/completions to the base_url
</p>
</div>
</div>
</div>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>