fix
This commit is contained in:
+15
-12
@@ -1,8 +1,6 @@
|
|||||||
import asyncio
|
|
||||||
import hashlib
|
|
||||||
import json
|
import json
|
||||||
import secrets
|
|
||||||
import logging
|
import logging
|
||||||
|
import secrets
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
@@ -16,7 +14,11 @@ _SESSIONS_FILE = Path("data/sessions.json")
|
|||||||
|
|
||||||
|
|
||||||
class AuthManager:
|
class AuthManager:
|
||||||
"""Управляет авторизацией: хеширование паролей, сессии, верификация, роли."""
|
"""Управляет сессиями и ролями.
|
||||||
|
|
||||||
|
Личность пользователя подтверждает Telegram, поэтому пароля для входа нет.
|
||||||
|
ADMIN_PASSWORD остаётся единственным секретом — им повышают роль до admin.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, session_timeout_minutes: int = 120):
|
def __init__(self, session_timeout_minutes: int = 120):
|
||||||
self._sessions: Dict[int, datetime] = {}
|
self._sessions: Dict[int, datetime] = {}
|
||||||
@@ -28,15 +30,16 @@ class AuthManager:
|
|||||||
self._restore_sessions()
|
self._restore_sessions()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _hash_password(password: str, salt: str = None) -> tuple:
|
def verify_admin_password(password: str) -> bool:
|
||||||
if salt is None:
|
"""Проверяет пароль администратора.
|
||||||
salt = secrets.token_hex(16)
|
|
||||||
result = hashlib.sha256((salt + password).encode()).hexdigest()
|
|
||||||
return result, salt
|
|
||||||
|
|
||||||
def verify_password(self, password: str, password_hash: str, salt: str) -> bool:
|
Пустой ADMIN_PASSWORD означает, что админ-режим отключён — иначе
|
||||||
computed, _ = self._hash_password(password, salt)
|
ненастроенный бот пускал бы в админку по пустой строке.
|
||||||
return secrets.compare_digest(computed, password_hash)
|
"""
|
||||||
|
if not ADMIN_PASSWORD:
|
||||||
|
logger.warning("ADMIN_PASSWORD not set — admin mode is disabled")
|
||||||
|
return False
|
||||||
|
return secrets.compare_digest(password, ADMIN_PASSWORD)
|
||||||
|
|
||||||
def is_authenticated(self, user_id: int) -> bool:
|
def is_authenticated(self, user_id: int) -> bool:
|
||||||
if user_id not in self._sessions:
|
if user_id not in self._sessions:
|
||||||
|
|||||||
+9
-22
@@ -1,51 +1,38 @@
|
|||||||
"""
|
"""
|
||||||
Хранилище учётных данных пользователей.
|
Хранилище зарегистрированных пользователей.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Dict
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AuthStorage:
|
class AuthStorage:
|
||||||
"""Хранилище паролей пользователей в JSON-файле."""
|
"""Реестр пользователей бота в JSON-файле.
|
||||||
|
|
||||||
|
Паролей не хранит: личность подтверждает Telegram, идентификатором
|
||||||
|
служит user_id.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, file_path: str = "data/users.json"):
|
def __init__(self, file_path: str = "data/users.json"):
|
||||||
self.file_path = Path(file_path)
|
self.file_path = Path(file_path)
|
||||||
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
async def register(self, user_id: int, password_hash: str, salt: str) -> bool:
|
async def register(self, user_id: int) -> bool:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
data = await asyncio.to_thread(self._load_sync)
|
data = await asyncio.to_thread(self._load_sync)
|
||||||
if str(user_id) in data:
|
if str(user_id) in data:
|
||||||
return False
|
return False
|
||||||
data[str(user_id)] = {"password_hash": password_hash, "salt": salt}
|
data[str(user_id)] = {"registered_at": datetime.now().isoformat()}
|
||||||
await asyncio.to_thread(self._save_sync, data)
|
await asyncio.to_thread(self._save_sync, data)
|
||||||
logger.info(f"User {user_id} registered")
|
logger.info(f"User {user_id} registered")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_user(self, user_id: int) -> Optional[Dict]:
|
|
||||||
async with self._lock:
|
|
||||||
data = await asyncio.to_thread(self._load_sync)
|
|
||||||
return data.get(str(user_id))
|
|
||||||
|
|
||||||
async def change_password(self, user_id: int, new_hash: str, new_salt: str) -> bool:
|
|
||||||
async with self._lock:
|
|
||||||
data = await asyncio.to_thread(self._load_sync)
|
|
||||||
user_str = str(user_id)
|
|
||||||
if user_str not in data:
|
|
||||||
return False
|
|
||||||
data[user_str]["password_hash"] = new_hash
|
|
||||||
data[user_str]["salt"] = new_salt
|
|
||||||
await asyncio.to_thread(self._save_sync, data)
|
|
||||||
logger.info(f"User {user_id} changed password")
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def delete_user(self, user_id: int) -> bool:
|
async def delete_user(self, user_id: int) -> bool:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
data = await asyncio.to_thread(self._load_sync)
|
data = await asyncio.to_thread(self._load_sync)
|
||||||
|
|||||||
+10
-108
@@ -33,7 +33,6 @@ from services.payment_service import HelketPayment
|
|||||||
from services.twitch_api import get_viewer_count
|
from services.twitch_api import get_viewer_count
|
||||||
from auth.manager import AuthManager
|
from auth.manager import AuthManager
|
||||||
from auth.storage import AuthStorage
|
from auth.storage import AuthStorage
|
||||||
from auth import ADMIN_PASSWORD
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ class BotInterface:
|
|||||||
self._menu_top_msg: Dict[int, int] = {} # user_id -> reply keyboard message_id
|
self._menu_top_msg: Dict[int, int] = {} # user_id -> reply keyboard message_id
|
||||||
self._chat_history: Dict[int, list] = {}
|
self._chat_history: Dict[int, list] = {}
|
||||||
self._bot_ref = bot_ref
|
self._bot_ref = bot_ref
|
||||||
self._waiting_password: Dict[int, str] = {} # user_id -> "register" or "login"
|
self._waiting_password: Dict[int, str] = {} # user_id -> "admin_upgrade"
|
||||||
self._start_time = datetime.now()
|
self._start_time = datetime.now()
|
||||||
|
|
||||||
async def _update_commands(self):
|
async def _update_commands(self):
|
||||||
@@ -102,74 +101,12 @@ class BotInterface:
|
|||||||
await interface._show_main_menu(message)
|
await interface._show_main_menu(message)
|
||||||
return
|
return
|
||||||
|
|
||||||
if interface.auth_manager.is_authenticated(user_id):
|
# Пароля для входа нет — личность подтверждает сам Telegram.
|
||||||
await interface._show_user_menu(message)
|
# Первый /start заодно регистрирует пользователя.
|
||||||
return
|
await interface.auth_storage.register(user_id)
|
||||||
|
if not interface.auth_manager.is_authenticated(user_id):
|
||||||
exists = await interface.auth_storage.user_exists(user_id)
|
|
||||||
if exists:
|
|
||||||
sent = await message.answer("С возвращением! Введите пароль:")
|
|
||||||
interface._waiting_password[user_id] = "login"
|
|
||||||
else:
|
|
||||||
sent = await message.answer(
|
|
||||||
"Добро пожаловать!\n\nПридумайте пароль для входа (мин. 4 символа):"
|
|
||||||
)
|
|
||||||
interface._waiting_password[user_id] = "register"
|
|
||||||
interface._menu_msg[user_id] = sent.message_id
|
|
||||||
|
|
||||||
@dp.message(Command("register"))
|
|
||||||
async def cmd_register(message: Message):
|
|
||||||
"""Регистрация: /register <пароль>"""
|
|
||||||
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
|
|
||||||
parts = message.text.split(maxsplit=1)
|
|
||||||
if len(parts) < 2 or len(parts[1]) < 4:
|
|
||||||
await interface._send_temp(message, "❌ Использование: /register <пароль от 4 символов>")
|
|
||||||
return
|
|
||||||
password = parts[1]
|
|
||||||
user_id = message.from_user.id
|
|
||||||
exists = await interface.auth_storage.user_exists(user_id)
|
|
||||||
if exists:
|
|
||||||
await interface._send_temp(message, "⚠️ Вы уже зарегистрированы. Используйте /login <пароль>")
|
|
||||||
return
|
|
||||||
password_hash, salt = interface.auth_manager._hash_password(password)
|
|
||||||
await interface.auth_storage.register(user_id, password_hash, salt)
|
|
||||||
is_admin = (password == ADMIN_PASSWORD)
|
|
||||||
interface.auth_manager.login(user_id, is_admin=is_admin)
|
|
||||||
if is_admin:
|
|
||||||
await interface._update_commands()
|
|
||||||
await interface._show_main_menu(message)
|
|
||||||
else:
|
|
||||||
await interface._show_user_menu(message)
|
|
||||||
|
|
||||||
@dp.message(Command("login"))
|
|
||||||
async def cmd_login(message: Message):
|
|
||||||
"""Вход: /login <пароль>"""
|
|
||||||
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
|
|
||||||
parts = message.text.split(maxsplit=1)
|
|
||||||
if len(parts) < 2:
|
|
||||||
await interface._send_temp(message, "❌ Использование: /login <пароль>")
|
|
||||||
return
|
|
||||||
password = parts[1]
|
|
||||||
user_id = message.from_user.id
|
|
||||||
user_data = await interface.auth_storage.get_user(user_id)
|
|
||||||
if not user_data:
|
|
||||||
await interface._send_temp(message, "❌ Вы не зарегистрированы.\nИспользуйте /register <пароль>")
|
|
||||||
return
|
|
||||||
if interface.auth_manager.is_locked_out(user_id):
|
|
||||||
await interface._send_temp(message, "🔒 Слишком много попыток. Попробуйте позже.")
|
|
||||||
return
|
|
||||||
if password == ADMIN_PASSWORD:
|
|
||||||
interface.auth_manager.login(user_id, is_admin=True)
|
|
||||||
await interface._update_commands()
|
|
||||||
await interface._show_main_menu(message)
|
|
||||||
elif interface.auth_manager.verify_password(
|
|
||||||
password, user_data["password_hash"], user_data["salt"]
|
|
||||||
):
|
|
||||||
interface.auth_manager.login(user_id, is_admin=False)
|
interface.auth_manager.login(user_id, is_admin=False)
|
||||||
await interface._show_user_menu(message)
|
await interface._show_user_menu(message)
|
||||||
else:
|
|
||||||
interface.auth_manager.record_failed_attempt(user_id)
|
|
||||||
await interface._send_temp(message, "❌ Неверный пароль")
|
|
||||||
|
|
||||||
@dp.message(Command("logout"))
|
@dp.message(Command("logout"))
|
||||||
async def cmd_logout(message: Message):
|
async def cmd_logout(message: Message):
|
||||||
@@ -1345,59 +1282,24 @@ class BotInterface:
|
|||||||
await message.answer(f"❌ Не удалось отправить: {e}")
|
await message.answer(f"❌ Не удалось отправить: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Ввод пароля (неавторизованный пользователь ждёт пароль)
|
# Ввод пароля администратора (единственный оставшийся пароль)
|
||||||
if user_id in interface._waiting_password:
|
if user_id in interface._waiting_password:
|
||||||
if not text:
|
if not text:
|
||||||
return
|
return
|
||||||
# Пароль удаляем немедленно
|
# Пароль удаляем немедленно
|
||||||
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
|
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
|
||||||
mode = interface._waiting_password.pop(user_id)
|
interface._waiting_password.pop(user_id)
|
||||||
|
|
||||||
if mode == "register":
|
|
||||||
if len(text) < 4:
|
|
||||||
sent = await message.answer("Пароль слишком короткий, мин. 4 символа:")
|
|
||||||
interface._menu_msg[user_id] = sent.message_id
|
|
||||||
interface._waiting_password[user_id] = "register"
|
|
||||||
return
|
|
||||||
password_hash, salt = interface.auth_manager._hash_password(text)
|
|
||||||
await interface.auth_storage.register(user_id, password_hash, salt)
|
|
||||||
is_admin = (text == ADMIN_PASSWORD)
|
|
||||||
interface.auth_manager.login(user_id, is_admin=is_admin)
|
|
||||||
if is_admin:
|
|
||||||
await interface._update_commands()
|
|
||||||
await interface._show_main_menu(message)
|
|
||||||
else:
|
|
||||||
await interface._show_user_menu(message)
|
|
||||||
|
|
||||||
elif mode == "login":
|
|
||||||
user_data = await interface.auth_storage.get_user(user_id)
|
|
||||||
if not user_data:
|
|
||||||
await interface._send_temp(message, "Вы не зарегистрированы")
|
|
||||||
return
|
|
||||||
if interface.auth_manager.is_locked_out(user_id):
|
if interface.auth_manager.is_locked_out(user_id):
|
||||||
await interface._send_temp(message, "Слишком много попыток, попробуйте позже")
|
await interface._send_temp(message, "🔒 Слишком много попыток. Попробуйте позже.")
|
||||||
return
|
return
|
||||||
if text == ADMIN_PASSWORD:
|
|
||||||
|
if interface.auth_manager.verify_admin_password(text):
|
||||||
interface.auth_manager.login(user_id, is_admin=True)
|
interface.auth_manager.login(user_id, is_admin=True)
|
||||||
await interface._update_commands()
|
await interface._update_commands()
|
||||||
await interface._show_main_menu(message)
|
await interface._show_main_menu(message)
|
||||||
elif interface.auth_manager.verify_password(
|
|
||||||
text, user_data["password_hash"], user_data["salt"]
|
|
||||||
):
|
|
||||||
interface.auth_manager.login(user_id, is_admin=False)
|
|
||||||
await interface._show_user_menu(message)
|
|
||||||
else:
|
else:
|
||||||
interface.auth_manager.record_failed_attempt(user_id)
|
interface.auth_manager.record_failed_attempt(user_id)
|
||||||
sent = await message.answer("Неверный пароль, попробуйте ещё раз:")
|
|
||||||
interface._menu_msg[user_id] = sent.message_id
|
|
||||||
interface._waiting_password[user_id] = "login"
|
|
||||||
|
|
||||||
elif mode == "admin_upgrade":
|
|
||||||
if text == ADMIN_PASSWORD:
|
|
||||||
interface.auth_manager.login(user_id, is_admin=True)
|
|
||||||
await interface._update_commands()
|
|
||||||
await interface._show_main_menu(message)
|
|
||||||
else:
|
|
||||||
sent = await message.answer("Неверный пароль администратора:")
|
sent = await message.answer("Неверный пароль администратора:")
|
||||||
interface._menu_msg[user_id] = sent.message_id
|
interface._menu_msg[user_id] = sent.message_id
|
||||||
interface._waiting_password[user_id] = "admin_upgrade"
|
interface._waiting_password[user_id] = "admin_upgrade"
|
||||||
|
|||||||
Reference in New Issue
Block a user