Compare commits
No commits in common. "feat/mera-b2c-perimeter" and "main" have entirely different histories.
feat/mera-
...
main
6 changed files with 0 additions and 295 deletions
|
|
@ -1,38 +0,0 @@
|
||||||
# Регресс-тест публичного B2C-периметра МЕРА (ЭТАП 1 плана B2C-запуска).
|
|
||||||
#
|
|
||||||
# НЕ pre-merge гейт — эти 4 проверки требуют реального DNS + выпущенного TLS-
|
|
||||||
# сертификата для mera.gendsgn.ru, т.е. осмысленны ТОЛЬКО против прода после
|
|
||||||
# деплоя. Запускается вручную (workflow_dispatch) или раз в сутки (cron) —
|
|
||||||
# страхует от случайной регрессии периметра (например, будущий PR по ошибке
|
|
||||||
# открывает B2B-путь на публичном поддомене, или basic_auth gate на gendsgn.ru
|
|
||||||
# случайно снимают).
|
|
||||||
#
|
|
||||||
# ДО того как появится DNS A-record mera.gendsgn.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
|
|
||||||
62
Caddyfile
62
Caddyfile
|
|
@ -135,68 +135,6 @@ 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 mera.gendsgn.ru → IP VPS — ТРЕБУЕТСЯ ДО того, как сюда придёт
|
|
||||||
# реальный трафик. Если записи ещё нет на момент деплоя этого блока: `caddy
|
|
||||||
# reload`/`up -d --force-recreate caddy` в deploy.yml НЕ падает (конфиг
|
|
||||||
# синтаксически валиден, ошибка сертификата асинхронна и per-hostname) — Caddy
|
|
||||||
# просто залогирует неудачную попытку ACME-выпуска для mera.gendsgn.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).
|
|
||||||
mera.gendsgn.ru {
|
|
||||||
encode zstd gzip
|
|
||||||
|
|
||||||
log {
|
|
||||||
output file /var/log/caddy/mera.gendsgn.ru.log
|
|
||||||
}
|
|
||||||
|
|
||||||
# Единственная публичная страница этого этапа — заглушка "скоро".
|
|
||||||
# rewrite добавляет basePath-префикс только для Caddy→backend хопа.
|
|
||||||
handle / {
|
|
||||||
rewrite * /trade-in/mera-public
|
|
||||||
reverse_proxy tradein-frontend:3000
|
|
||||||
}
|
|
||||||
|
|
||||||
# Next.js уже эмитит ссылки на статику с /trade-in-префиксом (тот же
|
|
||||||
# basePath) — passthrough без rewrite. Нужны для рендера страницы (JS/CSS
|
|
||||||
# чанки), сами по себе не содержат ни B2B-данных, ни секретов.
|
|
||||||
handle /trade-in/_next/* {
|
|
||||||
reverse_proxy tradein-frontend:3000
|
|
||||||
}
|
|
||||||
|
|
||||||
# Allowlist-by-default: любой другой путь (включая B2B — /v2, /admin,
|
|
||||||
# /scrapers/*, /trade-in/api/*, /history, ...) — 404, НЕ проксируется.
|
|
||||||
handle {
|
|
||||||
respond 404
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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).
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Регресс-тест публичного B2C-периметра МЕРА (ЭТАП 1 плана B2C-запуска).
|
|
||||||
#
|
|
||||||
# Проверяет 4 инварианта периметра (см. корневой Caddyfile):
|
|
||||||
# 1. mera.gendsgn.ru отдаёт 200 анонимно (публичная заглушка).
|
|
||||||
# 2. mera.gendsgn.ru/v2 (B2B-путь) отдаёт 404 — allowlist-by-default,
|
|
||||||
# НЕ был случайно проброшен на B2B-дерево tradein-frontend.
|
|
||||||
# 3. gendsgn.ru/trade-in/v2 по-прежнему отдаёт 401 анониму (basic_auth gate
|
|
||||||
# не тронут этим этапом).
|
|
||||||
# 4. gendsgn.ru/api/v1/admin/* отдаёт 401 анониму (тот же gate).
|
|
||||||
#
|
|
||||||
# ВАЖНО: проверки 1 и 2 требуют, чтобы DNS A-record mera.gendsgn.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://mera.gendsgn.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 "mera.gendsgn.ru root — public 200" "$BASE_MERA/" 200
|
|
||||||
|
|
||||||
# 2. B2B-путь на публичном поддомене — 404 (allowlist-by-default), не 200/401.
|
|
||||||
check "mera.gendsgn.ru/v2 — B2B path must 404" "$BASE_MERA/v2" 404
|
|
||||||
|
|
||||||
# 3. gendsgn.ru/trade-in/v2 по-прежнему закрыт basic_auth анониму.
|
|
||||||
check "gendsgn.ru/trade-in/v2 — still 401 anonymous" "$BASE_MAIN/trade-in/v2" 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
|
|
||||||
|
|
||||||
echo "========================================"
|
|
||||||
if [ "$fail" -eq 0 ]; then
|
|
||||||
echo "ALL CHECKS PASSED"
|
|
||||||
else
|
|
||||||
echo "SOME CHECKS FAILED — see FAIL lines above"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit "$fail"
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import type { Metadata } from "next";
|
|
||||||
import { IBM_Plex_Mono, Manrope } from "next/font/google";
|
|
||||||
|
|
||||||
import { pageBg } from "@/components/trade-in/v2/tokens";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ЭТАП 1 плана B2C-запуска МЕРА: публичный периметр БЕЗ функционала.
|
|
||||||
*
|
|
||||||
* Обслуживается ОТДЕЛЬНЫМ поддоменом mera.gendsgn.ru (см. корневой Caddyfile,
|
|
||||||
* site-блок `mera.gendsgn.ru { ... }`) — не напрямую по basePath. Caddy на
|
|
||||||
* этом поддомене rewrite'ит запрос корня "/" в "/trade-in/mera-public" (тот
|
|
||||||
* же образ tradein-frontend, что обслуживает и gendsgn.ru/trade-in/*, у него
|
|
||||||
* запечён NEXT_PUBLIC_BASE_PATH=/trade-in) — пользователь префикс /trade-in
|
|
||||||
* никогда не видит, это внутренний Caddy→backend hop.
|
|
||||||
*
|
|
||||||
* Изоляция от B2B-дерева (намеренная, см. задачу ЭТАП 1):
|
|
||||||
* - НЕ импортирует SupportChatProvider (тот живёт только в app/v2/layout.tsx —
|
|
||||||
* поддержка нужна оценщикам с доступом, не анонимным посетителям заглушки).
|
|
||||||
* - RouteGuard (components/auth/RouteGuard.tsx) содержит явный безусловный
|
|
||||||
* bypass именно для пути "/mera-public" — страница рендерится сразу, без
|
|
||||||
* ожидания /api/v1/me и без экрана "нет доступа" (в отличие от /ui-preview,
|
|
||||||
* чей bypass живёт только под dev/CI-флагом ENABLE_PREVIEW, этот — always-on,
|
|
||||||
* т.к. страница публична по продуктовому решению, а не временный QA-артефакт).
|
|
||||||
* - page.tsx — server component без хуков (useMe/useQuota/useHistory не
|
|
||||||
* импортируются вообще); функционала на этом этапе нет.
|
|
||||||
*
|
|
||||||
* robots: noindex — прецедент app/ui-preview/layout.tsx. На ЭТАПЕ 1 страница
|
|
||||||
* ещё не должна попадать в поисковую индексацию.
|
|
||||||
*/
|
|
||||||
const manrope = Manrope({
|
|
||||||
subsets: ["latin", "cyrillic"],
|
|
||||||
weight: ["400", "500", "600", "700"],
|
|
||||||
variable: "--font-manrope",
|
|
||||||
display: "swap",
|
|
||||||
});
|
|
||||||
|
|
||||||
const plexMono = IBM_Plex_Mono({
|
|
||||||
subsets: ["latin", "cyrillic"],
|
|
||||||
weight: ["400", "500"],
|
|
||||||
variable: "--font-plex-mono",
|
|
||||||
display: "swap",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "МЕРА — оценка вторичного жилья",
|
|
||||||
description:
|
|
||||||
"Онлайн-оценка стоимости квартиры на вторичном рынке. Сервис скоро откроется.",
|
|
||||||
robots: { index: false, follow: false },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function MeraPublicLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${manrope.variable} ${plexMono.variable}`}
|
|
||||||
style={{
|
|
||||||
minHeight: "100vh",
|
|
||||||
background: pageBg,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import { tokens } from "@/components/trade-in/v2/tokens";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ЭТАП 1 B2C-плана — публичная заглушка, БЕЗ функционала (никаких форм,
|
|
||||||
* запросов к API, авторизации). Server component намеренно: нет "use client",
|
|
||||||
* нет хуков — useMe/useQuota/useHistory здесь не используются вообще.
|
|
||||||
* Изоляция от RouteGuard/B2B-дерева — см. комментарий в layout.tsx.
|
|
||||||
*/
|
|
||||||
export default function MeraPublicPage() {
|
|
||||||
return (
|
|
||||||
<main
|
|
||||||
style={{
|
|
||||||
fontFamily: tokens.font.sans,
|
|
||||||
color: tokens.ink,
|
|
||||||
textAlign: "center",
|
|
||||||
padding: "48px 24px",
|
|
||||||
maxWidth: 560,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontFamily: tokens.font.mono,
|
|
||||||
fontSize: 13,
|
|
||||||
letterSpacing: "0.08em",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
color: tokens.muted,
|
|
||||||
marginBottom: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
МЕРА
|
|
||||||
</div>
|
|
||||||
<h1
|
|
||||||
style={{
|
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: 600,
|
|
||||||
marginBottom: 12,
|
|
||||||
color: tokens.ink2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Оценка квартиры на вторичном рынке
|
|
||||||
</h1>
|
|
||||||
<p style={{ fontSize: 16, color: tokens.body, lineHeight: 1.5 }}>
|
|
||||||
Сервис скоро откроется для всех. Мы готовим публичный запуск — загляните
|
|
||||||
позже.
|
|
||||||
</p>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -22,11 +22,6 @@ import { useMe } from "@/lib/useMe";
|
||||||
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||||
// #801: dev/CI-only preview-маршрут (a11y/lighthouse) рендерится оффлайн без RBAC.
|
// #801: dev/CI-only preview-маршрут (a11y/lighthouse) рендерится оффлайн без RBAC.
|
||||||
const ENABLE_PREVIEW = process.env.NEXT_PUBLIC_ENABLE_PREVIEW === "1";
|
const ENABLE_PREVIEW = process.env.NEXT_PUBLIC_ENABLE_PREVIEW === "1";
|
||||||
// ЭТАП 1 B2C-плана МЕРА: публичный периметр (mera.gendsgn.ru → rewrite на
|
|
||||||
// /trade-in/mera-public, см. Caddyfile). В отличие от ENABLE_PREVIEW выше —
|
|
||||||
// ЭТОТ bypass ВСЕГДА включён (в т.ч. в проде): страница публична по
|
|
||||||
// продуктовому решению, а не временный QA-артефакт.
|
|
||||||
const PUBLIC_PATHS = ["/mera-public"];
|
|
||||||
|
|
||||||
interface RouteGuardProps {
|
interface RouteGuardProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -47,15 +42,6 @@ export function RouteGuard({ children }: RouteGuardProps) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ЭТАП 1 B2C: публичная заглушка МЕРА не ждёт /api/v1/me и не проверяет
|
|
||||||
// RBAC — рендерится сразу для анонимного посетителя. useMe() выше уже
|
|
||||||
// вызван (Rules of Hooks — нельзя условно), но его результат здесь
|
|
||||||
// игнорируется: фоновый запрос к /me (скорее всего 401 без сессии) ни на
|
|
||||||
// что не влияет, страница не зависит от него — как и /ui-preview выше.
|
|
||||||
if (PUBLIC_PATHS.some((p) => rawPath === p || rawPath.startsWith(`${p}/`))) {
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) return null;
|
if (isLoading) return null;
|
||||||
|
|
||||||
if (error instanceof HTTPError && error.status === 401) {
|
if (error instanceof HTTPError && error.status === 401) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue