diff --git a/backend/alwrity_utils/database_setup.py b/backend/alwrity_utils/database_setup.py index 01b27264..597c66e7 100644 --- a/backend/alwrity_utils/database_setup.py +++ b/backend/alwrity_utils/database_setup.py @@ -35,6 +35,7 @@ class DatabaseSetup: self._create_monitoring_tables() self._create_subscription_tables() self._create_persona_tables() + self._create_onboarding_tables() if verbose: print("✅ Essential database tables created") @@ -97,6 +98,22 @@ class DatabaseSetup: print(f" ⚠️ Persona tables failed: {e}") return True # Non-critical + def _create_onboarding_tables(self) -> bool: + """Create onboarding tables.""" + import os + verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" + + try: + from models.onboarding import Base as OnboardingBase + OnboardingBase.metadata.create_all(bind=engine) + if verbose: + print(" ✅ Onboarding tables created") + return True + except Exception as e: + if verbose: + print(f" ⚠️ Onboarding tables failed: {e}") + return True # Non-critical + def verify_tables(self) -> bool: """Verify that essential tables exist.""" import os @@ -120,7 +137,9 @@ class DatabaseSetup: essential_tables = [ 'api_monitoring_logs', 'subscription_plans', - 'user_subscriptions' + 'user_subscriptions', + 'onboarding_sessions', + 'persona_data' ] existing_tables = [table for table in essential_tables if table in tables] diff --git a/backend/api/onboarding_utils/onboarding_summary_service.py b/backend/api/onboarding_utils/onboarding_summary_service.py index 5c464482..be9b496d 100644 --- a/backend/api/onboarding_utils/onboarding_summary_service.py +++ b/backend/api/onboarding_utils/onboarding_summary_service.py @@ -186,8 +186,12 @@ class OnboardingSummaryService: async def get_research_preferences_data(self) -> Dict[str, Any]: """Get research preferences data for the user.""" try: - research_prefs_service = ResearchPreferencesService() - return await research_prefs_service.get_research_preferences(self.user_id) + db = next(get_db()) + research_prefs_service = ResearchPreferencesService(db) + # Use the new method that accepts user_id directly + result = research_prefs_service.get_research_preferences_by_user_id(self.user_id) + db.close() + return result except Exception as e: logger.error(f"Error getting research preferences data: {e}") raise \ No newline at end of file diff --git a/backend/api/subscription_api.py b/backend/api/subscription_api.py index 736987d0..ac6a88c2 100644 --- a/backend/api/subscription_api.py +++ b/backend/api/subscription_api.py @@ -13,6 +13,7 @@ from functools import lru_cache from services.database import get_db from services.usage_tracking_service import UsageTrackingService from services.pricing_service import PricingService +from middleware.auth_middleware import get_current_user from models.subscription_models import ( APIProvider, SubscriptionPlan, UserSubscription, UsageSummary, APIProviderPricing, UsageAlert, SubscriptionTier, BillingCycle, UsageStatus @@ -30,10 +31,15 @@ _DASHBOARD_CACHE_TTL_SEC = 2.0 async def get_user_usage( user_id: str, billing_period: Optional[str] = Query(None, description="Billing period (YYYY-MM)"), - db: Session = Depends(get_db) + db: Session = Depends(get_db), + current_user: Dict[str, Any] = Depends(get_current_user) ) -> Dict[str, Any]: """Get comprehensive usage statistics for a user.""" + # Verify user can only access their own data + if current_user.get('id') != user_id: + raise HTTPException(status_code=403, detail="Access denied") + try: usage_service = UsageTrackingService(db) stats = usage_service.get_user_usage_stats(user_id, billing_period) @@ -122,10 +128,15 @@ async def get_subscription_plans( @router.get("/user/{user_id}/subscription") async def get_user_subscription( user_id: str, - db: Session = Depends(get_db) + db: Session = Depends(get_db), + current_user: Dict[str, Any] = Depends(get_current_user) ) -> Dict[str, Any]: """Get user's current subscription information.""" + # Verify user can only access their own data + if current_user.get('id') != user_id: + raise HTTPException(status_code=403, detail="Access denied") + try: subscription = db.query(UserSubscription).filter( UserSubscription.user_id == user_id, @@ -212,10 +223,15 @@ async def get_user_subscription( @router.get("/status/{user_id}") async def get_subscription_status( user_id: str, - db: Session = Depends(get_db) + db: Session = Depends(get_db), + current_user: Dict[str, Any] = Depends(get_current_user) ) -> Dict[str, Any]: """Get simple subscription status for enforcement checks.""" + # Verify user can only access their own data + if current_user.get('id') != user_id: + raise HTTPException(status_code=403, detail="Access denied") + try: subscription = db.query(UserSubscription).filter( UserSubscription.user_id == user_id, diff --git a/backend/app.py b/backend/app.py index 404d4e27..2641c4d0 100644 --- a/backend/app.py +++ b/backend/app.py @@ -120,7 +120,8 @@ async def rate_limit_middleware(request: Request, call_next): return await rate_limiter.rate_limit_middleware(request, call_next) # 3. LAST REGISTERED (runs FIRST) - API key injection -# API key injection middleware removed - now using environment variables directly +from middleware.api_key_injection_middleware import api_key_injection_middleware +app.middleware("http")(api_key_injection_middleware) # Health check endpoints using modular utilities @app.get("/health") diff --git a/backend/middleware/api_key_injection_middleware.py b/backend/middleware/api_key_injection_middleware.py index 0c26993f..0fd6c559 100644 --- a/backend/middleware/api_key_injection_middleware.py +++ b/backend/middleware/api_key_injection_middleware.py @@ -41,12 +41,17 @@ class APIKeyInjectionMiddleware: if user: # Try different possible keys for user_id user_id = user.get('user_id') or user.get('clerk_user_id') or user.get('id') - logger.debug(f"[API Key Injection] Extracted user_id: {user_id}") - - # Store user_id in request.state for monitoring middleware - request.state.user_id = user_id + if user_id: + logger.info(f"[API Key Injection] Extracted user_id: {user_id}") + + # Store user_id in request.state for monitoring middleware + request.state.user_id = user_id + else: + logger.warning(f"[API Key Injection] User object missing ID: {user}") + else: + logger.warning("[API Key Injection] Token verification failed") except Exception as e: - logger.debug(f"[API Key Injection] Could not extract user from token: {e}") + logger.error(f"[API Key Injection] Could not extract user from token: {e}") if not user_id: # No authenticated user, proceed without injection diff --git a/backend/middleware/monitoring_middleware.py b/backend/middleware/monitoring_middleware.py index ce1a0c4c..717ad36c 100644 --- a/backend/middleware/monitoring_middleware.py +++ b/backend/middleware/monitoring_middleware.py @@ -488,9 +488,9 @@ async def monitoring_middleware(request: Request, call_next): # Check for authorization header with user info elif 'authorization' in request.headers: # Auth middleware should have set request.state.user_id - # If not, skip usage limits (unauthenticated or auth will handle) + # If not, this indicates an authentication failure that should be logged user_id = None - logger.debug("Monitoring: Auth header present but no user_id in state - skipping limits") + logger.warning("Monitoring: Auth header present but no user_id in state - authentication may have failed") # For alpha testing, use IP address as user identifier if no other ID found # But only if there's no auth header (truly anonymous) diff --git a/backend/services/research_preferences_service.py b/backend/services/research_preferences_service.py index b2b350c9..386ba481 100644 --- a/backend/services/research_preferences_service.py +++ b/backend/services/research_preferences_service.py @@ -101,6 +101,32 @@ class ResearchPreferencesService: logger.error(f"Error getting research preferences: {e}") return None + def get_research_preferences_by_user_id(self, user_id: str) -> Optional[Dict[str, Any]]: + """ + Get research preferences for a user by their Clerk user ID. + + Args: + user_id: Clerk user ID (string) + + Returns: + Research preferences data or None if not found + """ + try: + # First get the onboarding session for this user + session = self.db.query(OnboardingSession).filter_by(user_id=user_id).first() + if not session: + logger.warning(f"No onboarding session found for user {user_id}") + return None + + # Then get the research preferences for that session + preferences = self.db.query(ResearchPreferences).filter_by(session_id=session.id).first() + if preferences: + return preferences.to_dict() + return None + except Exception as e: + logger.error(f"Error getting research preferences by user_id: {e}") + return None + def get_style_data_from_analysis(self, session_id: int) -> Optional[Dict[str, Any]]: """ Get style detection data from website analysis for a session. diff --git a/frontend/src/components/shared/BackgroundJobManager.tsx b/frontend/src/components/shared/BackgroundJobManager.tsx index f664ba04..9f472e4d 100644 --- a/frontend/src/components/shared/BackgroundJobManager.tsx +++ b/frontend/src/components/shared/BackgroundJobManager.tsx @@ -64,13 +64,19 @@ const BackgroundJobManager: React.FC = ({ const [loading, setLoading] = useState(false); const [selectedJob, setSelectedJob] = useState(null); const [jobDialogOpen, setJobDialogOpen] = useState(false); + const [hasRunningJobs, setHasRunningJobs] = useState(false); // Fetch user jobs const fetchJobs = useCallback(async () => { try { const response = await apiClient.get('/api/background-jobs/user-jobs?limit=10'); if (response.data.success) { - setJobs(response.data.data.jobs || []); + const newJobs = response.data.data.jobs || []; + setJobs(newJobs); + + // Update running jobs state + const runningJobs = newJobs.some((job: Job) => job.status === 'running' || job.status === 'pending'); + setHasRunningJobs(runningJobs); } } catch (error) { console.error('Error fetching jobs:', error); @@ -204,16 +210,21 @@ const BackgroundJobManager: React.FC = ({ useEffect(() => { fetchJobs(); - // Poll every 5 seconds for running jobs - const interval = setInterval(() => { - const hasRunningJobs = jobs.some(job => job.status === 'running' || job.status === 'pending'); - if (hasRunningJobs) { - fetchJobs(); - } - }, 5000); + // Only start polling if there are running jobs + let interval: NodeJS.Timeout | null = null; + + if (hasRunningJobs) { + interval = setInterval(() => { + fetchJobs().catch(console.error); + }, 5000); + } - return () => clearInterval(interval); - }, [fetchJobs, jobs]); + return () => { + if (interval) { + clearInterval(interval); + } + }; + }, [fetchJobs, hasRunningJobs]); // Only depend on hasRunningJobs state return (