diff --git a/tradein-mvp/frontend/src/app/scrapers/cian-cookies/page.tsx b/tradein-mvp/frontend/src/app/scrapers/cian-cookies/page.tsx new file mode 100644 index 00000000..d7042f21 --- /dev/null +++ b/tradein-mvp/frontend/src/app/scrapers/cian-cookies/page.tsx @@ -0,0 +1,351 @@ +"use client"; + +import { useState } from "react"; +import { useMutation, useQuery } from "@tanstack/react-query"; + +import "@/components/trade-in/trade-in.css"; +import { Topbar } from "@/components/trade-in/Topbar"; +import { apiFetch } from "@/lib/api"; + +interface UploadResponse { + ok?: boolean; + userId?: number; + cookieCount?: number; + detail?: string; +} + +interface TestAuthResponse { + authenticated: boolean; + userId?: number; + reason?: string; +} + +interface CookieEditorEntry { + name: string; + value: string | number | boolean; + [key: string]: unknown; +} + +function convertCookiesFormat(input: string): Record { + const parsed: unknown = JSON.parse(input.trim()); + if (Array.isArray(parsed)) { + // Cookie-Editor array format: [{name, value, domain, ...}, ...] + return (parsed as CookieEditorEntry[]).reduce>( + (acc, c) => { + if ( + typeof c === "object" && + c !== null && + "name" in c && + "value" in c + ) { + acc[String(c.name)] = String(c.value); + } + return acc; + }, + {}, + ); + } + if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) { + // Already a dict: {"name1": "val1", "name2": "val2"} + return Object.fromEntries( + Object.entries(parsed as Record).map(([k, v]) => [ + k, + String(v), + ]), + ); + } + throw new Error("Expected JSON array or plain object"); +} + +function parsePreview(raw: string): { + cookies: Record | null; + error: string | null; +} { + if (!raw.trim()) return { cookies: null, error: null }; + try { + const cookies = convertCookiesFormat(raw); + return { cookies, error: null }; + } catch (e) { + return { cookies: null, error: e instanceof Error ? e.message : String(e) }; + } +} + +export default function CianCookiesPage() { + const [rawInput, setRawInput] = useState(""); + const [showInstructions, setShowInstructions] = useState(false); + + const preview = parsePreview(rawInput); + const previewKeys = preview.cookies ? Object.keys(preview.cookies) : []; + + const uploadMutation = useMutation({ + mutationFn: () => { + if (!preview.cookies) { + throw new Error("Invalid JSON — fix the input first"); + } + return apiFetch( + "/api/v1/admin/scrape/cian/upload-cookies", + { + method: "POST", + body: JSON.stringify(preview.cookies), + }, + ); + }, + onSuccess: () => setRawInput(""), + }); + + const testAuthQuery = useQuery({ + queryKey: ["cian-test-auth"], + queryFn: () => + apiFetch("/api/v1/admin/scrape/cian/test-auth"), + enabled: false, // only on explicit button click + retry: false, + }); + + const canUpload = + !!preview.cookies && previewKeys.length > 0 && !uploadMutation.isPending; + + return ( + <> + +
+

Cian — обновление cookies

+

+ Позволяет обновить сессию Cian для скрапера оценщика без перезапуска + backend. Endpoints:{" "} + POST /api/v1/admin/scrape/cian/upload-cookies ·{" "} + GET /api/v1/admin/scrape/cian/test-auth +

+ + {/* Instructions collapsible */} +
+ + {showInstructions && ( +
    +
  1. + Открой{" "} + + cian.ru + {" "} + и войди под нужным аккаунтом. +
  2. +
  3. + Установи расширение Cookie-Editor{" "} + (Chrome/Firefox) или открой DevTools → Application → + Cookies → www.cian.ru. +
  4. +
  5. + Cookie-Editor: нажми Export → Export as JSON → + скопируй массив{" "} + [{"{"}name, value, ...{"}"}]. +
  6. +
  7. + DevTools (ручной способ): собери объект{" "} + + {"{"} + {'"'}name1{'"'}: {'"'}val1{'"'}, ...{"}"} + + . +
  8. +
  9. + Вставь JSON в поле ниже — формат определится автоматически. +
  10. +
  11. + Убедись что preview показывает нужные ключи, затем нажми + Upload. +
  12. +
+ )} +
+ + {/* Cookie input */} +
+

Cookie JSON

+

+ Вставьте Cookie-Editor array или plain dict. Формат определяется + автоматически. +

+
+