106 lines
3.2 KiB
Python
106 lines
3.2 KiB
Python
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
|
|
|
|
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")
|
|
|
|
yield
|
|
# Shutdown: cleanup if needed
|
|
|
|
|
|
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")
|