mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Auth session info (#10271)
* https://github.com/inventree/InvenTree/pull/6293 * refactor to a shared component * refactoring container stuff to a wrapper * move title to wrapper * move logoff and loader to wrapper * mvoe functions to general auth * seperate login and register into seperate pages * unify auth styling * rename component * adapt to new look * check if registration is enabled * feat(frontend):add authentication debug window * clear state on logout * add reload button * reduce diff * export helper * move hover out * only show to superusers * fix state args * fix merge * fix merge * clean up diff * reduce diff * re-diff * fix shallow loading * fix test * fix umport * Move session info to user settings panel * Restrict to superuser accounts --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
		| @@ -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 }; | ||||
| } | ||||
|   | ||||
| @@ -17,7 +17,6 @@ import { | ||||
|   IconUserCog | ||||
| } from '@tabler/icons-react'; | ||||
| import { Link, useNavigate } from 'react-router-dom'; | ||||
|  | ||||
| import { useShallow } from 'zustand/react/shallow'; | ||||
| import { doLogout } from '../../functions/auth'; | ||||
| import * as classes from '../../main.css'; | ||||
| @@ -32,70 +31,76 @@ export function MainMenu() { | ||||
|   const { colorScheme, toggleColorScheme } = useMantineColorScheme(); | ||||
|  | ||||
|   return ( | ||||
|     <Menu width={260} position='bottom-end'> | ||||
|       <Menu.Target> | ||||
|         <UnstyledButton className={classes.layoutHeaderUser}> | ||||
|           <Group gap={7}> | ||||
|             {username() ? ( | ||||
|               <Text fw={500} size='sm' style={{ lineHeight: 1 }} mr={3}> | ||||
|                 {username()} | ||||
|               </Text> | ||||
|             ) : ( | ||||
|               <Skeleton height={20} width={40} radius={vars.radiusDefault} /> | ||||
|             )} | ||||
|             <IconChevronDown /> | ||||
|           </Group> | ||||
|         </UnstyledButton> | ||||
|       </Menu.Target> | ||||
|       <Menu.Dropdown> | ||||
|         <Menu.Label> | ||||
|           <Trans>Settings</Trans> | ||||
|         </Menu.Label> | ||||
|         <Menu.Item | ||||
|           leftSection={<IconUserCog />} | ||||
|           component={Link} | ||||
|           to='/settings/user' | ||||
|         > | ||||
|           <Trans>Account Settings</Trans> | ||||
|         </Menu.Item> | ||||
|         {user?.is_staff && ( | ||||
|     <> | ||||
|       <Menu width={260} position='bottom-end'> | ||||
|         <Menu.Target> | ||||
|           <UnstyledButton className={classes.layoutHeaderUser}> | ||||
|             <Group gap={7}> | ||||
|               {username() ? ( | ||||
|                 <Text fw={500} size='sm' style={{ lineHeight: 1 }} mr={3}> | ||||
|                   {username()} | ||||
|                 </Text> | ||||
|               ) : ( | ||||
|                 <Skeleton height={20} width={40} radius={vars.radiusDefault} /> | ||||
|               )} | ||||
|               <IconChevronDown /> | ||||
|             </Group> | ||||
|           </UnstyledButton> | ||||
|         </Menu.Target> | ||||
|         <Menu.Dropdown> | ||||
|           <Menu.Label> | ||||
|             <Trans>Settings</Trans> | ||||
|           </Menu.Label> | ||||
|           <Menu.Item | ||||
|             leftSection={<IconSettings />} | ||||
|             leftSection={<IconUserCog />} | ||||
|             component={Link} | ||||
|             to='/settings/system' | ||||
|             to='/settings/user' | ||||
|           > | ||||
|             <Trans>System Settings</Trans> | ||||
|             <Trans>Account Settings</Trans> | ||||
|           </Menu.Item> | ||||
|         )} | ||||
|         {user?.is_staff && ( | ||||
|           {user?.is_staff && ( | ||||
|             <Menu.Item | ||||
|               leftSection={<IconSettings />} | ||||
|               component={Link} | ||||
|               to='/settings/system' | ||||
|             > | ||||
|               <Trans>System Settings</Trans> | ||||
|             </Menu.Item> | ||||
|           )} | ||||
|           {user?.is_staff && ( | ||||
|             <Menu.Item | ||||
|               leftSection={<IconUserBolt />} | ||||
|               component={Link} | ||||
|               to='/settings/admin' | ||||
|             > | ||||
|               <Trans>Admin Center</Trans> | ||||
|             </Menu.Item> | ||||
|           )} | ||||
|           {user?.is_staff && <Menu.Divider />} | ||||
|           <Menu.Item | ||||
|             leftSection={<IconUserBolt />} | ||||
|             component={Link} | ||||
|             to='/settings/admin' | ||||
|             onClick={toggleColorScheme} | ||||
|             leftSection={ | ||||
|               colorScheme === 'dark' ? <IconSun /> : <IconMoonStars /> | ||||
|             } | ||||
|             c={ | ||||
|               colorScheme === 'dark' | ||||
|                 ? vars.colors.yellow[4] | ||||
|                 : vars.colors.blue[6] | ||||
|             } | ||||
|           > | ||||
|             <Trans>Admin Center</Trans> | ||||
|             <Trans>Change Color Mode</Trans> | ||||
|           </Menu.Item> | ||||
|         )} | ||||
|         {user?.is_staff && <Menu.Divider />} | ||||
|         <Menu.Item | ||||
|           onClick={toggleColorScheme} | ||||
|           leftSection={colorScheme === 'dark' ? <IconSun /> : <IconMoonStars />} | ||||
|           c={ | ||||
|             colorScheme === 'dark' ? vars.colors.yellow[4] : vars.colors.blue[6] | ||||
|           } | ||||
|         > | ||||
|           <Trans>Change Color Mode</Trans> | ||||
|         </Menu.Item> | ||||
|         <Menu.Divider /> | ||||
|         <Menu.Item | ||||
|           leftSection={<IconLogout />} | ||||
|           onClick={() => { | ||||
|             doLogout(navigate); | ||||
|           }} | ||||
|         > | ||||
|           <Trans>Logout</Trans> | ||||
|         </Menu.Item> | ||||
|       </Menu.Dropdown> | ||||
|     </Menu> | ||||
|           <Menu.Divider /> | ||||
|           <Menu.Item | ||||
|             leftSection={<IconLogout />} | ||||
|             onClick={() => { | ||||
|               doLogout(navigate); | ||||
|             }} | ||||
|           > | ||||
|             <Trans>Logout</Trans> | ||||
|           </Menu.Item> | ||||
|         </Menu.Dropdown> | ||||
|       </Menu> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -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'); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import { t } from '@lingui/core/macro'; | ||||
| import { Trans } from '@lingui/react/macro'; | ||||
| import { | ||||
|   Accordion, | ||||
|   ActionIcon, | ||||
|   Alert, | ||||
|   Badge, | ||||
|   Button, | ||||
| @@ -27,15 +28,17 @@ import { | ||||
|   IconAlertCircle, | ||||
|   IconAt, | ||||
|   IconExclamationCircle, | ||||
|   IconRefresh, | ||||
|   IconX | ||||
| } from '@tabler/icons-react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import { useMemo, useState } from 'react'; | ||||
| import { useCallback, useMemo, useState } from 'react'; | ||||
| import { useShallow } from 'zustand/react/shallow'; | ||||
| import { api } from '../../../../App'; | ||||
| import { StylishText } from '../../../../components/items/StylishText'; | ||||
| import { ProviderLogin, authApi } from '../../../../functions/auth'; | ||||
| import { useServerApiState } from '../../../../states/ServerApiState'; | ||||
| import { useUserState } from '../../../../states/UserState'; | ||||
| import { ApiTokenTable } from '../../../../tables/settings/ApiTokenTable'; | ||||
| import { QrRegistrationForm } from './QrRegistrationForm'; | ||||
| import { useReauth } from './useConfirm'; | ||||
| @@ -45,9 +48,11 @@ export function SecurityContent() { | ||||
|     useShallow((state) => [state.auth_config, state.sso_enabled]) | ||||
|   ); | ||||
|  | ||||
|   const user = useUserState(); | ||||
|  | ||||
|   return ( | ||||
|     <Stack> | ||||
|       <Accordion multiple defaultValue={['email', 'sso', 'mfa', 'token']}> | ||||
|       <Accordion multiple defaultValue={['email']}> | ||||
|         <Accordion.Item value='email'> | ||||
|           <Accordion.Control> | ||||
|             <StylishText size='lg'>{t`Email Addresses`}</StylishText> | ||||
| @@ -90,11 +95,64 @@ export function SecurityContent() { | ||||
|             <ApiTokenTable only_myself /> | ||||
|           </Accordion.Panel> | ||||
|         </Accordion.Item> | ||||
|         {user.isSuperuser() && ( | ||||
|           <Accordion.Item value='session'> | ||||
|             <Accordion.Control> | ||||
|               <StylishText size='lg'>{t`Session Information`}</StylishText> | ||||
|             </Accordion.Control> | ||||
|             <Accordion.Panel> | ||||
|               <AuthContextSection /> | ||||
|             </Accordion.Panel> | ||||
|           </Accordion.Item> | ||||
|         )} | ||||
|       </Accordion> | ||||
|     </Stack> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function AuthContextSection() { | ||||
|   const [auth_context, setAuthContext] = useServerApiState( | ||||
|     useShallow((state) => [state.auth_context, state.setAuthContext]) | ||||
|   ); | ||||
|  | ||||
|   const fetchAuthContext = useCallback(() => { | ||||
|     authApi(apiUrl(ApiEndpoints.auth_session)).then((resp) => { | ||||
|       setAuthContext(resp.data.data); | ||||
|     }); | ||||
|   }, [setAuthContext]); | ||||
|  | ||||
|   return ( | ||||
|     <Stack gap='xs'> | ||||
|       <Group> | ||||
|         <ActionIcon | ||||
|           onClick={fetchAuthContext} | ||||
|           variant='transparent' | ||||
|           aria-label='refresh-auth-context' | ||||
|         > | ||||
|           <IconRefresh /> | ||||
|         </ActionIcon> | ||||
|       </Group> | ||||
|  | ||||
|       <Table> | ||||
|         <Table.Thead> | ||||
|           <Table.Tr> | ||||
|             <Table.Th>{t`Timestamp`}</Table.Th> | ||||
|             <Table.Th>{t`Method`}</Table.Th> | ||||
|           </Table.Tr> | ||||
|         </Table.Thead> | ||||
|         <Table.Tbody> | ||||
|           {auth_context?.methods?.map((method: any, index: number) => ( | ||||
|             <Table.Tr key={`auth-method-${index}`}> | ||||
|               <Table.Td>{parseDate(method.at)}</Table.Td> | ||||
|               <Table.Td>{method.method}</Table.Td> | ||||
|             </Table.Tr> | ||||
|           ))} | ||||
|         </Table.Tbody> | ||||
|       </Table> | ||||
|     </Stack> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function EmailSection() { | ||||
|   const [selectedEmail, setSelectedEmail] = useState<string>(''); | ||||
|   const [newEmailValue, setNewEmailValue] = useState(''); | ||||
| @@ -392,9 +450,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) => ( | ||||
| @@ -719,3 +774,6 @@ async function runActionWithFallback( | ||||
|       }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const parseDate = (date: number) => | ||||
|   date == null ? 'Never' : new Date(date * 1000).toLocaleString(); | ||||
|   | ||||
| @@ -14,7 +14,7 @@ interface ServerApiStateProps { | ||||
|   fetchServerApiState: () => Promise<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; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user