diff --git a/web/package-lock.json b/web/package-lock.json
index 1a7ae02..0a398cd 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -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",
diff --git a/web/package.json b/web/package.json
index f2eee39..916888d 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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",
diff --git a/web/src/api/auth.js b/web/src/api/auth.js
index c1a0725..a3e9d74 100644
--- a/web/src/api/auth.js
+++ b/web/src/api/auth.js
@@ -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;
diff --git a/web/src/app/context/AuthProvider.jsx b/web/src/app/context/AuthProvider.jsx
index 964ee0f..8bdc5f2 100644
--- a/web/src/app/context/AuthProvider.jsx
+++ b/web/src/app/context/AuthProvider.jsx
@@ -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 = () => (
+
+
+
+);
+
+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 (
-
- {loading ? (
-
-
-
- ) : (
- children
- )}
+
+ {loading ? : children}
);
};
+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 ;
+
+ if (authError) {
+ return {authError}
;
+ }
+
+ return {children};
+};
+
+export const AuthProvider = ({ children }) => {
+ if (Config.IS_PRODUCTION) {
+ return {children};
+ }
+
+ return {children};
+};
+
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
diff --git a/web/src/components/HeaderMenu/HeaderMenu.jsx b/web/src/components/HeaderMenu/HeaderMenu.jsx
index 45241c9..3075412 100644
--- a/web/src/components/HeaderMenu/HeaderMenu.jsx
+++ b/web/src/components/HeaderMenu/HeaderMenu.jsx
@@ -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() {
{/* на стенде прода не показываем кнопку выхода */}
- {isTestMode && }
- {isTestMode && isMenuOpen && (
+ {!Config.IS_PRODUCTION && }
+ {!Config.IS_PRODUCTION && isMenuOpen && (
Выйти
diff --git a/web/src/conf/config.ts b/web/src/conf/config.ts
index 90f6c29..2a38704 100644
--- a/web/src/conf/config.ts
+++ b/web/src/conf/config.ts
@@ -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 ?? '';
diff --git a/web/src/index.jsx b/web/src/index.jsx
index afa1635..1030b7d 100644
--- a/web/src/index.jsx
+++ b/web/src/index.jsx
@@ -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 = (
- ,
+
);
+
+const root = Config.IS_PRODUCTION ? (
+
+ {application}
+
+) : (
+ application
+);
+
+createRoot(document.getElementById('root')).render(root);