arkan-api / app /main.py
masry86's picture
initial commit - arkan backend
de0f1ef
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}