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
70 lines
2.7 KiB
TypeScript
70 lines
2.7 KiB
TypeScript
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>
|
||
);
|
||
}
|