from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from contextlib import asynccontextmanager import logging import os from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from .config import get_settings from .database import init_db, SessionLocal from .routes import router from .pypi_proxy import router as pypi_router from .seed import seed_database from .auth import create_default_admin from .rate_limit import limiter from .pypi_cache_worker import init_cache_worker_pool, shutdown_cache_worker_pool settings = get_settings() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): # Startup: initialize database init_db() # Create default admin user if no users exist db = SessionLocal() try: admin = create_default_admin(db) if admin: logger.warning( "Default admin user created with username 'admin' and password 'changeme123'. " "CHANGE THIS PASSWORD IMMEDIATELY!" ) finally: db.close() # Seed test data in development mode if settings.is_development: logger.info(f"Running in {settings.env} mode - checking for seed data") db = SessionLocal() try: seed_database(db) finally: db.close() else: logger.info(f"Running in {settings.env} mode - skipping seed data") # Initialize PyPI cache worker pool init_cache_worker_pool() yield # Shutdown: cleanup shutdown_cache_worker_pool() app = FastAPI( title="Orchard", description="Content-Addressable Storage System", version="1.0.0", lifespan=lifespan, ) # Set up rate limiting app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # Include API routes app.include_router(router) app.include_router(pypi_router) # Serve static files (React build) if the directory exists static_dir = os.path.join(os.path.dirname(__file__), "..", "..", "frontend", "dist") if os.path.exists(static_dir): app.mount( "/assets", StaticFiles(directory=os.path.join(static_dir, "assets")), name="assets", ) @app.get("/") async def serve_spa(): return FileResponse(os.path.join(static_dir, "index.html")) # Catch-all for SPA routing (must be last) @app.get("/{full_path:path}") async def serve_spa_routes(full_path: str): # Don't catch API routes or health endpoint if full_path.startswith("api/") or full_path.startswith("health"): from fastapi import HTTPException raise HTTPException(status_code=404, detail="Not found") # Check if requesting a static file from dist root (favicon, etc.) static_file_path = os.path.join(static_dir, full_path) if os.path.isfile(static_file_path) and not full_path.startswith("."): return FileResponse(static_file_path) # Serve SPA for all other routes (including /project/*) index_path = os.path.join(static_dir, "index.html") if os.path.exists(index_path): return FileResponse(index_path) from fastapi import HTTPException raise HTTPException(status_code=404, detail="Not found")