| 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 |
|
|
| |
| app.state.limiter = limiter |
| app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) |
|
|
| |
| app.add_exception_handler(AppException, app_exception_handler) |
| app.add_exception_handler(Exception, generic_exception_handler) |
|
|
| |
| 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"], |
| ) |
|
|
|
|
|
|
| |
| |
| |
| app.add_middleware( |
| CSRFMiddleware, |
| exempt_paths=[ |
| "/api/docs", |
| "/api/redoc", |
| "/api/openapi.json", |
| "/health", |
| "/api/v1/auth/login", |
| "/api/v1/bookings/public", |
| ], |
| ) |
|
|
|
|
| |
| 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} |