diff --git a/.env.example b/.env.example index 0aecb243..a153d08e 100644 --- a/.env.example +++ b/.env.example @@ -1029,10 +1029,4 @@ WEB_API_TOKEN_HASH_ALGORITHM=sha256 # Логирование запросов WEB_API_REQUEST_LOGGING=true -# Внешний админ-токен (для интеграции с другими ботами/системами) -# Токен для доступа через API другого бота -# EXTERNAL_ADMIN_TOKEN= -# ID бота, от которого принимается токен -# EXTERNAL_ADMIN_TOKEN_BOT_ID= - MINIAPP_STATIC_PATH=miniapp diff --git a/app/config.py b/app/config.py index 2281e5ef..412bda59 100644 --- a/app/config.py +++ b/app/config.py @@ -1,5 +1,3 @@ -import hashlib -import hmac import html import os import re @@ -844,9 +842,6 @@ class Settings(BaseSettings): BACKUP_SEND_TOPIC_ID: int | None = None BACKUP_ARCHIVE_PASSWORD: str | None = None - EXTERNAL_ADMIN_TOKEN: str | None = None - EXTERNAL_ADMIN_TOKEN_BOT_ID: int | None = None - # Cabinet (Personal Account) settings CABINET_ENABLED: bool = False CABINET_JWT_SECRET: str | None = None @@ -1653,37 +1648,6 @@ class Settings(BaseSettings): def get_app_config_cache_ttl(self) -> int: return self.APP_CONFIG_CACHE_TTL - def build_external_admin_token(self, bot_username: str) -> str: - """Генерирует детерминированный и криптографически стойкий токен внешней админки.""" - normalized = (bot_username or '').strip().lstrip('@').lower() - if not normalized: - raise ValueError('Bot username is required to build external admin token') - - secret = (self.BOT_TOKEN or '').strip() - if not secret: - raise ValueError('Bot token is required to build external admin token') - - digest = hmac.new( - key=secret.encode('utf-8'), - msg=f'remnawave.external_admin::{normalized}'.encode(), - digestmod=hashlib.sha256, - ).hexdigest() - return digest[:48] - - def get_external_admin_token(self) -> str | None: - token = (self.EXTERNAL_ADMIN_TOKEN or '').strip() - return token or None - - def get_external_admin_bot_id(self) -> int | None: - try: - return int(self.EXTERNAL_ADMIN_TOKEN_BOT_ID) if self.EXTERNAL_ADMIN_TOKEN_BOT_ID else None - except (TypeError, ValueError): # pragma: no cover - защитная ветка для некорректных значений - logger.warning( - 'Некорректный идентификатор бота для внешней админки', - EXTERNAL_ADMIN_TOKEN_BOT_ID=self.EXTERNAL_ADMIN_TOKEN_BOT_ID, - ) - return None - def is_traffic_selectable(self) -> bool: return self.TRAFFIC_SELECTION_MODE.lower() == 'selectable' diff --git a/app/handlers/admin/bot_configuration.py b/app/handlers/admin/bot_configuration.py index 6ca53208..106f191d 100644 --- a/app/handlers/admin/bot_configuration.py +++ b/app/handlers/admin/bot_configuration.py @@ -158,7 +158,6 @@ CATEGORY_GROUP_METADATA: dict[str, dict[str, object]] = { 'LOG', 'MODERATION', 'DEBUG', - 'EXTERNAL_ADMIN', ), }, } diff --git a/app/services/external_admin_service.py b/app/services/external_admin_service.py deleted file mode 100644 index 12a7b797..00000000 --- a/app/services/external_admin_service.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Утилиты для синхронизации токена внешней админки.""" - -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 diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py index d5976229..47b25c26 100644 --- a/app/services/system_settings_service.py +++ b/app/services/system_settings_service.py @@ -70,8 +70,8 @@ class ReadOnlySettingError(RuntimeError): class BotConfigurationService: EXCLUDED_KEYS: set[str] = {'BOT_TOKEN', 'ADMIN_IDS'} - READ_ONLY_KEYS: set[str] = {'EXTERNAL_ADMIN_TOKEN', 'EXTERNAL_ADMIN_TOKEN_BOT_ID'} - PLAIN_TEXT_KEYS: set[str] = {'EXTERNAL_ADMIN_TOKEN', 'EXTERNAL_ADMIN_TOKEN_BOT_ID'} + READ_ONLY_KEYS: set[str] = set() + PLAIN_TEXT_KEYS: set[str] = set() CATEGORY_TITLES: dict[str, str] = { 'CORE': '🤖 Основные настройки', @@ -101,7 +101,6 @@ class BotConfigurationService: 'MULENPAY': '💰 {mulenpay_name}', 'PAL24': '🏦 PAL24 / PayPalych', 'WATA': '💠 Wata', - 'EXTERNAL_ADMIN': '🛡️ Внешняя админка', 'SUBSCRIPTIONS_CORE': '📅 Подписки и лимиты', 'SIMPLE_SUBSCRIPTION': '⚡ Простая покупка', 'PERIODS': '📆 Периоды подписок', @@ -168,7 +167,6 @@ class BotConfigurationService: 'TELEGRAM_WIDGET': 'Внешний вид виджета авторизации Telegram на странице входа в кабинет.', 'TELEGRAM_OIDC': 'OpenID Connect авторизация через Telegram (новая система). Требует настройки в BotFather > Bot Settings > Web Login.', 'WATA': 'Wata: токен доступа, тип платежа и пределы сумм.', - 'EXTERNAL_ADMIN': 'Токен внешней админки для проверки запросов.', 'SUBSCRIPTIONS_CORE': 'Лимиты устройств, трафика и базовые цены подписок.', 'SIMPLE_SUBSCRIPTION': 'Параметры упрощённой покупки: период, трафик, устройства и сквады.', 'PERIODS': 'Доступные периоды подписок и продлений.', @@ -381,7 +379,6 @@ class BotConfigurationService: 'PAYMENT_': 'PAYMENT', 'PAYMENT_VERIFICATION_': 'PAYMENT_VERIFICATION', 'WATA_': 'WATA', - 'EXTERNAL_ADMIN_': 'EXTERNAL_ADMIN', 'SIMPLE_SUBSCRIPTION_': 'SIMPLE_SUBSCRIPTION', 'CONNECT_BUTTON_HAPP': 'HAPP', 'HAPP_': 'HAPP', @@ -738,20 +735,6 @@ class BotConfigurationService: 'Если результат пустой, используется user_{telegram_id}.' ), }, - 'EXTERNAL_ADMIN_TOKEN': { - 'description': 'Приватный токен, который использует внешняя админка для проверки запросов.', - 'format': 'Значение генерируется автоматически из username бота и его токена и доступно только для чтения.', - 'example': 'Генерируется автоматически', - 'warning': 'Токен обновится при смене username или токена бота.', - 'dependencies': 'Username телеграм-бота, токен бота', - }, - 'EXTERNAL_ADMIN_TOKEN_BOT_ID': { - 'description': 'Идентификатор телеграм-бота, с которым связан токен внешней админки.', - 'format': 'Проставляется автоматически после первого запуска и не редактируется вручную.', - 'example': '123456789', - 'warning': 'Несовпадение ID блокирует обновление токена, предотвращая его подмену на другом боте.', - 'dependencies': 'Результат вызова getMe() в Telegram Bot API', - }, 'TRIAL_USER_TAG': { 'description': ( 'Тег, который бот передаст пользователю при активации триальной подписки в панели RemnaWave.' diff --git a/docs/project_structure_reference.md b/docs/project_structure_reference.md index b54b8190..c1ec1320 100644 --- a/docs/project_structure_reference.md +++ b/docs/project_structure_reference.md @@ -402,9 +402,6 @@ - `app/services/campaign_service.py` — Python-модуль Классы: `CampaignBonusResult`, `AdvertisingCampaignService` (1 методов) Функции: нет -- `app/services/external_admin_service.py` — Утилиты для синхронизации токена внешней админки. - Классы: нет - Функции: нет - `app/services/faq_service.py` — Python-модуль Классы: `FaqService` (3 методов) Функции: нет diff --git a/main.py b/main.py index b662f334..8d7353bf 100644 --- a/main.py +++ b/main.py @@ -22,7 +22,6 @@ from app.services.ban_notification_service import ban_notification_service from app.services.broadcast_service import broadcast_service from app.services.contest_rotation_service import contest_rotation_service from app.services.daily_subscription_service import daily_subscription_service -from app.services.external_admin_service import ensure_external_admin_token from app.services.log_rotation_service import log_rotation_service from app.services.maintenance_service import maintenance_service from app.services.monitoring_service import monitoring_service @@ -515,24 +514,6 @@ async def main(): else: stage.skip('NaloGO отключен настройками') - async with timeline.stage( - 'Внешняя админка', - '🛡️', - success_message='Токен внешней админки готов', - ) as stage: - try: - token = await ensure_external_admin_token( - bot_user.username, - bot_user.id, - ) - if token: - stage.log('Токен синхронизирован') - else: - stage.warning('Не удалось получить токен внешней админки') - except Exception as error: # pragma: no cover - защитный блок - stage.warning(f'Ошибка подготовки внешней админки: {error}') - logger.error('❌ Ошибка подготовки внешней админки', error=error) - bot_run_mode = settings.get_bot_run_mode() polling_enabled = bot_run_mode == 'polling' telegram_webhook_enabled = bot_run_mode == 'webhook'