Страницы аутенти��икации — полный код¶
LoginPage¶
Файл: frontend/src/pages/LoginPage.tsx
import { useState } from 'react';
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
import { Form, message } from 'antd';
import { Shirt } from 'lucide-react';
import { useAuthStore } from '../stores/auth';
import { authApi } from '../api/auth';
import { performInitialSync, registerRefreshSyncListener } from '../offline/services/initialSync';
import AuthShell from '../components/ui/AuthShell';
import authStyles from '../components/ui/AuthShell.module.css';
import TextField from '../components/ui/TextField';
import PrimaryButton from '../components/ui/PrimaryButton';
import SecondaryButton from '../components/ui/SecondaryButton';
export default function LoginPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [loading, setLoading] = useState(false);
const { setTokens, setUser } = useAuthStore();
// Целевая страница после успешного входа (из query-параметра redirect)
const redirectTarget = searchParams.get('redirect') || '/';
const doLogin = async (login: string, password: string) => {
setLoading(true);
try {
const data = await authApi.login({ login, password });
setTokens(data.access_token, data.refresh_token);
try {
const me = await authApi.getMe();
setUser(me);
} catch { /* профиль загрузится позже */ }
message.success('Добро пожаловать!');
// Запуск начальной синхронизации offline-данных
performInitialSync();
registerRefreshSyncListener();
navigate(decodeURIComponent(redirectTarget), { replace: true });
} catch {
message.error('Неверная почта или пароль');
} finally {
setLoading(false);
}
};
const onFinish = async (values: { login: string; password: string }) => {
await doLogin(values.login, values.password);
};
return (
<AuthShell
icon={<Shirt size={20} strokeWidth={2.1} />}
title="Вход в ваш гардероб"
description="Сохраняйте вещи, собирайте образы и возвращайтесь к ним в любой момент."
tone="warm"
footer={(
<div className={authStyles.footerCopy}>
<span>Нет аккаунта?</span>{' '}
<Link
className={authStyles.footerLink}
to={`/register${redirectTarget !== '/' ? `?redirect=${encodeURIComponent(redirectTarget)}` : ''}`}
>
Создать
</Link>
</div>
)}
>
<Form layout="vertical" onFinish={onFinish} requiredMark={false}>
<TextField
name="login"
label="Почта"
placeholder="you@example.com"
type="email"
autoComplete="email"
rules={[
{ required: true, message: 'Введите почту' },
{ type: 'email', message: 'Введите корректный email' },
]}
/>
<TextField
name="password"
label="Пароль"
placeholder="Пароль"
autoComplete="current-password"
password
rules={[{ required: true, message: 'Введите пароль' }]}
formItemProps={{ style: { marginBottom: 16 } }}
/>
<Form.Item style={{ marginBottom: 0 }}>
<PrimaryButton htmlType="submit" block disabled={loading}>
Войти
</PrimaryButton>
</Form.Item>
</Form>
{/* Кнопка для быстрого входа тестовым пользователем (для разработки/демонстрации) */}
<SecondaryButton
block
disabled={loading}
onClick={() => doLogin('dmitriy_e2e@plechiki.test', 'password123')}
style={{ marginTop: 12 }}
>
Войти как тестовый пользователь
</SecondaryButton>
</AuthShell>
);
}
RegisterPage¶
Файл: frontend/src/pages/RegisterPage.tsx
import { useState } from 'react';
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
import { Form, message } from 'antd';
import { Sparkles } from 'lucide-react';
import { useAuthStore } from '../stores/auth';
import { authApi } from '../api/auth';
import AuthShell from '../components/ui/AuthShell';
import authStyles from '../components/ui/AuthShell.module.css';
import TextField from '../components/ui/TextField';
import PrimaryButton from '../components/ui/PrimaryButton';
export default function RegisterPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [loading, setLoading] = useState(false);
const { setTokens, setUser } = useAuthStore();
const redirectTarget = searchParams.get('redirect') || '/';
const onFinish = async (values: { name: string; email: string; password: string }) => {
setLoading(true);
try {
const data = await authApi.register(values);
setTokens(data.access_token, data.refresh_token);
// TokenResponse не содержит пользователя — получаем профиль отдельным запросом
try {
const me = await authApi.getMe();
setUser(me);
} catch {
// Профиль может не успеть загрузиться; аккаунт уже создан
}
message.success('Аккаунт создан!');
navigate(decodeURIComponent(redirectTarget), { replace: true });
} catch {
message.error('Не удалось ��оздать аккаунт. Возможно, email уже занят.');
} finally {
setLoading(false);
}
};
return (
<AuthShell
icon={<Sparkles size={20} strokeWidth={2.1} />}
title="Создайте свой гардероб"
description="Добавляйте вещи, собирайте образы и делитесь ссылкой без лишних шагов."
tone="sage"
footer={(
<div className={authStyles.footerCopy}>
<span>Уже есть аккаунт?</span>{' '}
<Link
className={authStyles.footerLink}
to={`/login${redirectTarget !== '/' ? `?redirect=${encodeURIComponent(redirectTarget)}` : ''}`}
>
Войти
</Link>
</div>
)}
>
<Form layout="vertical" onFinish={onFinish} requiredMark={false}>
<TextField
name="name"
label="Имя"
placeholder="Ваше имя"
autoComplete="name"
rules={[{ required: true, message: 'Введите имя' }]}
/>
<TextField
name="email"
label="Email"
placeholder="email@example.com"
autoComplete="email"
rules={[
{ required: true, message: 'Введите email' },
{ type: 'email', message: 'Некорректный email' },
]}
/>
<TextField
name="password"
label="Пароль"
placeholder="Минимум 8 символов"
autoComplete="new-password"
password
rules={[
{ required: true, message: 'Введите пароль' },
{ min: 8, message: 'Минимум 8 символов' },
]}
formItemProps={{ style: { marginBottom: 16 } }}
/>
<Form.Item style={{ marginBottom: 0 }}>
<PrimaryButton htmlType="submit" block disabled={loading}>
Создать аккаунт
</PrimaryButton>
</Form.Item>
</Form>
</AuthShell>
);
}
Валидация форм¶
| Поле | Правила валидации |
|---|---|
| Email (login) | required, type: email |
| Password (login) | required |
| Name (register) | required |
| Email (register) | required, type: email |
| Password (register) | required, min: 8 символов |
Обработка ошибок¶
| Ошибка | Сообщение пользовател�� |
|---|---|
| Неверный логин/пароль | «Неверна�� почта или пароль» |
| Email уже занят | «Не удало��ь создать аккаунт. Возможно, email уже занят.» |
| Профиль не загрузился | Молча игнорируется (аккаунт уже создан) |