From 822f758571e1fd80bb9a286b1915558d5dcb7eec Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Fri, 9 Oct 2026 20:29:42 +0400 Subject: [PATCH] =?UTF-8?q?fix(auth):=20=D0=BE=D0=B1=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BA=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BA=D0=BE=D0=BD=D1=82=D1=83=D1=80=D0=B0=20=E2=80=94?= =?UTF-8?q?=20vitest=203.1.4,=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=20dev-?= =?UTF-8?q?=D0=B2=D1=85=D0=BE=D0=B4=D0=B0=20=D0=B2=20=D0=BB=D1=8E=D0=B1?= =?UTF-8?q?=D0=BE=D0=B9=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B5=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зеркало arm-core #92: vitest зафиксирован на 3.1.4 (в зеркале npm банка нет 3.2.x); DevLogin не зависит от NODE_ENV — форма входа по email показывается всегда, заглушка «Вход выполняется через РАИСА» появляется после 404 «Not Found» от бэкенда (dev-эмитент выключен). Признак вынесен в isDevIssuerDisabled и покрыт тестами. CLAUDE.md: описание DevLogin обновлено. Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 2 +- package.json | 2 +- src/auth/DevLogin.tsx | 31 ++++++++++++++++++++++--------- src/auth/devIssuer.test.ts | 20 ++++++++++++++++++++ src/auth/devIssuer.ts | 10 ++++++++++ 5 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 src/auth/devIssuer.test.ts create mode 100644 src/auth/devIssuer.ts diff --git a/CLAUDE.md b/CLAUDE.md index adc2d99..54f0321 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,7 +39,7 @@ SPA кредитного аналитика АРМ КА: справочники - `src/api/client.ts` также даёт `apiFetchBlob(path) → {blob, filename}` для бинарных ответов (скан PDF, экспорт xlsx) с той же обработкой токена/401/ошибок. - `src/services/api/` — функции по ресурсам arm-core (consolidations, files, documents, osv, directories/…). - `src/utils/` — чистые функции без React/store/API: деньги (`money.ts`, суммы — строки), подписи статусов/типов/периодов (`labels.ts`), скачивание и имя файла (`download.ts`). -- `src/auth/` — `AuthGate` (профиль `/me`), `DevLogin` (`POST /dev/token`, только не в production), `token.ts`. +- `src/auth/` — `AuthGate` (профиль `/me`), `DevLogin` (`POST /dev/token`; форма показывается в любой сборке, заглушка «вход через РАИСА» — после 404 «Not Found» от бэкенда, то есть когда dev-эмитент выключен), `token.ts`. - `src/store/` — Redux Toolkit: `store.ts`, слайсы `companyGroupsSlice` и `consolidations` (+ `thunks.ts`). - `src/layouts/` — страницы (маршруты в `src/App.tsx`, в т.ч. `/documents/:document_id` и `/groups/:group_id/versions/:version_id/osv`), `src/widgets/` — блоки экранов (`DocumentReview` — проверка ББ/ОФР/ОСВ со сканом, `ConsolidatedOsv` — сводная ОСВ, `FilesDrawer` — файлы ЮЛ), `src/components/` — примитивы (`GenericTable`, `TableTag`, `PageTitle`), `src/types/` — DTO API. diff --git a/package.json b/package.json index e6f4175..dfb4f18 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "daisyui": "^4.12.10", "tailwindcss": "^3.4.13", "typescript": "^5.7.3", - "vitest": "^3.2.7" + "vitest": "3.1.4" }, "optionalDependencies": { "@biomejs/cli-linux-x64-musl": "^1.9.4", diff --git a/src/auth/DevLogin.tsx b/src/auth/DevLogin.tsx index ef5a76d..b6b6724 100644 --- a/src/auth/DevLogin.tsx +++ b/src/auth/DevLogin.tsx @@ -2,12 +2,14 @@ import { Alert, Button, Form, Input } from 'antd'; import { useState } from 'react'; import { api } from '../api/client'; import type { DevTokenResponse } from '../api/types'; +import { isDevIssuerDisabled } from './devIssuer'; import { setToken } from './token'; type Props = { onLogin: () => void; error?: string | null }; -// Вход по email для разработки: бэкенд выпускает токен только при AUTH_DEV_ISSUER=true и DEBUG=true. -function DevLoginForm({ onLogin, error }: Props) { +// Вход по email для разработки и контура без SSO: бэкенд выпускает токен только при AUTH_DEV_ISSUER=true и DEBUG=true. +// `onDisabled` — бэкенд ответил, что маршрута /dev/token нет (dev-эмитент выключен): вход возможен только через SSO. +function DevLoginForm({ onLogin, error, onDisabled }: Props & { onDisabled: () => void }) { const [submitting, setSubmitting] = useState(false); const [failure, setFailure] = useState(null); @@ -19,6 +21,10 @@ function DevLoginForm({ onLogin, error }: Props) { setToken(access_token); onLogin(); } catch (e: unknown) { + if (isDevIssuerDisabled(e)) { + onDisabled(); + return; + } setFailure(e instanceof Error ? e.message : 'Не удалось выполнить вход'); } finally { setSubmitting(false); @@ -41,8 +47,8 @@ function DevLoginForm({ onLogin, error }: Props) { ); } -// Production: вход выполняет внешний SSO; если загрузка профиля не удалась, показываем причину и даём повторить запрос -function ProdNotice({ onLogin, error }: Props) { +// Dev-эмитент выключен: вход выполняет внешний SSO; если загрузка профиля не удалась, показываем причину и даём повторить запрос +function SsoNotice({ onLogin, error }: Props) { return (

Вход выполняется через РАИСА

@@ -54,15 +60,22 @@ function ProdNotice({ onLogin, error }: Props) { ); } +/** + * Какую форму показать, решает не режим сборки, а бэкенд во время выполнения: платформа всегда собирает фронт + * в production, при этом контур может работать на dev-заглушке аутентификации (переменные сборки в пайплайне + * задать нельзя). Поэтому форма входа по email показывается всегда, а заглушка про SSO — только после того, + * как бэкенд ответил, что маршрута /dev/token нет. + */ export function DevLogin(props: Props) { + const [ssoOnly, setSsoOnly] = useState(false); + return (
- {/* Литерал process.env.NODE_ENV rsbuild подставляет при сборке: в production форма вырезается из бандла целиком */} - {process.env.NODE_ENV !== 'production' ? ( - + {ssoOnly ? ( + // TODO(OQ-54): параметры JWT РАИСА (издатель, claim с email) и сам сценарий входа через SSO + ) : ( - // TODO(OQ): параметры JWT РАИСА (издатель, claim с email) - + setSsoOnly(true)} /> )}
); diff --git a/src/auth/devIssuer.test.ts b/src/auth/devIssuer.test.ts new file mode 100644 index 0000000..f666e3c --- /dev/null +++ b/src/auth/devIssuer.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import { ApiError } from '../api/client'; +import { isDevIssuerDisabled } from './devIssuer'; + +describe('isDevIssuerDisabled', () => { + it('404 «Not Found» без кода — маршрута /dev/token нет, dev-эмитент выключен', () => { + expect(isDevIssuerDisabled(new ApiError(404, undefined, 'Not Found', { detail: 'Not Found' }))).toBe(true); + }); + + it('404 «пользователь не найден или отключён» — маршрут есть, это ошибка входа', () => { + expect(isDevIssuerDisabled(new ApiError(404, undefined, 'пользователь не найден или отключён'))).toBe(false); + }); + + it('другие статусы и не-ApiError не считаются выключенным эмитентом', () => { + expect(isDevIssuerDisabled(new ApiError(409, undefined, 'неоднозначная учётная запись'))).toBe(false); + expect(isDevIssuerDisabled(new ApiError(0, 'NETWORK', 'Нет связи с сервером'))).toBe(false); + expect(isDevIssuerDisabled(new Error('Not Found'))).toBe(false); + expect(isDevIssuerDisabled(undefined)).toBe(false); + }); +}); diff --git a/src/auth/devIssuer.ts b/src/auth/devIssuer.ts new file mode 100644 index 0000000..fe0b126 --- /dev/null +++ b/src/auth/devIssuer.ts @@ -0,0 +1,10 @@ +import { ApiError } from '../api/client'; + +/** + * Маршрут `POST /dev/token` существует только при `DEBUG=true` и `AUTH_DEV_ISSUER=true` на бэкенде; иначе приложение + * отвечает 404 со строкой «Not Found» (а не доменной ошибкой с кодом). Это и есть признак «dev-эмитент выключен, + * вход только через SSO». 404 «пользователь не найден или отключён» приходит с того же статуса, но от самого маршрута. + */ +export function isDevIssuerDisabled(error: unknown): boolean { + return error instanceof ApiError && error.status === 404 && error.code === undefined && error.message === 'Not Found'; +}