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
|
||||
}
|
||||
|
||||
# МЕРА 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).
|
||||
# Auto-TLS Let's Encrypt. CORS уже включён на стороне CouchDB через bootstrap
|
||||
# (см. 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 ?? "";
|
||||
// #801: dev/CI-only preview-маршрут (a11y/lighthouse) рендерится оффлайн без RBAC.
|
||||
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 {
|
||||
children: React.ReactNode;
|
||||
|
|
@ -47,15 +42,6 @@ export function RouteGuard({ children }: RouteGuardProps) {
|
|||
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 (error instanceof HTTPError && error.status === 401) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue