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:
@ -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'
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 ? (
|
||||
|
@ -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/'
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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}>
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user