From d065fc31ddd4201cb3ad3e95befaf32fab172fe8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Fri, 9 Oct 2026 20:29:39 +0400 Subject: [PATCH] =?UTF-8?q?fix(web):=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(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vitest зафиксирован точно на 3.1.4: в зеркале npm банка нет 3.2.x, lock-файл не в git, каретка давала бы разные версии снаружи и внутри контура; - DevLogin больше не выбирает форму по NODE_ENV: платформа всегда собирает фронт в production, а контур работает на dev-заглушке аутентификации, переменные сборки в пайплайне задать нельзя. Форма входа по email показывается всегда; заглушка «Вход выполняется через РАИСА» — только после 404 «Not Found» от бэкенда (маршрута /dev/token нет → dev-эмитент выключен). Признак вынесен в isDevIssuerDisabled и покрыт тестами: 404 «пользователь не найден» остаётся ошибкой в форме. Co-Authored-By: Claude Fable 5.1 --- web/package.json | 2 +- web/src/auth/DevLogin.tsx | 31 ++++++++++++++++++++++--------- web/src/auth/devIssuer.test.ts | 20 ++++++++++++++++++++ web/src/auth/devIssuer.ts | 10 ++++++++++ 4 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 web/src/auth/devIssuer.test.ts create mode 100644 web/src/auth/devIssuer.ts diff --git a/web/package.json b/web/package.json index e6f4175..dfb4f18 100644 --- a/web/package.json +++ b/web/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/web/src/auth/DevLogin.tsx b/web/src/auth/DevLogin.tsx index ef5a76d..b6b6724 100644 --- a/web/src/auth/DevLogin.tsx +++ b/web/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/web/src/auth/devIssuer.test.ts b/web/src/auth/devIssuer.test.ts new file mode 100644 index 0000000..f666e3c --- /dev/null +++ b/web/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/web/src/auth/devIssuer.ts b/web/src/auth/devIssuer.ts new file mode 100644 index 0000000..fe0b126 --- /dev/null +++ b/web/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'; +}