diff --git a/services/api/app/api/auth.py b/services/api/app/api/auth.py index fc464db..35953c1 100644 --- a/services/api/app/api/auth.py +++ b/services/api/app/api/auth.py @@ -213,15 +213,22 @@ async def resend_verification( detail="Email уже подтверждён", ) - if not current_user.verification_token: - current_user.verification_token = secrets.token_urlsafe(32) + # current_user из dependency может быть из Redis-кэша (без verification_token + # и не привязан к сессии — commit/refresh на нём не сработают) — грузим "живого" + result = await db.execute(select(User).where(User.id == current_user.id)) + user = result.scalar_one_or_none() + if user is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден") + + if not user.verification_token: + user.verification_token = secrets.token_urlsafe(32) await db.commit() - await db.refresh(current_user) + await db.refresh(user) try: celery_app.send_task( "notify.send_verification", - args=[current_user.email, current_user.name, current_user.verification_token], + args=[user.email, user.name, user.verification_token], queue="queue.notify", ) except Exception as e: diff --git a/services/api/app/core/security.py b/services/api/app/core/security.py index e063c42..6da220d 100644 --- a/services/api/app/core/security.py +++ b/services/api/app/core/security.py @@ -2,6 +2,7 @@ import json import logging +from dataclasses import dataclass from datetime import UTC, datetime, timedelta from typing import Any @@ -65,6 +66,28 @@ def _decode_token(token: str) -> int: ) from None +@dataclass +class CachedUser: + """Пользователь из Redis-кэша — обычный dataclass, НЕ SQLAlchemy-модель. + + Раньше здесь был User.__new__(User) + __dict__.update(data) — казалось + эквивалентом, но замапленные атрибуты User (id, email, ...) — дескрипторы + данных: их __get__ обращается к self.impl, который берётся из InstanceState, + а у объекта, созданного в обход __init__/ORM-машинерии, состояния нет. + Итог — AttributeError на любое обращение к полю из кэша (нашли на + GET /tasks/{id}: current_user.id падал 500-й, как только юзер брался не + из БД, а из кэша). Обычный dataclass с теми же именами полей — просто + plain-атрибуты, без дескрипторов, падать нечему. + """ + + id: int + email: str + name: str + plan: str + is_verified: bool + is_admin: bool + + async def _load_user(user_id: int, db: AsyncSession): """Загрузить пользователя из Redis-кэша или из БД.""" from app.models.user import User # избегаем circular import на уровне модуля @@ -75,12 +98,7 @@ async def _load_user(user_id: int, db: AsyncSession): # Пробуем кэш cached = await r.get(cache_key) if cached: - data = json.loads(cached) - # Возвращаем "живой" объект из БД только по id, но без лишнего SELECT - # Создаём User без ORM-связей (достаточно для проверок в роутерах) - u = User.__new__(User) - u.__dict__.update(data) - return u + return CachedUser(**json.loads(cached)) # Кэш пустой — идём в БД result = await db.execute(select(User).where(User.id == user_id))