core/auth/tokenstorage.py

194 lines
6.8 KiB
Python
Raw Normal View History

2023-10-26 20:38:31 +00:00
from datetime import datetime, timedelta, timezone
2025-05-16 06:23:48 +00:00
import json
from typing import Dict, Any, Optional
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
2024-11-01 12:06:21 +00:00
from settings import ONETIME_TOKEN_LIFE_SPAN, SESSION_TOKEN_LIFE_SPAN
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-05-16 06:23:48 +00:00
class TokenStorage:
"""
Хранилище токенов в Redis.
Обеспечивает создание, проверку и отзыв токенов.
"""
@staticmethod
async def get(token_key: str) -> Optional[str]:
"""
Получает токен из хранилища.
2025-05-16 06:23:48 +00:00
Args:
token_key: Ключ токена
2025-05-16 06:23:48 +00:00
Returns:
str или None, если токен не найден
2022-11-24 14:31:52 +00:00
"""
2025-05-16 06:23:48 +00:00
logger.debug(f"[tokenstorage.get] Запрос токена: {token_key}")
return await redis.get(token_key)
@staticmethod
async def exists(token_key: str) -> bool:
2022-11-24 14:31:52 +00:00
"""
2025-05-16 06:23:48 +00:00
Проверяет наличие токена в хранилище.
2022-11-24 14:31:52 +00:00
2025-05-16 06:23:48 +00:00
Args:
token_key: Ключ токена
2022-11-24 14:31:52 +00:00
2025-05-16 06:23:48 +00:00
Returns:
bool: True, если токен существует
"""
return bool(await redis.execute("EXISTS", token_key))
2022-11-24 14:31:52 +00:00
@staticmethod
2025-05-16 06:23:48 +00:00
async def save_token(token_key: str, data: Dict[str, Any], life_span: int) -> bool:
"""
Сохраняет токен в хранилище с указанным временем жизни.
Args:
token_key: Ключ токена
data: Данные токена
life_span: Время жизни токена в секундах
Returns:
bool: True, если токен успешно сохранен
"""
try:
# Если данные не строка, преобразуем их в JSON
value = json.dumps(data) if isinstance(data, dict) else data
# Сохраняем токен и устанавливаем время жизни
await redis.set(token_key, value, ex=life_span)
return True
except Exception as e:
logger.error(f"[tokenstorage.save_token] Ошибка сохранения токена: {str(e)}")
return False
@staticmethod
async def create_onetime(user: AuthInput) -> str:
2025-05-16 06:23:48 +00:00
"""
Создает одноразовый токен для пользователя.
Args:
user: Объект пользователя
Returns:
str: Сгенерированный токен
"""
life_span = ONETIME_TOKEN_LIFE_SPAN
2022-11-13 23:38:06 +00:00
exp = datetime.now(tz=timezone.utc) + timedelta(seconds=life_span)
2022-10-31 21:25:25 +00:00
one_time_token = JWTCodec.encode(user, exp)
2025-05-16 06:23:48 +00:00
# Сохраняем токен в Redis
token_key = f"{user.id}-{user.username}-{one_time_token}"
await TokenStorage.save_token(token_key, "TRUE", life_span)
return one_time_token
@staticmethod
async def create_session(user: AuthInput) -> str:
2025-05-16 06:23:48 +00:00
"""
Создает сессионный токен для пользователя.
Args:
user: Объект пользователя
Returns:
str: Сгенерированный токен
"""
life_span = SESSION_TOKEN_LIFE_SPAN
2022-11-13 23:38:06 +00:00
exp = datetime.now(tz=timezone.utc) + timedelta(seconds=life_span)
2022-10-31 21:25:25 +00:00
session_token = JWTCodec.encode(user, exp)
2025-05-16 06:23:48 +00:00
# Сохраняем токен в Redis
token_key = f"{user.id}-{user.username}-{session_token}"
user_sessions_key = f"user_sessions:{user.id}"
# Создаем данные сессии
session_data = {
"user_id": str(user.id),
"username": user.username,
"created_at": datetime.now(tz=timezone.utc).timestamp(),
"expires_at": exp.timestamp(),
}
# Сохраняем токен и добавляем его в список сессий пользователя
pipe = redis.pipeline()
await pipe.hmset(token_key, session_data)
await pipe.expire(token_key, life_span)
await pipe.sadd(user_sessions_key, session_token)
await pipe.expire(user_sessions_key, life_span)
await pipe.execute()
return session_token
@staticmethod
async def revoke(token: str) -> bool:
2025-05-16 06:23:48 +00:00
"""
Отзывает токен.
Args:
token: Токен для отзыва
Returns:
bool: True, если токен успешно отозван
"""
try:
2025-05-16 06:23:48 +00:00
logger.debug("[tokenstorage.revoke] Отзыв токена")
# Декодируем токен
payload = JWTCodec.decode(token)
2025-05-16 06:23:48 +00:00
if not payload:
logger.warning("[tokenstorage.revoke] Невозможно декодировать токен")
return False
# Формируем ключи
token_key = f"{payload.user_id}-{payload.username}-{token}"
user_sessions_key = f"user_sessions:{payload.user_id}"
# Удаляем токен и запись из списка сессий пользователя
pipe = redis.pipeline()
await pipe.delete(token_key)
await pipe.srem(user_sessions_key, token)
await pipe.execute()
return True
except Exception as e:
logger.error(f"[tokenstorage.revoke] Ошибка отзыва токена: {str(e)}")
return False
@staticmethod
2025-05-16 06:23:48 +00:00
async def revoke_all(user: AuthInput) -> bool:
"""
Отзывает все токены пользователя.
Args:
user: Объект пользователя
Returns:
bool: True, если все токены успешно отозваны
"""
try:
# Формируем ключи
user_sessions_key = f"user_sessions:{user.id}"
# Получаем все токены пользователя
tokens = await redis.smembers(user_sessions_key)
if not tokens:
return True
# Формируем список ключей для удаления
keys_to_delete = [f"{user.id}-{user.username}-{token}" for token in tokens]
keys_to_delete.append(user_sessions_key)
# Удаляем все токены и список сессий
await redis.delete(*keys_to_delete)
return True
except Exception as e:
logger.error(f"[tokenstorage.revoke_all] Ошибка отзыва всех токенов: {str(e)}")
return False