mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 07:24:34 +01:00
cleanup of api key generation
This commit is contained in:
@@ -957,130 +957,3 @@ async def external_chatbot_chat_completions(
|
|||||||
await db.rollback()
|
await db.rollback()
|
||||||
log_api_request("external_chatbot_chat_completions_error", {"error": str(e), "chatbot_id": chatbot_id})
|
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)}")
|
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)}")
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -31,7 +32,8 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Clock,
|
Clock,
|
||||||
MoreHorizontal
|
MoreHorizontal,
|
||||||
|
Bot
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiClient } from "@/lib/api-client";
|
import { apiClient } from "@/lib/api-client";
|
||||||
@@ -42,9 +44,6 @@ interface ApiKey {
|
|||||||
description: string;
|
description: string;
|
||||||
key_prefix: string;
|
key_prefix: string;
|
||||||
scopes: string[];
|
scopes: string[];
|
||||||
rate_limit_per_minute: number;
|
|
||||||
rate_limit_per_hour: number;
|
|
||||||
rate_limit_per_day: number;
|
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
expires_at: string | null;
|
expires_at: string | null;
|
||||||
last_used_at: string | null;
|
last_used_at: string | null;
|
||||||
@@ -56,35 +55,30 @@ interface ApiKey {
|
|||||||
budget_limit?: number;
|
budget_limit?: number;
|
||||||
budget_type?: "total" | "monthly";
|
budget_type?: "total" | "monthly";
|
||||||
is_unlimited: boolean;
|
is_unlimited: boolean;
|
||||||
|
allowed_models: string[];
|
||||||
|
allowed_chatbots: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NewApiKeyData {
|
interface NewApiKeyData {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
scopes: string[];
|
scopes: string[];
|
||||||
rate_limit_per_minute: number;
|
|
||||||
rate_limit_per_hour: number;
|
|
||||||
rate_limit_per_day: number;
|
|
||||||
expires_at: string | null;
|
expires_at: string | null;
|
||||||
is_unlimited: boolean;
|
is_unlimited: boolean;
|
||||||
budget_limit_cents?: number;
|
budget_limit_cents?: number;
|
||||||
budget_type?: "total" | "monthly";
|
budget_type?: "total" | "monthly";
|
||||||
|
allowed_models: string[];
|
||||||
|
allowed_chatbots: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const PERMISSION_OPTIONS = [
|
const PERMISSION_OPTIONS = [
|
||||||
{ value: "llm:chat", label: "LLM Chat Completions" },
|
{ value: "llm:chat", label: "LLM Chat Completions" },
|
||||||
{ value: "llm:embeddings", label: "LLM Embeddings" },
|
{ 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() {
|
export default function ApiKeysPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||||
@@ -94,23 +88,48 @@ export default function ApiKeysPage() {
|
|||||||
const [newKeyVisible, setNewKeyVisible] = useState<string | null>(null);
|
const [newKeyVisible, setNewKeyVisible] = useState<string | null>(null);
|
||||||
const [visibleKeys, setVisibleKeys] = useState<Record<string, boolean>>({});
|
const [visibleKeys, setVisibleKeys] = useState<Record<string, boolean>>({});
|
||||||
const [editKeyData, setEditKeyData] = useState<Partial<ApiKey>>({});
|
const [editKeyData, setEditKeyData] = useState<Partial<ApiKey>>({});
|
||||||
|
const [availableModels, setAvailableModels] = useState<any[]>([]);
|
||||||
|
const [availableChatbots, setAvailableChatbots] = useState<any[]>([]);
|
||||||
|
|
||||||
const [newKeyData, setNewKeyData] = useState<NewApiKeyData>({
|
const [newKeyData, setNewKeyData] = useState<NewApiKeyData>({
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
scopes: [],
|
scopes: [],
|
||||||
rate_limit_per_minute: 100,
|
|
||||||
rate_limit_per_hour: 1000,
|
|
||||||
rate_limit_per_day: 10000,
|
|
||||||
expires_at: null,
|
expires_at: null,
|
||||||
is_unlimited: true,
|
is_unlimited: false,
|
||||||
budget_limit_cents: 1000, // $10.00 default
|
budget_limit_cents: 1000, // $10.00 default
|
||||||
budget_type: "monthly",
|
budget_type: "monthly",
|
||||||
|
allowed_models: [],
|
||||||
|
allowed_chatbots: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchApiKeys();
|
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 () => {
|
const fetchApiKeys = async () => {
|
||||||
try {
|
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 () => {
|
const handleCreateApiKey = async () => {
|
||||||
try {
|
try {
|
||||||
setActionLoading("create");
|
setActionLoading("create");
|
||||||
@@ -145,13 +184,12 @@ export default function ApiKeysPage() {
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
scopes: [],
|
scopes: [],
|
||||||
rate_limit_per_minute: 100,
|
|
||||||
rate_limit_per_hour: 1000,
|
|
||||||
rate_limit_per_day: 10000,
|
|
||||||
expires_at: null,
|
expires_at: null,
|
||||||
is_unlimited: true,
|
is_unlimited: false,
|
||||||
budget_limit_cents: 1000, // $10.00 default
|
budget_limit_cents: 1000, // $10.00 default
|
||||||
budget_type: "monthly",
|
budget_type: "monthly",
|
||||||
|
allowed_models: [],
|
||||||
|
allowed_chatbots: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
await fetchApiKeys();
|
await fetchApiKeys();
|
||||||
@@ -248,9 +286,6 @@ export default function ApiKeysPage() {
|
|||||||
await apiClient.put(`/api-internal/v1/api-keys/${keyId}`, {
|
await apiClient.put(`/api-internal/v1/api-keys/${keyId}`, {
|
||||||
name: editKeyData.name,
|
name: editKeyData.name,
|
||||||
description: editKeyData.description,
|
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,
|
is_unlimited: editKeyData.is_unlimited,
|
||||||
budget_limit_cents: editKeyData.is_unlimited ? null : editKeyData.budget_limit,
|
budget_limit_cents: editKeyData.is_unlimited ? null : editKeyData.budget_limit,
|
||||||
budget_type: editKeyData.is_unlimited ? null : editKeyData.budget_type,
|
budget_type: editKeyData.is_unlimited ? null : editKeyData.budget_type,
|
||||||
@@ -281,9 +316,6 @@ export default function ApiKeysPage() {
|
|||||||
setEditKeyData({
|
setEditKeyData({
|
||||||
name: apiKey.name,
|
name: apiKey.name,
|
||||||
description: apiKey.description,
|
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,
|
is_unlimited: apiKey.is_unlimited,
|
||||||
budget_limit: apiKey.budget_limit,
|
budget_limit: apiKey.budget_limit,
|
||||||
budget_type: apiKey.budget_type || "monthly",
|
budget_type: apiKey.budget_type || "monthly",
|
||||||
@@ -419,6 +451,79 @@ export default function ApiKeysPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Budget Configuration */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-2">
|
<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 }))}
|
onChange={(e) => setNewKeyData(prev => ({ ...prev, is_unlimited: e.target.checked }))}
|
||||||
className="rounded"
|
className="rounded"
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="unlimited-budget">Unlimited budget</Label>
|
<Label htmlFor="unlimited-budget">Set budget</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!newKeyData.is_unlimited && (
|
{newKeyData.is_unlimited && (
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="budget-type">Budget Type</Label>
|
<Label htmlFor="budget-type">Budget Type</Label>
|
||||||
@@ -468,35 +573,6 @@ export default function ApiKeysPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -641,20 +717,6 @@ export default function ApiKeysPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -760,10 +822,10 @@ export default function ApiKeysPage() {
|
|||||||
onChange={(e) => setEditKeyData(prev => ({ ...prev, is_unlimited: e.target.checked }))}
|
onChange={(e) => setEditKeyData(prev => ({ ...prev, is_unlimited: e.target.checked }))}
|
||||||
className="rounded"
|
className="rounded"
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="edit-unlimited-budget">Unlimited budget</Label>
|
<Label htmlFor="edit-unlimited-budget">Set budget</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!editKeyData.is_unlimited && (
|
{editKeyData.is_unlimited && (
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-budget-type">Budget Type</Label>
|
<Label htmlFor="edit-budget-type">Budget Type</Label>
|
||||||
@@ -799,36 +861,6 @@ export default function ApiKeysPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Expiration */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
import { useToast } from '@/hooks/use-toast'
|
import { useToast } from '@/hooks/use-toast'
|
||||||
import { apiClient } from '@/lib/api-client'
|
import { apiClient } from '@/lib/api-client'
|
||||||
import { ProtectedRoute } from '@/components/auth/ProtectedRoute'
|
import { ProtectedRoute } from '@/components/auth/ProtectedRoute'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
interface APIKey {
|
interface APIKey {
|
||||||
id: number
|
id: number
|
||||||
@@ -69,20 +70,11 @@ function LLMPageContent() {
|
|||||||
const [apiKeys, setApiKeys] = useState<APIKey[]>([])
|
const [apiKeys, setApiKeys] = useState<APIKey[]>([])
|
||||||
const [models, setModels] = useState<Model[]>([])
|
const [models, setModels] = useState<Model[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
|
||||||
const [showEditDialog, setShowEditDialog] = useState(false)
|
const [showEditDialog, setShowEditDialog] = useState(false)
|
||||||
const [editingKey, setEditingKey] = useState<APIKey | null>(null)
|
const [editingKey, setEditingKey] = useState<APIKey | null>(null)
|
||||||
const [showSecretKeyDialog, setShowSecretKeyDialog] = useState(false)
|
|
||||||
const [newSecretKey, setNewSecretKey] = useState('')
|
|
||||||
const { toast } = useToast()
|
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
|
// Edit API Key form state
|
||||||
const [editKey, setEditKey] = useState({
|
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) => {
|
const openEditDialog = (apiKey: APIKey) => {
|
||||||
setEditingKey(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) => {
|
const formatCurrency = (cents: number) => {
|
||||||
return `$${(cents / 100).toFixed(4)}`
|
return `$${(cents / 100).toFixed(4)}`
|
||||||
@@ -364,79 +315,10 @@ function LLMPageContent() {
|
|||||||
<Key className="h-5 w-5" />
|
<Key className="h-5 w-5" />
|
||||||
API Keys
|
API Keys
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
<Button onClick={() => router.push('/api-keys')}>
|
||||||
<DialogTrigger asChild>
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
<Button>
|
Create API Key
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
</Button>
|
||||||
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>
|
</div>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
OpenAI-compatible API keys for accessing your LLM endpoints.
|
OpenAI-compatible API keys for accessing your LLM endpoints.
|
||||||
@@ -643,65 +525,6 @@ function LLMPageContent() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
@@ -123,6 +124,7 @@ interface PromptTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ChatbotManager() {
|
export function ChatbotManager() {
|
||||||
|
const router = useRouter()
|
||||||
const [chatbots, setChatbots] = useState<ChatbotInstance[]>([])
|
const [chatbots, setChatbots] = useState<ChatbotInstance[]>([])
|
||||||
const [ragCollections, setRagCollections] = useState<RagCollection[]>([])
|
const [ragCollections, setRagCollections] = useState<RagCollection[]>([])
|
||||||
const [promptTemplates, setPromptTemplates] = useState<PromptTemplate[]>([])
|
const [promptTemplates, setPromptTemplates] = useState<PromptTemplate[]>([])
|
||||||
@@ -135,11 +137,6 @@ export function ChatbotManager() {
|
|||||||
const [editingChatbot, setEditingChatbot] = useState<ChatbotInstance | null>(null)
|
const [editingChatbot, setEditingChatbot] = useState<ChatbotInstance | null>(null)
|
||||||
const [showChatInterface, setShowChatInterface] = useState(false)
|
const [showChatInterface, setShowChatInterface] = useState(false)
|
||||||
const [testingChatbot, setTestingChatbot] = useState<ChatbotInstance | null>(null)
|
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()
|
const { toast } = useToast()
|
||||||
|
|
||||||
// New chatbot form state
|
// New chatbot form state
|
||||||
@@ -356,44 +353,9 @@ export function ChatbotManager() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleManageApiKeys = async (chatbot: ChatbotInstance) => {
|
const handleManageApiKeys = (chatbot: ChatbotInstance) => {
|
||||||
setApiKeyChatbot(chatbot)
|
// Navigate to unified API keys page with chatbot context
|
||||||
setShowApiKeyDialog(true)
|
router.push(`/api-keys?chatbot=${chatbot.id}&chatbot_name=${encodeURIComponent(chatbot.name)}`)
|
||||||
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 getChatbotTypeInfo = (type: string) => {
|
const getChatbotTypeInfo = (type: string) => {
|
||||||
@@ -1082,200 +1044,6 @@ export function ChatbotManager() {
|
|||||||
</Dialog>
|
</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 */}
|
{/* Delete Confirmation Dialog */}
|
||||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
|||||||
Reference in New Issue
Block a user