refactor: remove unused EXTERNAL_ADMIN_TOKEN functionality
- Delete app/services/external_admin_service.py entirely - Remove EXTERNAL_ADMIN_TOKEN and EXTERNAL_ADMIN_TOKEN_BOT_ID from config - Remove build_external_admin_token, get_external_admin_token, get_external_admin_bot_id methods - Remove unused hashlib/hmac imports from config.py - Remove from system_settings_service: READ_ONLY_KEYS, PLAIN_TEXT_KEYS, category title, category description, prefix mapping, documentation metadata - Remove from bot_configuration.py category group - Remove from main.py startup sequence (ensure_external_admin_token call) - Remove from .env.example - Remove from docs/project_structure_reference.md
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -158,7 +158,6 @@ CATEGORY_GROUP_METADATA: dict[str, dict[str, object]] = {
|
||||
'LOG',
|
||||
'MODERATION',
|
||||
'DEBUG',
|
||||
'EXTERNAL_ADMIN',
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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.'
|
||||
|
||||
@@ -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 методов)
|
||||
Функции: нет
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user