This commit is contained in:
Iliyan Angelov
2025-11-21 01:20:51 +02:00
parent a38ab4fa82
commit 6f85b8cf17
242 changed files with 7154 additions and 14492 deletions

View File

@@ -11,222 +11,111 @@ from slowapi.errors import RateLimitExceeded
from pathlib import Path
from datetime import datetime
import sys
# 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 . import models
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,
jwt_error_handler,
http_exception_handler,
general_exception_handler
)
logger.info(f'Starting {settings.APP_NAME} v{settings.APP_VERSION} in {settings.ENVIRONMENT} mode')
from .middleware.error_handler import validation_exception_handler, integrity_error_handler, jwt_error_handler, http_exception_handler, general_exception_handler
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)")
logger.info('Creating database tables (development mode)')
Base.metadata.create_all(bind=engine)
else:
# Ensure new 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
from .models.page_content import PageContent
logger.info("Ensuring required tables exist")
logger.info('Ensuring required tables exist')
CookiePolicy.__table__.create(bind=engine, checkfirst=True)
CookieIntegrationConfig.__table__.create(bind=engine, checkfirst=True)
PageContent.__table__.create(bind=engine, checkfirst=True)
except Exception as e:
logger.error(f"Failed to ensure required tables exist: {e}")
logger.error(f'Failed to ensure required tables exist: {e}')
from .routes import auth_routes
from .routes import privacy_routes
# Initialize FastAPI app
app = FastAPI(
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 = FastAPI(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)
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
if settings.RATE_LIMIT_ENABLED:
limiter = Limiter(
key_func=get_remote_address,
default_limits=[f"{settings.RATE_LIMIT_PER_MINUTE}/minute"]
)
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
logger.info(f'Rate limiting enabled: {settings.RATE_LIMIT_PER_MINUTE} requests/minute')
if settings.is_development:
# For development, use regex to allow any localhost port
app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"http://(localhost|127\.0\.0\.1)(:\d+)?",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
logger.info("CORS configured for development (allowing localhost)")
app.add_middleware(CORSMiddleware, allow_origin_regex='http://(localhost|127\\.0\\.0\\.1)(:\\d+)?', allow_credentials=True, allow_methods=['*'], allow_headers=['*'])
logger.info('CORS configured for development (allowing localhost)')
else:
# Production: use specific origins
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
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)
app.add_middleware(CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allow_headers=['*'])
logger.info(f'CORS configured for production with {len(settings.CORS_ORIGINS)} allowed origins')
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")
# Exception handlers
app.mount('/uploads', StaticFiles(directory=str(uploads_dir)), name='uploads')
app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(IntegrityError, integrity_error_handler)
app.add_exception_handler(JWTError, jwt_error_handler)
app.add_exception_handler(Exception, general_exception_handler)
# Enhanced Health check with database connectivity
@app.get("/health", tags=["health"])
@app.get("/api/health", tags=["health"])
async def health_check(db: Session = Depends(get_db)):
"""
Enhanced health check endpoint with database connectivity test
Available at both /health and /api/health for consistency
"""
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
@app.get('/health', tags=['health'])
@app.get('/api/health', tags=['health'])
async def health_check(db: Session=Depends(get_db)):
health_status = {'status': 'healthy', 'timestamp': datetime.utcnow().isoformat(), 'service': settings.APP_NAME, 'version': settings.APP_VERSION, 'environment': settings.ENVIRONMENT, 'checks': {'api': 'ok', 'database': 'unknown'}}
try:
from sqlalchemy import text
db.execute(text("SELECT 1"))
health_status["checks"]["database"] = "ok"
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
)
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
)
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"])
@app.get('/metrics', tags=['monitoring'])
async def metrics():
"""
Basic metrics endpoint (can be extended with Prometheus or similar)
"""
return {
"status": "success",
"service": settings.APP_NAME,
"version": settings.APP_VERSION,
"environment": settings.ENVIRONMENT,
"timestamp": datetime.utcnow().isoformat()
}
# 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)
return {'status': 'success', 'service': settings.APP_NAME, 'version': settings.APP_VERSION, 'environment': settings.ENVIRONMENT, 'timestamp': datetime.utcnow().isoformat()}
app.include_router(auth_routes.router, prefix='/api')
app.include_router(privacy_routes.router, prefix='/api')
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, invoice_routes, banner_routes,
favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes,
review_routes, user_routes, audit_routes, admin_privacy_routes,
system_settings_routes, contact_routes, page_content_routes,
home_routes, about_routes, contact_content_routes, footer_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")
app.include_router(invoice_routes.router, prefix="/api")
app.include_router(banner_routes.router, prefix="/api")
app.include_router(favorite_routes.router, prefix="/api")
app.include_router(service_routes.router, prefix="/api")
app.include_router(service_booking_routes.router, prefix="/api")
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")
app.include_router(system_settings_routes.router, prefix="/api")
app.include_router(contact_routes.router, prefix="/api")
app.include_router(home_routes.router, prefix="/api")
app.include_router(about_routes.router, prefix="/api")
app.include_router(contact_content_routes.router, prefix="/api")
app.include_router(footer_routes.router, prefix="/api")
# Versioned routes (v1)
from .routes import room_routes, booking_routes, payment_routes, invoice_routes, banner_routes, favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes, review_routes, user_routes, audit_routes, admin_privacy_routes, system_settings_routes, contact_routes, page_content_routes, home_routes, about_routes, contact_content_routes, footer_routes, chat_routes
app.include_router(room_routes.router, prefix='/api')
app.include_router(booking_routes.router, prefix='/api')
app.include_router(payment_routes.router, prefix='/api')
app.include_router(invoice_routes.router, prefix='/api')
app.include_router(banner_routes.router, prefix='/api')
app.include_router(favorite_routes.router, prefix='/api')
app.include_router(service_routes.router, prefix='/api')
app.include_router(service_booking_routes.router, prefix='/api')
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')
app.include_router(system_settings_routes.router, prefix='/api')
app.include_router(contact_routes.router, prefix='/api')
app.include_router(home_routes.router, prefix='/api')
app.include_router(about_routes.router, prefix='/api')
app.include_router(contact_content_routes.router, prefix='/api')
app.include_router(footer_routes.router, prefix='/api')
app.include_router(chat_routes.router, prefix='/api')
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)
@@ -247,52 +136,24 @@ app.include_router(home_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(about_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(contact_content_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(footer_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(page_content_routes.router, prefix="/api")
app.include_router(chat_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(page_content_routes.router, prefix='/api')
app.include_router(page_content_routes.router, prefix=settings.API_V1_PREFIX)
logger.info('All routes registered successfully')
logger.info("All routes registered successfully")
# Startup event
@app.on_event("startup")
@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}")
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")
@app.on_event('shutdown')
async def shutdown_event():
"""Run on application shutdown"""
logger.info(f"{settings.APP_NAME} shutting down gracefully")
if __name__ == "__main__":
logger.info(f'{settings.APP_NAME} shutting down gracefully')
if __name__ == '__main__':
import uvicorn
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
)
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)