fix(tradein): убрать require_admin полностью — Caddy basic_auth достаточен (#474)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Successful in 22s
Deploy Trade-In / build-backend (push) Successful in 39s
Deploy Trade-In / deploy (push) Successful in 22s

This commit is contained in:
lekss361 2026-05-23 14:21:01 +00:00
parent b26e0ff7c3
commit dbad7a626b
4 changed files with 11 additions and 50 deletions

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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