Merge branch 'auth' of ssh://git.aepif.ru:2222/RSHB/DFiP_Budget_planing into auth
This commit is contained in:
commit
ae6640796d
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"material-react-table": "^3.2.1",
|
"material-react-table": "^3.2.1",
|
||||||
|
"raisa-fastapi-protected-web": "1.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-daisyui": "^5.0.5",
|
"react-daisyui": "^5.0.5",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@ -2775,6 +2776,9 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/raisa-fastapi-protected-web": {
|
||||||
|
"version": "1.1.0"
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"material-react-table": "^3.2.1",
|
"material-react-table": "^3.2.1",
|
||||||
|
"raisa-fastapi-protected-web": "1.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-daisyui": "^5.0.5",
|
"react-daisyui": "^5.0.5",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@ -12,6 +12,10 @@ export const AuthApi = {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
async loginWithKeycloak(token) {
|
||||||
|
const { data } = await api.post('/auth/login', { token });
|
||||||
|
return data;
|
||||||
|
},
|
||||||
async refresh() {
|
async refresh() {
|
||||||
const refresh_token = localStorage.getItem('refresh_token');
|
const refresh_token = localStorage.getItem('refresh_token');
|
||||||
if (!refresh_token) return null;
|
if (!refresh_token) return null;
|
||||||
|
|||||||
@ -1,12 +1,20 @@
|
|||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
|
import { TokensContext } from 'raisa-fastapi-protected-web';
|
||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { AuthApi } from '../../api/auth';
|
import { AuthApi } from '../../api/auth';
|
||||||
import { UsersApi } from '../../api/users';
|
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);
|
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 [loading, setLoading] = useState(true);
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(() => {
|
const [isAuthenticated, setIsAuthenticated] = useState(() => {
|
||||||
@ -59,7 +67,7 @@ export const AuthProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = () => {
|
||||||
try {
|
try {
|
||||||
AuthApi.logout();
|
AuthApi.logout();
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
@ -99,18 +107,92 @@ export const AuthProvider = ({ children }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ isAuthenticated, user, login, logout, loadUser }}>
|
<AuthContext.Provider value={{ isAuthenticated, user, login, logout, loadUser, loading }}>
|
||||||
{loading ? (
|
{loading ? <LoadingScreen /> : children}
|
||||||
<div className='flex items-center justify-center min-h-screen'>
|
|
||||||
<CircularProgress />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</AuthContext.Provider>
|
</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 = () => {
|
export const useAuth = () => {
|
||||||
const context = useContext(AuthContext);
|
const context = useContext(AuthContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../../app/context/AuthProvider';
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
import { isTestMode } from '../../conf/config';
|
import { Config } from '../../conf/config';
|
||||||
|
|
||||||
export function HeaderMenu() {
|
export function HeaderMenu() {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
@ -58,8 +58,8 @@ export function HeaderMenu() {
|
|||||||
</UserNameAndRole>
|
</UserNameAndRole>
|
||||||
|
|
||||||
{/* на стенде прода не показываем кнопку выхода */}
|
{/* на стенде прода не показываем кнопку выхода */}
|
||||||
{isTestMode && <DownButton onClick={toggleMenu} isOpen={isMenuOpen} />}
|
{!Config.IS_PRODUCTION && <DownButton onClick={toggleMenu} isOpen={isMenuOpen} />}
|
||||||
{isTestMode && isMenuOpen && (
|
{!Config.IS_PRODUCTION && isMenuOpen && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<LogoutButton onClick={handleLogout}>Выйти</LogoutButton>
|
<LogoutButton onClick={handleLogout}>Выйти</LogoutButton>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@ -3,14 +3,17 @@ const environment = process.env;
|
|||||||
|
|
||||||
let env_var: string | undefined;
|
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;
|
env_var = environment.OPENBAO__SETTINGS_TEST__REACT_APP_API_URL;
|
||||||
} else {
|
} else {
|
||||||
env_var = environment.OPENBAO__SETTINGS__REACT_APP_API_URL;
|
env_var = environment.OPENBAO__SETTINGS__REACT_APP_API_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/noStaticOnlyClass: существующий публичный интерфейс конфигурации приложения.
|
||||||
export class Config {
|
export class Config {
|
||||||
static API_URL: string = env_var ?? environment.REACT_APP_API_URL ?? environment.REACT_APP_ROOT_PATH ?? '';
|
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 KEYCLOAK_URL: string = environment.REACT_APP_KEYCLOAK_URL ?? '';
|
||||||
static REALM: string = environment.REACT_APP_REALM ?? '';
|
static REALM: string = environment.REACT_APP_REALM ?? '';
|
||||||
static CLIENT_ID: string = environment.REACT_APP_CLIENT_ID ?? '';
|
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 { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
import { ruRU } from '@mui/x-date-pickers/locales';
|
import { ruRU } from '@mui/x-date-pickers/locales';
|
||||||
import ruLocale from 'date-fns/locale/ru';
|
import ruLocale from 'date-fns/locale/ru';
|
||||||
|
import { RaisaAuthContext, createAuthContext } from 'raisa-fastapi-protected-web';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { AuthProvider } from './app/context/AuthProvider';
|
import { AuthProvider } from './app/context/AuthProvider';
|
||||||
@ -10,11 +11,12 @@ import theme from './app/theme/theme';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import { Config } from './conf/config';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
const baseName = (process.env.REACT_APP_ROOT_PATH || '/').replace(/\/$/, '');
|
const baseName = (process.env.REACT_APP_ROOT_PATH || '/').replace(/\/$/, '');
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
const application = (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<LocalizationProvider
|
<LocalizationProvider
|
||||||
dateAdapter={AdapterDateFns}
|
dateAdapter={AdapterDateFns}
|
||||||
@ -32,5 +34,17 @@ createRoot(document.getElementById('root')).render(
|
|||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</LocalizationProvider>
|
</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