301 lines
11 KiB
TypeScript
301 lines
11 KiB
TypeScript
"use client";
|
||
|
||
import React, { createContext, useContext, useState, useEffect, ReactNode, useRef } from "react";
|
||
import { useRouter, usePathname } from "next/navigation";
|
||
import api from "./api";
|
||
import { authApi } from "./auth-api";
|
||
|
||
// Расширенный интерфейс пользователя с API
|
||
export interface User {
|
||
id: number;
|
||
email: string;
|
||
first_name: string;
|
||
last_name: string;
|
||
is_admin: boolean;
|
||
role?: string; // Добавляем опциональное поле role для совместимости с API
|
||
}
|
||
|
||
// Интерфейс пользователя из API
|
||
interface UserProfile {
|
||
id: number;
|
||
email?: string;
|
||
first_name?: string;
|
||
last_name?: string;
|
||
role?: string;
|
||
[key: string]: any; // Для других полей, которые могут быть в ответе API
|
||
}
|
||
|
||
// Интерфейс контекста аутентификации
|
||
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);
|
||
|
||
// Константа для ключа токена в localStorage
|
||
const TOKEN_KEY = 'token';
|
||
|
||
// Глобальная переменная для отслеживания состояния аутентификации между рендерами
|
||
let globalAuthState = {
|
||
isAuthenticated: false,
|
||
isAdmin: false,
|
||
redirectAttempted: false
|
||
};
|
||
|
||
// Функция для получения токена из localStorage
|
||
const getToken = (): string | null => {
|
||
try {
|
||
return localStorage.getItem(TOKEN_KEY);
|
||
} catch (error) {
|
||
console.error('Ошибка при получении токена (auth.tsx):', error);
|
||
return null;
|
||
}
|
||
};
|
||
|
||
// Провайдер аутентификации
|
||
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 = getToken();
|
||
console.log('Проверка токена в AuthProvider:', token ? token.substring(0, 20) + '...' : 'нет токена');
|
||
|
||
if (token) {
|
||
const userProfile = await authApi.getProfile();
|
||
|
||
if (userProfile && 'id' in userProfile) {
|
||
// Создаем объект пользователя
|
||
const userData: User = {
|
||
id: userProfile.id,
|
||
email: userProfile.email || '',
|
||
first_name: userProfile.first_name || '',
|
||
last_name: userProfile.last_name || '',
|
||
is_admin: userProfile.is_admin,
|
||
role: userProfile.role
|
||
};
|
||
|
||
setUser(userData);
|
||
|
||
// Обновляем глобальное состояние аутентификации
|
||
globalAuthState.isAuthenticated = true;
|
||
globalAuthState.isAdmin = userData.is_admin;
|
||
globalAuthState.redirectAttempted = false;
|
||
|
||
console.log("Пользователь авторизован при загрузке:", userData);
|
||
} else {
|
||
console.log("Токен недействителен или неверный формат данных");
|
||
setUser(null);
|
||
|
||
// Сбрасываем глобальное состояние
|
||
globalAuthState.isAuthenticated = false;
|
||
globalAuthState.isAdmin = false;
|
||
}
|
||
} else {
|
||
console.log("Токен отсутствует");
|
||
setUser(null);
|
||
|
||
// Сбрасываем глобальное состояние
|
||
globalAuthState.isAuthenticated = false;
|
||
globalAuthState.isAdmin = false;
|
||
}
|
||
} catch (error) {
|
||
console.error("Ошибка при проверке аутентификации:", error);
|
||
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);
|
||
|
||
if (response && response.success) {
|
||
// Получение данных пользователя
|
||
const userProfile = await authApi.getProfile();
|
||
|
||
if (userProfile && 'id' in userProfile) {
|
||
// Создаем объект пользователя
|
||
const userData: User = {
|
||
id: userProfile.id,
|
||
email: userProfile.email || '',
|
||
first_name: userProfile.first_name || '',
|
||
last_name: userProfile.last_name || '',
|
||
is_admin: userProfile.is_admin,
|
||
role: userProfile.role
|
||
};
|
||
|
||
setUser(userData);
|
||
|
||
// Обновляем глобальное состояние
|
||
globalAuthState.isAuthenticated = true;
|
||
globalAuthState.isAdmin = userData.is_admin;
|
||
globalAuthState.redirectAttempted = false;
|
||
|
||
console.log("Пользователь авторизован:", userData);
|
||
|
||
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 = () => {
|
||
authApi.logout(); // Удаляет токен из localStorage
|
||
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 { AuthContext };
|
||
|
||
// Компонент для защиты маршрутов администратора
|
||
export function AdminProtected({ children }: { children: ReactNode }) {
|
||
const { user, loading, isAdmin, authChecked } = useContext(AuthContext) || { user: null, loading: true, isAdmin: false, authChecked: false };
|
||
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,
|
||
globalAuthState
|
||
});
|
||
|
||
// Проверяем, авторизован ли пользователь и является ли он администратором
|
||
if (!user || !isAdmin) {
|
||
// Предотвращаем множественные редиректы
|
||
if (!redirectAttemptedRef.current && !redirected) {
|
||
redirectAttemptedRef.current = true;
|
||
setRedirected(true);
|
||
|
||
console.log("Перенаправление на страницу входа для администратора");
|
||
|
||
// Очищаем предыдущий таймер, если он существует
|
||
if (redirectionTimer) {
|
||
clearTimeout(redirectionTimer);
|
||
}
|
||
|
||
// Устанавливаем новый таймер для перенаправления
|
||
const timer = setTimeout(() => {
|
||
router.push('/admin/login');
|
||
}, 100);
|
||
|
||
setRedirectionTimer(timer);
|
||
}
|
||
} else {
|
||
// Сбрасываем флаг перенаправления, если пользователь авторизован и является администратором
|
||
redirectAttemptedRef.current = false;
|
||
setRedirected(false);
|
||
}
|
||
}, [user, isAdmin, loading, authChecked, pathname, router, redirected, redirectionTimer]);
|
||
|
||
// Эффект для очистки таймера при размонтировании компонента
|
||
useEffect(() => {
|
||
return () => {
|
||
if (redirectionTimer) {
|
||
clearTimeout(redirectionTimer);
|
||
}
|
||
};
|
||
}, [redirectionTimer]);
|
||
|
||
// Показываем загрузку во время проверки аутентификации
|
||
if (loading || !authChecked) {
|
||
return <div>Загрузка...</div>;
|
||
}
|
||
|
||
// Если пользователь авторизован и является администратором, показываем содержимое
|
||
if (user && isAdmin) {
|
||
return <>{children}</>;
|
||
}
|
||
|
||
// В противном случае ничего не показываем (перенаправление уже должно быть запущено)
|
||
return null;
|
||
}
|