mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 04:31:09 +00:00
Добавление авторизации и фронтэнда
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"""Модуль объединения роутеров"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .auth import router as auth_router
|
||||
from .authors import router as authors_router
|
||||
from .books import router as books_router
|
||||
from .genres import router as genres_router
|
||||
@@ -9,6 +11,7 @@ from .misc import router as misc_router
|
||||
api_router = APIRouter()
|
||||
|
||||
# Подключение всех маршрутов
|
||||
api_router.include_router(auth_router)
|
||||
api_router.include_router(authors_router)
|
||||
api_router.include_router(books_router)
|
||||
api_router.include_router(genres_router)
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Модуль работы с авторизацией и аутентификацией пользователей"""
|
||||
from datetime import timedelta
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from library_service.models.db import Role, User
|
||||
from library_service.models.dto import Token, UserCreate, UserRead, UserUpdate
|
||||
from library_service.settings import get_session
|
||||
from library_service.auth import (ACCESS_TOKEN_EXPIRE_MINUTES, RequireAdmin,
|
||||
RequireAuth, authenticate_user, get_password_hash,
|
||||
create_access_token, create_refresh_token)
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["authentication"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/register",
|
||||
response_model=UserRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Регистрация нового пользователя",
|
||||
description="Создает нового пользователя в системе",
|
||||
)
|
||||
def register(user_data: UserCreate, session: Session = Depends(get_session)):
|
||||
"""Эндпоинт регистрации пользователя"""
|
||||
# Проверка если username существует
|
||||
existing_user = session.exec(
|
||||
select(User).where(User.username == user_data.username)
|
||||
).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Username already registered",
|
||||
)
|
||||
|
||||
# Проверка если email существует
|
||||
existing_email = session.exec(
|
||||
select(User).where(User.email == user_data.email)
|
||||
).first()
|
||||
if existing_email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered"
|
||||
)
|
||||
|
||||
# Создание пользователя
|
||||
db_user = User(
|
||||
**user_data.model_dump(exclude={"password"}),
|
||||
hashed_password=get_password_hash(user_data.password)
|
||||
)
|
||||
|
||||
# Назначение роли по умолчанию
|
||||
default_role = session.exec(select(Role).where(Role.name == "user")).first()
|
||||
if default_role:
|
||||
db_user.roles.append(default_role)
|
||||
|
||||
session.add(db_user)
|
||||
session.commit()
|
||||
session.refresh(db_user)
|
||||
|
||||
return UserRead(**db_user.model_dump(), roles=[role.name for role in db_user.roles])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/token",
|
||||
response_model=Token,
|
||||
summary="Получение токена",
|
||||
description="Аутентификация и получение JWT токена",
|
||||
)
|
||||
def login(
|
||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт аутентификации и получения JWT токена"""
|
||||
user = authenticate_user(session, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username, "user_id": user.id},
|
||||
expires_delta=access_token_expires,
|
||||
)
|
||||
refresh_token = create_refresh_token(
|
||||
data={"sub": user.username, "user_id": user.id}
|
||||
)
|
||||
|
||||
return Token(
|
||||
access_token=access_token, refresh_token=refresh_token, token_type="bearer"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/me",
|
||||
response_model=UserRead,
|
||||
summary="Текущий пользователь",
|
||||
description="Получить информацию о текущем авторизованном пользователе",
|
||||
)
|
||||
def read_users_me(current_user: RequireAuth):
|
||||
"""Эндпоинт получения информации о себе"""
|
||||
return UserRead(
|
||||
**current_user.model_dump(), roles=[role.name for role in current_user.roles]
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/me",
|
||||
response_model=UserRead,
|
||||
summary="Обновить профиль",
|
||||
description="Обновить информацию текущего пользователя",
|
||||
)
|
||||
def update_user_me(
|
||||
user_update: UserUpdate,
|
||||
current_user: RequireAuth,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт обновления пользователя"""
|
||||
if user_update.email:
|
||||
current_user.email = user_update.email
|
||||
if user_update.full_name:
|
||||
current_user.full_name = user_update.full_name
|
||||
if user_update.password:
|
||||
current_user.hashed_password = get_password_hash(user_update.password)
|
||||
|
||||
session.add(current_user)
|
||||
session.commit()
|
||||
session.refresh(current_user)
|
||||
|
||||
return UserRead(
|
||||
**current_user.model_dump(), roles=[role.name for role in current_user.roles]
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/users",
|
||||
response_model=list[UserRead],
|
||||
summary="Список пользователей",
|
||||
description="Получить список всех пользователей (только для админов)",
|
||||
)
|
||||
def read_users(
|
||||
admin: RequireAdmin,
|
||||
session: Session = Depends(get_session),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
):
|
||||
"""Эндпоинт получения списка всех пользователей"""
|
||||
users = session.exec(select(User).offset(skip).limit(limit)).all()
|
||||
return [
|
||||
UserRead(**user.model_dump(), roles=[role.name for role in user.roles])
|
||||
for user in users
|
||||
]
|
||||
@@ -1,28 +1,28 @@
|
||||
from fastapi import APIRouter, Path, Depends, HTTPException
|
||||
"""Модуль работы с авторами"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from library_service.auth import RequireAuth
|
||||
from library_service.settings import get_session
|
||||
from library_service.models.db import Author, AuthorBookLink, Book, AuthorWithBooks
|
||||
from library_service.models.dto import (
|
||||
AuthorCreate,
|
||||
AuthorUpdate,
|
||||
AuthorRead,
|
||||
AuthorList,
|
||||
BookRead,
|
||||
)
|
||||
|
||||
from library_service.models.db import Author, AuthorBookLink, Book
|
||||
from library_service.models.dto import (BookRead, AuthorWithBooks,
|
||||
AuthorCreate, AuthorList, AuthorRead, AuthorUpdate)
|
||||
|
||||
router = APIRouter(prefix="/authors", tags=["authors"])
|
||||
|
||||
|
||||
# Create an author
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=AuthorRead,
|
||||
summary="Создать автора",
|
||||
description="Добавляет автора в систему",
|
||||
)
|
||||
def create_author(author: AuthorCreate, session: Session = Depends(get_session)):
|
||||
def create_author(
|
||||
current_user: RequireAuth,
|
||||
author: AuthorCreate,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт создания автора"""
|
||||
db_author = Author(**author.model_dump())
|
||||
session.add(db_author)
|
||||
session.commit()
|
||||
@@ -30,7 +30,6 @@ def create_author(author: AuthorCreate, session: Session = Depends(get_session))
|
||||
return AuthorRead(**db_author.model_dump())
|
||||
|
||||
|
||||
# Read authors
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=AuthorList,
|
||||
@@ -38,6 +37,7 @@ def create_author(author: AuthorCreate, session: Session = Depends(get_session))
|
||||
description="Возвращает список всех авторов в системе",
|
||||
)
|
||||
def read_authors(session: Session = Depends(get_session)):
|
||||
"""Эндпоинт чтения списка авторов"""
|
||||
authors = session.exec(select(Author)).all()
|
||||
return AuthorList(
|
||||
authors=[AuthorRead(**author.model_dump()) for author in authors],
|
||||
@@ -45,7 +45,6 @@ def read_authors(session: Session = Depends(get_session)):
|
||||
)
|
||||
|
||||
|
||||
# Read an author with their books
|
||||
@router.get(
|
||||
"/{author_id}",
|
||||
response_model=AuthorWithBooks,
|
||||
@@ -56,6 +55,7 @@ def get_author(
|
||||
author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт чтения конкретного автора"""
|
||||
author = session.get(Author, author_id)
|
||||
if not author:
|
||||
raise HTTPException(status_code=404, detail="Author not found")
|
||||
@@ -72,7 +72,6 @@ def get_author(
|
||||
return AuthorWithBooks(**author_data)
|
||||
|
||||
|
||||
# Update an author
|
||||
@router.put(
|
||||
"/{author_id}",
|
||||
response_model=AuthorRead,
|
||||
@@ -80,10 +79,12 @@ def get_author(
|
||||
description="Обновляет информацию об авторе в системе",
|
||||
)
|
||||
def update_author(
|
||||
current_user: RequireAuth,
|
||||
author: AuthorUpdate,
|
||||
author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт обновления автора"""
|
||||
db_author = session.get(Author, author_id)
|
||||
if not db_author:
|
||||
raise HTTPException(status_code=404, detail="Author not found")
|
||||
@@ -97,7 +98,6 @@ def update_author(
|
||||
return AuthorRead(**db_author.model_dump())
|
||||
|
||||
|
||||
# Delete an author
|
||||
@router.delete(
|
||||
"/{author_id}",
|
||||
response_model=AuthorRead,
|
||||
@@ -105,9 +105,11 @@ def update_author(
|
||||
description="Удаляет автора из системы",
|
||||
)
|
||||
def delete_author(
|
||||
current_user: RequireAuth,
|
||||
author_id: int = Path(..., description="ID автора (целое число, > 0)", gt=0),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт удаления автора"""
|
||||
author = session.get(Author, author_id)
|
||||
if not author:
|
||||
raise HTTPException(status_code=404, detail="Author not found")
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
from fastapi import APIRouter, Path, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
"""Модуль работы с книгами"""
|
||||
from typing import List
|
||||
|
||||
from library_service.models.db.links import BookWithAuthorsAndGenres
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from sqlmodel import Session, select, col, func
|
||||
|
||||
from library_service.auth import RequireAuth
|
||||
from library_service.settings import get_session
|
||||
from library_service.models.db import Author, Book, BookWithAuthors, AuthorBookLink
|
||||
from library_service.models.dto import (
|
||||
AuthorRead,
|
||||
BookList,
|
||||
BookRead,
|
||||
BookCreate,
|
||||
BookUpdate,
|
||||
from library_service.models.db import Author, AuthorBookLink, Book
|
||||
from library_service.models.dto import AuthorRead, BookCreate, BookList, BookRead, BookUpdate
|
||||
from library_service.models.dto.combined import (
|
||||
BookWithAuthorsAndGenres,
|
||||
BookFilteredList
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/books", tags=["books"])
|
||||
|
||||
|
||||
# Create a book
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=Book,
|
||||
summary="Создать книгу",
|
||||
description="Добавляет книгу в систему",
|
||||
)
|
||||
def create_book(book: BookCreate, session: Session = Depends(get_session)):
|
||||
def create_book(
|
||||
current_user: RequireAuth, book: BookCreate, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Эндпоинт создания книги"""
|
||||
db_book = Book(**book.model_dump())
|
||||
session.add(db_book)
|
||||
session.commit()
|
||||
@@ -31,7 +34,6 @@ def create_book(book: BookCreate, session: Session = Depends(get_session)):
|
||||
return BookRead(**db_book.model_dump())
|
||||
|
||||
|
||||
# Read books
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=BookList,
|
||||
@@ -39,13 +41,13 @@ def create_book(book: BookCreate, session: Session = Depends(get_session)):
|
||||
description="Возвращает список всех книг в системе",
|
||||
)
|
||||
def read_books(session: Session = Depends(get_session)):
|
||||
"""Эндпоинт чтения списка книг"""
|
||||
books = session.exec(select(Book)).all()
|
||||
return BookList(
|
||||
books=[BookRead(**book.model_dump()) for book in books], total=len(books)
|
||||
)
|
||||
|
||||
|
||||
# Read a book with their authors and genres
|
||||
@router.get(
|
||||
"/{book_id}",
|
||||
response_model=BookWithAuthorsAndGenres,
|
||||
@@ -56,6 +58,7 @@ def get_book(
|
||||
book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт чтения конкретной книги"""
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
@@ -76,10 +79,9 @@ def get_book(
|
||||
book_data["authors"] = author_reads
|
||||
book_data["genres"] = genre_reads
|
||||
|
||||
return BookWithAuthors(**book_data)
|
||||
return BookWithAuthorsAndGenres(**book_data)
|
||||
|
||||
|
||||
# Update a book
|
||||
@router.put(
|
||||
"/{book_id}",
|
||||
response_model=Book,
|
||||
@@ -87,10 +89,12 @@ def get_book(
|
||||
description="Обновляет информацию о книге в системе",
|
||||
)
|
||||
def update_book(
|
||||
current_user: RequireAuth,
|
||||
book: BookUpdate,
|
||||
book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт обновления книги"""
|
||||
db_book = session.get(Book, book_id)
|
||||
if not db_book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
@@ -102,7 +106,6 @@ def update_book(
|
||||
return db_book
|
||||
|
||||
|
||||
# Delete a book
|
||||
@router.delete(
|
||||
"/{book_id}",
|
||||
response_model=BookRead,
|
||||
@@ -110,9 +113,11 @@ def update_book(
|
||||
description="Удаляет книгу их системы",
|
||||
)
|
||||
def delete_book(
|
||||
current_user: RequireAuth,
|
||||
book_id: int = Path(..., description="ID книги (целое число, > 0)", gt=0),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт удаления книги"""
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
@@ -122,3 +127,51 @@ def delete_book(
|
||||
session.delete(book)
|
||||
session.commit()
|
||||
return book_read
|
||||
|
||||
|
||||
@router.get(
|
||||
"/filter",
|
||||
response_model=BookFilteredList,
|
||||
summary="Фильтрация книг",
|
||||
description="Фильтрация списка книг по названию, авторам и жанрам с пагинацией"
|
||||
)
|
||||
def filter_books(
|
||||
session: Session = Depends(get_session),
|
||||
q: str | None = Query(None, min_length=3, max_length=50, description="Поиск"),
|
||||
author_ids: List[int] | None = Query(None, description="Список ID авторов"),
|
||||
genre_ids: List[int] | None = Query(None, description="Список ID жанров"),
|
||||
page: int = Query(1, gt=0, description="Номер страницы"),
|
||||
size: int = Query(20, gt=0, lt=101, description="Количество элементов на странице"),
|
||||
):
|
||||
"""Эндпоинт получения отфильтрованного списка книг"""
|
||||
statement = select(Book).distinct()
|
||||
|
||||
if q:
|
||||
statement = statement.where(
|
||||
(col(Book.title).ilike(f"%{q}%")) | (col(Book.description).ilike(f"%{q}%"))
|
||||
)
|
||||
|
||||
if author_ids:
|
||||
statement = statement.join(AuthorBookLink).where(AuthorBookLink.author_id.in_(author_ids))
|
||||
|
||||
if genre_ids:
|
||||
statement = statement.join(GenreBookLink).where(GenreBookLink.genre_id.in_(genre_ids))
|
||||
|
||||
total_statement = select(func.count()).select_from(statement.subquery())
|
||||
total = session.exec(total_statement).one()
|
||||
|
||||
offset = (page - 1) * size
|
||||
statement = statement.offset(offset).limit(size)
|
||||
results = session.exec(statement).all()
|
||||
|
||||
books_with_data = []
|
||||
for db_book in results:
|
||||
books_with_data.append(
|
||||
BookWithAuthorsAndGenres(
|
||||
**db_book.model_dump(),
|
||||
authors=[AuthorRead(**a.model_dump()) for a in db_book.authors],
|
||||
genres=[GenreRead(**g.model_dump()) for g in db_book.genres]
|
||||
)
|
||||
)
|
||||
|
||||
return BookFilteredList(books=books_with_data, total=total)
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
from fastapi import APIRouter, Path, Depends, HTTPException
|
||||
"""Модуль работы с жанрами"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from library_service.auth import RequireAuth
|
||||
from library_service.models.db import Book, Genre, GenreBookLink
|
||||
from library_service.models.dto import BookRead, GenreCreate, GenreList, GenreRead, GenreUpdate, GenreWithBooks
|
||||
from library_service.settings import get_session
|
||||
from library_service.models.db import Genre, GenreBookLink, Book, GenreWithBooks
|
||||
from library_service.models.dto import (
|
||||
GenreCreate,
|
||||
GenreUpdate,
|
||||
GenreRead,
|
||||
GenreList,
|
||||
BookRead,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/genres", tags=["genres"])
|
||||
|
||||
|
||||
# Create a genre
|
||||
# Создание жанра
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=GenreRead,
|
||||
summary="Создать жанр",
|
||||
description="Добавляет жанр книг в систему",
|
||||
)
|
||||
def create_genre(genre: GenreCreate, session: Session = Depends(get_session)):
|
||||
def create_genre(
|
||||
current_user: RequireAuth,
|
||||
genre: GenreCreate,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт создания жанра"""
|
||||
db_genre = Genre(**genre.model_dump())
|
||||
session.add(db_genre)
|
||||
session.commit()
|
||||
@@ -30,7 +30,7 @@ def create_genre(genre: GenreCreate, session: Session = Depends(get_session)):
|
||||
return GenreRead(**db_genre.model_dump())
|
||||
|
||||
|
||||
# Read genres
|
||||
# Чтение жанров
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=GenreList,
|
||||
@@ -38,13 +38,14 @@ def create_genre(genre: GenreCreate, session: Session = Depends(get_session)):
|
||||
description="Возвращает список всех жанров в системе",
|
||||
)
|
||||
def read_genres(session: Session = Depends(get_session)):
|
||||
"""Эндпоинт чтения списка жанров"""
|
||||
genres = session.exec(select(Genre)).all()
|
||||
return GenreList(
|
||||
genres=[GenreRead(**genre.model_dump()) for genre in genres], total=len(genres)
|
||||
)
|
||||
|
||||
|
||||
# Read a genre with their books
|
||||
# Чтение жанра с его книгами
|
||||
@router.get(
|
||||
"/{genre_id}",
|
||||
response_model=GenreWithBooks,
|
||||
@@ -55,6 +56,7 @@ def get_genre(
|
||||
genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт чтения конкретного жанра"""
|
||||
genre = session.get(Genre, genre_id)
|
||||
if not genre:
|
||||
raise HTTPException(status_code=404, detail="Genre not found")
|
||||
@@ -71,7 +73,7 @@ def get_genre(
|
||||
return GenreWithBooks(**genre_data)
|
||||
|
||||
|
||||
# Update a genre
|
||||
# Обновление жанра
|
||||
@router.put(
|
||||
"/{genre_id}",
|
||||
response_model=GenreRead,
|
||||
@@ -79,10 +81,12 @@ def get_genre(
|
||||
description="Обновляет информацию о жанре в системе",
|
||||
)
|
||||
def update_genre(
|
||||
current_user: RequireAuth,
|
||||
genre: GenreUpdate,
|
||||
genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт обновления жанра"""
|
||||
db_genre = session.get(Genre, genre_id)
|
||||
if not db_genre:
|
||||
raise HTTPException(status_code=404, detail="Genre not found")
|
||||
@@ -96,7 +100,7 @@ def update_genre(
|
||||
return GenreRead(**db_genre.model_dump())
|
||||
|
||||
|
||||
# Delete a genre
|
||||
# Удаление жанра
|
||||
@router.delete(
|
||||
"/{genre_id}",
|
||||
response_model=GenreRead,
|
||||
@@ -104,9 +108,11 @@ def update_genre(
|
||||
description="Удаляет автора из системы",
|
||||
)
|
||||
def delete_genre(
|
||||
current_user: RequireAuth,
|
||||
genre_id: int = Path(..., description="ID жанра (целое число, > 0)", gt=0),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""Эндпоинт удаления жанра"""
|
||||
genre = session.get(Genre, genre_id)
|
||||
if not genre:
|
||||
raise HTTPException(status_code=404, detail="Genre not found")
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
from fastapi import APIRouter, Path, Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pathlib import Path
|
||||
"""Модуль прочих эндпоинтов"""
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from library_service.settings import get_app
|
||||
|
||||
# Загрузка шаблонов
|
||||
templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates")
|
||||
|
||||
router = APIRouter(tags=["misc"])
|
||||
templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates")
|
||||
|
||||
|
||||
# Форматированная информация о приложении
|
||||
def get_info(app) -> Dict:
|
||||
"""Форматированная информация о приложении"""
|
||||
return {
|
||||
"status": "ok",
|
||||
"app_info": {
|
||||
@@ -27,29 +28,49 @@ def get_info(app) -> Dict:
|
||||
}
|
||||
|
||||
|
||||
# Эндпоинт главной страницы
|
||||
@router.get("/", include_in_schema=False)
|
||||
async def root(request: Request, app=Depends(get_app)):
|
||||
"""Эндпоинт главной страницы"""
|
||||
return templates.TemplateResponse(request, "index.html", get_info(app))
|
||||
|
||||
|
||||
# Редирект иконки вкладки
|
||||
@router.get("/api", include_in_schema=False)
|
||||
async def root(request: Request, app=Depends(get_app)):
|
||||
"""Страница с сылками на документацию API"""
|
||||
return templates.TemplateResponse(request, "api.html", get_info(app))
|
||||
|
||||
|
||||
@router.get("/favicon.ico", include_in_schema=False)
|
||||
def redirect_favicon():
|
||||
"""Редирект иконки вкладки"""
|
||||
return RedirectResponse("/favicon.svg")
|
||||
|
||||
|
||||
# Эндпоинт иконки вкладки
|
||||
@router.get("/favicon.svg", include_in_schema=False)
|
||||
async def favicon():
|
||||
return FileResponse("library_service/favicon.svg", media_type="image/svg+xml")
|
||||
"""Эндпоинт иконки вкладки"""
|
||||
return FileResponse(
|
||||
"library_service/static/favicon.svg", media_type="image/svg+xml"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/static/{path:path}", include_in_schema=False)
|
||||
async def serve_static(path: str):
|
||||
"""Статические файлы"""
|
||||
static_dir = Path(__file__).parent.parent / "static"
|
||||
file_path = static_dir / path
|
||||
|
||||
if not file_path.is_file() or not file_path.is_relative_to(static_dir):
|
||||
return JSONResponse(status_code=404, content={"error": "File not found"})
|
||||
|
||||
return FileResponse(file_path)
|
||||
|
||||
|
||||
# Эндпоинт информации об API
|
||||
@router.get(
|
||||
"/api/info",
|
||||
summary="Информация о сервисе",
|
||||
description="Возвращает информацию о системе",
|
||||
)
|
||||
async def api_info(app=Depends(get_app)):
|
||||
"""Эндпоинт информации об API"""
|
||||
return JSONResponse(content=get_info(app))
|
||||
|
||||
@@ -1,15 +1,82 @@
|
||||
"""Модуль работы со связями"""
|
||||
from typing import Dict, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
from typing import List, Dict
|
||||
|
||||
from library_service.settings import get_session
|
||||
from library_service.models.db import Author, Book, Genre, AuthorBookLink, GenreBookLink
|
||||
from library_service.auth import RequireAuth
|
||||
from library_service.models.db import Author, AuthorBookLink, Book, Genre, GenreBookLink
|
||||
from library_service.models.dto import AuthorRead, BookRead, GenreRead
|
||||
from library_service.settings import get_session
|
||||
|
||||
|
||||
router = APIRouter(tags=["relations"])
|
||||
|
||||
|
||||
# Add author to book
|
||||
def check_entity_exists(session, model, entity_id, entity_name):
|
||||
"""Проверка существования связи между сущностями в БД"""
|
||||
entity = session.get(model, entity_id)
|
||||
if not entity:
|
||||
raise HTTPException(status_code=404, detail=f"{entity_name} not found")
|
||||
return entity
|
||||
|
||||
|
||||
def add_relationship(session, link_model, id1, field1, id2, field2, detail):
|
||||
"""Создание связи между сущностями в БД"""
|
||||
existing_link = session.exec(
|
||||
select(link_model)
|
||||
.where(getattr(link_model, field1) == id1)
|
||||
.where(getattr(link_model, field2) == id2)
|
||||
).first()
|
||||
|
||||
if existing_link:
|
||||
raise HTTPException(status_code=400, detail=detail)
|
||||
|
||||
link = link_model(**{field1: id1, field2: id2})
|
||||
session.add(link)
|
||||
session.commit()
|
||||
session.refresh(link)
|
||||
return link
|
||||
|
||||
|
||||
def remove_relationship(session, link_model, id1, field1, id2, field2):
|
||||
"""Удаление связи между сущностями в БД"""
|
||||
link = session.exec(
|
||||
select(link_model)
|
||||
.where(getattr(link_model, field1) == id1)
|
||||
.where(getattr(link_model, field2) == id2)
|
||||
).first()
|
||||
|
||||
if not link:
|
||||
raise HTTPException(status_code=404, detail="Relationship not found")
|
||||
|
||||
session.delete(link)
|
||||
session.commit()
|
||||
return {"message": "Relationship removed successfully"}
|
||||
|
||||
|
||||
def get_related(
|
||||
session,
|
||||
main_model,
|
||||
main_id,
|
||||
main_name,
|
||||
related_model,
|
||||
link_model,
|
||||
link_main_field,
|
||||
link_related_field,
|
||||
read_model
|
||||
):
|
||||
"""Получение связанных в БД сущностей"""
|
||||
check_entity_exists(session, main_model, main_id, main_name)
|
||||
|
||||
related = session.exec(
|
||||
select(related_model).join(link_model)
|
||||
.where(getattr(link_model, link_main_field) == main_id)
|
||||
).all()
|
||||
|
||||
return [read_model(**obj.model_dump()) for obj in related]
|
||||
|
||||
|
||||
@router.post(
|
||||
"/relationships/author-book",
|
||||
response_model=AuthorBookLink,
|
||||
@@ -17,33 +84,19 @@ router = APIRouter(tags=["relations"])
|
||||
description="Добавляет связь между автором и книгой в систему",
|
||||
)
|
||||
def add_author_to_book(
|
||||
author_id: int, book_id: int, session: Session = Depends(get_session)
|
||||
current_user: RequireAuth,
|
||||
author_id: int,
|
||||
book_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
author = session.get(Author, author_id)
|
||||
if not author:
|
||||
raise HTTPException(status_code=404, detail="Author not found")
|
||||
"""Эндпоинт добавления автора к книге"""
|
||||
check_entity_exists(session, Author, author_id, "Author")
|
||||
check_entity_exists(session, Book, book_id, "Book")
|
||||
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
|
||||
existing_link = session.exec(
|
||||
select(AuthorBookLink)
|
||||
.where(AuthorBookLink.author_id == author_id)
|
||||
.where(AuthorBookLink.book_id == book_id)
|
||||
).first()
|
||||
|
||||
if existing_link:
|
||||
raise HTTPException(status_code=400, detail="Relationship already exists")
|
||||
|
||||
link = AuthorBookLink(author_id=author_id, book_id=book_id)
|
||||
session.add(link)
|
||||
session.commit()
|
||||
session.refresh(link)
|
||||
return link
|
||||
return add_relationship(session, AuthorBookLink,
|
||||
author_id, "author_id", book_id, "book_id", "Relationship already exists")
|
||||
|
||||
|
||||
# Remove author from book
|
||||
@router.delete(
|
||||
"/relationships/author-book",
|
||||
response_model=Dict[str, str],
|
||||
@@ -51,23 +104,16 @@ def add_author_to_book(
|
||||
description="Удаляет связь между автором и книгой в системе",
|
||||
)
|
||||
def remove_author_from_book(
|
||||
author_id: int, book_id: int, session: Session = Depends(get_session)
|
||||
current_user: RequireAuth,
|
||||
author_id: int,
|
||||
book_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
link = session.exec(
|
||||
select(AuthorBookLink)
|
||||
.where(AuthorBookLink.author_id == author_id)
|
||||
.where(AuthorBookLink.book_id == book_id)
|
||||
).first()
|
||||
|
||||
if not link:
|
||||
raise HTTPException(status_code=404, detail="Relationship not found")
|
||||
|
||||
session.delete(link)
|
||||
session.commit()
|
||||
return {"message": "Relationship removed successfully"}
|
||||
"""Эндпоинт удаления автора из книги"""
|
||||
return remove_relationship(session, AuthorBookLink,
|
||||
author_id, "author_id", book_id, "book_id")
|
||||
|
||||
|
||||
# Get author's books
|
||||
@router.get(
|
||||
"/authors/{author_id}/books/",
|
||||
response_model=List[BookRead],
|
||||
@@ -75,18 +121,12 @@ def remove_author_from_book(
|
||||
description="Возвращает все книги в системе, написанные автором",
|
||||
)
|
||||
def get_books_for_author(author_id: int, session: Session = Depends(get_session)):
|
||||
author = session.get(Author, author_id)
|
||||
if not author:
|
||||
raise HTTPException(status_code=404, detail="Author not found")
|
||||
|
||||
books = session.exec(
|
||||
select(Book).join(AuthorBookLink).where(AuthorBookLink.author_id == author_id)
|
||||
).all()
|
||||
|
||||
return [BookRead(**book.model_dump()) for book in books]
|
||||
"""Эндпоинт получения книг, написанных автором"""
|
||||
return get_related(session,
|
||||
Author, author_id, "Author", Book,
|
||||
AuthorBookLink, "author_id", "book_id", BookRead)
|
||||
|
||||
|
||||
# Get book's authors
|
||||
@router.get(
|
||||
"/books/{book_id}/authors/",
|
||||
response_model=List[AuthorRead],
|
||||
@@ -94,18 +134,12 @@ def get_books_for_author(author_id: int, session: Session = Depends(get_session)
|
||||
description="Возвращает всех авторов книги в системе",
|
||||
)
|
||||
def get_authors_for_book(book_id: int, session: Session = Depends(get_session)):
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
|
||||
authors = session.exec(
|
||||
select(Author).join(AuthorBookLink).where(AuthorBookLink.book_id == book_id)
|
||||
).all()
|
||||
|
||||
return [AuthorRead(**author.model_dump()) for author in authors]
|
||||
"""Эндпоинт получения авторов книги"""
|
||||
return get_related(session,
|
||||
Book, book_id, "Book", Author,
|
||||
AuthorBookLink, "book_id", "author_id", AuthorRead)
|
||||
|
||||
|
||||
# Add genre to book
|
||||
@router.post(
|
||||
"/relationships/genre-book",
|
||||
response_model=GenreBookLink,
|
||||
@@ -113,33 +147,19 @@ def get_authors_for_book(book_id: int, session: Session = Depends(get_session)):
|
||||
description="Добавляет связь между книгой и жанром в систему",
|
||||
)
|
||||
def add_genre_to_book(
|
||||
genre_id: int, book_id: int, session: Session = Depends(get_session)
|
||||
current_user: RequireAuth,
|
||||
genre_id: int,
|
||||
book_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
genre = session.get(Genre, genre_id)
|
||||
if not genre:
|
||||
raise HTTPException(status_code=404, detail="Genre not found")
|
||||
"""Эндпоинт добавления жанра к книге"""
|
||||
check_entity_exists(session, Genre, genre_id, "Genre")
|
||||
check_entity_exists(session, Book, book_id, "Book")
|
||||
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
|
||||
existing_link = session.exec(
|
||||
select(GenreBookLink)
|
||||
.where(GenreBookLink.genre_id == genre_id)
|
||||
.where(GenreBookLink.book_id == book_id)
|
||||
).first()
|
||||
|
||||
if existing_link:
|
||||
raise HTTPException(status_code=400, detail="Relationship already exists")
|
||||
|
||||
link = GenreBookLink(genre_id=genre_id, book_id=book_id)
|
||||
session.add(link)
|
||||
session.commit()
|
||||
session.refresh(link)
|
||||
return link
|
||||
return add_relationship(session, GenreBookLink,
|
||||
genre_id, "genre_id", book_id, "book_id", "Relationship already exists")
|
||||
|
||||
|
||||
# Remove author from book
|
||||
@router.delete(
|
||||
"/relationships/genre-book",
|
||||
response_model=Dict[str, str],
|
||||
@@ -147,55 +167,37 @@ def add_genre_to_book(
|
||||
description="Удаляет связь между жанром и книгой в системе",
|
||||
)
|
||||
def remove_genre_from_book(
|
||||
genre_id: int, book_id: int, session: Session = Depends(get_session)
|
||||
current_user: RequireAuth,
|
||||
genre_id: int,
|
||||
book_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
link = session.exec(
|
||||
select(GenreBookLink)
|
||||
.where(GenreBookLink.genre_id == genre_id)
|
||||
.where(GenreBookLink.book_id == book_id)
|
||||
).first()
|
||||
|
||||
if not link:
|
||||
raise HTTPException(status_code=404, detail="Relationship not found")
|
||||
|
||||
session.delete(link)
|
||||
session.commit()
|
||||
return {"message": "Relationship removed successfully"}
|
||||
"""Эндпоинт удаления жанра из книги"""
|
||||
return remove_relationship(session, GenreBookLink,
|
||||
genre_id, "genre_id", book_id, "book_id")
|
||||
|
||||
|
||||
# Get genre's books
|
||||
@router.get(
|
||||
"/genres/{author_id}/books/",
|
||||
"/genres/{genre_id}/books/",
|
||||
response_model=List[BookRead],
|
||||
summary="Получить книги, написанные в жанре",
|
||||
description="Возвращает все книги в системе в этом жанре",
|
||||
)
|
||||
def get_books_for_genre(genre_id: int, session: Session = Depends(get_session)):
|
||||
genre = session.get(Genre, genre_id)
|
||||
if not genre:
|
||||
raise HTTPException(status_code=404, detail="Genre not found")
|
||||
|
||||
books = session.exec(
|
||||
select(Book).join(GenreBookLink).where(GenreBookLink.author_id == genre_id)
|
||||
).all()
|
||||
|
||||
return [BookRead(**book.model_dump()) for book in books]
|
||||
"""Эндпоинт получения книг с жанром"""
|
||||
return get_related(session,
|
||||
Genre, genre_id, "Genre", Book,
|
||||
GenreBookLink, "genre_id", "book_id", BookRead)
|
||||
|
||||
|
||||
# Get book's genres
|
||||
@router.get(
|
||||
"/books/{book_id}/genres/",
|
||||
response_model=List[GenreRead],
|
||||
summary="Получить жанры книги",
|
||||
description="Возвращает все жанры книги в системе",
|
||||
)
|
||||
def get_authors_for_book(book_id: int, session: Session = Depends(get_session)):
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
raise HTTPException(status_code=404, detail="Book not found")
|
||||
|
||||
genres = session.exec(
|
||||
select(Genre).join(GenreBookLink).where(GenreBookLink.book_id == book_id)
|
||||
).all()
|
||||
|
||||
return [GenreRead(**author.model_dump()) for genre in genres]
|
||||
def get_genres_for_book(book_id: int, session: Session = Depends(get_session)):
|
||||
"""Эндпоинт получения жанров книги"""
|
||||
return get_related(session,
|
||||
Book, book_id, "Book", Genre,
|
||||
GenreBookLink, "book_id", "genre_id", GenreRead)
|
||||
|
||||
Reference in New Issue
Block a user