Перейти к содержанию

Страницы аутенти��икации — полный код

Аутентификация

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 уже занят.»
Профиль не загрузился Молча игнорируется (аккаунт уже создан)