diff --git a/README.md b/README.md index 20fb952..e8e97f2 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Production release отделяет Alembic от runtime: одноразовый Тяжёлый production bootstrap вынесен в отдельный ручной/еженедельный CI workflow с 30-минутным timeout и сохраняемыми diagnostics; обычный push по-прежнему использует быстрый Compose E2E. -Публичный API зафиксирован генерируемым [OpenAPI-контрактом](docs/api-contract.md): CI сравнивает `apps/api/openapi.json` с фактической схемой FastAPI, поэтому рефакторинг routers не может незаметно изменить URL, параметры или response models. Декомпозиция выполняется инкрементально: общий `Db` вынесен в dependencies; catalog, activity/spots и records/community/status/import-history уже обслуживают отдельные `APIRouter`. Проверка доверенных proxy и persistent rate limit отправки улова изолированы в `submission_security`. +Публичный API зафиксирован генерируемым [OpenAPI-контрактом](docs/api-contract.md): CI сравнивает `apps/api/openapi.json` с фактической схемой FastAPI, поэтому рефакторинг routers не может незаметно изменить URL, параметры или response models. Декомпозиция выполняется инкрементально: catalog, activity/spots, public data и submissions принадлежат отдельным `APIRouter`; проверка доверенных proxy и persistent rate limit отправки улова изолированы в `submission_security`. После повторных ошибок scheduler увеличивает паузу экспоненциально до 24 часов и возвращается к 30 минутам после успеха. Публичная страница `/status` показывает свежесть и состояние источников без URL запросов, внутренних ошибок и другой диагностической информации. diff --git a/apps/api/app/main.py b/apps/api/app/main.py index bbb2bf3..b77114f 100644 --- a/apps/api/app/main.py +++ b/apps/api/app/main.py @@ -29,6 +29,7 @@ from .readiness import readiness_report from .routers.activity import router as activity_router from .routers.catalog import router as catalog_router from .routers.public_data import router as public_data_router +from .routers.submissions import router as submissions_router from .public_cache import public_cache from .schemas import ActivityOut, AdminCatchReportOut, CatchReportAccepted, CatchReportCreate, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationUpdate from .storage import ScreenshotError, client as storage_client, delete_screenshot, signed_screenshot_url, upload_screenshot @@ -255,7 +256,7 @@ def admin_reject_external_observation( raise HTTPException(status_code=409, detail=str(exc)) from exc -@app.post("/api/v1/catch-reports", response_model=CatchReportAccepted, status_code=201) +@submissions_router.post("/api/v1/catch-reports", response_model=CatchReportAccepted, status_code=201) def create_catch_report( payload: CatchReportCreate, request: Request, db: Db, idempotency_key: Annotated[str | None, Header()] = None, @@ -331,7 +332,7 @@ def create_catch_report( return CatchReportAccepted(id=report.id, moderation_status=report.moderation_status.value, screenshot_upload_token=upload_token, idempotent=False) -@app.post("/api/v1/catch-reports/{report_id}/screenshot", status_code=204, response_class=Response) +@submissions_router.post("/api/v1/catch-reports/{report_id}/screenshot", status_code=204, response_class=Response) def add_screenshot( report_id: UUID, db: Db, screenshot: UploadFile = File(), upload_token: Annotated[str | None, Header(alias="X-Upload-Token")] = None, @@ -354,6 +355,9 @@ def add_screenshot( return Response(status_code=204) +app.include_router(submissions_router) + + @app.get("/api/v1/admin/catch-reports", response_model=list[AdminCatchReportOut]) def admin_reports(db: Db, _: Annotated[str, Depends(_admin)], status: ModerationStatus = ModerationStatus.pending, limit: int = Query(50, ge=1, le=100), offset: int = Query(0, ge=0)) -> list[AdminCatchReportOut]: reports = list(db.scalars(select(CatchReport).options(joinedload(CatchReport.fish), joinedload(CatchReport.waterbody), joinedload(CatchReport.spot), joinedload(CatchReport.bait)).where(CatchReport.source_type == SourceType.user, CatchReport.moderation_status == status, CatchReport.deleted_at.is_(None)).order_by(CatchReport.reported_at, CatchReport.id).offset(offset).limit(limit))) diff --git a/apps/api/app/routers/submissions.py b/apps/api/app/routers/submissions.py new file mode 100644 index 0000000..af9233c --- /dev/null +++ b/apps/api/app/routers/submissions.py @@ -0,0 +1,3 @@ +from fastapi import APIRouter + +router = APIRouter() diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 057855a..87f1c1d 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -51,7 +51,7 @@ Аудит выполнен на старой базе `13e04e6`; рекомендации ниже повторно проверены по текущей ветке. Уже реализованные или неприменимые предложения не возвращаются в backlog. -- [ ] **Q11 · Декомпозиция API — в работе.** После фиксации OpenAPI публичные catalog, activity/spots и records/community/status/import-history вынесены в отдельные `APIRouter`; общий `Db` находится в dependencies, нормализация времени — в `time_utils`, доверие к proxy и persistent rate limit — в `submission_security`. URL, response models и generated contract сохранены. Далее: submission endpoints и admin отдельными пакетами. +- [ ] **Q11 · Декомпозиция API — в работе.** После фиксации OpenAPI публичные catalog, activity/spots и records/community/status/import-history вынесены в отдельные `APIRouter`; submission endpoints уже принадлежат собственному router, а их security-слой изолирован в `submission_security`. URL, response models и generated contract сохранены. Далее: физически вынести submission handlers из `main.py`, затем admin. - [ ] **Q12 · Query-plan gate.** В рамках Q07 снять `EXPLAIN (ANALYZE, BUFFERS)` для activity, records, spot detail и public spot pages на реалистичном наборе данных. Существующие индексы миграции `0011_query_indexes` не дублировать; индекс с `fish_id`, SQL-агрегацию или materialized view добавлять только по измеренному плану и p95. - [x] **Q13 · Production bootstrap в CI.** Отдельный workflow запускает `deploy/test-production-bootstrap.sh` вручную или раз в неделю, а не на каждом push. Вывод bootstrap всегда сохраняется 14 дней; при падении добавляются Compose status и Playwright diagnostics. - [ ] **Q14 · Полная CSP.** Расширить текущую CSP (`frame-ancestors`, `base-uri`, `object-src`) до `default-src`, `script-src`, `style-src`, `img-src`, `connect-src` и `form-action`. Сначала инвентаризировать inline scripts/styles Astro, затем внедрить nonce/hash или безопасное вынесение; проверить report/admin/OG без ослабления до произвольных внешних origin.