This commit is contained in:
MarkLo127 2026-01-16 00:56:14 +08:00
parent 3145d08c30
commit 0bdb3fb774
5 changed files with 268 additions and 43 deletions

View File

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

View File

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

View File

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

View File

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

View File

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