front
This commit is contained in:
parent
57290a8d03
commit
820d53e931
14
README.md
14
README.md
@ -161,3 +161,17 @@ fetch(`${Config.API_URL}/api`, {
|
||||
Речь идет об вызываемом эндпоинте ${Config.API_URL}/api, Обязательно используйте конструкцию данного вида т.к. при сборке приложения статический сайт обращается относительно api и вместо
|
||||
запросов вида http://localhost:8000/api должно быть так: /api
|
||||
|
||||
# Авторизация фронтенда
|
||||
|
||||
По умолчанию фронтенд использует существующую локальную форму входа. Keycloak включается только при точном значении `true` у production-флага:
|
||||
|
||||
```dotenv
|
||||
REACT_APP_IS_PRODUCTION=true
|
||||
REACT_APP_KEYCLOAK_URL=https://keycloak.example.ru
|
||||
REACT_APP_REALM=example-realm
|
||||
REACT_APP_CLIENT_ID=example-client
|
||||
REACT_APP_NAMESPACE=example-namespace
|
||||
REACT_APP_APP_NAME=example-app
|
||||
```
|
||||
|
||||
При `REACT_APP_IS_PRODUCTION=false` или если переменная отсутствует, Keycloak не инициализируется.
|
||||
|
||||
10
web/package-lock.json
generated
10
web/package-lock.json
generated
@ -14,6 +14,7 @@
|
||||
"@mui/x-tree-view": "^9.1.0",
|
||||
"axios": "^1.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"keycloak-js": "^26.2.4",
|
||||
"material-react-table": "^3.2.1",
|
||||
"react": "^18.3.1",
|
||||
"react-daisyui": "^5.0.5",
|
||||
@ -2362,6 +2363,15 @@
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/keycloak-js": {
|
||||
"version": "26.2.4",
|
||||
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.4.tgz",
|
||||
"integrity": "sha512-PnXpR3ubETGOt0B/Qt2lxmPbkZr5bc3vlQsOqDoTPPQsZRp7JjhTKxlJ187uWh8qJhvBab6Gsjb06a8ayOPfuw==",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"test"
|
||||
]
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"@mui/x-tree-view": "^9.1.0",
|
||||
"axios": "^1.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"keycloak-js": "^26.2.4",
|
||||
"material-react-table": "^3.2.1",
|
||||
"react": "^18.3.1",
|
||||
"react-daisyui": "^5.0.5",
|
||||
|
||||
@ -25,7 +25,7 @@ export const PrivateRoute = () => {
|
||||
const { isAuthenticated, loading } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
<CircularProgress />;
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
return isAuthenticated ? <Outlet /> : <Navigate to='/login' replace />;
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import Keycloak from 'keycloak-js';
|
||||
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(() => {
|
||||
@ -99,18 +107,123 @@ 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 KeycloakAuthProvider = ({ children }) => {
|
||||
const [keycloak] = useState(
|
||||
() =>
|
||||
new Keycloak({
|
||||
url: Config.KEYCLOAK_URL,
|
||||
realm: Config.REALM,
|
||||
clientId: Config.CLIENT_ID,
|
||||
}),
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
const saveKeycloakToken = useCallback(() => {
|
||||
if (!keycloak.token) return;
|
||||
|
||||
localStorage.setItem('access_token', keycloak.token);
|
||||
localStorage.removeItem('refresh_token');
|
||||
window.dispatchEvent(new CustomEvent('auth:changed', { detail: { loggedIn: true } }));
|
||||
}, [keycloak]);
|
||||
|
||||
const loadUser = useCallback(async () => {
|
||||
try {
|
||||
const userData = await UsersApi.me();
|
||||
setUser(userData);
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки пользователя:', error);
|
||||
setUser(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const login = useCallback(() => keycloak.login(), [keycloak]);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
await keycloak.logout({ redirectUri: window.location.origin });
|
||||
}, [keycloak]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
const initialize = async () => {
|
||||
try {
|
||||
const authenticated = await keycloak.init({
|
||||
onLoad: 'login-required',
|
||||
checkLoginIframe: false,
|
||||
pkceMethod: 'S256',
|
||||
});
|
||||
|
||||
if (!active) return;
|
||||
setIsAuthenticated(authenticated);
|
||||
|
||||
if (authenticated) {
|
||||
saveKeycloakToken();
|
||||
await loadUser();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка инициализации Keycloak:', error);
|
||||
if (active) {
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
}
|
||||
} finally {
|
||||
if (active) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
keycloak.onAuthRefreshSuccess = saveKeycloakToken;
|
||||
keycloak.onTokenExpired = async () => {
|
||||
try {
|
||||
await keycloak.updateToken(30);
|
||||
saveKeycloakToken();
|
||||
} catch (error) {
|
||||
console.error('Не удалось обновить токен Keycloak:', error);
|
||||
await login();
|
||||
}
|
||||
};
|
||||
keycloak.onAuthLogout = () => {
|
||||
localStorage.removeItem('access_token');
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
initialize();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
keycloak.onAuthRefreshSuccess = undefined;
|
||||
keycloak.onTokenExpired = undefined;
|
||||
keycloak.onAuthLogout = undefined;
|
||||
};
|
||||
}, [keycloak, loadUser, login, saveKeycloakToken]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isAuthenticated, user, login, logout, loadUser, loading }}>
|
||||
{loading ? <LoadingScreen /> : children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
if (Config.IS_PRODUCTION) {
|
||||
return <KeycloakAuthProvider>{children}</KeycloakAuthProvider>;
|
||||
}
|
||||
|
||||
return <LocalAuthProvider>{children}</LocalAuthProvider>;
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
//@ts-ignore
|
||||
const environment = process.env;
|
||||
|
||||
const isTrue = (value: string | undefined): boolean => value?.trim().toLowerCase() === 'true';
|
||||
|
||||
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;
|
||||
@ -11,6 +13,7 @@ if (environment.APP_ENV == 'test') {
|
||||
|
||||
export class Config {
|
||||
static API_URL: string = env_var ?? environment.REACT_APP_API_URL ?? environment.REACT_APP_ROOT_PATH ?? '';
|
||||
static IS_PRODUCTION: boolean = isTrue(environment.REACT_APP_IS_PRODUCTION);
|
||||
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 ?? '';
|
||||
|
||||
11
web/src/env.d.ts
vendored
11
web/src/env.d.ts
vendored
@ -1 +1,12 @@
|
||||
/// <reference types="@rsbuild/core/types" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
readonly REACT_APP_IS_PRODUCTION?: string;
|
||||
readonly REACT_APP_KEYCLOAK_URL?: string;
|
||||
readonly REACT_APP_REALM?: string;
|
||||
readonly REACT_APP_CLIENT_ID?: string;
|
||||
readonly REACT_APP_NAMESPACE?: string;
|
||||
readonly REACT_APP_APP_NAME?: string;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user