gonets/SRV/storage/json_storage.py

212 lines
9.4 KiB
Python
Raw Permalink Normal View History

import json
import os
import logging
from passlib.context import CryptContext
2025-11-25 13:52:54 +05:00
import passlib.handlers.bcrypt # noqa: F401 # ensure Nuitka bundles bcrypt handlers
from typing import Optional
from models.auth import UserCreate, UserResponse
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
DATA_DIR = os.getenv("GONETS_DATA_DIR", "/etc/gonets")
DEFAULT_USERS_PATH = os.getenv("GONETS_USERS_PATH",
os.path.join(DATA_DIR, "users.json"))
class JsonStorage:
2025-06-23 12:47:05 +05:00
"""Хранилище пользователей в JSON-файле.
2025-10-14 11:42:57 +05:00
Назначение:
- Обеспечивает простое файловое хранилище учетных записей без полноценной СУБД.
- Подходит для встраиваемых окружений, где критична минимальная зависимость от внешних сервисов.
Формат данных::
2025-10-14 11:42:57 +05:00
{
"users": [
{"id": int, "username": str, "password_hash": str, "is_admin": bool},
...
],
"next_id": int
}
Безопасность:
- Пароли никогда не сохраняются в открытом виде: используется `passlib.context.CryptContext` со схемой `bcrypt`.
- Файл создаётся с правами доступа ``0o600`` (чтение/запись только владельцу).
Ограничения:
- Нет блокировок для конкурентного доступа — в многопроцессной среде требуется внешняя синхронизация.
- Значение пути по умолчанию ``/etc/gonets/users.json`` предполагает наличие прав на запись и предварительную настройку окружения.
2025-06-23 12:47:05 +05:00
"""
def __init__(self, file_path: Optional[str] = None):
2025-06-23 12:47:05 +05:00
"""Инициализирует хранилище пользователей.
Args:
file_path (str, optional): Путь к JSON-файлу для хранения данных.
По умолчанию "/etc/gonets/users.json".
Создаёт объект CryptContext для хеширования паролей и проверяет/создаёт
файл, если он отсутствует.
"""
self.file_path = file_path or DEFAULT_USERS_PATH
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
self._ensure_file_exists()
def _ensure_file_exists(self):
2025-06-23 12:47:05 +05:00
"""Создаёт JSON-файл, если он не существует.
Создаёт родительские директории и файл с начальной структурой данных,
устанавливая права доступа 600.
Raises:
Exception: Если произошла ошибка при создании файла или директории.
"""
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)}")
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({"users": [], "next_id": 1}, f)
os.chmod(self.file_path, 0o600)
logger.info(f"Создан файл {self.file_path}")
else:
logger.info(f"Файл {self.file_path} уже существует")
except Exception as e:
logger.error(
f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}"
)
raise
def _read_data(self) -> dict:
2025-06-23 12:47:05 +05:00
"""Читает данные из JSON-файла.
Returns:
dict: Словарь с данными пользователей и следующим ID.
Raises:
FileNotFoundError: Если файл отсутствует (возвращает пустую структуру).
Exception: Если произошла ошибка при чтении файла.
"""
try:
with open(self.file_path, "r") as f:
return json.load(f)
except FileNotFoundError:
logger.info(
f"Файл {self.file_path} не существует, возвращаем пустую структуру"
)
return {"users": [], "next_id": 1}
except Exception as e:
logger.error(
f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}"
)
raise
def _write_data(self, data: dict):
2025-06-23 12:47:05 +05:00
"""Записывает данные в JSON-файл.
Args:
data (dict): Словарь с данными для записи (пользователи и next_id).
Raises:
Exception: Если произошла ошибка при записи в файл.
"""
try:
with open(self.file_path, "w") as f:
json.dump(data, f, indent=2)
except Exception as e:
logger.error(
f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}"
)
raise
def create_user(self, user: UserCreate) -> UserResponse:
2025-06-23 12:47:05 +05:00
"""Создаёт нового пользователя с хешированием пароля.
Args:
user (UserCreate): Объект с данными нового пользователя (username, password, is_admin).
Returns:
UserResponse: Объект с данными созданного пользователя (id, username, is_admin).
Raises:
ValueError: Если пользователь с таким именем уже существует.
Exception: Если произошла ошибка при записи данных.
"""
try:
data = self._read_data()
users = data["users"]
next_id = data["next_id"]
if any(u["username"] == user.username for u in users):
logger.error(f"Пользователь {user.username} уже существует")
raise ValueError("Пользователь уже существует")
password_hash = self.pwd_context.hash(user.password)
new_user = {
"id": next_id,
"username": user.username,
"password_hash": password_hash,
"is_admin": user.is_admin,
}
users.append(new_user)
data["next_id"] = next_id + 1
self._write_data(data)
2025-06-12 16:47:27 +05:00
logger.info(
f"Пользователь {user.username} успешно зарегистрирован")
return UserResponse(
id=new_user["id"],
username=new_user["username"],
is_admin=new_user["is_admin"],
)
except Exception as e:
logger.error(
f"Ошибка при создании пользователя {user.username}: {type(e).__name__} - {str(e)}"
)
raise
def get_user_by_username(self, username: str) -> Optional[dict]:
2025-06-23 12:47:05 +05:00
"""Получает данные пользователя по имени.
Args:
username (str): Имя пользователя для поиска.
Returns:
Optional[dict]: Словарь с данными пользователя или None, если пользователь не найден.
Raises:
Exception: Если произошла ошибка при чтении файла.
"""
try:
if not os.path.exists(self.file_path):
logger.info(
f"Файл {self.file_path} не существует, пользователь {username} не найден"
)
return None
data = self._read_data()
for user in data["users"]:
if user["username"] == username:
return user
return None
except Exception as e:
logger.error(
f"Ошибка при получении пользователя {username}: {type(e).__name__} - {str(e)}"
)
raise
2025-06-23 12:47:05 +05:00
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
"""Проверяет соответствие введённого пароля хэшированному.
Args:
plain_password (str): Введённый пользователем пароль.
hashed_password (str): Хэшированный пароль из хранилища.
Returns:
bool: True, если пароль верен, False в противном случае.
"""
return self.pwd_context.verify(plain_password, hashed_password)