исправление обработки 404

This commit is contained in:
2026-02-04 23:21:53 +03:00
parent a336d50ad0
commit 8ad70cdb7c
21 changed files with 173 additions and 201 deletions
+1 -18
View File
@@ -11,7 +11,7 @@ services:
- ./data/db:/var/lib/postgresql/data - ./data/db:/var/lib/postgresql/data
networks: networks:
- proxy - proxy
ports: # !сменить внешний порт перед использованием! ports: # !только локальный тест!
- 5432:5432 - 5432:5432
env_file: env_file:
- ./.env - ./.env
@@ -31,23 +31,6 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
replication-setup:
image: postgres:17-alpine
container_name: replication-setup
restart: "no"
networks:
- proxy
env_file:
- ./.env
volumes:
- ./setup-replication.sh:/setup-replication.sh
entrypoint: ["/bin/sh", "/setup-replication.sh"]
depends_on:
api:
condition: service_started
db:
condition: service_healthy
llm: llm:
image: ollama/ollama:latest image: ollama/ollama:latest
container_name: llm container_name: llm
-3
View File
@@ -4,9 +4,6 @@ POSTGRES_PORT=5432
POSTGRES_USER=postgres POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres POSTGRES_PASSWORD=postgres
POSTGRES_DB=lib POSTGRES_DB=lib
REMOTE_HOST=
REMOTE_PORT=
NODE_ID=
# Ollama # Ollama
OLLAMA_URL="http://llm:11434" OLLAMA_URL="http://llm:11434"
-4
View File
@@ -1,4 +0,0 @@
CREATE PUBLICATION all_tables_pub FOR ALL TABLES;
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
SELECT pg_reload_conf();
+7 -2
View File
@@ -73,16 +73,21 @@ app = get_app(lifespan)
@app.exception_handler(status.HTTP_404_NOT_FOUND) @app.exception_handler(status.HTTP_404_NOT_FOUND)
async def custom_not_found_handler(request: Request, exc: HTTPException): async def custom_not_found_handler(request: Request, exc: HTTPException):
if exc.detail == "Not Found":
path = request.url.path path = request.url.path
if path.startswith("/api"): if path.startswith("/api/"):
return JSONResponse( return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
content={"detail": "API endpoint not found", "path": path}, content={"detail": "API endpoint not found", "path": path},
) )
return await unknown(request, app) return await unknown(request, app)
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
)
@app.middleware("http") @app.middleware("http")
async def catch_exceptions_middleware(request: Request, call_next): async def catch_exceptions_middleware(request: Request, call_next):
+4 -2
View File
@@ -287,7 +287,8 @@ def enable_2fa(
if not current_user.totp_secret: if not current_user.totp_secret:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Secret key not generated" status_code=status.HTTP_400_BAD_REQUEST,
detail="Secret key not generated",
) )
if not verify_totp_code(secret, data.code): if not verify_totp_code(secret, data.code):
@@ -299,7 +300,8 @@ def enable_2fa(
decrypted = cipher.decrypt(base64.b64decode(current_user.totp_secret.encode())) decrypted = cipher.decrypt(base64.b64decode(current_user.totp_secret.encode()))
if secret != decrypted.decode(): if secret != decrypted.decode():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Incorret secret" status_code=status.HTTP_400_BAD_REQUEST,
detail="Incorret secret",
) )
current_user.is_2fa_enabled = True current_user.is_2fa_enabled = True
+6 -3
View File
@@ -67,7 +67,8 @@ def get_author(
author = session.get(Author, author_id) author = session.get(Author, author_id)
if not author: if not author:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Author not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Author not found",
) )
books = session.exec( books = session.exec(
@@ -98,7 +99,8 @@ def update_author(
db_author = session.get(Author, author_id) db_author = session.get(Author, author_id)
if not db_author: if not db_author:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Author not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Author not found",
) )
update_data = author.model_dump(exclude_unset=True) update_data = author.model_dump(exclude_unset=True)
@@ -125,7 +127,8 @@ def delete_author(
author = session.get(Author, author_id) author = session.get(Author, author_id)
if not author: if not author:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Author not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Author not found",
) )
author_read = AuthorRead(**author.model_dump()) author_read = AuthorRead(**author.model_dump())
+34 -18
View File
@@ -1,23 +1,20 @@
"""Модуль работы с книгами""" """Модуль работы с книгами"""
from library_service.services import transcode_image
import shutil
from uuid import uuid4
from pydantic import Field
from typing_extensions import Annotated from typing_extensions import Annotated
from uuid import uuid4
from sqlalchemy.orm import selectinload, defer import shutil
from sqlalchemy import text, case, distinct
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import List from typing import List
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status, UploadFile, File from fastapi import APIRouter, Depends, HTTPException, Path, Query, status, UploadFile, File
from ollama import Client from ollama import Client
from pydantic import Field
from sqlalchemy import text, case, distinct
from sqlalchemy.orm import selectinload, defer
from sqlmodel import Session, select, col, func from sqlmodel import Session, select, col, func
from library_service.auth import RequireStaff from library_service.auth import RequireStaff
from library_service.services import transcode_image
from library_service.settings import get_session, OLLAMA_URL, BOOKS_PREVIEW_DIR from library_service.settings import get_session, OLLAMA_URL, BOOKS_PREVIEW_DIR
from library_service.models.enums import BookStatus from library_service.models.enums import BookStatus
from library_service.models.db import ( from library_service.models.db import (
@@ -139,7 +136,8 @@ def create_book(
session.commit() session.commit()
session.refresh(db_book) session.refresh(db_book)
book_data = db_book.model_dump(exclude={"embedding", "preview_id"}) book_dict = {k: v for k, v in db_book.__dict__.items() if not k.startswith('_')}
book_data = {k: v for k, v in book_dict.items() if k not in {"embedding", "preview_id"}}
book_data["preview_urls"] = { book_data["preview_urls"] = {
"png": f"/static/books/{db_book.preview_id}.png", "png": f"/static/books/{db_book.preview_id}.png",
"jpeg": f"/static/books/{db_book.preview_id}.jpg", "jpeg": f"/static/books/{db_book.preview_id}.jpg",
@@ -160,12 +158,17 @@ def read_books(session: Session = Depends(get_session)):
books_data = [] books_data = []
for book in books: for book in books:
book_data = book.model_dump(exclude={"embedding", "preview_id"}) book = book[0]
book_dict = dict(book)
book_data = {k: v for k, v in book_dict.items() if k not in {"embedding", "preview_id"}}
book_data["preview_urls"] = { book_data["preview_urls"] = {
"png": f"/static/books/{book.preview_id}.png", "png": f"/static/books/{book.preview_id}.png",
"jpeg": f"/static/books/{book.preview_id}.jpg", "jpeg": f"/static/books/{book.preview_id}.jpg",
"webp": f"/static/books/{book.preview_id}.webp", "webp": f"/static/books/{book.preview_id}.webp",
} if book.preview_id else {} } if book.preview_id else {}
books_data.append(book_data) books_data.append(book_data)
return BookList( return BookList(
@@ -188,7 +191,8 @@ def get_book(
book = session.get(Book, book_id) book = session.get(Book, book_id)
if not book: if not book:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found",
) )
authors = session.scalars( authors = session.scalars(
@@ -203,7 +207,8 @@ def get_book(
genre_reads = [GenreRead(**genre.model_dump()) for genre in genres] genre_reads = [GenreRead(**genre.model_dump()) for genre in genres]
book_data = book.model_dump(exclude={"embedding", "preview_id"}) book_dict = {k: v for k, v in book.__dict__.items() if not k.startswith('_')}
book_data = {k: v for k, v in book_dict.items() if k not in {"embedding", "preview_id"}}
book_data["preview_urls"] = { book_data["preview_urls"] = {
"png": f"/static/books/{book.preview_id}.png", "png": f"/static/books/{book.preview_id}.png",
"jpeg": f"/static/books/{book.preview_id}.jpg", "jpeg": f"/static/books/{book.preview_id}.jpg",
@@ -231,7 +236,8 @@ def update_book(
db_book = session.get(Book, book_id) db_book = session.get(Book, book_id)
if not db_book: if not db_book:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found",
) )
if book_update.status is not None: if book_update.status is not None:
@@ -292,7 +298,8 @@ def delete_book(
book = session.get(Book, book_id) book = session.get(Book, book_id)
if not book: if not book:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found",
) )
book_read = BookRead( book_read = BookRead(
id=(book.id or 0), id=(book.id or 0),
@@ -313,10 +320,16 @@ async def upload_book_preview(
session: Session = Depends(get_session) session: Session = Depends(get_session)
): ):
if not (file.content_type or "").startswith("image/"): if not (file.content_type or "").startswith("image/"):
raise HTTPException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, "Image required") raise HTTPException(
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
detail="Image required",
)
if (file.size or 0) > 32 * 1024 * 1024: if (file.size or 0) > 32 * 1024 * 1024:
raise HTTPException(status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, "File larger than 10 MB") raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail="File larger than 10 MB",
)
file_uuid= uuid4() file_uuid= uuid4()
tmp_path = BOOKS_PREVIEW_DIR / f"{file_uuid}.upload" tmp_path = BOOKS_PREVIEW_DIR / f"{file_uuid}.upload"
@@ -327,7 +340,10 @@ async def upload_book_preview(
book = session.get(Book, book_id) book = session.get(Book, book_id)
if not book: if not book:
tmp_path.unlink() tmp_path.unlink()
raise HTTPException(status.HTTP_404_NOT_FOUND, "Book not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found",
)
transcode_image(tmp_path) transcode_image(tmp_path)
tmp_path.unlink() tmp_path.unlink()
+14 -6
View File
@@ -27,11 +27,13 @@ async def challenge(request: Request, ip: str = Depends(get_ip)):
"""Возвращает задачу capjs""" """Возвращает задачу capjs"""
if challenges_by_ip[ip] >= MAX_CHALLENGES_PER_IP: if challenges_by_ip[ip] >= MAX_CHALLENGES_PER_IP:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many challenges" status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Too many challenges",
) )
if len(active_challenges) >= MAX_TOTAL_CHALLENGES: if len(active_challenges) >= MAX_TOTAL_CHALLENGES:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Server busy" status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Server busy",
) )
token = secrets.token_hex(25) token = secrets.token_hex(25)
@@ -60,17 +62,22 @@ async def redeem(request: Request, payload: dict, ip: str = Depends(get_ip)):
if token not in active_challenges: if token not in active_challenges:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Invalid challenge" status_code=status.HTTP_404_NOT_FOUND,
detail="Invalid challenge",
) )
ch = active_challenges.pop(token) ch = active_challenges.pop(token)
challenges_by_ip[ch["ip"]] -= 1 challenges_by_ip[ch["ip"]] -= 1
if now_ms() > ch["expires"]: if now_ms() > ch["expires"]:
raise HTTPException(status_code=status.HTTP_410_GONE, detail="Expired") raise HTTPException(
status_code=status.HTTP_410_GONE,
detail="Expired"
)
if len(solutions) < ch["c"]: if len(solutions) < ch["c"]:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Bad solutions" status_code=status.HTTP_400_BAD_REQUEST,
detail="Bad solutions",
) )
def verify(i: int) -> bool: def verify(i: int) -> bool:
@@ -84,7 +91,8 @@ async def redeem(request: Request, payload: dict, ip: str = Depends(get_ip)):
) )
if not all(results): if not all(results):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid solution" status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid solution",
) )
r_token = ch["redeem_token"] r_token = ch["redeem_token"]
+6 -3
View File
@@ -66,7 +66,8 @@ def get_genre(
genre = session.get(Genre, genre_id) genre = session.get(Genre, genre_id)
if not genre: if not genre:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Genre not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Genre not found",
) )
books = session.exec( books = session.exec(
@@ -97,7 +98,8 @@ def update_genre(
db_genre = session.get(Genre, genre_id) db_genre = session.get(Genre, genre_id)
if not db_genre: if not db_genre:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Genre not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Genre not found",
) )
update_data = genre.model_dump(exclude_unset=True) update_data = genre.model_dump(exclude_unset=True)
@@ -124,7 +126,8 @@ def delete_genre(
genre = session.get(Genre, genre_id) genre = session.get(Genre, genre_id)
if not genre: if not genre:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Genre not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Genre not found",
) )
genre_read = GenreRead(**genre.model_dump()) genre_read = GenreRead(**genre.model_dump())
+30 -15
View File
@@ -41,7 +41,8 @@ def create_loan(
book = session.get(Book, loan.book_id) book = session.get(Book, loan.book_id)
if not book: if not book:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found",
) )
if book.status != BookStatus.ACTIVE: if book.status != BookStatus.ACTIVE:
@@ -53,7 +54,8 @@ def create_loan(
target_user = session.get(User, loan.user_id) target_user = session.get(User, loan.user_id)
if not target_user: if not target_user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found" status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
) )
db_loan = BookUserLink( db_loan = BookUserLink(
@@ -253,14 +255,16 @@ def get_loan(
if not loan: if not loan:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Loan not found",
) )
is_staff = is_user_staff(current_user) is_staff = is_user_staff(current_user)
if not is_staff and loan.user_id != current_user.id: if not is_staff and loan.user_id != current_user.id:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to this loan" status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to this loan",
) )
return LoanRead(**loan.model_dump()) return LoanRead(**loan.model_dump())
@@ -282,7 +286,8 @@ def update_loan(
db_loan = session.get(BookUserLink, loan_id) db_loan = session.get(BookUserLink, loan_id)
if not db_loan: if not db_loan:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Loan not found",
) )
is_staff = is_user_staff(current_user) is_staff = is_user_staff(current_user)
@@ -296,7 +301,8 @@ def update_loan(
book = session.get(Book, db_loan.book_id) book = session.get(Book, db_loan.book_id)
if not book: if not book:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found",
) )
if loan_update.user_id is not None: if loan_update.user_id is not None:
@@ -308,7 +314,8 @@ def update_loan(
new_user = session.get(User, loan_update.user_id) new_user = session.get(User, loan_update.user_id)
if not new_user: if not new_user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found" status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
) )
db_loan.user_id = loan_update.user_id db_loan.user_id = loan_update.user_id
@@ -347,18 +354,21 @@ def confirm_loan(
loan = session.get(BookUserLink, loan_id) loan = session.get(BookUserLink, loan_id)
if not loan: if not loan:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Loan not found",
) )
if loan.returned_at: if loan.returned_at:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Loan is already returned" status_code=status.HTTP_400_BAD_REQUEST,
detail="Loan is already returned",
) )
book = session.get(Book, loan.book_id) book = session.get(Book, loan.book_id)
if not book: if not book:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found",
) )
if book.status not in [BookStatus.RESERVED, BookStatus.ACTIVE]: if book.status not in [BookStatus.RESERVED, BookStatus.ACTIVE]:
@@ -392,12 +402,14 @@ def return_loan(
loan = session.get(BookUserLink, loan_id) loan = session.get(BookUserLink, loan_id)
if not loan: if not loan:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Loan not found",
) )
if loan.returned_at: if loan.returned_at:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Loan is already returned" status_code=status.HTTP_400_BAD_REQUEST,
detail="Loan is already returned",
) )
loan.returned_at = datetime.now(timezone.utc) loan.returned_at = datetime.now(timezone.utc)
@@ -429,7 +441,8 @@ def delete_loan(
loan = session.get(BookUserLink, loan_id) loan = session.get(BookUserLink, loan_id)
if not loan: if not loan:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Loan not found",
) )
is_staff = is_user_staff(current_user) is_staff = is_user_staff(current_user)
@@ -499,7 +512,8 @@ def issue_book_directly(
book = session.get(Book, loan.book_id) book = session.get(Book, loan.book_id)
if not book: if not book:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found",
) )
if book.status != BookStatus.ACTIVE: if book.status != BookStatus.ACTIVE:
@@ -511,7 +525,8 @@ def issue_book_directly(
target_user = session.get(User, loan.user_id) target_user = session.get(User, loan.user_id)
if not target_user: if not target_user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found" status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
) )
db_loan = BookUserLink( db_loan = BookUserLink(
+49 -10
View File
@@ -56,8 +56,14 @@ async def create_genre(request: Request, app=Depends(lambda: get_app())):
@router.get("/genre/{genre_id}/edit", include_in_schema=False) @router.get("/genre/{genre_id}/edit", include_in_schema=False)
async def edit_genre(request: Request, genre_id: int, app=Depends(lambda: get_app())): async def edit_genre(request: Request, genre_id: str, app=Depends(lambda: get_app())):
"""Рендерит страницу редактирования жанра""" """Рендерит страницу редактирования жанра"""
try:
assert int(genre_id) > 0
except:
return await unknown(request, app)
return templates.TemplateResponse(request, "edit_genre.html", get_info(app) | {"request": request, "title": "LiB - Редактировать жанр", "id": genre_id}) return templates.TemplateResponse(request, "edit_genre.html", get_info(app) | {"request": request, "title": "LiB - Редактировать жанр", "id": genre_id})
@@ -74,15 +80,32 @@ async def create_author(request: Request, app=Depends(lambda: get_app())):
@router.get("/author/{author_id}/edit", include_in_schema=False) @router.get("/author/{author_id}/edit", include_in_schema=False)
async def edit_author(request: Request, author_id: int, app=Depends(lambda: get_app())): async def edit_author(request: Request, author_id: str, app=Depends(lambda: get_app()), session=Depends(get_session)):
"""Рендерит страницу редактирования автора""" """Рендерит страницу редактирования автора"""
return templates.TemplateResponse(request, "edit_author.html", get_info(app) | {"request": request, "title": "LiB - Редактировать автора", "id": author_id})
try:
author = session.get(Author, int(author_id))
assert author is not None
except:
return await unknown(request, app)
return templates.TemplateResponse(request, "edit_author.html", get_info(app) | {"request": request, "title": f"LiB - Редактировать автора \"{author.name}\"", "id": author_id})
@router.get("/author/{author_id}", include_in_schema=False) @router.get("/author/{author_id}", include_in_schema=False)
async def author(request: Request, author_id: int, app=Depends(lambda: get_app())): async def author(request: Request, author_id: str, app=Depends(lambda: get_app()), session=Depends(get_session)):
"""Рендерит страницу просмотра автора""" """Рендерит страницу просмотра автора"""
return templates.TemplateResponse(request, "author.html", get_info(app) | {"request": request, "title": "LiB - Автор", "id": author_id})
if author_id == "":
return RedirectResponse("/authors")
try:
author = session.get(Author, int(author_id))
assert author is not None
except:
return await unknown(request, app)
return templates.TemplateResponse(request, "author.html", get_info(app) | {"request": request, "title": f"LiB - Автор \"{author.name}\"", "id": author_id})
@router.get("/books", include_in_schema=False) @router.get("/books", include_in_schema=False)
@@ -98,16 +121,32 @@ async def create_book(request: Request, app=Depends(lambda: get_app())):
@router.get("/book/{book_id}/edit", include_in_schema=False) @router.get("/book/{book_id}/edit", include_in_schema=False)
async def edit_book(request: Request, book_id: int, app=Depends(lambda: get_app())): async def edit_book(request: Request, book_id: str, app=Depends(lambda: get_app()), session=Depends(get_session)):
"""Рендерит страницу редактирования книги""" """Рендерит страницу редактирования книги"""
return templates.TemplateResponse(request, "edit_book.html", get_info(app) | {"request": request, "title": "LiB - Редактировать книгу", "id": book_id})
try:
book = session.get(Book, int(book_id))
assert book is not None
except:
return await unknown(request, app)
return templates.TemplateResponse(request, "edit_book.html", get_info(app) | {"request": request, "title": f"LiB - Редактировать книгу \"{book.title}\"", "id": book_id})
@router.get("/book/{book_id}", include_in_schema=False) @router.get("/book/{book_id}", include_in_schema=False)
async def book(request: Request, book_id: int, app=Depends(lambda: get_app()), session=Depends(get_session)): async def book(request: Request, book_id: str, app=Depends(lambda: get_app()), session=Depends(get_session)):
"""Рендерит страницу просмотра книги""" """Рендерит страницу просмотра книги"""
book = session.get(Book, book_id)
return templates.TemplateResponse(request, "book.html", get_info(app) | {"request": request, "title": "LiB - Книга", "id": book_id, "img": book.preview_id}) if book_id == "":
return RedirectResponse("/books")
try:
book = session.get(Book, int(book_id))
assert book is not None
except:
return await unknown(request, app)
return templates.TemplateResponse(request, "book.html", get_info(app) | {"request": request, "title": f"LiB - Книга \"{book.title}\"", "id": book_id, "img": book.preview_id})
@router.get("/auth", include_in_schema=False) @router.get("/auth", include_in_schema=False)
+8 -3
View File
@@ -19,7 +19,8 @@ def check_entity_exists(session, model, entity_id, entity_name):
entity = session.get(model, entity_id) entity = session.get(model, entity_id)
if not entity: if not entity:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"{entity_name} not found" status_code=status.HTTP_404_NOT_FOUND,
detail=f"{entity_name} not found",
) )
return entity return entity
@@ -33,7 +34,10 @@ def add_relationship(session, link_model, id1, field1, id2, field2, detail):
).first() ).first()
if existing_link: if existing_link:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail) raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=detail,
)
link = link_model(**{field1: id1, field2: id2}) link = link_model(**{field1: id1, field2: id2})
session.add(link) session.add(link)
@@ -52,7 +56,8 @@ def remove_relationship(session, link_model, id1, field1, id2, field2):
if not link: if not link:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Relationship not found" status_code=status.HTTP_404_NOT_FOUND,
detail="Relationship not found",
) )
session.delete(link) session.delete(link)
+2 -1
View File
@@ -72,6 +72,7 @@ async def require_captcha(request: Request):
token = request.cookies.get("capjs_token") token = request.cookies.get("capjs_token")
if not token or token not in redeem_tokens or redeem_tokens[token] < now_ms(): if not token or token not in redeem_tokens or redeem_tokens[token] < now_ms():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail={"error": "captcha_required"} status_code=status.HTTP_403_FORBIDDEN,
detail={"error": "captcha_required"},
) )
del redeem_tokens[token] del redeem_tokens[token]
-1
View File
@@ -9,7 +9,6 @@ $(document).ready(() => {
Api.get(`/api/authors/${authorId}`) Api.get(`/api/authors/${authorId}`)
.then((author) => { .then((author) => {
document.title = `LiB - ${author.name}`;
renderAuthor(author); renderAuthor(author);
renderBooks(author.books); renderBooks(author.books);
if (window.canManage()) { if (window.canManage()) {
-1
View File
@@ -366,7 +366,6 @@ $(document).ready(() => {
Api.get(`/api/books/${bookId}`) Api.get(`/api/books/${bookId}`)
.then((book) => { .then((book) => {
currentBook = book; currentBook = book;
document.title = `LiB - ${book.title}`;
renderBook(book); renderBook(book);
if (window.canManage()) { if (window.canManage()) {
$("#edit-book-btn") $("#edit-book-btn")
@@ -32,7 +32,6 @@ $(document).ready(() => {
originalAuthor = author; originalAuthor = author;
authorBooks = booksData.books || booksData || []; authorBooks = booksData.books || booksData || [];
document.title = `Редактирование: ${author.name} | LiB`;
populateForm(author); populateForm(author);
renderAuthorBooks(authorBooks); renderAuthorBooks(authorBooks);
-1
View File
@@ -49,7 +49,6 @@ $(document).ready(() => {
currentGenres.set(g.id, g.name), currentGenres.set(g.id, g.name),
); );
document.title = `Редактирование: ${book.title} | LiB`;
populateForm(book); populateForm(book);
initAuthorsDropdown(); initAuthorsDropdown();
initGenresDropdown(); initGenresDropdown();
@@ -35,7 +35,6 @@ $(document).ready(() => {
originalGenre = genre; originalGenre = genre;
genreBooks = booksData.books || booksData || []; genreBooks = booksData.books || booksData || [];
document.title = `Редактирование: ${genre.name} | LiB`;
populateForm(genre); populateForm(genre);
renderGenreBooks(genreBooks); renderGenreBooks(genreBooks);
+1 -1
View File
@@ -17,7 +17,7 @@ $(document).ready(() => {
Api.get("/api/auth/recovery-codes/status").catch(() => null), Api.get("/api/auth/recovery-codes/status").catch(() => null),
]) ])
.then(async ([user, rolesData, recoveryStatus]) => { .then(async ([user, rolesData, recoveryStatus]) => {
document.title = `LiB - ${user.full_name || user.username}`; document.title = `LiB - Профиль ${user.full_name || user.username}`;
currentUsername = user.username; currentUsername = user.username;
await renderProfileHeader(user); await renderProfileHeader(user);
+6 -1
View File
@@ -2,10 +2,15 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title id="pageTitle">Loading...</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:title" content="LiB - API Документация" />
<meta property="og:type" content="website" />
<meta property="og:description" content="Ваша персональная библиотека книг" />
<meta property="og:url" content="{{ request.url.scheme }}://{{ domain }}/" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsPlumb/2.15.6/js/jsplumb.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jsPlumb/2.15.6/js/jsplumb.min.js"></script>
<title id="pageTitle">Loading...</title>
<style> <style>
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
-101
View File
@@ -1,101 +0,0 @@
#!/bin/sh
set -e
echo "=== Настройка репликации ==="
echo "Этот узел: NODE_ID=${NODE_ID}"
echo "Удаленный хост: ${REMOTE_HOST}"
echo "Ждем локальную базу..."
sleep 10
until PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c '\q' 2>/dev/null; do
echo "Локальная база не готова, ждем..."
sleep 2
done
echo "Локальная база готова"
echo "Настройка генераторов ID (NODE_ID=${NODE_ID})..."
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <<EOF
DO \$\$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE is_identity = 'YES'
AND table_schema = 'public'
LOOP
EXECUTE format(
'ALTER TABLE %I.%I ALTER COLUMN %I SET GENERATED BY DEFAULT AS IDENTITY (START WITH %s INCREMENT BY 2)',
r.table_schema, r.table_name, r.column_name, ${NODE_ID}
);
RAISE NOTICE 'Настроен ID для %.%', r.table_name, r.column_name;
END LOOP;
END \$\$;
EOF
echo "Проверяем/создаем публикацию..."
PUB_EXISTS=$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -tAc "SELECT COUNT(*) FROM pg_publication WHERE pubname = 'all_tables_pub';")
if [ "$PUB_EXISTS" -gt 0 ]; then
echo "Публикация уже существует"
else
echo "Создаем публикацию..."
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <<EOF
CREATE PUBLICATION all_tables_pub FOR ALL TABLES;
EOF
echo "Публикация создана!"
fi
echo "Ждем удаленный хост ${REMOTE_HOST}:${REMOTE_PORT}..."
TIMEOUT=300
ELAPSED=0
while ! PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${REMOTE_HOST}" -p ${REMOTE_PORT} -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c '\q' 2>/dev/null; do
sleep 5
ELAPSED=$((ELAPSED + 5))
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "Таймаут ожидания удаленного хоста. Подписка НЕ настроена."
echo "Публикация создана - удаленный хост сможет подписаться на нас."
echo "Для создания подписки запустите позже:"
echo "docker compose restart replication-setup"
exit 0
fi
echo "Удаленный хост недоступен, ждем... (${ELAPSED}s/${TIMEOUT}s)"
done
echo "Удаленный хост доступен"
REMOTE_PUB=$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${REMOTE_HOST}" -p ${REMOTE_PORT} -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -tAc "SELECT COUNT(*) FROM pg_publication WHERE pubname = 'all_tables_pub';" 2>/dev/null || echo "0")
if [ "$REMOTE_PUB" -eq 0 ]; then
echo "ВНИМАНИЕ: На удалённом хосте нет публикации 'all_tables_pub'!"
echo "Подписка не будет создана. Сначала запустите скрипт на удалённом хосте."
exit 0
fi
EXISTING=$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -tAc "SELECT COUNT(*) FROM pg_subscription WHERE subname = 'sub_from_remote';")
if [ "$EXISTING" -gt 0 ]; then
echo "Подписка уже существует, пропускаем создание"
else
echo "Создаем подписку на удаленный хост..."
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <<EOF
CREATE SUBSCRIPTION sub_from_remote
CONNECTION 'host=${REMOTE_HOST} port=${REMOTE_PORT} user=${POSTGRES_USER} password=${POSTGRES_PASSWORD} dbname=${POSTGRES_DB}'
PUBLICATION all_tables_pub
WITH (
origin = none
);
EOF
echo "Подписка создана!"
fi
echo ""
echo "=== Репликация настроена! ==="
echo "Публикация: all_tables_pub (другие могут подписаться на нас)"
echo "Подписка: sub_from_remote (мы получаем данные от ${REMOTE_HOST})"