mirror of
https://github.com/inventree/InvenTree.git
synced 2025-09-13 14:11:37 +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 {
|
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 };
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user