From 6582333def7bd0fb3f0b7674e516f32261ecba07 Mon Sep 17 00:00:00 2001 From: nns Date: Sun, 4 Oct 2026 15:41:50 +0500 Subject: [PATCH] =?UTF-8?q?feat(s30):=20QuickLoginBlock=20+=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B2=20Log?= =?UTF-8?q?inPage=20(=D1=88=D0=B0=D0=B3=205/9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Над обычной формой логина теперь висит жёлтый блок «Быстрый вход (тестовый стенд)»: клик по карточке → моментальный логин через мастер-пароль 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 --- docs/quick-login-progress.md | 2 +- .../src/components/QuickLoginBlock.tsx | 122 ++++++++++++++++++ src/food-market.web/src/pages/LoginPage.tsx | 36 +++++- 3 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 src/food-market.web/src/components/QuickLoginBlock.tsx diff --git a/docs/quick-login-progress.md b/docs/quick-login-progress.md index c1a88dc..e860a9e 100644 --- a/docs/quick-login-progress.md +++ b/docs/quick-login-progress.md @@ -28,7 +28,7 @@ существующий `DemoTenantSeeder`) + сотрудники с ролями Admin / Cashier / Storekeeper (`demo-admin@food-market.local`, `demo-cashier@...`, `demo-keeper@...`). Запускается только при Enabled=true. -- [ ] 5. **Страница входа (web)** — если `GET .../accounts` вернул 200 — над +- [x] 5. **Страница входа (web)** — если `GET .../accounts` вернул 200 — над обычной формой блок «Быстрый вход (тестовый стенд)» с карточками + поиск. Клик — моментальный логин через мастер-пароль. На проде блока нет. - [ ] 6. **Плашка «ТЕСТОВЫЙ СТЕНД»** — при Enabled=true жёлтая полоса diff --git a/src/food-market.web/src/components/QuickLoginBlock.tsx b/src/food-market.web/src/components/QuickLoginBlock.tsx new file mode 100644 index 0000000..987af09 --- /dev/null +++ b/src/food-market.web/src/components/QuickLoginBlock.tsx @@ -0,0 +1,122 @@ +import { useEffect, useState } from 'react' +import { api } from '@/lib/api' + +/// Карточка демо-аккаунта с полей, которые отдаёт /api/dev/quick-login/accounts. +export interface QuickLoginAccount { + email: string + fullName: string + role: string | null + organizationName: string | null + isSuperAdmin: boolean + isDemo: boolean +} + +interface Props { + /** Вызывается когда пользователь кликнул по карточке. Email → затем + * LoginPage сам делает login(email, '123456'). */ + onPick(email: string): void +} + +/** + * Блок «Быстрый вход (тестовый стенд)» над формой логина. Показывается + * ТОЛЬКО когда /api/dev/quick-login/accounts вернул 200. На проде этот + * эндпоинт отдаёт 404 — компонент после первого запроса рендерит null, + * ничего не светится. + */ +export function QuickLoginBlock({ onPick }: Props) { + const [accounts, setAccounts] = useState(null) + const [loading, setLoading] = useState(true) + const [q, setQ] = useState('') + + useEffect(() => { + let cancelled = false + api + .get('/api/dev/quick-login/accounts') + .then((r) => { if (!cancelled) setAccounts(r.data) }) + .catch(() => { if (!cancelled) setAccounts(null) }) + .finally(() => { if (!cancelled) setLoading(false) }) + return () => { cancelled = true } + }, []) + + // Отдельный запрос при изменении строки поиска — чтобы серверный ?q= + // работал и всегда отражал серверные ограничения (топ-50, порядок). + useEffect(() => { + if (!accounts) return // не инициализировано — не дёргаемся + if (q === '') return // первая загрузка уже без ?q + const t = setTimeout(() => { + api.get(`/api/dev/quick-login/accounts?q=${encodeURIComponent(q)}`) + .then((r) => setAccounts(r.data)) + .catch(() => { /* молча — не ломаем страницу */ }) + }, 200) + return () => clearTimeout(t) + // Намеренно не включаем accounts в deps — он меняется из этого же эффекта. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [q]) + + if (loading) return null + if (!accounts) return null // 404 или ошибка — на проде ничего не рендерим + + return ( +
+
+
+

+ Быстрый вход (тестовый стенд) +

+

+ Кликните по аккаунту — пароль подставится автоматически. +

+
+
+ + setQ(e.target.value)} + placeholder="Поиск: email, ФИО, роль, магазин" + className="w-full rounded-md border border-amber-300 dark:border-amber-700 bg-white dark:bg-amber-950/60 px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-amber-400" + /> + + {accounts.length === 0 && ( +

+ Ничего не нашлось. Попробуйте очистить фильтр. +

+ )} + +
+ {accounts.map((a) => ( + + ))} +
+
+ ) +} diff --git a/src/food-market.web/src/pages/LoginPage.tsx b/src/food-market.web/src/pages/LoginPage.tsx index 278415e..c44076d 100644 --- a/src/food-market.web/src/pages/LoginPage.tsx +++ b/src/food-market.web/src/pages/LoginPage.tsx @@ -3,8 +3,14 @@ import { useNavigate, useLocation } from 'react-router-dom' import { login } from '@/lib/auth' import { api } from '@/lib/api' import { Logo } from '@/components/Logo' +import { QuickLoginBlock } from '@/components/QuickLoginBlock' import { validateEmail, messages } from '@/lib/validation' +/** Мастер-пароль «быстрого входа» — должен совпадать с server-side + * QuickLoginOptions.MasterPassword. Используется только на стенде; + * в проде блок быстрого входа не показывается, поэтому значение не утекает. */ +const QUICK_LOGIN_MASTER_PASSWORD = '123456' + interface MeResp { roles: string[] } export function LoginPage() { @@ -45,8 +51,36 @@ export function LoginPage() { } } + /** Клик по карточке «быстрого входа»: подставляем email + мастер-пароль + * и сразу шлём /connect/token. На проде этот путь недоступен — блок + * QuickLoginBlock рендерится только после 200 от GET /accounts, которого + * в проде нет (404). */ + async function handleQuickLogin(pickedEmail: string) { + setEmail(pickedEmail) + setPassword(QUICK_LOGIN_MASTER_PASSWORD) + setEmailErr(null); setPasswordErr(null) + setLoading(true) + setError(null) + try { + await login(pickedEmail, QUICK_LOGIN_MASTER_PASSWORD) + let target = from + try { + const me = (await api.get('/api/me')).data + if (me.roles?.includes('SuperAdmin') && (from === '/' || from === '/dashboard')) { + target = '/super-admin' + } + } catch { /* fallback на from */ } + navigate(target, { replace: true }) + } catch (err) { + setError(err instanceof Error ? err.message : 'Ошибка входа') + } finally { + setLoading(false) + } + } + return ( -
+
+