Files
evo-sync/web/routes/connections.py
mguschin 796cf49ff9 feat: Evotor + VK catalog sync, connections, and store/group filters
- Evotor catalog: background Celery task syncing stores/groups/products
  from Evotor API; UI pages with per-store and per-group sync toggles
- VK connection: manual token + group ID entry with inline test button
- Evotor connection: inline test button (calls /stores)
- VK catalog: background task syncing VK Market albums and products;
  separate catalog UI at /vk-catalog/albums
- SyncFilter extended to support entity_type=group with parent_entity_id
- Migration 0004: vk_cached_albums + vk_cached_products tables
- Beat schedule updated to run both refresh_catalog and refresh_vk_catalog
- README updated with new schema, routes, tasks, and config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:09:11 +03:00

230 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import secrets
from datetime import datetime, timezone
import httpx
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from sqlalchemy.orm import Session
from web.auth.session import get_current_user
from web.config import settings
from web.database import get_db
from web.models.connections import EvotorConnection, VkConnection
from web.templates_env import templates
router = APIRouter()
def _render(request: Request, template: str, ctx: dict) -> HTMLResponse:
ctx["request"] = request
ctx.setdefault("jivosite_widget_id", settings.JIVOSITE_WIDGET_ID)
return templates.TemplateResponse(ctx.pop("request"), template, ctx)
def _now() -> datetime:
return datetime.now(timezone.utc).replace(tzinfo=None)
@router.get("/connections")
async def connections_get(request: Request, db: Session = Depends(get_db)):
try:
user = get_current_user(request, db)
except Exception:
return RedirectResponse("/login", 303)
evotor = db.query(EvotorConnection).filter_by(user_id=user.id).first()
vk = db.query(VkConnection).filter_by(user_id=user.id).first()
return _render(request, "connections.html", {"user": user, "evotor": evotor, "vk": vk})
@router.post("/connections/evotor")
async def connections_evotor_post(request: Request, db: Session = Depends(get_db)):
try:
user = get_current_user(request, db)
except Exception:
return RedirectResponse("/login", 303)
form = await request.form()
access_token = str(form.get("access_token", "")).strip()
evotor_user_id = str(form.get("evotor_user_id", "")).strip() or None
if not access_token:
evotor = db.query(EvotorConnection).filter_by(user_id=user.id).first()
return _render(request, "connections.html", {
"user": user,
"evotor": evotor,
"errors": ["API-токен обязателен"],
})
now = _now()
conn = db.query(EvotorConnection).filter_by(user_id=user.id).first()
if conn:
conn.access_token = access_token
if evotor_user_id:
conn.evotor_user_id = evotor_user_id
conn.updated_at = now
else:
conn = EvotorConnection(
user_id=user.id,
evotor_user_id=evotor_user_id,
access_token=access_token,
api_token=secrets.token_urlsafe(32),
connected_at=now,
updated_at=now,
)
db.add(conn)
if evotor_user_id and not user.evotor_user_id:
user.evotor_user_id = evotor_user_id
db.commit()
return RedirectResponse("/connections?success=1", 303)
@router.post("/connections/evotor/disconnect")
async def connections_evotor_disconnect(request: Request, db: Session = Depends(get_db)):
try:
user = get_current_user(request, db)
except Exception:
return RedirectResponse("/login", 303)
conn = db.query(EvotorConnection).filter_by(user_id=user.id).first()
if conn:
db.delete(conn)
db.commit()
return RedirectResponse("/connections", 303)
@router.post("/connections/vk")
async def connections_vk_post(request: Request, db: Session = Depends(get_db)):
try:
user = get_current_user(request, db)
except Exception:
return RedirectResponse("/login", 303)
form = await request.form()
access_token = str(form.get("access_token", "")).strip()
vk_group_id = str(form.get("vk_group_id", "")).strip() or None
if not access_token:
evotor = db.query(EvotorConnection).filter_by(user_id=user.id).first()
vk = db.query(VkConnection).filter_by(user_id=user.id).first()
return _render(request, "connections.html", {
"user": user,
"evotor": evotor,
"vk": vk,
"errors": ["Токен VK обязателен"],
})
now = _now()
conn = db.query(VkConnection).filter_by(user_id=user.id).first()
if conn:
conn.access_token = access_token
if vk_group_id:
conn.vk_user_id = vk_group_id
conn.updated_at = now
else:
conn = VkConnection(
user_id=user.id,
access_token=access_token,
vk_user_id=vk_group_id,
connected_at=now,
updated_at=now,
)
db.add(conn)
db.commit()
return RedirectResponse("/connections?success=1", 303)
@router.post("/connections/vk/disconnect")
async def connections_vk_disconnect(request: Request, db: Session = Depends(get_db)):
try:
user = get_current_user(request, db)
except Exception:
return RedirectResponse("/login", 303)
conn = db.query(VkConnection).filter_by(user_id=user.id).first()
if conn:
db.delete(conn)
db.commit()
return RedirectResponse("/connections", 303)
@router.post("/connections/evotor/test")
async def connections_evotor_test(request: Request, db: Session = Depends(get_db)):
try:
user = get_current_user(request, db)
except Exception:
return JSONResponse({"ok": False, "message": "Не авторизован"}, status_code=401)
conn = db.query(EvotorConnection).filter_by(user_id=user.id).first()
if not conn:
return JSONResponse({"ok": False, "message": "Подключение не настроено"})
try:
r = httpx.get(
"https://api.evotor.ru/stores",
headers={
"Authorization": f"Bearer {conn.access_token}",
"Accept": "application/vnd.evotor.v2+json",
},
timeout=10,
)
if r.status_code == 200:
data = r.json()
items = data.get("items", data) if isinstance(data, dict) else data
count = len(items) if isinstance(items, list) else "?"
return JSONResponse({"ok": True, "message": f"Успешно. Найдено магазинов: {count}"})
elif r.status_code == 401:
return JSONResponse({"ok": False, "message": "Токен недействителен (401)"})
else:
return JSONResponse({"ok": False, "message": f"Ошибка API: HTTP {r.status_code}"})
except httpx.TimeoutException:
return JSONResponse({"ok": False, "message": "Таймаут запроса к Эвотор"})
except Exception as e:
return JSONResponse({"ok": False, "message": f"Ошибка: {e}"})
@router.post("/connections/vk/test")
async def connections_vk_test(request: Request, db: Session = Depends(get_db)):
try:
user = get_current_user(request, db)
except Exception:
return JSONResponse({"ok": False, "message": "Не авторизован"}, status_code=401)
conn = db.query(VkConnection).filter_by(user_id=user.id).first()
if not conn:
return JSONResponse({"ok": False, "message": "Подключение не настроено"})
try:
params = {
"access_token": conn.access_token,
"v": settings.VK_API_VERSION,
}
if conn.vk_user_id:
params["group_ids"] = conn.vk_user_id
r = httpx.get(
"https://api.vk.com/method/groups.getById",
params=params,
timeout=10,
)
data = r.json()
if "error" in data:
code = data["error"].get("error_code")
msg = data["error"].get("error_msg", "Неизвестная ошибка")
return JSONResponse({"ok": False, "message": f"Ошибка VK API ({code}): {msg}"})
groups = data.get("response", {}).get("groups", [])
if groups:
name = groups[0].get("name", "")
return JSONResponse({"ok": True, "message": f"Успешно. Сообщество: «{name}»"})
else:
return JSONResponse({"ok": True, "message": "Токен действителен. Укажите ID сообщества для полной проверки."})
except httpx.TimeoutException:
return JSONResponse({"ok": False, "message": "Таймаут запроса к VK"})
except Exception as e:
return JSONResponse({"ok": False, "message": f"Ошибка: {e}"})