mirror of
https://github.com/wowlikon/LiB.git
synced 2026-03-21 23:53:38 +00:00
исправление обработки 404
This commit is contained in:
+1
-18
@@ -11,7 +11,7 @@ services:
|
||||
- ./data/db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- proxy
|
||||
ports: # !сменить внешний порт перед использованием!
|
||||
ports: # !только локальный тест!
|
||||
- 5432:5432
|
||||
env_file:
|
||||
- ./.env
|
||||
@@ -31,23 +31,6 @@ services:
|
||||
timeout: 5s
|
||||
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:
|
||||
image: ollama/ollama:latest
|
||||
container_name: llm
|
||||
|
||||
@@ -4,9 +4,6 @@ POSTGRES_PORT=5432
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DB=lib
|
||||
REMOTE_HOST=
|
||||
REMOTE_PORT=
|
||||
NODE_ID=
|
||||
|
||||
# Ollama
|
||||
OLLAMA_URL="http://llm:11434"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
CREATE PUBLICATION all_tables_pub FOR ALL TABLES;
|
||||
|
||||
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
|
||||
SELECT pg_reload_conf();
|
||||
@@ -73,16 +73,21 @@ app = get_app(lifespan)
|
||||
|
||||
@app.exception_handler(status.HTTP_404_NOT_FOUND)
|
||||
async def custom_not_found_handler(request: Request, exc: HTTPException):
|
||||
if exc.detail == "Not Found":
|
||||
path = request.url.path
|
||||
|
||||
if path.startswith("/api"):
|
||||
if path.startswith("/api/"):
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
content={"detail": "API endpoint not found", "path": path},
|
||||
)
|
||||
|
||||
return await unknown(request, app)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"detail": exc.detail},
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def catch_exceptions_middleware(request: Request, call_next):
|
||||
|
||||
@@ -287,7 +287,8 @@ def enable_2fa(
|
||||
|
||||
if not current_user.totp_secret:
|
||||
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):
|
||||
@@ -299,7 +300,8 @@ def enable_2fa(
|
||||
decrypted = cipher.decrypt(base64.b64decode(current_user.totp_secret.encode()))
|
||||
if secret != decrypted.decode():
|
||||
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
|
||||
|
||||
@@ -67,7 +67,8 @@ def get_author(
|
||||
author = session.get(Author, author_id)
|
||||
if not author:
|
||||
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(
|
||||
@@ -98,7 +99,8 @@ def update_author(
|
||||
db_author = session.get(Author, author_id)
|
||||
if not db_author:
|
||||
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)
|
||||
@@ -125,7 +127,8 @@ def delete_author(
|
||||
author = session.get(Author, author_id)
|
||||
if not author:
|
||||
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())
|
||||
|
||||
@@ -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 sqlalchemy.orm import selectinload, defer
|
||||
|
||||
from sqlalchemy import text, case, distinct
|
||||
from uuid import uuid4
|
||||
import shutil
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status, UploadFile, File
|
||||
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 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.models.enums import BookStatus
|
||||
from library_service.models.db import (
|
||||
@@ -139,7 +136,8 @@ def create_book(
|
||||
session.commit()
|
||||
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"] = {
|
||||
"png": f"/static/books/{db_book.preview_id}.png",
|
||||
"jpeg": f"/static/books/{db_book.preview_id}.jpg",
|
||||
@@ -160,12 +158,17 @@ def read_books(session: Session = Depends(get_session)):
|
||||
|
||||
books_data = []
|
||||
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"] = {
|
||||
"png": f"/static/books/{book.preview_id}.png",
|
||||
"jpeg": f"/static/books/{book.preview_id}.jpg",
|
||||
"webp": f"/static/books/{book.preview_id}.webp",
|
||||
} if book.preview_id else {}
|
||||
|
||||
books_data.append(book_data)
|
||||
|
||||
return BookList(
|
||||
@@ -188,7 +191,8 @@ def get_book(
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
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(
|
||||
@@ -203,7 +207,8 @@ def get_book(
|
||||
|
||||
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"] = {
|
||||
"png": f"/static/books/{book.preview_id}.png",
|
||||
"jpeg": f"/static/books/{book.preview_id}.jpg",
|
||||
@@ -231,7 +236,8 @@ def update_book(
|
||||
db_book = session.get(Book, book_id)
|
||||
if not db_book:
|
||||
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:
|
||||
@@ -292,7 +298,8 @@ def delete_book(
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
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(
|
||||
id=(book.id or 0),
|
||||
@@ -313,10 +320,16 @@ async def upload_book_preview(
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
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:
|
||||
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()
|
||||
tmp_path = BOOKS_PREVIEW_DIR / f"{file_uuid}.upload"
|
||||
@@ -327,7 +340,10 @@ async def upload_book_preview(
|
||||
book = session.get(Book, book_id)
|
||||
if not book:
|
||||
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)
|
||||
tmp_path.unlink()
|
||||
|
||||
@@ -27,11 +27,13 @@ async def challenge(request: Request, ip: str = Depends(get_ip)):
|
||||
"""Возвращает задачу capjs"""
|
||||
if challenges_by_ip[ip] >= MAX_CHALLENGES_PER_IP:
|
||||
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:
|
||||
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)
|
||||
@@ -60,17 +62,22 @@ async def redeem(request: Request, payload: dict, ip: str = Depends(get_ip)):
|
||||
|
||||
if token not in active_challenges:
|
||||
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)
|
||||
challenges_by_ip[ch["ip"]] -= 1
|
||||
|
||||
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"]:
|
||||
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:
|
||||
@@ -84,7 +91,8 @@ async def redeem(request: Request, payload: dict, ip: str = Depends(get_ip)):
|
||||
)
|
||||
if not all(results):
|
||||
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"]
|
||||
|
||||
@@ -66,7 +66,8 @@ def get_genre(
|
||||
genre = session.get(Genre, genre_id)
|
||||
if not genre:
|
||||
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(
|
||||
@@ -97,7 +98,8 @@ def update_genre(
|
||||
db_genre = session.get(Genre, genre_id)
|
||||
if not db_genre:
|
||||
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)
|
||||
@@ -124,7 +126,8 @@ def delete_genre(
|
||||
genre = session.get(Genre, genre_id)
|
||||
if not genre:
|
||||
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())
|
||||
|
||||
@@ -41,7 +41,8 @@ def create_loan(
|
||||
book = session.get(Book, loan.book_id)
|
||||
if not book:
|
||||
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:
|
||||
@@ -53,7 +54,8 @@ def create_loan(
|
||||
target_user = session.get(User, loan.user_id)
|
||||
if not target_user:
|
||||
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(
|
||||
@@ -253,14 +255,16 @@ def get_loan(
|
||||
|
||||
if not loan:
|
||||
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)
|
||||
|
||||
if not is_staff and loan.user_id != current_user.id:
|
||||
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())
|
||||
@@ -282,7 +286,8 @@ def update_loan(
|
||||
db_loan = session.get(BookUserLink, loan_id)
|
||||
if not db_loan:
|
||||
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)
|
||||
@@ -296,7 +301,8 @@ def update_loan(
|
||||
book = session.get(Book, db_loan.book_id)
|
||||
if not book:
|
||||
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:
|
||||
@@ -308,7 +314,8 @@ def update_loan(
|
||||
new_user = session.get(User, loan_update.user_id)
|
||||
if not new_user:
|
||||
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
|
||||
|
||||
@@ -347,18 +354,21 @@ def confirm_loan(
|
||||
loan = session.get(BookUserLink, loan_id)
|
||||
if not loan:
|
||||
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:
|
||||
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)
|
||||
if not book:
|
||||
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]:
|
||||
@@ -392,12 +402,14 @@ def return_loan(
|
||||
loan = session.get(BookUserLink, loan_id)
|
||||
if not loan:
|
||||
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:
|
||||
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)
|
||||
@@ -429,7 +441,8 @@ def delete_loan(
|
||||
loan = session.get(BookUserLink, loan_id)
|
||||
if not loan:
|
||||
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)
|
||||
@@ -499,7 +512,8 @@ def issue_book_directly(
|
||||
book = session.get(Book, loan.book_id)
|
||||
if not book:
|
||||
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:
|
||||
@@ -511,7 +525,8 @@ def issue_book_directly(
|
||||
target_user = session.get(User, loan.user_id)
|
||||
if not target_user:
|
||||
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(
|
||||
|
||||
@@ -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)
|
||||
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})
|
||||
|
||||
|
||||
@@ -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)
|
||||
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)
|
||||
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)
|
||||
@@ -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)
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -19,7 +19,8 @@ def check_entity_exists(session, model, entity_id, entity_name):
|
||||
entity = session.get(model, entity_id)
|
||||
if not entity:
|
||||
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
|
||||
|
||||
@@ -33,7 +34,10 @@ def add_relationship(session, link_model, id1, field1, id2, field2, detail):
|
||||
).first()
|
||||
|
||||
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})
|
||||
session.add(link)
|
||||
@@ -52,7 +56,8 @@ def remove_relationship(session, link_model, id1, field1, id2, field2):
|
||||
|
||||
if not link:
|
||||
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)
|
||||
|
||||
@@ -72,6 +72,7 @@ async def require_captcha(request: Request):
|
||||
token = request.cookies.get("capjs_token")
|
||||
if not token or token not in redeem_tokens or redeem_tokens[token] < now_ms():
|
||||
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]
|
||||
|
||||
@@ -9,7 +9,6 @@ $(document).ready(() => {
|
||||
|
||||
Api.get(`/api/authors/${authorId}`)
|
||||
.then((author) => {
|
||||
document.title = `LiB - ${author.name}`;
|
||||
renderAuthor(author);
|
||||
renderBooks(author.books);
|
||||
if (window.canManage()) {
|
||||
|
||||
@@ -366,7 +366,6 @@ $(document).ready(() => {
|
||||
Api.get(`/api/books/${bookId}`)
|
||||
.then((book) => {
|
||||
currentBook = book;
|
||||
document.title = `LiB - ${book.title}`;
|
||||
renderBook(book);
|
||||
if (window.canManage()) {
|
||||
$("#edit-book-btn")
|
||||
|
||||
@@ -32,7 +32,6 @@ $(document).ready(() => {
|
||||
originalAuthor = author;
|
||||
authorBooks = booksData.books || booksData || [];
|
||||
|
||||
document.title = `Редактирование: ${author.name} | LiB`;
|
||||
populateForm(author);
|
||||
renderAuthorBooks(authorBooks);
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ $(document).ready(() => {
|
||||
currentGenres.set(g.id, g.name),
|
||||
);
|
||||
|
||||
document.title = `Редактирование: ${book.title} | LiB`;
|
||||
populateForm(book);
|
||||
initAuthorsDropdown();
|
||||
initGenresDropdown();
|
||||
|
||||
@@ -35,7 +35,6 @@ $(document).ready(() => {
|
||||
originalGenre = genre;
|
||||
genreBooks = booksData.books || booksData || [];
|
||||
|
||||
document.title = `Редактирование: ${genre.name} | LiB`;
|
||||
populateForm(genre);
|
||||
renderGenreBooks(genreBooks);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ $(document).ready(() => {
|
||||
Api.get("/api/auth/recovery-codes/status").catch(() => null),
|
||||
])
|
||||
.then(async ([user, rolesData, recoveryStatus]) => {
|
||||
document.title = `LiB - ${user.full_name || user.username}`;
|
||||
document.title = `LiB - Профиль ${user.full_name || user.username}`;
|
||||
currentUsername = user.username;
|
||||
|
||||
await renderProfileHeader(user);
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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" />
|
||||
<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>
|
||||
<title id="pageTitle">Loading...</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
|
||||
@@ -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})"
|
||||
Reference in New Issue
Block a user