Files
demo_spa_bot/app/handlers/booking.py
T
prog1764 896c26bd57 fix: закрыть IDOR в бронях, пароль админа и обработку callback_data (PRJ-3)
- IDOR: отмена/редактирование брони только владельцем (проверка user_id)
- пароль админа: убран дефолт admin123, обязателен из env; сравнение через hmac.compare_digest; TTL сессии 1ч
- callback_data: безопасный парсинг int + .get() вместо прямого индексирования (KeyError/ValueError)
2026-08-04 19:33:08 +04:00

349 lines
11 KiB
Python

import re
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.filters import StateFilter
from aiogram.types import Message, CallbackQuery
from app.keyboards.services import get_services_keyboard, get_extra_services_keyboard
from app.keyboards.masters import get_masters_keyboard
from app.states.booking import BookingState
from app.services.booking_service import (
create_booking,
update_booking,
get_busy_times,
is_slot_busy,
cleanup_cancelled
)
from app.keyboards.menu import menu, cancel_menu
from app.keyboards.calendar import get_dates_keyboard
from app.keyboards.time_slots import get_time_keyboard
from app.keyboards.confirm_booking import get_confirm_booking_keyboard
from app.logger import logger
router = Router()
def _services_to_str(data):
"""Combine main + extra services into one string."""
main = data.get("service", "?")
extra = data.get("extra_services", [])
if extra:
return main + ", " + ", ".join(extra)
return main
# отмена записи на любом шаге
@router.message(F.text.lower() == "отмена")
async def cancel(message: Message, state: FSMContext):
await state.clear()
await message.answer(
"❌ Запись отменена",
reply_markup=menu
)
# старт записи
@router.message(F.text == "📅 Записаться")
async def start_booking(message: Message, state: FSMContext):
await state.clear()
await state.set_state(BookingState.service)
await message.answer(
"Выберите услугу:",
reply_markup=get_services_keyboard()
)
# выбор первой услуги
@router.callback_query(
StateFilter(BookingState.service),
F.data.startswith("service_")
)
async def choose_service(callback: CallbackQuery, state: FSMContext):
from app.config import SERVICES
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=[])
await state.set_state(BookingState.extra_services)
await callback.message.edit_text(
"✅ Выбрано: " + service_name + "\n\n"
"Хотите добавить ещё услугу?\n"
"Нажмите нужную или «Готово»:",
reply_markup=get_extra_services_keyboard()
)
await callback.answer()
# выбор дополнительных услуг
@router.callback_query(
StateFilter(BookingState.extra_services),
F.data.startswith("extra_")
)
async def choose_extra_service(callback: CallbackQuery, state: FSMContext):
from app.config import SERVICES
if callback.data == "extra_done":
# Go to master selection
await state.set_state(BookingState.master)
await callback.message.edit_text(
"👤 Выберите специалиста:",
reply_markup=get_masters_keyboard()
)
await callback.answer()
return
# Toggle extra service
actual_key = callback.data.replace("extra_", "", 1)
service_name = SERVICES.get(actual_key)
if not service_name:
await callback.answer("Услуга не найдена")
return
data = await state.get_data()
extra = data.get("extra_services", [])
if service_name in extra:
extra.remove(service_name)
logger.debug("User {uid} removed extra service: {srv}", uid=callback.from_user.id, srv=service_name)
else:
extra.append(service_name)
logger.debug("User {uid} added extra service: {srv}", uid=callback.from_user.id, srv=service_name)
await state.update_data(extra_services=extra)
data = await state.get_data()
services_text = _services_to_str(data)
await callback.message.edit_text(
"✅ Выбрано: " + services_text + "\n\n"
"Хотите добавить ещё услугу?\n"
"Нажмите нужную или «Готово»:",
reply_markup=get_extra_services_keyboard(extra)
)
await callback.answer()
# выбор мастера
@router.callback_query(
StateFilter(BookingState.master),
F.data.startswith("master_")
)
async def choose_master(callback: CallbackQuery, state: FSMContext):
from app.config import MASTERS
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)
await state.set_state(BookingState.date)
await callback.message.edit_text(
"📅 Выберите дату:",
reply_markup=get_dates_keyboard()
)
await callback.answer()
# выбор даты
@router.callback_query(
StateFilter(BookingState.date),
F.data.startswith("date_")
)
async def choose_date(callback: CallbackQuery, state: FSMContext):
selected_date = callback.data.replace("date_", "")
await state.update_data(date=selected_date)
logger.debug("User {uid} selected date: {d}", uid=callback.from_user.id, d=selected_date)
data = await state.get_data()
busy_times = await get_busy_times(master=data.get("master", "?"), date=selected_date)
await state.set_state(BookingState.time)
await callback.message.edit_text(
"🕒 Выберите время:",
reply_markup=get_time_keyboard(busy_times)
)
await callback.answer()
# выбор времени
@router.callback_query(
StateFilter(BookingState.time),
F.data.startswith("time_")
)
async def choose_time(callback: CallbackQuery, state: FSMContext):
selected_time = callback.data.replace("time_", "")
await state.update_data(time=selected_time)
logger.debug("User {uid} selected time: {t}", uid=callback.from_user.id, t=selected_time)
data = await state.get_data()
services_str = _services_to_str(data)
await state.set_state(BookingState.confirm)
await callback.message.edit_text(
"📋 Проверьте запись:\n\n"
f"💆 Услуги: {services_str}\n"
f"📅 Дата: {data.get('date', '?')}\n"
f"👤 Мастер: {data.get('master', '?')}\n"
f"🕒 Время: {selected_time}",
reply_markup=get_confirm_booking_keyboard()
)
await callback.answer()
# подтверждение
@router.callback_query(
StateFilter(BookingState.confirm),
F.data == "confirm_booking"
)
async def confirm_booking_callback(callback: CallbackQuery, state: FSMContext):
data = await state.get_data()
logger.info("User {uid} confirmed booking: {data}", uid=callback.from_user.id, data=data)
services_str = _services_to_str(data)
editing_booking_id = data.get("editing_booking_id")
if editing_booking_id:
await update_booking(
booking_id=editing_booking_id,
service=services_str,
master=data.get("master", "?"),
date=data.get("date", "?"),
time=data.get("time", "?")
)
result_text = "✅ Запись успешно обновлена"
else:
master = data.get("master", "?")
date = data.get("date", "?")
time = data.get("time", "?")
if await is_slot_busy(master, date, time):
logger.warning("Slot already busy: {mst} {dt} {tm}", mst=master, dt=date, tm=time)
await callback.message.edit_text("❌ Это время уже занято. Пожалуйста, выберите другое время.")
await state.clear()
await callback.answer()
return
await create_booking(
user_id=callback.from_user.id,
service=services_str,
master=master,
date=date,
time=time,
user_name=callback.from_user.full_name or callback.from_user.username or str(callback.from_user.id)
)
cleanup_cancelled()
result_text = "✅ Запись успешно создана"
await state.clear()
await callback.message.edit_text(result_text)
await callback.answer()
@router.callback_query(
StateFilter(BookingState.confirm),
F.data == "cancel_booking"
)
async def cancel_booking_callback(callback: CallbackQuery, state: FSMContext):
await state.clear()
await callback.message.edit_text("❌ Запись отменена")
await callback.answer()
@router.callback_query(
StateFilter(BookingState.confirm),
F.data == "edit_booking"
)
async def edit_booking_callback(callback: CallbackQuery, state: FSMContext):
old_data = await state.get_data()
await state.clear()
await state.update_data(service=old_data.get("service"))
await state.set_state(BookingState.service)
await callback.message.edit_text(
"Выберите услугу:",
reply_markup=get_services_keyboard()
)
@router.callback_query(F.data == "go_back")
async def go_back_handler(callback: CallbackQuery, state: FSMContext):
current_state = await state.get_state()
if current_state == BookingState.extra_services:
# Back to first service
await state.set_state(BookingState.service)
data = await state.get_data()
data.pop("extra_services", None)
data.pop("service", None)
await state.set_data(data)
await callback.message.edit_text(
"Выберите услугу:",
reply_markup=get_services_keyboard()
)
elif current_state == BookingState.master:
# Back to extra services
await state.set_state(BookingState.extra_services)
data = await state.get_data()
extra = data.get("extra_services", [])
services_text = _services_to_str(data)
await callback.message.edit_text(
"✅ Выбрано: " + services_text + "\n\n"
"Хотите добавить ещё услугу?\n"
"Нажмите нужную или «Готово»:",
reply_markup=get_extra_services_keyboard(extra)
)
elif current_state == BookingState.date:
await state.set_state(BookingState.master)
data = await state.get_data()
data.pop("date", None)
data.pop("time", None)
await state.set_data(data)
await callback.message.edit_text(
"👤 Выберите специалиста:",
reply_markup=get_masters_keyboard()
)
elif current_state == BookingState.time:
await state.set_state(BookingState.date)
data = await state.get_data()
data.pop("time", None)
await state.set_data(data)
await callback.message.edit_text(
"📅 Выберите дату:",
reply_markup=get_dates_keyboard()
)
elif current_state == BookingState.confirm:
await state.set_state(BookingState.time)
data = await state.get_data()
busy_times = await get_busy_times(
master=data.get("master", "?"),
date=data.get("date", "?")
)
await callback.message.edit_text(
"🕒 Выберите время:",
reply_markup=get_time_keyboard(busy_times)
)
else:
await state.clear()
await callback.message.edit_text("Отмена записи")
await callback.answer()