arm-core/web/src/auth/DevLogin.tsx
Andrey Epifancev d76ca570d8 fix(web): клиент — сетевые/не-JSON ошибки, ошибка входа в prod, мелочи (T13.1–T13.3, T13.5–T13.10)
apiFetch оборачивает сбой сети в ApiError(0, NETWORK) и не-JSON тело 2xx в ApiError(BAD_RESPONSE); AbortError пробрасывается как есть, пустое тело даёт undefined.
В production-ветке входа показываются ошибка загрузки профиля и кнопка «Повторить», форма dev-входа по-прежнему вырезается из бандла литералом process.env.NODE_ENV.
Мелочи: «Сессия истекла» при 401, trim и whitespace-правило для email, центрирование по 100svh, union-типы прав, срез хвостовых слэшей в API_BASE.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 00:39:59 +04:00

70 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Alert, Button, Form, Input } from 'antd';
import { useState } from 'react';
import { api } from '../api/client';
import type { DevTokenResponse } from '../api/types';
import { setToken } from './token';
type Props = { onLogin: () => void; error?: string | null };
// Вход по email для разработки: бэкенд выпускает токен только при AUTH_DEV_ISSUER=true и DEBUG=true.
function DevLoginForm({ onLogin, error }: Props) {
const [submitting, setSubmitting] = useState(false);
const [failure, setFailure] = useState<string | null>(null);
const submit = async ({ email }: { email: string }) => {
setSubmitting(true);
setFailure(null);
try {
const { access_token } = await api.post<DevTokenResponse>('/dev/token', { email: email.trim() });
setToken(access_token);
onLogin();
} catch (e: unknown) {
setFailure(e instanceof Error ? e.message : 'Не удалось выполнить вход');
} finally {
setSubmitting(false);
}
};
const shownError = failure ?? error ?? null;
return (
<Form<{ email: string }> layout='vertical' onFinish={submit} style={{ width: 360 }}>
<h3>Вход для разработки</h3>
{shownError && <Alert type='error' showIcon message={shownError} style={{ marginBottom: 16 }} />}
<Form.Item name='email' label='Email' rules={[{ required: true, whitespace: true, message: 'Укажите email' }]}>
<Input placeholder='admin@armka.local' autoFocus />
</Form.Item>
<Button type='primary' htmlType='submit' loading={submitting} block>
Войти (dev)
</Button>
</Form>
);
}
// Production: вход выполняет внешний SSO; если загрузка профиля не удалась, показываем причину и даём повторить запрос
function ProdNotice({ onLogin, error }: Props) {
return (
<div style={{ width: 360 }}>
<p>Вход выполняется через РАИСА</p>
{error && <Alert type='error' showIcon message={error} style={{ marginBottom: 16 }} />}
<Button onClick={onLogin} block>
Повторить
</Button>
</div>
);
}
export function DevLogin(props: Props) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100svh' }}>
{/* Литерал process.env.NODE_ENV rsbuild подставляет при сборке: в production форма вырезается из бандла целиком */}
{process.env.NODE_ENV !== 'production' ? (
<DevLoginForm {...props} />
) : (
// TODO(OQ): параметры JWT РАИСА (издатель, claim с email)
<ProdNotice {...props} />
)}
</div>
);
}