mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-15 19:45:46 +00:00
[PUI] SSO Support (#6333)
* Add sso buttons Fixes #5753 * Added more icons * fix callback url * made heading dynamic * allow either sso or normal reg * Added SSO registration * added divider * added preferred ui API * fix test * fix update function * refactor * fix naming * fix import * add coverage ignore * more ignore * fixed missing key * renamed button * revert coverage statements * set prefered mode before sso login * added dynamic login redirect * fixed test assert * use API Endpoints instead of hardcoding * fix lookup
This commit is contained in:
64
src/frontend/src/components/buttons/SSOButton.tsx
Normal file
64
src/frontend/src/components/buttons/SSOButton.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { Button } from '@mantine/core';
|
||||
import {
|
||||
IconBrandAzure,
|
||||
IconBrandBitbucket,
|
||||
IconBrandDiscord,
|
||||
IconBrandFacebook,
|
||||
IconBrandFlickr,
|
||||
IconBrandGithub,
|
||||
IconBrandGitlab,
|
||||
IconBrandGoogle,
|
||||
IconBrandReddit,
|
||||
IconBrandTwitch,
|
||||
IconBrandTwitter,
|
||||
IconLogin
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { Provider } from '../../states/states';
|
||||
|
||||
const brandIcons: { [key: string]: JSX.Element } = {
|
||||
google: <IconBrandGoogle />,
|
||||
github: <IconBrandGithub />,
|
||||
facebook: <IconBrandFacebook />,
|
||||
discord: <IconBrandDiscord />,
|
||||
twitter: <IconBrandTwitter />,
|
||||
bitbucket: <IconBrandBitbucket />,
|
||||
flickr: <IconBrandFlickr />,
|
||||
gitlab: <IconBrandGitlab />,
|
||||
reddit: <IconBrandReddit />,
|
||||
twitch: <IconBrandTwitch />,
|
||||
microsoft: <IconBrandAzure />
|
||||
};
|
||||
|
||||
export function SsoButton({ provider }: { 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;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
leftIcon={getBrandIcon(provider)}
|
||||
radius="xl"
|
||||
component="a"
|
||||
onClick={login}
|
||||
>
|
||||
{provider.display_name}{' '}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
function getBrandIcon(provider: Provider) {
|
||||
return brandIcons[provider.id] || <IconLogin />;
|
||||
}
|
@ -2,6 +2,7 @@ import { Trans, t } from '@lingui/macro';
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
Loader,
|
||||
PasswordInput,
|
||||
@ -21,6 +22,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { doBasicLogin, doSimpleLogin } from '../../functions/auth';
|
||||
import { apiUrl, useServerApiState } from '../../states/ApiState';
|
||||
import { useSessionState } from '../../states/SessionState';
|
||||
import { SsoButton } from '../buttons/SSOButton';
|
||||
|
||||
export function AuthenticationForm() {
|
||||
const classicForm = useForm({
|
||||
@ -83,76 +85,93 @@ export function AuthenticationForm() {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={classicForm.onSubmit(() => {})}>
|
||||
{classicLoginMode ? (
|
||||
<Stack spacing={0}>
|
||||
<TextInput
|
||||
required
|
||||
label={t`Username`}
|
||||
placeholder={t`Your username`}
|
||||
{...classicForm.getInputProps('username')}
|
||||
/>
|
||||
<PasswordInput
|
||||
required
|
||||
label={t`Password`}
|
||||
placeholder={t`Your password`}
|
||||
{...classicForm.getInputProps('password')}
|
||||
/>
|
||||
{auth_settings?.password_forgotten_enabled === true && (
|
||||
<Group position="apart" mt="0">
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
color="dimmed"
|
||||
size="xs"
|
||||
onClick={() => navigate('/reset-password')}
|
||||
>
|
||||
<Trans>Reset password</Trans>
|
||||
</Anchor>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack>
|
||||
<TextInput
|
||||
required
|
||||
label={t`Email`}
|
||||
description={t`We will send you a link to login - if you are registered`}
|
||||
placeholder="email@example.org"
|
||||
{...simpleForm.getInputProps('email')}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<>
|
||||
{auth_settings?.sso_enabled === true ? (
|
||||
<>
|
||||
<Group grow mb="md" mt="md">
|
||||
{auth_settings.providers.map((provider) => (
|
||||
<SsoButton provider={provider} key={provider.id} />
|
||||
))}
|
||||
</Group>
|
||||
|
||||
<Group position="apart" mt="xl">
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
color="dimmed"
|
||||
size="xs"
|
||||
onClick={() => setMode.toggle()}
|
||||
>
|
||||
{classicLoginMode ? (
|
||||
<Trans>Send me an email</Trans>
|
||||
) : (
|
||||
<Trans>Use username and password</Trans>
|
||||
)}
|
||||
</Anchor>
|
||||
<Button type="submit" disabled={isLoggingIn} onClick={handleLogin}>
|
||||
{isLoggingIn ? (
|
||||
<Loader size="sm" />
|
||||
) : (
|
||||
<>
|
||||
{classicLoginMode ? (
|
||||
<Trans>Log In</Trans>
|
||||
) : (
|
||||
<Trans>Send Email</Trans>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
<Divider
|
||||
label={t`Or continue with other methods`}
|
||||
labelPosition="center"
|
||||
my="lg"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<form onSubmit={classicForm.onSubmit(() => {})}>
|
||||
{classicLoginMode ? (
|
||||
<Stack spacing={0}>
|
||||
<TextInput
|
||||
required
|
||||
label={t`Username`}
|
||||
placeholder={t`Your username`}
|
||||
{...classicForm.getInputProps('username')}
|
||||
/>
|
||||
<PasswordInput
|
||||
required
|
||||
label={t`Password`}
|
||||
placeholder={t`Your password`}
|
||||
{...classicForm.getInputProps('password')}
|
||||
/>
|
||||
{auth_settings?.password_forgotten_enabled === true && (
|
||||
<Group position="apart" mt="0">
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
color="dimmed"
|
||||
size="xs"
|
||||
onClick={() => navigate('/reset-password')}
|
||||
>
|
||||
<Trans>Reset password</Trans>
|
||||
</Anchor>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack>
|
||||
<TextInput
|
||||
required
|
||||
label={t`Email`}
|
||||
description={t`We will send you a link to login - if you are registered`}
|
||||
placeholder="email@example.org"
|
||||
{...simpleForm.getInputProps('email')}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Group position="apart" mt="xl">
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
color="dimmed"
|
||||
size="xs"
|
||||
onClick={() => setMode.toggle()}
|
||||
>
|
||||
{classicLoginMode ? (
|
||||
<Trans>Send me an email</Trans>
|
||||
) : (
|
||||
<Trans>Use username and password</Trans>
|
||||
)}
|
||||
</Anchor>
|
||||
<Button type="submit" disabled={isLoggingIn} onClick={handleLogin}>
|
||||
{isLoggingIn ? (
|
||||
<Loader size="sm" />
|
||||
) : (
|
||||
<>
|
||||
{classicLoginMode ? (
|
||||
<Trans>Log In</Trans>
|
||||
) : (
|
||||
<Trans>Send Email</Trans>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -161,6 +180,7 @@ export function RegistrationForm() {
|
||||
initialValues: { username: '', email: '', password1: '', password2: '' }
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const [auth_settings] = useServerApiState((state) => [state.auth_settings]);
|
||||
const [isRegistering, setIsRegistering] = useState<boolean>(false);
|
||||
|
||||
function handleRegistration() {
|
||||
@ -201,47 +221,63 @@ export function RegistrationForm() {
|
||||
});
|
||||
}
|
||||
|
||||
const both_reg_enabled =
|
||||
auth_settings?.registration_enabled && auth_settings?.sso_registration;
|
||||
return (
|
||||
<form onSubmit={registrationForm.onSubmit(() => {})}>
|
||||
<Stack spacing={0}>
|
||||
<TextInput
|
||||
required
|
||||
label={t`Username`}
|
||||
placeholder={t`Your username`}
|
||||
{...registrationForm.getInputProps('username')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label={t`Email`}
|
||||
description={t`This will be used for a confirmation`}
|
||||
placeholder="email@example.org"
|
||||
{...registrationForm.getInputProps('email')}
|
||||
/>
|
||||
<PasswordInput
|
||||
required
|
||||
label={t`Password`}
|
||||
placeholder={t`Your password`}
|
||||
{...registrationForm.getInputProps('password1')}
|
||||
/>
|
||||
<PasswordInput
|
||||
required
|
||||
label={t`Password repeat`}
|
||||
placeholder={t`Repeat password`}
|
||||
{...registrationForm.getInputProps('password2')}
|
||||
/>
|
||||
</Stack>
|
||||
<>
|
||||
{auth_settings?.registration_enabled && (
|
||||
<form onSubmit={registrationForm.onSubmit(() => {})}>
|
||||
<Stack spacing={0}>
|
||||
<TextInput
|
||||
required
|
||||
label={t`Username`}
|
||||
placeholder={t`Your username`}
|
||||
{...registrationForm.getInputProps('username')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label={t`Email`}
|
||||
description={t`This will be used for a confirmation`}
|
||||
placeholder="email@example.org"
|
||||
{...registrationForm.getInputProps('email')}
|
||||
/>
|
||||
<PasswordInput
|
||||
required
|
||||
label={t`Password`}
|
||||
placeholder={t`Your password`}
|
||||
{...registrationForm.getInputProps('password1')}
|
||||
/>
|
||||
<PasswordInput
|
||||
required
|
||||
label={t`Password repeat`}
|
||||
placeholder={t`Repeat password`}
|
||||
{...registrationForm.getInputProps('password2')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Group position="apart" mt="xl">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isRegistering}
|
||||
onClick={handleRegistration}
|
||||
fullWidth
|
||||
>
|
||||
<Trans>Register</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
<Group position="apart" mt="xl">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isRegistering}
|
||||
onClick={handleRegistration}
|
||||
fullWidth
|
||||
>
|
||||
<Trans>Register</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
)}
|
||||
{both_reg_enabled && (
|
||||
<Divider label={t`Or use SSO`} labelPosition="center" my="lg" />
|
||||
)}
|
||||
{auth_settings?.sso_registration === true && (
|
||||
<Group grow mb="md" mt="md">
|
||||
{auth_settings.providers.map((provider) => (
|
||||
<SsoButton provider={provider} key={provider.id} />
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -253,8 +289,12 @@ export function ModeSelector({
|
||||
setMode: any;
|
||||
}) {
|
||||
const [auth_settings] = useServerApiState((state) => [state.auth_settings]);
|
||||
const registration_enabled =
|
||||
auth_settings?.registration_enabled ||
|
||||
auth_settings?.sso_registration ||
|
||||
false;
|
||||
|
||||
if (auth_settings?.registration_enabled === false) return null;
|
||||
if (registration_enabled === false) return null;
|
||||
return (
|
||||
<Text ta="center" size={'xs'} mt={'md'}>
|
||||
{loginMode ? (
|
||||
|
@ -26,6 +26,7 @@ export enum ApiEndpoints {
|
||||
user_logout = 'auth/logout/',
|
||||
user_register = 'auth/registration/',
|
||||
|
||||
// Generic API endpoints
|
||||
currency_list = 'currency/exchange/',
|
||||
currency_refresh = 'currency/refresh/',
|
||||
task_overview = 'background-task/',
|
||||
@ -43,6 +44,8 @@ export enum ApiEndpoints {
|
||||
sso_providers = 'auth/providers/',
|
||||
group_list = 'user/group/',
|
||||
owner_list = 'user/owner/',
|
||||
|
||||
// Build API endpoints
|
||||
build_order_list = 'build/',
|
||||
build_order_attachment_list = 'build/attachment/',
|
||||
build_line_list = 'build/line/',
|
||||
@ -61,6 +64,7 @@ export enum ApiEndpoints {
|
||||
part_attachment_list = 'part/attachment/',
|
||||
part_test_template_list = 'part/test-template/',
|
||||
|
||||
// Company API endpoints
|
||||
company_list = 'company/',
|
||||
contact_list = 'company/contact/',
|
||||
address_list = 'company/address/',
|
||||
@ -76,6 +80,8 @@ export enum ApiEndpoints {
|
||||
stock_location_list = 'stock/location/',
|
||||
stock_location_tree = 'stock/location/tree/',
|
||||
stock_attachment_list = 'stock/attachment/',
|
||||
|
||||
// Order API endpoints
|
||||
purchase_order_list = 'order/po/',
|
||||
purchase_order_line_list = 'order/po-line/',
|
||||
purchase_order_attachment_list = 'order/po/attachment/',
|
||||
@ -84,12 +90,17 @@ export enum ApiEndpoints {
|
||||
sales_order_shipment_list = 'order/so/shipment/',
|
||||
return_order_list = 'order/ro/',
|
||||
return_order_attachment_list = 'order/ro/attachment/',
|
||||
|
||||
// Plugin API endpoints
|
||||
plugin_list = 'plugins/',
|
||||
plugin_setting_list = 'plugins/:plugin/settings/',
|
||||
plugin_registry_status = 'plugins/status/',
|
||||
plugin_install = 'plugins/install/',
|
||||
plugin_reload = 'plugins/reload/',
|
||||
|
||||
// Miscellaneous API endpoints
|
||||
error_report_list = 'error-report/',
|
||||
project_code_list = 'project-code/',
|
||||
custom_unit_list = 'units/'
|
||||
custom_unit_list = 'units/',
|
||||
ui_preference = 'web/ui_preference/'
|
||||
}
|
||||
|
@ -71,7 +71,11 @@ export default function Login() {
|
||||
<>
|
||||
<Paper radius="md" p="xl" withBorder>
|
||||
<Text size="lg" weight={500}>
|
||||
<Trans>Welcome, log in below</Trans>
|
||||
{loginMode ? (
|
||||
<Trans>Welcome, log in below</Trans>
|
||||
) : (
|
||||
<Trans>Register below</Trans>
|
||||
)}
|
||||
</Text>
|
||||
{loginMode ? <AuthenticationForm /> : <RegistrationForm />}
|
||||
<ModeSelector loginMode={loginMode} setMode={setMode} />
|
||||
|
Reference in New Issue
Block a user