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 и вместо
|
Речь идет об вызываемом эндпоинте ${Config.API_URL}/api, Обязательно используйте конструкцию данного вида т.к. при сборке приложения статический сайт обращается относительно api и вместо
|
||||||
запросов вида http://localhost:8000/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",
|
"@mui/x-tree-view": "^9.1.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"keycloak-js": "^26.2.4",
|
||||||
"material-react-table": "^3.2.1",
|
"material-react-table": "^3.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-daisyui": "^5.0.5",
|
"react-daisyui": "^5.0.5",
|
||||||
@ -2362,6 +2363,15 @@
|
|||||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lilconfig": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"@mui/x-tree-view": "^9.1.0",
|
"@mui/x-tree-view": "^9.1.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"keycloak-js": "^26.2.4",
|
||||||
"material-react-table": "^3.2.1",
|
"material-react-table": "^3.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-daisyui": "^5.0.5",
|
"react-daisyui": "^5.0.5",
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export const PrivateRoute = () => {
|
|||||||
const { isAuthenticated, loading } = useAuth();
|
const { isAuthenticated, loading } = useAuth();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
<CircularProgress />;
|
return <CircularProgress />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isAuthenticated ? <Outlet /> : <Navigate to='/login' replace />;
|
return isAuthenticated ? <Outlet /> : <Navigate to='/login' replace />;
|
||||||
|
|||||||
@ -1,12 +1,20 @@
|
|||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
|
import Keycloak from 'keycloak-js';
|
||||||
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 { 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(() => {
|
||||||
@ -99,18 +107,123 @@ 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 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 = () => {
|
export const useAuth = () => {
|
||||||
const context = useContext(AuthContext);
|
const context = useContext(AuthContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const environment = process.env;
|
const environment = process.env;
|
||||||
|
|
||||||
|
const isTrue = (value: string | undefined): boolean => value?.trim().toLowerCase() === 'true';
|
||||||
|
|
||||||
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;
|
||||||
@ -11,6 +13,7 @@ if (environment.APP_ENV == 'test') {
|
|||||||
|
|
||||||
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 ?? '';
|
||||||
|
static IS_PRODUCTION: boolean = isTrue(environment.REACT_APP_IS_PRODUCTION);
|
||||||
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 ?? '';
|
||||||
|
|||||||
11
web/src/env.d.ts
vendored
11
web/src/env.d.ts
vendored
@ -1 +1,12 @@
|
|||||||
/// <reference types="@rsbuild/core/types" />
|
/// <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