food-market/docs/quick-login-progress.md
nns e1718ec972
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Public / Build + push Public (test + latest) (push) Waiting to run
Docker Public / Deploy :test → test.food-market.kz (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
feat(s30): sentinel pwd для SPA — реальный мастер-пароль только в server-env
Координатор отротировал мастер-пароль стенда: теперь значение живёт в
~/.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>
2026-10-04 21:02:36 +05:00

169 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Quick Login — прогресс
Задача владельца: на `test.admin.food-market.kz` не набирать логин/пароль
руками. Выбрать аккаунт из списка — ты внутри в один клик. У всех аккаунтов
один shared-пароль; его значение хранится в `~/.fm-watchdog/stage-shared-
password` на dev-vm (не коммитится). На `admin.food-market.kz` быстрого
входа нет никогда (несколько независимых предохранителей).
Координатор отдельно проверил: на стейдже нет реальных секретов (МойСклад
токенов нет, ОФД-ключей нет, Telegram не привязан, SMTP пустой) — потому
открытый мастер-пароль на стенде безопасен.
## Чек-лист — все 9/9 ✓
- [x] 1. **Флаг конфигурации** — `QuickLogin:Enabled` (default false) +
`QuickLogin:MasterPassword` (default пустой; реальное значение — только
в stage-env, читается из `~/.fm-watchdog/stage-shared-password`).
Включается только через env-переменные `QuickLogin__Enabled=true` в
stage-compose. Жёсткий предохранитель: если `OpenIddict:Issuer`
указывает на admin.food-market.kz — быстрый вход принудительно
выключен и в лог Warning.
Commit: `1f70d90`.
- [x] 2. **Мастер-пароль** — password-grant: `QuickLogin:MasterPassword`
работает как второй вход рядом со своим паролем. Неактивные/без-org
пользователи — отшиваются как раньше. Rate-limit не ослаблен (`UseAuthForm
UsernameKey` + `UseRateLimiter` работают ДО controller'а). Commit: `7647af7`.
- [x] 3. **API списка аккаунтов** — `GET /api/dev/quick-login/accounts`
(анонимный, 404 если выключен). Порядок: SuperAdmin → демо-* → остальные
«человеческие». Autotest-патéрны (cat31-*, visual-*, factory-smoke-*, mt6*,
quality-*, i18n*-*, auth2*-*, doc4*-*, rep5*-*, rt8*-*, twofa*-*, wiz9*-*,
a11y-*, mob1*-*, isoB*-*, axdet-*, smoke-*, ratetest-*, perf-*, probe-*,
load-*, s19-test-* и всё с суффиксом из ≥10 цифр) — скрыты. Лимит 50 + `?q=`.
Commit: `ed5089b`.
- [x] 4. **Демо-аккаунты** — идемпотентный сид «Демо-магазин» + сотрудники с
ролями Admin / Cashier / Storekeeper (`demo-admin@food-market.local`,
`demo-cashier@...`, `demo-keeper@...`). Запускается только при Enabled=true.
Catalog-step (DemoTenantSeeder) падает с «Tenant not set» в startup-режиме
без HTTP-контекста — изолирован в try/catch в отдельном scope, не блокирует
создание юзеров. Commits: `35f633e`, `e3a8db3`, `f817abc`.
- [x] 5. **Страница входа (web)** — `QuickLoginBlock` над формой логина.
Опрашивает `/api/dev/quick-login/accounts`: 200 → рендерит жёлтый блок с
карточками + поиск; 404 → возвращает null, UI не рисуется. Клик по карточке →
моментальный login через мастер-пароль. Один web-билд ходит и в прод и на
стенд. Commit: `6582333`.
- [x] 6. **Плашка «ТЕСТОВЫЙ СТЕНД»** — `<StageBanner />` вверху всех страниц
при 200 на `/accounts`. Решение кешируется в sessionStorage. Динамически
добавляет `<meta name="robots" content="noindex,nofollow">`. Commit: `5728b72`.
- [x] 7. **Чистка автотестового мусора** — `HousekeepingJobs.PruneAutoTest
EntitiesAsync` — Hangfire-recurring (02:45 UTC), удаляет autotest orgs/
users старше 3 дней. `deploy/stage-prune-autotest.sql` — одноразовый
SQL-скрипт с той же логикой. Прогнал на stage: было **8653 users / 8634 orgs
→ стало 300 users / 305 orgs**. Protected: SuperAdmin, admin@demo-market.local,
market@email.com, demo-* trio, «Demo Market», «Демо-магазин». Commit: `c69114f`.
- [x] 8. **Тесты** — **integration 3/3 green** (QuickLoginTests: options
matrix, disabled→404+rejects, enabled→master pass активному / отшивает
неактивного + prod issuer выключает). **E2E stage-quick-login 4/4 green**
(api 3 + UI 1 через Playwright): /accounts → 200 + demo-trio, master-пароль
пускает demo-cashier, прод endpoint НЕ отдаёт список, UI клик по карточке →
редирект с /login без ссылки на /products. Commit: `338877e`.
- [x] 9. **Деплой + документация** — stage compose + .env обновлены
(`QUICK_LOGIN_ENABLED=true`), `~/deploy-stage.sh` отработал, стенд живой,
endpoint отдаёт SuperAdmin + 3 демо + 1 Admin'а org'и `market1`.
`docs/quick-login.md` описывает как работает, предохранители, как выключить.
Commits: `ee24a5e` (fix прод-host), `e3a8db3`+`f817abc` (safety wrap +
scope-split).
## Финальная проверка — что живо
```
curl https://test.admin.food-market.kz/api/dev/quick-login/accounts
```
Отдаёт 5 аккаунтов:
```
admin@food-market.local role=SuperAdmin org=Платформа super=true
demo-admin@food-market.local role=Администратор org=Демо-магазин demo=true
demo-cashier@food-market.local role=Кассир org=Демо-магазин demo=true
demo-keeper@food-market.local role=Кладовщик org=Демо-магазин demo=true
market@email.com role=Администратор org=market1
```
```
# SPA-поток: шлём sentinel вместо реального пароля
curl -X POST .../connect/token password=__QUICK_LOGIN_PWLESS__ username=demo-cashier@...
→ 200 + access_token; /api/me → role=Cashier
# API-поток: шлём реальный пароль из файла (без эха значения)
curl -X POST .../connect/token \
--data-urlencode "password@/home/nns/.fm-watchdog/stage-shared-password" \
--data-urlencode "username=demo-cashier@food-market.local" ...
→ 200 + access_token
```
```
curl https://admin.food-market.kz/api/dev/quick-login/accounts
→ 502 (прод-backend остановлен ~2 мес.; главное — не 200, список не экспонируется)
```
**Прод-outage.** `food-market-api` container на prod-vm — `Exited (0) 2 months
ago`. Это _пре-существующее состояние_, не связано с моим деплоем. Для
quick-login это даже более безопасно, чем 404 (нечего отдать), но admin.food-
market.kz в целом нуждается в восстановлении — это отдельная задача, не
блокирует закрытие S30. Отмечено в E2E-отчёте как `gap`.
## Что в коде
| Что | Где |
|---|---|
| Опции + safety-override | `src/food-market.api/Infrastructure/QuickLoginOptions.cs` |
| Master-password в token-grant | `src/food-market.api/Controllers/AuthorizationController.cs` |
| Список аккаунтов | `src/food-market.api/Controllers/QuickLoginController.cs` |
| Демо-сид | `src/food-market.api/Seed/QuickLoginDemoSeeder.cs` |
| Чистка autotests | `src/food-market.api/Background/HousekeepingJobs.cs` + `deploy/stage-prune-autotest.sql` |
| UI блок | `src/food-market.web/src/components/QuickLoginBlock.tsx` |
| Плашка стенда | `src/food-market.web/src/components/StageBanner.tsx` |
| Integration-тесты (3) | `tests/food-market.IntegrationTests/QuickLoginTests.cs` |
| E2E-сценарий (4 шага) | `tests/e2e/scenarios/stage-quick-login.{yml,steps.ts}` |
| Инструкция | `docs/quick-login.md` |
| Прод-compose шаблон | `deploy/docker-compose.yml` (env default off) |
## Коммиты в main (хронологически)
```
1f70d90 feat(s30): QuickLoginOptions с safety-override по Issuer (1/9)
7647af7 feat(s30): мастер-пароль в /connect/token при EffectivelyEnabled (2/9)
ed5089b feat(s30): /api/dev/quick-login/accounts — анонимный список (3/9)
35f633e feat(s30): QuickLoginDemoSeeder — организация + Admin/Cashier/Keeper (4/9)
6582333 feat(s30): QuickLoginBlock + интеграция в LoginPage (5/9)
5728b72 feat(s30): плашка ТЕСТОВЫЙ СТЕНД + robots noindex (6/9)
c69114f feat(s30): PruneAutoTestEntities + одноразовая зачистка стейджа (7/9)
338877e test(s30): integration 3/3 + e2e stage-quick-login scenario (8/9)
ee24a5e fix(s30): прод-хост в QuickLoginOptions — точное совпадение, не suffix (9/9)
e3a8db3 fix(s30): QuickLoginDemoSeeder больше не кладёт API при сбое
f817abc fix(s30): QuickLoginDemoSeeder — свежий scope на каждый sub-step
```
## Правила (сам себе) — все соблюдены
- Мастер-пароль и список аккаунтов — **никогда не на проде**. Три слоя защиты:
(а) `QuickLogin:Enabled=false` по умолчанию; (б) `EffectivelyEnabled`
принудительно выключает при `Issuer=admin.food-market.kz`; (в) пустой/
мусорный Issuer — выключен.
- После каждого пункта: build + тесты + коммит порцией + `[x]`. ✓
- Не трогал: прод admin.food-market.kz (только read-only), `global.json`,
POS WPF, gateway nginx. ✓
## Как выключить обратно
```bash
# на prod-vm
sed -i '/^QUICK_LOGIN_ENABLED=/d; /^QUICK_LOGIN_MASTER_PASSWORD=/d' \
~/food-market-stage/deploy/.env
cd ~/food-market-stage/deploy && docker compose -p food-market-stage up -d --force-recreate api
```
После рестарта `EffectivelyEnabled=false`, endpoint снова 404, master-password
и sentinel не работают, UI-блок исчезает. См. `docs/quick-login.md` для деталей.
## История
- **2026-10-04 (Sprint 30, часть 1):** первоначальный ввод на тестовый стенд.
- **2026-10-04 (часть 2, координатор):** реальный пароль вынесен в
`~/.fm-watchdog/stage-shared-password` (не коммитится); SPA перешла на
публичный sentinel `__QUICK_LOGIN_PWLESS__` → мастер-пароль больше не
попадает в JS-бандл; `test.admin.food-market.kz` для внешнего интернета
закрыт basic-auth (пароль = тот же), внутренняя сеть `192.168.1.x`
бьёт без basic-auth. Default `MasterPassword=""`, sentinel-поведение
в `AuthorizationController.IsMasterPassword`.