fix on logout auth.error - change oidp provider to keycloak-js

This commit is contained in:
NlightN22 2024-03-20 23:15:25 +07:00
parent e074e0ef4e
commit 6de8e3ecd6
18 changed files with 161 additions and 192 deletions

View File

@ -1,3 +1,4 @@
FRIGATE_PROXY=http://localhost:4000 FRIGATE_PROXY=http://localhost:4000
OPENID_SERVER=https://your.server.com:443/realms/your-realm OPENID_SERVER=https://your.server.com:443/realms/your-realm
REALM=frigate-realm
CLIENT_ID=frontend-client CLIENT_ID=frontend-client

View File

@ -8,7 +8,8 @@ services:
- /etc/localtime:/etc/localtime:ro # for Unix Time - /etc/localtime:/etc/localtime:ro # for Unix Time
environment: environment:
FRIGATE_PROXY: http://localhost:4000 FRIGATE_PROXY: http://localhost:4000
OPENID_SERVER: https://server:port/realms/your-realm OPENID_SERVER: https://server:port
CLIENT_ID: frontend-client CLIENT_ID: frontend-client
REALM: frigate-realm
ports: ports:
- 80:80 # set your port here - 80:80 # set your port here

View File

@ -12,6 +12,7 @@
"@mantine/modals": "^6.0.16", "@mantine/modals": "^6.0.16",
"@mantine/notifications": "^6.0.16", "@mantine/notifications": "^6.0.16",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@react-keycloak/web": "^3.4.0",
"@tabler/icons-react": "^2.24.0", "@tabler/icons-react": "^2.24.0",
"@tanstack/react-query": "^5.21.2", "@tanstack/react-query": "^5.21.2",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
@ -35,18 +36,17 @@
"i18next-browser-languagedetector": "^7.2.0", "i18next-browser-languagedetector": "^7.2.0",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"keycloak-js": "^24.0.1",
"mantine-react-table": "^1.0.0-beta.25", "mantine-react-table": "^1.0.0-beta.25",
"mobx": "^6.9.0", "mobx": "^6.9.0",
"mobx-react-lite": "^3.4.3", "mobx-react-lite": "^3.4.3",
"mobx-utils": "^6.0.7", "mobx-utils": "^6.0.7",
"monaco-editor": "^0.46.0", "monaco-editor": "^0.46.0",
"monaco-yaml": "^5.1.1", "monaco-yaml": "^5.1.1",
"oidc-client-ts": "^2.2.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.0",
"react-oidc-context": "^2.2.2",
"react-router-dom": "^6.14.1", "react-router-dom": "^6.14.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-use-websocket": "^4.7.0", "react-use-websocket": "^4.7.0",

View File

@ -1,19 +1,15 @@
import { useEffect, useState } from 'react';
import { hasAuthParams, useAuth } from 'react-oidc-context';
import CenterLoader from './shared/components/loaders/CenterLoader';
import { ColorScheme, ColorSchemeProvider, MantineProvider } from '@mantine/core'; import { ColorScheme, ColorSchemeProvider, MantineProvider } from '@mantine/core';
import { useColorScheme } from '@mantine/hooks'; import { useColorScheme } from '@mantine/hooks';
import { getCookie, setCookie } from 'cookies-next'; import { ModalsProvider } from '@mantine/modals';
import AppBody from './AppBody';
import Forbidden from './pages/403';
import { Notifications } from '@mantine/notifications'; import { Notifications } from '@mantine/notifications';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import RetryErrorPage from './pages/RetryErrorPage'; import { getCookie, setCookie } from 'cookies-next';
import { keycloakConfig } from '.'; import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import AppBody from './AppBody';
import { ModalsProvider } from '@mantine/modals';
import { FfprobeModal } from './shared/components/modal.windows/FfprobeModal'; import { FfprobeModal } from './shared/components/modal.windows/FfprobeModal';
import { VaInfoModal } from './shared/components/modal.windows/VaInfoModal'; import { VaInfoModal } from './shared/components/modal.windows/VaInfoModal';
import { useRealmAccessRoles } from './hooks/useRealmAccessRoles';
import { useAdminRole } from './hooks/useAdminRole';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@ -35,70 +31,14 @@ declare module '@mantine/modals' {
} }
function App() { function App() {
const maxErrorAuthCounts = 2
const systemColorScheme = useColorScheme() const systemColorScheme = useColorScheme()
const [colorScheme, setColorScheme] = useState<ColorScheme>(getCookie('mantine-color-scheme') as ColorScheme || systemColorScheme) const [colorScheme, setColorScheme] = useState<ColorScheme>(getCookie('mantine-color-scheme') as ColorScheme || systemColorScheme)
const [authErrorCounter, setAuthErrorCounter] = useState(0)
const toggleColorScheme = (value?: ColorScheme) => { const toggleColorScheme = (value?: ColorScheme) => {
const nextColorScheme = value || (colorScheme === 'dark' ? 'light' : 'dark'); const nextColorScheme = value || (colorScheme === 'dark' ? 'light' : 'dark');
setColorScheme(nextColorScheme) setColorScheme(nextColorScheme)
setCookie('mantine-color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 }); setCookie('mantine-color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 });
} }
const auth = useAuth()
const location = useLocation()
const navigate = useNavigate()
// automatically sign-in
useEffect(() => {
if (!hasAuthParams() &&
!auth.isAuthenticated && !auth.activeNavigator && !auth.isLoading && authErrorCounter < maxErrorAuthCounts) {
console.error('Not authenticated! Redirect! ErrorCounter', authErrorCounter)
setAuthErrorCounter(prevCount => prevCount + 1)
auth.signinRedirect()
}
}, [auth, auth.isAuthenticated, auth.activeNavigator, auth.isLoading, auth.signinRedirect, authErrorCounter])
if (auth.activeNavigator || auth.isLoading) {
return <CenterLoader />
}
if (authErrorCounter > maxErrorAuthCounts) {
console.error('maxErrorAuthCounts authority', keycloakConfig.authority)
console.error('maxErrorAuthCounts client_id', keycloakConfig.client_id)
console.error('maxErrorAuthCounts redirect_uri', keycloakConfig.redirect_uri)
return <RetryErrorPage backVisible={false} mainVisible={false} onRetry={() => auth.signinRedirect()} />
}
if (hasAuthParams()) {
const urlParams = new URLSearchParams(location.search);
urlParams.delete('state');
urlParams.delete('session_state');
urlParams.delete('code');
urlParams.delete('iss');
navigate(`${location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`, { replace: true })
}
if (!auth.isAuthenticated && !auth.isLoading && authErrorCounter < maxErrorAuthCounts) {
if (hasAuthParams()) {
console.warn('Not authenticated, isAuthenticated:', auth.isAuthenticated)
console.warn('Not authenticated, isLoading:', auth.isLoading)
return <RetryErrorPage backVisible={false} mainVisible={false} onRetry={() => auth.signinRedirect()} />
} else {
console.error('Not authenticated! Redirect! Error Counter:', authErrorCounter)
setAuthErrorCounter(prevCount => prevCount + 1);
auth.signinRedirect()
}
}
if ((!hasAuthParams() && !auth.isAuthenticated && !auth.isLoading) || auth.error) {
setAuthErrorCounter(prevCount => prevCount + 1)
console.error(`auth.error:`, auth.error)
return <Forbidden />
}
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<div className="App"> <div className="App">

View File

@ -8,6 +8,8 @@ import { routesPath } from './router/routes.path';
import SideBar from './shared/components/SideBar'; import SideBar from './shared/components/SideBar';
import { isProduction } from './shared/env.const'; import { isProduction } from './shared/env.const';
import { HeaderAction } from './widgets/header/HeaderAction'; import { HeaderAction } from './widgets/header/HeaderAction';
import { useAdminRole } from "./hooks/useAdminRole";
import { useRealmAccessRoles } from "./hooks/useRealmAccessRoles";
const AppBody = () => { const AppBody = () => {
const { t } = useTranslation() const { t } = useTranslation()

View File

@ -3,29 +3,30 @@ import { frigateQueryKeys, frigateApi } from "../services/frigate.proxy/frigate.
import { useRealmAccessRoles } from "./useRealmAccessRoles"; import { useRealmAccessRoles } from "./useRealmAccessRoles";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export interface AdminRole {
isLoading: boolean
isAdmin: boolean
}
export const useAdminRole = () => { export const useAdminRole = (): AdminRole => {
const { data: adminConfig, isError, isFetching } = useQuery({ const { data: adminConfig, isError, isLoading } = useQuery({
queryKey: [frigateQueryKeys.getAdminRole], queryKey: [frigateQueryKeys.getAdminRole],
queryFn: frigateApi.getAdminRole, queryFn: frigateApi.getAdminRole,
staleTime: 1000 * 60 * 60, staleTime: 1000 * 60 * 60,
gcTime: 1000 * 60 * 60 * 24, gcTime: 1000 * 60 * 60 * 24,
}) })
const roles = useRealmAccessRoles() const roles = useRealmAccessRoles()
const [initialized, setInitialized] = useState(false) const [isAdmin, setIsAdmin] = useState(false)
const isLoading = isFetching || roles === undefined
useEffect(() => { useEffect(() => {
if (!isLoading) { if (adminConfig) {
setInitialized(true); const checkAdmin = roles.some(role => role === adminConfig.value)
setIsAdmin(checkAdmin)
} else {
setIsAdmin(false)
} }
}, [isLoading]); }, [roles, adminConfig, isLoading])
if (!initialized || isLoading) return { isAdmin: undefined, isLoading: true } return { isLoading, isAdmin }
if (isError) return { isAdmin: false, isError: true, isLoading: false }
if (!adminConfig) return { isAdmin: true, isLoading: false }
if (adminConfig && !adminConfig.value) return { isAdmin: true, isLoading: false }
const isAdmin = roles.some(role => role === adminConfig.value)
return { isAdmin, isLoading: false }
} }

View File

@ -1,30 +1,36 @@
import { jwtDecode } from "jwt-decode";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useAuth } from "react-oidc-context"; import { isProduction } from "../shared/env.const";
import { useKeycloak } from "@react-keycloak/web";
interface CustomJwtPayload {
realm_access?: {
roles: string[];
};
}
export const useRealmAccessRoles = () => { export const useRealmAccessRoles = () => {
const { user } = useAuth();
const [roles, setRoles] = useState<string[]>([]); const [roles, setRoles] = useState<string[]>([]);
const { keycloak } = useKeycloak()
useEffect(() => { useEffect(() => {
if (user) { const updateRoles = () => {
try { const tokenRoles = keycloak.tokenParsed?.realm_access?.roles;
const decoded = jwtDecode<CustomJwtPayload>(user.access_token); if (!isProduction) console.log(`tokenRoles:`, tokenRoles);
const realmAccess = decoded.realm_access; if (tokenRoles) {
if (realmAccess && realmAccess.roles) { setRoles(tokenRoles);
setRoles(realmAccess.roles); } else {
} setRoles([])
} catch (error) {
console.error("Error decoding token:", error);
} }
} }
}, [user]);
updateRoles()
return roles; keycloak.onAuthSuccess = () => {
}; updateRoles()
}
keycloak.onAuthRefreshSuccess = () => {
updateRoles()
}
return () => {
keycloak.onAuthSuccess = undefined
keycloak.onAuthRefreshSuccess = undefined
}
}, [keycloak, keycloak.onAuthSuccess, keycloak.onAuthRefreshSuccess ])
return roles
}

View File

@ -3,10 +3,11 @@ import ReactDOM from 'react-dom/client';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import RootStore from './shared/stores/root.store'; import RootStore from './shared/stores/root.store';
import { AuthProvider, AuthProviderProps } from 'react-oidc-context';
import { isProduction, oidpSettings } from './shared/env.const';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import './services/i18n'; import './services/i18n';
import { ReactKeycloakProvider } from '@react-keycloak/web';
import keycloak from './services/keycloak-config';
import CenterLoader from './shared/components/loaders/CenterLoader';
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
@ -14,39 +15,35 @@ const root = ReactDOM.createRoot(
export const hostURL = new URL(window.location.href) export const hostURL = new URL(window.location.href)
export const keycloakConfig: AuthProviderProps = {
authority: oidpSettings.server,
client_id: oidpSettings.clientId,
redirect_uri: hostURL.toString(),
onSigninCallback: () => {
const currentUrl = new URL(window.location.href);
const params = currentUrl.searchParams;
params.delete('state');
params.delete('session_state');
params.delete('code');
params.delete('iss');
const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`
window.history.replaceState({}, document.title, newUrl)
}
}
const rootStore = new RootStore() const rootStore = new RootStore()
export const Context = createContext<RootStore>(rootStore) export const Context = createContext<RootStore>(rootStore)
if (!isProduction) { const eventLogger = (event: string, error?: any) => {
console.log('keycloakConfig.authority', keycloakConfig.authority) console.log('onKeycloakEvent', event, error);
console.log('keycloakConfig.client_id', keycloakConfig.client_id) };
console.log('keycloakConfig.redirect_uri', keycloakConfig.redirect_uri)
} const tokenLogger = (tokens: any) => {
console.log('onKeycloakTokens', tokens);
};
root.render( root.render(
<Context.Provider value={rootStore}> <ReactKeycloakProvider
<AuthProvider {...keycloakConfig}> authClient={keycloak}
LoadingComponent={<CenterLoader />}
onEvent={eventLogger}
onTokens={tokenLogger}
initOptions={{
onLoad: 'login-required',
checkLoginIframe: false
}}
>
<Context.Provider value={rootStore}>
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</AuthProvider> </Context.Provider>
</Context.Provider> </ReactKeycloakProvider>
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

View File

@ -5,9 +5,11 @@ import { useTranslation } from 'react-i18next';
import { Context } from '..'; import { Context } from '..';
import { routesPath } from '../router/routes.path'; import { routesPath } from '../router/routes.path';
import CogWheelWithText from '../shared/components/loaders/CogWheelWithText'; import CogWheelWithText from '../shared/components/loaders/CogWheelWithText';
import { useNavigate } from 'react-router-dom';
const Forbidden = () => { const Forbidden = () => {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate()
const executed = useRef(false) const executed = useRef(false)
const { sideBarsStore } = useContext(Context) const { sideBarsStore } = useContext(Context)
@ -21,7 +23,7 @@ const Forbidden = () => {
}, [sideBarsStore]) }, [sideBarsStore])
const handleGoToMain = () => { const handleGoToMain = () => {
window.location.replace(routesPath.MAIN_PATH) navigate(routesPath.MAIN_PATH)
} }
return ( return (

View File

@ -8,19 +8,10 @@ import {
import { FrigateConfig } from "../../types/frigateConfig"; import { FrigateConfig } from "../../types/frigateConfig";
import { RecordSummary } from "../../types/record"; import { RecordSummary } from "../../types/record";
import { EventFrigate } from "../../types/event"; import { EventFrigate } from "../../types/event";
import { keycloakConfig } from "../..";
import { getResolvedTimeZone } from "../../shared/utils/dateUtil"; import { getResolvedTimeZone } from "../../shared/utils/dateUtil";
import { FrigateStats, GetFfprobe, GetHostStorage, GetVaInfo } from "../../types/frigateStats"; import { FrigateStats, GetFfprobe, GetHostStorage, GetVaInfo } from "../../types/frigateStats";
import { hostname } from "os";
import { PostSaveConfig, SaveOption } from "../../types/saveConfig"; import { PostSaveConfig, SaveOption } from "../../types/saveConfig";
import keycloak from "../keycloak-config";
export const getToken = (): string | undefined => {
const key = `oidc.user:${keycloakConfig.authority}:${keycloakConfig.client_id}`;
const stored = sessionStorage.getItem(key);
const storedObject = stored ? JSON.parse(stored) : null;
return storedObject?.access_token;
}
const instanceApi = axios.create({ const instanceApi = axios.create({
baseURL: proxyURL.toString(), baseURL: proxyURL.toString(),
@ -29,9 +20,9 @@ const instanceApi = axios.create({
instanceApi.interceptors.request.use( instanceApi.interceptors.request.use(
config => { config => {
const token = getToken(); const accessToken = keycloak.token;
if (token) { if (accessToken) {
config.headers.Authorization = `Bearer ${token}` config.headers.Authorization = `Bearer ${accessToken}`
} }
return config; return config;
}, },

View File

@ -0,0 +1,12 @@
import Keycloak from "keycloak-js";
import { oidpSettings } from "../shared/env.const";
const keycloakConfig = {
url: oidpSettings.server,
realm: oidpSettings.realm,
clientId: oidpSettings.clientId,
};
const keycloak = new Keycloak(keycloakConfig);
export default keycloak;

View File

@ -2,10 +2,9 @@ import { Avatar, Button, Flex, Group, Menu, Text } from "@mantine/core";
import { useMediaQuery } from '@mantine/hooks'; import { useMediaQuery } from '@mantine/hooks';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAuth } from 'react-oidc-context';
import { keycloakConfig } from '../..';
import { dimensions } from '../dimensions/dimensions'; import { dimensions } from '../dimensions/dimensions';
import ColorSchemeToggle from './buttons/ColorSchemeToggle'; import ColorSchemeToggle from './buttons/ColorSchemeToggle';
import keycloak from "../../services/keycloak-config";
interface UserMenuProps { interface UserMenuProps {
user: { name: string; image: string } user: { name: string; image: string }
@ -20,15 +19,10 @@ const UserMenu = ({ user }: UserMenuProps) => {
{ lng: 'ru', name: 'Rus' }, { lng: 'ru', name: 'Rus' },
] ]
const auth = useAuth()
const isMiddleScreen = useMediaQuery(dimensions.middleScreenSize) const isMiddleScreen = useMediaQuery(dimensions.middleScreenSize)
const handleLogout = async () => { const handleLogout = async () => {
await auth.removeUser() keycloak.logout({ redirectUri: window.location.origin })
const id_token_hint = auth.user?.id_token
await auth.signoutRedirect({ post_logout_redirect_uri: keycloakConfig.redirect_uri, id_token_hint: id_token_hint })
} }
const handleChangeLanguage = async (lng: string) => { const handleChangeLanguage = async (lng: string) => {

View File

@ -39,7 +39,7 @@ const DrawerMenu = ({
links links
}: DrawerMenuProps) => { }: DrawerMenuProps) => {
const navigate = useNavigate() const navigate = useNavigate()
const { isAdmin } = useAdminRole() const isAdmin = useAdminRole()
const { classes } = useStyles(); const { classes } = useStyles();

View File

@ -2,14 +2,15 @@ import React, { useRef, useEffect } from 'react';
import videojs from 'video.js'; import videojs from 'video.js';
import Player from 'video.js/dist/types/player'; import Player from 'video.js/dist/types/player';
import 'video.js/dist/video-js.css' import 'video.js/dist/video-js.css'
import { getToken } from '../../../services/frigate.proxy/frigate.api';
import { isProduction } from '../../env.const'; import { isProduction } from '../../env.const';
import { useKeycloak } from '@react-keycloak/web';
interface VideoPlayerProps { interface VideoPlayerProps {
videoUrl: string videoUrl: string
} }
const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => { const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => {
const { keycloak } = useKeycloak()
const executed = useRef(false) const executed = useRef(false)
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const playerRef = useRef<Player | null>(null); const playerRef = useRef<Player | null>(null);
@ -20,7 +21,7 @@ const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => {
videojs.Vhs.xhr.beforeRequest = function (options: any) { videojs.Vhs.xhr.beforeRequest = function (options: any) {
options.headers = { options.headers = {
...options.headers, ...options.headers,
Authorization: `Bearer ${getToken()}`, Authorization: `Bearer ${keycloak.token}`,
}; };
return options; return options;
}; };

View File

@ -12,7 +12,11 @@ const oidpServer = isProduction ? window.env?.OPENID_SERVER : process.env.REACT_
const oidpServerParsed= z.string().url().safeParse(oidpServer) const oidpServerParsed= z.string().url().safeParse(oidpServer)
if (!oidpServerParsed.success) throw Error(`OPENID_SERVER must be string and URL. OPENID_SERVER:${oidpServer}`) if (!oidpServerParsed.success) throw Error(`OPENID_SERVER must be string and URL. OPENID_SERVER:${oidpServer}`)
const oidpClientId = isProduction ? window.env?.CLIENT_ID : process.env.REACT_APP_CLIENT_ID const oidpClientId = isProduction ? window.env?.CLIENT_ID : process.env.REACT_APP_CLIENT_ID
const oidpRealm = isProduction ? window.env?.REALM : process.env.REACT_APP_REALM
const parsedRealm = z.string().safeParse(oidpRealm)
if (!parsedRealm.success) throw Error(`REALM must be string and exist. REALM:${oidpRealm}`)
export const oidpSettings = { export const oidpSettings = {
server: oidpServer || '', server: oidpServer || '',
clientId: oidpClientId || '', clientId: oidpClientId || '',
realm: oidpRealm || '',
} }

View File

@ -1,9 +1,7 @@
import { makeAutoObservable, runInAction } from "mobx" import { makeAutoObservable, runInAction } from "mobx"
import { User } from "oidc-client-ts";
import { Resource } from "../utils/resource" import { Resource } from "../utils/resource"
import { sleep } from "../utils/async.sleep"; import { sleep } from "../utils/async.sleep";
import { z } from 'zod' import { z } from 'zod'
import { keycloakConfig } from "../..";
export interface UserServer { export interface UserServer {
@ -71,20 +69,20 @@ export class UserStore {
} }
getSessionStorage() { getSessionStorage() {
const oidcStorage = sessionStorage.getItem(`oidc.user:${keycloakConfig.authority}:${keycloakConfig.client_id}`) // const oidcStorage = sessionStorage.getItem(`oidc.user:${keycloakConfig.authority}:${keycloakConfig.client_id}`)
if (!oidcStorage) { // if (!oidcStorage) {
return undefined; // return undefined;
} // }
return User.fromStorageString(oidcStorage) // return User.fromStorageString(oidcStorage)
} }
getUser() { getUser() {
return this.getSessionStorage()?.profile // return this.getSessionStorage()?.profile
} }
getAccessToken() { getAccessToken() {
return this.getSessionStorage()?.access_token // return this.getSessionStorage()?.access_token
} }
} }

View File

@ -1,5 +1,4 @@
import { Button, Container, Flex, Group, Header, Menu, createStyles, rem } from "@mantine/core"; import { Button, Container, Flex, Group, Header, Menu, createStyles, rem } from "@mantine/core";
import { useAuth } from 'react-oidc-context';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAdminRole } from "../../hooks/useAdminRole"; import { useAdminRole } from "../../hooks/useAdminRole";
import { routesPath } from "../../router/routes.path"; import { routesPath } from "../../router/routes.path";
@ -7,6 +6,7 @@ import UserMenu from '../../shared/components/UserMenu';
import ColorSchemeToggle from "../../shared/components/buttons/ColorSchemeToggle"; import ColorSchemeToggle from "../../shared/components/buttons/ColorSchemeToggle";
import Logo from "../../shared/components/images/LogoImage"; import Logo from "../../shared/components/images/LogoImage";
import DrawerMenu from "../../shared/components/menu/DrawerMenu"; import DrawerMenu from "../../shared/components/menu/DrawerMenu";
import keycloak from "../../services/keycloak-config";
const HEADER_HEIGHT = rem(60) const HEADER_HEIGHT = rem(60)
@ -61,7 +61,6 @@ export interface HeaderActionProps {
export const HeaderAction = ({ links }: HeaderActionProps) => { export const HeaderAction = ({ links }: HeaderActionProps) => {
const { classes } = useStyles(); const { classes } = useStyles();
const navigate = useNavigate() const navigate = useNavigate()
const auth = useAuth()
const { isAdmin } = useAdminRole() const { isAdmin } = useAdminRole()
const handleNavigate = (link: string) => { const handleNavigate = (link: string) => {
@ -78,6 +77,8 @@ export const HeaderAction = ({ links }: HeaderActionProps) => {
</Menu> </Menu>
) )
const userName = keycloak.tokenParsed?.preferred_username + (isAdmin ? ' (admin)' : '')
return ( return (
<Header height={HEADER_HEIGHT} sx={{ borderBottom: 0 }}> <Header height={HEADER_HEIGHT} sx={{ borderBottom: 0 }}>
<Container className={classes.inner} fluid> <Container className={classes.inner} fluid>
@ -93,7 +94,7 @@ export const HeaderAction = ({ links }: HeaderActionProps) => {
</Container> </Container>
<Group position="right"> <Group position="right">
<ColorSchemeToggle className={classes.colorToggle} /> <ColorSchemeToggle className={classes.colorToggle} />
<UserMenu user={{ name: auth.user?.profile.preferred_username || "", image: "" }} /> <UserMenu user={{ name: userName, image: "" }} />
</Group> </Group>
</Container> </Container>
</Header > </Header >

View File

@ -1129,6 +1129,13 @@
dependencies: dependencies:
regenerator-runtime "^0.14.0" regenerator-runtime "^0.14.0"
"@babel/runtime@^7.9.0":
version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.1.tgz#431f9a794d173b53720e69a6464abc6f0e2a5c57"
integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.3.3": "@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.3.3":
version "7.24.0" version "7.24.0"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50"
@ -1979,6 +1986,22 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@react-keycloak/core@^3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@react-keycloak/core/-/core-3.2.0.tgz#e46f1951b0d7873f7f2fcd73dd0c270cb0b18db8"
integrity sha512-1yzU7gQzs+6E1v6hGqxy0Q+kpMHg9sEcke2yxZR29WoU8KNE8E50xS6UbI8N7rWsgyYw8r9W1cUPCOF48MYjzw==
dependencies:
react-fast-compare "^3.2.0"
"@react-keycloak/web@^3.4.0":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@react-keycloak/web/-/web-3.4.0.tgz#725d96fab8e5fa47faff9615cc08574e5dff2222"
integrity sha512-yKKSCyqBtn7dt+VckYOW1IM5NW999pPkxDZOXqJ6dfXPXstYhOQCkTZqh8l7UL14PkpsoaHDh7hSJH8whah01g==
dependencies:
"@babel/runtime" "^7.9.0"
"@react-keycloak/core" "^3.2.0"
hoist-non-react-statics "^3.3.2"
"@remix-run/router@1.15.2": "@remix-run/router@1.15.2":
version "1.15.2" version "1.15.2"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.2.tgz#35726510d332ba5349c6398d13259d5da184553d" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.2.tgz#35726510d332ba5349c6398d13259d5da184553d"
@ -3978,11 +4001,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
crypto-js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
crypto-random-string@^2.0.0: crypto-random-string@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
@ -5649,7 +5667,7 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hoist-non-react-statics@^3.3.1: hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -6818,6 +6836,11 @@ jiti@^1.19.1:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
js-sha256@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.11.0.tgz#256a921d9292f7fe98905face82e367abaca9576"
integrity sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -6961,16 +6984,19 @@ jsonpointer@^5.0.0:
object.assign "^4.1.4" object.assign "^4.1.4"
object.values "^1.1.6" object.values "^1.1.6"
jwt-decode@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==
jwt-decode@^4.0.0: jwt-decode@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b"
integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==
keycloak-js@^24.0.1:
version "24.0.1"
resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-24.0.1.tgz#8de412bb2b914457b0dc9387a4a824bd0e8d171d"
integrity sha512-leV4mlpa0dqYUXTAuq1ufUfk8DOSBCembjQwMwzYrM6xfHSKpcZMxviTWXqro52LMSsYAnivSKVNEvBkLzi7Eg==
dependencies:
js-sha256 "^0.11.0"
jwt-decode "^4.0.0"
keycode@2.2.0: keycode@2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
@ -7645,14 +7671,6 @@ obuf@^1.0.0, obuf@^1.1.2:
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
oidc-client-ts@^2.2.4:
version "2.4.0"
resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz#764c8a33de542026e2798de9849ce8049047d7e5"
integrity sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==
dependencies:
crypto-js "^4.2.0"
jwt-decode "^3.1.2"
on-finished@2.4.1: on-finished@2.4.1:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
@ -8685,6 +8703,11 @@ react-error-overlay@^6.0.11:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
react-fast-compare@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
react-i18next@^14.1.0: react-i18next@^14.1.0:
version "14.1.0" version "14.1.0"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-14.1.0.tgz#44da74fbffd416f5d0c5307ef31735cf10cc91d9" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-14.1.0.tgz#44da74fbffd416f5d0c5307ef31735cf10cc91d9"
@ -8708,11 +8731,6 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-oidc-context@^2.2.2:
version "2.3.1"
resolved "https://registry.yarnpkg.com/react-oidc-context/-/react-oidc-context-2.3.1.tgz#04eea5aaea972af1a49de6b8d5ff103b9daa0e73"
integrity sha512-WdhmEU6odNzMk9pvOScxUkf6/1aduiI/nQryr7+iCl2VDnYLASDTIV/zy58KuK4VXG3fBaRKukc/mRpMjF9a3Q==
react-refresh@^0.11.0: react-refresh@^0.11.0:
version "0.11.0" version "0.11.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"