import secrets from datetime import datetime, timedelta, timezone from urllib.parse import urlencode 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 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: 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.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 = 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: if not conn.vk_user_id: return JSONResponse({"ok": False, "message": "Укажите ID сообщества для проверки подключения."}) r = httpx.get( "https://api.vk.com/method/groups.getById", 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}"})