upgrade schema, resolvers, panel added
This commit is contained in:
228
auth/sessions.py
Normal file
228
auth/sessions.py
Normal file
@@ -0,0 +1,228 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
from services.redis import redis
|
||||
from auth.jwtcodec import JWTCodec, TokenPayload
|
||||
from settings import SESSION_TOKEN_LIFE_SPAN
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
class SessionData(BaseModel):
|
||||
"""Модель данных сессии"""
|
||||
|
||||
user_id: str
|
||||
username: str
|
||||
created_at: datetime
|
||||
expires_at: datetime
|
||||
device_info: Optional[dict] = None
|
||||
|
||||
|
||||
class SessionManager:
|
||||
"""
|
||||
Менеджер сессий в Redis.
|
||||
Управляет созданием, проверкой и отзывом сессий пользователей.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _make_session_key(user_id: str, token: str) -> str:
|
||||
"""Формирует ключ сессии в Redis"""
|
||||
return f"session:{user_id}:{token}"
|
||||
|
||||
@staticmethod
|
||||
def _make_user_sessions_key(user_id: str) -> str:
|
||||
"""Формирует ключ для списка сессий пользователя в Redis"""
|
||||
return f"user_sessions:{user_id}"
|
||||
|
||||
@classmethod
|
||||
async def create_session(cls, user_id: str, username: str, device_info: dict = None) -> str:
|
||||
"""
|
||||
Создает новую сессию для пользователя.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
username: Имя пользователя/логин
|
||||
device_info: Информация об устройстве (опционально)
|
||||
|
||||
Returns:
|
||||
str: Токен сессии
|
||||
"""
|
||||
try:
|
||||
# Создаем JWT токен
|
||||
exp = datetime.now(tz=timezone.utc) + timedelta(seconds=SESSION_TOKEN_LIFE_SPAN)
|
||||
session_token = JWTCodec.encode({"id": user_id, "email": username}, exp)
|
||||
|
||||
# Создаем данные сессии
|
||||
session_data = SessionData(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
created_at=datetime.now(tz=timezone.utc),
|
||||
expires_at=exp,
|
||||
device_info=device_info,
|
||||
)
|
||||
|
||||
# Ключи в Redis
|
||||
session_key = cls._make_session_key(user_id, session_token)
|
||||
user_sessions_key = cls._make_user_sessions_key(user_id)
|
||||
|
||||
# Сохраняем в Redis
|
||||
pipe = redis.pipeline()
|
||||
await pipe.hset(session_key, mapping=session_data.dict())
|
||||
await pipe.expire(session_key, SESSION_TOKEN_LIFE_SPAN)
|
||||
await pipe.sadd(user_sessions_key, session_token)
|
||||
await pipe.expire(user_sessions_key, SESSION_TOKEN_LIFE_SPAN)
|
||||
await pipe.execute()
|
||||
|
||||
return session_token
|
||||
except Exception as e:
|
||||
logger.error(f"[SessionManager.create_session] Ошибка: {str(e)}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
async def verify_session(cls, token: str) -> Optional[TokenPayload]:
|
||||
"""
|
||||
Проверяет валидность сессии.
|
||||
|
||||
Args:
|
||||
token: Токен сессии
|
||||
|
||||
Returns:
|
||||
TokenPayload: Данные токена или None, если токен недействителен
|
||||
"""
|
||||
try:
|
||||
# Декодируем JWT
|
||||
payload = JWTCodec.decode(token)
|
||||
|
||||
# Формируем ключ сессии
|
||||
session_key = cls._make_session_key(payload.user_id, token)
|
||||
|
||||
# Проверяем существование сессии в Redis
|
||||
session_exists = await redis.exists(session_key)
|
||||
if not session_exists:
|
||||
logger.debug(f"[SessionManager.verify_session] Сессия не найдена: {payload.user_id}")
|
||||
return None
|
||||
|
||||
return payload
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[SessionManager.verify_session] Ошибка: {str(e)}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def get_session_data(cls, user_id: str, token: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Получает данные сессии.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
token: Токен сессии
|
||||
|
||||
Returns:
|
||||
dict: Данные сессии или None, если сессия не найдена
|
||||
"""
|
||||
try:
|
||||
session_key = cls._make_session_key(user_id, token)
|
||||
session_data = await redis.hgetall(session_key)
|
||||
return session_data if session_data else None
|
||||
except Exception as e:
|
||||
logger.error(f"[SessionManager.get_session_data] Ошибка: {str(e)}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def revoke_session(cls, user_id: str, token: str) -> bool:
|
||||
"""
|
||||
Отзывает конкретную сессию.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
token: Токен сессии
|
||||
|
||||
Returns:
|
||||
bool: True, если сессия успешно отозвана
|
||||
"""
|
||||
try:
|
||||
session_key = cls._make_session_key(user_id, token)
|
||||
user_sessions_key = cls._make_user_sessions_key(user_id)
|
||||
|
||||
# Удаляем сессию и запись из списка сессий пользователя
|
||||
pipe = redis.pipeline()
|
||||
await pipe.delete(session_key)
|
||||
await pipe.srem(user_sessions_key, token)
|
||||
await pipe.execute()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"[SessionManager.revoke_session] Ошибка: {str(e)}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def revoke_all_sessions(cls, user_id: str) -> bool:
|
||||
"""
|
||||
Отзывает все сессии пользователя.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
|
||||
Returns:
|
||||
bool: True, если все сессии успешно отозваны
|
||||
"""
|
||||
try:
|
||||
user_sessions_key = cls._make_user_sessions_key(user_id)
|
||||
|
||||
# Получаем все токены пользователя
|
||||
tokens = await redis.smembers(user_sessions_key)
|
||||
if not tokens:
|
||||
return True
|
||||
|
||||
# Создаем команды для удаления всех сессий
|
||||
pipe = redis.pipeline()
|
||||
|
||||
# Формируем список ключей для удаления
|
||||
for token in tokens:
|
||||
session_key = cls._make_session_key(user_id, token)
|
||||
await pipe.delete(session_key)
|
||||
|
||||
# Удаляем список сессий
|
||||
await pipe.delete(user_sessions_key)
|
||||
await pipe.execute()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"[SessionManager.revoke_all_sessions] Ошибка: {str(e)}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def refresh_session(cls, user_id: str, old_token: str, device_info: dict = None) -> Optional[str]:
|
||||
"""
|
||||
Обновляет сессию пользователя, заменяя старый токен новым.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
old_token: Старый токен сессии
|
||||
device_info: Информация об устройстве (опционально)
|
||||
|
||||
Returns:
|
||||
str: Новый токен сессии или None в случае ошибки
|
||||
"""
|
||||
try:
|
||||
# Получаем данные старой сессии
|
||||
old_session_key = cls._make_session_key(user_id, old_token)
|
||||
old_session_data = await redis.hgetall(old_session_key)
|
||||
|
||||
if not old_session_data:
|
||||
logger.warning(f"[SessionManager.refresh_session] Сессия не найдена: {user_id}")
|
||||
return None
|
||||
|
||||
# Используем старые данные устройства, если новые не предоставлены
|
||||
if not device_info and "device_info" in old_session_data:
|
||||
device_info = old_session_data.get("device_info")
|
||||
|
||||
# Создаем новую сессию
|
||||
new_token = await cls.create_session(user_id, old_session_data.get("username", ""), device_info)
|
||||
|
||||
# Отзываем старую сессию
|
||||
await cls.revoke_session(user_id, old_token)
|
||||
|
||||
return new_token
|
||||
except Exception as e:
|
||||
logger.error(f"[SessionManager.refresh_session] Ошибка: {str(e)}")
|
||||
return None
|
Reference in New Issue
Block a user