personal-lifecycle-tracker / backend / app / main.py
main.py
Raw
from __future__ import annotations

import os
from fastapi import FastAPI, HTTPException, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

from app.routers import auth, dev, groups, health, items, reminders, users
from app.telemetry import init_opentelemetry, setup_telemetry_middleware


def create_app() -> FastAPI:
    app = FastAPI(
        title="Personal Lifecycle Tracker API",
        description=(
            "Production REST API for Personal Lifecycle Tracker. "
            "Manages Renewals, Maintenance, and Warranties for independent users and shared groups, "
            "including action summaries and scheduled daily morning email digests."
        ),
        version="1.0.0",
    )

    init_opentelemetry(app)
    setup_telemetry_middleware(app)

    raw_origins = os.getenv("CORS_ORIGINS", "")
    origins = [o.strip() for o in raw_origins.split(",") if o.strip()]
    is_lambda = bool(os.getenv("AWS_LAMBDA_FUNCTION_NAME") or os.getenv("AWS_EXECUTION_ENV"))

    default_dev_origins = [
        "http://localhost:3000",
        "http://localhost:3003",
        "http://localhost:8000",
        "http://localhost:8003",
        "http://localhost:8088",
        "http://127.0.0.1:3000",
        "http://127.0.0.1:3003",
        "http://127.0.0.1:8088",
    ]
    allowed_origins = origins if origins else default_dev_origins

    # In AWS Lambda, the Lambda Function URL natively attaches CORS headers.
    # Avoid duplicate headers unless specific origins are explicitly configured.
    if not is_lambda or raw_origins:
        app.add_middleware(
            CORSMiddleware,
            allow_origins=allowed_origins,
            allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$",
            allow_credentials=True,
            allow_methods=["*"],
            allow_headers=["*"],
        )

    @app.exception_handler(HTTPException)
    async def http_exception_handler(request: Request, exc: HTTPException):
        detail = exc.detail
        if isinstance(detail, dict) and "error" in detail:
            msg = detail["error"]
        elif isinstance(detail, dict) and "detail" in detail:
            msg = detail["detail"]
        else:
            msg = str(detail)
        return JSONResponse(
            status_code=exc.status_code,
            content={"error": msg},
        )

    @app.exception_handler(RequestValidationError)
    async def validation_exception_handler(request: Request, exc: RequestValidationError):
        errors = exc.errors()
        msg = errors[0].get("msg", "Validation error") if errors else "Validation error"
        return JSONResponse(
            status_code=status.HTTP_400_BAD_REQUEST,
            content={"error": msg},
        )

    # Register routers
    app.include_router(health.router)
    app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
    app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
    app.include_router(items.router, prefix="/api/v1/items", tags=["items"])
    app.include_router(groups.router, prefix="/api/v1/groups", tags=["groups"])
    app.include_router(reminders.router, prefix="/api/v1/reminders", tags=["reminders"])
    app.include_router(dev.router, prefix="/api/v1/dev", tags=["dev"])

    # Eagerly warm DatabaseStore during container boot
    try:
        from app.store import get_store
        get_store()
    except Exception as e:
        # Avoid crashing container boot if database is temporarily unreachable
        pass

    return app


app = create_app()