audit of client actions/ protection of admin TG v2/ fix of cash accounting status/ update of dependencies

This commit is contained in:
Vladless
2026-03-18 01:17:44 +03:00
parent 77f927660b
commit 6d341a9f57
30 changed files with 2219 additions and 36 deletions
+142
View File
@@ -0,0 +1,142 @@
import uuid
from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from pydantic import BaseModel
from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession
from api.depends import get_session, verify_identity_admin
from api.v2.schemas import WebPageResponse, WebPageUpdate, WebBlockResponse, WebTheme
from api.v2.schemas.web import WebUploadResponse
from database.models import WebPage, WebBlock, WebTheme as WebThemeModel
UPLOAD_DIR = Path("static/web_uploads")
ALLOWED_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".mp4", ".webm"})
MAX_FILE_SIZE = 100 * 1024 * 1024
router = APIRouter(tags=["Web"])
class WebPagesListResponse(BaseModel):
slugs: list[str]
KNOWN_PAGE_SLUGS = ["landing", "tariffs", "faq", "login", "dashboard"]
@router.get("/api/web/pages", response_model=WebPagesListResponse)
async def list_web_pages(
session: AsyncSession = Depends(get_session),
):
"""Список slug всех страниц сайта (для экспорта/импорта). Включает известные страницы, даже если запись ещё не создана."""
result = await session.execute(select(WebPage.slug).order_by(WebPage.slug))
from_db = {row[0] for row in result.fetchall()}
slugs = sorted(from_db | set(KNOWN_PAGE_SLUGS))
return WebPagesListResponse(slugs=slugs)
async def get_or_create_page(session: AsyncSession, slug: str) -> WebPage:
result = await session.execute(select(WebPage).where(WebPage.slug == slug))
page = result.scalar_one_or_none()
if page is None:
page = WebPage(slug=slug, title=slug)
session.add(page)
await session.flush()
return page
@router.get("/api/web/pages/{slug}", response_model=WebPageResponse)
async def get_web_page(
slug: str,
session: AsyncSession = Depends(get_session),
):
await get_or_create_page(session, slug)
blocks_result = await session.execute(
select(WebBlock).where(WebBlock.page_slug == slug).order_by(WebBlock.order, WebBlock.id)
)
blocks = [WebBlockResponse.model_validate(b) for b in blocks_result.scalars().all()]
theme_result = await session.execute(select(WebThemeModel).where(WebThemeModel.page_slug == slug))
theme_row = theme_result.scalar_one_or_none()
theme = WebTheme(tokens=theme_row.tokens) if theme_row else None
return WebPageResponse(slug=slug, blocks=blocks, theme=theme)
@router.put("/api/web/pages/{slug}", response_model=WebPageResponse)
async def update_web_page(
slug: str,
body: WebPageUpdate,
session: AsyncSession = Depends(get_session),
identity=Depends(verify_identity_admin),
):
await get_or_create_page(session, slug)
await session.execute(delete(WebBlock).where(WebBlock.page_slug == slug))
for block in body.blocks:
session.add(
WebBlock(
page_slug=slug,
order=block.order,
type=block.type,
data=block.data,
)
)
theme_row = None
if body.theme is not None:
result = await session.execute(select(WebThemeModel).where(WebThemeModel.page_slug == slug))
theme_row = result.scalar_one_or_none()
if theme_row is None:
theme_row = WebThemeModel(page_slug=slug, tokens=body.theme.tokens)
session.add(theme_row)
else:
theme_row.tokens = body.theme.tokens
await session.flush()
blocks_result = await session.execute(
select(WebBlock).where(WebBlock.page_slug == slug).order_by(WebBlock.order, WebBlock.id)
)
blocks = [WebBlockResponse.model_validate(b) for b in blocks_result.scalars().all()]
if theme_row is None:
theme_result = await session.execute(select(WebThemeModel).where(WebThemeModel.page_slug == slug))
theme_row = theme_result.scalar_one_or_none()
theme = WebTheme(tokens=theme_row.tokens) if theme_row else None
return WebPageResponse(slug=slug, blocks=blocks, theme=theme)
@router.post("/api/web/upload", response_model=WebUploadResponse)
async def upload_media(
file: UploadFile = File(...),
identity=Depends(verify_identity_admin),
):
"""Upload image or video for landing blocks and return same-origin URL."""
if not file.filename or "." not in file.filename:
raise HTTPException(400, "Файл должен иметь расширение")
ext = Path(file.filename).suffix.lower()
if ext not in ALLOWED_EXTENSIONS:
raise HTTPException(
400,
f"Разрешены только: {', '.join(sorted(ALLOWED_EXTENSIONS))}",
)
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
size = 0
for chunk in file.file:
size += len(chunk)
if size > MAX_FILE_SIZE:
raise HTTPException(400, f"Размер файла не более {MAX_FILE_SIZE // (1024*1024)} МБ")
await file.seek(0)
name = f"{uuid.uuid4().hex}{ext}"
path = UPLOAD_DIR / name
with open(path, "wb") as f:
while chunk := await file.read(64 * 1024):
f.write(chunk)
url = f"/api/web/uploads/{name}"
return WebUploadResponse(url=url)