Compare commits

..

1 Commits

Author SHA1 Message Date
ي
d13cce7a46 Ensure subscription router is always mounted without duplicates 2026-03-30 07:38:19 +05:30
4 changed files with 23 additions and 61 deletions

View File

@@ -16,15 +16,22 @@ class RouterManager:
self.app = app self.app = app
self.included_routers = [] self.included_routers = []
self.failed_routers = [] self.failed_routers = []
self._included_router_names = set()
def include_router_safely(self, router, router_name: str = None) -> bool: def include_router_safely(self, router, router_name: str = None) -> bool:
"""Include a router safely with error handling.""" """Include a router safely with error handling."""
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
router_name = router_name or getattr(router, 'prefix', 'unknown')
if router_name in self._included_router_names:
if verbose:
logger.info(f"↩️ Router already included, skipping duplicate: {router_name}")
return True
try: try:
self.app.include_router(router) self.app.include_router(router)
router_name = router_name or getattr(router, 'prefix', 'unknown')
self.included_routers.append(router_name) self.included_routers.append(router_name)
self._included_router_names.add(router_name)
if verbose: if verbose:
logger.info(f"✅ Router included successfully: {router_name}") logger.info(f"✅ Router included successfully: {router_name}")
return True return True
@@ -40,19 +47,21 @@ class RouterManager:
# Import os locally to avoid UnboundLocalError if it's shadowed # Import os locally to avoid UnboundLocalError if it's shadowed
import os import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
demo_mode = os.getenv("ALWRITY_DEMO_MODE", "false").lower() == "true"
try: try:
if verbose: if verbose:
logger.info("Including core routers...") logger.info(f"Including core routers (demo_mode={demo_mode})...")
# Subscription router MUST always be included (including demo mode) so
# payment/preflight/subscription endpoints remain available.
from api.subscription import router as subscription_router
self.include_router_safely(subscription_router, "subscription")
# Component logic router # Component logic router
from api.component_logic import router as component_logic_router from api.component_logic import router as component_logic_router
self.include_router_safely(component_logic_router, "component_logic") self.include_router_safely(component_logic_router, "component_logic")
# Subscription router
from api.subscription import router as subscription_router
self.include_router_safely(subscription_router, "subscription")
# Step 3 Research router (core onboarding functionality) # Step 3 Research router (core onboarding functionality)
from api.onboarding_utils.step3_routes import router as step3_research_router from api.onboarding_utils.step3_routes import router as step3_research_router
self.include_router_safely(step3_research_router, "step3_research") self.include_router_safely(step3_research_router, "step3_research")

View File

@@ -260,6 +260,9 @@ async def onboarding_status():
# Include routers using modular utilities # Include routers using modular utilities
router_manager.include_core_routers() router_manager.include_core_routers()
# Safety net: keep subscription routes available even if core inclusion flow changes
# in special modes (e.g., demo mode). De-dup is handled by RouterManager.
router_manager.include_router_safely(subscription_router, "subscription")
router_manager.include_optional_routers() router_manager.include_optional_routers()
# Include assets serving router (must be mounted to serve generated images) # Include assets serving router (must be mounted to serve generated images)

View File

@@ -244,6 +244,9 @@ async def onboarding_status():
# Include routers using modular utilities # Include routers using modular utilities
router_manager.include_core_routers() router_manager.include_core_routers()
# Safety net: keep subscription routes available even if core inclusion flow changes
# in special modes (e.g., demo mode). De-dup is handled by RouterManager.
router_manager.include_router_safely(subscription_router, "subscription")
router_manager.include_optional_routers() router_manager.include_optional_routers()
# SEO Dashboard endpoints # SEO Dashboard endpoints

View File

@@ -8,7 +8,6 @@ IMPORTANT: This is a compatibility layer. For new code, use UserAPIKeyContext di
""" """
import os import os
import time
from fastapi import Request from fastapi import Request
from loguru import logger from loguru import logger
from typing import Callable from typing import Callable
@@ -21,62 +20,9 @@ class APIKeyInjectionMiddleware:
for the duration of each request. for the duration of each request.
""" """
# Shared across middleware instances (module currently instantiates per request)
_missing_keys_log_timestamps = {}
def __init__(self): def __init__(self):
self.original_keys = {} self.original_keys = {}
@staticmethod
def _should_skip_missing_key_warning(request: Request) -> bool:
"""
Optionally suppress missing-key warnings for non-AI/internal routes.
Controlled by API_KEY_INJECTION_SKIP_NON_AI_WARNINGS (default: true).
"""
skip_non_ai_warnings = os.getenv('API_KEY_INJECTION_SKIP_NON_AI_WARNINGS', 'true').lower() in ('1', 'true', 'yes')
if not skip_non_ai_warnings:
return False
path_lower = (request.url.path or '').lower()
return (
path_lower.startswith('/api/subscription/')
or path_lower.startswith('/api/onboarding/')
or path_lower.endswith('/status')
or path_lower.endswith('/health')
or path_lower == '/health'
or path_lower == '/status'
)
def _log_missing_keys_non_blocking(self, request: Request, user_id: str) -> None:
"""
Log missing API keys without interrupting request flow.
- Defaults to debug-level logging.
- Optional warn once-per-user-per-interval via env:
API_KEY_INJECTION_MISSING_KEYS_LOG_MODE=warn_once
API_KEY_INJECTION_MISSING_KEYS_LOG_INTERVAL_SECONDS=900
"""
try:
if self._should_skip_missing_key_warning(request):
logger.debug(f"[API Key Injection] Missing keys for user {user_id} on non-AI route; skipping warning")
return
log_mode = os.getenv('API_KEY_INJECTION_MISSING_KEYS_LOG_MODE', 'debug').lower()
if log_mode != 'warn_once':
logger.debug(f"No API keys found for user {user_id}")
return
interval_seconds = int(os.getenv('API_KEY_INJECTION_MISSING_KEYS_LOG_INTERVAL_SECONDS', '900'))
now = time.time()
last_logged_at = self._missing_keys_log_timestamps.get(user_id, 0)
if (now - last_logged_at) >= max(interval_seconds, 1):
logger.warning(f"No API keys found for user {user_id}")
self._missing_keys_log_timestamps[user_id] = now
else:
logger.debug(f"No API keys found for user {user_id} (warning suppressed by interval)")
except Exception as log_error:
# Logging should never block request processing
logger.debug(f"[API Key Injection] Failed to log missing keys state for user {user_id}: {log_error}")
async def __call__(self, request: Request, call_next: Callable): async def __call__(self, request: Request, call_next: Callable):
""" """
Inject user-specific API keys before processing request, Inject user-specific API keys before processing request,
@@ -122,7 +68,7 @@ class APIKeyInjectionMiddleware:
# Get user-specific API keys from database # Get user-specific API keys from database
with user_api_keys(user_id) as user_keys: with user_api_keys(user_id) as user_keys:
if not user_keys: if not user_keys:
self._log_missing_keys_non_blocking(request, user_id) logger.warning(f"No API keys found for user {user_id}")
return await call_next(request) return await call_next(request)
# Save original environment values # Save original environment values
@@ -174,3 +120,4 @@ async def api_key_injection_middleware(request: Request, call_next: Callable):
""" """
middleware = APIKeyInjectionMiddleware() middleware = APIKeyInjectionMiddleware()
return await middleware(request, call_next) return await middleware(request, call_next)