Public source file

backend/app/main.py

Documentation home
101 lines3,495 bytesread-only generated view
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
from pathlib import Path

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from contextlib import asynccontextmanager
from time import perf_counter
from backend.app.api.routes import router
from backend.app.core.config import config, DEFAULT_SECRET_KEY
from backend.app.core.log_setup import setup_logging
from backend.app.core.request_id import safe_request_id

# Set up logging
logger = setup_logging()
_STATIC_DIR = Path(__file__).resolve().parent / "static"
_LANDING_HTML_PATH = _STATIC_DIR / "index.html"

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Handle startup and shutdown events"""
    # Startup
    logger.info("Application startup")

    # Connect to Redis for replay attack prevention
    from backend.app.services.redis_service import redis_service
    if config.REDIS_STARTUP_ENABLED:
        try:
            await redis_service.connect()
            logger.info("Redis connected successfully")
        except Exception as e:
            logger.error(f"Failed to connect to Redis: {str(e)}")
            logger.warning("Replay attack prevention will not be available")
    else:
        logger.info("Redis startup is disabled; verifier will use local fallback services")

    yield

    # Shutdown
    logger.info("Application shutdown")
    try:
        await redis_service.disconnect()
        logger.info("Redis disconnected")
    except Exception as e:
        logger.error(f"Error disconnecting from Redis: {str(e)}")

# Create FastAPI application
app = FastAPI(
    title="QR Code Verification API",
    description="API for generating and verifying digitally signed QR codes",
    version="0.1.0",
    lifespan=lifespan
)

if config.SECRET_KEY == DEFAULT_SECRET_KEY:
    logger.warning(
        "Application is using the default SECRET_KEY. Set a unique value before "
        "deploying beyond local development."
    )

# Configure CORS only when explicit origins are provided. The browser lab is
# same-origin by default and does not need wildcard cross-origin access.
if config.CORS_ORIGINS:
    allow_all_origins = "*" in config.CORS_ORIGINS
    app.add_middleware(
        CORSMiddleware,  # type: ignore
        allow_origins=["*"] if allow_all_origins else config.CORS_ORIGINS,
        allow_credentials=False if allow_all_origins else config.CORS_ALLOW_CREDENTIALS,
        allow_methods=["*"],
        allow_headers=["*"],
    )


@app.middleware("http")
async def add_security_headers(request, call_next):  # type: ignore[no-untyped-def]
    request_id = safe_request_id(request.headers.get("X-Request-ID"))
    request.state.request_id = request_id
    start = perf_counter()
    response = await call_next(request)
    duration_ms = (perf_counter() - start) * 1000
    logger.info(
        "request_id=%s method=%s path=%s status=%s duration_ms=%.2f",
        request_id,
        request.method,
        request.url.path,
        response.status_code,
        duration_ms,
    )
    response.headers["X-Request-ID"] = request_id
    response.headers.setdefault("X-Content-Type-Options", "nosniff")
    response.headers.setdefault("X-Frame-Options", "DENY")
    response.headers.setdefault("Referrer-Policy", "no-referrer")
    return response


@app.get("/", include_in_schema=False)
async def get_root_landing_page() -> FileResponse:
    return FileResponse(_LANDING_HTML_PATH, headers={"Cache-Control": "no-store"})

# Include API routes
app.include_router(router)