2025-06-09 10:17:12 +05:00
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from typing import Optional, Dict
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class SessionStorage:
|
2025-06-23 12:47:05 +05:00
|
|
|
|
"""Хранилище сессий пользователей в JSON-файле.
|
|
|
|
|
|
|
|
|
|
|
|
Предоставляет методы для создания, получения, удаления и очистки сессий с
|
|
|
|
|
|
учётом срока действия.
|
|
|
|
|
|
"""
|
2025-06-12 16:47:27 +05:00
|
|
|
|
|
2025-06-09 10:17:12 +05:00
|
|
|
|
def __init__(self, file_path: str = "/etc/gonets/sessions.json"):
|
2025-06-23 12:47:05 +05:00
|
|
|
|
"""Инициализирует хранилище сессий.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
file_path (str, optional): Путь к JSON-файлу для хранения сессий.
|
|
|
|
|
|
По умолчанию "/etc/gonets/sessions.json".
|
|
|
|
|
|
|
|
|
|
|
|
Создаёт файл и директории, если они отсутствуют, с правами доступа 600.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
self.file_path = file_path
|
|
|
|
|
|
self._ensure_file_exists()
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_file_exists(self):
|
2025-06-23 12:47:05 +05:00
|
|
|
|
"""Создаёт JSON-файл, если он не существует.
|
|
|
|
|
|
|
|
|
|
|
|
Создаёт родительские директории и файл с начальной структурой данных,
|
|
|
|
|
|
устанавливая права доступа 600.
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
Exception: Если произошла ошибка при создании файла или директории.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
try:
|
|
|
|
|
|
logger.info(f"Проверка существования файла {self.file_path}")
|
|
|
|
|
|
if not os.path.exists(self.file_path):
|
2025-06-23 12:47:05 +05:00
|
|
|
|
logger.info(f"Создание директории {os.path.dirname(self.file_path)}")
|
2025-06-09 10:17:12 +05:00
|
|
|
|
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
|
|
|
|
|
|
logger.info(f"Создание файла {self.file_path}")
|
|
|
|
|
|
with open(self.file_path, "w") as f:
|
|
|
|
|
|
json.dump({"sessions": {}}, f)
|
|
|
|
|
|
os.chmod(self.file_path, 0o600)
|
|
|
|
|
|
logger.info(f"Создан файл {self.file_path}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.info(f"Файл {self.file_path} уже существует")
|
|
|
|
|
|
except Exception as e:
|
2025-06-12 16:32:05 +05:00
|
|
|
|
logger.error(
|
|
|
|
|
|
f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}"
|
|
|
|
|
|
)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def _read_data(self) -> Dict:
|
2025-06-23 12:47:05 +05:00
|
|
|
|
"""Читает данные из JSON-файла.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Dict: Словарь с данными сессий.
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
Exception: Если произошла ошибка при чтении файла.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
try:
|
|
|
|
|
|
with open(self.file_path, "r") as f:
|
|
|
|
|
|
return json.load(f)
|
|
|
|
|
|
except Exception as e:
|
2025-06-12 16:32:05 +05:00
|
|
|
|
logger.error(
|
|
|
|
|
|
f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}"
|
|
|
|
|
|
)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def _write_data(self, data: Dict):
|
2025-06-23 12:47:05 +05:00
|
|
|
|
"""Записывает данные в JSON-файл.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
data (Dict): Словарь с данными сессий для записи.
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
Exception: Если произошла ошибка при записи в файл.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
try:
|
|
|
|
|
|
with open(self.file_path, "w") as f:
|
|
|
|
|
|
json.dump(data, f, indent=2)
|
|
|
|
|
|
except Exception as e:
|
2025-06-12 16:32:05 +05:00
|
|
|
|
logger.error(
|
|
|
|
|
|
f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}"
|
|
|
|
|
|
)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def create_session(self, username: str, token: str, client_id: str) -> str:
|
2025-06-23 12:47:05 +05:00
|
|
|
|
"""Создаёт новую сессию и возвращает уникальный session_id.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
username (str): Имя пользователя, для которого создаётся сессия.
|
|
|
|
|
|
token (str): Токен аутентификации.
|
|
|
|
|
|
client_id (str): Идентификатор клиента.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
str: Уникальный идентификатор сессии (session_id).
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
Exception: Если произошла ошибка при создании или записи сессии.
|
|
|
|
|
|
|
|
|
|
|
|
Note:
|
|
|
|
|
|
Удаляет истекшие сессии перед созданием новой и устанавливает срок
|
|
|
|
|
|
действия 30 минут.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
try:
|
|
|
|
|
|
self.clean_expired_sessions()
|
|
|
|
|
|
data = self._read_data()
|
|
|
|
|
|
session_id = str(uuid.uuid4())
|
|
|
|
|
|
created_at = datetime.utcnow().isoformat()
|
|
|
|
|
|
expiry = (datetime.utcnow() + timedelta(minutes=30)).isoformat()
|
|
|
|
|
|
data["sessions"][session_id] = {
|
|
|
|
|
|
"username": username,
|
|
|
|
|
|
"token": token,
|
|
|
|
|
|
"client_id": client_id,
|
|
|
|
|
|
"created_at": created_at,
|
2025-06-12 16:32:05 +05:00
|
|
|
|
"expiry": expiry,
|
2025-06-09 10:17:12 +05:00
|
|
|
|
}
|
|
|
|
|
|
self._write_data(data)
|
2025-06-12 16:32:05 +05:00
|
|
|
|
logger.info(
|
|
|
|
|
|
f"Сессия создана для пользователя {username} с session_id {session_id}"
|
|
|
|
|
|
)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
return session_id
|
|
|
|
|
|
except Exception as e:
|
2025-06-12 16:32:05 +05:00
|
|
|
|
logger.error(
|
|
|
|
|
|
f"Ошибка при создании сессии для {username}: {type(e).__name__} - {str(e)}"
|
|
|
|
|
|
)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def get_latest_session(self, client_id: str) -> Optional[Dict]:
|
2025-06-23 12:47:05 +05:00
|
|
|
|
"""Получает последнюю активную сессию для указанного client_id.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
client_id (str): Идентификатор клиента.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Optional[Dict]: Словарь с данными последней активной сессии, включая
|
|
|
|
|
|
"session_id", или None, если сессия не найдена.
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
Exception: Если произошла ошибка при чтении или обработке данных.
|
|
|
|
|
|
|
|
|
|
|
|
Note:
|
|
|
|
|
|
Очищает истекшие сессии перед поиском и возвращает сессию с самым
|
|
|
|
|
|
поздним временем создания.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
try:
|
|
|
|
|
|
self.clean_expired_sessions()
|
|
|
|
|
|
data = self._read_data()
|
|
|
|
|
|
latest_session = None
|
|
|
|
|
|
latest_time = None
|
|
|
|
|
|
for session_id, session in data["sessions"].items():
|
|
|
|
|
|
if session["client_id"] == client_id:
|
|
|
|
|
|
expiry = datetime.fromisoformat(session["expiry"])
|
|
|
|
|
|
if datetime.utcnow() > expiry:
|
|
|
|
|
|
logger.info(f"Сессия {session_id} истекла")
|
|
|
|
|
|
del data["sessions"][session_id]
|
|
|
|
|
|
continue
|
|
|
|
|
|
created_at = datetime.fromisoformat(session["created_at"])
|
|
|
|
|
|
if latest_time is None or created_at > latest_time:
|
|
|
|
|
|
latest_time = created_at
|
|
|
|
|
|
latest_session = session
|
|
|
|
|
|
latest_session["session_id"] = session_id
|
|
|
|
|
|
if latest_session:
|
|
|
|
|
|
logger.info(f"Найдена сессия для client_id {client_id}")
|
|
|
|
|
|
return latest_session
|
|
|
|
|
|
logger.info(f"Сессия для client_id {client_id} не найдена")
|
|
|
|
|
|
self._write_data(data)
|
|
|
|
|
|
return None
|
|
|
|
|
|
except Exception as e:
|
2025-06-12 16:32:05 +05:00
|
|
|
|
logger.error(
|
|
|
|
|
|
f"Ошибка при получении сессии для client_id {client_id}: {type(e).__name__} - {str(e)}"
|
|
|
|
|
|
)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def delete_session(self, session_id: str):
|
2025-06-23 12:47:05 +05:00
|
|
|
|
"""Удаляет сессию по указанному session_id.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
session_id (str): Уникальный идентификатор сессии.
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
Exception: Если произошла ошибка при чтении или записи данных.
|
|
|
|
|
|
|
|
|
|
|
|
Note:
|
|
|
|
|
|
Очищает истекшие сессии перед удалением.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
try:
|
|
|
|
|
|
self.clean_expired_sessions()
|
|
|
|
|
|
data = self._read_data()
|
|
|
|
|
|
if session_id in data["sessions"]:
|
|
|
|
|
|
del data["sessions"][session_id]
|
|
|
|
|
|
self._write_data(data)
|
|
|
|
|
|
logger.info(f"Сессия с session_id {session_id} удалена")
|
|
|
|
|
|
except Exception as e:
|
2025-06-12 16:32:05 +05:00
|
|
|
|
logger.error(
|
|
|
|
|
|
f"Ошибка при удалении сессии {session_id}: {type(e).__name__} - {str(e)}"
|
|
|
|
|
|
)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def clean_expired_sessions(self):
|
2025-06-23 12:47:05 +05:00
|
|
|
|
"""Удаляет все истекшие сессии из хранилища.
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
Exception: Если произошла ошибка при чтении или записи данных.
|
|
|
|
|
|
|
|
|
|
|
|
Note:
|
|
|
|
|
|
Проверяет срок действия каждой сессии и удаляет те, что истекли,
|
|
|
|
|
|
записывая обновлённые данные.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
try:
|
|
|
|
|
|
data = self._read_data()
|
|
|
|
|
|
expired_sessions = []
|
|
|
|
|
|
current_time = datetime.utcnow()
|
|
|
|
|
|
|
|
|
|
|
|
for session_id, session in list(data["sessions"].items()):
|
|
|
|
|
|
expiry = datetime.fromisoformat(session["expiry"])
|
|
|
|
|
|
if current_time > expiry:
|
|
|
|
|
|
expired_sessions.append(session_id)
|
|
|
|
|
|
|
|
|
|
|
|
for session_id in expired_sessions:
|
|
|
|
|
|
del data["sessions"][session_id]
|
2025-06-12 16:47:27 +05:00
|
|
|
|
logger.info(
|
|
|
|
|
|
f"Удалена истекшая сессия с session_id {session_id}")
|
2025-06-09 10:17:12 +05:00
|
|
|
|
|
|
|
|
|
|
if expired_sessions:
|
|
|
|
|
|
self._write_data(data)
|
|
|
|
|
|
logger.info(f"Удалено {len(expired_sessions)} истекших сессий")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.debug("Истекшие сессии не найдены")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-06-12 16:32:05 +05:00
|
|
|
|
logger.error(
|
|
|
|
|
|
f"Ошибка при очистке истекших сессий: {type(e).__name__} - {str(e)}"
|
|
|
|
|
|
)
|
2025-06-23 12:47:05 +05:00
|
|
|
|
raise
|