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", " restore_data: RestoreRequest = Body(...),\n",
"):\n", "):\n",
" # 1. Валидация и очистка входных данных\n", " # 1. Валидация и очистка входных данных\n",
" identifier = restore_data.identifier.strip().lower()\n", " identifier = restore_data.identifier.strip()\n",
" \n", " \n",
" if not identifier or not is_valid_email(identifier):\n", " if not identifier or not is_valid_email(identifier):\n",
" raise HTTPException(status_code=400, detail=\"Некорректный формат email\")\n", " raise HTTPException(status_code=400, detail=\"Некорректный формат email\")\n",
@ -1821,7 +1821,10 @@
" await redis_client.set(rate_limit_key, \"1\", ex=60)\n", " await redis_client.set(rate_limit_key, \"1\", ex=60)\n",
"\n", "\n",
" # 3. Поиск пользователя\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", " \n",
" if not user:\n", " if not user:\n",
" # Безопасность: всегда возвращать одинаковое время ответа/структуру сообщения, если возможно\n", " # Безопасность: всегда возвращать одинаковое время ответа/структуру сообщения, если возможно\n",

91
main.py
View File

@ -782,12 +782,6 @@ async def login(
identifier = safe_strip(login_data.identifier) identifier = safe_strip(login_data.identifier)
await check_bruteforce(identifier) await check_bruteforce(identifier)
users = [doc async for doc in users_collection.find({
"$or": [
{"email": identifier},
{"email": identifier.lower()}
]
})]
or_conditions = [ or_conditions = [
{"email": identifier}, {"email": identifier},
{"email": identifier.lower()}, {"email": identifier.lower()},
@ -1334,43 +1328,80 @@ async def restore_api(
request: Request, request: Request,
restore_data: RestoreRequest = Body(...), restore_data: RestoreRequest = Body(...),
): ):
email = restore_data.identifier.strip().lower() # 1. Валидация и очистка входных данных
next = restore_data.next 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': [ user = await users_collection.find_one({'$or': [
{'email': email}, {'email': identifier},
{'email': email.lower()} {'email': identifier.lower()}
]}) ]})
if not user: if not user:
raise HTTPException(status_code=404, detail='Email не найден') # Безопасность: всегда возвращать одинаковое время ответа/структуру сообщения, если возможно
# Но для удобства использования мы можем сказать "не найден".
# Чтобы предотвратить перечисление, некоторые приложения всегда возвращают "OK", но отправляют письмо только если найдено.
# Здесь мы придерживаемся вашей оригинальной логики, но помните о риске перечисления.
raise HTTPException(status_code=404, detail="Пользователь с таким email не найден")
# 4. Генерация токена
token = secrets.token_urlsafe(32) token = secrets.token_urlsafe(32)
expires_at = datetime.utcnow() + timedelta(hours=1) expires_at = datetime.now(timezone.utc) + timedelta(hours=1)
token_data = { token_data = {
"user_id": str(user["_id"]), "user_id": str(user["_id"]),
"email": user["email"], "email": user["email"],
"next": next, "next": restore_data.next,
"expires_at": expires_at.isoformat() "expires_at": expires_at.isoformat()
} }
await redis_client.set( # 5. Сохранение токена сброса в Redis
f"reset_token:{token}", reset_token_key = f"reset_token:{token}"
json.dumps(token_data), await redis_client.set(reset_token_key, json.dumps(token_data), ex=3600)
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}"
# 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} 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) @app.get("/login", response_class=HTMLResponse)
async def login_( async def login_(
@ -2577,7 +2608,7 @@ async def post_competition(
"serviceSocial": payload.serviceSocial, "serviceSocial": payload.serviceSocial,
"userSocial": payload.userSocial, "userSocial": payload.userSocial,
"short_desc": safe_strip(payload.short_desc) if payload.short_desc else None, "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, "poster_orig_id": poster_id,
"rules_file_id": rules_id, "rules_file_id": rules_id,
"rules_link": payload.rules_link, "rules_link": payload.rules_link,