diff --git a/.env.example b/.env.example index fed060d..3f2734c 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,19 @@ # =================================== -# ENCLAVA MINIMAL CONFIGURATION +# ENCLAVA CONFIGURATION # =================================== # Only essential environment variables that CANNOT have defaults # Other settings should be configurable through the app UI +# Admin user (created on first startup only) +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=admin123 + + +# =================================== +# APPLICATION BASE URL (Required - derives all URLs and CORS) +# =================================== +BASE_URL=localhost + # =================================== # INFRASTRUCTURE (Required) # =================================== @@ -18,9 +28,7 @@ POSTGRES_PASSWORD=enclava_pass JWT_SECRET=your-super-secret-jwt-key-here-change-in-production PRIVATEMODE_API_KEY=your-privatemode-api-key-here -# Admin user (created on first startup only) -ADMIN_EMAIL=admin@example.com -ADMIN_PASSWORD=admin123 + # =================================== # ADDITIONAL SECURITY SETTINGS (Optional but recommended) @@ -36,29 +44,31 @@ ADMIN_PASSWORD=admin123 # API Key prefix (default: en_) # API_KEY_PREFIX=en_ -# Security thresholds (0.0-1.0) -# API_SECURITY_RISK_THRESHOLD=0.8 -# API_SECURITY_WARNING_THRESHOLD=0.6 -# API_SECURITY_ANOMALY_THRESHOLD=0.7 -# IP security (comma-separated for multiple IPs) -# API_BLOCKED_IPS= -# API_ALLOWED_IPS= # =================================== -# APPLICATION BASE URL (Required - derives all URLs and CORS) +# FRONTEND ENVIRONMENT (Required for production) # =================================== -BASE_URL=localhost -# Frontend derives: APP_URL=http://localhost, API_URL=http://localhost, WS_URL=ws://localhost -# Backend derives: CORS_ORIGINS=["http://localhost"] +NODE_ENV=production +NEXT_PUBLIC_APP_NAME=Enclava +# NEXT_PUBLIC_BASE_URL is derived from BASE_URL in Docker configuration # =================================== -# DOCKER NETWORKING (Required for containers) +# LOGGING CONFIGURATION # =================================== -BACKEND_INTERNAL_PORT=8000 -FRONTEND_INTERNAL_PORT=3000 -# Hosts are fixed: enclava-backend, enclava-frontend -# Upstreams derive: enclava-backend:8000, enclava-frontend:3000 +LOG_LLM_PROMPTS=false + +# For production HTTPS deployments, set: +# BASE_URL=your-domain.com +# The system will automatically detect HTTPS and use it for all URLs and CORS + +# =================================== +# DOCKER NETWORKING (Optional - defaults provided) +# =================================== +# Internal ports use defaults: backend=8000, frontend=3000 +# Override only if you need to change these defaults: +# BACKEND_INTERNAL_PORT=8000 +# FRONTEND_INTERNAL_PORT=3000 # =================================== # QDRANT (Required for RAG) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index c5cb8c3..2c1ae90 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -48,7 +48,8 @@ class Settings(BaseSettings): """Derive CORS origins from BASE_URL if not explicitly set""" if v is None: base_url = info.data.get('BASE_URL', 'localhost') - return [f"http://{base_url}"] + # Support both HTTP and HTTPS for production environments + return [f"http://{base_url}", f"https://{base_url}"] return v if isinstance(v, list) else [v] # CORS origins (derived from BASE_URL) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 969e428..2ce3e9f 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -23,9 +23,11 @@ services: # Database migration service - runs once to apply migrations enclava-migrate: - build: + build: context: ./backend dockerfile: Dockerfile.prod + env_file: + - ./.env environment: - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@enclava-postgres:5432/${POSTGRES_DB} depends_on: @@ -40,6 +42,8 @@ services: build: context: ./backend dockerfile: Dockerfile.prod + env_file: + - ./.env environment: - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@enclava-postgres:5432/${POSTGRES_DB} - REDIS_URL=redis://enclava-redis:6379 @@ -76,11 +80,11 @@ services: context: ./frontend dockerfile: Dockerfile target: runner # Use the production stage from multi-stage build + env_file: + - ./.env environment: - BASE_URL=${BASE_URL} - NEXT_PUBLIC_BASE_URL=${BASE_URL} - - BACKEND_INTERNAL_PORT=8000 - - FRONTEND_INTERNAL_PORT=3000 - INTERNAL_API_URL=http://enclava-backend:8000 - NODE_ENV=production - NEXT_TELEMETRY_DISABLED=1 diff --git a/docker-compose.yml b/docker-compose.yml index 8210ba9..ab9859a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,11 +68,8 @@ services: # Required base URL (derives APP/API/WS URLs) - BASE_URL=${BASE_URL} - NEXT_PUBLIC_BASE_URL=${BASE_URL} - # Docker internal ports - - BACKEND_INTERNAL_PORT=${BACKEND_INTERNAL_PORT} - - FRONTEND_INTERNAL_PORT=${FRONTEND_INTERNAL_PORT} # Internal API URL - - INTERNAL_API_URL=http://enclava-backend:${BACKEND_INTERNAL_PORT} + - INTERNAL_API_URL=http://enclava-backend:8000 depends_on: - enclava-backend ports: diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index ecc7af7..e0f3c58 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -16,8 +16,21 @@ export const viewport: Viewport = { initialScale: 1, } +// Function to determine the base URL with proper protocol +const getBaseUrl = () => { + // In production, we need to detect if we're behind HTTPS + if (typeof window !== 'undefined') { + const protocol = window.location.protocol === 'https:' ? 'https' : 'http' + const host = process.env.NEXT_PUBLIC_BASE_URL || window.location.hostname + return `${protocol}://${host}` + } + // For build time/server side, default to HTTP for dev, HTTPS for production + const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http' + return `${protocol}://${process.env.NEXT_PUBLIC_BASE_URL || 'localhost'}` +} + export const metadata: Metadata = { - metadataBase: new URL(`http://${process.env.NEXT_PUBLIC_BASE_URL || 'localhost'}`), + metadataBase: new URL(getBaseUrl()), title: 'Enclava Platform', description: 'Secure AI processing platform with plugin-based architecture and confidential computing', keywords: ['AI', 'Enclava', 'Confidential Computing', 'LLM', 'TEE'], @@ -26,7 +39,7 @@ export const metadata: Metadata = { openGraph: { type: 'website', locale: 'en_US', - url: `http://${process.env.NEXT_PUBLIC_BASE_URL || 'localhost'}`, + url: getBaseUrl(), title: 'Enclava Platform', description: 'Secure AI processing platform with plugin-based architecture and confidential computing', siteName: 'Enclava', diff --git a/frontend/src/components/rag/document-upload.tsx b/frontend/src/components/rag/document-upload.tsx index 387bc8c..040eefb 100644 --- a/frontend/src/components/rag/document-upload.tsx +++ b/frontend/src/components/rag/document-upload.tsx @@ -91,8 +91,9 @@ export function DocumentUpload({ collections, selectedCollection, onDocumentUplo updateProgress(60) await uploadFile( - '/api-internal/v1/rag/documents', uploadingFile.file, + '/api-internal/v1/rag/documents', + (progress) => updateProgress(progress), { collection_id: targetCollection } ) diff --git a/frontend/src/components/ui/user-menu.tsx b/frontend/src/components/ui/user-menu.tsx index cbec7cc..78e070d 100644 --- a/frontend/src/components/ui/user-menu.tsx +++ b/frontend/src/components/ui/user-menu.tsx @@ -18,6 +18,16 @@ import { import { User, Settings, Lock, LogOut, ChevronDown } from "lucide-react" import { useState } from "react" +// Helper function to get API URL with proper protocol +const getApiUrl = () => { + if (typeof window !== 'undefined') { + const protocol = window.location.protocol.slice(0, -1) // Remove ':' from 'https:' + const host = window.location.hostname + return `${protocol}://${host}` + } + return `http://${process.env.NEXT_PUBLIC_BASE_URL || 'localhost'}` +} + export function UserMenu() { const { user, logout } = useAuth() const { toast } = useToast() @@ -62,7 +72,7 @@ export function UserMenu() { throw new Error('Authentication required') } - const response = await fetch('/api-internal/v1/auth/change-password', { + const response = await fetch(`${getApiUrl()}/api-internal/v1/auth/change-password`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index c5be4be..33030d8 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -4,6 +4,16 @@ import { createContext, useContext, useState, useEffect, ReactNode } from "react import { useRouter } from "next/navigation" import { tokenManager } from "@/lib/token-manager" +// Helper function to get API URL with proper protocol +const getApiUrl = () => { + if (typeof window !== 'undefined') { + const protocol = window.location.protocol.slice(0, -1) // Remove ':' from 'https:' + const host = window.location.hostname + return `${protocol}://${host}` + } + return `http://${process.env.NEXT_PUBLIC_BASE_URL || 'localhost'}` +} + interface User { id: string email: string @@ -84,7 +94,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { const token = await tokenManager.getAccessToken() if (!token) return - const response = await fetch('/api-internal/v1/auth/me', { + const response = await fetch(`${getApiUrl()}/api-internal/v1/auth/me`, { headers: { 'Authorization': `Bearer ${token}`, }, @@ -114,7 +124,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { setIsLoading(true) try { - const response = await fetch('/api-internal/v1/auth/login', { + const response = await fetch(`${getApiUrl()}/api-internal/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/frontend/src/lib/api-client.ts b/frontend/src/lib/api-client.ts index 75ae0aa..f08d5b2 100644 --- a/frontend/src/lib/api-client.ts +++ b/frontend/src/lib/api-client.ts @@ -1,10 +1,23 @@ import axios from 'axios'; import Cookies from 'js-cookie'; -const API_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || ''; +// Dynamic base URL with protocol detection +const getApiBaseUrl = (): string => { + if (typeof window !== 'undefined') { + // Client-side: use the same protocol as the current page + const protocol = window.location.protocol.slice(0, -1); // Remove ':' from 'https:' + const host = window.location.hostname; + return `${protocol}://${host}`; + } + + // Server-side: use environment variable or default to localhost + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'localhost'; + const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; + return `${protocol}://${baseUrl}`; +}; const axiosInstance = axios.create({ - baseURL: API_BASE_URL, + baseURL: getApiBaseUrl(), headers: { 'Content-Type': 'application/json', }, @@ -36,7 +49,7 @@ axiosInstance.interceptors.response.use( try { const refreshToken = Cookies.get('refresh_token'); if (refreshToken) { - const response = await axios.post(`${API_BASE_URL}/api/auth/refresh`, { + const response = await axios.post(`${getApiBaseUrl()}/api/auth/refresh`, { refresh_token: refreshToken, }); diff --git a/frontend/src/lib/file-download.ts b/frontend/src/lib/file-download.ts index bb0ea2e..c0b9d48 100644 --- a/frontend/src/lib/file-download.ts +++ b/frontend/src/lib/file-download.ts @@ -76,12 +76,20 @@ export const downloadFileFromData = ( export const uploadFile = async ( file: File, url: string, - onProgress?: (progress: number) => void + onProgress?: (progress: number) => void, + additionalData?: Record ): Promise => { try { const formData = new FormData(); formData.append('file', file); + // Add additional form data if provided + if (additionalData) { + Object.entries(additionalData).forEach(([key, value]) => { + formData.append(key, value); + }); + } + const xhr = new XMLHttpRequest(); return new Promise((resolve, reject) => {