- Welcome back, {user.name}
+ Welcome back, {user?.name || 'User'}
Manage your Enclava platform and modules
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index c3d993c..ecc7af7 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -17,7 +17,7 @@ export const viewport: Viewport = {
}
export const metadata: Metadata = {
- metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'),
+ metadataBase: new URL(`http://${process.env.NEXT_PUBLIC_BASE_URL || 'localhost'}`),
title: 'Enclava Platform',
description: 'Secure AI processing platform with plugin-based architecture and confidential computing',
keywords: ['AI', 'Enclava', 'Confidential Computing', 'LLM', 'TEE'],
@@ -26,7 +26,7 @@ export const metadata: Metadata = {
openGraph: {
type: 'website',
locale: 'en_US',
- url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
+ url: `http://${process.env.NEXT_PUBLIC_BASE_URL || 'localhost'}`,
title: 'Enclava Platform',
description: 'Secure AI processing platform with plugin-based architecture and confidential computing',
siteName: 'Enclava',
diff --git a/frontend/src/app/llm/page.tsx b/frontend/src/app/llm/page.tsx
index 823f35f..cda9764 100644
--- a/frontend/src/app/llm/page.tsx
+++ b/frontend/src/app/llm/page.tsx
@@ -8,7 +8,6 @@ import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
-import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import { Separator } from '@/components/ui/separator'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
@@ -20,7 +19,6 @@ import {
Settings,
Trash2,
Copy,
- DollarSign,
Calendar,
Lock,
Unlock,
@@ -49,20 +47,9 @@ interface APIKey {
rate_limit_per_day?: number
allowed_ips: string[]
allowed_models: string[]
- budget_limit_cents?: number
- budget_type?: string
- is_unlimited: boolean
tags: string[]
}
-interface Budget {
- id: string
- name: string
- limit_cents: number
- used_cents: number
- is_active: boolean
-}
-
interface Model {
id: string
name: string
@@ -80,7 +67,6 @@ export default function LLMPage() {
function LLMPageContent() {
const [activeTab, setActiveTab] = useState('api-keys')
const [apiKeys, setApiKeys] = useState([])
- const [budgets, setBudgets] = useState([])
const [models, setModels] = useState([])
const [loading, setLoading] = useState(true)
const [showCreateDialog, setShowCreateDialog] = useState(false)
@@ -92,9 +78,6 @@ function LLMPageContent() {
const [newKey, setNewKey] = useState({
name: '',
model: '',
- is_unlimited: true,
- budget_limit_cents: 1000, // $10.00 default
- budget_type: 'monthly',
expires_at: '',
description: ''
})
@@ -112,16 +95,12 @@ function LLMPageContent() {
throw new Error('No authentication token found')
}
- // Fetch API keys, budgets, and models using API client
- const [keysData, budgetsData, modelsData] = await Promise.all([
+ // Fetch API keys and models using API client
+ const [keysData, modelsData] = await Promise.all([
apiClient.get('/api-internal/v1/api-keys').catch(e => {
console.error('Failed to fetch API keys:', e)
return { data: [] }
}),
- apiClient.get('/api-internal/v1/llm/budget/status').catch(e => {
- console.error('Failed to fetch budgets:', e)
- return { data: [] }
- }),
apiClient.get('/api-internal/v1/llm/models').catch(e => {
console.error('Failed to fetch models:', e)
return { data: [] }
@@ -129,9 +108,8 @@ function LLMPageContent() {
])
console.log('API keys data:', keysData)
- setApiKeys(keysData.data || [])
- console.log('API keys state updated, count:', keysData.data?.length || 0)
- setBudgets(budgetsData.data || [])
+ setApiKeys(keysData.api_keys || [])
+ console.log('API keys state updated, count:', keysData.api_keys?.length || 0)
setModels(modelsData.data || [])
console.log('Data fetch completed successfully')
@@ -149,16 +127,25 @@ function LLMPageContent() {
const createAPIKey = async () => {
try {
- const result = await apiClient.post('/api-internal/v1/api-keys', newKey)
+ // 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: '',
- is_unlimited: true,
- budget_limit_cents: 1000, // $10.00 default
- budget_type: 'monthly',
expires_at: '',
description: ''
})
@@ -226,9 +213,6 @@ function LLMPageContent() {
return new Date(dateStr).toLocaleDateString()
}
- const getBudgetUsagePercentage = (budget: Budget) => {
- return budget.limit_cents > 0 ? (budget.used_cents / budget.limit_cents) * 100 : 0
- }
// Get the public API URL from the current window location
const getPublicApiUrl = () => {
@@ -249,7 +233,7 @@ function LLMPageContent() {
LLM Configuration
- Manage API keys, budgets, and model access for your LLM integrations.
+ Manage API keys and model access for your LLM integrations.
@@ -325,9 +309,8 @@ function LLMPageContent() {
-
+
API Keys
- Budgets
Models
@@ -350,7 +333,7 @@ function LLMPageContent() {
Create New API Key
- Create a new API key with optional model and budget restrictions.
+ Create a new API key with optional model restrictions.
@@ -384,54 +367,13 @@ function LLMPageContent() {
All Models
{models.map(model => (
- {model.name} ({model.provider})
+ {model.id}
))}
-
- setNewKey(prev => ({ ...prev, is_unlimited: checked }))}
- />
-
-
-
- {!newKey.is_unlimited && (
-
-
-
-
-
-
-
- setNewKey(prev => ({
- ...prev,
- budget_limit_cents: Math.round(parseFloat(e.target.value || "0") * 100)
- }))}
- placeholder="0.00"
- />
-
-
- )}
-
Name
Key
Model
-
Budget
Expires
Usage
Status
@@ -499,15 +440,6 @@ function LLMPageContent() {
All Models
)}
-
- {apiKey.is_unlimited ? (
- Unlimited
- ) : (
-
- {formatCurrency(apiKey.budget_limit_cents || 0)}
-
- )}
-
{formatDate(apiKey.expires_at)}
@@ -574,54 +506,6 @@ function LLMPageContent() {
-
-
-
-
-
- Budget Management
-
-
- Monitor and manage spending limits for your API keys.
-
-
-
-
- {Array.isArray(budgets) && budgets.map((budget) => (
-
-
-
{budget.name}
-
- {budget.is_active ? "Active" : "Inactive"}
-
-
-
-
- Used: {formatCurrency(budget.used_cents)}
- Limit: {formatCurrency(budget.limit_cents)}
-
-
-
- {getBudgetUsagePercentage(budget).toFixed(1)}% used
-
-
-
- ))}
- {(!Array.isArray(budgets) || budgets.length === 0) && (
-
- No budgets configured. Configure budgets in the Analytics section.
-
- )}
-
-
-
-
-
@@ -634,10 +518,10 @@ function LLMPageContent() {
{models.map((model) => (
-
{model.name}
-
{model.provider}
+
{model.id}
+
Provider: {model.owned_by}
- {model.id}
+ {model.object}
))}
diff --git a/frontend/src/components/auth/ProtectedRoute.tsx b/frontend/src/components/auth/ProtectedRoute.tsx
index 4e3998f..b30b51b 100644
--- a/frontend/src/components/auth/ProtectedRoute.tsx
+++ b/frontend/src/components/auth/ProtectedRoute.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useEffect } from "react"
+import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { useAuth } from "@/contexts/AuthContext"
@@ -11,15 +11,21 @@ interface ProtectedRouteProps {
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { user, isLoading } = useAuth()
const router = useRouter()
+ const [isClient, setIsClient] = useState(false)
useEffect(() => {
- if (!isLoading && !user) {
+ setIsClient(true)
+ }, [])
+
+ useEffect(() => {
+ if (isClient && !isLoading && !user) {
router.push("/login")
}
- }, [user, isLoading, router])
+ }, [user, isLoading, router, isClient])
- // Show loading spinner while checking authentication
- if (isLoading) {
+ // During SSR and initial client render, always show loading
+ // This ensures consistent rendering between server and client
+ if (!isClient || isLoading) {
return (
@@ -27,9 +33,14 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) {
)
}
- // If user is not authenticated, don't render anything (redirect is handled by useEffect)
+ // If user is not authenticated after client hydration, don't render anything
+ // (redirect is handled by useEffect)
if (!user) {
- return null
+ return (
+
+ )
}
// User is authenticated, render the protected content
diff --git a/frontend/src/components/plugins/PluginManager.tsx b/frontend/src/components/plugins/PluginManager.tsx
index a7cc267..4399045 100644
--- a/frontend/src/components/plugins/PluginManager.tsx
+++ b/frontend/src/components/plugins/PluginManager.tsx
@@ -237,6 +237,12 @@ export const PluginManager: React.FC = () => {
const [searchQuery, setSearchQuery] = useState
('');
const [selectedCategory, setSelectedCategory] = useState('');
const [configuringPlugin, setConfiguringPlugin] = useState(null);
+ const [isClient, setIsClient] = useState(false);
+
+ // Fix hydration mismatch with client-side detection
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
// Load initial data only when authenticated
useEffect(() => {
@@ -301,8 +307,8 @@ export const PluginManager: React.FC = () => {
const categories = Array.from(new Set(availablePlugins.map(p => p.category)));
- // Show authentication required message if not authenticated
- if (!user || !token) {
+ // Show authentication required message if not authenticated (client-side only)
+ if (isClient && (!user || !token)) {
return (
@@ -315,6 +321,18 @@ export const PluginManager: React.FC = () => {
);
}
+ // Show loading state during hydration
+ if (!isClient) {
+ return (
+
+ );
+ }
+
return (
{error && (
diff --git a/frontend/src/components/plugins/PluginPageRenderer.tsx b/frontend/src/components/plugins/PluginPageRenderer.tsx
index afb7d00..1c133f5 100644
--- a/frontend/src/components/plugins/PluginPageRenderer.tsx
+++ b/frontend/src/components/plugins/PluginPageRenderer.tsx
@@ -49,8 +49,7 @@ const PluginIframe: React.FC
= ({
const allowedOrigins = [
window.location.origin,
config.getBackendUrl(),
- config.getApiUrl(),
- process.env.NEXT_PUBLIC_API_URL
+ config.getApiUrl()
].filter(Boolean);
if (!allowedOrigins.some(origin => event.origin.startsWith(origin))) {
diff --git a/frontend/src/components/rag/document-browser.tsx b/frontend/src/components/rag/document-browser.tsx
index b5a83e4..0b04d17 100644
--- a/frontend/src/components/rag/document-browser.tsx
+++ b/frontend/src/components/rag/document-browser.tsx
@@ -60,11 +60,12 @@ export function DocumentBrowser({ collections, selectedCollection, onCollectionS
useEffect(() => {
loadDocuments()
- }, [])
+ }, [filterCollection])
useEffect(() => {
+ // Apply client-side filters for search, type, and status
filterDocuments()
- }, [documents, searchTerm, filterCollection, filterType, filterStatus])
+ }, [documents, searchTerm, filterType, filterStatus])
useEffect(() => {
if (selectedCollection !== filterCollection) {
@@ -75,7 +76,16 @@ export function DocumentBrowser({ collections, selectedCollection, onCollectionS
const loadDocuments = async () => {
setLoading(true)
try {
- const data = await apiClient.get('/api-internal/v1/rag/documents')
+ // Build query parameters based on current filter
+ const params = new URLSearchParams()
+ if (filterCollection && filterCollection !== "all") {
+ params.append('collection_id', filterCollection)
+ }
+
+ const queryString = params.toString()
+ const url = queryString ? `/api-internal/v1/rag/documents?${queryString}` : '/api-internal/v1/rag/documents'
+
+ const data = await apiClient.get(url)
setDocuments(data.documents || [])
} catch (error) {
console.error('Failed to load documents:', error)
@@ -97,11 +107,7 @@ export function DocumentBrowser({ collections, selectedCollection, onCollectionS
)
}
- // Collection filter
- if (filterCollection !== "all") {
- filtered = filtered.filter(doc => doc.collection_id === filterCollection)
- }
-
+ // Collection filter is now handled server-side
// Type filter
if (filterType !== "all") {
filtered = filtered.filter(doc => doc.file_type === filterType)
diff --git a/frontend/src/components/ui/navigation.tsx b/frontend/src/components/ui/navigation.tsx
index 6afb255..04a99d2 100644
--- a/frontend/src/components/ui/navigation.tsx
+++ b/frontend/src/components/ui/navigation.tsx
@@ -33,6 +33,11 @@ const Navigation = () => {
const { user, logout } = useAuth()
const { isModuleEnabled } = useModules()
const { installedPlugins, getPluginPages } = usePlugin()
+ const [isClient, setIsClient] = React.useState(false)
+
+ React.useEffect(() => {
+ setIsClient(true)
+ }, [])
// Get plugin navigation items
const pluginNavItems = installedPlugins
@@ -96,13 +101,13 @@ const Navigation = () => {
-
+
Enclava
- {user && (
+ {isClient && user && (
diff --git a/nginx/nginx.test.conf b/nginx/nginx.test.conf
new file mode 100644
index 0000000..b1f25fc
--- /dev/null
+++ b/nginx/nginx.test.conf
@@ -0,0 +1,149 @@
+events {
+ worker_connections 1024;
+}
+
+http {
+ upstream backend {
+ server enclava-backend-test:8000;
+ }
+
+ # Frontend service disabled for simplified testing
+
+ # Logging configuration for tests
+ log_format test_format '$remote_addr - $remote_user [$time_local] '
+ '"$request" $status $body_bytes_sent '
+ '"$http_referer" "$http_user_agent" '
+ 'rt=$request_time uct="$upstream_connect_time" '
+ 'uht="$upstream_header_time" urt="$upstream_response_time"';
+
+ access_log /var/log/nginx/test_access.log test_format;
+ error_log /var/log/nginx/test_error.log debug;
+
+ server {
+ listen 80;
+ server_name localhost;
+
+ # Frontend routes (simplified for testing)
+ location / {
+ return 200 '{"message": "Enclava Test Environment", "backend_api": "/api/", "internal_api": "/api-internal/", "health": "/health", "docs": "/docs"}';
+ add_header Content-Type application/json;
+ }
+
+ # Internal API routes - proxy to backend (for frontend only)
+ location /api-internal/ {
+ proxy_pass http://backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Request/Response buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ # Timeouts for long-running requests
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 60s;
+ proxy_read_timeout 60s;
+
+ # CORS headers for frontend
+ add_header 'Access-Control-Allow-Origin' '*' always;
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
+ add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
+
+ # Handle preflight requests
+ if ($request_method = 'OPTIONS') {
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
+ add_header 'Access-Control-Max-Age' 1728000;
+ add_header 'Content-Type' 'text/plain; charset=utf-8';
+ add_header 'Content-Length' 0;
+ return 204;
+ }
+ }
+
+ # Public API routes - proxy to backend (for external clients)
+ location /api/ {
+ proxy_pass http://backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Request/Response buffering
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ # Timeouts for long-running requests (LLM streaming)
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 300s;
+ proxy_read_timeout 300s;
+
+ # CORS headers for external clients
+ add_header 'Access-Control-Allow-Origin' '*' always;
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
+ add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
+
+ # Handle preflight requests
+ if ($request_method = 'OPTIONS') {
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
+ add_header 'Access-Control-Max-Age' 1728000;
+ add_header 'Content-Type' 'text/plain; charset=utf-8';
+ add_header 'Content-Length' 0;
+ return 204;
+ }
+ }
+
+ # Health check endpoints
+ location /health {
+ proxy_pass http://backend/health;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Add test marker header
+ add_header X-Test-Environment 'true' always;
+ }
+
+ # Backend docs endpoint (for testing)
+ location /docs {
+ proxy_pass http://backend/docs;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # Static files (simplified for testing - return 404 for now)
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ return 404 '{"error": "Static files not available in test environment", "status": 404}';
+ add_header Content-Type application/json;
+ }
+
+ # Test-specific endpoints
+ location /test-status {
+ return 200 '{"status": "test environment active", "timestamp": "$time_iso8601"}';
+ add_header Content-Type application/json;
+ }
+
+ # Error pages for testing
+ error_page 404 /404.html;
+ error_page 500 502 503 504 /50x.html;
+
+ location = /404.html {
+ return 404 '{"error": "Not Found", "status": 404}';
+ add_header Content-Type application/json;
+ }
+
+ location = /50x.html {
+ return 500 '{"error": "Internal Server Error", "status": 500}';
+ add_header Content-Type application/json;
+ }
+ }
+}
\ No newline at end of file