Раздел SESSION, object_type app_user, IP и user-agent запроса (`services/session_audit.py`): - LOGIN — выдача dev-токена (в транзакции запроса) и первый успешный `current_user` пользователя после старта процесса (`SessionAudit.logged_in`, в памяти процесса; параллельные первые запросы — одно событие); - LOGIN_FAILED — 401 (токен не принят: user_id пуст, причина без токена) и 403 аутентификации (не зарегистрирован / отключён / неоднозначен: email в description, user_id — если найден). `AuthenticationFailedError` (подкласс HTTPException) обрабатывается после отката транзакции запроса, запись — в отдельной транзакции `session_transaction`, поэтому остаётся в журнале. Без заголовка Authorization и 403 «недостаточно прав» — не события входа; ошибка записи аудита входа ответ не меняет. Тесты: `test_login_audit.py` (в т.ч. фиксация при откате запроса на `committed_app`); счётчики следов импорта не учитывают события SESSION. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
35 lines
1.7 KiB
Python
35 lines
1.7 KiB
Python
from collections.abc import AsyncIterator
|
||
from contextlib import asynccontextmanager
|
||
|
||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||
|
||
# from fastapi_users.db import SQLAlchemyUserDatabase, SQLAlchemyBaseUserTableUUID
|
||
from src.core.config import settings
|
||
|
||
engine = create_async_engine(
|
||
settings.OPENBAO.DATABASE.DATABASE_URL, connect_args={"server_settings": {"search_path": "dkb"}}
|
||
)
|
||
async_session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||
|
||
|
||
async def get_async_session() -> AsyncIterator[AsyncSession]:
|
||
"""Сессия запроса = одна транзакция: commit при успешном ответе, rollback при любом исключении (R-1, design #12).
|
||
|
||
В маршрутах объявляется только через `src.api.dependencies.db.SessionDep` (`scope="function"`): тогда commit
|
||
выполняется до отправки ответа, а все зависимости запроса получают одну и ту же сессию.
|
||
"""
|
||
async with async_session_maker() as session, session.begin():
|
||
yield session
|
||
|
||
|
||
@asynccontextmanager
|
||
async def session_transaction(
|
||
maker: async_sessionmaker[AsyncSession] | None = None,
|
||
) -> AsyncIterator[AsyncSession]:
|
||
"""Отдельная короткая транзакция вне сессии запроса (аудит входа): commit на выходе, rollback при исключении.
|
||
|
||
`maker` по умолчанию — `async_session_maker` приложения (читается при вызове: тесты его подменяют).
|
||
"""
|
||
async with (maker or async_session_maker)() as session, session.begin():
|
||
yield session
|