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

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
+29 -3
View File
@@ -1,13 +1,29 @@
"""Модуль DTO-моделей"""
from .author import AuthorBase, AuthorCreate, AuthorList, AuthorRead, AuthorUpdate
from .genre import GenreBase, GenreCreate, GenreList, GenreRead, GenreUpdate
from .book import BookBase, BookCreate, BookList, BookRead, BookUpdate
from .role import RoleBase, RoleCreate, RoleList, RoleRead, RoleUpdate
from .user import UserBase, UserCreate, UserList, UserRead, UserUpdate, UserLogin
from .loan import LoanBase, LoanCreate, LoanList, LoanRead, LoanUpdate
from .token import Token, TokenData
from .combined import (AuthorWithBooks, GenreWithBooks, BookWithAuthors, BookWithGenres,
BookWithAuthorsAndGenres, BookFilteredList, BookStatusUpdate, LoanWithBook)
from .recovery import RecoveryCodesResponse, RecoveryCodesStatus, RecoveryCodeUse
from .token import Token, TokenData, PartialToken
from .combined import (
AuthorWithBooks,
GenreWithBooks,
BookWithAuthors,
BookWithGenres,
BookWithAuthorsAndGenres,
BookFilteredList,
BookStatusUpdate,
LoanWithBook,
LoginResponse,
RegisterResponse,
TOTPSetupResponse,
TOTPVerifyRequest,
TOTPDisableRequest,
PasswordResetResponse,
)
__all__ = [
"AuthorBase",
@@ -46,4 +62,14 @@ __all__ = [
"RoleList",
"Token",
"TokenData",
"PartialToken",
"TOTPSetupResponse",
"TOTPVerifyRequest",
"TOTPDisableRequest",
"RecoveryCodeUse",
"LoginResponse",
"RegisterResponse",
"RecoveryCodesStatus",
"PasswordResetResponse",
"RecoveryCodesResponse",
]
+67
View File
@@ -1,5 +1,8 @@
"""Модуль объединёных объектов"""
from datetime import datetime
from typing import List
from sqlmodel import SQLModel, Field
from .author import AuthorRead
@@ -8,8 +11,13 @@ from .book import BookRead
from .loan import LoanRead
from ..enums import BookStatus
from .user import UserRead
from .recovery import RecoveryCodesResponse, RecoveryCodesStatus
class AuthorWithBooks(SQLModel):
"""Модель автора с книгами"""
id: int
name: str
books: List[BookRead] = Field(default_factory=list)
@@ -17,6 +25,7 @@ class AuthorWithBooks(SQLModel):
class GenreWithBooks(SQLModel):
"""Модель жанра с книгами"""
id: int
name: str
books: List[BookRead] = Field(default_factory=list)
@@ -24,6 +33,7 @@ class GenreWithBooks(SQLModel):
class BookWithAuthors(SQLModel):
"""Модель книги с авторами"""
id: int
title: str
description: str
@@ -32,6 +42,7 @@ class BookWithAuthors(SQLModel):
class BookWithGenres(SQLModel):
"""Модель книги с жанрами"""
id: int
title: str
description: str
@@ -41,6 +52,7 @@ class BookWithGenres(SQLModel):
class BookWithAuthorsAndGenres(SQLModel):
"""Модель с авторами и жанрами"""
id: int
title: str
description: str
@@ -51,13 +63,68 @@ class BookWithAuthorsAndGenres(SQLModel):
class BookFilteredList(SQLModel):
"""Список книг с фильтрацией"""
books: List[BookWithAuthorsAndGenres]
total: int
class LoanWithBook(LoanRead):
"""Модель выдачи, включающая данные о книге"""
book: BookRead
class BookStatusUpdate(SQLModel):
"""Модель для ручного изменения статуса библиотекарем"""
status: str
class LoginResponse(SQLModel):
"""Модель для авторизации пользователя"""
access_token: str | None = None
partial_token: str | None = None
refresh_token: str | None = None
token_type: str = "bearer"
requires_2fa: bool = False
class RegisterResponse(SQLModel):
"""Модель для регистрации пользователя"""
user: UserRead
recovery_codes: RecoveryCodesResponse
class PasswordResetResponse(SQLModel):
"""Модель для сброса пароля"""
total: int
remaining: int
used_codes: list[bool]
generated_at: datetime | None
should_regenerate: bool
class TOTPSetupResponse(SQLModel):
"""Модель для генерации данных для настройки TOTP"""
secret: str
username: str
issuer: str
size: int
padding: int
bitmap_b64: str
class TOTPVerifyRequest(SQLModel):
"""Модель для проверки TOTP кода"""
code: str = Field(min_length=6, max_length=6, regex=r"^\d{6}$")
class TOTPDisableRequest(SQLModel):
"""Модель для отключения TOTP 2FA"""
password: str
+52
View File
@@ -0,0 +1,52 @@
"""Модуль DTO-моделей для резервных кодов восстановления"""
from datetime import datetime
import re
from pydantic import field_validator
from sqlmodel import SQLModel, Field
class RecoveryCodesResponse(SQLModel):
"""Ответ при генерации резервных кодов"""
codes: list[str]
generated_at: datetime
class RecoveryCodesStatus(SQLModel):
"""Статус резервных кодов пользователя"""
total: int
remaining: int
used_codes: list[bool]
generated_at: datetime | None
should_regenerate: bool
class RecoveryCodeUse(SQLModel):
"""Запрос на сброс пароля через резервный код"""
username: str
recovery_code: str = Field(min_length=19, max_length=19)
new_password: str = Field(min_length=8, max_length=100)
@field_validator("recovery_code")
@classmethod
def validate_recovery_code(cls, v: str) -> str:
if not re.match(
r"^[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}$", v
):
raise ValueError("Invalid recovery code format")
return v.lower()
@field_validator("new_password")
@classmethod
def validate_password(cls, v: str) -> str:
if not re.search(r"[A-Z]", v):
raise ValueError("Password must contain uppercase")
if not re.search(r"[a-z]", v):
raise ValueError("Password must contain lowercase")
if not re.search(r"\d", v):
raise ValueError("Password must contain digit")
return v
+12
View File
@@ -1,15 +1,27 @@
"""Модуль DTO-моделей токенов"""
from sqlmodel import SQLModel
class Token(SQLModel):
"""Модель токена"""
access_token: str
token_type: str = "bearer"
refresh_token: str | None = None
class PartialToken(SQLModel):
"""Частичный токен — для подтверждения 2FA"""
partial_token: str
token_type: str = "partial"
requires_2fa: bool = True
class TokenData(SQLModel):
"""Модель содержимого токена"""
username: str | None = None
user_id: int | None = None
is_partial: bool = False
+8
View File
@@ -1,4 +1,5 @@
"""Модуль DTO-моделей пользователей"""
import re
from typing import List
@@ -8,6 +9,7 @@ from sqlmodel import Field, SQLModel
class UserBase(SQLModel):
"""Базовая модель пользователя"""
username: str = Field(min_length=3, max_length=50, index=True, unique=True)
email: EmailStr = Field(index=True, unique=True)
full_name: str | None = Field(default=None, max_length=100)
@@ -25,6 +27,7 @@ class UserBase(SQLModel):
class UserCreate(UserBase):
"""Модель пользователя для создания"""
password: str = Field(min_length=8, max_length=100)
@field_validator("password")
@@ -42,20 +45,24 @@ class UserCreate(UserBase):
class UserLogin(SQLModel):
"""Модель аутентификации для пользователя"""
username: str
password: str
class UserRead(UserBase):
"""Модель пользователя для чтения"""
id: int
is_active: bool
is_verified: bool
is_2fa_enabled: bool
roles: List[str] = []
class UserUpdate(SQLModel):
"""Модель пользователя для обновления"""
email: EmailStr | None = None
full_name: str | None = None
password: str | None = None
@@ -63,5 +70,6 @@ class UserUpdate(SQLModel):
class UserList(SQLModel):
"""Список пользователей"""
users: List[UserRead]
total: int