Compare commits
2 Commits
57290a8d03
...
534aeb9d50
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
534aeb9d50 | ||
|
|
4ec957cbcd |
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@ -15,6 +15,7 @@
|
||||
"axios": "^1.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"material-react-table": "^3.2.1",
|
||||
"raisa-fastapi-protected-web": "1.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-daisyui": "^5.0.5",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -2775,6 +2776,9 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/raisa-fastapi-protected-web": {
|
||||
"version": "1.1.0"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"axios": "^1.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"material-react-table": "^3.2.1",
|
||||
"raisa-fastapi-protected-web": "1.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-daisyui": "^5.0.5",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@ -12,6 +12,10 @@ export const AuthApi = {
|
||||
}
|
||||
return data;
|
||||
},
|
||||
async loginWithKeycloak(token) {
|
||||
const { data } = await api.post('/auth/login', { token });
|
||||
return data;
|
||||
},
|
||||
async refresh() {
|
||||
const refresh_token = localStorage.getItem('refresh_token');
|
||||
if (!refresh_token) return null;
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import { CircularProgress } from '@mui/material';
|
||||
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';
|
||||
import { getAuthToken, isTokenExpired, saveAuthTokens } from '../../api/utils/token';
|
||||
import { clearAuthTokens, getAuthToken, isTokenExpired, saveAuthTokens } from '../../api/utils/token';
|
||||
import { Config } from '../../conf/config';
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
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(() => {
|
||||
@ -59,7 +67,7 @@ export const AuthProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
const logout = () => {
|
||||
try {
|
||||
AuthApi.logout();
|
||||
setIsAuthenticated(false);
|
||||
@ -99,18 +107,92 @@ export const AuthProvider = ({ children }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isAuthenticated, user, login, logout, loadUser }}>
|
||||
{loading ? (
|
||||
<div className='flex items-center justify-center min-h-screen'>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
<AuthContext.Provider value={{ isAuthenticated, user, login, logout, loadUser, loading }}>
|
||||
{loading ? <LoadingScreen /> : children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const ProductionAuthProvider = ({ children }) => {
|
||||
const tokenInfo = useContext(TokensContext);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [user, setUser] = useState(null);
|
||||
const [authError, setAuthError] = useState(null);
|
||||
|
||||
const loadUser = useCallback(async () => {
|
||||
const userData = await UsersApi.me();
|
||||
setUser(userData);
|
||||
return userData;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const keycloakAccessToken = tokenInfo?.accessToken;
|
||||
|
||||
if (!keycloakAccessToken) {
|
||||
clearAuthTokens();
|
||||
setLoading(true);
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}
|
||||
|
||||
const loginWithKeycloak = async () => {
|
||||
setLoading(true);
|
||||
setAuthError(null);
|
||||
|
||||
try {
|
||||
const tokens = await AuthApi.loginWithKeycloak(keycloakAccessToken);
|
||||
if (!active) return;
|
||||
|
||||
saveAuthTokens(tokens);
|
||||
setIsAuthenticated(true);
|
||||
try {
|
||||
await loadUser();
|
||||
} catch (userError) {
|
||||
console.error('Ошибка загрузки пользователя:', userError);
|
||||
if (active) setUser(null);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!active) return;
|
||||
|
||||
AuthApi.logout();
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
setAuthError(error?.response?.data?.detail || error?.response?.data?.message || 'Не удалось войти через Keycloak');
|
||||
console.error('Ошибка авторизации через Keycloak:', error);
|
||||
} finally {
|
||||
if (active) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loginWithKeycloak();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [loadUser, tokenInfo?.accessToken]);
|
||||
|
||||
if (loading) return <LoadingScreen />;
|
||||
|
||||
if (authError) {
|
||||
return <div className='flex items-center justify-center min-h-screen'>{authError}</div>;
|
||||
}
|
||||
|
||||
return <AuthContext.Provider value={{ isAuthenticated, user, loadUser, loading }}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
if (Config.IS_PRODUCTION) {
|
||||
return <ProductionAuthProvider>{children}</ProductionAuthProvider>;
|
||||
}
|
||||
|
||||
return <LocalAuthProvider>{children}</LocalAuthProvider>;
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
|
||||
@ -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,14 +3,17 @@ const environment = process.env;
|
||||
|
||||
let env_var: string | undefined;
|
||||
|
||||
if (environment.APP_ENV == 'test') {
|
||||
if (environment.APP_ENV === 'test') {
|
||||
env_var = environment.OPENBAO__SETTINGS_TEST__REACT_APP_API_URL;
|
||||
} else {
|
||||
env_var = environment.OPENBAO__SETTINGS__REACT_APP_API_URL;
|
||||
}
|
||||
|
||||
// biome-ignore lint/complexity/noStaticOnlyClass: существующий публичный интерфейс конфигурации приложения.
|
||||
export class Config {
|
||||
static API_URL: string = env_var ?? environment.REACT_APP_API_URL ?? environment.REACT_APP_ROOT_PATH ?? '';
|
||||
// TODO: заменить заглушку на признак production-сборки, эквивалентный !settings.DEBUG на бэкенде.
|
||||
static IS_PRODUCTION = true;
|
||||
static KEYCLOAK_URL: string = environment.REACT_APP_KEYCLOAK_URL ?? '';
|
||||
static REALM: string = environment.REACT_APP_REALM ?? '';
|
||||
static CLIENT_ID: string = environment.REACT_APP_CLIENT_ID ?? '';
|
||||
|
||||
@ -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';
|
||||
@ -10,11 +11,12 @@ import theme from './app/theme/theme';
|
||||
import './index.css';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import App from './App';
|
||||
import { Config } from './conf/config';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user