← Назад к вопросам

Как организовал работу по реализации авторизации?

2.3 Middle🔥 181 комментариев
#Soft Skills и рабочие процессы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Организация работы по реализации авторизации на фронтенде

Авторизация — критичная часть приложения, требующая надёжной архитектуры. Рассмотрю, как я организую эту работу в реальных проектах.

1. Архитектурное разделение — слои приложения

// frontend/
// ├── contexts/
// │   └── AuthContext.tsx         // Источник истины для auth state
// ├── hooks/
// │   ├── useAuth.ts              // Хук для доступа к auth
// │   └── useAuthGuard.ts         // Защита маршрутов
// ├── lib/
// │   └── api.ts                  // HTTP клиент с автоматичным токеном
// ├── types/
// │   └── auth.ts                 // TypeScript типы
// ├── api/
// │   └── auth.ts                 // API слой (login, logout, refresh)
// └── middleware/
//     └── auth.ts                 // Next.js middleware

// Слои зависимостей: presentation -> application -> domain
// Никакого API вызова в компонентах! Только через хуки и сервисы

2. Контекст как хранилище состояния авторизации

// contexts/AuthContext.tsx
import { createContext, useCallback, useEffect, useState } from "react";

interface AuthUser {
  id: string;
  email: string;
  name: string;
}

interface AuthContextType {
  user: AuthUser | null;
  isLoading: boolean;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
  refreshToken: () => Promise<void>;
}

export const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<AuthUser | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  // Восстановление сессии при загрузке приложения
  useEffect(() => {
    const restoreSession = async () => {
      try {
        const response = await fetch("/api/v1/auth/me", {
          credentials: "include", // Отправляем cookies
        });
        if (response.ok) {
          setUser(await response.json());
        }
      } catch (error) {
        console.error("Failed to restore session:", error);
      } finally {
        setIsLoading(false);
      }
    };

    restoreSession();
  }, []);

  const login = useCallback(async (email: string, password: string) => {
    setIsLoading(true);
    try {
      const response = await fetch("/api/v1/auth/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password }),
        credentials: "include", // Сохраняем cookies
      });

      if (!response.ok) throw new Error("Login failed");

      const userData = await response.json();
      setUser(userData);
    } finally {
      setIsLoading(false);
    }
  }, []);

  const logout = useCallback(async () => {
    setIsLoading(true);
    try {
      await fetch("/api/v1/auth/logout", {
        method: "POST",
        credentials: "include",
      });
      setUser(null);
    } finally {
      setIsLoading(false);
    }
  }, []);

  const refreshToken = useCallback(async () => {
    try {
      const response = await fetch("/api/v1/auth/refresh", {
        method: "POST",
        credentials: "include",
      });
      if (response.ok) {
        setUser(await response.json());
      }
    } catch (error) {
      setUser(null);
    }
  }, []);

  return (
    <AuthContext.Provider value={{ user, isLoading, isAuthenticated: !!user, login, logout, refreshToken }}>
      {children}
    </AuthContext.Provider>
  );
}

3. Кастомный хук для удобного доступа к авторизации

// hooks/useAuth.ts
import { useContext } from "react";
import { AuthContext } from "@/contexts/AuthContext";

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return context;
}

// Использование в компонентах
function UserProfile() {
  const { user, isLoading, logout } = useAuth();

  if (isLoading) return <div>Loading...</div>;
  if (!user) return <div>Not authenticated</div>;

  return (
    <div>
      <p>Welcome, {user.name}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

4. HTTP клиент с автоматической обработкой токенов

// lib/api.ts
const API_BASE = process.env.NEXT_PUBLIC_API_URL;
const TOKEN_REFRESH_ENDPOINT = "/api/v1/auth/refresh";
let isRefreshing = false;
let failedQueue: Array<(token: string) => void> = [];

function processQueue(token: string) {
  failedQueue.forEach(prom => prom(token));
  failedQueue = [];
  isRefreshing = false;
}

export async function fetchAPI(url: string, options: RequestInit = {}) {
  const response = await fetch(`${API_BASE}${url}`, {
    credentials: "include",
    ...options,
    headers: {
      "Content-Type": "application/json",
      ...options.headers,
    },
  });

  // Обработка 401 — пытаемся обновить токен
  if (response.status === 401) {
    if (!isRefreshing) {
      isRefreshing = true;
      try {
        const refreshResponse = await fetch(`${API_BASE}${TOKEN_REFRESH_ENDPOINT}`, {
          method: "POST",
          credentials: "include",
        });
        if (refreshResponse.ok) {
          processQueue("");
          // Повторяем исходный запрос
          return fetchAPI(url, options);
        }
      } catch (error) {
        failedQueue = [];
        // Редирект на логин
        window.location.href = "/login";
      }
    } else {
      return new Promise(resolve => {
        failedQueue.push(() => {
          resolve(fetchAPI(url, options));
        });
      });
    }
  }

  return response;
}

5. Защита маршрутов через middleware в Next.js

// middleware.ts
import { NextRequest, NextResponse } from "next/server";

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const publicRoutes = ["/login", "/register", "/forgot-password"];

  // Проверяем наличие сессии (через cookies)
  const sessionCookie = request.cookies.get("session");
  const isPublicRoute = publicRoutes.some(route => pathname.startsWith(route));

  // Если есть токен и пытаемся попасть на логин — редирект на главную
  if (sessionCookie && isPublicRoute) {
    return NextResponse.redirect(new URL("/", request.url));
  }

  // Если нет токена и маршрут защищённый — редирект на логин
  if (!sessionCookie && !isPublicRoute && pathname !== "/") {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

6. Организация работы — жизненный цикл разработки

// Этап 1: Планирование
// - Изучаю требования: какой тип auth (cookie, JWT, OAuth)?
// - Обсуждаю с бэком API контракты

// Этап 2: Подготовка
// 1. Создаю типы (auth.ts)
// 2. Создаю API слой (api/auth.ts)
// 3. Создаю контекст (contexts/AuthContext.tsx)
// 4. Создаю хуки (hooks/useAuth.ts)

// Этап 3: Страницы и компоненты
// 1. Страница логина (pages/login.tsx)
// 2. Страница регистрации (pages/register.tsx)
// 3. Protected компоненты (используют useAuth())

// Этап 4: Тестирование
// - Unit тесты для хуков
// - E2E тесты для полного флоу
// - Проверка refresh token логики

7. Обработка ошибок без дублирования

// Ошибки обрабатываются на границе (API слой) и не логируются в компонентах
// Правило: каждая ошибка логируется один раз

// api/auth.ts
export async function login(email: string, password: string) {
  const response = await fetchAPI("/auth/login", {
    method: "POST",
    body: JSON.stringify({ email, password }),
  });

  if (!response.ok) {
    const error = await response.json();
    // Логируем ЗДЕСЬ и выбрасываем ошибку
    console.error("Login failed:", error);
    throw new Error(error.detail || "Login failed");
  }

  return response.json();
}

// Компонент просто ловит ошибку, не логирует
function LoginForm() {
  const handleSubmit = async (e) => {
    try {
      await login(email, password);
      // success
    } catch (error) {
      // Ошибка уже залогирована в API слое
      // Компонент только показывает сообщение пользователю
      setError(error.message);
    }
  };
}

Рекомендация

Ключевая организация:

  1. AuthContext — источник истины
  2. useAuth() — доступ из компонентов
  3. fetchAPI() — автоматическая обработка токенов
  4. API слой — вся бизнес-логика auth
  5. Middleware — защита маршрутов
  6. Один логирует — ошибка логируется в API слое, не в компонентах