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)