import asyncio import time from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager import subprocess import sys import os import logging # Import from same directory from .routes import account, messaging, profile, push, webrtc, devices, moderation, download, keys, envelope_messaging from .models import User from .constants import OWNER_USERNAME from .utils import get_client_ip from .db import POOL_CONFIG, SessionLocal from .logging_config import access_logger # noqa: F401 - ensure loggers configured from .security.audit import log_access from .security.rate_limit import limiter from slowapi.middleware import SlowAPIMiddleware logger = logging.getLogger("uvicorn.error") def _running_in_docker() -> bool: """ Detect whether the process is running inside a Docker container. Uses presence of /.dockerenv or checking cgroup entries for docker/kubernetes. """ try: if os.path.exists("/.dockerenv"): return True # Check cgroup for docker/kubepods indicators cgroup_path = "/proc/1/cgroup" if os.path.exists(cgroup_path): with open(cgroup_path, "rt", encoding="utf-8") as f: data = f.read() if "docker" in data or "kubepods" in data or "containerd" in data: return True except Exception: pass return False @asynccontextmanager async def lifespan(app: FastAPI): # Startup - run migration in subprocess to avoid logging interference try: logger.info("Starting database migration check...") # Run migration in a separate process result = subprocess.run( [ sys.executable, "-c", "import sys; sys.path.append('.'); from migration import run_migrations; run_migrations()" ], cwd=os.path.dirname(os.path.abspath(__file__)), capture_output=True, text=True, timeout=60 ) if result.returncode != 0: logger.error(f"Migration subprocess failed with code {result.returncode}") if result.stdout: logger.error(f"Migration stdout: {result.stdout}") if result.stderr: logger.error(f"Migration stderr: {result.stderr}") else: logger.info("Database migrations completed successfully") except Exception as e: logger.error(f"Failed to run database migrations: {e}") raise try: with SessionLocal() as db: owner = db.query(User).filter(User.id == 1).first() if owner and not owner.verified: owner.verified = True db.commit() logger.info(f"Owner user '{OWNER_USERNAME}' has been verified") elif owner and owner.verified: logger.info(f"Owner user '{OWNER_USERNAME}' is already verified") else: logger.warning(f"Owner user '{OWNER_USERNAME}' not found") except Exception as e: logger.error(f"Failed to ensure owner verification: {e}") logger.info( "SQLAlchemy pool configured (size=%s, max_overflow=%s, timeout=%ss, recycle=%ss, pre_ping=%s)", POOL_CONFIG["pool_size"], POOL_CONFIG["max_overflow"], POOL_CONFIG["pool_timeout"], POOL_CONFIG["pool_recycle"], POOL_CONFIG["pool_pre_ping"], ) # Start the messaging cleanup task try: # Use absolute import to avoid import errors when package context differs from services.main.routes.messaging import messagingManager messagingManager.start_cleanup_task() logger.info("Messaging cleanup task started") except Exception as e: logger.error(f"Failed to start messaging cleanup task: {e}") # Reset all rate limits on startup to ensure clean state # This prevents rate limits from persisting across restarts try: from .security.rate_limit import reset_all_rate_limits cleared = reset_all_rate_limits() if cleared > 0: logger.info(f"Cleared {cleared} rate limit entries on startup") except Exception as e: logger.warning(f"Failed to reset rate limits on startup: {e}") # Start the rate limit cleanup task try: from .security.rate_limit import start_rate_limit_cleanup_task cleanup_task = asyncio.create_task(start_rate_limit_cleanup_task()) logger.info("Rate limit cleanup task started") except Exception as e: logger.error(f"Failed to start rate limit cleanup task: {e}") cleanup_task = None yield # Shutdown - cancel cleanup task if it exists if cleanup_task: cleanup_task.cancel() try: await cleanup_task except asyncio.CancelledError: pass # Initialize FastAPI app = FastAPI(title="FromChat", lifespan=lifespan) # Add rate limiting middleware app.state.limiter = limiter app.add_middleware(SlowAPIMiddleware) # In development (not running inside Docker), mount messaging and file_storage apps directly if not _running_in_docker(): try: # Import sub-apps from the services package and mount them to the main app # Try absolute import first, fall back to relative import try: from backend.services.messaging import main as messaging_service_module from backend.services.file_storage import main as file_storage_service_module except (ImportError, ModuleNotFoundError): # Fall back to relative imports when backend is not in path import sys import os current_dir = os.path.dirname(os.path.abspath(__file__)) services_dir = os.path.dirname(current_dir) backend_dir = os.path.dirname(services_dir) sys.path.insert(0, backend_dir) from services.messaging import main as messaging_service_module from services.file_storage import main as file_storage_service_module # Mount as sub-applications so their routes are available in-process for development app.mount("/internal/messaging", messaging_service_module.app) app.mount("/internal/file_storage", file_storage_service_module.app) logger.info("Mounted messaging and file_storage services in development mode") except Exception as e: # If mounting fails, continue without blocking startup; log for debugging logger.warning(f"Failed to mount internal services for development: {e}") @app.middleware("http") async def access_logging_middleware(request: Request, call_next): # Log incoming request and Authorization header presence for debugging auth issues try: auth_header = request.headers.get("authorization") if auth_header: short = auth_header[:20] + "..." if len(auth_header) > 20 else auth_header logger.info("Incoming request %s %s Authorization=%s", request.method, request.url.path, short) else: logger.info("Incoming request %s %s Authorization=NONE", request.method, request.url.path) except Exception: pass start = time.perf_counter() try: response = await call_next(request) except Exception as exc: duration = time.perf_counter() - start user = getattr(getattr(request, "state", None), "current_user", None) log_access( "http_error", method=request.method, path=request.url.path, status="error", user=getattr(user, "username", None), ip=get_client_ip(request), duration=f"{duration:.3f}s", error=str(exc), ) raise else: duration = time.perf_counter() - start user = getattr(getattr(request, "state", None), "current_user", None) log_access( "http_request", method=request.method, path=request.url.path, status=response.status_code, user=getattr(user, "username", None), ip=get_client_ip(request), duration=f"{duration:.3f}s", ) return response # Add security middleware (request size limiting and audit logging) try: from services.shared.middleware import add_security_middleware except ImportError: try: from backend.services.shared.middleware import add_security_middleware except ImportError: add_security_middleware = None if add_security_middleware: add_security_middleware(app) # CORS app.add_middleware( CORSMiddleware, allow_origins=[ "https://fromchat.ru", "https://beta.fromchat.ru", "https://www.fromchat.ru", "http://127.0.0.1:8301", "http://127.0.0.1:8300", "http://localhost:8301", "http://localhost:8300", ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Routes app.include_router(account.router) app.include_router(envelope_messaging.router) app.include_router(messaging.router) app.include_router(profile.router) app.include_router(push.router, prefix="/push") app.include_router(webrtc.router, prefix="/webrtc") app.include_router(devices.router, prefix="/devices") app.include_router(moderation.router) app.include_router(download.router) app.include_router(keys.router) @app.get("/key/public") async def key_public_proxy(): """ Proxy endpoint for messaging public key. In dev this calls the in-process function, in production it will proxy to the external messaging service via the keys helper. """ return await keys.get_public_key() @app.post("/key/invalidate") async def key_invalidate_proxy(): """ Proxy endpoint to invalidate messaging ephemeral key. """ return await keys.invalidate_key()