2025-06-09 10:17:12 +05:00
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from passlib.context import CryptContext
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
from models.auth import UserCreate, UserResponse
|
|
|
|
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class JsonStorage:
|
2025-06-23 12:47:05 +05:00
|
|
|
|
"""Хранилище пользователей в JSON-файле.
|
|
|
|
|
|
|
|
|
|
|
|
Предоставляет методы для создания, чтения и проверки данных пользователей,
|
|
|
|
|
|
используя шифрование паролей с помощью bcrypt.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, file_path: str = "/etc/gonets/users.json"):
|
2025-06-23 12:47:05 +05:00
|
|
|
|
"""Инициализирует хранилище пользователей.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
file_path (str, optional): Путь к JSON-файлу для хранения данных.
|
|
|
|
|
|
По умолчанию "/etc/gonets/users.json".
|
|
|
|
|
|
|
|
|
|
|
|
Создаёт объект CryptContext для хеширования паролей и проверяет/создаёт
|
|
|
|
|
|
файл, если он отсутствует.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
self.file_path = file_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: Если произошла ошибка при создании файла или директории.
|
|
|
|
|
|
"""
|
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({"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:
|
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: Словарь с данными пользователей и следующим ID.
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
FileNotFoundError: Если файл отсутствует (возвращает пустую структуру).
|
|
|
|
|
|
Exception: Если произошла ошибка при чтении файла.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
try:
|
|
|
|
|
|
with open(self.file_path, "r") as f:
|
|
|
|
|
|
return json.load(f)
|
|
|
|
|
|
except FileNotFoundError:
|
2025-06-12 16:32:05 +05:00
|
|
|
|
logger.info(
|
|
|
|
|
|
f"Файл {self.file_path} не существует, возвращаем пустую структуру"
|
|
|
|
|
|
)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
return {"users": [], "next_id": 1}
|
|
|
|
|
|
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): Словарь с данными для записи (пользователи и next_id).
|
|
|
|
|
|
|
|
|
|
|
|
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_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: Если произошла ошибка при записи данных.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
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,
|
2025-06-12 16:32:05 +05:00
|
|
|
|
"is_admin": user.is_admin,
|
2025-06-09 10:17:12 +05:00
|
|
|
|
}
|
|
|
|
|
|
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} успешно зарегистрирован")
|
2025-06-12 16:32:05 +05:00
|
|
|
|
return UserResponse(
|
|
|
|
|
|
id=new_user["id"],
|
|
|
|
|
|
username=new_user["username"],
|
|
|
|
|
|
is_admin=new_user["is_admin"],
|
|
|
|
|
|
)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
except Exception as e:
|
2025-06-12 16:32:05 +05:00
|
|
|
|
logger.error(
|
|
|
|
|
|
f"Ошибка при создании пользователя {user.username}: {type(e).__name__} - {str(e)}"
|
|
|
|
|
|
)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
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: Если произошла ошибка при чтении файла.
|
|
|
|
|
|
"""
|
2025-06-09 10:17:12 +05:00
|
|
|
|
try:
|
|
|
|
|
|
if not os.path.exists(self.file_path):
|
2025-06-12 16:32:05 +05:00
|
|
|
|
logger.info(
|
|
|
|
|
|
f"Файл {self.file_path} не существует, пользователь {username} не найден"
|
|
|
|
|
|
)
|
2025-06-09 10:17:12 +05:00
|
|
|
|
return None
|
|
|
|
|
|
data = self._read_data()
|
|
|
|
|
|
for user in data["users"]:
|
|
|
|
|
|
if user["username"] == username:
|
|
|
|
|
|
return user
|
|
|
|
|
|
return None
|
|
|
|
|
|
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
|
|
|
|
|
|
|
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)
|