Subscription API and API key injection middleware added
This commit is contained in:
@@ -35,6 +35,7 @@ class DatabaseSetup:
|
|||||||
self._create_monitoring_tables()
|
self._create_monitoring_tables()
|
||||||
self._create_subscription_tables()
|
self._create_subscription_tables()
|
||||||
self._create_persona_tables()
|
self._create_persona_tables()
|
||||||
|
self._create_onboarding_tables()
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print("✅ Essential database tables created")
|
print("✅ Essential database tables created")
|
||||||
@@ -97,6 +98,22 @@ class DatabaseSetup:
|
|||||||
print(f" ⚠️ Persona tables failed: {e}")
|
print(f" ⚠️ Persona tables failed: {e}")
|
||||||
return True # Non-critical
|
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:
|
def verify_tables(self) -> bool:
|
||||||
"""Verify that essential tables exist."""
|
"""Verify that essential tables exist."""
|
||||||
import os
|
import os
|
||||||
@@ -120,7 +137,9 @@ class DatabaseSetup:
|
|||||||
essential_tables = [
|
essential_tables = [
|
||||||
'api_monitoring_logs',
|
'api_monitoring_logs',
|
||||||
'subscription_plans',
|
'subscription_plans',
|
||||||
'user_subscriptions'
|
'user_subscriptions',
|
||||||
|
'onboarding_sessions',
|
||||||
|
'persona_data'
|
||||||
]
|
]
|
||||||
|
|
||||||
existing_tables = [table for table in essential_tables if table in tables]
|
existing_tables = [table for table in essential_tables if table in tables]
|
||||||
|
|||||||
@@ -186,8 +186,12 @@ class OnboardingSummaryService:
|
|||||||
async def get_research_preferences_data(self) -> Dict[str, Any]:
|
async def get_research_preferences_data(self) -> Dict[str, Any]:
|
||||||
"""Get research preferences data for the user."""
|
"""Get research preferences data for the user."""
|
||||||
try:
|
try:
|
||||||
research_prefs_service = ResearchPreferencesService()
|
db = next(get_db())
|
||||||
return await research_prefs_service.get_research_preferences(self.user_id)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error getting research preferences data: {e}")
|
logger.error(f"Error getting research preferences data: {e}")
|
||||||
raise
|
raise
|
||||||
@@ -13,6 +13,7 @@ from functools import lru_cache
|
|||||||
from services.database import get_db
|
from services.database import get_db
|
||||||
from services.usage_tracking_service import UsageTrackingService
|
from services.usage_tracking_service import UsageTrackingService
|
||||||
from services.pricing_service import PricingService
|
from services.pricing_service import PricingService
|
||||||
|
from middleware.auth_middleware import get_current_user
|
||||||
from models.subscription_models import (
|
from models.subscription_models import (
|
||||||
APIProvider, SubscriptionPlan, UserSubscription, UsageSummary,
|
APIProvider, SubscriptionPlan, UserSubscription, UsageSummary,
|
||||||
APIProviderPricing, UsageAlert, SubscriptionTier, BillingCycle, UsageStatus
|
APIProviderPricing, UsageAlert, SubscriptionTier, BillingCycle, UsageStatus
|
||||||
@@ -30,10 +31,15 @@ _DASHBOARD_CACHE_TTL_SEC = 2.0
|
|||||||
async def get_user_usage(
|
async def get_user_usage(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
billing_period: Optional[str] = Query(None, description="Billing period (YYYY-MM)"),
|
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]:
|
) -> Dict[str, Any]:
|
||||||
"""Get comprehensive usage statistics for a user."""
|
"""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:
|
try:
|
||||||
usage_service = UsageTrackingService(db)
|
usage_service = UsageTrackingService(db)
|
||||||
stats = usage_service.get_user_usage_stats(user_id, billing_period)
|
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")
|
@router.get("/user/{user_id}/subscription")
|
||||||
async def get_user_subscription(
|
async def get_user_subscription(
|
||||||
user_id: str,
|
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]:
|
) -> Dict[str, Any]:
|
||||||
"""Get user's current subscription information."""
|
"""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:
|
try:
|
||||||
subscription = db.query(UserSubscription).filter(
|
subscription = db.query(UserSubscription).filter(
|
||||||
UserSubscription.user_id == user_id,
|
UserSubscription.user_id == user_id,
|
||||||
@@ -212,10 +223,15 @@ async def get_user_subscription(
|
|||||||
@router.get("/status/{user_id}")
|
@router.get("/status/{user_id}")
|
||||||
async def get_subscription_status(
|
async def get_subscription_status(
|
||||||
user_id: str,
|
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]:
|
) -> Dict[str, Any]:
|
||||||
"""Get simple subscription status for enforcement checks."""
|
"""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:
|
try:
|
||||||
subscription = db.query(UserSubscription).filter(
|
subscription = db.query(UserSubscription).filter(
|
||||||
UserSubscription.user_id == user_id,
|
UserSubscription.user_id == user_id,
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ async def rate_limit_middleware(request: Request, call_next):
|
|||||||
return await rate_limiter.rate_limit_middleware(request, call_next)
|
return await rate_limiter.rate_limit_middleware(request, call_next)
|
||||||
|
|
||||||
# 3. LAST REGISTERED (runs FIRST) - API key injection
|
# 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
|
# Health check endpoints using modular utilities
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
|
|||||||
@@ -41,12 +41,17 @@ class APIKeyInjectionMiddleware:
|
|||||||
if user:
|
if user:
|
||||||
# Try different possible keys for user_id
|
# Try different possible keys for user_id
|
||||||
user_id = user.get('user_id') or user.get('clerk_user_id') or user.get('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}")
|
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
|
# 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:
|
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:
|
if not user_id:
|
||||||
# No authenticated user, proceed without injection
|
# No authenticated user, proceed without injection
|
||||||
|
|||||||
@@ -488,9 +488,9 @@ async def monitoring_middleware(request: Request, call_next):
|
|||||||
# Check for authorization header with user info
|
# Check for authorization header with user info
|
||||||
elif 'authorization' in request.headers:
|
elif 'authorization' in request.headers:
|
||||||
# Auth middleware should have set request.state.user_id
|
# 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
|
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
|
# For alpha testing, use IP address as user identifier if no other ID found
|
||||||
# But only if there's no auth header (truly anonymous)
|
# But only if there's no auth header (truly anonymous)
|
||||||
|
|||||||
@@ -101,6 +101,32 @@ class ResearchPreferencesService:
|
|||||||
logger.error(f"Error getting research preferences: {e}")
|
logger.error(f"Error getting research preferences: {e}")
|
||||||
return None
|
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]]:
|
def get_style_data_from_analysis(self, session_id: int) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Get style detection data from website analysis for a session.
|
Get style detection data from website analysis for a session.
|
||||||
|
|||||||
@@ -64,13 +64,19 @@ const BackgroundJobManager: React.FC<BackgroundJobManagerProps> = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
||||||
const [jobDialogOpen, setJobDialogOpen] = useState(false);
|
const [jobDialogOpen, setJobDialogOpen] = useState(false);
|
||||||
|
const [hasRunningJobs, setHasRunningJobs] = useState(false);
|
||||||
|
|
||||||
// Fetch user jobs
|
// Fetch user jobs
|
||||||
const fetchJobs = useCallback(async () => {
|
const fetchJobs = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/api/background-jobs/user-jobs?limit=10');
|
const response = await apiClient.get('/api/background-jobs/user-jobs?limit=10');
|
||||||
if (response.data.success) {
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching jobs:', error);
|
console.error('Error fetching jobs:', error);
|
||||||
@@ -204,16 +210,21 @@ const BackgroundJobManager: React.FC<BackgroundJobManagerProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchJobs();
|
fetchJobs();
|
||||||
|
|
||||||
// Poll every 5 seconds for running jobs
|
// Only start polling if there are running jobs
|
||||||
const interval = setInterval(() => {
|
let interval: NodeJS.Timeout | null = null;
|
||||||
const hasRunningJobs = jobs.some(job => job.status === 'running' || job.status === 'pending');
|
|
||||||
if (hasRunningJobs) {
|
if (hasRunningJobs) {
|
||||||
fetchJobs();
|
interval = setInterval(() => {
|
||||||
}
|
fetchJobs().catch(console.error);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => {
|
||||||
}, [fetchJobs, jobs]);
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [fetchJobs, hasRunningJobs]); // Only depend on hasRunningJobs state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user