"""FastAPI application factory for SneakySwole. Creates and configures the FastAPI app with routes, templates, static files, and structured logging. """ import os import secrets from pathlib import Path import structlog from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from sqlmodel import SQLModel, Session from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.requests import Request as StarletteRequest from starlette.responses import Response from fastapi.responses import RedirectResponse from app.models.user import User from app.config import get_settings from app.database import get_engine, get_db_session from app.logging_config import setup_logging from app.routes.auth import router as auth_router from app.routes.exercises import router as exercises_router from app.routes.history import router as history_router from app.routes.health import router as health_router from app.routes.pages import router as pages_router from app.routes.profiles import router as profiles_router from app.routes.logging import router as logging_router from app.routes.workouts import router as workouts_router from app.routes.dashboard import router as dashboard_router from app.routes.schedule import router as schedule_router from app.services.seed_service import SeedService from app.services.auth_service import AuthService from app.services.user_service import UserService from app.utils.auth import SESSION_COOKIE_NAME, NotAuthenticatedError logger = structlog.get_logger(__name__) # Template and static file directories _BASE_DIR = Path(__file__).resolve().parent TEMPLATES_DIR = _BASE_DIR / "templates" STATIC_DIR = _BASE_DIR / "static" class NavContextMiddleware(BaseHTTPMiddleware): """Injects admin, profiles, and active_profile into request.state for templates.""" async def dispatch(self, request: StarletteRequest, call_next: RequestResponseEndpoint) -> Response: request.state.admin = None request.state.profiles = [] request.state.active_profile = None token = request.cookies.get(SESSION_COOKIE_NAME) if token and hasattr(request.app.state, "engine"): try: with Session(request.app.state.engine) as session: secret_key = getattr(request.app.state, "secret_key", "") auth_service = AuthService(session, secret_key=secret_key) user_id = auth_service.validate_session_token(token) if user_id: admin = session.get(User, user_id) if admin and admin.is_admin: request.state.admin = admin user_service = UserService(session) request.state.profiles = user_service.list_users(exclude_admin=True) profile_id = request.cookies.get("active_profile_id") if profile_id and profile_id.isdigit(): request.state.active_profile = user_service.get_user_by_id(int(profile_id)) except Exception: pass return await call_next(request) def create_app() -> FastAPI: """Create and configure the FastAPI application. Returns: A fully configured FastAPI application instance. """ settings = get_settings() setup_logging(log_level=settings.app_log_level) app = FastAPI( title="SneakySwole", description="Open-source workout tracking and programming", version="0.1.0", ) # Redirect unauthenticated requests to login @app.exception_handler(NotAuthenticatedError) async def _not_authenticated_handler(request, exc): return RedirectResponse(url="/login", status_code=302) # Mount static files app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") # Jinja2 templates (available to routes via request.state or dependency) templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) app.state.templates = templates # Secret key for session signing app.state.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32)) # Nav context middleware — injects admin/profiles/active_profile into request.state app.add_middleware(NavContextMiddleware) # Register route modules app.include_router(auth_router) app.include_router(exercises_router) app.include_router(health_router) app.include_router(history_router) app.include_router(logging_router) app.include_router(pages_router) app.include_router(profiles_router) app.include_router(workouts_router) app.include_router(dashboard_router) app.include_router(schedule_router) # Database setup engine = get_engine(settings.database_url) SQLModel.metadata.create_all(engine) app.state.engine = engine # DB session dependency for routes def _get_session(): yield from get_db_session(engine) app.dependency_overrides[get_db_session] = _get_session # Seed database on startup @app.on_event("startup") async def on_startup(): with Session(engine) as session: seed_service = SeedService(session) seed_service.seed_all() logger.info("app_started", environment=settings.app_env) return app # Uvicorn entry point app = create_app()