349 lines
13 KiB
TypeScript
349 lines
13 KiB
TypeScript
"use client";
|
||
|
||
import { createContext, useContext, useState, useEffect, ReactNode, useRef } from "react";
|
||
import { useRouter, usePathname } from "next/navigation";
|
||
import { authApi } from "./api";
|
||
|
||
// Интерфейс пользователя
|
||
export interface User {
|
||
id: number;
|
||
email: string;
|
||
first_name: string;
|
||
last_name: string;
|
||
is_admin: boolean;
|
||
}
|
||
|
||
// Интерфейс ответа API с данными пользователя
|
||
interface UserApiResponse {
|
||
user: {
|
||
id: number;
|
||
email: string;
|
||
first_name: string;
|
||
last_name: string;
|
||
is_admin: boolean;
|
||
[key: string]: any; // Для других полей, которые могут быть в ответе
|
||
};
|
||
[key: string]: any; // Для других полей, которые могут быть в ответе
|
||
}
|
||
|
||
// Интерфейс контекста аутентификации
|
||
interface AuthContextType {
|
||
user: User | null;
|
||
loading: boolean;
|
||
isAdmin: boolean;
|
||
authChecked: boolean;
|
||
login: (email: string, password: string) => Promise<boolean>;
|
||
logout: () => void;
|
||
}
|
||
|
||
// Создание контекста аутентификации
|
||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||
|
||
// Глобальная переменная для отслеживания состояния аутентификации между рендерами
|
||
let globalAuthState = {
|
||
isAuthenticated: false,
|
||
isAdmin: false,
|
||
redirectAttempted: false
|
||
};
|
||
|
||
// Провайдер аутентификации
|
||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||
const [user, setUser] = useState<User | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [authChecked, setAuthChecked] = useState(false);
|
||
const router = useRouter();
|
||
|
||
// Проверка аутентификации при загрузке
|
||
useEffect(() => {
|
||
const checkAuth = async () => {
|
||
try {
|
||
const token = localStorage.getItem("token");
|
||
|
||
if (token) {
|
||
const response = await authApi.getProfile();
|
||
console.log("Проверка аутентификации:", response);
|
||
|
||
if (response.data && typeof response.data === 'object' && response.data !== null && 'user' in response.data) {
|
||
// Приводим ответ к нужному типу
|
||
const apiResponse = response.data as UserApiResponse;
|
||
|
||
// Создаем объект пользователя
|
||
const userData: User = {
|
||
id: apiResponse.user.id,
|
||
email: apiResponse.user.email,
|
||
first_name: apiResponse.user.first_name,
|
||
last_name: apiResponse.user.last_name,
|
||
is_admin: apiResponse.user.is_admin
|
||
};
|
||
|
||
setUser(userData);
|
||
|
||
// Обновляем глобальное состояние аутентификации
|
||
globalAuthState.isAuthenticated = true;
|
||
globalAuthState.isAdmin = userData.is_admin;
|
||
globalAuthState.redirectAttempted = false;
|
||
|
||
console.log("Пользователь авторизован при загрузке:", userData);
|
||
console.log("Глобальное состояние обновлено:", globalAuthState);
|
||
} else {
|
||
console.log("Токен недействителен или неверный формат данных, удаляем");
|
||
localStorage.removeItem("token");
|
||
setUser(null);
|
||
|
||
// Сбрасываем глобальное состояние
|
||
globalAuthState.isAuthenticated = false;
|
||
globalAuthState.isAdmin = false;
|
||
}
|
||
} else {
|
||
console.log("Токен отсутствует");
|
||
setUser(null);
|
||
|
||
// Сбрасываем глобальное состояние
|
||
globalAuthState.isAuthenticated = false;
|
||
globalAuthState.isAdmin = false;
|
||
}
|
||
} catch (error) {
|
||
console.error("Ошибка при проверке аутентификации:", error);
|
||
localStorage.removeItem("token");
|
||
setUser(null);
|
||
|
||
// Сбрасываем глобальное состояние
|
||
globalAuthState.isAuthenticated = false;
|
||
globalAuthState.isAdmin = false;
|
||
} finally {
|
||
setLoading(false);
|
||
setAuthChecked(true);
|
||
}
|
||
};
|
||
|
||
checkAuth();
|
||
}, []);
|
||
|
||
// Функция входа
|
||
const login = async (email: string, password: string): Promise<boolean> => {
|
||
try {
|
||
console.log("Попытка входа:", email);
|
||
const response = await authApi.login(email, password);
|
||
|
||
console.log("Ответ API при входе:", response);
|
||
|
||
if (response.data?.access_token) {
|
||
localStorage.setItem("token", response.data.access_token);
|
||
|
||
// Получение данных пользователя
|
||
const userResponse = await authApi.getProfile();
|
||
console.log("Данные пользователя:", userResponse);
|
||
|
||
if (userResponse.data && typeof userResponse.data === 'object' && userResponse.data !== null && 'user' in userResponse.data) {
|
||
// Приводим ответ к нужному типу
|
||
const apiResponse = userResponse.data as UserApiResponse;
|
||
|
||
// Создаем объект пользователя
|
||
const userData: User = {
|
||
id: apiResponse.user.id,
|
||
email: apiResponse.user.email,
|
||
first_name: apiResponse.user.first_name,
|
||
last_name: apiResponse.user.last_name,
|
||
is_admin: apiResponse.user.is_admin
|
||
};
|
||
|
||
setUser(userData);
|
||
|
||
// Обновляем глобальное состояние
|
||
globalAuthState.isAuthenticated = true;
|
||
globalAuthState.isAdmin = userData.is_admin;
|
||
globalAuthState.redirectAttempted = false;
|
||
|
||
console.log("Пользователь авторизован:", userData);
|
||
console.log("Глобальное состояние обновлено:", globalAuthState);
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
console.log("Ошибка входа: неверные учетные данные");
|
||
|
||
// Сбрасываем глобальное состояние
|
||
globalAuthState.isAuthenticated = false;
|
||
globalAuthState.isAdmin = false;
|
||
|
||
return false;
|
||
} catch (error) {
|
||
console.error("Ошибка при входе:", error);
|
||
|
||
// Сбрасываем глобальное состояние
|
||
globalAuthState.isAuthenticated = false;
|
||
globalAuthState.isAdmin = false;
|
||
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// Функция выхода
|
||
const logout = () => {
|
||
localStorage.removeItem("token");
|
||
setUser(null);
|
||
|
||
// Сбрасываем глобальное состояние
|
||
globalAuthState.isAuthenticated = false;
|
||
globalAuthState.isAdmin = false;
|
||
globalAuthState.redirectAttempted = false;
|
||
|
||
console.log("Пользователь вышел из системы, глобальное состояние сброшено");
|
||
|
||
// Добавляем небольшую задержку перед перенаправлением
|
||
setTimeout(() => {
|
||
router.push("/admin/login");
|
||
}, 100);
|
||
};
|
||
|
||
// Проверка на администратора
|
||
const isAdmin = user?.is_admin || false;
|
||
|
||
const value = {
|
||
user,
|
||
loading,
|
||
isAdmin,
|
||
login,
|
||
logout,
|
||
authChecked
|
||
};
|
||
|
||
return (
|
||
<AuthContext.Provider value={value}>
|
||
{children}
|
||
</AuthContext.Provider>
|
||
);
|
||
}
|
||
|
||
// Хук для использования контекста аутентификации
|
||
export function useAuth() {
|
||
const context = useContext(AuthContext);
|
||
|
||
if (context === undefined) {
|
||
throw new Error("useAuth должен использоваться внутри AuthProvider");
|
||
}
|
||
|
||
return context;
|
||
}
|
||
|
||
// Компонент для защиты маршрутов администратора
|
||
export function AdminProtected({ children }: { children: ReactNode }) {
|
||
const { user, loading, isAdmin, authChecked } = useAuth();
|
||
const router = useRouter();
|
||
const pathname = usePathname();
|
||
const [redirected, setRedirected] = useState(false);
|
||
const [redirectionTimer, setRedirectionTimer] = useState<NodeJS.Timeout | null>(null);
|
||
const redirectAttemptedRef = useRef(false);
|
||
|
||
// Обновляем глобальное состояние при изменении пользователя
|
||
useEffect(() => {
|
||
if (user) {
|
||
globalAuthState.isAuthenticated = true;
|
||
globalAuthState.isAdmin = isAdmin;
|
||
}
|
||
}, [user, isAdmin]);
|
||
|
||
useEffect(() => {
|
||
// Пропускаем проверку во время загрузки или если проверка аутентификации еще не завершена
|
||
if (loading || !authChecked) return;
|
||
|
||
console.log("AdminProtected проверка:", {
|
||
user: user ? `${user.email} (admin: ${user.is_admin})` : "null",
|
||
isAdmin,
|
||
pathname,
|
||
authChecked,
|
||
redirectAttempted: redirectAttemptedRef.current,
|
||
globalState: globalAuthState
|
||
});
|
||
|
||
// Очищаем предыдущий таймер, если он существует
|
||
if (redirectionTimer) {
|
||
clearTimeout(redirectionTimer);
|
||
setRedirectionTimer(null);
|
||
}
|
||
|
||
// Если пользователь уже аутентифицирован в глобальном состоянии, пропускаем перенаправление
|
||
if (globalAuthState.isAuthenticated && globalAuthState.isAdmin) {
|
||
console.log("Доступ разрешен: пользователь авторизован в глобальном состоянии");
|
||
return;
|
||
}
|
||
|
||
// Предотвращаем бесконечные перенаправления
|
||
if (redirected || redirectAttemptedRef.current || globalAuthState.redirectAttempted) return;
|
||
|
||
// Если пользователь не авторизован, перенаправляем на страницу входа
|
||
if (!user) {
|
||
console.log("Перенаправление: неавторизованный пользователь -> /admin/login");
|
||
setRedirected(true);
|
||
redirectAttemptedRef.current = true;
|
||
globalAuthState.redirectAttempted = true;
|
||
|
||
// Используем таймер для перенаправления
|
||
const timer = setTimeout(() => {
|
||
router.push("/admin/login");
|
||
}, 100);
|
||
|
||
setRedirectionTimer(timer);
|
||
return;
|
||
}
|
||
|
||
// Если пользователь не админ, перенаправляем на главную страницу
|
||
if (!isAdmin) {
|
||
console.log("Перенаправление: авторизованный не-админ -> /");
|
||
setRedirected(true);
|
||
redirectAttemptedRef.current = true;
|
||
globalAuthState.redirectAttempted = true;
|
||
|
||
// Используем таймер для перенаправления
|
||
const timer = setTimeout(() => {
|
||
router.push("/");
|
||
}, 100);
|
||
|
||
setRedirectionTimer(timer);
|
||
return;
|
||
}
|
||
|
||
// Если пользователь авторизован и является админом, обновляем глобальное состояние
|
||
globalAuthState.isAuthenticated = true;
|
||
globalAuthState.isAdmin = true;
|
||
console.log("Доступ разрешен: авторизованный админ");
|
||
}, [user, loading, isAdmin, router, pathname, redirected, authChecked]);
|
||
|
||
// Сбрасываем флаг перенаправления при размонтировании компонента
|
||
useEffect(() => {
|
||
return () => {
|
||
if (redirectionTimer) {
|
||
clearTimeout(redirectionTimer);
|
||
}
|
||
// Сбрасываем флаг при размонтировании
|
||
redirectAttemptedRef.current = false;
|
||
};
|
||
}, [redirectionTimer]);
|
||
|
||
// Показываем индикатор загрузки
|
||
if (loading) {
|
||
return (
|
||
<div className="flex h-screen items-center justify-center">
|
||
<div className="text-lg">Загрузка...</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Если пользователь авторизован в глобальном состоянии, рендерим содержимое
|
||
if (globalAuthState.isAuthenticated && globalAuthState.isAdmin) {
|
||
return <>{children}</>;
|
||
}
|
||
|
||
// Если пользователь не авторизован или не админ, не рендерим содержимое
|
||
if (!user || !isAdmin) {
|
||
return (
|
||
<div className="flex h-screen items-center justify-center">
|
||
<div className="text-lg">Перенаправление...</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Если все проверки пройдены, рендерим содержимое
|
||
return <>{children}</>;
|
||
}
|