# Quick Login — быстрый вход на тестовом стенде **Статус:** включено на `https://test.admin.food-market.kz` (стенд). **Прод:** `https://admin.food-market.kz` — выключено, попытки активации принудительно блокируются safety-override'ом в коде. Это dev-удобство: на стенде не нужно каждый раз вводить логин+пароль. Выбираешь карточку аккаунта на `/login`, клик — ты внутри. ## Доступ к стенду Стенд `test.admin.food-market.kz` для публичного интернета закрыт HTTP basic-auth на host-уровне nginx (логин `food-market`). Из внутренней сети `192.168.1.x` (dev-vm, prod-vm) basic-auth не требуется — e2e, quality-watchdog и обычная разработка работают как раньше. Пароль basic-auth = пароль quick-login (чтобы не плодить секреты). Хранится на dev-vm в файле `~/.fm-watchdog/stage-shared-password` (mode 600, не коммитится). ## Как это работает ### Server-side Опции в `appsettings.json` / env: ```yaml QuickLogin: Enabled: false # default — выключено MasterPassword: "" # default пустой; реальный секрет — ТОЛЬКО в stage-env ``` На стенде env в `~/food-market-stage/deploy/.env`: ``` QUICK_LOGIN_ENABLED=true QUICK_LOGIN_MASTER_PASSWORD=<значение из ~/.fm-watchdog/stage-shared-password> ``` Пароль прокидывается в контейнер build'ом из файла и в репозиторий не попадает. Прод-compose этой переменной не содержит, флаг остаётся false. Логика срабатывает в двух местах: - **`POST /connect/token` (grant=password)** — при `EffectivelyEnabled=true` принимает ДВА варианта пароля наравне с собственным паролем юзера: 1. **MasterPassword** (значение env'а) — для ручного/скриптового входа, когда у тебя есть сам пароль. Константное сравнение (`FixedTimeEquals`). 2. **Sentinel** — публичная строка `__QUICK_LOGIN_PWLESS__`, которую шлёт SPA при клике по карточке. Сервер трактует её как «пустить этого юзера». Это позволяет SPA не хранить реальный пароль в JS-бандле. И тот и другой путь гейтованы `EffectivelyEnabled` — вне стенда оба отфутболиваются как любой другой чужой пароль. - **`GET /api/dev/quick-login/accounts`** — анонимный эндпоинт, который возвращает «человеческие» аккаунты для карточек: SuperAdmin → demo-trio → остальные. Автотестовые логины отсеиваются по regex-патéрнам (всё с суффиксом из 10+ цифр или prefix cat31-/visual-/factory-smoke-/mt6*/ quality-/...). Поиск `?q=` фильтрует по email/ФИО/роли/магазину. На проде эндпоинт отдаёт 404 — наличие пути не палит окружение. ### Client-side (SPA) - `` над формой логина сам опрашивает `/api/dev/quick- login/accounts`. 200 → рендерит жёлтый блок с карточками + поиск. 404 → возвращает `null`, никакого UI не рисует. - Клик по карточке → `POST /connect/token` с паролем **sentinel** `__QUICK_LOGIN_PWLESS__`. Реальный мастер-пароль в бандл не попадает — ротация пароля в env не требует rebuild'а веба. - `` жёлтой полосой сверху всех страниц, когда эндпоинт доступен. При том же сигнале динамически добавляет ``. Один web-билд используется и в проде, и на стенде — секрет на проде не утекает: `/api/dev/quick-login/accounts` там 404, значит блока нет, а sentinel сервером всё равно отвергается. ## Безопасность — предохранители 1. **Enabled=false по умолчанию.** Нужна явная env-переменная, чтобы включить. В прод-compose её нет и не должно быть. 2. **Issuer safety-override.** В `QuickLoginOptions.EffectivelyEnabled`: если `OpenIddict:Issuer` указывает на `admin.food-market.kz` (ТОЧНО, без поддоменов), быстрый вход **принудительно выключен**. Защита от ошибки конфигурации — поставили `QUICK_LOGIN_ENABLED=true` в прод-env по ошибке, но Issuer остался прод-овым → ничего не включилось, в лог Warning. 3. **Пустой или кривой Issuer тоже отбивает.** Не рискуем в неизвестном окружении. 4. **Пустой MasterPassword отбивает мастер-пароль.** Пустая строка не матчится в `IsMasterPassword` (отбивается до FixedTimeEquals). Это защищает от случайно разворачиваемого стенда без секрета. 5. **Rate-limit не ослаблен.** Sentinel и master-password идут через ту же защиту, что обычный password-grant. 6. **Неактивные пользователи не пускаются.** Проверка `IsActive` сверху `/connect/token` — ни sentinel, ни master-password это не обходят. 7. **Startup Warning.** Program.cs логирует одно решение на старте: «включён для стенда» или «Enabled=true, но безопасность выключила». 8. **Basic auth на host-nginx.** Внешние запросы к `test.admin.food- market.kz` отбиваются 401 без корректных basic-auth creds. Внутренняя сеть исключена из этого gate'а (чтобы автоматика работала). Этот уровень живёт за пределами репозитория — в gateway nginx на prod-vm. ## Демо-аккаунты `QuickLoginDemoSeeder` при старте с `EffectivelyEnabled=true` поднимает: - Организацию **«Демо-магазин»** (идемпотентно). - Полный demo-каталог через `DemoTenantSeeder` (50 товаров / 30 продаж / приёмки / контрагенты). Если демо-каталог не наполнился (нет HTTP- контекста при startup → `Tenant not set`), юзеры всё равно создаются. - Трио учёток с ролями: - `demo-admin@food-market.local` → `Admin` - `demo-cashier@food-market.local` → `Cashier` - `demo-keeper@food-market.local` → `Storekeeper` Собственный пароль у них `Admin12345!` (унаследовано от шаблона), но sentinel и master-password тоже работают. ## Как ротирнуть секрет-пароль 1. На dev-vm обновить `~/.fm-watchdog/stage-shared-password` (mode 600). 2. На prod-vm обновить `~/food-market-stage/deploy/.env`: `QUICK_LOGIN_MASTER_PASSWORD=<новое значение>`. Удобно без эха: ```bash # с dev-vm scp ~/.fm-watchdog/stage-shared-password nns@192.168.1.190:/tmp/.ql-pw ssh nns@192.168.1.190 'python3 -c " import pathlib, re pw = pathlib.Path(\"/tmp/.ql-pw\").read_text().strip() env = pathlib.Path(\"/home/nns/food-market-stage/deploy/.env\") env.write_text(re.sub(r\"^QUICK_LOGIN_MASTER_PASSWORD=.*$\", f\"QUICK_LOGIN_MASTER_PASSWORD={pw}\", env.read_text(), flags=re.M))" \ && rm /tmp/.ql-pw && grep -c QUICK_LOGIN_MASTER_PASSWORD \ ~/food-market-stage/deploy/.env' ``` 3. На prod-vm также обновить basic-auth htpasswd для `test.admin.food- market.kz` в nginx — тот же пароль с юзером `food-market`. 4. Рестартнуть API: `ssh nns@192.168.1.190 "cd ~/food-market-stage/deploy && docker compose -p food-market-stage up -d --force-recreate api"`. 5. Веб rebuild **не нужен** — sentinel не меняется. ## Как выключить обратно ```bash # на prod-vm nns@192.168.1.190:$ sed -i '/^QUICK_LOGIN_ENABLED=/d; /^QUICK_LOGIN_MASTER_PASSWORD=/d' \ ~/food-market-stage/deploy/.env nns@192.168.1.190:$ cd ~/food-market-stage/deploy && docker compose -p food-market-stage up -d --force-recreate api ``` API перестартует, `EffectivelyEnabled` увидит `Enabled=false`, эндпоинт снова 404, sentinel и master-password не работают. Карточки на `/login` исчезнут — `QuickLoginBlock` увидит 404 и вернёт `null`. ## Проверить что прод не задет ```bash curl -s -o /dev/null -w "%{http_code}\n" \ https://admin.food-market.kz/api/dev/quick-login/accounts # ожидается: 404 (или 502, если прод-API вообще не поднят) ``` Любой `200` — проблема. ## Файлы | Что | Где | |---|---| | Опции + safety-override + sentinel-константа | `src/food-market.api/Infrastructure/QuickLoginOptions.cs` | | Master-password + sentinel в token-grant | `src/food-market.api/Controllers/AuthorizationController.cs` | | Список аккаунтов | `src/food-market.api/Controllers/QuickLoginController.cs` | | Демо-сид | `src/food-market.api/Seed/QuickLoginDemoSeeder.cs` | | Чистка автотестов | `src/food-market.api/Background/HousekeepingJobs.cs` + `deploy/stage-prune-autotest.sql` | | UI блок (sentinel-flow) | `src/food-market.web/src/pages/LoginPage.tsx` + `src/food-market.web/src/components/QuickLoginBlock.tsx` | | Плашка стенда | `src/food-market.web/src/components/StageBanner.tsx` | | Integration-тесты | `tests/food-market.IntegrationTests/QuickLoginTests.cs` | | E2E-сценарий | `tests/e2e/scenarios/stage-quick-login.{yml,steps.ts}` | | Секрет-пароль (на dev-vm) | `~/.fm-watchdog/stage-shared-password` (вне репо) | ## История - **2026-10-04 (Sprint 30, часть 1):** первоначальный ввод на тестовый стенд. - **2026-10-04 (Sprint 30, часть 2):** секрет-пароль вынесен в `~/.fm- watchdog/stage-shared-password` (вне репо); SPA перешла на sentinel — реальный пароль в JS-бандл больше не попадает; стенд закрыт basic-auth на host-nginx для внешнего интернета, внутренние 192.168.1.x бьют без пароля.