Исходный код EJIO.user.model
## /user/model.py
import hashlib
from dataclasses import dataclass, asdict
from typing import Any, Self
from .flags import UserRole
from .err import IncorrectLoginError, SaltNotFoundError, IncorrectPasswordError, ServiceNotInitializedError
__all__ = [
'User',
]
[документация]
@dataclass(slots=True)
class User:
"""
Модель пользователя.
Сравнивается по регистронезависимому логину.
"""
id: str = ""
login: str = ""
name: str = ""
role: UserRole = UserRole.USER
crypto_key: str = ""
# Системные поля метаданных
avatar_id: str | None = None
password_hash: str = ""
salt: str = ""
def __post_init__(self) -> None:
"""Нормализация текстовых полей при сборке."""
self.login = self.login.strip()
if not self.name and self.login:
self.name = self.login
[документация]
@classmethod
def create_bare(cls, login: str) -> Self:
"""
Внутренний фабричный метод создания «голой» заготовки пользователя.
Имя по умолчанию выставляется равным логину.
"""
lg = login.strip()
if not lg:
raise IncorrectLoginError("User login must be of a valid format.")
return cls(login=lg, name=lg)
@property
def password(self) -> str:
"""Возвращает текущий сохраненный хэш пароля."""
return self.password_hash
@password.setter
def password(self, raw_password: str) -> None:
"""
Автоматически солит и хэширует входящий текстовый пароль.
Использует соль, которая должна быть сгенерирована при создании пользователя.
"""
if not self.salt:
raise SaltNotFoundError("Unable to hash the password: a salt has not yet been assigned to the user.")
if not raw_password or len(raw_password.strip()) < 4:
raise IncorrectPasswordError("The password cannot be empty or shorter than 4 characters.")
salted_payload = raw_password + self.salt
self.password_hash = hashlib.sha256(salted_payload.encode('utf-8')).hexdigest()
@property
def avatar(self) -> str | None:#todo: проверка инициализации
"""Возвращает абсолютный путь к физическому файлу изображения в ОС."""
if not self.avatar_id:
return None
from .facade import UserFacade
if not UserFacade.avatar_service:
raise ServiceNotInitializedError("Service not initialized yet.")
return str(UserFacade.avatar_service.get_absolute_path(self.avatar_id))
@avatar.setter
def avatar(self, source_path: str | None) -> None:
"""Принимает путь к внешнему файлу, автоматически копирует его через медиа-фасад."""
from .facade import UserFacade
if not UserFacade.avatar_service:
raise ServiceNotInitializedError("Service not initialized yet.")
if source_path is None:
if self.avatar_id:
UserFacade.avatar_service.delete_file(self.avatar_id)
self.avatar_id = None
return
# Копируем файл через выделенный сервис, получая UUID-идентификатор
new_id = UserFacade.avatar_service.store_file(source_path)
if self.avatar_id:
UserFacade.avatar_service.delete_file(self.avatar_id)
self.avatar_id = new_id
[документация]
def to_dict(self) -> dict[str, Any]:
"""Автоматическая сериализация свойств на базе dataclasses.asdict."""
data = asdict(self)
data["role"] = int(self.role) # Переводим Enum в число для сохранения
return data
[документация]
@classmethod
def from_dict(cls, data: dict[str, Any]) -> 'User':
"""Восстанавливает объект из словаря, сохраняя все оригинальные ID с диска."""
if "role" in data:
data["role"] = UserRole(data["role"])
return cls(**data)