feat(tradein): auth на admin-эндпоинтах (#2)
/api/v1/admin/scrape и geocode-missing закрыты X-Admin-Token. settings.admin_token (env ADMIN_TOKEN). Пусто = открыто (dev), задано = требуем заголовок. cron шлёт токен из .env.runtime.
This commit is contained in:
parent
0b9bf55ba0
commit
7adb7cff66
4 changed files with 30 additions and 3 deletions
|
|
@ -9,11 +9,12 @@ import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Annotated, Literal
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, Header, HTTPException
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
from app.core.db import get_db
|
from app.core.db import get_db
|
||||||
from app.services.geocoder import geocode
|
from app.services.geocoder import geocode
|
||||||
from app.services.scrapers.avito import AvitoScraper
|
from app.services.scrapers.avito import AvitoScraper
|
||||||
|
|
@ -27,6 +28,20 @@ logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
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):
|
class ScrapeRequest(BaseModel):
|
||||||
lat: float
|
lat: float
|
||||||
lon: float
|
lon: float
|
||||||
|
|
@ -52,7 +67,7 @@ class ScrapeResponse(BaseModel):
|
||||||
by_source: list[ScrapeResult]
|
by_source: list[ScrapeResult]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/scrape", response_model=ScrapeResponse)
|
@router.post("/scrape", response_model=ScrapeResponse, dependencies=[Depends(require_admin)])
|
||||||
async def scrape_around(
|
async def scrape_around(
|
||||||
payload: ScrapeRequest,
|
payload: ScrapeRequest,
|
||||||
db: Annotated[Session, Depends(get_db)],
|
db: Annotated[Session, Depends(get_db)],
|
||||||
|
|
@ -121,7 +136,7 @@ def _clean_address_for_geocode(addr: str) -> str:
|
||||||
return main or addr
|
return main or addr
|
||||||
|
|
||||||
|
|
||||||
@router.post("/geocode-missing")
|
@router.post("/geocode-missing", dependencies=[Depends(require_admin)])
|
||||||
async def geocode_missing(
|
async def geocode_missing(
|
||||||
db: Annotated[Session, Depends(get_db)],
|
db: Annotated[Session, Depends(get_db)],
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,9 @@ class Settings(BaseSettings):
|
||||||
# Public URL — для QR-кода в PDF, shareable links, etc.
|
# Public URL — для QR-кода в PDF, shareable links, etc.
|
||||||
public_url: str = "http://127.0.0.1:8080"
|
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
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,10 @@ build_payload() {
|
||||||
|
|
||||||
log "=== cron-scrape start: source=$SOURCE mode=$MODE container=$CONTAINER ==="
|
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
|
for anchor in "${ANCHORS[@]}"; do
|
||||||
read -r lat lon name <<< "$anchor"
|
read -r lat lon name <<< "$anchor"
|
||||||
payload=$(build_payload "$lat" "$lon" "$SOURCE" "$MODE")
|
payload=$(build_payload "$lat" "$lon" "$SOURCE" "$MODE")
|
||||||
|
|
@ -57,6 +61,7 @@ for anchor in "${ANCHORS[@]}"; do
|
||||||
resp=$(docker exec "$CONTAINER" curl -s -X POST \
|
resp=$(docker exec "$CONTAINER" curl -s -X POST \
|
||||||
http://localhost:8000/api/v1/admin/scrape \
|
http://localhost:8000/api/v1/admin/scrape \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "$ADMIN_HDR" \
|
||||||
-d "$payload" \
|
-d "$payload" \
|
||||||
-m 600 2>/dev/null || echo '{"error":"exec_or_timeout"}')
|
-m 600 2>/dev/null || echo '{"error":"exec_or_timeout"}')
|
||||||
log "← $name $resp"
|
log "← $name $resp"
|
||||||
|
|
@ -70,6 +75,7 @@ log "→ geocode-missing (чанками)"
|
||||||
for gi in $(seq 1 15); do
|
for gi in $(seq 1 15); do
|
||||||
geo=$(docker exec "$CONTAINER" curl -s -X POST \
|
geo=$(docker exec "$CONTAINER" curl -s -X POST \
|
||||||
"http://localhost:8000/api/v1/admin/geocode-missing?limit=100" \
|
"http://localhost:8000/api/v1/admin/geocode-missing?limit=100" \
|
||||||
|
-H "$ADMIN_HDR" \
|
||||||
-m 320 2>/dev/null || echo '{"error":"geocode_timeout","remaining":0}')
|
-m 320 2>/dev/null || echo '{"error":"geocode_timeout","remaining":0}')
|
||||||
log "← geocode чанк $gi: $geo"
|
log "← geocode чанк $gi: $geo"
|
||||||
remaining=$(echo "$geo" | grep -oE '"remaining":[0-9]+' | grep -oE '[0-9]+' || echo 0)
|
remaining=$(echo "$geo" | grep -oE '"remaining":[0-9]+' | grep -oE '[0-9]+' || echo 0)
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ services:
|
||||||
ENVIRONMENT: "production"
|
ENVIRONMENT: "production"
|
||||||
CONTACT_EMAIL: "${TRADEIN_CONTACT_EMAIL:-tradein@gendsgn.ru}"
|
CONTACT_EMAIL: "${TRADEIN_CONTACT_EMAIL:-tradein@gendsgn.ru}"
|
||||||
YANDEX_GEOCODER_KEY: "${YANDEX_GEOCODER_KEY:-}"
|
YANDEX_GEOCODER_KEY: "${YANDEX_GEOCODER_KEY:-}"
|
||||||
|
# Защита /api/v1/admin/* — задаётся в .env.runtime. Пусто = открыто (dev).
|
||||||
|
ADMIN_TOKEN: "${ADMIN_TOKEN:-}"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue