diff --git a/app/config.py b/app/config.py index e342491..754f875 100644 --- a/app/config.py +++ b/app/config.py @@ -4,11 +4,14 @@ from dotenv import load_dotenv load_dotenv() BOT_TOKEN = os.getenv("BOT_TOKEN") -ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123") +ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD") if not BOT_TOKEN: raise ValueError("BOT_TOKEN not found in environment") +if not ADMIN_PASSWORD: + raise ValueError("ADMIN_PASSWORD not found in environment (no default allowed)") + # Services catalog SERVICES = { "service_massage": "💪 Массаж", diff --git a/app/db/memory.py b/app/db/memory.py index 85a2e89..6abcc8a 100644 --- a/app/db/memory.py +++ b/app/db/memory.py @@ -88,6 +88,15 @@ def get_user_bookings(user_id): ] +def get_booking(booking_id): + """Return a booking by id regardless of status, or None.""" + bookings = load_bookings() + for b in bookings: + if b["id"] == booking_id: + return b + return None + + def cancel_booking(booking_id): bookings = load_bookings() for b in bookings: diff --git a/app/handlers/admin.py b/app/handlers/admin.py index c2320ed..29337ae 100644 --- a/app/handlers/admin.py +++ b/app/handlers/admin.py @@ -1,4 +1,7 @@ """Admin handler for spa-telegram-bot — secure with password.""" +import hmac +import time + from aiogram import Router, F from aiogram.types import Message, CallbackQuery from aiogram.filters import Command, CommandObject @@ -10,17 +13,22 @@ router = Router() ADMIN_IDS = {991309145} # Store approved sessions — user_ids that passed password check -_approved = set() +_approved: dict[int, float] = {} +APPROVED_TTL_SECONDS = 60 * 60 # 1 hour def is_admin(user_id: int, password: str | None = None) -> bool: """Check if user is admin: by user_id, by password, or by approved session.""" if user_id in ADMIN_IDS: return True - if user_id in _approved: + now = time.time() + expires_at = _approved.get(user_id) + if expires_at and expires_at > now: return True - if password and password == ADMIN_PASSWORD: - _approved.add(user_id) + if expires_at: + _approved.pop(user_id, None) # expired session + if password and hmac.compare_digest(password.encode("utf-8"), ADMIN_PASSWORD.encode("utf-8")): + _approved[user_id] = now + APPROVED_TTL_SECONDS return True return False @@ -69,7 +77,12 @@ async def admin_cancel_booking(callback: CallbackQuery): await callback.answer("⛔ Нет доступа") return - booking_id = int(callback.data.split("_")[2]) + try: + booking_id = int(callback.data.split("_", 2)[2]) + except (ValueError, IndexError): + await callback.answer("Некорректные данные") + return + logger.info("Admin {uid} cancelled booking #{bid}", uid=callback.from_user.id, bid=booking_id) from app.db.memory import cancel_booking diff --git a/app/handlers/booking.py b/app/handlers/booking.py index 09157af..f70b69d 100644 --- a/app/handlers/booking.py +++ b/app/handlers/booking.py @@ -61,7 +61,11 @@ async def start_booking(message: Message, state: FSMContext): async def choose_service(callback: CallbackQuery, state: FSMContext): from app.config import SERVICES - service_name = SERVICES[callback.data] + service_name = SERVICES.get(callback.data) + if not service_name: + await callback.answer("Услуга не найдена") + return + logger.debug("User {uid} selected first service: {srv}", uid=callback.from_user.id, srv=service_name) await state.update_data(service=service_name, extra_services=[]) @@ -133,7 +137,11 @@ async def choose_extra_service(callback: CallbackQuery, state: FSMContext): async def choose_master(callback: CallbackQuery, state: FSMContext): from app.config import MASTERS - master_name = MASTERS[callback.data] + master_name = MASTERS.get(callback.data) + if not master_name: + await callback.answer("Мастер не найден") + return + logger.debug("User {uid} selected master: {m}", uid=callback.from_user.id, m=master_name) await state.update_data(master=master_name) diff --git a/app/handlers/mybookings.py b/app/handlers/mybookings.py index c6467f8..408ccdf 100644 --- a/app/handlers/mybookings.py +++ b/app/handlers/mybookings.py @@ -2,7 +2,7 @@ from aiogram import Router, F from aiogram.types import Message, CallbackQuery from aiogram.fsm.context import FSMContext -from app.db.memory import get_user_bookings, cancel_booking, update_booking +from app.db.memory import get_user_bookings, get_booking, cancel_booking, update_booking from app.keyboards.menu import menu from app.keyboards.my_booking import get_booking_actions from app.logger import logger @@ -55,7 +55,18 @@ async def my_bookings( F.data.startswith("cancel_booking_") ) async def cancel_booking_handler(callback: CallbackQuery): - booking_id = int(callback.data.split("_")[2]) + try: + booking_id = int(callback.data.split("_", 2)[2]) + except (ValueError, IndexError): + await callback.answer("Некорректные данные") + return + + booking = get_booking(booking_id) + if not booking or booking.get("user_id") != callback.from_user.id: + logger.warning("User {uid} tried to cancel foreign booking #{bid}", uid=callback.from_user.id, bid=booking_id) + await callback.answer("⛔ Это не ваша запись") + return + logger.info("User {uid} cancelled booking #{bid}", uid=callback.from_user.id, bid=booking_id) cancel_booking( booking_id @@ -73,7 +84,17 @@ async def edit_booking_handler( callback: CallbackQuery, state: FSMContext ): - booking_id = int(callback.data.split("_")[2]) + try: + booking_id = int(callback.data.split("_", 2)[2]) + except (ValueError, IndexError): + await callback.answer("Некорректные данные") + return + + booking = get_booking(booking_id) + if not booking or booking.get("user_id") != callback.from_user.id: + logger.warning("User {uid} tried to edit foreign booking #{bid}", uid=callback.from_user.id, bid=booking_id) + await callback.answer("⛔ Это не ваша запись") + return update_booking( booking_id=booking_id,