klg-asutk-app/backend/app/db/session.py
Yuriy a7da43be0e apply recommendations: security, get_db, exceptions, eslint, api-client
- session: set_tenant use bound param (SQL injection fix)
- health: text('SELECT 1'), REDIS_URL from config
- deps: re-export get_db from session, use settings.ENABLE_DEV_AUTH (default False)
- routes: all get_db from app.api.deps; conftest overrides deps.get_db
- main: register exception handlers from app.api.exceptions
- next.config: enable ESLint and TypeScript checks
- .eslintrc: drop @typescript-eslint/recommended; fix no-console (logger, ws-client, regulations)
- backend/.env.example added
- frontend: export apiFetch; dashboard, profile, settings, risks use api-client
- docs/ANALYSIS_AND_RECOMMENDATIONS.md

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 21:48:58 +03:00

63 lines
2.2 KiB
Python

"""
Database session management.
Sync engine for Alembic migrations + async-compatible session for routes.
Production: use connection pool with proper limits.
"""
from sqlalchemy import create_engine, event, text
from sqlalchemy.orm import sessionmaker, Session
from app.core.config import settings
# ---------------------------------------------------------------------------
# Engine configuration
# ---------------------------------------------------------------------------
_is_sqlite = "sqlite" in (settings.database_url or "")
_connect_args = {"check_same_thread": False} if _is_sqlite else {}
engine = create_engine(
settings.database_url,
pool_pre_ping=not _is_sqlite,
connect_args=_connect_args,
# Production pool settings for multi-user
pool_size=20 if not _is_sqlite else 5,
max_overflow=10 if not _is_sqlite else 0,
pool_timeout=30,
pool_recycle=1800, # recycle connections every 30 min
echo=False,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# ---------------------------------------------------------------------------
# Multi-tenancy: set current org_id on connection (for RLS)
# ---------------------------------------------------------------------------
@event.listens_for(engine, "checkout")
def _reset_tenant(dbapi_conn, connection_record, connection_proxy):
"""Reset tenant context on connection checkout from pool."""
cursor = dbapi_conn.cursor()
try:
cursor.execute("SET LOCAL app.current_org_id = ''")
except Exception:
pass # SQLite doesn't support SET LOCAL
finally:
cursor.close()
def set_tenant(db: Session, org_id: str | None):
"""Set the current tenant for RLS policies. Uses bound parameter to avoid SQL injection."""
if org_id is not None and not _is_sqlite:
db.execute(text("SET LOCAL app.current_org_id = :org_id").bindparams(org_id=org_id or ""))
# ---------------------------------------------------------------------------
# Dependency injection
# ---------------------------------------------------------------------------
def get_db():
"""FastAPI dependency: yields a DB session, closes on exit."""
db = SessionLocal()
try:
yield db
finally:
db.close()