234 lines
5.9 KiB
JavaScript
234 lines
5.9 KiB
JavaScript
import { CircularProgress } from '@mui/material';
|
||
import Keycloak from 'keycloak-js';
|
||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||
import { AuthApi } from '../../api/auth';
|
||
import { UsersApi } from '../../api/users';
|
||
import { getAuthToken, isTokenExpired, saveAuthTokens } from '../../api/utils/token';
|
||
import { Config } from '../../conf/config';
|
||
|
||
const AuthContext = createContext(null);
|
||
|
||
const LoadingScreen = () => (
|
||
<div className='flex items-center justify-center min-h-screen'>
|
||
<CircularProgress />
|
||
</div>
|
||
);
|
||
|
||
const LocalAuthProvider = ({ children }) => {
|
||
const [loading, setLoading] = useState(true);
|
||
const [user, setUser] = useState(null);
|
||
const [isAuthenticated, setIsAuthenticated] = useState(() => {
|
||
const token = getAuthToken();
|
||
return !!token && !isTokenExpired();
|
||
});
|
||
|
||
const loadUser = useCallback(async () => {
|
||
if (!isAuthenticated) {
|
||
setUser(null);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const userData = await UsersApi.me();
|
||
setUser(userData);
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки пользователя:', error);
|
||
if (error.response?.status === 401) {
|
||
setIsAuthenticated(false);
|
||
setUser(null);
|
||
} else {
|
||
setUser(null);
|
||
}
|
||
}
|
||
}, [isAuthenticated]);
|
||
|
||
const login = async (username, password) => {
|
||
setLoading(true);
|
||
try {
|
||
const data = await AuthApi.login(username, password);
|
||
saveAuthTokens(data);
|
||
setIsAuthenticated(true);
|
||
|
||
try {
|
||
const userData = await UsersApi.me();
|
||
setUser(userData);
|
||
} catch (userError) {
|
||
console.error('Ошибка загрузки пользователя:', userError);
|
||
setUser(null);
|
||
}
|
||
} catch (error) {
|
||
setIsAuthenticated(false);
|
||
setUser(null);
|
||
console.error('Ошибка авторизации:', error);
|
||
|
||
throw new Error(error?.response?.data?.detail || 'Неверный логин или пароль', { cause: error });
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const logout = async () => {
|
||
try {
|
||
AuthApi.logout();
|
||
setIsAuthenticated(false);
|
||
setUser(null);
|
||
} catch (error) {
|
||
console.error('Ошибка выхода:', error);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
setLoading(false);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadUser();
|
||
}, [loadUser]);
|
||
|
||
useEffect(() => {
|
||
const handleAuthChange = (_event) => {
|
||
const token = getAuthToken();
|
||
const newIsAuthenticated = !!token && !isTokenExpired();
|
||
setIsAuthenticated(newIsAuthenticated);
|
||
};
|
||
|
||
const handleAuthExpired = () => {
|
||
setIsAuthenticated(false);
|
||
setUser(null);
|
||
};
|
||
|
||
window.addEventListener('auth:changed', handleAuthChange);
|
||
window.addEventListener('auth:expired', handleAuthExpired);
|
||
|
||
return () => {
|
||
window.removeEventListener('auth:changed', handleAuthChange);
|
||
window.removeEventListener('auth:expired', handleAuthExpired);
|
||
};
|
||
}, []);
|
||
|
||
return (
|
||
<AuthContext.Provider value={{ isAuthenticated, user, login, logout, loadUser, loading }}>
|
||
{loading ? <LoadingScreen /> : children}
|
||
</AuthContext.Provider>
|
||
);
|
||
};
|
||
|
||
const KeycloakAuthProvider = ({ children }) => {
|
||
const [keycloak] = useState(
|
||
() =>
|
||
new Keycloak({
|
||
url: Config.KEYCLOAK_URL,
|
||
realm: Config.REALM,
|
||
clientId: Config.CLIENT_ID,
|
||
}),
|
||
);
|
||
const [loading, setLoading] = useState(true);
|
||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||
const [user, setUser] = useState(null);
|
||
|
||
const saveKeycloakToken = useCallback(() => {
|
||
if (!keycloak.token) return;
|
||
|
||
localStorage.setItem('access_token', keycloak.token);
|
||
localStorage.removeItem('refresh_token');
|
||
window.dispatchEvent(new CustomEvent('auth:changed', { detail: { loggedIn: true } }));
|
||
}, [keycloak]);
|
||
|
||
const loadUser = useCallback(async () => {
|
||
try {
|
||
const userData = await UsersApi.me();
|
||
setUser(userData);
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки пользователя:', error);
|
||
setUser(null);
|
||
}
|
||
}, []);
|
||
|
||
const login = useCallback(() => keycloak.login(), [keycloak]);
|
||
|
||
const logout = useCallback(async () => {
|
||
localStorage.removeItem('access_token');
|
||
localStorage.removeItem('refresh_token');
|
||
setIsAuthenticated(false);
|
||
setUser(null);
|
||
await keycloak.logout({ redirectUri: window.location.origin });
|
||
}, [keycloak]);
|
||
|
||
useEffect(() => {
|
||
let active = true;
|
||
|
||
const initialize = async () => {
|
||
try {
|
||
const authenticated = await keycloak.init({
|
||
onLoad: 'login-required',
|
||
checkLoginIframe: false,
|
||
pkceMethod: 'S256',
|
||
});
|
||
|
||
if (!active) return;
|
||
setIsAuthenticated(authenticated);
|
||
|
||
if (authenticated) {
|
||
saveKeycloakToken();
|
||
await loadUser();
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка инициализации Keycloak:', error);
|
||
if (active) {
|
||
setIsAuthenticated(false);
|
||
setUser(null);
|
||
}
|
||
} finally {
|
||
if (active) setLoading(false);
|
||
}
|
||
};
|
||
|
||
keycloak.onAuthRefreshSuccess = saveKeycloakToken;
|
||
keycloak.onTokenExpired = async () => {
|
||
try {
|
||
await keycloak.updateToken(30);
|
||
saveKeycloakToken();
|
||
} catch (error) {
|
||
console.error('Не удалось обновить токен Keycloak:', error);
|
||
await login();
|
||
}
|
||
};
|
||
keycloak.onAuthLogout = () => {
|
||
localStorage.removeItem('access_token');
|
||
setIsAuthenticated(false);
|
||
setUser(null);
|
||
};
|
||
|
||
initialize();
|
||
|
||
return () => {
|
||
active = false;
|
||
keycloak.onAuthRefreshSuccess = undefined;
|
||
keycloak.onTokenExpired = undefined;
|
||
keycloak.onAuthLogout = undefined;
|
||
};
|
||
}, [keycloak, loadUser, login, saveKeycloakToken]);
|
||
|
||
return (
|
||
<AuthContext.Provider value={{ isAuthenticated, user, login, logout, loadUser, loading }}>
|
||
{loading ? <LoadingScreen /> : children}
|
||
</AuthContext.Provider>
|
||
);
|
||
};
|
||
|
||
export const AuthProvider = ({ children }) => {
|
||
if (Config.IS_PRODUCTION) {
|
||
return <KeycloakAuthProvider>{children}</KeycloakAuthProvider>;
|
||
}
|
||
|
||
return <LocalAuthProvider>{children}</LocalAuthProvider>;
|
||
};
|
||
|
||
export const useAuth = () => {
|
||
const context = useContext(AuthContext);
|
||
if (!context) {
|
||
throw new Error('useAuth must be used within an AuthProvider');
|
||
}
|
||
return context;
|
||
};
|