2025-06-02 21:50:58 +03:00
|
|
|
|
"""
|
|
|
|
|
|
Управление токенами подтверждения
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import secrets
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
|
|
from services.redis import redis as redis_adapter
|
|
|
|
|
|
from utils.logger import root_logger as logger
|
|
|
|
|
|
|
|
|
|
|
|
from .base import BaseTokenManager
|
|
|
|
|
|
from .types import TokenData
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VerificationTokenManager(BaseTokenManager):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Менеджер токенов подтверждения
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
async def create_verification_token(
|
|
|
|
|
|
self,
|
|
|
|
|
|
user_id: str,
|
|
|
|
|
|
verification_type: str,
|
|
|
|
|
|
data: TokenData,
|
2025-08-17 16:33:54 +03:00
|
|
|
|
ttl: int | None = None,
|
2025-06-02 21:50:58 +03:00
|
|
|
|
) -> str:
|
|
|
|
|
|
"""Создает токен подтверждения"""
|
|
|
|
|
|
token_data = {"verification_type": verification_type, **data}
|
|
|
|
|
|
|
|
|
|
|
|
# TTL по типу подтверждения
|
|
|
|
|
|
if ttl is None:
|
|
|
|
|
|
verification_ttls = {
|
|
|
|
|
|
"email_change": 3600, # 1 час
|
|
|
|
|
|
"phone_change": 600, # 10 минут
|
|
|
|
|
|
"password_reset": 1800, # 30 минут
|
|
|
|
|
|
}
|
|
|
|
|
|
ttl = verification_ttls.get(verification_type, 3600)
|
|
|
|
|
|
|
|
|
|
|
|
return await self._create_verification_token(user_id, token_data, ttl)
|
|
|
|
|
|
|
|
|
|
|
|
async def _create_verification_token(
|
2025-08-17 16:33:54 +03:00
|
|
|
|
self, user_id: str, token_data: TokenData, ttl: int, token: str | None = None
|
2025-06-02 21:50:58 +03:00
|
|
|
|
) -> str:
|
|
|
|
|
|
"""Оптимизированное создание токена подтверждения"""
|
|
|
|
|
|
verification_token = token or secrets.token_urlsafe(32)
|
|
|
|
|
|
token_key = self._make_token_key("verification", user_id, verification_token)
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем метаданные
|
|
|
|
|
|
token_data.update({"user_id": user_id, "token_type": "verification", "created_at": int(time.time())})
|
|
|
|
|
|
|
|
|
|
|
|
# Отменяем предыдущие токены того же типа
|
|
|
|
|
|
verification_type = token_data.get("verification_type", "unknown")
|
|
|
|
|
|
await self._cancel_verification_tokens_optimized(user_id, verification_type)
|
|
|
|
|
|
|
|
|
|
|
|
# Используем SETEX для атомарной операции установки с TTL
|
|
|
|
|
|
serialized_data = json.dumps(token_data, ensure_ascii=False)
|
|
|
|
|
|
await redis_adapter.execute("setex", token_key, ttl, serialized_data)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Создан токен подтверждения {verification_type} для пользователя {user_id}")
|
|
|
|
|
|
return verification_token
|
|
|
|
|
|
|
2025-08-17 16:33:54 +03:00
|
|
|
|
async def get_verification_token_data(self, token: str) -> TokenData | None:
|
2025-06-02 21:50:58 +03:00
|
|
|
|
"""Получает данные токена подтверждения"""
|
|
|
|
|
|
token_key = self._make_token_key("verification", "", token)
|
|
|
|
|
|
return await redis_adapter.get_and_deserialize(token_key)
|
|
|
|
|
|
|
2025-08-17 16:33:54 +03:00
|
|
|
|
async def validate_verification_token(self, token_str: str) -> tuple[bool, TokenData | None]:
|
2025-06-02 21:50:58 +03:00
|
|
|
|
"""Проверяет валидность токена подтверждения"""
|
|
|
|
|
|
token_key = self._make_token_key("verification", "", token_str)
|
|
|
|
|
|
token_data = await redis_adapter.get_and_deserialize(token_key)
|
|
|
|
|
|
if token_data:
|
|
|
|
|
|
return True, token_data
|
|
|
|
|
|
return False, None
|
|
|
|
|
|
|
2025-08-17 16:33:54 +03:00
|
|
|
|
async def confirm_verification_token(self, token_str: str) -> TokenData | None:
|
2025-06-02 21:50:58 +03:00
|
|
|
|
"""Подтверждает и использует токен подтверждения (одноразовый)"""
|
|
|
|
|
|
token_data = await self.get_verification_token_data(token_str)
|
|
|
|
|
|
if token_data:
|
|
|
|
|
|
# Удаляем токен после использования
|
|
|
|
|
|
await self.revoke_verification_token(token_str)
|
|
|
|
|
|
return token_data
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
async def revoke_verification_token(self, token: str) -> bool:
|
|
|
|
|
|
"""Отзывает токен подтверждения"""
|
|
|
|
|
|
token_key = self._make_token_key("verification", "", token)
|
|
|
|
|
|
result = await redis_adapter.delete(token_key)
|
|
|
|
|
|
return result > 0
|
|
|
|
|
|
|
|
|
|
|
|
async def revoke_user_verification_tokens(self, user_id: str) -> int:
|
|
|
|
|
|
"""Оптимизированный отзыв токенов подтверждения пользователя используя SCAN вместо KEYS"""
|
|
|
|
|
|
count = 0
|
|
|
|
|
|
cursor = 0
|
|
|
|
|
|
delete_keys = []
|
|
|
|
|
|
|
|
|
|
|
|
# Используем SCAN для безопасного поиска токенов
|
|
|
|
|
|
while True:
|
|
|
|
|
|
cursor, keys = await redis_adapter.execute("scan", cursor, "verification_token:*", 100)
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем каждый ключ в пакете
|
|
|
|
|
|
if keys:
|
|
|
|
|
|
async with redis_adapter.pipeline() as pipe:
|
|
|
|
|
|
for key in keys:
|
|
|
|
|
|
await pipe.get(key)
|
|
|
|
|
|
results = await pipe.execute()
|
|
|
|
|
|
|
2025-08-17 16:33:54 +03:00
|
|
|
|
for key, data in zip(keys, results, strict=False):
|
2025-06-02 21:50:58 +03:00
|
|
|
|
if data:
|
|
|
|
|
|
try:
|
|
|
|
|
|
token_data = json.loads(data)
|
|
|
|
|
|
if token_data.get("user_id") == user_id:
|
|
|
|
|
|
delete_keys.append(key)
|
|
|
|
|
|
count += 1
|
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if cursor == 0:
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# Удаляем найденные токены пакетно
|
|
|
|
|
|
if delete_keys:
|
|
|
|
|
|
await redis_adapter.delete(*delete_keys)
|
|
|
|
|
|
|
|
|
|
|
|
return count
|
|
|
|
|
|
|
|
|
|
|
|
async def _cancel_verification_tokens_optimized(self, user_id: str, verification_type: str) -> None:
|
|
|
|
|
|
"""Оптимизированная отмена токенов подтверждения используя SCAN"""
|
|
|
|
|
|
cursor = 0
|
|
|
|
|
|
delete_keys = []
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
cursor, keys = await redis_adapter.execute("scan", cursor, "verification_token:*", 100)
|
|
|
|
|
|
|
|
|
|
|
|
if keys:
|
|
|
|
|
|
# Получаем данные пакетно
|
|
|
|
|
|
async with redis_adapter.pipeline() as pipe:
|
|
|
|
|
|
for key in keys:
|
|
|
|
|
|
await pipe.get(key)
|
|
|
|
|
|
results = await pipe.execute()
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем какие токены нужно удалить
|
2025-08-17 16:33:54 +03:00
|
|
|
|
for key, data in zip(keys, results, strict=False):
|
2025-06-02 21:50:58 +03:00
|
|
|
|
if data:
|
|
|
|
|
|
try:
|
|
|
|
|
|
token_data = json.loads(data)
|
|
|
|
|
|
if (
|
|
|
|
|
|
token_data.get("user_id") == user_id
|
|
|
|
|
|
and token_data.get("verification_type") == verification_type
|
|
|
|
|
|
):
|
|
|
|
|
|
delete_keys.append(key)
|
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if cursor == 0:
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# Удаляем найденные токены пакетно
|
|
|
|
|
|
if delete_keys:
|
|
|
|
|
|
await redis_adapter.delete(*delete_keys)
|