This commit is contained in:
Vladislav 2026-08-02 12:38:39 -04:00
parent d60dc1f8c2
commit b990cd91f0
2 changed files with 66 additions and 32 deletions

View File

@ -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",

89
main.py
View File

@ -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,42 +1328,79 @@ 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
)
# 5. Сохранение токена сброса в Redis
reset_token_key = f"reset_token:{token}"
await redis_client.set(reset_token_key, json.dumps(token_data), ex=3600)
reset_link = f"{str(request.base_url).rstrip('/')}/restore/{token}"
# 6. Генерация ссылки для сброса
base_url = str(request.base_url).rstrip('/')
reset_link = f"{base_url}/restore/{token}"
email_task.notify.delay(
user['email'],
{'link': reset_link, 'email': user['email']},
'Восстановление пароля',
'restore.html'
)
# 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="Ошибка отправки письма")
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,