2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-09-13 06:01:35 +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:
Oliver
2025-09-05 16:07:32 +10:00
committed by GitHub
parent 9dadc2b475
commit 335d87ef16
5 changed files with 141 additions and 65 deletions

View File

@@ -1,5 +1,16 @@
export interface AuthContext { export interface AuthContext {
status: number; status: number;
user?: {
id: number;
display: string;
has_usable_password: boolean;
username: string;
};
methods?: {
method: string;
at: number;
username: string;
}[];
data: { flows: Flow[] }; data: { flows: Flow[] };
meta: { is_authenticated: boolean }; meta: { is_authenticated: boolean };
} }

View File

@@ -17,7 +17,6 @@ import {
IconUserCog IconUserCog
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { doLogout } from '../../functions/auth'; import { doLogout } from '../../functions/auth';
import * as classes from '../../main.css'; import * as classes from '../../main.css';
@@ -32,70 +31,76 @@ export function MainMenu() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { colorScheme, toggleColorScheme } = useMantineColorScheme();
return ( return (
<Menu width={260} position='bottom-end'> <>
<Menu.Target> <Menu width={260} position='bottom-end'>
<UnstyledButton className={classes.layoutHeaderUser}> <Menu.Target>
<Group gap={7}> <UnstyledButton className={classes.layoutHeaderUser}>
{username() ? ( <Group gap={7}>
<Text fw={500} size='sm' style={{ lineHeight: 1 }} mr={3}> {username() ? (
{username()} <Text fw={500} size='sm' style={{ lineHeight: 1 }} mr={3}>
</Text> {username()}
) : ( </Text>
<Skeleton height={20} width={40} radius={vars.radiusDefault} /> ) : (
)} <Skeleton height={20} width={40} radius={vars.radiusDefault} />
<IconChevronDown /> )}
</Group> <IconChevronDown />
</UnstyledButton> </Group>
</Menu.Target> </UnstyledButton>
<Menu.Dropdown> </Menu.Target>
<Menu.Label> <Menu.Dropdown>
<Trans>Settings</Trans> <Menu.Label>
</Menu.Label> <Trans>Settings</Trans>
<Menu.Item </Menu.Label>
leftSection={<IconUserCog />}
component={Link}
to='/settings/user'
>
<Trans>Account Settings</Trans>
</Menu.Item>
{user?.is_staff && (
<Menu.Item <Menu.Item
leftSection={<IconSettings />} leftSection={<IconUserCog />}
component={Link} component={Link}
to='/settings/system' to='/settings/user'
> >
<Trans>System Settings</Trans> <Trans>Account Settings</Trans>
</Menu.Item> </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 <Menu.Item
leftSection={<IconUserBolt />} onClick={toggleColorScheme}
component={Link} leftSection={
to='/settings/admin' 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> </Menu.Item>
)} <Menu.Divider />
{user?.is_staff && <Menu.Divider />} <Menu.Item
<Menu.Item leftSection={<IconLogout />}
onClick={toggleColorScheme} onClick={() => {
leftSection={colorScheme === 'dark' ? <IconSun /> : <IconMoonStars />} doLogout(navigate);
c={ }}
colorScheme === 'dark' ? vars.colors.yellow[4] : vars.colors.blue[6] >
} <Trans>Logout</Trans>
> </Menu.Item>
<Trans>Change Color Mode</Trans> </Menu.Dropdown>
</Menu.Item> </Menu>
<Menu.Divider /> </>
<Menu.Item
leftSection={<IconLogout />}
onClick={() => {
doLogout(navigate);
}}
>
<Trans>Logout</Trans>
</Menu.Item>
</Menu.Dropdown>
</Menu>
); );
} }

View File

@@ -169,6 +169,7 @@ export async function doBasicLogin(
*/ */
export const doLogout = async (navigate: NavigateFunction) => { export const doLogout = async (navigate: NavigateFunction) => {
const { clearUserState, isLoggedIn } = useUserState.getState(); const { clearUserState, isLoggedIn } = useUserState.getState();
const { setAuthContext } = useServerApiState.getState();
// Logout from the server session // Logout from the server session
if (isLoggedIn() || !!getCsrfCookie()) { if (isLoggedIn() || !!getCsrfCookie()) {
@@ -183,6 +184,7 @@ export const doLogout = async (navigate: NavigateFunction) => {
clearUserState(); clearUserState();
clearCsrfCookie(); clearCsrfCookie();
setAuthContext(undefined);
navigate('/login'); navigate('/login');
}; };

View File

@@ -5,6 +5,7 @@ import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { import {
Accordion, Accordion,
ActionIcon,
Alert, Alert,
Badge, Badge,
Button, Button,
@@ -27,15 +28,17 @@ import {
IconAlertCircle, IconAlertCircle,
IconAt, IconAt,
IconExclamationCircle, IconExclamationCircle,
IconRefresh,
IconX IconX
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { api } from '../../../../App'; import { api } from '../../../../App';
import { StylishText } from '../../../../components/items/StylishText'; import { StylishText } from '../../../../components/items/StylishText';
import { ProviderLogin, authApi } from '../../../../functions/auth'; import { ProviderLogin, authApi } from '../../../../functions/auth';
import { useServerApiState } from '../../../../states/ServerApiState'; import { useServerApiState } from '../../../../states/ServerApiState';
import { useUserState } from '../../../../states/UserState';
import { ApiTokenTable } from '../../../../tables/settings/ApiTokenTable'; import { ApiTokenTable } from '../../../../tables/settings/ApiTokenTable';
import { QrRegistrationForm } from './QrRegistrationForm'; import { QrRegistrationForm } from './QrRegistrationForm';
import { useReauth } from './useConfirm'; import { useReauth } from './useConfirm';
@@ -45,9 +48,11 @@ export function SecurityContent() {
useShallow((state) => [state.auth_config, state.sso_enabled]) useShallow((state) => [state.auth_config, state.sso_enabled])
); );
const user = useUserState();
return ( return (
<Stack> <Stack>
<Accordion multiple defaultValue={['email', 'sso', 'mfa', 'token']}> <Accordion multiple defaultValue={['email']}>
<Accordion.Item value='email'> <Accordion.Item value='email'>
<Accordion.Control> <Accordion.Control>
<StylishText size='lg'>{t`Email Addresses`}</StylishText> <StylishText size='lg'>{t`Email Addresses`}</StylishText>
@@ -90,11 +95,64 @@ export function SecurityContent() {
<ApiTokenTable only_myself /> <ApiTokenTable only_myself />
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </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> </Accordion>
</Stack> </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() { function EmailSection() {
const [selectedEmail, setSelectedEmail] = useState<string>(''); const [selectedEmail, setSelectedEmail] = useState<string>('');
const [newEmailValue, setNewEmailValue] = useState(''); 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(() => { const rows = useMemo(() => {
if (isLoading || !data) return null; if (isLoading || !data) return null;
return data.map((token: any) => ( 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();

View File

@@ -14,7 +14,7 @@ interface ServerApiStateProps {
fetchServerApiState: () => Promise<void>; fetchServerApiState: () => Promise<void>;
auth_config?: AuthConfig; auth_config?: AuthConfig;
auth_context?: AuthContext; auth_context?: AuthContext;
setAuthContext: (auth_context: AuthContext) => void; setAuthContext: (auth_context: AuthContext | undefined) => void;
sso_enabled: () => boolean; sso_enabled: () => boolean;
registration_enabled: () => boolean; registration_enabled: () => boolean;
sso_registration_enabled: () => boolean; sso_registration_enabled: () => boolean;