Улучшение безопасности

This commit is contained in:
2026-01-19 23:22:29 +03:00
parent 758e0fc9e6
commit d6ecd4066f
59 changed files with 2712 additions and 1010 deletions
+6 -2
View File
@@ -1,4 +1,5 @@
"""Модуль DB-моделей книг"""
from typing import TYPE_CHECKING, List
from sqlalchemy import Column, String
@@ -15,10 +16,11 @@ if TYPE_CHECKING:
class Book(BookBase, table=True):
"""Модель книги в базе данных"""
id: int | None = Field(default=None, primary_key=True, index=True)
status: BookStatus = Field(
default=BookStatus.ACTIVE,
sa_column=Column(String, nullable=False, default="active")
sa_column=Column(String, nullable=False, default="active"),
)
authors: List["Author"] = Relationship(
back_populates="books", link_model=AuthorBookLink
@@ -26,4 +28,6 @@ class Book(BookBase, table=True):
genres: List["Genre"] = Relationship(
back_populates="books", link_model=GenreBookLink
)
loans: List["BookUserLink"] = Relationship(sa_relationship_kwargs={"cascade": "all, delete"})
loans: List["BookUserLink"] = Relationship( # ty: ignore[unresolved-reference]
sa_relationship_kwargs={"cascade": "all, delete"}
)
+10 -5
View File
@@ -1,10 +1,12 @@
"""Модуль связей между сущностями в БД"""
from datetime import datetime
from datetime import datetime, timezone
from sqlmodel import SQLModel, Field
class AuthorBookLink(SQLModel, table=True):
"""Модель связи автора и книги"""
author_id: int | None = Field(
default=None, foreign_key="author.id", primary_key=True
)
@@ -13,12 +15,14 @@ class AuthorBookLink(SQLModel, table=True):
class GenreBookLink(SQLModel, table=True):
"""Модель связи жанра и книги"""
genre_id: int | None = Field(default=None, foreign_key="genre.id", primary_key=True)
book_id: int | None = Field(default=None, foreign_key="book.id", primary_key=True)
class UserRoleLink(SQLModel, table=True):
"""Модель связи роли и пользователя"""
__tablename__ = "user_roles"
user_id: int | None = Field(default=None, foreign_key="users.id", primary_key=True)
@@ -30,13 +34,14 @@ class BookUserLink(SQLModel, table=True):
Модель истории выдачи книг (Loan).
Связывает книгу и пользователя с фиксацией времени.
"""
__tablename__ = "book_loans"
id: int | None = Field(default=None, primary_key=True, index=True)
book_id: int = Field(foreign_key="book.id")
user_id: int = Field(foreign_key="users.id")
borrowed_at: datetime = Field(default_factory=datetime.utcnow)
borrowed_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
due_date: datetime
returned_at: datetime | None = Field(default=None)
returned_at: datetime | None = Field(default=None)
+47 -5
View File
@@ -1,5 +1,6 @@
"""Модуль DB-моделей пользователей"""
from datetime import datetime
from datetime import datetime, timezone
from typing import TYPE_CHECKING, List
from sqlmodel import Field, Relationship
@@ -13,17 +14,58 @@ if TYPE_CHECKING:
class User(UserBase, table=True):
"""Модель пользователя в базе данных"""
__tablename__ = "users"
id: int | None = Field(default=None, primary_key=True, index=True)
hashed_password: str = Field(nullable=False)
is_2fa_enabled: bool = Field(default=False)
totp_secret: str | None = Field(default=None, max_length=64)
recovery_code_hashes: str | None = Field(default=None, max_length=1500)
recovery_codes_generated_at: datetime | None = Field(default=None)
is_active: bool = Field(default=True)
is_verified: bool = Field(default=False)
created_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime | None = Field(
default=None, sa_column_kwargs={"onupdate": datetime.utcnow}
default=None, sa_column_kwargs={"onupdate": lambda: datetime.now(timezone.utc)}
)
# Связи
roles: List["Role"] = Relationship(back_populates="users", link_model=UserRoleLink)
loans: List["BookUserLink"] = Relationship(sa_relationship_kwargs={"cascade": "all, delete"})
loans: List["BookUserLink"] = Relationship( # ty: ignore[unresolved-reference]
sa_relationship_kwargs={"cascade": "all, delete"}
)
@property
def recovery_codes_list(self) -> list[str]:
"""Список хешей"""
if not self.recovery_code_hashes:
return []
return self.recovery_code_hashes.split(" ")
@property
def recovery_codes_total(self) -> int:
"""Общее количество слотов"""
if not self.recovery_code_hashes:
return 0
return len(self.recovery_codes_list)
@property
def recovery_codes_remaining(self) -> int:
"""Количество неиспользованных кодов"""
return sum(1 for h in self.recovery_codes_list if h)
@property
def recovery_codes_used(self) -> int:
"""Количество использованных кодов"""
return self.recovery_codes_total - self.recovery_codes_remaining
def get_recovery_code_positions(self) -> dict[str, list[int]]:
"""Возвращает позиции использованных и оставшихся кодов"""
used = []
remaining = []
for i, h in enumerate(self.recovery_codes_list, start=1):
if h:
remaining.append(i)
else:
used.append(i)
return {"used": used, "remaining": remaining}