2025-05-16 06:23:48 +00:00
|
|
|
|
import json
|
2025-06-01 23:56:11 +00:00
|
|
|
|
import secrets
|
2025-05-19 21:00:24 +00:00
|
|
|
|
import time
|
2025-06-01 23:56:11 +00:00
|
|
|
|
from typing import Any, Dict, Literal, Optional, Union
|
2023-10-26 21:07:35 +00:00
|
|
|
|
|
2024-11-01 12:06:21 +00:00
|
|
|
|
from auth.jwtcodec import JWTCodec
|
2025-02-11 09:00:35 +00:00
|
|
|
|
from auth.validations import AuthInput
|
|
|
|
|
from services.redis import redis
|
2025-05-16 06:23:48 +00:00
|
|
|
|
from utils.logger import root_logger as logger
|
2024-11-01 12:06:21 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Типы токенов
|
|
|
|
|
TokenType = Literal["session", "verification", "oauth_access", "oauth_refresh"]
|
|
|
|
|
|
|
|
|
|
# TTL по умолчанию для разных типов токенов
|
|
|
|
|
DEFAULT_TTL = {
|
|
|
|
|
"session": 30 * 24 * 60 * 60, # 30 дней
|
|
|
|
|
"verification": 3600, # 1 час
|
|
|
|
|
"oauth_access": 3600, # 1 час
|
|
|
|
|
"oauth_refresh": 86400 * 30, # 30 дней
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-17 18:12:14 +00:00
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
class TokenStorage:
|
|
|
|
|
"""
|
2025-06-01 23:56:11 +00:00
|
|
|
|
Единый менеджер всех типов токенов в системе:
|
|
|
|
|
- Токены сессий (session)
|
|
|
|
|
- Токены подтверждения (verification)
|
|
|
|
|
- OAuth токены (oauth_access, oauth_refresh)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
"""
|
|
|
|
|
|
2025-05-19 21:00:24 +00:00
|
|
|
|
@staticmethod
|
2025-06-01 23:56:11 +00:00
|
|
|
|
def _make_token_key(token_type: TokenType, identifier: str, token: Optional[str] = None) -> str:
|
2025-05-19 21:00:24 +00:00
|
|
|
|
"""
|
2025-06-01 23:56:11 +00:00
|
|
|
|
Создает унифицированный ключ для токена
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
Args:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
token_type: Тип токена
|
|
|
|
|
identifier: Идентификатор (user_id, user_id:provider, etc)
|
|
|
|
|
token: Сам токен (для session и verification)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: Ключ токена
|
|
|
|
|
"""
|
2025-06-01 23:56:11 +00:00
|
|
|
|
if token_type == "session":
|
|
|
|
|
return f"session:{token}"
|
|
|
|
|
if token_type == "verification":
|
|
|
|
|
return f"verification_token:{token}"
|
|
|
|
|
if token_type == "oauth_access":
|
|
|
|
|
return f"oauth_access:{identifier}"
|
|
|
|
|
if token_type == "oauth_refresh":
|
|
|
|
|
return f"oauth_refresh:{identifier}"
|
|
|
|
|
raise ValueError(f"Неизвестный тип токена: {token_type}")
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
@staticmethod
|
2025-06-01 23:56:11 +00:00
|
|
|
|
def _make_user_tokens_key(user_id: str, token_type: TokenType) -> str:
|
|
|
|
|
"""Создает ключ для списка токенов пользователя"""
|
|
|
|
|
return f"user_tokens:{user_id}:{token_type}"
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
async def create_token(
|
|
|
|
|
cls,
|
|
|
|
|
token_type: TokenType,
|
|
|
|
|
user_id: str,
|
|
|
|
|
data: Dict[str, Any],
|
|
|
|
|
ttl: Optional[int] = None,
|
|
|
|
|
token: Optional[str] = None,
|
|
|
|
|
provider: Optional[str] = None,
|
|
|
|
|
) -> str:
|
2025-05-19 21:00:24 +00:00
|
|
|
|
"""
|
2025-06-01 23:56:11 +00:00
|
|
|
|
Универсальный метод создания токена любого типа
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
Args:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
token_type: Тип токена
|
2025-05-19 21:00:24 +00:00
|
|
|
|
user_id: ID пользователя
|
2025-06-01 23:56:11 +00:00
|
|
|
|
data: Данные токена
|
|
|
|
|
ttl: Время жизни (по умолчанию из DEFAULT_TTL)
|
|
|
|
|
token: Существующий токен (для verification)
|
|
|
|
|
provider: OAuth провайдер (для oauth токенов)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
Returns:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
str: Токен или ключ токена
|
2025-05-19 21:00:24 +00:00
|
|
|
|
"""
|
2025-06-01 23:56:11 +00:00
|
|
|
|
if ttl is None:
|
|
|
|
|
ttl = DEFAULT_TTL[token_type]
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Подготавливаем данные токена
|
|
|
|
|
token_data = {"user_id": user_id, "token_type": token_type, "created_at": int(time.time()), **data}
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
if token_type == "session":
|
|
|
|
|
# Генерируем новый токен сессии
|
|
|
|
|
session_token = cls.generate_token()
|
|
|
|
|
token_key = cls._make_token_key(token_type, user_id, session_token)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Сохраняем данные сессии
|
|
|
|
|
for field, value in token_data.items():
|
|
|
|
|
await redis.hset(token_key, field, str(value))
|
|
|
|
|
await redis.expire(token_key, ttl)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Добавляем в список сессий пользователя
|
|
|
|
|
user_tokens_key = cls._make_user_tokens_key(user_id, token_type)
|
|
|
|
|
await redis.sadd(user_tokens_key, session_token)
|
|
|
|
|
await redis.expire(user_tokens_key, ttl)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.info(f"Создан токен сессии для пользователя {user_id}")
|
|
|
|
|
return session_token
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
if token_type == "verification":
|
|
|
|
|
# Используем переданный токен или генерируем новый
|
|
|
|
|
verification_token = token or secrets.token_urlsafe(32)
|
|
|
|
|
token_key = cls._make_token_key(token_type, user_id, verification_token)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Отменяем предыдущие токены того же типа
|
|
|
|
|
verification_type = data.get("verification_type", "unknown")
|
|
|
|
|
await cls._cancel_verification_tokens(user_id, verification_type)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Сохраняем токен подтверждения
|
|
|
|
|
await redis.serialize_and_set(token_key, token_data, ex=ttl)
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.info(f"Создан токен подтверждения {verification_type} для пользователя {user_id}")
|
|
|
|
|
return verification_token
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
if token_type in ["oauth_access", "oauth_refresh"]:
|
|
|
|
|
if not provider:
|
|
|
|
|
raise ValueError("OAuth токены требуют указания провайдера")
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
identifier = f"{user_id}:{provider}"
|
|
|
|
|
token_key = cls._make_token_key(token_type, identifier)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Добавляем провайдера в данные
|
|
|
|
|
token_data["provider"] = provider
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Сохраняем OAuth токен
|
|
|
|
|
await redis.serialize_and_set(token_key, token_data, ex=ttl)
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.info(f"Создан {token_type} токен для пользователя {user_id}, провайдер {provider}")
|
|
|
|
|
return token_key
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
raise ValueError(f"Неподдерживаемый тип токена: {token_type}")
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def get_token_data(
|
|
|
|
|
cls,
|
|
|
|
|
token_type: TokenType,
|
|
|
|
|
token_or_identifier: str,
|
|
|
|
|
user_id: Optional[str] = None,
|
|
|
|
|
provider: Optional[str] = None,
|
|
|
|
|
) -> Optional[Dict[str, Any]]:
|
2025-05-19 21:00:24 +00:00
|
|
|
|
"""
|
2025-06-01 23:56:11 +00:00
|
|
|
|
Универсальный метод получения данных токена
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
Args:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
token_type: Тип токена
|
|
|
|
|
token_or_identifier: Токен или идентификатор
|
|
|
|
|
user_id: ID пользователя (для OAuth)
|
|
|
|
|
provider: OAuth провайдер
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
Returns:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
Dict с данными токена или None
|
2025-05-19 21:00:24 +00:00
|
|
|
|
"""
|
2025-06-01 23:56:11 +00:00
|
|
|
|
try:
|
|
|
|
|
if token_type == "session":
|
|
|
|
|
token_key = cls._make_token_key(token_type, "", token_or_identifier)
|
|
|
|
|
token_data = await redis.hgetall(token_key)
|
|
|
|
|
if token_data:
|
|
|
|
|
# Обновляем время последней активности
|
|
|
|
|
await redis.hset(token_key, "last_activity", str(int(time.time())))
|
|
|
|
|
return {k: v for k, v in token_data.items()}
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
if token_type == "verification":
|
|
|
|
|
token_key = cls._make_token_key(token_type, "", token_or_identifier)
|
|
|
|
|
return await redis.get_and_deserialize(token_key)
|
|
|
|
|
|
|
|
|
|
if token_type in ["oauth_access", "oauth_refresh"]:
|
|
|
|
|
if not user_id or not provider:
|
|
|
|
|
raise ValueError("OAuth токены требуют user_id и provider")
|
|
|
|
|
|
|
|
|
|
identifier = f"{user_id}:{provider}"
|
|
|
|
|
token_key = cls._make_token_key(token_type, identifier)
|
|
|
|
|
token_data = await redis.get_and_deserialize(token_key)
|
|
|
|
|
|
|
|
|
|
if token_data:
|
|
|
|
|
# Добавляем информацию о TTL
|
|
|
|
|
ttl = await redis.execute("TTL", token_key)
|
|
|
|
|
if ttl > 0:
|
|
|
|
|
token_data["ttl_remaining"] = ttl
|
|
|
|
|
return token_data
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка получения токена {token_type}: {e}")
|
|
|
|
|
return None
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def validate_token(
|
|
|
|
|
cls, token: str, token_type: Optional[TokenType] = None
|
|
|
|
|
) -> tuple[bool, Optional[dict[str, Any]]]:
|
2025-05-19 21:00:24 +00:00
|
|
|
|
"""
|
|
|
|
|
Проверяет валидность токена
|
|
|
|
|
|
|
|
|
|
Args:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
token: Токен для проверки
|
|
|
|
|
token_type: Тип токена (если не указан - определяется автоматически)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
Returns:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
Tuple[bool, Dict]: (Валиден ли токен, данные токена)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
"""
|
|
|
|
|
try:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Для JWT токенов (сессии) - декодируем
|
|
|
|
|
if not token_type or token_type == "session":
|
|
|
|
|
payload = JWTCodec.decode(token)
|
|
|
|
|
if payload:
|
|
|
|
|
user_id = payload.user_id
|
|
|
|
|
username = payload.username
|
|
|
|
|
|
|
|
|
|
# Проверяем в разных форматах для совместимости
|
|
|
|
|
old_token_key = f"{user_id}-{username}-{token}"
|
|
|
|
|
new_token_key = cls._make_token_key("session", user_id, token)
|
|
|
|
|
|
|
|
|
|
old_exists = await redis.exists(old_token_key)
|
|
|
|
|
new_exists = await redis.exists(new_token_key)
|
|
|
|
|
|
|
|
|
|
if old_exists or new_exists:
|
|
|
|
|
# Получаем данные из актуального хранилища
|
|
|
|
|
if new_exists:
|
|
|
|
|
token_data = await redis.hgetall(new_token_key)
|
|
|
|
|
else:
|
|
|
|
|
token_data = await redis.hgetall(old_token_key)
|
|
|
|
|
# Миграция в новый формат
|
|
|
|
|
if not new_exists:
|
|
|
|
|
for field, value in token_data.items():
|
|
|
|
|
await redis.hset(new_token_key, field, value)
|
|
|
|
|
await redis.expire(new_token_key, DEFAULT_TTL["session"])
|
|
|
|
|
|
|
|
|
|
return True, {k: v for k, v in token_data.items()}
|
|
|
|
|
|
|
|
|
|
# Для токенов подтверждения - прямая проверка
|
|
|
|
|
if not token_type or token_type == "verification":
|
|
|
|
|
token_key = cls._make_token_key("verification", "", token)
|
|
|
|
|
token_data = await redis.get_and_deserialize(token_key)
|
|
|
|
|
if token_data:
|
|
|
|
|
return True, token_data
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
return False, None
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-05-19 21:00:24 +00:00
|
|
|
|
except Exception as e:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.error(f"Ошибка валидации токена: {e}")
|
2025-05-19 21:00:24 +00:00
|
|
|
|
return False, None
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def revoke_token(
|
|
|
|
|
cls,
|
|
|
|
|
token_type: TokenType,
|
|
|
|
|
token_or_identifier: str,
|
|
|
|
|
user_id: Optional[str] = None,
|
|
|
|
|
provider: Optional[str] = None,
|
|
|
|
|
) -> bool:
|
2025-05-19 21:00:24 +00:00
|
|
|
|
"""
|
2025-06-01 23:56:11 +00:00
|
|
|
|
Универсальный метод отзыва токена
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
Args:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
token_type: Тип токена
|
|
|
|
|
token_or_identifier: Токен или идентификатор
|
|
|
|
|
user_id: ID пользователя
|
|
|
|
|
provider: OAuth провайдер
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
Returns:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
bool: Успех операции
|
2025-05-19 21:00:24 +00:00
|
|
|
|
"""
|
|
|
|
|
try:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
if token_type == "session":
|
|
|
|
|
# Декодируем JWT для получения данных
|
|
|
|
|
payload = JWTCodec.decode(token_or_identifier)
|
|
|
|
|
if payload:
|
|
|
|
|
user_id = payload.user_id
|
|
|
|
|
username = payload.username
|
|
|
|
|
|
|
|
|
|
# Удаляем в обоих форматах
|
|
|
|
|
old_token_key = f"{user_id}-{username}-{token_or_identifier}"
|
|
|
|
|
new_token_key = cls._make_token_key(token_type, user_id, token_or_identifier)
|
|
|
|
|
user_tokens_key = cls._make_user_tokens_key(user_id, token_type)
|
|
|
|
|
|
|
|
|
|
result1 = await redis.delete(old_token_key)
|
|
|
|
|
result2 = await redis.delete(new_token_key)
|
|
|
|
|
result3 = await redis.srem(user_tokens_key, token_or_identifier)
|
|
|
|
|
|
|
|
|
|
return result1 > 0 or result2 > 0 or result3 > 0
|
|
|
|
|
|
|
|
|
|
elif token_type == "verification":
|
|
|
|
|
token_key = cls._make_token_key(token_type, "", token_or_identifier)
|
|
|
|
|
result = await redis.delete(token_key)
|
|
|
|
|
return result > 0
|
|
|
|
|
|
|
|
|
|
elif token_type in ["oauth_access", "oauth_refresh"]:
|
|
|
|
|
if not user_id or not provider:
|
|
|
|
|
raise ValueError("OAuth токены требуют user_id и provider")
|
|
|
|
|
|
|
|
|
|
identifier = f"{user_id}:{provider}"
|
|
|
|
|
token_key = cls._make_token_key(token_type, identifier)
|
|
|
|
|
result = await redis.delete(token_key)
|
|
|
|
|
return result > 0
|
|
|
|
|
|
|
|
|
|
return False
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-05-19 21:00:24 +00:00
|
|
|
|
except Exception as e:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.error(f"Ошибка отзыва токена {token_type}: {e}")
|
2025-05-19 21:00:24 +00:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def revoke_user_tokens(cls, user_id: str, token_type: Optional[TokenType] = None) -> int:
|
2025-05-19 21:00:24 +00:00
|
|
|
|
"""
|
2025-06-01 23:56:11 +00:00
|
|
|
|
Отзывает все токены пользователя определенного типа или все
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
user_id: ID пользователя
|
2025-06-01 23:56:11 +00:00
|
|
|
|
token_type: Тип токенов для отзыва (None = все типы)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
Returns:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
int: Количество отозванных токенов
|
2025-05-19 21:00:24 +00:00
|
|
|
|
"""
|
2025-06-01 23:56:11 +00:00
|
|
|
|
count = 0
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
try:
|
|
|
|
|
types_to_revoke = (
|
|
|
|
|
[token_type] if token_type else ["session", "verification", "oauth_access", "oauth_refresh"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for t_type in types_to_revoke:
|
|
|
|
|
if t_type == "session":
|
|
|
|
|
user_tokens_key = cls._make_user_tokens_key(user_id, t_type)
|
|
|
|
|
tokens = await redis.smembers(user_tokens_key)
|
|
|
|
|
|
|
|
|
|
for token in tokens:
|
|
|
|
|
token_str = token.decode("utf-8") if isinstance(token, bytes) else str(token)
|
|
|
|
|
success = await cls.revoke_token(t_type, token_str, user_id)
|
|
|
|
|
if success:
|
|
|
|
|
count += 1
|
|
|
|
|
|
|
|
|
|
await redis.delete(user_tokens_key)
|
|
|
|
|
|
|
|
|
|
elif t_type == "verification":
|
|
|
|
|
# Ищем все токены подтверждения пользователя
|
|
|
|
|
pattern = "verification_token:*"
|
|
|
|
|
keys = await redis.keys(pattern)
|
|
|
|
|
|
|
|
|
|
for key in keys:
|
|
|
|
|
token_data = await redis.get_and_deserialize(key)
|
|
|
|
|
if token_data and token_data.get("user_id") == user_id:
|
|
|
|
|
await redis.delete(key)
|
|
|
|
|
count += 1
|
|
|
|
|
|
|
|
|
|
elif t_type in ["oauth_access", "oauth_refresh"]:
|
|
|
|
|
# Ищем OAuth токены по паттерну
|
|
|
|
|
pattern = f"{t_type}:{user_id}:*"
|
|
|
|
|
keys = await redis.keys(pattern)
|
|
|
|
|
|
|
|
|
|
for key in keys:
|
|
|
|
|
await redis.delete(key)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
count += 1
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.info(f"Отозвано {count} токенов для пользователя {user_id}")
|
|
|
|
|
return count
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка отзыва токенов пользователя: {e}")
|
2025-05-19 21:00:24 +00:00
|
|
|
|
return count
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
async def _cancel_verification_tokens(user_id: str, verification_type: str) -> None:
|
|
|
|
|
"""Отменяет предыдущие токены подтверждения определенного типа"""
|
|
|
|
|
try:
|
|
|
|
|
pattern = "verification_token:*"
|
|
|
|
|
keys = await redis.keys(pattern)
|
|
|
|
|
|
|
|
|
|
for key in keys:
|
|
|
|
|
token_data = await redis.get_and_deserialize(key)
|
|
|
|
|
if (
|
|
|
|
|
token_data
|
|
|
|
|
and token_data.get("user_id") == user_id
|
|
|
|
|
and token_data.get("verification_type") == verification_type
|
|
|
|
|
):
|
|
|
|
|
await redis.delete(key)
|
|
|
|
|
|
2025-05-19 21:00:24 +00:00
|
|
|
|
except Exception as e:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.error(f"Ошибка отмены токенов подтверждения: {e}")
|
|
|
|
|
|
|
|
|
|
# === УДОБНЫЕ МЕТОДЫ ДЛЯ СЕССИЙ ===
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def create_session(
|
|
|
|
|
cls,
|
|
|
|
|
user_id: str,
|
|
|
|
|
auth_data: Optional[dict] = None,
|
|
|
|
|
username: Optional[str] = None,
|
|
|
|
|
device_info: Optional[dict] = None,
|
|
|
|
|
) -> str:
|
|
|
|
|
"""Создает токен сессии"""
|
|
|
|
|
session_data = {}
|
|
|
|
|
|
|
|
|
|
if auth_data:
|
|
|
|
|
session_data["auth_data"] = json.dumps(auth_data)
|
|
|
|
|
if username:
|
|
|
|
|
session_data["username"] = username
|
|
|
|
|
if device_info:
|
|
|
|
|
session_data["device_info"] = json.dumps(device_info)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
return await cls.create_token("session", user_id, session_data)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
@classmethod
|
|
|
|
|
async def get_session_data(cls, token: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Получает данные сессии"""
|
|
|
|
|
valid, data = await cls.validate_token(token, "session")
|
2025-05-19 21:00:24 +00:00
|
|
|
|
return data if valid else None
|
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# === УДОБНЫЕ МЕТОДЫ ДЛЯ ТОКЕНОВ ПОДТВЕРЖДЕНИЯ ===
|
2022-09-17 18:12:14 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
@classmethod
|
|
|
|
|
async def create_verification_token(
|
|
|
|
|
cls,
|
|
|
|
|
user_id: str,
|
|
|
|
|
verification_type: str,
|
|
|
|
|
data: Dict[str, Any],
|
|
|
|
|
ttl: Optional[int] = None,
|
|
|
|
|
) -> 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 cls.create_token("verification", user_id, token_data, ttl)
|
2022-09-17 18:12:14 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
@classmethod
|
|
|
|
|
async def confirm_verification_token(cls, token_str: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Подтверждает и использует токен подтверждения (одноразовый)"""
|
|
|
|
|
token_data = await cls.get_token_data("verification", token_str)
|
|
|
|
|
if token_data:
|
|
|
|
|
# Удаляем токен после использования
|
|
|
|
|
await cls.revoke_token("verification", token_str)
|
|
|
|
|
return token_data
|
|
|
|
|
return None
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# === УДОБНЫЕ МЕТОДЫ ДЛЯ OAUTH ТОКЕНОВ ===
|
2022-11-24 14:31:52 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
@classmethod
|
|
|
|
|
async def store_oauth_tokens(
|
|
|
|
|
cls,
|
|
|
|
|
user_id: str,
|
|
|
|
|
provider: str,
|
|
|
|
|
access_token: str,
|
|
|
|
|
refresh_token: Optional[str] = None,
|
|
|
|
|
expires_in: Optional[int] = None,
|
|
|
|
|
additional_data: Optional[Dict[str, Any]] = None,
|
|
|
|
|
) -> bool:
|
|
|
|
|
"""Сохраняет OAuth токены"""
|
|
|
|
|
try:
|
|
|
|
|
# Сохраняем access token
|
|
|
|
|
access_data = {
|
|
|
|
|
"token": access_token,
|
|
|
|
|
"provider": provider,
|
|
|
|
|
"expires_in": expires_in,
|
|
|
|
|
**(additional_data or {}),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
access_ttl = expires_in if expires_in else DEFAULT_TTL["oauth_access"]
|
|
|
|
|
await cls.create_token("oauth_access", user_id, access_data, access_ttl, provider=provider)
|
|
|
|
|
|
|
|
|
|
# Сохраняем refresh token если есть
|
|
|
|
|
if refresh_token:
|
|
|
|
|
refresh_data = {
|
|
|
|
|
"token": refresh_token,
|
|
|
|
|
"provider": provider,
|
|
|
|
|
}
|
|
|
|
|
await cls.create_token("oauth_refresh", user_id, refresh_data, provider=provider)
|
2022-11-24 14:31:52 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
return True
|
2022-11-24 14:31:52 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка сохранения OAuth токенов: {e}")
|
|
|
|
|
return False
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
@classmethod
|
|
|
|
|
async def get_oauth_token(cls, user_id: int, provider: str, token_type: str = "access") -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Получает OAuth токен"""
|
|
|
|
|
oauth_type = f"oauth_{token_type}"
|
|
|
|
|
if oauth_type in ["oauth_access", "oauth_refresh"]:
|
|
|
|
|
return await cls.get_token_data(oauth_type, "", user_id, provider) # type: ignore[arg-type]
|
|
|
|
|
return None
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
@classmethod
|
|
|
|
|
async def revoke_oauth_tokens(cls, user_id: str, provider: str) -> bool:
|
|
|
|
|
"""Удаляет все OAuth токены для провайдера"""
|
2025-05-16 06:23:48 +00:00
|
|
|
|
try:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
result1 = await cls.revoke_token("oauth_access", "", user_id, provider)
|
|
|
|
|
result2 = await cls.revoke_token("oauth_refresh", "", user_id, provider)
|
|
|
|
|
return result1 or result2
|
2025-05-16 06:23:48 +00:00
|
|
|
|
except Exception as e:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.error(f"Ошибка удаления OAuth токенов: {e}")
|
2025-05-16 06:23:48 +00:00
|
|
|
|
return False
|
2022-09-17 18:12:14 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# === ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ===
|
|
|
|
|
|
2022-09-17 18:12:14 +00:00
|
|
|
|
@staticmethod
|
2025-06-01 23:56:11 +00:00
|
|
|
|
def generate_token() -> str:
|
|
|
|
|
"""Генерирует криптографически стойкий токен"""
|
|
|
|
|
return secrets.token_urlsafe(32)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
async def cleanup_expired_tokens() -> int:
|
|
|
|
|
"""Очищает истекшие токены (Redis делает это автоматически)"""
|
|
|
|
|
# Redis автоматически удаляет истекшие ключи
|
|
|
|
|
# Здесь можем очистить связанные структуры данных
|
|
|
|
|
try:
|
|
|
|
|
user_session_keys = await redis.keys("user_tokens:*:session")
|
|
|
|
|
cleaned_count = 0
|
|
|
|
|
|
|
|
|
|
for user_tokens_key in user_session_keys:
|
|
|
|
|
tokens = await redis.smembers(user_tokens_key)
|
|
|
|
|
active_tokens = []
|
|
|
|
|
|
|
|
|
|
for token in tokens:
|
|
|
|
|
token_str = token.decode("utf-8") if isinstance(token, bytes) else str(token)
|
|
|
|
|
session_key = f"session:{token_str}"
|
|
|
|
|
exists = await redis.exists(session_key)
|
|
|
|
|
if exists:
|
|
|
|
|
active_tokens.append(token_str)
|
|
|
|
|
else:
|
|
|
|
|
cleaned_count += 1
|
|
|
|
|
|
|
|
|
|
# Обновляем список активных токенов
|
|
|
|
|
if active_tokens:
|
|
|
|
|
await redis.delete(user_tokens_key)
|
|
|
|
|
for token in active_tokens:
|
|
|
|
|
await redis.sadd(user_tokens_key, token)
|
|
|
|
|
else:
|
|
|
|
|
await redis.delete(user_tokens_key)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
if cleaned_count > 0:
|
|
|
|
|
logger.info(f"Очищено {cleaned_count} ссылок на истекшие токены")
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
return cleaned_count
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка очистки токенов: {e}")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
# === ОБРАТНАЯ СОВМЕСТИМОСТЬ ===
|
2022-09-17 18:12:14 +00:00
|
|
|
|
|
|
|
|
|
@staticmethod
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def get(token_key: str) -> Optional[str]:
|
|
|
|
|
"""Обратная совместимость - получение токена по ключу"""
|
|
|
|
|
result = await redis.get(token_key)
|
|
|
|
|
if isinstance(result, bytes):
|
|
|
|
|
return result.decode("utf-8")
|
|
|
|
|
return result
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
async def save_token(token_key: str, token_data: Dict[str, Any], life_span: int = 3600) -> bool:
|
|
|
|
|
"""Обратная совместимость - сохранение токена"""
|
|
|
|
|
try:
|
|
|
|
|
return await redis.serialize_and_set(token_key, token_data, ex=life_span)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка сохранения токена {token_key}: {e}")
|
|
|
|
|
return False
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
async def get_token(token_key: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Обратная совместимость - получение данных токена"""
|
|
|
|
|
try:
|
|
|
|
|
return await redis.get_and_deserialize(token_key)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка получения токена {token_key}: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
async def delete_token(token_key: str) -> bool:
|
|
|
|
|
"""Обратная совместимость - удаление токена"""
|
2022-09-17 18:12:14 +00:00
|
|
|
|
try:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
result = await redis.delete(token_key)
|
|
|
|
|
return result > 0
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка удаления токена {token_key}: {e}")
|
|
|
|
|
return False
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Остальные методы для обратной совместимости...
|
|
|
|
|
async def exists(self, token_key: str) -> bool:
|
|
|
|
|
"""Совместимость - проверка существования"""
|
|
|
|
|
return bool(await redis.exists(token_key))
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def invalidate_token(self, token: str) -> bool:
|
|
|
|
|
"""Совместимость - инвалидация токена"""
|
|
|
|
|
return await self.revoke_token("session", token)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def invalidate_all_tokens(self, user_id: str) -> int:
|
|
|
|
|
"""Совместимость - инвалидация всех токенов"""
|
|
|
|
|
return await self.revoke_user_tokens(user_id)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
def generate_session_token(self) -> str:
|
|
|
|
|
"""Совместимость - генерация токена сессии"""
|
|
|
|
|
return self.generate_token()
|
2022-09-17 18:12:14 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def get_session(self, session_token: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Совместимость - получение сессии"""
|
|
|
|
|
return await self.get_session_data(session_token)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def revoke_session(self, session_token: str) -> bool:
|
|
|
|
|
"""Совместимость - отзыв сессии"""
|
|
|
|
|
return await self.revoke_token("session", session_token)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def revoke_all_user_sessions(self, user_id: Union[int, str]) -> bool:
|
|
|
|
|
"""Совместимость - отзыв всех сессий"""
|
|
|
|
|
count = await self.revoke_user_tokens(str(user_id), "session")
|
|
|
|
|
return count > 0
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def get_user_sessions(self, user_id: Union[int, str]) -> list[Dict[str, Any]]:
|
|
|
|
|
"""Совместимость - получение сессий пользователя"""
|
|
|
|
|
try:
|
|
|
|
|
user_tokens_key = f"user_tokens:{user_id}:session"
|
|
|
|
|
tokens = await redis.smembers(user_tokens_key)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
sessions = []
|
|
|
|
|
for token in tokens:
|
|
|
|
|
token_str = token.decode("utf-8") if isinstance(token, bytes) else str(token)
|
|
|
|
|
session_data = await self.get_session_data(token_str)
|
|
|
|
|
if session_data:
|
|
|
|
|
session_data["token"] = token_str
|
|
|
|
|
sessions.append(session_data)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
return sessions
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.error(f"Ошибка получения сессий пользователя: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
async def revoke_all_tokens_for_user(self, user: AuthInput) -> bool:
|
|
|
|
|
"""Совместимость - отзыв всех токенов пользователя"""
|
|
|
|
|
user_id = getattr(user, "id", 0) or 0
|
|
|
|
|
count = await self.revoke_user_tokens(str(user_id))
|
|
|
|
|
return count > 0
|
|
|
|
|
|
|
|
|
|
async def get_one_time_token_value(self, token_key: str) -> Optional[str]:
|
|
|
|
|
"""Совместимость - одноразовые токены"""
|
|
|
|
|
token_data = await self.get_token(token_key)
|
|
|
|
|
if token_data and token_data.get("valid"):
|
|
|
|
|
return "TRUE"
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def save_one_time_token(self, user: AuthInput, one_time_token: str, life_span: int = 300) -> bool:
|
|
|
|
|
"""Совместимость - сохранение одноразового токена"""
|
|
|
|
|
user_id = getattr(user, "id", 0) or 0
|
|
|
|
|
token_key = f"{user_id}-{user.username}-{one_time_token}"
|
|
|
|
|
token_data = {"valid": True, "user_id": user_id, "username": user.username}
|
|
|
|
|
return await self.save_token(token_key, token_data, life_span)
|
|
|
|
|
|
|
|
|
|
async def extend_token_lifetime(self, token_key: str, additional_seconds: int = 3600) -> bool:
|
|
|
|
|
"""Совместимость - продление времени жизни"""
|
|
|
|
|
token_data = await self.get_token(token_key)
|
|
|
|
|
if not token_data:
|
2025-05-16 06:23:48 +00:00
|
|
|
|
return False
|
2025-06-01 23:56:11 +00:00
|
|
|
|
return await self.save_token(token_key, token_data, additional_seconds)
|
|
|
|
|
|
|
|
|
|
async def cleanup_expired_sessions(self) -> None:
|
|
|
|
|
"""Совместимость - очистка сессий"""
|
|
|
|
|
await self.cleanup_expired_tokens()
|