fix
This commit is contained in:
parent
d60dc1f8c2
commit
b990cd91f0
@ -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
91
main.py
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user