From 2e6ba4de915fa4a09a4af96dd01db95ae5eab7c9 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 7 Jan 2025 16:56:43 +0100 Subject: [PATCH] implement more provider stuff --- src/backend/InvenTree/InvenTree/settings.py | 1 + .../src/components/buttons/SSOButton.tsx | 37 +------- .../components/forms/AuthenticationForm.tsx | 41 ++++---- src/frontend/src/enums/ApiEndpoints.tsx | 8 +- src/frontend/src/functions/auth.tsx | 19 +++- .../AccountSettings/SecurityContent.tsx | 93 ++++++++----------- src/frontend/src/states/ApiState.tsx | 30 +++++- src/frontend/src/states/states.tsx | 18 ---- 8 files changed, 118 insertions(+), 129 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index ec4e2458f5..a1053b16aa 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -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' diff --git a/src/frontend/src/components/buttons/SSOButton.tsx b/src/frontend/src/components/buttons/SSOButton.tsx index fc136768f7..ba7e8cb44b 100644 --- a/src/frontend/src/components/buttons/SSOButton.tsx +++ b/src/frontend/src/components/buttons/SSOButton.tsx @@ -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 ( ); diff --git a/src/frontend/src/components/forms/AuthenticationForm.tsx b/src/frontend/src/components/forms/AuthenticationForm.tsx index 71b99f6573..0335b76943 100644 --- a/src/frontend/src/components/forms/AuthenticationForm.tsx +++ b/src/frontend/src/components/forms/AuthenticationForm.tsx @@ -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() ? ( <> - {auth_settings.providers.map((provider) => ( + {auth_settings?.socialaccount.providers.map((provider) => ( ))} @@ -130,7 +135,7 @@ export function AuthenticationForm() { placeholder={t`Your password`} {...classicForm.getInputProps('password')} /> - {auth_settings?.password_forgotten_enabled === true && ( + {password_forgotten_enabled() === true && ( [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(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() && (
{})}> )} - {auth_settings?.sso_registration === true && ( + {sso_registration() && ( - {auth_settings.providers.map((provider) => ( + {auth_settings?.socialaccount.providers.map((provider) => ( ))} @@ -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 ( {loginMode ? ( diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 180b6f26d1..d9e60b4ec8 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -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/' } diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index b09526f2d4..dc75d90d84 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -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); +} diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx index ce7aa1b533..703f323631 100644 --- a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx @@ -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(false); - const [isMfaEnabled, setIsMfaEnabled] = useState(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 ( @@ -54,8 +40,8 @@ export function SecurityContent() { <Trans>Single Sign On Accounts</Trans> - {isSsoEnabled ? ( - + {sso_enabled() ? ( + ) : ( } @@ -69,7 +55,7 @@ export function SecurityContent() { Multifactor - {isMfaEnabled ? ( + {mfa_enabled() ? ( ) : ( ) { +function ProviderButton({ provider }: Readonly<{ provider: Provider }>) { + return ( + + ); +} + +function SsoContent({ + auth_settings +}: Readonly<{ auth_settings: SecuritySetting | undefined }>) { const [value, setValue] = useState(''); - const [currentProviders, setCurrentProviders] = useState<[]>(); + const [currentProviders, setCurrentProviders] = useState(); + 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 ; - function ProviderButton({ provider }: Readonly<{ provider: any }>) { - const button = ( - - ); - - if (provider.configured) return button; - return ( - {button} - ); - } - return ( diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index 82989dd137..52b93ae7a6 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -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()( persist( - (set) => ({ + (set, get) => ({ server: emptyServerAPI, setServer: (newServer: ServerAPIProps) => set({ server: newServer }), fetchServerApiState: async () => { @@ -24,6 +29,7 @@ export const useServerApiState = create()( .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()( 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', diff --git a/src/frontend/src/states/states.tsx b/src/frontend/src/states/states.tsx index 47f0df3de5..c6b1fb26f1 100644 --- a/src/frontend/src/states/states.tsx +++ b/src/frontend/src/states/states.tsx @@ -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;