Files
remnawave-bedolaga-telegram…/app/webapi/routes/pages.py
2025-12-10 02:09:46 +03:00

513 lines
16 KiB
Python

from __future__ import annotations
from typing import Any, List, Optional
from fastapi import (
APIRouter,
Body,
Depends,
HTTPException,
Query,
Response,
Security,
status,
)
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.crud.faq import get_faq_page_by_id
from app.database.crud.rules import (
clear_all_rules,
create_or_update_rules,
get_all_rules_versions,
get_rules_by_language,
restore_rules_version,
)
from app.database.models import ServiceRule
from app.services.faq_service import FaqService
from app.services.privacy_policy_service import PrivacyPolicyService
from app.services.public_offer_service import PublicOfferService
from ..dependencies import get_db_session, require_api_token
from ..schemas.pages import (
FaqPageCreateRequest,
FaqPageListResponse,
FaqPageResponse,
FaqPageUpdateRequest,
FaqReorderRequest,
FaqStatusResponse,
FaqStatusUpdateRequest,
RichTextPageResponse,
RichTextPageUpdateRequest,
ServiceRulesHistoryResponse,
ServiceRulesResponse,
ServiceRulesUpdateRequest,
)
router = APIRouter()
def _serialize_rich_page(
*,
requested_language: str,
content: str,
language: str,
is_enabled: Optional[bool],
created_at,
updated_at,
splitter,
) -> RichTextPageResponse:
pages = splitter(content or "")
return RichTextPageResponse(
requested_language=requested_language,
language=language,
is_enabled=is_enabled,
content=content or "",
content_pages=pages,
created_at=created_at,
updated_at=updated_at,
)
def _serialize_faq_page(page) -> FaqPageResponse:
return FaqPageResponse(
id=page.id,
language=page.language,
title=page.title,
content=page.content,
content_pages=FaqService.split_content_into_pages(page.content),
display_order=page.display_order,
is_active=page.is_active,
created_at=page.created_at,
updated_at=page.updated_at,
)
def _serialize_rules(rule: ServiceRule) -> ServiceRulesResponse:
return ServiceRulesResponse(
id=rule.id,
title=rule.title,
content=rule.content,
language=rule.language,
is_active=rule.is_active,
created_at=rule.created_at,
updated_at=rule.updated_at,
)
@router.get("/public-offer", response_model=RichTextPageResponse)
async def get_public_offer(
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
language: str = Query("ru", min_length=2, max_length=10),
fallback: bool = Query(True, description="Использовать запасной язык, если контента нет"),
include_disabled: bool = Query(
True,
description="Возвращать контент даже если страница выключена",
),
) -> RichTextPageResponse:
requested_lang = PublicOfferService.normalize_language(language)
offer = await PublicOfferService.get_offer(db, requested_lang, fallback=fallback)
if not offer:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Public offer not found")
if not include_disabled and not offer.is_enabled:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Public offer disabled")
return _serialize_rich_page(
requested_language=requested_lang,
language=offer.language,
is_enabled=offer.is_enabled,
content=offer.content or "",
created_at=offer.created_at,
updated_at=offer.updated_at,
splitter=PublicOfferService.split_content_into_pages,
)
@router.put("/public-offer", response_model=RichTextPageResponse)
async def update_public_offer(
payload: RichTextPageUpdateRequest,
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> RichTextPageResponse:
lang = PublicOfferService.normalize_language(payload.language)
offer = await PublicOfferService.save_offer(db, lang, payload.content)
if payload.is_enabled is not None:
offer = await PublicOfferService.set_enabled(db, lang, payload.is_enabled)
refreshed = await PublicOfferService.get_offer(db, lang, fallback=False)
offer = refreshed or offer
return _serialize_rich_page(
requested_language=lang,
language=offer.language,
is_enabled=offer.is_enabled,
content=offer.content or "",
created_at=offer.created_at,
updated_at=offer.updated_at,
splitter=PublicOfferService.split_content_into_pages,
)
@router.get("/privacy-policy", response_model=RichTextPageResponse)
async def get_privacy_policy(
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
language: str = Query("ru", min_length=2, max_length=10),
fallback: bool = Query(True),
include_disabled: bool = Query(True),
) -> RichTextPageResponse:
requested_lang = PrivacyPolicyService.normalize_language(language)
policy = await PrivacyPolicyService.get_policy(db, requested_lang, fallback=fallback)
if not policy:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Privacy policy not found")
if not include_disabled and not policy.is_enabled:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Privacy policy disabled")
return _serialize_rich_page(
requested_language=requested_lang,
language=policy.language,
is_enabled=policy.is_enabled,
content=policy.content or "",
created_at=policy.created_at,
updated_at=policy.updated_at,
splitter=PrivacyPolicyService.split_content_into_pages,
)
@router.put("/privacy-policy", response_model=RichTextPageResponse)
async def update_privacy_policy(
payload: RichTextPageUpdateRequest,
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> RichTextPageResponse:
lang = PrivacyPolicyService.normalize_language(payload.language)
policy = await PrivacyPolicyService.save_policy(db, lang, payload.content)
if payload.is_enabled is not None:
policy = await PrivacyPolicyService.set_enabled(db, lang, payload.is_enabled)
refreshed = await PrivacyPolicyService.get_policy(db, lang, fallback=False)
policy = refreshed or policy
return _serialize_rich_page(
requested_language=lang,
language=policy.language,
is_enabled=policy.is_enabled,
content=policy.content or "",
created_at=policy.created_at,
updated_at=policy.updated_at,
splitter=PrivacyPolicyService.split_content_into_pages,
)
@router.get("/faq", response_model=FaqPageListResponse)
async def list_faq_pages(
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
language: str = Query("ru", min_length=2, max_length=10),
include_inactive: bool = Query(True),
fallback: bool = Query(True),
) -> FaqPageListResponse:
requested_lang = FaqService.normalize_language(language)
pages = await FaqService.get_pages(
db,
requested_lang,
include_inactive=include_inactive,
fallback=fallback,
)
resolved_language = requested_lang
if pages:
resolved_language = pages[0].language
setting = await FaqService.get_setting(db, requested_lang, fallback=fallback)
is_enabled = bool(setting.is_enabled) if setting else False
if setting:
resolved_language = setting.language
serialized = [_serialize_faq_page(page) for page in pages]
return FaqPageListResponse(
requested_language=requested_lang,
language=resolved_language,
is_enabled=is_enabled,
total=len(serialized),
items=serialized,
)
@router.get("/faq/status", response_model=FaqStatusResponse)
async def get_faq_status(
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
language: str = Query("ru", min_length=2, max_length=10),
fallback: bool = Query(True),
) -> FaqStatusResponse:
requested_lang = FaqService.normalize_language(language)
setting = await FaqService.get_setting(db, requested_lang, fallback=fallback)
if not setting:
return FaqStatusResponse(
requested_language=requested_lang,
language=requested_lang,
is_enabled=False,
)
return FaqStatusResponse(
requested_language=requested_lang,
language=setting.language,
is_enabled=bool(setting.is_enabled),
)
@router.put("/faq/status", response_model=FaqStatusResponse)
async def update_faq_status(
payload: Optional[FaqStatusUpdateRequest] = Body(None),
language: str = Query("ru", min_length=2, max_length=10),
is_enabled: Optional[bool] = Query(None),
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> FaqStatusResponse:
resolved_language = FaqService.normalize_language(
payload.language if payload and payload.language else language
)
enabled_status = payload.is_enabled if payload else is_enabled
if enabled_status is None:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Parameter 'is_enabled' is required"
)
setting = await FaqService.set_enabled(db, resolved_language, enabled_status)
return FaqStatusResponse(
requested_language=resolved_language,
language=setting.language,
is_enabled=bool(setting.is_enabled),
)
@router.post("/faq", response_model=FaqPageResponse, status_code=status.HTTP_201_CREATED)
async def create_faq_page(
payload: FaqPageCreateRequest,
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> FaqPageResponse:
lang = FaqService.normalize_language(payload.language)
is_active = True if payload.is_active is None else payload.is_active
page = await FaqService.create_page(
db,
language=lang,
title=payload.title,
content=payload.content,
display_order=payload.display_order,
is_active=is_active,
)
return _serialize_faq_page(page)
@router.get("/faq/{page_id}", response_model=FaqPageResponse)
async def get_faq_page(
page_id: int,
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
language: str = Query("ru", min_length=2, max_length=10),
include_inactive: bool = Query(True),
) -> FaqPageResponse:
requested_lang = FaqService.normalize_language(language)
page = await FaqService.get_page(
db,
page_id,
requested_lang,
include_inactive=include_inactive,
fallback=True,
)
if not page:
raise HTTPException(status.HTTP_404_NOT_FOUND, "FAQ page not found")
return _serialize_faq_page(page)
@router.put("/faq/{page_id}", response_model=FaqPageResponse)
async def update_faq_page(
page_id: int,
payload: FaqPageUpdateRequest,
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> FaqPageResponse:
page = await get_faq_page_by_id(db, page_id)
if not page:
raise HTTPException(status.HTTP_404_NOT_FOUND, "FAQ page not found")
updated = await FaqService.update_page(
db,
page,
title=payload.title,
content=payload.content,
display_order=payload.display_order,
is_active=payload.is_active,
)
return _serialize_faq_page(updated)
@router.delete("/faq/{page_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_faq_page(
page_id: int,
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> Response:
page = await get_faq_page_by_id(db, page_id)
if not page:
raise HTTPException(status.HTTP_404_NOT_FOUND, "FAQ page not found")
await FaqService.delete_page(db, page_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.post("/faq/reorder", response_model=FaqPageListResponse)
async def reorder_faq_pages(
payload: FaqReorderRequest,
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> FaqPageListResponse:
lang = FaqService.normalize_language(payload.language)
ordered_payload = sorted(payload.items, key=lambda item: item.display_order)
existing_pages = await FaqService.get_pages(
db,
lang,
include_inactive=True,
fallback=False,
)
pages_by_id = {page.id: page for page in existing_pages}
pages: List[Any] = []
for item in ordered_payload:
page = pages_by_id.get(item.id)
if not page:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
f"FAQ page {item.id} not found for language {lang}",
)
pages.append(page)
ordered_ids = {item.id for item in ordered_payload}
remaining = [page for page in existing_pages if page.id not in ordered_ids]
pages.extend(sorted(remaining, key=lambda page: (page.display_order, page.id)))
await FaqService.reorder_pages(db, lang, pages)
updated_pages = await FaqService.get_pages(
db,
lang,
include_inactive=True,
fallback=False,
)
setting = await FaqService.get_setting(db, lang, fallback=False)
serialized = [_serialize_faq_page(page) for page in updated_pages]
return FaqPageListResponse(
requested_language=lang,
language=lang,
is_enabled=bool(setting.is_enabled) if setting else False,
total=len(serialized),
items=serialized,
)
@router.get("/service-rules", response_model=ServiceRulesResponse)
async def get_service_rules(
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
language: str = Query("ru", min_length=2, max_length=10),
fallback: bool = Query(True),
) -> ServiceRulesResponse:
requested_lang = language.split("-")[0].lower()
rules = await get_rules_by_language(db, requested_lang)
if not rules and fallback:
default_lang = (settings.DEFAULT_LANGUAGE or "ru").split("-")[0].lower()
if default_lang != requested_lang:
rules = await get_rules_by_language(db, default_lang)
if not rules:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Service rules not found")
return _serialize_rules(rules)
@router.put("/service-rules", response_model=ServiceRulesResponse)
async def update_service_rules(
payload: ServiceRulesUpdateRequest,
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> ServiceRulesResponse:
lang = payload.language.split("-")[0].lower()
title = payload.title or "Правила сервиса"
rules = await create_or_update_rules(
db,
content=payload.content,
language=lang,
title=title,
)
return _serialize_rules(rules)
@router.delete("/service-rules", status_code=status.HTTP_204_NO_CONTENT)
async def clear_service_rules(
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
language: str = Query("ru", min_length=2, max_length=10),
) -> Response:
lang = language.split("-")[0].lower()
await clear_all_rules(db, lang)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/service-rules/history", response_model=ServiceRulesHistoryResponse)
async def get_service_rules_history(
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
language: str = Query("ru", min_length=2, max_length=10),
limit: int = Query(10, ge=1, le=100),
) -> ServiceRulesHistoryResponse:
lang = language.split("-")[0].lower()
history = await get_all_rules_versions(db, lang, limit=limit)
items = [_serialize_rules(item) for item in history]
return ServiceRulesHistoryResponse(
language=lang,
total=len(items),
items=items,
)
@router.post(
"/service-rules/history/{rule_id}/restore",
response_model=ServiceRulesResponse,
status_code=status.HTTP_201_CREATED,
)
async def restore_service_rules_version(
rule_id: int,
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
language: str = Query("ru", min_length=2, max_length=10),
) -> ServiceRulesResponse:
lang = language.split("-")[0].lower()
restored = await restore_rules_version(db, rule_id, language=lang)
if not restored:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Rules version not found")
return _serialize_rules(restored)