feat(mera/b2c): публичный периметр на отдельном домене meraocenka.ru — этап 1 из 8 #2545
3 changed files with 251 additions and 0 deletions
38
.forgejo/workflows/perimeter-smoke.yml
Normal file
38
.forgejo/workflows/perimeter-smoke.yml
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Регресс-тест публичного B2C-периметра МЕРА (ЭТАП 1 плана B2C-запуска).
|
||||||
|
#
|
||||||
|
# НЕ pre-merge гейт — эти 4 проверки требуют реального DNS + выпущенного TLS-
|
||||||
|
# сертификата для meraocenka.ru, т.е. осмысленны ТОЛЬКО против прода после
|
||||||
|
# деплоя. Запускается вручную (workflow_dispatch) или раз в сутки (cron) —
|
||||||
|
# страхует от случайной регрессии периметра (например, будущий PR по ошибке
|
||||||
|
# открывает B2B-путь на публичном домене, или basic_auth gate на gendsgn.ru
|
||||||
|
# случайно снимают).
|
||||||
|
#
|
||||||
|
# ДО того как появится DNS A-record meraocenka.ru → IP VPS, проверки 1 и 2
|
||||||
|
# (см. scripts/smoke-mera-perimeter.sh) ожидаемо КРАСНЫЕ — это не регресс,
|
||||||
|
# просто домен ещё не резолвится. Проверки 3 и 4 не зависят от DNS нового
|
||||||
|
# домена и обязаны быть зелёными всегда.
|
||||||
|
name: perimeter-smoke-mera
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
schedule:
|
||||||
|
# Раз в сутки, 06:17 UTC — вне пиков, время произвольное.
|
||||||
|
- cron: '17 6 * * *'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: perimeter-smoke-mera
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
smoke:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run perimeter smoke checks
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/smoke-mera-perimeter.sh
|
||||||
|
./scripts/smoke-mera-perimeter.sh
|
||||||
113
Caddyfile
113
Caddyfile
|
|
@ -193,6 +193,119 @@ www.gendsgn.ru {
|
||||||
redir https://gendsgn.ru{uri} permanent
|
redir https://gendsgn.ru{uri} permanent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# МЕРА B2C — публичный периметр (ЭТАП 1 плана B2C-запуска, БЕЗ функционала).
|
||||||
|
#
|
||||||
|
# Архитектурное решение: отдельный домен, а НЕ дырка в блоке gendsgn.ru
|
||||||
|
# выше. На gendsgn.ru модель "запрещено всё, кроме дырок ВЫШЕ auth-import" —
|
||||||
|
# порядко-зависимая и общая для B2B (trade-in v2, admin, scrapers, /api/*).
|
||||||
|
# Здесь, наоборот, allowlist-by-default: basic_auth НЕТ ВООБЩЕ (не импортируем
|
||||||
|
# caddy/users.caddy.snippet), потому что на этом site-блоке B2B-маршрутов
|
||||||
|
# физически не объявлено — их нечего "открывать". Явно перечислены РОВНО два
|
||||||
|
# handle (корень "/" + статика Next _next/*), всё остальное — финальный
|
||||||
|
# catch-all `handle { respond 404 }`. Регресс-тест на эту модель:
|
||||||
|
# scripts/smoke-mera-perimeter.sh (проверяет, что B2B-путь здесь = 404, а не
|
||||||
|
# 200/401 — т.е. не был случайно проброшен).
|
||||||
|
#
|
||||||
|
# Next.js basePath=/trade-in запечён в prod-образ tradein-frontend (тот же
|
||||||
|
# контейнер, что обслуживает и gendsgn.ru/trade-in/*, см. build-args в
|
||||||
|
# .forgejo/workflows/deploy-tradein.yml) — поэтому корень домена rewrite'ится
|
||||||
|
# на internal-путь /trade-in/mera-public (страница-заглушка,
|
||||||
|
# tradein-mvp/frontend/src/app/mera-public/). Пользователь префикс /trade-in
|
||||||
|
# никогда не видит — rewrite меняет путь ТОЛЬКО для Caddy→backend запроса,
|
||||||
|
# это не HTTP-редирект браузера.
|
||||||
|
#
|
||||||
|
# DNS: A-record meraocenka.ru → IP VPS — ТРЕБУЕТСЯ ДО того, как сюда придёт
|
||||||
|
# реальный трафик. Если записи ещё нет на момент деплоя этого блока: `caddy
|
||||||
|
# reload`/`up -d --force-recreate caddy` в deploy.yml НЕ падает (конфиг
|
||||||
|
# синтаксически валиден, ошибка сертификата асинхронна и per-hostname) — Caddy
|
||||||
|
# просто залогирует неудачную попытку ACME-выпуска для meraocenka.ru (DNS не
|
||||||
|
# резолвится на этот сервер → HTTP-01/TLS-ALPN challenge недостижим) и продолжит
|
||||||
|
# ретраить с backoff, ПОКА запись не появится. Остальные site-блоки в этом же
|
||||||
|
# Caddyfile (gendsgn.ru, obsidian.gendsgn.ru и т.д.) не затрагиваются —
|
||||||
|
# автоматический HTTPS в Caddy изолирован per-hostname (тот же принцип, что
|
||||||
|
# уже описан для status.gendsgn.ru ниже). Повторные неудачные попытки ДО
|
||||||
|
# появления DNS могут исчерпать rate-limit Let's Encrypt (5 failed
|
||||||
|
# validations/hostname/hour) — не критично, просто подождать; `docker volume
|
||||||
|
# rm gendesign_caddy_data` для этого НЕ нужен (и вообще требует user-approval).
|
||||||
|
meraocenka.ru {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/meraocenka.ru.log
|
||||||
|
}
|
||||||
|
|
||||||
|
# Корень домена → лэндинг МЕРЫ (#2615 заменил заглушку этого этапа на
|
||||||
|
# полноценную страницу). rewrite добавляет basePath-префикс только для
|
||||||
|
# Caddy→backend хопа, пользователь /trade-in никогда не видит.
|
||||||
|
handle / {
|
||||||
|
rewrite * /trade-in/mera-public
|
||||||
|
reverse_proxy tradein-frontend:3000 {
|
||||||
|
# Тот же периметр-скраб, что у @uipreview (:87) и @tradein ниже.
|
||||||
|
# Этот блок вообще не под basic_auth, поэтому анонимный клиент
|
||||||
|
# тем более может прислать свой X-Authenticated-User. Сейчас
|
||||||
|
# инертно (лэндинг статичен, backend-вызовов нет), но снимаем
|
||||||
|
# ради единообразия периметра, а не полагаясь на то, что
|
||||||
|
# downstream ничего не делает с заголовком — иначе на этапе 5,
|
||||||
|
# когда откроется публичный /estimate, это станет дырой.
|
||||||
|
header_up -X-Authenticated-User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Подстраницы САМОГО лэндинга. Нужны с момента мержа #2615: футер ссылается
|
||||||
|
# на политику обработки ПДн через next/link (`PRIVACY_PATH`), а Next с
|
||||||
|
# basePath эмитит её как /trade-in/mera-public/privacy. Без этого handle
|
||||||
|
# ссылка уходила бы в catch-all 404 ниже — то есть обязательный по 152-ФЗ
|
||||||
|
# документ был бы недоступен с публичной страницы.
|
||||||
|
#
|
||||||
|
# Matcher намеренно узкий — ровно поддерево лэндинга, НЕ /trade-in/*.
|
||||||
|
# B2B-дерево (/trade-in/v2, /trade-in/api/*, /trade-in/admin/*, /history)
|
||||||
|
# под него не подпадает и по-прежнему отдаёт 404. Регресс-тест на это —
|
||||||
|
# в scripts/smoke-mera-perimeter.sh.
|
||||||
|
handle /trade-in/mera-public/* {
|
||||||
|
reverse_proxy tradein-frontend:3000 {
|
||||||
|
header_up -X-Authenticated-User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next.js уже эмитит ссылки на статику с /trade-in-префиксом (тот же
|
||||||
|
# basePath) — passthrough без rewrite. Нужны для рендера страницы (JS/CSS
|
||||||
|
# чанки), сами по себе не содержат ни B2B-данных, ни секретов.
|
||||||
|
#
|
||||||
|
# Именно `static/*`, а не весь `_next/*` — тот же матчер, что у @uipreview
|
||||||
|
# (:78), который в проде доказал, что этого хватает для рендера. Широкий
|
||||||
|
# `_next/*` открыл бы анонимам ещё и `/_next/image` (оптимизация картинок,
|
||||||
|
# CPU-нагрузка по запросу), который на лэндинге не используется вообще:
|
||||||
|
# next/image в tradein-mvp/frontend/src/app/mera-public/ не импортируется.
|
||||||
|
handle /trade-in/_next/static/* {
|
||||||
|
reverse_proxy tradein-frontend:3000 {
|
||||||
|
header_up -X-Authenticated-User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Allowlist-by-default: любой другой путь (включая B2B — /v2, /admin,
|
||||||
|
# /scrapers/*, /trade-in/api/*, /history, ...) — 404, НЕ проксируется.
|
||||||
|
handle {
|
||||||
|
respond 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Домены-спутники МЕРА → 301 на канонический meraocenka.ru.
|
||||||
|
# Решение 2026-07-31: канонический адрес ровно один, остальные две регистрации
|
||||||
|
# ловят (а) альтернативный транслит «оценка» — ocenka/otsenka, на слух
|
||||||
|
# неразличимы, (б) прежний рабочий вариант merahome. Отдельные site-блоки, а не
|
||||||
|
# matcher внутри основного: Caddy матчит по hostname и выпускает свой
|
||||||
|
# сертификат на каждый, поэтому DNS A-record нужен для КАЖДОГО из них — иначе
|
||||||
|
# ACME для этого хоста будет ретраиться (безвредно, см. комментарий выше, но
|
||||||
|
# лучше завести записи сразу).
|
||||||
|
# `{uri}` сохраняет путь и query — короткая ссылка с визитки не теряет ?id=.
|
||||||
|
merahome.ru {
|
||||||
|
redir https://meraocenka.ru{uri} permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
meraotsenka.ru {
|
||||||
|
redir https://meraocenka.ru{uri} permanent
|
||||||
|
}
|
||||||
|
|
||||||
# Obsidian Self-hosted LiveSync (CouchDB backend).
|
# Obsidian Self-hosted LiveSync (CouchDB backend).
|
||||||
# Auto-TLS Let's Encrypt. CORS уже включён на стороне CouchDB через bootstrap
|
# Auto-TLS Let's Encrypt. CORS уже включён на стороне CouchDB через bootstrap
|
||||||
# (см. scripts/setup-couchdb.sh). Basic-auth — на стороне CouchDB (admin user).
|
# (см. scripts/setup-couchdb.sh). Basic-auth — на стороне CouchDB (admin user).
|
||||||
|
|
|
||||||
100
scripts/smoke-mera-perimeter.sh
Normal file
100
scripts/smoke-mera-perimeter.sh
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Регресс-тест публичного B2C-периметра МЕРА (ЭТАП 1 плана B2C-запуска).
|
||||||
|
#
|
||||||
|
# Проверяет инварианты периметра (см. корневой Caddyfile):
|
||||||
|
# 1. meraocenka.ru отдаёт 200 анонимно (публичный лэндинг).
|
||||||
|
# 1b. Подстраница лэндинга /trade-in/mera-public/privacy отдаёт 200 —
|
||||||
|
# политика ПДн, на которую ссылается футер.
|
||||||
|
# 2. meraocenka.ru/v2 и /trade-in/v2, /trade-in/api/* (B2B-пути) отдают 404 —
|
||||||
|
# allowlist-by-default, НЕ были случайно проброшены на B2B-дерево
|
||||||
|
# tradein-frontend. Проверяются обе формы — с basePath-префиксом и без.
|
||||||
|
# 3. trade-in API (/me, /history, /admin/*) отдаёт 401 анониму — данные B2B
|
||||||
|
# закрыты. Именно API, а не страница: см. комментарий у проверки ниже.
|
||||||
|
# 4. gendsgn.ru/api/v1/admin/* отдаёт 401 анониму (gate Site Finder).
|
||||||
|
# 5. merahome.ru и meraotsenka.ru отдают 301 на канонический meraocenka.ru.
|
||||||
|
#
|
||||||
|
# ВАЖНО: проверки 1 и 2 требуют, чтобы DNS A-record meraocenka.ru → IP VPS
|
||||||
|
# уже существовал И деплой прошёл (сертификат Let's Encrypt выпущен). Пока
|
||||||
|
# записи нет — они ожидаемо падают (DNS resolution failure / TLS handshake
|
||||||
|
# failure), это НЕ регресс периметра gendsgn.ru. Проверки 3 и 4 не зависят от
|
||||||
|
# DNS нового домена и обязаны быть зелёными всегда.
|
||||||
|
#
|
||||||
|
# Запуск вручную:
|
||||||
|
# bash scripts/smoke-mera-perimeter.sh
|
||||||
|
# Запуск в CI: .forgejo/workflows/perimeter-smoke.yml (workflow_dispatch + daily cron).
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
BASE_MERA="${SMOKE_MERA_BASE:-https://meraocenka.ru}"
|
||||||
|
BASE_MAIN="${SMOKE_MAIN_BASE:-https://gendsgn.ru}"
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local desc="$1" url="$2" expected="$3"
|
||||||
|
local code
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$url" 2>/dev/null)
|
||||||
|
if [ "$code" = "$expected" ]; then
|
||||||
|
echo "PASS: $desc ($url -> $code)"
|
||||||
|
else
|
||||||
|
echo "FAIL: $desc ($url -> got '${code:-<no response>}', expected $expected)"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "== МЕРА B2C perimeter smoke (ЭТАП 1) =="
|
||||||
|
|
||||||
|
# 1. Публичный домен отдаёт 200 анонимно.
|
||||||
|
check "meraocenka.ru root — public 200" "$BASE_MERA/" 200
|
||||||
|
|
||||||
|
# 1b. Подстраница лэндинга (политика ПДн) доступна — на неё ссылается футер.
|
||||||
|
# Путь приезжает с basePath: next/link + basePath=/trade-in эмитит именно
|
||||||
|
# /trade-in/mera-public/privacy. Если этот handle выпадет из Caddyfile,
|
||||||
|
# обязательный по 152-ФЗ документ станет недоступен с публичной страницы.
|
||||||
|
check "meraocenka.ru privacy — public 200" "$BASE_MERA/trade-in/mera-public/privacy" 200
|
||||||
|
|
||||||
|
# 2. B2B-путь на публичном домене — 404 (allowlist-by-default), не 200/401.
|
||||||
|
check "meraocenka.ru/v2 — B2B path must 404" "$BASE_MERA/v2" 404
|
||||||
|
|
||||||
|
# 2b. Те же B2B-пути в basePath-форме — 404. Это регресс-тест именно на
|
||||||
|
# matcher `handle /trade-in/mera-public/*`: расширь его случайно до
|
||||||
|
# `/trade-in/*` — и B2B-дерево уедет наружу через публичный домен, а
|
||||||
|
# проверка 2 (/v2 без префикса) этого НЕ заметит.
|
||||||
|
check "meraocenka.ru/trade-in/v2 — B2B path must 404" "$BASE_MERA/trade-in/v2" 404
|
||||||
|
check "meraocenka.ru/trade-in/api/* — must 404 (не проксируем API)" "$BASE_MERA/trade-in/api/v1/me" 404
|
||||||
|
|
||||||
|
# 2c. Статика проксируется ТОЛЬКО из _next/static/*. Оптимизатор картинок
|
||||||
|
# /_next/image на лэндинге не нужен (next/image там не импортируется) и
|
||||||
|
# наружу не открыт — иначе аноним получил бы CPU-нагрузку по запросу.
|
||||||
|
# Ловит расширение матчера обратно до `/trade-in/_next/*`.
|
||||||
|
check "meraocenka.ru/_next/image — must 404 (не открываем оптимизатор)" "$BASE_MERA/trade-in/_next/image?url=%2Ftest.png&w=64&q=75" 404
|
||||||
|
|
||||||
|
# 3. B2B-данные trade-in по-прежнему закрыты анониму.
|
||||||
|
#
|
||||||
|
# ВНИМАНИЕ: проверять СТРАНИЦУ (/trade-in/v2) больше нельзя — она отдаёт 200.
|
||||||
|
# После #2555/#2558 trade-in ушёл с Caddy basic_auth на собственный логин:
|
||||||
|
# страница рендерится анониму, а RouteGuard уже на клиенте уводит на /login.
|
||||||
|
# Гейт данных переехал на API — там и проверяем, иначе тест зелёный при
|
||||||
|
# открытом наружу бэкенде.
|
||||||
|
check "trade-in /api/v1/me — 401 anonymous" "$BASE_MAIN/trade-in/api/v1/me" 401
|
||||||
|
check "trade-in /api/v1/history — 401 anonymous (чужие оценки)" "$BASE_MAIN/trade-in/api/v1/history" 401
|
||||||
|
check "trade-in /api/v1/admin/* — 401 anonymous" "$BASE_MAIN/trade-in/api/v1/admin/users" 401
|
||||||
|
|
||||||
|
# 4. gendsgn.ru/api/v1/admin/* отдаёт 401 анониму (auth gate стоит ДО роутинга
|
||||||
|
# в FastAPI — конкретный путь неважен, любой /api/v1/admin/* перехватывается
|
||||||
|
# на уровне Caddy до бэкенда).
|
||||||
|
check "gendsgn.ru/api/v1/admin/* — 401 anonymous" "$BASE_MAIN/api/v1/admin/users" 401
|
||||||
|
|
||||||
|
# 5. Домены-спутники ведут на канонический (301, без следования редиректу —
|
||||||
|
# curl без -L, поэтому ждём именно код редиректа, а не 200 конечной страницы).
|
||||||
|
# Как и проверки 1-2, требуют DNS + выпущенного сертификата.
|
||||||
|
check "merahome.ru — 301 to canonical" "https://merahome.ru/" 301
|
||||||
|
check "meraotsenka.ru — 301 to canonical" "https://meraotsenka.ru/" 301
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
if [ "$fail" -eq 0 ]; then
|
||||||
|
echo "ALL CHECKS PASSED"
|
||||||
|
else
|
||||||
|
echo "SOME CHECKS FAILED — see FAIL lines above"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "$fail"
|
||||||
Loading…
Add table
Reference in a new issue