Files
remnawave-bedolaga-telegram…/app/services/external_admin_service.py
T
Fringg 1f0fef114b refactor: complete structlog migration with contextvars, kwargs, and logging hardening
- Add ContextVarsMiddleware for automatic user_id/chat_id/username binding
  via structlog contextvars (aiogram) and http_method/http_path (FastAPI)
- Use bound_contextvars() context manager instead of clear_contextvars()
  to safely restore previous state instead of wiping all context
- Register ContextVarsMiddleware as outermost middleware (before GlobalError)
  so all error logs include user context
- Replace structlog.get_logger() with structlog.get_logger(__name__) across
  270 calls in 265 files for meaningful logger names
- Switch wrapper_class from BoundLogger to make_filtering_bound_logger()
  for pre-processor level filtering (performance optimization)
- Migrate 1411 %-style positional arg logger calls to structlog kwargs
  style across 161 files via AST script
- Migrate log_rotation_service.py from stdlib logging to structlog
- Add payment module prefixes to TelegramNotifierProcessor.IGNORED_LOGGER_PREFIXES
  and ExcludePaymentFilter.PAYMENT_MODULES to prevent payment data leaking
  to Telegram notifications and general log files
- Fix LoggingMiddleware: add from_user null-safety for channel posts,
  switch time.time() to time.monotonic() for duration measurement
- Remove duplicate logger assignments in purchase.py, config.py,
  inline.py, and admin/payments.py
2026-02-16 09:18:12 +03:00

147 lines
6.7 KiB
Python

"""Утилиты для синхронизации токена внешней админки."""
from __future__ import annotations
import structlog
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from app.config import settings
from app.database.database import AsyncSessionLocal
from app.database.models import SystemSetting
from app.services.system_settings_service import (
ReadOnlySettingError,
bot_configuration_service,
)
logger = structlog.get_logger(__name__)
async def ensure_external_admin_token(
bot_username: str | None,
bot_id: int | None,
) -> str | None:
"""Генерирует и сохраняет токен внешней админки, если требуется."""
username_raw = (bot_username or '').strip()
if not username_raw:
logger.warning(
'⚠️ Не удалось обеспечить токен внешней админки: username бота отсутствует',
)
return None
normalized_username = username_raw.lstrip('@').lower()
if not normalized_username:
logger.warning(
'⚠️ Не удалось обеспечить токен внешней админки: username пустой после нормализации',
)
return None
try:
token = settings.build_external_admin_token(normalized_username)
except Exception as error: # pragma: no cover - защитный блок
logger.error('❌ Ошибка генерации токена внешней админки', error=error)
return None
try:
async with AsyncSessionLocal() as session:
result = await session.execute(
select(SystemSetting.key, SystemSetting.value).where(
SystemSetting.key.in_(['EXTERNAL_ADMIN_TOKEN', 'EXTERNAL_ADMIN_TOKEN_BOT_ID'])
)
)
rows = dict(result.all())
existing_token = rows.get('EXTERNAL_ADMIN_TOKEN')
existing_bot_id_raw = rows.get('EXTERNAL_ADMIN_TOKEN_BOT_ID')
existing_bot_id: int | None = None
if existing_bot_id_raw is not None:
try:
existing_bot_id = int(existing_bot_id_raw)
except (TypeError, ValueError): # pragma: no cover - защита от мусорных значений
logger.warning(
'⚠️ Не удалось разобрать сохраненный идентификатор бота внешней админки',
existing_bot_id_raw=existing_bot_id_raw,
)
if existing_token == token and existing_bot_id == bot_id:
if settings.get_external_admin_token() != token:
settings.EXTERNAL_ADMIN_TOKEN = token
if existing_bot_id != settings.EXTERNAL_ADMIN_TOKEN_BOT_ID:
settings.EXTERNAL_ADMIN_TOKEN_BOT_ID = existing_bot_id
return token
if existing_bot_id is not None and bot_id is not None and existing_bot_id != bot_id:
logger.error(
'❌ Обнаружено несовпадение ID бота для токена внешней админки: сохранен , текущий',
existing_bot_id=existing_bot_id,
bot_id=bot_id,
)
try:
await bot_configuration_service.reset_value(
session,
'EXTERNAL_ADMIN_TOKEN',
force=True,
)
await bot_configuration_service.reset_value(
session,
'EXTERNAL_ADMIN_TOKEN_BOT_ID',
force=True,
)
await session.commit()
logger.warning(
'⚠️ Токен внешней админки очищен из-за несовпадения идентификаторов бота',
)
except Exception as cleanup_error: # pragma: no cover - защитный блок
await session.rollback()
logger.error(
'❌ Не удалось очистить токен внешней админки после обнаружения подмены',
cleanup_error=cleanup_error,
)
finally:
settings.EXTERNAL_ADMIN_TOKEN = None
settings.EXTERNAL_ADMIN_TOKEN_BOT_ID = None
return None
updates: list[tuple[str, object]] = []
if existing_token != token:
updates.append(('EXTERNAL_ADMIN_TOKEN', token))
if bot_id is not None and existing_bot_id != bot_id:
updates.append(('EXTERNAL_ADMIN_TOKEN_BOT_ID', bot_id))
if not updates:
# Токен совпал, но могли отсутствовать значения в настройках приложения
if settings.get_external_admin_token() != (existing_token or token):
settings.EXTERNAL_ADMIN_TOKEN = existing_token or token
if existing_bot_id is not None and (existing_bot_id != settings.EXTERNAL_ADMIN_TOKEN_BOT_ID):
settings.EXTERNAL_ADMIN_TOKEN_BOT_ID = existing_bot_id
elif bot_id is not None and bot_id != settings.EXTERNAL_ADMIN_TOKEN_BOT_ID and existing_bot_id is None:
settings.EXTERNAL_ADMIN_TOKEN_BOT_ID = bot_id
return existing_token or token
try:
for key, value in updates:
await bot_configuration_service.set_value(
session,
key,
value,
force=True,
)
await session.commit()
logger.info('✅ Токен внешней админки синхронизирован для @', normalized_username=normalized_username)
except ReadOnlySettingError: # pragma: no cover - force=True предотвращает исключение
await session.rollback()
logger.warning(
'⚠️ Не удалось сохранить токен внешней админки из-за ограничения доступа',
)
return None
return token
except SQLAlchemyError as error:
logger.error('❌ Ошибка сохранения токена внешней админки', error=error)
return None