diff --git a/ADMIN_GUIDE.txt b/ADMIN_GUIDE.txt index 71efe50..cd2be07 100644 --- a/ADMIN_GUIDE.txt +++ b/ADMIN_GUIDE.txt @@ -24,10 +24,23 @@ • Или отправьте команду /start ШАГ 2: Вход в админ-панель + +СПОСОБ 1: Получение прав администратора через пароль + • Отправьте команду /admin_login + • Введите пароль администратора (получите у создателя бота) + • При правильном пароле вы получите права администратора + • Создатель бота получит уведомление о новом администраторе + • После этого используйте команду /admin для входа в панель + +СПОСОБ 2: Прямой вход (если уже администратор) • Отправьте команду /admin • Если вы администратор, откроется админ-панель - • Если команда не работает - обратитесь к создателю бота (@kolo_id) - • Только создатель бота имеет права назначать администраторов + • Если команда не работает - используйте /admin_login + +⚠️ ВАЖНО: + • Лимит попыток ввода пароля: 5 попыток в час + • При превышении лимита нужно подождать + • Только создатель бота имеет права назначать администраторов напрямую ═══════════════════════════════════════════════════════════════════════════════ 2. ГЛАВНОЕ МЕНЮ АДМИНКИ diff --git a/config.example.toml b/config.example.toml index 96497a4..605973c 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,6 +1,7 @@ [bot] token = "1234567890" creator_id = 1234567890 +admin_password = "your_admin_password" [security] encode_key = "encode_key" diff --git a/src/quizzi/application/__main__.py b/src/quizzi/application/__main__.py index 92d2328..44bc7b3 100644 --- a/src/quizzi/application/__main__.py +++ b/src/quizzi/application/__main__.py @@ -25,6 +25,7 @@ from quizzi.application.bot.user_dialogs.deeplink import deeplink_dialog from quizzi.application.bot.user_dialogs.main_menu import user_menu_dialog from quizzi.application.bot.user_dialogs.registration import registration_dialog from quizzi.application.bot.user_dialogs.take_test import take_test_dialog +from quizzi.application.bot.user_dialogs.admin_login import admin_login_dialog from quizzi.infrastructure.database.repo.test import TestRepository from quizzi.infrastructure.database.repo.user import UserRepository from quizzi.infrastructure.di import DatabaseProvider, SchedulerProvider, ServiceProvider @@ -62,6 +63,7 @@ async def main() -> None: take_test_dialog, registration_dialog, deeplink_dialog, + admin_login_dialog, shared_tests_dialog, shared_groups_dialog, shared_broadcast_dialog, diff --git a/src/quizzi/application/bot/handlers.py b/src/quizzi/application/bot/handlers.py index dfee906..c3d01e9 100644 --- a/src/quizzi/application/bot/handlers.py +++ b/src/quizzi/application/bot/handlers.py @@ -9,7 +9,7 @@ from dishka.integrations.aiogram import FromDishka from quizzi.application.bot.admin_dialogs.states import AdminMenuSG from quizzi.application.bot.creator_dialogs.states import CreatorMenuSG -from quizzi.application.bot.user_dialogs.states import UserDeeplinkSG, UserMenuSG, UserRegistrationSG +from quizzi.application.bot.user_dialogs.states import UserAdminLoginSG, UserDeeplinkSG, UserMenuSG, UserRegistrationSG from quizzi.service.test import TestService from quizzi.service.user import UserService @@ -165,6 +165,13 @@ async def creator_command(_message: Message, dialog_manager: DialogManager) -> N await dialog_manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK) +@router.message(Command("admin_login")) +async def admin_login_command(_message: Message, dialog_manager: DialogManager) -> None: + assert _message.from_user is not None + logger.info("Admin login attempt: user_id=%d", _message.from_user.id) + await dialog_manager.start(UserAdminLoginSG.password_input, mode=StartMode.RESET_STACK) + + @router.error() async def dialog_error_handler(event: ErrorEvent, dialog_manager: DialogManager) -> None: if isinstance(event.exception, (UnknownIntent, OutdatedIntent)): diff --git a/src/quizzi/application/bot/user_dialogs/admin_login.py b/src/quizzi/application/bot/user_dialogs/admin_login.py new file mode 100644 index 0000000..ab45d7f --- /dev/null +++ b/src/quizzi/application/bot/user_dialogs/admin_login.py @@ -0,0 +1,74 @@ +from aiogram.types import Message +from aiogram_dialog import Dialog, DialogManager, Window +from aiogram_dialog.widgets.input import MessageInput +from aiogram_dialog.widgets.kbd import Button +from aiogram_dialog.widgets.text import Const +from dishka import FromDishka +from dishka.integrations.aiogram_dialog import inject + +from quizzi.application.bot.user_dialogs.states import UserAdminLoginSG +from quizzi.infrastructure.database.dao.user import UserDAO +from quizzi.infrastructure.utils.config import Config +from quizzi.infrastructure.utils.rate_limiter import PasswordRateLimiter + + +password_limiter = PasswordRateLimiter() + + +@inject +async def on_password_input( + message: Message, + _widget: MessageInput, + manager: DialogManager, + user_dao: FromDishka[UserDAO], + config: FromDishka[Config], +): + assert message.from_user is not None + assert message.text is not None + + user_id = message.from_user.id + + allowed, wait_time = await password_limiter.check(user_id) + + if not allowed: + minutes = int(wait_time // 60) + seconds = int(wait_time % 60) + await message.answer( + f"❌ Слишком много попыток. Попробуйте через {minutes} мин {seconds} сек" + ) + return + + password = message.text.strip() + + if password == config.bot.admin_password: + await user_dao.update(user_id, is_admin=True) + await message.answer("✅ Вы успешно получили права администратора") + + try: + await message.bot.send_message( + config.bot.creator_id, + f"🔔 Новый администратор:\n" + f"ID: {user_id}\n" + f"Username: @{message.from_user.username or 'нет'}\n" + f"Имя: {message.from_user.first_name}" + ) + except Exception: + pass + + await manager.done() + else: + await message.answer("❌ Неверный пароль") + + +async def on_cancel(_callback, _button, manager: DialogManager): + await manager.done() + + +admin_login_dialog = Dialog( + Window( + Const("🔐 Вход в панель администратора\n\n🔑 Введите пароль администратора:"), + MessageInput(on_password_input), + Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel), + state=UserAdminLoginSG.password_input, + ), +) diff --git a/src/quizzi/application/bot/user_dialogs/states.py b/src/quizzi/application/bot/user_dialogs/states.py index beb550c..eecdc4d 100644 --- a/src/quizzi/application/bot/user_dialogs/states.py +++ b/src/quizzi/application/bot/user_dialogs/states.py @@ -30,3 +30,7 @@ class UserDeeplinkSG(StatesGroup): class UserRegistrationSG(StatesGroup): input_name = State() select_group = State() + + +class UserAdminLoginSG(StatesGroup): + password_input = State() diff --git a/src/quizzi/infrastructure/utils/config.py b/src/quizzi/infrastructure/utils/config.py index 447882e..db65bfb 100644 --- a/src/quizzi/infrastructure/utils/config.py +++ b/src/quizzi/infrastructure/utils/config.py @@ -8,6 +8,7 @@ from typing import Self class BotConfig: token: str creator_id: int + admin_password: str @dataclass @@ -47,7 +48,8 @@ class Config: return cls( bot=BotConfig( token=str(bot_data["token"]), - creator_id=int(bot_data["creator_id"]) + creator_id=int(bot_data["creator_id"]), + admin_password=str(bot_data["admin_password"]) ), database=DatabaseConfig( host=str(db_data["host"]),