fix
This commit is contained in:
parent
820d53e931
commit
a1d4ce0cdd
@ -174,4 +174,4 @@ REACT_APP_NAMESPACE=example-namespace
|
||||
REACT_APP_APP_NAME=example-app
|
||||
```
|
||||
|
||||
При `REACT_APP_IS_PRODUCTION=false` или если переменная отсутствует, Keycloak не инициализируется.
|
||||
При `REACT_APP_IS_PRODUCTION=false` или если переменная отсутствует, Keycloak не инициализируется и продолжает работать локальная форма входа. Production-авторизация подключается через `raisa-fastapi-protected-web`.
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
"@mui/x-tree-view": "^9.1.0",
|
||||
"axios": "^1.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"keycloak-js": "^26.2.4",
|
||||
"raisa-fastapi-protected-web": "1.1.0",
|
||||
"material-react-table": "^3.2.1",
|
||||
"react": "^18.3.1",
|
||||
"react-daisyui": "^5.0.5",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import Keycloak from 'keycloak-js';
|
||||
import { TokensContext } from 'raisa-fastapi-protected-web';
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { AuthApi } from '../../api/auth';
|
||||
import { UsersApi } from '../../api/users';
|
||||
@ -67,7 +67,7 @@ const LocalAuthProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
const logout = () => {
|
||||
try {
|
||||
AuthApi.logout();
|
||||
setIsAuthenticated(false);
|
||||
@ -113,27 +113,12 @@ const LocalAuthProvider = ({ children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const KeycloakAuthProvider = ({ children }) => {
|
||||
const [keycloak] = useState(
|
||||
() =>
|
||||
new Keycloak({
|
||||
url: Config.KEYCLOAK_URL,
|
||||
realm: Config.REALM,
|
||||
clientId: Config.CLIENT_ID,
|
||||
}),
|
||||
);
|
||||
const ProductionAuthProvider = ({ children }) => {
|
||||
const tokenInfo = useContext(TokensContext);
|
||||
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();
|
||||
@ -144,73 +129,41 @@ const KeycloakAuthProvider = ({ children }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
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 accessToken = tokenInfo?.accessToken;
|
||||
|
||||
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 = () => {
|
||||
if (!accessToken) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
};
|
||||
setLoading(true);
|
||||
|
||||
initialize();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}
|
||||
|
||||
// `token` ожидается библиотекой, а `access_token` — существующими API-клиентами приложения.
|
||||
localStorage.setItem('token', accessToken);
|
||||
localStorage.setItem('access_token', accessToken);
|
||||
localStorage.removeItem('refresh_token');
|
||||
setIsAuthenticated(true);
|
||||
setLoading(true);
|
||||
|
||||
loadUser().finally(() => {
|
||||
if (active) setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
keycloak.onAuthRefreshSuccess = undefined;
|
||||
keycloak.onTokenExpired = undefined;
|
||||
keycloak.onAuthLogout = undefined;
|
||||
};
|
||||
}, [keycloak, loadUser, login, saveKeycloakToken]);
|
||||
}, [loadUser, tokenInfo?.accessToken]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isAuthenticated, user, login, logout, loadUser, loading }}>
|
||||
<AuthContext.Provider value={{ isAuthenticated, user, loadUser, loading }}>
|
||||
{loading ? <LoadingScreen /> : children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
@ -218,7 +171,7 @@ const KeycloakAuthProvider = ({ children }) => {
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
if (Config.IS_PRODUCTION) {
|
||||
return <KeycloakAuthProvider>{children}</KeycloakAuthProvider>;
|
||||
return <ProductionAuthProvider>{children}</ProductionAuthProvider>;
|
||||
}
|
||||
|
||||
return <LocalAuthProvider>{children}</LocalAuthProvider>;
|
||||
|
||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../app/context/AuthProvider';
|
||||
import { isTestMode } from '../../conf/config';
|
||||
import { Config } from '../../conf/config';
|
||||
|
||||
export function HeaderMenu() {
|
||||
const { user, logout } = useAuth();
|
||||
@ -58,8 +58,8 @@ export function HeaderMenu() {
|
||||
</UserNameAndRole>
|
||||
|
||||
{/* на стенде прода не показываем кнопку выхода */}
|
||||
{isTestMode && <DownButton onClick={toggleMenu} isOpen={isMenuOpen} />}
|
||||
{isTestMode && isMenuOpen && (
|
||||
{!Config.IS_PRODUCTION && <DownButton onClick={toggleMenu} isOpen={isMenuOpen} />}
|
||||
{!Config.IS_PRODUCTION && isMenuOpen && (
|
||||
<DropdownMenu>
|
||||
<LogoutButton onClick={handleLogout}>Выйти</LogoutButton>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -3,6 +3,7 @@ import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { ruRU } from '@mui/x-date-pickers/locales';
|
||||
import ruLocale from 'date-fns/locale/ru';
|
||||
import { RaisaAuthContext, createAuthContext } from 'raisa-fastapi-protected-web';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { AuthProvider } from './app/context/AuthProvider';
|
||||
@ -11,10 +12,11 @@ import './index.css';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import App from './App';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { Config } from './conf/config';
|
||||
|
||||
const baseName = (process.env.REACT_APP_ROOT_PATH || '/').replace(/\/$/, '');
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
const application = (
|
||||
<ThemeProvider theme={theme}>
|
||||
<LocalizationProvider
|
||||
dateAdapter={AdapterDateFns}
|
||||
@ -32,5 +34,17 @@ createRoot(document.getElementById('root')).render(
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
</LocalizationProvider>
|
||||
</ThemeProvider>,
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
const root = Config.IS_PRODUCTION ? (
|
||||
<RaisaAuthContext
|
||||
authClient={createAuthContext(Config.KEYCLOAK_URL, Config.REALM, Config.CLIENT_ID)}
|
||||
app={{ appName: Config.APP_NAME, namespace: Config.NAMESPACE }}>
|
||||
{application}
|
||||
</RaisaAuthContext>
|
||||
) : (
|
||||
application
|
||||
);
|
||||
|
||||
createRoot(document.getElementById('root')).render(root);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../app/context/AuthProvider';
|
||||
import { isTestMode } from '../conf/config';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [username, setUsername] = useState('admin');
|
||||
@ -11,18 +10,17 @@ export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (isTestMode && isAuthenticated) {
|
||||
if (isAuthenticated) {
|
||||
navigate('/', { replace: true });
|
||||
return;
|
||||
}
|
||||
}, [isTestMode, isAuthenticated, navigate]);
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await login(username, password);
|
||||
navigate('/tasks');
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
setError('Неверный логин или пароль');
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user