from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from sqlalchemy import select from app.core.limiter import limiter from app.core.config import settings from app.core.exceptions import AppException, app_exception_handler, generic_exception_handler from app.core.logging import setup_logging from app.core.security import get_password_hash from app.database import AsyncSessionLocal, engine from app.middleware.csrf import CSRFMiddleware from app.models.base import Base from app.models.user import User import app.models.booking from app.routers import admin, auth, bookings, news @asynccontextmanager async def lifespan(app: FastAPI): setup_logging() async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async with AsyncSessionLocal() as session: result = await session.execute( select(User).where(User.username == settings.ADMIN_USERNAME) ) if not result.scalar_one_or_none(): session.add( User( username=settings.ADMIN_USERNAME, password_hash=get_password_hash(settings.ADMIN_PASSWORD), role="admin", ) ) await session.commit() yield await engine.dispose() app = FastAPI( title=settings.APP_NAME, version="1.0.0" if settings.DEBUG else "0.0.0", docs_url="/api/docs" if settings.DEBUG else None, redoc_url="/api/redoc" if settings.DEBUG else None, openapi_url="/api/openapi.json" if settings.DEBUG else None, lifespan=lifespan, ) @app.middleware("http") async def add_security_headers(request: Request, call_next): response = await call_next(request) response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["X-XSS-Protection"] = "1; mode=block" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()" if settings.ENV == "production": response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" return response # ─── Rate limiter ───────────────────────────────────────────────────────────── app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # ─── Exception handlers ─────────────────────────────────────────────────────── app.add_exception_handler(AppException, app_exception_handler) app.add_exception_handler(Exception, generic_exception_handler) # ─── Middleware ─────────────────────────────────────────────────────────────── app.add_middleware( CORSMiddleware, allow_origins=settings.allowed_origins_list, allow_credentials=True, allow_methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS"], allow_headers=["Content-Type", "Authorization", "X-CSRF-Token"], ) # ─── تضيق الـ CSRF exemption ───────────────────────────────────────────────── # main.py # ✅ بعد الإصلاح app.add_middleware( CSRFMiddleware, exempt_paths=[ "/api/docs", "/api/redoc", "/api/openapi.json", "/health", "/api/v1/auth/login", "/api/v1/bookings/public", # ← فقط هذا المسار العام ], ) # ─── Routers (/api/v1) ──────────────────────────────────────────────────────── app.include_router(auth.router, prefix="/api/v1") app.include_router(bookings.router, prefix="/api/v1") app.include_router(admin.router, prefix="/api/v1") app.include_router(news.router, prefix="/api/v1") @app.get("/health", tags=["Health"]) async def health_check(): return {"status": "ok", "app": settings.APP_NAME, "env": settings.ENV}