Добавление авторизации и фронтэнда

This commit is contained in:
2025-12-18 18:52:09 +03:00
parent 2c24f66de0
commit 756e941f99
55 changed files with 2314 additions and 577 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
from .dto import *
"""Модуль моделей"""
from .db import *
from .dto import *
+7 -10
View File
@@ -1,25 +1,22 @@
"""Модуль моделей для базы данных"""
from .author import Author
from .book import Book
from .genre import Genre
from .role import Role
from .user import User
from .links import (
AuthorBookLink,
GenreBookLink,
AuthorWithBooks,
BookWithAuthors,
GenreWithBooks,
BookWithGenres,
BookWithAuthorsAndGenres,
UserRoleLink
)
__all__ = [
"Author",
"Book",
"Genre",
"Role",
"User",
"AuthorBookLink",
"AuthorWithBooks",
"BookWithAuthors",
"GenreBookLink",
"GenreWithBooks",
"BookWithGenres",
"BookWithAuthorsAndGenres",
"UserRoleLink",
]
+9 -5
View File
@@ -1,14 +1,18 @@
from typing import List, Optional, TYPE_CHECKING
from sqlmodel import SQLModel, Field, Relationship
from ..dto.author import AuthorBase
from .links import AuthorBookLink
"""Модуль DB-моделей авторов"""
from typing import TYPE_CHECKING, List
from sqlmodel import Field, Relationship
from library_service.models.dto.author import AuthorBase
from library_service.models.db.links import AuthorBookLink
if TYPE_CHECKING:
from .book import Book
class Author(AuthorBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True, index=True)
"""Модель автора в базе данных"""
id: int | None = Field(default=None, primary_key=True, index=True)
books: List["Book"] = Relationship(
back_populates="authors", link_model=AuthorBookLink
)
+9 -5
View File
@@ -1,7 +1,10 @@
from typing import List, Optional, TYPE_CHECKING
from sqlmodel import SQLModel, Field, Relationship
from ..dto.book import BookBase
from .links import AuthorBookLink, GenreBookLink
"""Модуль DB-моделей книг"""
from typing import TYPE_CHECKING, List
from sqlmodel import Field, Relationship
from library_service.models.dto.book import BookBase
from library_service.models.db.links import AuthorBookLink, GenreBookLink
if TYPE_CHECKING:
from .author import Author
@@ -9,7 +12,8 @@ if TYPE_CHECKING:
class Book(BookBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True, index=True)
"""Модель книги в базе данных"""
id: int | None = Field(default=None, primary_key=True, index=True)
authors: List["Author"] = Relationship(
back_populates="books", link_model=AuthorBookLink
)
+9 -5
View File
@@ -1,14 +1,18 @@
from typing import List, Optional, TYPE_CHECKING
from sqlmodel import SQLModel, Field, Relationship
from ..dto.genre import GenreBase
from .links import GenreBookLink
"""Модуль DB-моделей жанров"""
from typing import TYPE_CHECKING, List
from sqlmodel import Field, Relationship
from library_service.models.dto.genre import GenreBase
from library_service.models.db.links import GenreBookLink
if TYPE_CHECKING:
from .book import Book
class Genre(GenreBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True, index=True)
"""Модель жанра в базе данных"""
id: int | None = Field(default=None, primary_key=True, index=True)
books: List["Book"] = Relationship(
back_populates="genres", link_model=GenreBookLink
)
+8 -23
View File
@@ -1,12 +1,9 @@
"""Модуль связей между сущностями в БД"""
from sqlmodel import SQLModel, Field
from typing import List
from library_service.models.dto.author import AuthorRead
from library_service.models.dto.book import BookRead
from library_service.models.dto.genre import GenreRead
class AuthorBookLink(SQLModel, table=True):
"""Модель связи автора и книги"""
author_id: int | None = Field(
default=None, foreign_key="author.id", primary_key=True
)
@@ -14,26 +11,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 AuthorWithBooks(AuthorRead):
books: List[BookRead] = Field(default_factory=list)
class UserRoleLink(SQLModel, table=True):
"""Модель связи роли и пользователя"""
__tablename__ = "user_roles"
class BookWithAuthors(BookRead):
authors: List[AuthorRead] = Field(default_factory=list)
class BookWithGenres(BookRead):
genres: List[GenreRead] = Field(default_factory=list)
class GenreWithBooks(GenreRead):
books: List[BookRead] = Field(default_factory=list)
class BookWithAuthorsAndGenres(BookRead):
authors: List[AuthorRead] = Field(default_factory=list)
genres: List[GenreRead] = Field(default_factory=list)
user_id: int | None = Field(default=None, foreign_key="users.id", primary_key=True)
role_id: int | None = Field(default=None, foreign_key="roles.id", primary_key=True)
+20
View File
@@ -0,0 +1,20 @@
"""Модуль DB-моделей ролей"""
from typing import TYPE_CHECKING, List
from sqlmodel import Field, Relationship
from library_service.models.dto.role import RoleBase
from library_service.models.db.links import UserRoleLink
if TYPE_CHECKING:
from .user import User
class Role(RoleBase, table=True):
"""Модель роли в базе данных"""
__tablename__ = "roles"
id: int | None = Field(default=None, primary_key=True, index=True)
# Связи
users: List["User"] = Relationship(back_populates="roles", link_model=UserRoleLink)
+28
View File
@@ -0,0 +1,28 @@
"""Модуль DB-моделей пользователей"""
from datetime import datetime
from typing import TYPE_CHECKING, List
from sqlmodel import Field, Relationship
from library_service.models.dto.user import UserBase
from library_service.models.db.links import UserRoleLink
if TYPE_CHECKING:
from .role import Role
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_active: bool = Field(default=True)
is_verified: bool = Field(default=False)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime | None = Field(
default=None, sa_column_kwargs={"onupdate": datetime.utcnow}
)
# Связи
roles: List["Role"] = Relationship(back_populates="users", link_model=UserRoleLink)
+22 -4
View File
@@ -1,7 +1,12 @@
from .author import AuthorBase, AuthorCreate, AuthorUpdate, AuthorRead, AuthorList
from .book import BookBase, BookCreate, BookUpdate, BookRead, BookList
from .genre import GenreBase, GenreCreate, GenreUpdate, GenreRead, GenreList
"""Модуль 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, UserLogin, UserRead, UserUpdate
from .token import Token, TokenData
from .combined import (AuthorWithBooks, GenreWithBooks, BookWithAuthors, BookWithGenres,
BookWithAuthorsAndGenres, BookFilteredList)
__all__ = [
"AuthorBase",
@@ -14,9 +19,22 @@ __all__ = [
"BookUpdate",
"BookRead",
"BookList",
"BookFilteredList",
"GenreBase",
"GenreCreate",
"GenreUpdate",
"GenreRead",
"GenreList",
"RoleBase",
"RoleCreate",
"RoleUpdate",
"RoleRead",
"RoleList",
"Token",
"TokenData",
"UserBase",
"UserCreate",
"UserRead",
"UserUpdate",
"UserLogin",
]
+10 -3
View File
@@ -1,9 +1,12 @@
from sqlmodel import SQLModel
"""Модуль DTO-моделей авторов"""
from typing import List
from pydantic import ConfigDict
from typing import Optional, List
from sqlmodel import SQLModel
class AuthorBase(SQLModel):
"""Базовая модель автора"""
name: str
model_config = ConfigDict( # pyright: ignore
@@ -12,17 +15,21 @@ class AuthorBase(SQLModel):
class AuthorCreate(AuthorBase):
"""Модель автора для создания"""
pass
class AuthorUpdate(SQLModel):
name: Optional[str] = None
"""Модель автора для обновления"""
name: str | None = None
class AuthorRead(AuthorBase):
"""Модель автора для чтения"""
id: int
class AuthorList(SQLModel):
"""Список авторов"""
authors: List[AuthorRead]
total: int
+14 -4
View File
@@ -1,9 +1,15 @@
from sqlmodel import SQLModel
"""Модуль DTO-моделей книг"""
from typing import List, TYPE_CHECKING
from pydantic import ConfigDict
from typing import Optional, List
from sqlmodel import SQLModel
if TYPE_CHECKING:
from .combined import BookWithAuthorsAndGenres
class BookBase(SQLModel):
"""Базовая модель книги"""
title: str
description: str
@@ -15,18 +21,22 @@ class BookBase(SQLModel):
class BookCreate(BookBase):
"""Модель книги для создания"""
pass
class BookUpdate(SQLModel):
title: Optional[str] = None
description: Optional[str] = None
"""Модель книги для обновления"""
title: str | None = None
description: str | None = None
class BookRead(BookBase):
"""Модель книги для чтения"""
id: int
class BookList(SQLModel):
"""Список книг"""
books: List[BookRead]
total: int
+53
View File
@@ -0,0 +1,53 @@
"""Модуль объединёных объектов"""
from typing import List
from sqlmodel import SQLModel, Field
from .author import AuthorRead
from .genre import GenreRead
from .book import BookRead
class AuthorWithBooks(SQLModel):
"""Модель автора с книгами"""
id: int
name: str
bio: str
books: List[BookRead] = Field(default_factory=list)
class GenreWithBooks(SQLModel):
"""Модель жанра с книгами"""
id: int
name: str
books: List[BookRead] = Field(default_factory=list)
class BookWithAuthors(SQLModel):
"""Модель книги с авторами"""
id: int
title: str
description: str
authors: List[AuthorRead] = Field(default_factory=list)
class BookWithGenres(SQLModel):
"""Модель книги с жанрами"""
id: int
title: str
description: str
genres: List[GenreRead] = Field(default_factory=list)
class BookWithAuthorsAndGenres(SQLModel):
"""Модель с авторами и жанрами"""
id: int
title: str
description: str
authors: List[AuthorRead] = Field(default_factory=list)
genres: List[GenreRead] = Field(default_factory=list)
class BookFilteredList(SQLModel):
"""Список книг с фильтрацией"""
books: List[BookWithAuthorsAndGenres]
total: int
+10 -3
View File
@@ -1,9 +1,12 @@
from sqlmodel import SQLModel
"""Модуль DTO-моделей жанров"""
from typing import List
from pydantic import ConfigDict
from typing import Optional, List
from sqlmodel import SQLModel
class GenreBase(SQLModel):
"""Базовая модель жанра"""
name: str
model_config = ConfigDict( # pyright: ignore
@@ -12,17 +15,21 @@ class GenreBase(SQLModel):
class GenreCreate(GenreBase):
"""Модель жанра для создания"""
pass
class GenreUpdate(SQLModel):
name: Optional[str] = None
"""Модель жанра для обновления"""
name: str | None = None
class GenreRead(GenreBase):
"""Модель жанра для чтения"""
id: int
class GenreList(SQLModel):
"""Списко жанров"""
genres: List[GenreRead]
total: int
+31
View File
@@ -0,0 +1,31 @@
"""Модуль DTO-моделей ролей"""
from typing import List
from sqlmodel import SQLModel
class RoleBase(SQLModel):
"""Базовая модель роли"""
name: str
description: str | None = None
class RoleCreate(RoleBase):
"""Модель роли для создания"""
pass
class RoleUpdate(SQLModel):
"""Модель роли для обновления"""
name: str | None = None
class RoleRead(RoleBase):
"""Модель роли для чтения"""
id: int
class RoleList(SQLModel):
"""Список ролей"""
roles: List[RoleRead]
total: int
+15
View File
@@ -0,0 +1,15 @@
"""Модуль DTO-моделей токенов"""
from sqlmodel import SQLModel
class Token(SQLModel):
"""Модель токена"""
access_token: str
token_type: str = "bearer"
refresh_token: str | None = None
class TokenData(SQLModel):
"""Модель содержимого токена"""
username: str | None = None
user_id: int | None = None
+61
View File
@@ -0,0 +1,61 @@
"""Модуль DTO-моделей пользователей"""
import re
from typing import List
from pydantic import ConfigDict, EmailStr, field_validator
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)
model_config = ConfigDict(
json_schema_extra={
"example": {
"username": "johndoe",
"email": "john@example.com",
"full_name": "John Doe",
}
}
)
class UserCreate(UserBase):
"""Модель пользователя для создания"""
password: str = Field(min_length=8, max_length=100)
@field_validator("password")
@classmethod
def validate_password(cls, v: str) -> str:
"""Валидация пароля"""
if not re.search(r"[A-Z]", v):
raise ValueError("Пароль должен содержать символы в верхнем регистре")
if not re.search(r"[a-z]", v):
raise ValueError("Пароль должен содержать символы в нижнем регистре")
if not re.search(r"\d", v):
raise ValueError("пароль должен содержать цифры")
return v
class UserLogin(SQLModel):
"""Модель аутентификации для пользователя"""
username: str
password: str
class UserRead(UserBase):
"""Модель пользователя для чтения"""
id: int
is_active: bool
is_verified: bool
roles: List[str] = []
class UserUpdate(SQLModel):
"""Модель пользователя для обновления"""
email: EmailStr | None = None
full_name: str | None = None
password: str | None = None