Merge pull request 'Обратный поток из контура: vitest 3.1.4 и форма dev-входа в production-сборке (#24)' (#25) from feat/24-contour-dev-login into test
Reviewed-on: #25
This commit is contained in:
commit
fc75edfded
@ -39,7 +39,7 @@ SPA кредитного аналитика АРМ КА: справочники
|
|||||||
- `src/api/client.ts` также даёт `apiFetchBlob(path) → {blob, filename}` для бинарных ответов (скан PDF, экспорт xlsx) с той же обработкой токена/401/ошибок.
|
- `src/api/client.ts` также даёт `apiFetchBlob(path) → {blob, filename}` для бинарных ответов (скан PDF, экспорт xlsx) с той же обработкой токена/401/ошибок.
|
||||||
- `src/services/api/` — функции по ресурсам arm-core (consolidations, files, documents, osv, directories/…).
|
- `src/services/api/` — функции по ресурсам arm-core (consolidations, files, documents, osv, directories/…).
|
||||||
- `src/utils/` — чистые функции без React/store/API: деньги (`money.ts`, суммы — строки), подписи статусов/типов/периодов (`labels.ts`), скачивание и имя файла (`download.ts`).
|
- `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/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/` — примитивы
|
- `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.
|
(`GenericTable`, `TableTag`, `PageTitle`), `src/types/` — DTO API.
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
"daisyui": "^4.12.10",
|
"daisyui": "^4.12.10",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vitest": "^3.2.7"
|
"vitest": "3.1.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@biomejs/cli-linux-x64-musl": "^1.9.4",
|
"@biomejs/cli-linux-x64-musl": "^1.9.4",
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import { Alert, Button, Form, Input } from 'antd';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import type { DevTokenResponse } from '../api/types';
|
import type { DevTokenResponse } from '../api/types';
|
||||||
|
import { isDevIssuerDisabled } from './devIssuer';
|
||||||
import { setToken } from './token';
|
import { setToken } from './token';
|
||||||
|
|
||||||
type Props = { onLogin: () => void; error?: string | null };
|
type Props = { onLogin: () => void; error?: string | null };
|
||||||
|
|
||||||
// Вход по email для разработки: бэкенд выпускает токен только при AUTH_DEV_ISSUER=true и DEBUG=true.
|
// Вход по email для разработки и контура без SSO: бэкенд выпускает токен только при AUTH_DEV_ISSUER=true и DEBUG=true.
|
||||||
function DevLoginForm({ onLogin, error }: Props) {
|
// `onDisabled` — бэкенд ответил, что маршрута /dev/token нет (dev-эмитент выключен): вход возможен только через SSO.
|
||||||
|
function DevLoginForm({ onLogin, error, onDisabled }: Props & { onDisabled: () => void }) {
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [failure, setFailure] = useState<string | null>(null);
|
const [failure, setFailure] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -19,6 +21,10 @@ function DevLoginForm({ onLogin, error }: Props) {
|
|||||||
setToken(access_token);
|
setToken(access_token);
|
||||||
onLogin();
|
onLogin();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
if (isDevIssuerDisabled(e)) {
|
||||||
|
onDisabled();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setFailure(e instanceof Error ? e.message : 'Не удалось выполнить вход');
|
setFailure(e instanceof Error ? e.message : 'Не удалось выполнить вход');
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
@ -41,8 +47,8 @@ function DevLoginForm({ onLogin, error }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Production: вход выполняет внешний SSO; если загрузка профиля не удалась, показываем причину и даём повторить запрос
|
// Dev-эмитент выключен: вход выполняет внешний SSO; если загрузка профиля не удалась, показываем причину и даём повторить запрос
|
||||||
function ProdNotice({ onLogin, error }: Props) {
|
function SsoNotice({ onLogin, error }: Props) {
|
||||||
return (
|
return (
|
||||||
<div style={{ width: 360 }}>
|
<div style={{ width: 360 }}>
|
||||||
<p>Вход выполняется через РАИСА</p>
|
<p>Вход выполняется через РАИСА</p>
|
||||||
@ -54,15 +60,22 @@ function ProdNotice({ onLogin, error }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Какую форму показать, решает не режим сборки, а бэкенд во время выполнения: платформа всегда собирает фронт
|
||||||
|
* в production, при этом контур может работать на dev-заглушке аутентификации (переменные сборки в пайплайне
|
||||||
|
* задать нельзя). Поэтому форма входа по email показывается всегда, а заглушка про SSO — только после того,
|
||||||
|
* как бэкенд ответил, что маршрута /dev/token нет.
|
||||||
|
*/
|
||||||
export function DevLogin(props: Props) {
|
export function DevLogin(props: Props) {
|
||||||
|
const [ssoOnly, setSsoOnly] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100svh' }}>
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100svh' }}>
|
||||||
{/* Литерал process.env.NODE_ENV rsbuild подставляет при сборке: в production форма вырезается из бандла целиком */}
|
{ssoOnly ? (
|
||||||
{process.env.NODE_ENV !== 'production' ? (
|
// TODO(OQ-54): параметры JWT РАИСА (издатель, claim с email) и сам сценарий входа через SSO
|
||||||
<DevLoginForm {...props} />
|
<SsoNotice {...props} />
|
||||||
) : (
|
) : (
|
||||||
// TODO(OQ): параметры JWT РАИСА (издатель, claim с email)
|
<DevLoginForm {...props} onDisabled={() => setSsoOnly(true)} />
|
||||||
<ProdNotice {...props} />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
20
src/auth/devIssuer.test.ts
Normal file
20
src/auth/devIssuer.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
10
src/auth/devIssuer.ts
Normal file
10
src/auth/devIssuer.ts
Normal file
@ -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';
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user