diff --git a/tradein-mvp/backend/app/api/v1/admin.py b/tradein-mvp/backend/app/api/v1/admin.py index f6e62f31..6dbe6923 100644 --- a/tradein-mvp/backend/app/api/v1/admin.py +++ b/tradein-mvp/backend/app/api/v1/admin.py @@ -1,6 +1,6 @@ """Admin endpoints — debug + ручной запуск парсеров. -Пока без auth (Слой 7), потом закроем общим middleware. +Auth: Caddy basic_auth gate'ит /trade-in/api/v1/admin/* — application layer открыт. """ from __future__ import annotations @@ -9,7 +9,7 @@ import logging import time from typing import Annotated, Literal -from fastapi import APIRouter, Depends, Header, HTTPException +from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from sqlalchemy import text from sqlalchemy.orm import Session @@ -37,20 +37,6 @@ logger = logging.getLogger(__name__) router = APIRouter() -def require_admin( - x_admin_token: Annotated[str | None, Header()] = None, -) -> None: - """Проверка admin-токена для /api/v1/admin/*. - - Если settings.admin_token не задан (dev) — пропускаем (открыто). - Если задан — требуем заголовок X-Admin-Token с этим значением. - """ - if settings.admin_token is None: - return # dev-режим: токен не настроен → эндпоинты открыты - if x_admin_token != settings.admin_token: - raise HTTPException(status_code=401, detail="Invalid or missing X-Admin-Token") - - class ScrapeRequest(BaseModel): lat: float lon: float @@ -76,7 +62,7 @@ class ScrapeResponse(BaseModel): by_source: list[ScrapeResult] -@router.post("/scrape", response_model=ScrapeResponse, dependencies=[Depends(require_admin)]) +@router.post("/scrape", response_model=ScrapeResponse) async def scrape_around( payload: ScrapeRequest, db: Annotated[Session, Depends(get_db)], @@ -145,7 +131,7 @@ def _clean_address_for_geocode(addr: str) -> str: return main or addr -@router.post("/geocode-missing", dependencies=[Depends(require_admin)]) +@router.post("/geocode-missing") async def geocode_missing( db: Annotated[Session, Depends(get_db)], limit: int = 100, @@ -244,7 +230,7 @@ async def geocode_missing( } -@router.post("/scrape/cian/upload-cookies", status_code=200, dependencies=[Depends(require_admin)]) +@router.post("/scrape/cian/upload-cookies", status_code=200) async def upload_cian_cookies( cookies: dict[str, str], db: Annotated[Session, Depends(get_db)], @@ -290,7 +276,7 @@ async def upload_cian_cookies( return {"ok": True, "userId": user_id, "cookieCount": len(cleaned)} -@router.get("/scrape/cian/test-auth", status_code=200, dependencies=[Depends(require_admin)]) +@router.get("/scrape/cian/test-auth", status_code=200) async def test_cian_auth( db: Annotated[Session, Depends(get_db)], ) -> dict: @@ -316,7 +302,7 @@ async def test_cian_auth( # ── Stage 4a: Avito enrichment endpoints (single-shot debug + manual triggers) ── -@router.post("/scrape/avito-house", dependencies=[Depends(require_admin)]) +@router.post("/scrape/avito-house") async def scrape_avito_house( db: Annotated[Session, Depends(get_db)], house_url: str, @@ -344,7 +330,7 @@ async def scrape_avito_house( return {"ok": True, "house_url": house_url, "counters": counters} -@router.post("/scrape/avito-detail", dependencies=[Depends(require_admin)]) +@router.post("/scrape/avito-detail") async def scrape_avito_detail( db: Annotated[Session, Depends(get_db)], item_url: str, @@ -376,7 +362,7 @@ async def scrape_avito_detail( return {"ok": True, "item_id": enrichment.item_id, "updated": updated} -@router.post("/scrape/avito-imv", dependencies=[Depends(require_admin)]) +@router.post("/scrape/avito-imv") async def scrape_avito_imv( db: Annotated[Session, Depends(get_db)], address: str, diff --git a/tradein-mvp/backend/app/core/config.py b/tradein-mvp/backend/app/core/config.py index 890bad97..85269d76 100644 --- a/tradein-mvp/backend/app/core/config.py +++ b/tradein-mvp/backend/app/core/config.py @@ -21,10 +21,6 @@ class Settings(BaseSettings): # Public URL — для QR-кода в PDF, shareable links, etc. public_url: str = "http://127.0.0.1:8080" - # Admin token — защищает /api/v1/admin/* (scrape, geocode-missing). - # Пусто = эндпоинты открыты (dev). В prod задаётся через env ADMIN_TOKEN. - admin_token: str | None = None - # GlitchTip DSN — мониторинг ошибок (Sentry-совместимый). #396. # Пусто = мониторинг выключен (dev). В prod — env GLITCHTIP_DSN из .env.runtime. glitchtip_dsn: str | None = None diff --git a/tradein-mvp/deploy/cron-scrape.sh b/tradein-mvp/deploy/cron-scrape.sh index 8b06ad52..c7df2b4e 100755 --- a/tradein-mvp/deploy/cron-scrape.sh +++ b/tradein-mvp/deploy/cron-scrape.sh @@ -12,8 +12,7 @@ set -euo pipefail -# cron запускается с ПУСТЫМ окружением — ADMIN_TOKEN в нём нет, и без него -# backend (#2 admin-auth) отвечает 401. Подхватываем .env.runtime сами: +# cron запускается с ПУСТЫМ окружением. Подхватываем .env.runtime сами: # он лежит на уровень выше скрипта — tradein-mvp/.env.runtime. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_RUNTIME="${ENV_RUNTIME:-$SCRIPT_DIR/../.env.runtime}" @@ -61,10 +60,6 @@ build_payload() { log "=== cron-scrape start: source=$SOURCE mode=$MODE container=$CONTAINER ===" -# X-Admin-Token заголовок — из ADMIN_TOKEN (.env.runtime). Если пусто — заголовок -# всё равно шлём, backend в dev-режиме (admin_token=None) его игнорирует. -ADMIN_HDR="X-Admin-Token: ${ADMIN_TOKEN:-}" - for anchor in "${ANCHORS[@]}"; do read -r lat lon name <<< "$anchor" payload=$(build_payload "$lat" "$lon" "$SOURCE" "$MODE") @@ -73,7 +68,6 @@ for anchor in "${ANCHORS[@]}"; do resp=$(docker exec "$CONTAINER" curl -s -X POST \ http://localhost:8000/api/v1/admin/scrape \ -H "Content-Type: application/json" \ - -H "$ADMIN_HDR" \ -d "$payload" \ -m 600 2>/dev/null || echo '{"error":"exec_or_timeout"}') log "← $name $resp" @@ -87,7 +81,6 @@ log "→ geocode-missing listings (чанками)" for gi in $(seq 1 15); do geo=$(docker exec "$CONTAINER" curl -s -X POST \ "http://localhost:8000/api/v1/admin/geocode-missing?limit=100&target=listings" \ - -H "$ADMIN_HDR" \ -m 320 2>/dev/null || echo '{"error":"geocode_timeout","remaining":0}') log "← listings чанк $gi: $geo" remaining=$(echo "$geo" | grep -oE '"remaining":[0-9]+' | grep -oE '[0-9]+' || echo 0) @@ -100,7 +93,6 @@ log "→ geocode-missing deals (чанками)" for gi in $(seq 1 30); do geo=$(docker exec "$CONTAINER" curl -s -X POST \ "http://localhost:8000/api/v1/admin/geocode-missing?limit=100&target=deals" \ - -H "$ADMIN_HDR" \ -m 320 2>/dev/null || echo '{"error":"geocode_timeout","remaining":0}') log "← deals чанк $gi: $geo" remaining=$(echo "$geo" | grep -oE '"remaining":[0-9]+' | grep -oE '[0-9]+' || echo 0) diff --git a/tradein-mvp/scripts/upload-cian-cookies.sh b/tradein-mvp/scripts/upload-cian-cookies.sh index 57be0858..6803fbb4 100644 --- a/tradein-mvp/scripts/upload-cian-cookies.sh +++ b/tradein-mvp/scripts/upload-cian-cookies.sh @@ -8,8 +8,8 @@ # или через расширение Cookie-Editor → Export → JSON (object format). # # Переменные окружения: -# TRADEIN_ADMIN_TOKEN — admin token (или берётся из .env.runtime) # TRADEIN_BASE_URL — base URL (default: https://gendsgn.ru/trade-in) +# Auth: Caddy basic_auth (auth handled by curl --user / browser session). set -euo pipefail @@ -21,24 +21,12 @@ if [[ ! -f "$COOKIES_FILE" ]]; then exit 1 fi -# Загружаем токен из env или .env.runtime -ADMIN_TOKEN="${TRADEIN_ADMIN_TOKEN:-}" -if [[ -z "$ADMIN_TOKEN" ]] && [[ -f ".env.runtime" ]]; then - ADMIN_TOKEN="$(grep '^TRADEIN_ADMIN_TOKEN=' .env.runtime 2>/dev/null | cut -d= -f2- || true)" -fi - -if [[ -z "$ADMIN_TOKEN" ]]; then - echo "Error: TRADEIN_ADMIN_TOKEN not set (env or .env.runtime)" >&2 - exit 1 -fi - BASE_URL="${TRADEIN_BASE_URL:-https://gendsgn.ru/trade-in}" echo "Uploading Cian cookies from $COOKIES_FILE to $BASE_URL ..." echo curl -fsSL -X POST "$BASE_URL/api/v1/admin/scrape/cian/upload-cookies" \ - -H "X-Admin-Token: $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d "@$COOKIES_FILE" \ | python3 -m json.tool @@ -46,5 +34,4 @@ curl -fsSL -X POST "$BASE_URL/api/v1/admin/scrape/cian/upload-cookies" \ echo echo "Verifying session:" curl -fsSL "$BASE_URL/api/v1/admin/scrape/cian/test-auth" \ - -H "X-Admin-Token: $ADMIN_TOKEN" \ | python3 -m json.tool