import secrets from datetime import datetime, timedelta, timezone from urllib.parse import urlencode import httpx import web.lib.api_logger as api_logger 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, get_viewed_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 VK_SCOPE = 335876 # photos(4) + wall(8192) + groups(262144) + offline(65536) 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: real_user, viewed_user = get_viewed_user(request, db) except Exception: return RedirectResponse("/login", 303) evotor = db.query(EvotorConnection).filter_by(user_id=viewed_user.id).first() vk = db.query(VkConnection).filter_by(user_id=viewed_user.id).first() return _render(request, "connections.html", { "user": real_user, "viewed_user": viewed_user if viewed_user.id != real_user.id else None, "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.get("/vk-auth") async def vk_auth(request: Request): try: get_current_user(request, next(get_db())) except Exception: return RedirectResponse("/login", 303) if not settings.VK_CLIENT_ID: return RedirectResponse("/connections?error=vk_not_configured", 303) state = secrets.token_urlsafe(16) request.session["vk_oauth_state"] = state redirect_uri = f"{settings.BASE_URL}/vk-callback" params = urlencode({ "client_id": settings.VK_CLIENT_ID, "redirect_uri": redirect_uri, "scope": VK_SCOPE, "response_type": "token", "display": "page", "state": state, "revoke": "1", }) return RedirectResponse(f"https://oauth.vk.com/authorize?{params}", 302) @router.get("/vk-callback") async def vk_callback_page(request: Request): """Serves the callback page that reads the token from the URL fragment and POSTs it.""" return HTMLResponse(""" VK авторизация…

Завершаем авторизацию…

""") @router.post("/vk-callback/save") async def vk_callback_save(request: Request, db: Session = Depends(get_db)): try: user = get_current_user(request, db) except Exception: return JSONResponse({"ok": False, "message": "Сессия истекла, войдите снова"}, status_code=401) body = await request.json() access_token = (body.get("access_token") or "").strip() state = body.get("state") or "" vk_user_id = str(body.get("user_id") or "").strip() or None expires_in = body.get("expires_in") expected_state = request.session.pop("vk_oauth_state", None) if not expected_state or state != expected_state: return JSONResponse({"ok": False, "message": "Недействительный state, попробуйте снова"}) if not access_token: return JSONResponse({"ok": False, "message": "Токен не получен"}) token_expires_at = None if expires_in and str(expires_in) != "0": try: token_expires_at = _now() + timedelta(seconds=int(expires_in)) except (ValueError, TypeError): pass now = _now() conn = db.query(VkConnection).filter_by(user_id=user.id).first() if conn: conn.access_token = access_token conn.token_expires_at = token_expires_at if vk_user_id: conn.vk_user_id = vk_user_id conn.updated_at = now else: conn = VkConnection( user_id=user.id, access_token=access_token, token_expires_at=token_expires_at, vk_user_id=vk_user_id, connected_at=now, updated_at=now, ) db.add(conn) db.commit() return JSONResponse({"ok": True}) @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 = api_logger.get( "https://api.evotor.ru/stores", user_id=user.id, 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: if not conn.vk_user_id: return JSONResponse({"ok": False, "message": "Укажите ID сообщества для проверки подключения."}) r = api_logger.get( "https://api.vk.com/method/groups.getById", user_id=user.id, params={ "group_id": conn.vk_user_id, "fields": "market", "access_token": conn.access_token, "v": settings.VK_API_VERSION, }, 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 not groups: return JSONResponse({"ok": False, "message": "Сообщество не найдено"}) group = groups[0] name = group.get("name", "—") market = group.get("market", {}) market_status = "включён" if market.get("enabled") else "выключен" return JSONResponse({"ok": True, "message": f"Успешно. Сообщество: «{name}», Маркет {market_status}"}) except httpx.TimeoutException: return JSONResponse({"ok": False, "message": "Таймаут запроса к VK"}) except Exception as e: return JSONResponse({"ok": False, "message": f"Ошибка: {e}"})