diff --git a/src/frontend/lib/types/Auth.tsx b/src/frontend/lib/types/Auth.tsx index 5f6b0670cfea..959b64e5fe47 100644 --- a/src/frontend/lib/types/Auth.tsx +++ b/src/frontend/lib/types/Auth.tsx @@ -1,5 +1,16 @@ export interface AuthContext { status: number; + user?: { + id: number; + display: string; + has_usable_password: boolean; + username: string; + }; + methods?: { + method: string; + at: number; + username: string; + }[]; data: { flows: Flow[] }; meta: { is_authenticated: boolean }; } diff --git a/src/frontend/src/components/nav/MainMenu.tsx b/src/frontend/src/components/nav/MainMenu.tsx index e18bf98b13f9..12595ac87c69 100644 --- a/src/frontend/src/components/nav/MainMenu.tsx +++ b/src/frontend/src/components/nav/MainMenu.tsx @@ -1,14 +1,19 @@ import { Trans } from '@lingui/react/macro'; import { + Button, Group, + HoverCard, + List, Menu, Skeleton, + Table, Text, UnstyledButton, useMantineColorScheme } from '@mantine/core'; import { IconChevronDown, + IconInfoCircle, IconLogout, IconMoonStars, IconSettings, @@ -18,10 +23,15 @@ import { } from '@tabler/icons-react'; import { Link, useNavigate } from 'react-router-dom'; +import { ApiEndpoints, apiUrl } from '@lib/index'; +import type { AuthContext } from '@lib/types/Auth'; import { useShallow } from 'zustand/react/shallow'; -import { doLogout } from '../../functions/auth'; +import { authApi, doLogout } from '../../functions/auth'; import * as classes from '../../main.css'; +import { parseDate } from '../../pages/Index/Settings/AccountSettings/SecurityContent'; +import { useServerApiState } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; +import type { ServerAPIProps } from '../../states/states'; import { vars } from '../../theme'; export function MainMenu() { @@ -30,72 +40,168 @@ export function MainMenu() { useShallow((state) => [state.user, state.username]) ); const { colorScheme, toggleColorScheme } = useMantineColorScheme(); - + const [server, auth_context] = useServerApiState( + useShallow((state) => [state.server, state.auth_context]) + ); return ( - - - - - {username() ? ( - - {username()} - - ) : ( - - )} - - - - - - - Settings - - } - component={Link} - to='/settings/user' - > - Account Settings - - {user?.is_staff && ( + <> + {user?.is_superuser && ( + + + + + + { + + } + + + )} + + + + + {username() ? ( + + {username()} + + ) : ( + + )} + + + + + + + Settings + } + leftSection={} component={Link} - to='/settings/system' + to='/settings/user' > - System Settings + Account Settings - )} - {user?.is_staff && ( + {user?.is_staff && ( + } + component={Link} + to='/settings/system' + > + System Settings + + )} + {user?.is_staff && ( + } + component={Link} + to='/settings/admin' + > + Admin Center + + )} + {user?.is_staff && } } - component={Link} - to='/settings/admin' + onClick={toggleColorScheme} + leftSection={ + colorScheme === 'dark' ? : + } + c={ + colorScheme === 'dark' + ? vars.colors.yellow[4] + : vars.colors.blue[6] + } > - Admin Center + Change Color Mode - )} - {user?.is_staff && } - : } - c={ - colorScheme === 'dark' ? vars.colors.yellow[4] : vars.colors.blue[6] - } - > - Change Color Mode - - - } - onClick={() => { - doLogout(navigate); - }} - > - Logout - - - + + } + onClick={() => { + doLogout(navigate); + }} + > + Logout + + + + + ); +} + +function AuthContextInformation({ + server, + auth_context +}: Readonly<{ + server: ServerAPIProps; + auth_context: AuthContext | undefined; +}>) { + const [setAuthContext] = useServerApiState( + useShallow((state) => [state.setAuthContext]) + ); + + function fetchAuthContext() { + authApi(apiUrl(ApiEndpoints.auth_session)).then((resp) => { + setAuthContext(resp.data.data); + }); + } + + const rows = [ + { name: 'Server version', value: server.version }, + { name: 'API version', value: server.apiVersion }, + { name: 'User ID', value: auth_context?.user?.id } + ]; + return ( + <> + + + + + Name + + + Value + + + + + {rows.map((element) => ( + + {element.name} + {element.value} + + ))} + +
+ {auth_context?.methods && ( + <> + + Methods + + + + {auth_context?.methods.map((method: any) => ( + + {parseDate(method.at)}: {method.method} + + ))} + + + )} + + ); } diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index 49d583c12364..1f967360c7b7 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -169,6 +169,7 @@ export async function doBasicLogin( */ export const doLogout = async (navigate: NavigateFunction) => { const { clearUserState, isLoggedIn } = useUserState.getState(); + const { setAuthContext } = useServerApiState.getState(); // Logout from the server session if (isLoggedIn() || !!getCsrfCookie()) { @@ -183,6 +184,7 @@ export const doLogout = async (navigate: NavigateFunction) => { clearUserState(); clearCsrfCookie(); + setAuthContext(undefined); navigate('/login'); }; diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx index f03a181215b1..22a68199fd69 100644 --- a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx @@ -388,9 +388,6 @@ function MfaSection() { ); }; - const parseDate = (date: number) => - date == null ? 'Never' : new Date(date * 1000).toLocaleString(); - const rows = useMemo(() => { if (isLoading || !data) return null; return data.map((token: any) => ( @@ -713,3 +710,6 @@ async function runActionWithFallback( }); } } + +export const parseDate = (date: number) => + date == null ? 'Never' : new Date(date * 1000).toLocaleString(); diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index a1b74a5c120d..5ed33e415361 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -14,7 +14,7 @@ interface ServerApiStateProps { fetchServerApiState: () => void; auth_config?: AuthConfig; auth_context?: AuthContext; - setAuthContext: (auth_context: AuthContext) => void; + setAuthContext: (auth_context: AuthContext | undefined) => void; sso_enabled: () => boolean; registration_enabled: () => boolean; sso_registration_enabled: () => boolean; diff --git a/src/frontend/tests/pages/pui_core.spec.ts b/src/frontend/tests/pages/pui_core.spec.ts index 290be4784435..cc7f12d0b2f0 100644 --- a/src/frontend/tests/pages/pui_core.spec.ts +++ b/src/frontend/tests/pages/pui_core.spec.ts @@ -15,7 +15,7 @@ test('Core User/Group/Contact', async ({ browser }) => { // users await loadTab(page, 'Users'); - await page.getByRole('cell', { name: 'admin' }).click(); + await page.getByRole('cell', { name: 'admin', exact: true }).click(); await page.getByText('User: admin', { exact: true }).waitFor(); await page.getByLabel('User Details').waitFor(); await page.getByLabel('breadcrumb-1-users').click();