From 4ec957cbcda48ee770270cdff8f0b7046b53c756 Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Wed, 2 Sep 2026 17:18:15 +0300 Subject: [PATCH 1/2] front v2 --- web/package-lock.json | 4 + web/package.json | 1 + web/src/api/auth.js | 4 + web/src/app/context/AuthProvider.jsx | 101 +++++++++++++++++-- web/src/components/HeaderMenu/HeaderMenu.jsx | 6 +- web/src/conf/config.ts | 5 +- web/src/index.jsx | 18 +++- 7 files changed, 123 insertions(+), 16 deletions(-) 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); From 534aeb9d50761972d2d23d7a80e01ad41ed7c91b Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Wed, 2 Sep 2026 17:20:14 +0300 Subject: [PATCH 2/2] fix --- web/src/app/context/AuthProvider.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/app/context/AuthProvider.jsx b/web/src/app/context/AuthProvider.jsx index 8bdc5f2..02b5016 100644 --- a/web/src/app/context/AuthProvider.jsx +++ b/web/src/app/context/AuthProvider.jsx @@ -3,7 +3,7 @@ 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); @@ -131,6 +131,7 @@ const ProductionAuthProvider = ({ children }) => { const keycloakAccessToken = tokenInfo?.accessToken; if (!keycloakAccessToken) { + clearAuthTokens(); setLoading(true); setIsAuthenticated(false); setUser(null);