diff --git a/main.ipynb b/main.ipynb index 913521e..5515977 100644 --- a/main.ipynb +++ b/main.ipynb @@ -1797,7 +1797,7 @@ " restore_data: RestoreRequest = Body(...),\n", "):\n", " # 1. Валидация и очистка входных данных\n", - " identifier = restore_data.identifier.strip().lower()\n", + " identifier = restore_data.identifier.strip()\n", " \n", " if not identifier or not is_valid_email(identifier):\n", " raise HTTPException(status_code=400, detail=\"Некорректный формат email\")\n", @@ -1821,7 +1821,10 @@ " await redis_client.set(rate_limit_key, \"1\", ex=60)\n", "\n", " # 3. Поиск пользователя\n", - " user = await users_collection.find_one({\"email\": identifier})\n", + " user = await users_collection.find_one({'$or': [\n", + " {'email': identifier}, \n", + " {'email': identifier.lower()}\n", + " ]})\n", " \n", " if not user:\n", " # Безопасность: всегда возвращать одинаковое время ответа/структуру сообщения, если возможно\n", diff --git a/main.py b/main.py index 2b921ec..645c696 100644 --- a/main.py +++ b/main.py @@ -782,12 +782,6 @@ async def login( identifier = safe_strip(login_data.identifier) await check_bruteforce(identifier) - users = [doc async for doc in users_collection.find({ - "$or": [ - {"email": identifier}, - {"email": identifier.lower()} - ] - })] or_conditions = [ {"email": identifier}, {"email": identifier.lower()}, @@ -1334,43 +1328,80 @@ async def restore_api( request: Request, restore_data: RestoreRequest = Body(...), ): - email = restore_data.identifier.strip().lower() - next = restore_data.next + # 1. Валидация и очистка входных данных + identifier = restore_data.identifier.strip() + if not identifier or not is_valid_email(identifier): + raise HTTPException(status_code=400, detail="Некорректный формат email") + + # 2. Проверка анти-спама / ограничения частоты запросов + client_ip = request.client.host + rate_limit_key = f"rate_limit:restore:{identifier}:{client_ip}" + + # Проверяем, запрашивал ли пользователь сброс недавно (например, за последние 60 секунд) + existing_request = await redis_client.get(rate_limit_key) + + if existing_request: + # Опционально: получаем TTL, чтобы сообщить пользователю, когда можно повторить попытку + ttl = await redis_client.ttl(rate_limit_key) + raise HTTPException( + status_code=429, + detail=f"Слишком много запросов. Попробуйте через {ttl} секунд." + ) + + # Устанавливаем флаг ограничения частоты (истекает через 60 секунд) + await redis_client.set(rate_limit_key, "1", ex=60) + + # 3. Поиск пользователя user = await users_collection.find_one({'$or': [ - {'email': email}, - {'email': email.lower()} - ]}) + {'email': identifier}, + {'email': identifier.lower()} + ]}) if not user: - raise HTTPException(status_code=404, detail='Email не найден') + # Безопасность: всегда возвращать одинаковое время ответа/структуру сообщения, если возможно + # Но для удобства использования мы можем сказать "не найден". + # Чтобы предотвратить перечисление, некоторые приложения всегда возвращают "OK", но отправляют письмо только если найдено. + # Здесь мы придерживаемся вашей оригинальной логики, но помните о риске перечисления. + raise HTTPException(status_code=404, detail="Пользователь с таким email не найден") + # 4. Генерация токена token = secrets.token_urlsafe(32) - expires_at = datetime.utcnow() + timedelta(hours=1) + expires_at = datetime.now(timezone.utc) + timedelta(hours=1) token_data = { "user_id": str(user["_id"]), "email": user["email"], - "next": next, + "next": restore_data.next, "expires_at": expires_at.isoformat() } - await redis_client.set( - f"reset_token:{token}", - json.dumps(token_data), - ex=3600 - ) - - reset_link = f"{str(request.base_url).rstrip('/')}/restore/{token}" + # 5. Сохранение токена сброса в Redis + reset_token_key = f"reset_token:{token}" + await redis_client.set(reset_token_key, json.dumps(token_data), ex=3600) + # 6. Генерация ссылки для сброса + base_url = str(request.base_url).rstrip('/') + reset_link = f"{base_url}/restore/{token}" + + # 7. Асинхронная отправка email + try: + email_task.notify.delay( + recipient=user['email'], + context={ + 'link': reset_link, + 'email': user['email'] + }, + subject='Восстановление пароля', + template_name='restore.html' + ) + except Exception as e: + # Логирование ошибки + print(f"Не удалось отправить письмо: {e}") + # В зависимости от политики, вы можете захотеть удалить токен, если письмо не отправлено + await redis_client.delete(reset_token_key) + raise HTTPException(status_code=500, detail="Ошибка отправки письма") - email_task.notify.delay( - user['email'], - {'link': reset_link, 'email': user['email']}, - 'Восстановление пароля', - 'restore.html' - ) - return {'ok': True} @@ -1424,7 +1455,7 @@ async def restore_init( ) -app.include_router(router, tags=['Restore passwrod']) +app.include_router(router, tags=['Восстановление пароля']) @app.get("/login", response_class=HTMLResponse) async def login_( @@ -2577,7 +2608,7 @@ async def post_competition( "serviceSocial": payload.serviceSocial, "userSocial": payload.userSocial, "short_desc": safe_strip(payload.short_desc) if payload.short_desc else None, - "poster_file_id": poster_id, + "poster_file_id": po/api/restorster_id, "poster_orig_id": poster_id, "rules_file_id": rules_id, "rules_link": payload.rules_link,