This commit is contained in:
Yuriy Yuriev
2026-07-18 18:33:54 +07:00
parent 6264b24f8f
commit 8bdd0c784e
3 changed files with 44 additions and 152 deletions
+15 -12
View File
@@ -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
View File
@@ -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)
+20 -118
View File
@@ -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,62 +1282,27 @@ 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 interface.auth_manager.is_locked_out(user_id):
if len(text) < 4: await interface._send_temp(message, "🔒 Слишком много попыток. Попробуйте позже.")
sent = await message.answer("Пароль слишком короткий, мин. 4 символа:") return
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": if interface.auth_manager.verify_admin_password(text):
user_data = await interface.auth_storage.get_user(user_id) interface.auth_manager.login(user_id, is_admin=True)
if not user_data: await interface._update_commands()
await interface._send_temp(message, "Вы не зарегистрированы") await interface._show_main_menu(message)
return else:
if interface.auth_manager.is_locked_out(user_id): interface.auth_manager.record_failed_attempt(user_id)
await interface._send_temp(message, "Слишком много попыток, попробуйте позже") sent = await message.answer("Неверный пароль администратора:")
return interface._menu_msg[user_id] = sent.message_id
if text == ADMIN_PASSWORD: interface._waiting_password[user_id] = "admin_upgrade"
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(
text, user_data["password_hash"], user_data["salt"]
):
interface.auth_manager.login(user_id, is_admin=False)
await interface._show_user_menu(message)
else:
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("Неверный пароль администратора:")
interface._menu_msg[user_id] = sent.message_id
interface._waiting_password[user_id] = "admin_upgrade"
return return
# Ввод шагов создания задачи (пользователь) # Ввод шагов создания задачи (пользователь)