This commit is contained in:
parent
3145d08c30
commit
0bdb3fb774
41
.env.example
41
.env.example
|
|
@ -1,19 +1,38 @@
|
|||
# Required API Keys
|
||||
OPENAI_API_KEY=openai_api_key_placeholder
|
||||
# ===== API Keys =====
|
||||
# Note: In production, users provide their own API keys via the web interface.
|
||||
# These server-side keys are optional and only used for CLI or testing.
|
||||
|
||||
# Optional API Keys (for alternative LLM providers)
|
||||
ANTHROPIC_API_KEY=anthropic_api_key_placeholder
|
||||
GEMINI_API_KEY=GEMINI_API_KEY_placeholder
|
||||
XAI_API_KEY=XAI_API_KEY_placeholder
|
||||
DEEPSEEK_API_KEY=deepseek_api_key_placeholder
|
||||
DASHSCOPE_API_KEY=dashscope_api_key_placeholder
|
||||
# Optional: Server-side API Keys (not required for web interface)
|
||||
OPENAI_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
GEMINI_API_KEY=
|
||||
XAI_API_KEY=
|
||||
DEEPSEEK_API_KEY=
|
||||
DASHSCOPE_API_KEY=
|
||||
|
||||
# Optional API Keys (for data sources)
|
||||
ALPHA_VANTAGE_API_KEY=alpha_vantage_api_key_placeholder
|
||||
FINMIND_API_KEY=finmind_api_key_placeholder
|
||||
ALPHA_VANTAGE_API_KEY=
|
||||
FINMIND_API_KEY=
|
||||
|
||||
|
||||
# Deployment Configuration
|
||||
# ===== Security Configuration =====
|
||||
|
||||
# JWT Secret - IMPORTANT: Change this in production!
|
||||
# Generate a secure secret: openssl rand -hex 32
|
||||
JWT_SECRET=dev-secret-please-change-in-production
|
||||
|
||||
# CORS Origins - Comma-separated list of allowed frontend URLs
|
||||
# Example: CORS_ORIGINS=https://your-app.railway.app,https://your-frontend.vercel.app
|
||||
# Leave empty to use default wildcard origins (less secure)
|
||||
CORS_ORIGINS=
|
||||
|
||||
# Google OAuth (optional - for user authentication and persistent storage)
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
|
||||
|
||||
# ===== Deployment Configuration =====
|
||||
TRADINGAGENTS_RESULTS_DIR=/app/results
|
||||
PYTHON_VERSION=3.13
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,70 @@
|
|||
"""
|
||||
Shared dependencies for API routes
|
||||
"""
|
||||
from fastapi import Depends
|
||||
from typing import Optional, Dict, Any
|
||||
from fastapi import Depends, HTTPException, Header
|
||||
from backend.app.services.trading_service import TradingService, trading_service
|
||||
from backend.app.services.auth_utils import verify_access_token
|
||||
|
||||
|
||||
def get_trading_service() -> TradingService:
|
||||
"""Dependency to get trading service instance"""
|
||||
return trading_service
|
||||
|
||||
|
||||
async def get_current_user_optional(
|
||||
authorization: Optional[str] = Header(None)
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get current user from JWT token (optional - returns None if not authenticated)
|
||||
|
||||
Use this for endpoints that work both with and without authentication.
|
||||
"""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
return None
|
||||
|
||||
token = authorization.replace("Bearer ", "")
|
||||
payload = verify_access_token(token)
|
||||
|
||||
if not payload:
|
||||
return None
|
||||
|
||||
return {
|
||||
"id": payload.get("sub"),
|
||||
"email": payload.get("email"),
|
||||
"name": payload.get("name"),
|
||||
"avatar_url": payload.get("avatar_url"),
|
||||
}
|
||||
|
||||
|
||||
async def get_current_user_required(
|
||||
authorization: Optional[str] = Header(None)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get current user from JWT token (required - raises 401 if not authenticated)
|
||||
|
||||
Use this for endpoints that require authentication.
|
||||
"""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Authentication required. Please login to use this feature.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
token = authorization.replace("Bearer ", "")
|
||||
payload = verify_access_token(token)
|
||||
|
||||
if not payload:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid or expired token. Please login again.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return {
|
||||
"id": payload.get("sub"),
|
||||
"email": payload.get("email"),
|
||||
"name": payload.get("name"),
|
||||
"avatar_url": payload.get("avatar_url"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from backend.app.models.schemas import (
|
|||
)
|
||||
from backend.app.services.trading_service import TradingService
|
||||
from backend.app.services.task_manager import task_manager
|
||||
from backend.app.api.dependencies import get_trading_service
|
||||
from backend.app.api.dependencies import get_trading_service, get_current_user_optional
|
||||
from backend.app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -54,6 +54,7 @@ async def get_config(service: TradingService = Depends(get_trading_service)):
|
|||
async def run_analysis(
|
||||
request: AnalysisRequest,
|
||||
service: TradingService = Depends(get_trading_service),
|
||||
current_user: dict = Depends(get_current_user_optional),
|
||||
):
|
||||
"""
|
||||
Start an async trading analysis task.
|
||||
|
|
@ -61,19 +62,42 @@ async def run_analysis(
|
|||
This endpoint creates an async task and returns immediately with a task ID.
|
||||
Use the /api/task/{task_id} endpoint to check the status and get results.
|
||||
|
||||
Authentication: Required by default. Set REQUIRE_AUTH_FOR_ANALYZE=false to disable.
|
||||
|
||||
Args:
|
||||
request: Analysis request configuration
|
||||
service: Trading service instance (injected)
|
||||
current_user: Authenticated user (optional based on config)
|
||||
|
||||
Returns:
|
||||
TaskCreatedResponse: Task ID and initial status
|
||||
"""
|
||||
logger.info(f"Creating analysis task for {request.ticker} on {request.analysis_date}")
|
||||
# Validate that user provides their own API key
|
||||
# This allows both authenticated and anonymous users to use the service
|
||||
# as long as they provide their own LLM API key
|
||||
has_api_key = bool(
|
||||
request.openai_api_key or
|
||||
request.quick_think_api_key or
|
||||
request.deep_think_api_key
|
||||
)
|
||||
|
||||
# Create task in Redis
|
||||
if not has_api_key:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="API Key required. Please provide your own LLM API key (OpenAI, Anthropic, etc.) to use the analysis service.",
|
||||
)
|
||||
|
||||
# Log with user info for tracking
|
||||
user_info = f"user={current_user['email']}" if current_user else "user=anonymous"
|
||||
logger.info(f"Creating analysis task for {request.ticker} on {request.analysis_date} ({user_info})")
|
||||
|
||||
# Create task in Redis with user info
|
||||
task_id = task_manager.create_task({
|
||||
"ticker": request.ticker,
|
||||
"analysis_date": request.analysis_date,
|
||||
"user_id": current_user["id"] if current_user else None,
|
||||
"user_email": current_user["email"] if current_user else None,
|
||||
})
|
||||
|
||||
# Start background analysis
|
||||
|
|
|
|||
|
|
@ -21,13 +21,29 @@ class Settings(BaseSettings):
|
|||
alpha_vantage_api_key: Optional[str] = None
|
||||
|
||||
# CORS Configuration
|
||||
cors_origins: list = [
|
||||
"http://localhost:3000",
|
||||
"http://frontend:3000",
|
||||
"https://*.vercel.app", # Vercel deployments
|
||||
"https://*.onrender.com", # Render deployments
|
||||
"https://*.railway.app", # Railway deployments
|
||||
]
|
||||
# Set CORS_ORIGINS environment variable to comma-separated list of allowed origins
|
||||
# Example: CORS_ORIGINS=https://your-app.railway.app,https://your-frontend.vercel.app
|
||||
cors_origins_str: Optional[str] = Field(default=None, alias="CORS_ORIGINS")
|
||||
|
||||
@property
|
||||
def cors_origins(self) -> list:
|
||||
"""Get CORS origins from environment or use defaults"""
|
||||
if self.cors_origins_str:
|
||||
# Parse comma-separated origins from environment
|
||||
origins = [o.strip() for o in self.cors_origins_str.split(",") if o.strip()]
|
||||
# Always include localhost for development
|
||||
if "http://localhost:3000" not in origins:
|
||||
origins.append("http://localhost:3000")
|
||||
return origins
|
||||
|
||||
# Default origins (fallback - consider removing wildcards in production)
|
||||
return [
|
||||
"http://localhost:3000",
|
||||
"http://frontend:3000",
|
||||
"https://*.vercel.app", # Vercel deployments
|
||||
"https://*.onrender.com", # Render deployments
|
||||
"https://*.railway.app", # Railway deployments
|
||||
]
|
||||
|
||||
# TradingAgentsX Configuration
|
||||
results_dir: str = "./results"
|
||||
|
|
@ -40,6 +56,7 @@ class Settings(BaseSettings):
|
|||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
extra = "ignore" # Ignore extra environment variables like ANTHROPIC_API_KEY, etc.
|
||||
populate_by_name = True # Allow using alias names
|
||||
|
||||
|
||||
# Global settings instance
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
/**
|
||||
* localStorage utility for API settings with encryption
|
||||
* API keys are encrypted using AES-256-GCM before storage
|
||||
* localStorage/sessionStorage utility for API settings with encryption
|
||||
*
|
||||
* Storage Strategy:
|
||||
* - Logged-in users: localStorage with encryption (persistent)
|
||||
* - Anonymous users: sessionStorage (cleared on browser close)
|
||||
*
|
||||
* API keys are encrypted using AES-256-GCM before storage (localStorage only)
|
||||
*/
|
||||
|
||||
import { encryptObject, decryptObject, isEncrypted, clearCryptoData } from "./crypto";
|
||||
|
|
@ -23,8 +28,13 @@ export interface ApiSettings {
|
|||
custom_api_key: string;
|
||||
}
|
||||
|
||||
// Storage keys
|
||||
const STORAGE_KEY = "tradingagents_api_settings";
|
||||
const ENCRYPTED_FLAG_KEY = "tradingagents_encrypted";
|
||||
const AUTH_TOKEN_KEY = "tradingagents_auth_token";
|
||||
|
||||
// Storage mode type
|
||||
export type StorageMode = "local" | "session";
|
||||
|
||||
export const DEFAULT_API_SETTINGS: ApiSettings = {
|
||||
openai_api_key: "",
|
||||
|
|
@ -39,6 +49,52 @@ export const DEFAULT_API_SETTINGS: ApiSettings = {
|
|||
custom_api_key: "",
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if user is currently authenticated
|
||||
*/
|
||||
function isUserAuthenticated(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
const token = localStorage.getItem(AUTH_TOKEN_KEY);
|
||||
if (!token) return false;
|
||||
|
||||
// Check if token is expired
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split(".")[1]));
|
||||
const exp = payload.exp * 1000;
|
||||
return Date.now() < exp;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate storage based on authentication status
|
||||
* - Authenticated: localStorage (persistent)
|
||||
* - Anonymous: sessionStorage (cleared on browser close)
|
||||
*/
|
||||
function getStorage(): Storage {
|
||||
if (typeof window === "undefined") {
|
||||
// Return a mock storage for SSR
|
||||
return {
|
||||
getItem: () => null,
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {},
|
||||
length: 0,
|
||||
key: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
return isUserAuthenticated() ? localStorage : sessionStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current storage mode
|
||||
*/
|
||||
export function getCurrentStorageMode(): StorageMode {
|
||||
return isUserAuthenticated() ? "local" : "session";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if stored data is using legacy (unencrypted) format
|
||||
*/
|
||||
|
|
@ -72,30 +128,55 @@ function isLegacyFormat(): boolean {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get API settings from localStorage (async due to decryption)
|
||||
* Get API settings (async due to potential decryption)
|
||||
*
|
||||
* Storage strategy:
|
||||
* - Authenticated users: Read from localStorage (encrypted)
|
||||
* - Anonymous users: Read from sessionStorage (plaintext, cleared on browser close)
|
||||
*/
|
||||
export async function getApiSettingsAsync(): Promise<ApiSettings> {
|
||||
if (typeof window === "undefined") {
|
||||
return DEFAULT_API_SETTINGS;
|
||||
}
|
||||
|
||||
const storage = getStorage();
|
||||
const authenticated = isUserAuthenticated();
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
const stored = storage.getItem(STORAGE_KEY);
|
||||
if (!stored) {
|
||||
// If authenticated, also check sessionStorage for data to migrate
|
||||
if (authenticated) {
|
||||
const sessionData = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (sessionData) {
|
||||
console.log("Migrating session data to localStorage after login");
|
||||
const parsed = JSON.parse(sessionData);
|
||||
const merged = { ...DEFAULT_API_SETTINGS, ...parsed };
|
||||
await saveApiSettingsAsync(merged);
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
return DEFAULT_API_SETTINGS;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(stored);
|
||||
|
||||
// If legacy format detected, return as-is (will be encrypted on next save)
|
||||
if (isLegacyFormat()) {
|
||||
console.warn("Legacy unencrypted settings detected. Will encrypt on next save.");
|
||||
return { ...DEFAULT_API_SETTINGS, ...parsed };
|
||||
// For authenticated users, decrypt the data
|
||||
if (authenticated) {
|
||||
// If legacy format detected, return as-is (will be encrypted on next save)
|
||||
if (isLegacyFormat()) {
|
||||
console.warn("Legacy unencrypted settings detected. Will encrypt on next save.");
|
||||
return { ...DEFAULT_API_SETTINGS, ...parsed };
|
||||
}
|
||||
|
||||
// Decrypt the settings
|
||||
const decrypted = await decryptObject(parsed);
|
||||
return { ...DEFAULT_API_SETTINGS, ...decrypted };
|
||||
}
|
||||
|
||||
// Decrypt the settings
|
||||
const decrypted = await decryptObject(parsed);
|
||||
return { ...DEFAULT_API_SETTINGS, ...decrypted };
|
||||
// For anonymous users, data is stored as plaintext in sessionStorage
|
||||
return { ...DEFAULT_API_SETTINGS, ...parsed };
|
||||
} catch (error) {
|
||||
console.error("Error reading API settings:", error);
|
||||
return DEFAULT_API_SETTINGS;
|
||||
|
|
@ -111,33 +192,49 @@ export function getApiSettings(): ApiSettings {
|
|||
return DEFAULT_API_SETTINGS;
|
||||
}
|
||||
|
||||
const storage = getStorage();
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
const stored = storage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
return { ...DEFAULT_API_SETTINGS, ...parsed };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error reading API settings from localStorage:", error);
|
||||
console.error("Error reading API settings:", error);
|
||||
}
|
||||
|
||||
return DEFAULT_API_SETTINGS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save API settings to localStorage with encryption
|
||||
* Save API settings (async)
|
||||
*
|
||||
* Storage strategy:
|
||||
* - Authenticated users: Save to localStorage with encryption (persistent)
|
||||
* - Anonymous users: Save to sessionStorage as plaintext (cleared on browser close)
|
||||
*/
|
||||
export async function saveApiSettingsAsync(settings: ApiSettings): Promise<void> {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const storage = getStorage();
|
||||
const authenticated = isUserAuthenticated();
|
||||
|
||||
try {
|
||||
// Encrypt sensitive fields
|
||||
const encrypted = await encryptObject(settings as unknown as Record<string, string>);
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(encrypted));
|
||||
localStorage.setItem(ENCRYPTED_FLAG_KEY, "true");
|
||||
if (authenticated) {
|
||||
// For authenticated users, encrypt and store in localStorage
|
||||
const encrypted = await encryptObject(settings as unknown as Record<string, string>);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(encrypted));
|
||||
localStorage.setItem(ENCRYPTED_FLAG_KEY, "true");
|
||||
console.log("API settings saved to localStorage (encrypted)");
|
||||
} else {
|
||||
// For anonymous users, store as plaintext in sessionStorage
|
||||
// This will be automatically cleared when browser closes
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
console.log("API settings saved to sessionStorage (will clear on browser close)");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving API settings:", error);
|
||||
throw error;
|
||||
|
|
@ -158,7 +255,7 @@ export function saveApiSettings(settings: ApiSettings): void {
|
|||
}
|
||||
|
||||
/**
|
||||
* Clear API settings from localStorage
|
||||
* Clear API settings from both localStorage and sessionStorage
|
||||
*/
|
||||
export function clearApiSettings(): void {
|
||||
if (typeof window === "undefined") {
|
||||
|
|
@ -166,11 +263,19 @@ export function clearApiSettings(): void {
|
|||
}
|
||||
|
||||
try {
|
||||
// Clear from localStorage
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(ENCRYPTED_FLAG_KEY);
|
||||
|
||||
// Clear from sessionStorage
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
|
||||
// Clear crypto data
|
||||
clearCryptoData();
|
||||
|
||||
console.log("API settings cleared from all storage");
|
||||
} catch (error) {
|
||||
console.error("Error clearing API settings from localStorage:", error);
|
||||
console.error("Error clearing API settings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue