Координатор отротировал мастер-пароль стенда: теперь значение живёт в
~/.fm-watchdog/stage-shared-password на dev-vm (не коммитится) и в
QUICK_LOGIN_MASTER_PASSWORD в stage-env на prod-vm. Старый дефолт '123456'
на обоих сторонах убран — он мог вводить в заблуждение и был записан в
бандл.
Клиент-серверный контракт:
- Server (AuthorizationController.IsMasterPassword) принимает ДВА пароля
как мастер-ключ: (a) QuickLoginOptions.MasterPassword (константное
сравнение, FixedTimeEquals); (b) публичный sentinel
QuickLoginOptions.SentinelPassword = '__QUICK_LOGIN_PWLESS__'. Оба
гейтованы EffectivelyEnabled — вне стенда отбиваются как любой чужой
пароль.
- SPA (LoginPage.handleQuickLogin) при клике карточки шлёт sentinel в
поле password. Реальный пароль в JS-бандл больше не попадает; ротация
секрета больше не требует rebuild'а веба.
- Форму мы НЕ наполняем sentinel'ом — поле password остаётся пустым,
чтобы ни одно значение не светилось в DOM.
Default QuickLoginOptions.MasterPassword = "" (пустая строка не
матчится в IsMasterPassword). deploy/docker-compose.yml дефолт
QUICK_LOGIN_MASTER_PASSWORD тоже пустой.
Тесты:
- integration (3/3 ok) — добавлены sentinel-кейсы: пропускает активного
при Enabled=true, отвергается при Enabled=false. Admin12345!-санити-
логин в Disabled-тесте перенесён вперёд, чтобы снять lockout-счётчик
от соседей по коллекции (Identity lockoutOnFailure=true).
- e2e stage-quick-login (4/4 ok) — ql02 шлёт sentinel вместо '123456'
(симулирует реальный SPA flow).
docs/quick-login.md:
- без упоминания реального пароля; хранилище — ~/.fm-watchdog/
stage-shared-password; ротация-рецепт через python читает файл
в-процессе (не экспонирует значение в ps/логи).
- раздел «Доступ к стенду» — host-nginx basic-auth для внешнего
интернета, 192.168.1.x бьёт без неё.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
E2E stage-quick-login финально 4/4 green. ql03 проверяет инвариант
безопасности «прод НЕ отдаёт список»: любой ответ ≠ 200 ok, 404 — норма,
502 — пре-существующий outage прод-API (отмечается как gap в отчёте).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Integration (QuickLoginTests.cs):
- Options_effective_matrix: Enabled=false → off; пустой/мусорный Issuer → off;
Issuer=admin.food-market.kz → off (прод); любой другой host (включая
test.admin.*) → on.
- Disabled_returns_404_and_rejects_master_password: default-factory
(Enabled=false) /accounts → 404, master-пароль → 400 invalid_grant,
обычный Admin12345! → 200 (sanity check).
- Enabled_at_runtime_...: мутируем IOptions<QuickLoginOptions>.Value +
IConfiguration в живом сервис-провайдере (так как WithWebHostBuilder на
уже-построенной parent-factory даёт ObjectDisposedException). Проверяем:
/accounts → 200 + SuperAdmin в списке; master-пароль пускает активного
SuperAdmin'а; НЕ пускает деактивированного; смена Issuer на прод
переключает всё в safety-off.
Также фикс в QuickLoginOptions: прод-хост теперь СТРОГО 'admin.food-market.kz'
без suffix-match (раньше test.admin.food-market.kz тоже считался прод-ом).
E2E (tests/e2e/scenarios/stage-quick-login.{yml,steps.ts}): 4 шага —
GET /accounts на стенде → 200 + демо-trio; master-пароль пускает
demo-cashier → /api/me role=Cashier; GET на прод → 404; Playwright: клик
по карточке demo-cashier → редирект с /login + нет ссылки /products
в sidebar (меню кассира).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
HousekeepingJobs.PruneAutoTestEntitiesAsync — Hangfire-recurring (02:45 UTC
daily, Cron:PruneAutoTestEntities). Удаляет:
- orgs по regex autotest-паттерна в имени (quality-, soak-, UI-a11y-,
SmokeOrgA/B-, PerfBaseline-, Reg-, RT-ratetest-, Probe Org, cat3*,
visual-, factory-smoke-, mt6*, doc4*, auth2*, rep5*, rt8*, wiz9*,
i18n*, twofa*, iso*, mob1*, axdet-, load-, s19-test- + любое имя
с [0-9]{10,}) старше 3 дней + все их таблицы с OrganizationId;
- orphan autotest-юзеров (без удалённой org) по regex в email и доменам
@e.kz / @test-fm.local.
Защищены: SuperAdmin (OrganizationId IS NULL), admin@food-market.local,
admin@demo-market.local, demo-*@food-market.local, market@email.com,
«Demo Market» и «Демо-магазин» — явные исключения.
deploy/stage-prune-autotest.sql — ровно та же логика как SQL-скрипт для
ручного запуска. Прогнал на stage: было 8653 user'ов / 8634 orgs →
стало 300 users / 305 orgs. SuperAdmin/demo/market остались.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
StageBanner — жёлтая полоса сверху всех страниц, когда /api/dev/
quick-login/accounts отвечает 200 (стенд). В проде эндпоинт 404 →
компонент возвращает null, никакая полоса не рисуется.
Решение «stage vs prod» кэшируется в sessionStorage, чтобы не стрелять
запрос на каждой навигации в SPA (один запрос за открытие вкладки).
На стенде динамически добавляется <meta name=robots content=noindex,
nofollow> — не хотим чтобы Google индексировал test.admin.food-market.kz.
Один web-билд ходит в оба окружения; никаких отдельных index.html.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Над обычной формой логина теперь висит жёлтый блок «Быстрый вход
(тестовый стенд)»: клик по карточке → моментальный логин через
мастер-пароль 123456 (сохранён как константа в LoginPage.tsx — должен
совпадать с server-side QuickLoginOptions.MasterPassword).
Блок сам опрашивает /api/dev/quick-login/accounts: 200 → рендерится,
404 (= прод) → возвращает null, НИЧЕГО не рисует. Так одним и тем же
web-билдом покрываются и стенд, и прод — секрета в UI нет.
Поиск: ?q= на сервере (debounced 200ms), фильтрует по email/ФИО/роли/
магазину. Карточки с badge SuperAdmin / Демо. Lazy-рендер, max-h-80
с overflow-y чтобы не ломать компоновку.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hosted-сервис, который на старте, когда EffectivelyEnabled=true, создаёт:
- организацию «Демо-магазин» (отдельная от «Demo Market» DevDataSeeder'а,
чтобы не ломать e2e, завязанные на DemoMarket);
- tenant references через существующий SeedTenantReferencesAsync (единицы,
магазин, PriceType, системные роли);
- каталог демо-товаров через DemoTenantSeeder (50 товаров + 30 продаж +
приёмки/контрагенты) — идемпотентно, маркер DEMO-*;
- три учётки с ролями Admin/Cashier/Storekeeper:
demo-admin@food-market.localdemo-cashier@food-market.localdemo-keeper@food-market.local
(пароль Admin12345!, но мастер-пароль 123456 тоже подойдёт — через
QuickLoginOptions в AuthorizationController).
На проде (EffectivelyEnabled=false) сервис тихо выходит в StartAsync —
ничего не трогает, ничего не логирует.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Анонимный эндпоинт, который ВИДЕН только когда QuickLogin эффективно
включён (EffectivelyEnabled=true). В проде отдаёт 404, чтобы по наличию
эндпоинта нельзя было вычислить среду.
Фильтрация:
- IsActive=false → не показываем;
- автотестовые (prefix cat31-/visual-/factory-/mt6*/doc4*/auth2*/rep5*/
rt8*/wiz9*/quality-/i18n*/twofa*/a11y-/mob14-/mob15-/iso*/axdet-/
smoke-/ratetest-/perf-/probe-/load-/s19-test-/admin-/cashier-/
keeper-) → не показываем;
- email@e.kz, @test-fm.local — служебные домены diag-probes, тоже скрыты;
- длинный числовой суффикс [0-9]{10,} в email → автотест.
Порядок: SuperAdmin → demo → остальные (ThenBy email для стабильности).
Лимит 50 + поиск ?q по email/full-name/org-name/role (case-insensitive).
Карточка: email, FullName, роль (SuperAdmin / EmployeeRole.Name),
название org, флаги IsSuperAdmin/IsDemo для сортировки/отображения.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AuthorizationController теперь после провала обычного CheckPasswordSignIn
даёт второй шанс через QuickLoginOptions.IsMasterPassword — FixedTimeEquals,
чтобы не светить длину. Включается только когда EffectivelyEnabled()=true
(т.е. Enabled=true И Issuer не prod).
Что сохранено:
- неактивные пользователи отшиваются до пароля (user.IsActive сверху);
- 2FA / org-check — после master-password пути ВСЁ ЕЩЁ работают;
- rate-limit (UseAuthFormUsernameKey + UseRateLimiter) отрабатывает ДО
нас, никак не ослабляется;
- собственный пароль продолжает работать — не ломает автотесты и seeded
admin'ов, которые используют Admin12345!.
Warning в лог при каждом срабатывании — чтобы в прод-логах (если кто-то
по ошибке активировал) было видно.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dev-only флаг «быстрого входа». Enabled=false по умолчанию, подхватывает
QuickLogin:Enabled и QuickLogin:MasterPassword из конфига (планируется
проброс через env в stage-compose).
EffectivelyEnabled() — жёсткий предохранитель: даже с Enabled=true, если
OpenIddict:Issuer указывает на admin.food-market.kz (прод) — считаем
выключенным. Пустой Issuer тоже отбивает — не рискуем в неизвестном окружении.
Program.cs логирует Warning на старте: включили → «QuickLogin включён для
стенда», Enabled=true но прод → «не активирован», Enabled=false — молчим.
Чтобы ошибка конфигурации всегда была видна в логах прод-инстанса.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>