upgrade schema, resolvers, panel added
This commit is contained in:
@@ -1,73 +1,193 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from auth.jwtcodec import JWTCodec
|
||||
from auth.validations import AuthInput
|
||||
from services.redis import redis
|
||||
from settings import ONETIME_TOKEN_LIFE_SPAN, SESSION_TOKEN_LIFE_SPAN
|
||||
|
||||
|
||||
async def save(token_key, life_span, auto_delete=True):
|
||||
await redis.execute("SET", token_key, "True")
|
||||
if auto_delete:
|
||||
expire_at = (datetime.now(tz=timezone.utc) + timedelta(seconds=life_span)).timestamp()
|
||||
await redis.execute("EXPIREAT", token_key, int(expire_at))
|
||||
|
||||
|
||||
class SessionToken:
|
||||
@classmethod
|
||||
async def verify(cls, token: str):
|
||||
"""
|
||||
Rules for a token to be valid.
|
||||
- token format is legal
|
||||
- token exists in redis database
|
||||
- token is not expired
|
||||
"""
|
||||
try:
|
||||
return JWTCodec.decode(token)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@classmethod
|
||||
async def get(cls, payload, token):
|
||||
return await TokenStorage.get(f"{payload.user_id}-{payload.username}-{token}")
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
class TokenStorage:
|
||||
"""
|
||||
Хранилище токенов в Redis.
|
||||
Обеспечивает создание, проверку и отзыв токенов.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
async def get(token_key):
|
||||
print("[tokenstorage.get] " + token_key)
|
||||
# 2041-user@domain.zn-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyMDQxLCJ1c2VybmFtZSI6ImFudG9uLnJld2luK3Rlc3QtbG9hZGNoYXRAZ21haWwuY29tIiwiZXhwIjoxNjcxNzgwNjE2LCJpYXQiOjE2NjkxODg2MTYsImlzcyI6ImRpc2NvdXJzIn0.Nml4oV6iMjMmc6xwM7lTKEZJKBXvJFEIZ-Up1C1rITQ
|
||||
return await redis.execute("GET", token_key)
|
||||
async def get(token_key: str) -> Optional[str]:
|
||||
"""
|
||||
Получает токен из хранилища.
|
||||
|
||||
Args:
|
||||
token_key: Ключ токена
|
||||
|
||||
Returns:
|
||||
str или None, если токен не найден
|
||||
"""
|
||||
logger.debug(f"[tokenstorage.get] Запрос токена: {token_key}")
|
||||
return await redis.get(token_key)
|
||||
|
||||
@staticmethod
|
||||
async def exists(token_key: str) -> bool:
|
||||
"""
|
||||
Проверяет наличие токена в хранилище.
|
||||
|
||||
Args:
|
||||
token_key: Ключ токена
|
||||
|
||||
Returns:
|
||||
bool: True, если токен существует
|
||||
"""
|
||||
return bool(await redis.execute("EXISTS", token_key))
|
||||
|
||||
@staticmethod
|
||||
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:
|
||||
"""
|
||||
Создает одноразовый токен для пользователя.
|
||||
|
||||
Args:
|
||||
user: Объект пользователя
|
||||
|
||||
Returns:
|
||||
str: Сгенерированный токен
|
||||
"""
|
||||
life_span = ONETIME_TOKEN_LIFE_SPAN
|
||||
exp = datetime.now(tz=timezone.utc) + timedelta(seconds=life_span)
|
||||
one_time_token = JWTCodec.encode(user, exp)
|
||||
await save(f"{user.id}-{user.username}-{one_time_token}", life_span)
|
||||
|
||||
# Сохраняем токен в 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:
|
||||
"""
|
||||
Создает сессионный токен для пользователя.
|
||||
|
||||
Args:
|
||||
user: Объект пользователя
|
||||
|
||||
Returns:
|
||||
str: Сгенерированный токен
|
||||
"""
|
||||
life_span = SESSION_TOKEN_LIFE_SPAN
|
||||
exp = datetime.now(tz=timezone.utc) + timedelta(seconds=life_span)
|
||||
session_token = JWTCodec.encode(user, exp)
|
||||
await save(f"{user.id}-{user.username}-{session_token}", life_span)
|
||||
|
||||
# Сохраняем токен в 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:
|
||||
payload = None
|
||||
"""
|
||||
Отзывает токен.
|
||||
|
||||
Args:
|
||||
token: Токен для отзыва
|
||||
|
||||
Returns:
|
||||
bool: True, если токен успешно отозван
|
||||
"""
|
||||
try:
|
||||
print("[auth.tokenstorage] revoke token")
|
||||
logger.debug("[tokenstorage.revoke] Отзыв токена")
|
||||
|
||||
# Декодируем токен
|
||||
payload = JWTCodec.decode(token)
|
||||
except: # noqa
|
||||
pass
|
||||
else:
|
||||
await redis.execute("DEL", f"{payload.user_id}-{payload.username}-{token}")
|
||||
return True
|
||||
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
|
||||
async def revoke_all(user: AuthInput):
|
||||
tokens = await redis.execute("KEYS", f"{user.id}-*")
|
||||
await redis.execute("DEL", *tokens)
|
||||
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
|
||||
|
Reference in New Issue
Block a user