update
This commit is contained in:
@@ -1,17 +1,30 @@
|
||||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi import FastAPI, Request, HTTPException, Depends, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.exc import IntegrityError, OperationalError
|
||||
from jose.exceptions import JWTError
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import sys
|
||||
|
||||
from .config.database import engine, Base
|
||||
# Import configuration and logging FIRST
|
||||
from .config.settings import settings
|
||||
from .config.logging_config import setup_logging, get_logger
|
||||
from .config.database import engine, Base, get_db
|
||||
from . import models # noqa: F401 - ensure models are imported so tables are created
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Setup logging before anything else
|
||||
logger = setup_logging()
|
||||
|
||||
logger.info(f"Starting {settings.APP_NAME} v{settings.APP_VERSION} in {settings.ENVIRONMENT} mode")
|
||||
|
||||
# Import middleware
|
||||
from .middleware.error_handler import (
|
||||
validation_exception_handler,
|
||||
integrity_error_handler,
|
||||
@@ -19,38 +32,65 @@ from .middleware.error_handler import (
|
||||
http_exception_handler,
|
||||
general_exception_handler
|
||||
)
|
||||
# Create database tables
|
||||
Base.metadata.create_all(bind=engine)
|
||||
from .middleware.request_id import RequestIDMiddleware
|
||||
from .middleware.security import SecurityHeadersMiddleware
|
||||
from .middleware.timeout import TimeoutMiddleware
|
||||
from .middleware.cookie_consent import CookieConsentMiddleware
|
||||
|
||||
# Create database tables (for development, migrations should be used in production)
|
||||
if settings.is_development:
|
||||
logger.info("Creating database tables (development mode)")
|
||||
Base.metadata.create_all(bind=engine)
|
||||
else:
|
||||
# Ensure new cookie-related tables exist even if full migrations haven't been run yet.
|
||||
try:
|
||||
from .models.cookie_policy import CookiePolicy
|
||||
from .models.cookie_integration_config import CookieIntegrationConfig
|
||||
logger.info("Ensuring cookie-related tables exist")
|
||||
CookiePolicy.__table__.create(bind=engine, checkfirst=True)
|
||||
CookieIntegrationConfig.__table__.create(bind=engine, checkfirst=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to ensure cookie tables exist: {e}")
|
||||
|
||||
from .routes import auth_routes
|
||||
from .routes import privacy_routes
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="Hotel Booking API",
|
||||
description="Hotel booking backend API",
|
||||
version="1.0.0"
|
||||
title=settings.APP_NAME,
|
||||
description="Enterprise-grade Hotel Booking API",
|
||||
version=settings.APP_VERSION,
|
||||
docs_url="/api/docs" if not settings.is_production else None,
|
||||
redoc_url="/api/redoc" if not settings.is_production else None,
|
||||
openapi_url="/api/openapi.json" if not settings.is_production else None
|
||||
)
|
||||
|
||||
# Add middleware in order (order matters!)
|
||||
# 1. Request ID middleware (first to add request ID)
|
||||
app.add_middleware(RequestIDMiddleware)
|
||||
|
||||
# 2. Cookie consent middleware (makes consent available on request.state)
|
||||
app.add_middleware(CookieConsentMiddleware)
|
||||
|
||||
# 3. Timeout middleware
|
||||
if settings.REQUEST_TIMEOUT > 0:
|
||||
app.add_middleware(TimeoutMiddleware)
|
||||
|
||||
# 4. Security headers middleware
|
||||
app.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
# Rate limiting
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
if settings.RATE_LIMIT_ENABLED:
|
||||
limiter = Limiter(
|
||||
key_func=get_remote_address,
|
||||
default_limits=[f"{settings.RATE_LIMIT_PER_MINUTE}/minute"]
|
||||
)
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
logger.info(f"Rate limiting enabled: {settings.RATE_LIMIT_PER_MINUTE} requests/minute")
|
||||
|
||||
# CORS configuration
|
||||
# Allow multiple origins for development
|
||||
client_url = os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
allowed_origins = [
|
||||
client_url,
|
||||
"http://localhost:5173", # Vite default
|
||||
"http://localhost:3000", # Alternative port
|
||||
"http://localhost:5174", # Vite alternative
|
||||
"http://127.0.0.1:5173",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://127.0.0.1:5174",
|
||||
]
|
||||
|
||||
# In development, allow all localhost origins using regex
|
||||
if os.getenv("ENVIRONMENT", "development") == "development":
|
||||
if settings.is_development:
|
||||
# For development, use regex to allow any localhost port
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
@@ -59,18 +99,20 @@ if os.getenv("ENVIRONMENT", "development") == "development":
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
logger.info("CORS configured for development (allowing localhost)")
|
||||
else:
|
||||
# Production: use specific origins
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=allowed_origins,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
logger.info(f"CORS configured for production with {len(settings.CORS_ORIGINS)} allowed origins")
|
||||
|
||||
# Serve static files (uploads)
|
||||
uploads_dir = Path(__file__).parent.parent / "uploads"
|
||||
uploads_dir = Path(__file__).parent.parent / settings.UPLOAD_DIR
|
||||
uploads_dir.mkdir(exist_ok=True)
|
||||
app.mount("/uploads", StaticFiles(directory=str(uploads_dir)), name="uploads")
|
||||
|
||||
@@ -81,25 +123,82 @@ app.add_exception_handler(IntegrityError, integrity_error_handler)
|
||||
app.add_exception_handler(JWTError, jwt_error_handler)
|
||||
app.add_exception_handler(Exception, general_exception_handler)
|
||||
|
||||
# Health check
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
# Enhanced Health check with database connectivity
|
||||
@app.get("/health", tags=["health"])
|
||||
async def health_check(db: Session = Depends(get_db)):
|
||||
"""
|
||||
Enhanced health check endpoint with database connectivity test
|
||||
"""
|
||||
health_status = {
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"service": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION,
|
||||
"environment": settings.ENVIRONMENT,
|
||||
"checks": {
|
||||
"api": "ok",
|
||||
"database": "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
# Check database connectivity
|
||||
try:
|
||||
from sqlalchemy import text
|
||||
db.execute(text("SELECT 1"))
|
||||
health_status["checks"]["database"] = "ok"
|
||||
except OperationalError as e:
|
||||
health_status["status"] = "unhealthy"
|
||||
health_status["checks"]["database"] = "error"
|
||||
health_status["error"] = str(e)
|
||||
logger.error(f"Database health check failed: {str(e)}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
content=health_status
|
||||
)
|
||||
except Exception as e:
|
||||
health_status["status"] = "unhealthy"
|
||||
health_status["checks"]["database"] = "error"
|
||||
health_status["error"] = str(e)
|
||||
logger.error(f"Health check failed: {str(e)}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
content=health_status
|
||||
)
|
||||
|
||||
return health_status
|
||||
|
||||
|
||||
# Metrics endpoint (basic)
|
||||
@app.get("/metrics", tags=["monitoring"])
|
||||
async def metrics():
|
||||
"""
|
||||
Basic metrics endpoint (can be extended with Prometheus or similar)
|
||||
"""
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Server is running",
|
||||
"timestamp": __import__("datetime").datetime.utcnow().isoformat()
|
||||
"service": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION,
|
||||
"environment": settings.ENVIRONMENT,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
# API Routes
|
||||
# API Routes with versioning
|
||||
# Legacy routes (maintain backward compatibility)
|
||||
app.include_router(auth_routes.router, prefix="/api")
|
||||
app.include_router(privacy_routes.router, prefix="/api")
|
||||
|
||||
# Versioned API routes (v1)
|
||||
app.include_router(auth_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(privacy_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
|
||||
# Import and include other routes
|
||||
from .routes import (
|
||||
room_routes, booking_routes, payment_routes, banner_routes,
|
||||
favorite_routes, service_routes, promotion_routes, report_routes,
|
||||
review_routes, user_routes
|
||||
review_routes, user_routes, audit_routes, admin_privacy_routes
|
||||
)
|
||||
|
||||
# Legacy routes (maintain backward compatibility)
|
||||
app.include_router(room_routes.router, prefix="/api")
|
||||
app.include_router(booking_routes.router, prefix="/api")
|
||||
app.include_router(payment_routes.router, prefix="/api")
|
||||
@@ -110,12 +209,66 @@ app.include_router(promotion_routes.router, prefix="/api")
|
||||
app.include_router(report_routes.router, prefix="/api")
|
||||
app.include_router(review_routes.router, prefix="/api")
|
||||
app.include_router(user_routes.router, prefix="/api")
|
||||
app.include_router(audit_routes.router, prefix="/api")
|
||||
app.include_router(admin_privacy_routes.router, prefix="/api")
|
||||
|
||||
# Note: FastAPI automatically handles 404s for unmatched routes
|
||||
# This handler is kept for custom 404 responses but may not be needed
|
||||
# Versioned routes (v1)
|
||||
app.include_router(room_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(booking_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(payment_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(banner_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(favorite_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(service_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(promotion_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(report_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(review_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(user_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(audit_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(admin_privacy_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
|
||||
logger.info("All routes registered successfully")
|
||||
|
||||
# Startup event
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Run on application startup"""
|
||||
logger.info(f"{settings.APP_NAME} started successfully")
|
||||
logger.info(f"Environment: {settings.ENVIRONMENT}")
|
||||
logger.info(f"Debug mode: {settings.DEBUG}")
|
||||
logger.info(f"API version: {settings.API_V1_PREFIX}")
|
||||
|
||||
# Shutdown event
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
"""Run on application shutdown"""
|
||||
logger.info(f"{settings.APP_NAME} shutting down gracefully")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
port = int(os.getenv("PORT", 3000))
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)
|
||||
from pathlib import Path
|
||||
|
||||
# Only watch the src directory to avoid watching logs, uploads, etc.
|
||||
base_dir = Path(__file__).parent.parent
|
||||
src_dir = str(base_dir / "src")
|
||||
|
||||
uvicorn.run(
|
||||
"src.main:app",
|
||||
host=settings.HOST,
|
||||
port=settings.PORT,
|
||||
reload=settings.is_development,
|
||||
log_level=settings.LOG_LEVEL.lower(),
|
||||
reload_dirs=[src_dir] if settings.is_development else None,
|
||||
reload_excludes=[
|
||||
"*.log",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.pyd",
|
||||
"__pycache__",
|
||||
"**/__pycache__/**",
|
||||
"*.db",
|
||||
"*.sqlite",
|
||||
"*.sqlite3"
|
||||
],
|
||||
reload_delay=0.5 # Increase delay to reduce false positives
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user