172 lines
5.6 KiB
Python
172 lines
5.6 KiB
Python
import logging
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.openapi.docs import get_swagger_ui_html
|
|
from fastapi.openapi.utils import get_openapi
|
|
import time
|
|
|
|
# Import API routers
|
|
from src.api.v1 import teams, users, images, auth, search
|
|
|
|
# Import configuration and database
|
|
from src.config.config import settings
|
|
from src.utils.logging import setup_logging
|
|
from src.db import db
|
|
from src.db.providers.firestore_provider import firestore_db
|
|
from src.db.repositories import init_repositories
|
|
|
|
# Setup logging
|
|
setup_logging()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Create FastAPI app
|
|
app = FastAPI(
|
|
title=settings.PROJECT_NAME,
|
|
description="API for securely storing, organizing, and retrieving images",
|
|
version="1.0.0",
|
|
docs_url=None,
|
|
redoc_url=None,
|
|
openapi_url="/api/v1/openapi.json"
|
|
)
|
|
|
|
# Add request logging middleware
|
|
@app.middleware("http")
|
|
async def log_requests(request: Request, call_next):
|
|
start_time = time.time()
|
|
|
|
# Log incoming request details
|
|
logger.info(f"Incoming request: {request.method} {request.url}")
|
|
logger.info(f"Headers: {dict(request.headers)}")
|
|
logger.info(f"Client: {request.client}")
|
|
|
|
response = await call_next(request)
|
|
|
|
process_time = time.time() - start_time
|
|
logger.info(f"Request completed in {process_time:.4f}s with status {response.status_code}")
|
|
|
|
return response
|
|
|
|
# Connect to database
|
|
try:
|
|
db.connect_to_database()
|
|
logger.info("Database connection initialized")
|
|
|
|
# Initialize the Firestore provider
|
|
firestore_db.connect()
|
|
logger.info("Firestore provider initialized")
|
|
|
|
# Initialize repositories
|
|
init_repositories()
|
|
logger.info("Repositories initialized")
|
|
except Exception as e:
|
|
logger.error(f"Failed to connect to database: {e}", exc_info=True)
|
|
# We'll continue without database for Swagger UI to work, but operations will fail
|
|
|
|
# Set up CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.CORS_ORIGINS,
|
|
allow_credentials=True,
|
|
allow_methods=settings.CORS_METHODS,
|
|
allow_headers=settings.CORS_HEADERS,
|
|
expose_headers=settings.CORS_EXPOSE_HEADERS,
|
|
max_age=settings.CORS_MAX_AGE,
|
|
)
|
|
|
|
# Include API routers
|
|
app.include_router(auth.router, prefix="/api/v1")
|
|
app.include_router(teams.router, prefix="/api/v1")
|
|
app.include_router(users.router, prefix="/api/v1")
|
|
app.include_router(images.router, prefix="/api/v1")
|
|
app.include_router(search.router, prefix="/api/v1")
|
|
|
|
# Custom exception handler
|
|
@app.exception_handler(Exception)
|
|
async def general_exception_handler(request: Request, exc: Exception):
|
|
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
|
# Include more details for easier debugging
|
|
error_details = {
|
|
"detail": "Internal server error",
|
|
"type": type(exc).__name__,
|
|
"path": request.url.path
|
|
}
|
|
# Only include exception message in development mode
|
|
if settings.ENVIRONMENT == "development":
|
|
error_details["message"] = str(exc)
|
|
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content=error_details
|
|
)
|
|
|
|
# Custom Swagger UI with API key authentication
|
|
@app.get("/docs", include_in_schema=False)
|
|
async def custom_swagger_ui_html():
|
|
return get_swagger_ui_html(
|
|
openapi_url=app.openapi_url,
|
|
title=f"{app.title} - Swagger UI",
|
|
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
|
|
swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js",
|
|
swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css",
|
|
)
|
|
|
|
# Custom OpenAPI schema to include API key auth
|
|
def custom_openapi():
|
|
if app.openapi_schema:
|
|
return app.openapi_schema
|
|
|
|
openapi_schema = get_openapi(
|
|
title=app.title,
|
|
version=app.version,
|
|
description=app.description,
|
|
routes=app.routes,
|
|
)
|
|
|
|
# Initialize components if not present
|
|
if "components" not in openapi_schema:
|
|
openapi_schema["components"] = {}
|
|
|
|
# Add API key security scheme
|
|
openapi_schema["components"]["securitySchemes"] = {
|
|
"ApiKeyAuth": {
|
|
"type": "apiKey",
|
|
"in": "header",
|
|
"name": "X-API-Key"
|
|
}
|
|
}
|
|
|
|
# Ensure schemas are included
|
|
if "schemas" not in openapi_schema["components"]:
|
|
openapi_schema["components"]["schemas"] = {}
|
|
|
|
# Note: Authentication is now handled properly in individual route modules
|
|
# Public endpoints (auth, users, teams) don't require authentication
|
|
# Protected endpoints (images, search) require API key authentication
|
|
|
|
app.openapi_schema = openapi_schema
|
|
return app.openapi_schema
|
|
|
|
app.openapi = custom_openapi
|
|
|
|
@app.get("/health", include_in_schema=False)
|
|
async def health_check():
|
|
"""Health check endpoint for monitoring and client connectivity tests"""
|
|
return {"status": "healthy", "message": "API is running"}
|
|
|
|
@app.get("/", include_in_schema=False)
|
|
async def root():
|
|
return {"message": "Welcome to the Image Management API. Please see /docs for API documentation."}
|
|
|
|
# Shutdown handler to close database connections
|
|
@app.on_event("shutdown")
|
|
async def shutdown_event():
|
|
logger.info("Application shutting down")
|
|
db.close_database_connection()
|
|
# Also disconnect the Firestore provider
|
|
firestore_db.disconnect()
|
|
logger.info("Firestore provider disconnected")
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) |