This commit is contained in:
PotapovaA 2026-09-02 17:18:15 +03:00
parent 57290a8d03
commit 4ec957cbcd
7 changed files with 123 additions and 16 deletions

4
web/package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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;

View File

@ -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 { 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,91 @@ 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) {
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) {

View File

@ -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>

View File

@ -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 ?? '';

View File

@ -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);