2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-16 20:15: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_from_key': 'http://localhost:8000/password-reset-key/{key}', # noqa: RUF027
'account_signup': 'http://localhost:8000/signup',
'socialaccount_login_error': 'http://localhost:8000/social-login-error',
}
HEADLESS_ONLY = True
HEADLESS_TOKEN_STRATEGY = 'InvenTree.auth_overrides.DRFTokenStrategy'

View File

@ -15,10 +15,7 @@ import {
} from '@tabler/icons-react';
import { t } from '@lingui/macro';
import { showNotification } from '@mantine/notifications';
import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
import { ProviderLogin } from '../../functions/auth';
import type { Provider } from '../../states/states';
const brandIcons: { [key: string]: JSX.Element } = {
@ -36,43 +33,17 @@ const brandIcons: { [key: string]: JSX.Element } = {
};
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 (
<Tooltip
label={
provider.login
? t`You will be redirected to the provider for further actions.`
: t`This provider is not full set up.`
}
label={t`You will be redirected to the provider for further actions.`}
>
<Button
leftSection={getBrandIcon(provider)}
radius='xl'
component='a'
onClick={login}
disabled={!provider.login}
onClick={() => ProviderLogin(provider)}
>
{provider.display_name}
{provider.name}
</Button>
</Tooltip>
);

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { ApiEndpoints } from '../enums/ApiEndpoints';
import { apiUrl } from '../states/ApiState';
import { useLocalState } from '../states/LocalState';
import { useUserState } from '../states/UserState';
import { fetchGlobalStates } from '../states/states';
import { type Provider, fetchGlobalStates } from '../states/states';
import { showLoginNotification } from './notifications';
export function followRedirect(navigate: NavigateFunction, redirect: any) {
@ -293,3 +293,20 @@ export function clearCsrfCookie() {
document.cookie =
'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,
Text,
TextInput,
Title,
Tooltip
Title
} from '@mantine/core';
import { IconAlertCircle, IconAt } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
@ -22,28 +21,15 @@ import { api, queryClient } from '../../../../App';
import { YesNoButton } from '../../../../components/buttons/YesNoButton';
import { PlaceholderPill } from '../../../../components/items/Placeholder';
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 type { Provider, SecuritySetting } from '../../../../states/states';
export function SecurityContent() {
const [isSsoEnabled, setIsSsoEnabled] = useState<boolean>(false);
const [isMfaEnabled, setIsMfaEnabled] = useState<boolean>(false);
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]);
const [auth_settings, sso_enabled, mfa_enabled] = useServerApiState(
(state) => [state.auth_settings, state.sso_enabled, state.mfa_enabled]
);
return (
<Stack>
@ -54,8 +40,8 @@ export function SecurityContent() {
<Title order={5}>
<Trans>Single Sign On Accounts</Trans>
</Title>
{isSsoEnabled ? (
<SsoContent dataProvider={dataProvider} />
{sso_enabled() ? (
<SsoContent auth_settings={auth_settings} />
) : (
<Alert
icon={<IconAlertCircle size='1rem' />}
@ -69,7 +55,7 @@ export function SecurityContent() {
<Trans>Multifactor</Trans>
</Title>
{isMfaEnabled ? (
{mfa_enabled() ? (
<MfaContent />
) : (
<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 [currentProviders, setCurrentProviders] = useState<[]>();
const [currentProviders, setCurrentProviders] = useState<Provider[]>();
const { session } = useUserState.getState();
const { isLoading, data } = useQuery({
queryKey: ['sso-list'],
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(() => {
if (dataProvider === undefined) return;
if (auth_settings === undefined) return;
if (data === undefined) return;
const configuredProviders = data.map((item: any) => {
@ -270,14 +275,16 @@ function SsoContent({ dataProvider }: Readonly<{ dataProvider: any }>) {
}
// remove providers that are used currently
let newData = dataProvider.providers;
newData = newData.filter(isAlreadyInUse);
const newData =
auth_settings.socialaccount.providers.filter(isAlreadyInUse);
setCurrentProviders(newData);
}, [dataProvider, data]);
}, [auth_settings, data]);
function removeProvider() {
api
.post(apiUrl(ApiEndpoints.user_sso_remove, undefined, { id: value }))
.delete(apiUrl(ApiEndpoints.user_sso), {
headers: { 'X-Session-Token': session }
})
.then(() => {
queryClient.removeQueries({
queryKey: ['sso-list']
@ -289,28 +296,6 @@ function SsoContent({ dataProvider }: Readonly<{ dataProvider: any }>) {
/* renderer */
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 (
<Grid>
<Grid.Col span={6}>

View File

@ -11,11 +11,16 @@ interface ServerApiStateProps {
setServer: (newServer: ServerAPIProps) => void;
fetchServerApiState: () => void;
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>()(
persist(
(set) => ({
(set, get) => ({
server: emptyServerAPI,
setServer: (newServer: ServerAPIProps) => set({ server: newServer }),
fetchServerApiState: async () => {
@ -24,6 +29,7 @@ export const useServerApiState = create<ServerApiStateProps>()(
.get(apiUrl(ApiEndpoints.api_server_info))
.then((response) => {
set({ server: response.data });
// set sso_enabled
})
.catch(() => {
console.error('ERR: Error fetching server info');
@ -41,7 +47,27 @@ export const useServerApiState = create<ServerApiStateProps>()(
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',

View File

@ -50,15 +50,6 @@ export interface ServerAPIProps {
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 {
account: {
authentication_method: string;
@ -79,15 +70,6 @@ export interface Provider {
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
export interface Setting {
pk: number;