2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 20:45:44 +00:00

implement more provider stuff

This commit is contained in:
Matthias Mair
2025-01-07 16:56:43 +01:00
parent 27e31e5a75
commit 2e6ba4de91
8 changed files with 118 additions and 129 deletions

View File

@ -1314,6 +1314,7 @@ HEADLESS_FRONTEND_URLS = {
'account_reset_password': 'http://localhost:8000/password-reset', 'account_reset_password': 'http://localhost:8000/password-reset',
'account_reset_password_from_key': 'http://localhost:8000/password-reset-key/{key}', # noqa: RUF027 'account_reset_password_from_key': 'http://localhost:8000/password-reset-key/{key}', # noqa: RUF027
'account_signup': 'http://localhost:8000/signup', 'account_signup': 'http://localhost:8000/signup',
'socialaccount_login_error': 'http://localhost:8000/social-login-error',
} }
HEADLESS_ONLY = True HEADLESS_ONLY = True
HEADLESS_TOKEN_STRATEGY = 'InvenTree.auth_overrides.DRFTokenStrategy' HEADLESS_TOKEN_STRATEGY = 'InvenTree.auth_overrides.DRFTokenStrategy'

View File

@ -15,10 +15,7 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { showNotification } from '@mantine/notifications'; import { ProviderLogin } from '../../functions/auth';
import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
import type { Provider } from '../../states/states'; import type { Provider } from '../../states/states';
const brandIcons: { [key: string]: JSX.Element } = { const brandIcons: { [key: string]: JSX.Element } = {
@ -36,43 +33,17 @@ const brandIcons: { [key: string]: JSX.Element } = {
}; };
export function SsoButton({ provider }: Readonly<{ provider: Provider }>) { export function SsoButton({ provider }: Readonly<{ provider: Provider }>) {
function login() {
// set preferred provider
api
.put(
apiUrl(ApiEndpoints.ui_preference),
{ preferred_method: 'pui' },
{ headers: { Authorization: '' } }
)
.then(() => {
// redirect to login
window.location.href = provider.login;
})
.catch(() => {
showNotification({
title: t`Error`,
message: t`Sign in redirect failed.`,
color: 'red'
});
});
}
return ( return (
<Tooltip <Tooltip
label={ label={t`You will be redirected to the provider for further actions.`}
provider.login
? t`You will be redirected to the provider for further actions.`
: t`This provider is not full set up.`
}
> >
<Button <Button
leftSection={getBrandIcon(provider)} leftSection={getBrandIcon(provider)}
radius='xl' radius='xl'
component='a' component='a'
onClick={login} onClick={() => ProviderLogin(provider)}
disabled={!provider.login}
> >
{provider.display_name} {provider.name}
</Button> </Button>
</Tooltip> </Tooltip>
); );

View File

@ -34,7 +34,12 @@ export function AuthenticationForm() {
}); });
const simpleForm = useForm({ initialValues: { email: '' } }); const simpleForm = useForm({ initialValues: { email: '' } });
const [classicLoginMode, setMode] = useDisclosure(true); const [classicLoginMode, setMode] = useDisclosure(true);
const [auth_settings] = useServerApiState((state) => [state.auth_settings]); const [auth_settings, sso_enabled, password_forgotten_enabled] =
useServerApiState((state) => [
state.auth_settings,
state.sso_enabled,
state.password_forgotten_enabled
]);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { isLoggedIn } = useUserState(); const { isLoggedIn } = useUserState();
@ -98,10 +103,10 @@ export function AuthenticationForm() {
return ( return (
<> <>
{auth_settings?.sso_enabled === true ? ( {sso_enabled() ? (
<> <>
<Group grow mb='md' mt='md'> <Group grow mb='md' mt='md'>
{auth_settings.providers.map((provider) => ( {auth_settings?.socialaccount.providers.map((provider) => (
<SsoButton provider={provider} key={provider.id} /> <SsoButton provider={provider} key={provider.id} />
))} ))}
</Group> </Group>
@ -130,7 +135,7 @@ export function AuthenticationForm() {
placeholder={t`Your password`} placeholder={t`Your password`}
{...classicForm.getInputProps('password')} {...classicForm.getInputProps('password')}
/> />
{auth_settings?.password_forgotten_enabled === true && ( {password_forgotten_enabled() === true && (
<Group justify='space-between' mt='0'> <Group justify='space-between' mt='0'>
<Anchor <Anchor
component='button' component='button'
@ -194,7 +199,12 @@ export function RegistrationForm() {
initialValues: { username: '', email: '', password1: '', password2: '' } initialValues: { username: '', email: '', password1: '', password2: '' }
}); });
const navigate = useNavigate(); const navigate = useNavigate();
const [auth_settings] = useServerApiState((state) => [state.auth_settings]); const [auth_settings, registration_enabled, sso_registration] =
useServerApiState((state) => [
state.auth_settings,
state.registration_enabled,
state.sso_registration_enabled
]);
const [isRegistering, setIsRegistering] = useState<boolean>(false); const [isRegistering, setIsRegistering] = useState<boolean>(false);
function handleRegistration() { function handleRegistration() {
@ -232,11 +242,10 @@ export function RegistrationForm() {
}); });
} }
const both_reg_enabled = const both_reg_enabled = registration_enabled() && sso_registration();
auth_settings?.registration_enabled && auth_settings?.sso_registration;
return ( return (
<> <>
{auth_settings?.registration_enabled && ( {registration_enabled() && (
<form onSubmit={registrationForm.onSubmit(() => {})}> <form onSubmit={registrationForm.onSubmit(() => {})}>
<Stack gap={0}> <Stack gap={0}>
<TextInput <TextInput
@ -285,9 +294,9 @@ export function RegistrationForm() {
{both_reg_enabled && ( {both_reg_enabled && (
<Divider label={t`Or use SSO`} labelPosition='center' my='lg' /> <Divider label={t`Or use SSO`} labelPosition='center' my='lg' />
)} )}
{auth_settings?.sso_registration === true && ( {sso_registration() && (
<Group grow mb='md' mt='md'> <Group grow mb='md' mt='md'>
{auth_settings.providers.map((provider) => ( {auth_settings?.socialaccount.providers.map((provider) => (
<SsoButton provider={provider} key={provider.id} /> <SsoButton provider={provider} key={provider.id} />
))} ))}
</Group> </Group>
@ -303,13 +312,13 @@ export function ModeSelector({
loginMode: boolean; loginMode: boolean;
setMode: any; setMode: any;
}>) { }>) {
const [auth_settings] = useServerApiState((state) => [state.auth_settings]); const [sso_registration, registration_enabled] = useServerApiState(
const registration_enabled = (state) => [state.sso_registration_enabled, state.registration_enabled]
auth_settings?.registration_enabled || );
auth_settings?.sso_registration || const both_reg_enabled =
false; registration_enabled() || sso_registration() || false;
if (registration_enabled === false) return null; if (both_reg_enabled === false) return null;
return ( return (
<Text ta='center' size={'xs'} mt={'md'}> <Text ta='center' size={'xs'} mt={'md'}>
{loginMode ? ( {loginMode ? (

View File

@ -19,14 +19,14 @@ export enum ApiEndpoints {
user_reset = 'auth/password/reset/', user_reset = 'auth/password/reset/',
user_reset_set = 'auth/password/reset/confirm/', user_reset_set = 'auth/password/reset/confirm/',
user_change_password = 'auth/password/change/', user_change_password = 'auth/password/change/',
user_sso = 'auth/social/', user_sso = '_allauth/app/v1/account/providers',
user_sso_remove = 'auth/social/:id/disconnect/',
user_login = '_allauth/app/v1/auth/login', user_login = '_allauth/app/v1/auth/login',
user_login_mfa = '_allauth/app/v1/auth/2fa/authenticate', user_login_mfa = '_allauth/app/v1/auth/2fa/authenticate',
user_logout = '_allauth/app/v1/auth/session', user_logout = '_allauth/app/v1/auth/session',
user_register = 'auth/registration/', user_register = 'auth/registration/',
user_mfa = '_allauth/app/v1/account/authenticators', user_mfa = '_allauth/app/v1/account/authenticators',
user_emails = '_allauth/app/v1/account/email', user_emails = '_allauth/app/v1/account/email',
login_provider_redirect = '_allauth/browser/v1/auth/provider/redirect',
// Generic API endpoints // Generic API endpoints
currency_list = 'currency/exchange/', currency_list = 'currency/exchange/',
@ -44,14 +44,13 @@ export enum ApiEndpoints {
custom_state_list = 'generic/status/custom/', custom_state_list = 'generic/status/custom/',
version = 'version/', version = 'version/',
license = 'license/', license = 'license/',
sso_providers = 'auth/providers/',
group_list = 'user/group/', group_list = 'user/group/',
owner_list = 'user/owner/', owner_list = 'user/owner/',
content_type_list = 'contenttype/', content_type_list = 'contenttype/',
icons = 'icons/', icons = 'icons/',
selectionlist_list = 'selection/', selectionlist_list = 'selection/',
selectionlist_detail = 'selection/:id/', selectionlist_detail = 'selection/:id/',
securtiy_settings = '_allauth/app/v1/config', securtiy_settings = '_allauth/browser/v1/config',
// Barcode API endpoints // Barcode API endpoints
barcode = 'barcode/', barcode = 'barcode/',
@ -221,6 +220,5 @@ export enum ApiEndpoints {
error_report_list = 'error-report/', error_report_list = 'error-report/',
project_code_list = 'project-code/', project_code_list = 'project-code/',
custom_unit_list = 'units/', custom_unit_list = 'units/',
ui_preference = 'web/ui_preference/',
notes_image_upload = 'notes-image-upload/' notes_image_upload = 'notes-image-upload/'
} }

View File

@ -7,7 +7,7 @@ import { ApiEndpoints } from '../enums/ApiEndpoints';
import { apiUrl } from '../states/ApiState'; import { apiUrl } from '../states/ApiState';
import { useLocalState } from '../states/LocalState'; import { useLocalState } from '../states/LocalState';
import { useUserState } from '../states/UserState'; import { useUserState } from '../states/UserState';
import { fetchGlobalStates } from '../states/states'; import { type Provider, fetchGlobalStates } from '../states/states';
import { showLoginNotification } from './notifications'; import { showLoginNotification } from './notifications';
export function followRedirect(navigate: NavigateFunction, redirect: any) { export function followRedirect(navigate: NavigateFunction, redirect: any) {
@ -293,3 +293,20 @@ export function clearCsrfCookie() {
document.cookie = document.cookie =
'csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; 'csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
} }
export function ProviderLogin(
provider: Provider,
process: 'login' | 'connect' = 'login'
) {
const { host } = useLocalState.getState();
// TODO
const loc = window.location;
const values = {
provider: provider.id,
callback_url: 'http://localhost:8000/logged-in',
process: process,
csrfmiddlewaretoken: getCsrfCookie()
};
const url = `${host}${apiUrl(ApiEndpoints.login_provider_redirect)}`;
post(url, values);
}

View File

@ -11,8 +11,7 @@ import {
Table, Table,
Text, Text,
TextInput, TextInput,
Title, Title
Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { IconAlertCircle, IconAt } from '@tabler/icons-react'; import { IconAlertCircle, IconAt } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
@ -22,28 +21,15 @@ import { api, queryClient } from '../../../../App';
import { YesNoButton } from '../../../../components/buttons/YesNoButton'; import { YesNoButton } from '../../../../components/buttons/YesNoButton';
import { PlaceholderPill } from '../../../../components/items/Placeholder'; import { PlaceholderPill } from '../../../../components/items/Placeholder';
import { ApiEndpoints } from '../../../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
import { apiUrl } from '../../../../states/ApiState'; import { ProviderLogin } from '../../../../functions/auth';
import { apiUrl, useServerApiState } from '../../../../states/ApiState';
import { useUserState } from '../../../../states/UserState'; import { useUserState } from '../../../../states/UserState';
import type { Provider, SecuritySetting } from '../../../../states/states';
export function SecurityContent() { export function SecurityContent() {
const [isSsoEnabled, setIsSsoEnabled] = useState<boolean>(false); const [auth_settings, sso_enabled, mfa_enabled] = useServerApiState(
const [isMfaEnabled, setIsMfaEnabled] = useState<boolean>(false); (state) => [state.auth_settings, state.sso_enabled, state.mfa_enabled]
);
const { isLoading: isLoadingProvider, data: dataProvider } = useQuery({
queryKey: ['sso-providers'],
queryFn: () =>
api.get(apiUrl(ApiEndpoints.sso_providers)).then((res) => res.data)
});
// evaluate if security options are enabled
useEffect(() => {
if (dataProvider === undefined) return;
// check if SSO is enabled on the server
setIsSsoEnabled(dataProvider.sso_enabled || false);
// check if MFa is enabled
setIsMfaEnabled(dataProvider.mfa_required || false);
}, [dataProvider]);
return ( return (
<Stack> <Stack>
@ -54,8 +40,8 @@ export function SecurityContent() {
<Title order={5}> <Title order={5}>
<Trans>Single Sign On Accounts</Trans> <Trans>Single Sign On Accounts</Trans>
</Title> </Title>
{isSsoEnabled ? ( {sso_enabled() ? (
<SsoContent dataProvider={dataProvider} /> <SsoContent auth_settings={auth_settings} />
) : ( ) : (
<Alert <Alert
icon={<IconAlertCircle size='1rem' />} icon={<IconAlertCircle size='1rem' />}
@ -69,7 +55,7 @@ export function SecurityContent() {
<Trans>Multifactor</Trans> <Trans>Multifactor</Trans>
</Title> </Title>
{isMfaEnabled ? ( {mfa_enabled() ? (
<MfaContent /> <MfaContent />
) : ( ) : (
<Alert <Alert
@ -249,17 +235,36 @@ function EmailContent() {
); );
} }
function SsoContent({ dataProvider }: Readonly<{ dataProvider: any }>) { function ProviderButton({ provider }: Readonly<{ provider: Provider }>) {
return (
<Button
key={provider.id}
variant='outline'
onClick={() => ProviderLogin(provider, 'connect')}
>
<Group justify='space-between'>{provider.name}</Group>
</Button>
);
}
function SsoContent({
auth_settings
}: Readonly<{ auth_settings: SecuritySetting | undefined }>) {
const [value, setValue] = useState<string>(''); const [value, setValue] = useState<string>('');
const [currentProviders, setCurrentProviders] = useState<[]>(); const [currentProviders, setCurrentProviders] = useState<Provider[]>();
const { session } = useUserState.getState();
const { isLoading, data } = useQuery({ const { isLoading, data } = useQuery({
queryKey: ['sso-list'], queryKey: ['sso-list'],
queryFn: () => queryFn: () =>
api.get(apiUrl(ApiEndpoints.user_sso)).then((res) => res.data) api
.get(apiUrl(ApiEndpoints.user_sso), {
headers: { 'X-Session-Token': session }
})
.then((res) => res.data.data)
}); });
useEffect(() => { useEffect(() => {
if (dataProvider === undefined) return; if (auth_settings === undefined) return;
if (data === undefined) return; if (data === undefined) return;
const configuredProviders = data.map((item: any) => { const configuredProviders = data.map((item: any) => {
@ -270,14 +275,16 @@ function SsoContent({ dataProvider }: Readonly<{ dataProvider: any }>) {
} }
// remove providers that are used currently // remove providers that are used currently
let newData = dataProvider.providers; const newData =
newData = newData.filter(isAlreadyInUse); auth_settings.socialaccount.providers.filter(isAlreadyInUse);
setCurrentProviders(newData); setCurrentProviders(newData);
}, [dataProvider, data]); }, [auth_settings, data]);
function removeProvider() { function removeProvider() {
api api
.post(apiUrl(ApiEndpoints.user_sso_remove, undefined, { id: value })) .delete(apiUrl(ApiEndpoints.user_sso), {
headers: { 'X-Session-Token': session }
})
.then(() => { .then(() => {
queryClient.removeQueries({ queryClient.removeQueries({
queryKey: ['sso-list'] queryKey: ['sso-list']
@ -289,28 +296,6 @@ function SsoContent({ dataProvider }: Readonly<{ dataProvider: any }>) {
/* renderer */ /* renderer */
if (isLoading) return <Loader />; if (isLoading) return <Loader />;
function ProviderButton({ provider }: Readonly<{ provider: any }>) {
const button = (
<Button
key={provider.id}
component='a'
href={provider.connect}
variant='outline'
disabled={!provider.configured}
>
<Group justify='space-between'>
{provider.display_name}
{provider.configured == false && <IconAlertCircle />}
</Group>
</Button>
);
if (provider.configured) return button;
return (
<Tooltip label={t`Provider has not been configured`}>{button}</Tooltip>
);
}
return ( return (
<Grid> <Grid>
<Grid.Col span={6}> <Grid.Col span={6}>

View File

@ -11,11 +11,16 @@ interface ServerApiStateProps {
setServer: (newServer: ServerAPIProps) => void; setServer: (newServer: ServerAPIProps) => void;
fetchServerApiState: () => void; fetchServerApiState: () => void;
auth_settings?: SecuritySetting; auth_settings?: SecuritySetting;
sso_enabled: () => boolean;
mfa_enabled: () => boolean;
registration_enabled: () => boolean;
sso_registration_enabled: () => boolean;
password_forgotten_enabled: () => boolean;
} }
export const useServerApiState = create<ServerApiStateProps>()( export const useServerApiState = create<ServerApiStateProps>()(
persist( persist(
(set) => ({ (set, get) => ({
server: emptyServerAPI, server: emptyServerAPI,
setServer: (newServer: ServerAPIProps) => set({ server: newServer }), setServer: (newServer: ServerAPIProps) => set({ server: newServer }),
fetchServerApiState: async () => { fetchServerApiState: async () => {
@ -24,6 +29,7 @@ export const useServerApiState = create<ServerApiStateProps>()(
.get(apiUrl(ApiEndpoints.api_server_info)) .get(apiUrl(ApiEndpoints.api_server_info))
.then((response) => { .then((response) => {
set({ server: response.data }); set({ server: response.data });
// set sso_enabled
}) })
.catch(() => { .catch(() => {
console.error('ERR: Error fetching server info'); console.error('ERR: Error fetching server info');
@ -41,7 +47,27 @@ export const useServerApiState = create<ServerApiStateProps>()(
console.error('ERR: Error fetching SSO information'); console.error('ERR: Error fetching SSO information');
}); });
}, },
status: undefined auth_settings: undefined,
sso_enabled: () => {
const data = get().auth_settings?.socialaccount.providers;
return !(data === undefined || data.length == 0);
},
mfa_enabled: () => {
// TODO
return true;
},
registration_enabled: () => {
// TODO
return false;
},
sso_registration_enabled: () => {
// TODO
return false;
},
password_forgotten_enabled: () => {
// TODO
return false;
}
}), }),
{ {
name: 'server-api-state', name: 'server-api-state',

View File

@ -50,15 +50,6 @@ export interface ServerAPIProps {
django_admin: null | string; django_admin: null | string;
} }
export interface AuthProps {
sso_enabled: boolean;
sso_registration: boolean;
mfa_required: boolean;
providers: Provider[];
registration_enabled: boolean;
password_forgotten_enabled: boolean;
}
export interface SecuritySetting { export interface SecuritySetting {
account: { account: {
authentication_method: string; authentication_method: string;
@ -79,15 +70,6 @@ export interface Provider {
client_id: string; client_id: string;
} }
export interface Provider {
id: string;
name: string;
configured: boolean;
login: string;
connect: string;
display_name: string;
}
// Type interface defining a single 'setting' object // Type interface defining a single 'setting' object
export interface Setting { export interface Setting {
pk: number; pk: number;