diff --git a/src/backend/InvenTree/InvenTree/social_auth_urls.py b/src/backend/InvenTree/InvenTree/social_auth_urls.py index 4be0b8d09f..a7ad13dd6f 100644 --- a/src/backend/InvenTree/InvenTree/social_auth_urls.py +++ b/src/backend/InvenTree/InvenTree/social_auth_urls.py @@ -6,6 +6,7 @@ from importlib import import_module from django.conf import settings from django.urls import NoReverseMatch, include, path, reverse +import allauth.socialaccount.providers.openid_connect.views as oidc_views from allauth.account.models import EmailAddress from allauth.socialaccount import providers from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter, OAuth2LoginView @@ -44,7 +45,7 @@ class GenericOAuth2ApiConnectView(GenericOAuth2ApiLoginView): return super().dispatch(request, *args, **kwargs) -def handle_oauth2(adapter: OAuth2Adapter): +def handle_oauth2(adapter: OAuth2Adapter, provider=None): """Define urls for oauth2 endpoints.""" return [ path( @@ -60,6 +61,22 @@ def handle_oauth2(adapter: OAuth2Adapter): ] +def handle_oidc(provider): + """Define urls for oidc endpoints.""" + return [ + path( + 'login/', + lambda x: oidc_views.login(x, provider.id), + name=f'{provider.id}_api_login', + ), + path( + 'connect/', + lambda x: oidc_views.callback(x, provider.id), + name=f'{provider.id}_api_connect', + ), + ] + + legacy = { 'twitter': 'twitter_oauth2', 'bitbucket': 'bitbucket_oauth2', @@ -70,47 +87,53 @@ legacy = { # Collect urls for all loaded providers -social_auth_urlpatterns = [] +def get_provider_urls() -> list: + """Collect urls for all loaded providers. -provider_urlpatterns = [] + Returns: + list: List of urls for all loaded providers. + """ + auth_provider_routes = [] -for name, provider in providers.registry.provider_map.items(): - try: - prov_mod = import_module(provider.get_package() + '.views') - except ImportError: - logger.exception('Could not import authentication provider %s', name) - continue + for name, provider in providers.registry.provider_map.items(): + try: + prov_mod = import_module(provider.get_package() + '.views') + except ImportError: + logger.exception('Could not import authentication provider %s', name) + continue - # Try to extract the adapter class - adapters = [ - cls - for cls in prov_mod.__dict__.values() - if isinstance(cls, type) - and cls != OAuth2Adapter - and issubclass(cls, OAuth2Adapter) - ] + # Try to extract the adapter class + adapters = [ + cls + for cls in prov_mod.__dict__.values() + if isinstance(cls, type) + and cls != OAuth2Adapter + and issubclass(cls, OAuth2Adapter) + ] - # Get urls - urls = [] - if len(adapters) == 1: - urls = handle_oauth2(adapter=adapters[0]) - elif provider.id in legacy: - logger.warning( - '`%s` is not supported on platform UI. Use `%s` instead.', - provider.id, - legacy[provider.id], - ) - continue - else: - logger.error( - 'Found handler that is not yet ready for platform UI: `%s`. Open an feature request on GitHub if you need it implemented.', - provider.id, - ) - continue - provider_urlpatterns += [path(f'{provider.id}/', include(urls))] + # Get urls + urls = [] + if len(adapters) == 1: + if provider.id == 'openid_connect': + urls = handle_oidc(provider) + else: + urls = handle_oauth2(adapter=adapters[0], provider=provider) + elif provider.id in legacy: + logger.warning( + '`%s` is not supported on platform UI. Use `%s` instead.', + provider.id, + legacy[provider.id], + ) + continue + else: + logger.error( + 'Found handler that is not yet ready for platform UI: `%s`. Open an feature request on GitHub if you need it implemented.', + provider.id, + ) + continue + auth_provider_routes += [path(f'{provider.id}/', include(urls))] - -social_auth_urlpatterns += provider_urlpatterns + return auth_provider_routes class SocialProviderListResponseSerializer(serializers.Serializer): diff --git a/src/backend/InvenTree/InvenTree/urls.py b/src/backend/InvenTree/InvenTree/urls.py index ef5e4d8eb5..43c16a14bf 100644 --- a/src/backend/InvenTree/InvenTree/urls.py +++ b/src/backend/InvenTree/InvenTree/urls.py @@ -54,7 +54,7 @@ from .social_auth_urls import ( EmailRemoveView, EmailVerifyView, SocialProviderListView, - social_auth_urlpatterns, + get_provider_urls, ) from .views import ( AboutView, @@ -78,6 +78,71 @@ from .views import ( admin.site.site_header = 'InvenTree Admin' +settings_urls = [ + path('i18n/', include('django.conf.urls.i18n')), + path('appearance/', AppearanceSelectView.as_view(), name='settings-appearance'), + # Catch any other urls + path( + '', + SettingsView.as_view(template_name='InvenTree/settings/settings.html'), + name='settings', + ), +] + +notifications_urls = [ + # Catch any other urls + path('', NotificationsView.as_view(), name='notifications') +] + +classic_frontendpatterns = [ + # Apps + # + path('build/', include(build_urls)), + path('common/', include(common_urls)), + path('company/', include(company_urls)), + path('order/', include(order_urls)), + path('manufacturer-part/', include(manufacturer_part_urls)), + path('part/', include(part_urls)), + path('stock/', include(stock_urls)), + path('supplier-part/', include(supplier_part_urls)), + path('edit-user/', EditUserView.as_view(), name='edit-user'), + path('set-password/', SetPasswordView.as_view(), name='set-password'), + path('index/', IndexView.as_view(), name='index'), + path('notifications/', include(notifications_urls)), + path('search/', SearchView.as_view(), name='search'), + path('settings/', include(settings_urls)), + path('about/', AboutView.as_view(), name='about'), + path('stats/', DatabaseStatsView.as_view(), name='stats'), + # DB user sessions + path( + 'accounts/sessions/other/delete/', + view=CustomSessionDeleteOtherView.as_view(), + name='session_delete_other', + ), + re_path( + r'^accounts/sessions/(?P\w+)/delete/$', + view=CustomSessionDeleteView.as_view(), + name='session_delete', + ), + # Single Sign On / allauth + # overrides of urlpatterns + path('accounts/email/', CustomEmailView.as_view(), name='account_email'), + path( + 'accounts/social/connections/', + CustomConnectionsView.as_view(), + name='socialaccount_connections', + ), + re_path( + r'^accounts/password/reset/key/(?P[0-9A-Za-z]+)-(?P.+)/$', + CustomPasswordResetFromKeyView.as_view(), + name='account_reset_password_from_key', + ), + # Override login page + path('accounts/login/', CustomLoginView.as_view(), name='account_login'), + path('accounts/', include('allauth_2fa.urls')), # MFA support + path('accounts/', include('allauth.urls')), # included urlpatterns +] + apipatterns = [ # Global search @@ -167,7 +232,7 @@ apipatterns = [ path('', EmailListView.as_view(), name='email-list'), ]), ), - path('social/', include(social_auth_urlpatterns)), + path('social/', include(get_provider_urls())), path( 'social/', SocialAccountListView.as_view(), name='social_account_list' ), @@ -197,21 +262,6 @@ apipatterns = [ re_path(r'^.*$', NotFoundView.as_view(), name='api-404'), ] -settings_urls = [ - path('i18n/', include('django.conf.urls.i18n')), - path('appearance/', AppearanceSelectView.as_view(), name='settings-appearance'), - # Catch any other urls - path( - '', - SettingsView.as_view(template_name='InvenTree/settings/settings.html'), - name='settings', - ), -] - -notifications_urls = [ - # Catch any other urls - path('', NotificationsView.as_view(), name='notifications') -] # These javascript files are served "dynamically" - i.e. rendered on demand dynamic_javascript_urls = [ @@ -400,54 +450,6 @@ if settings.ENABLE_CLASSIC_FRONTEND: re_path(r'^js/i18n/', include(translated_javascript_urls)), ] -classic_frontendpatterns = [ - # Apps - # - path('build/', include(build_urls)), - path('common/', include(common_urls)), - path('company/', include(company_urls)), - path('order/', include(order_urls)), - path('manufacturer-part/', include(manufacturer_part_urls)), - path('part/', include(part_urls)), - path('stock/', include(stock_urls)), - path('supplier-part/', include(supplier_part_urls)), - path('edit-user/', EditUserView.as_view(), name='edit-user'), - path('set-password/', SetPasswordView.as_view(), name='set-password'), - path('index/', IndexView.as_view(), name='index'), - path('notifications/', include(notifications_urls)), - path('search/', SearchView.as_view(), name='search'), - path('settings/', include(settings_urls)), - path('about/', AboutView.as_view(), name='about'), - path('stats/', DatabaseStatsView.as_view(), name='stats'), - # DB user sessions - path( - 'accounts/sessions/other/delete/', - view=CustomSessionDeleteOtherView.as_view(), - name='session_delete_other', - ), - re_path( - r'^accounts/sessions/(?P\w+)/delete/$', - view=CustomSessionDeleteView.as_view(), - name='session_delete', - ), - # Single Sign On / allauth - # overrides of urlpatterns - path('accounts/email/', CustomEmailView.as_view(), name='account_email'), - path( - 'accounts/social/connections/', - CustomConnectionsView.as_view(), - name='socialaccount_connections', - ), - re_path( - r'^accounts/password/reset/key/(?P[0-9A-Za-z]+)-(?P.+)/$', - CustomPasswordResetFromKeyView.as_view(), - name='account_reset_password_from_key', - ), - # Override login page - path('accounts/login/', CustomLoginView.as_view(), name='account_login'), - path('accounts/', include('allauth_2fa.urls')), # MFA support - path('accounts/', include('allauth.urls')), # included urlpatterns -] urlpatterns = [] diff --git a/src/frontend/src/components/buttons/SSOButton.tsx b/src/frontend/src/components/buttons/SSOButton.tsx index e16548a331..4454c200e8 100644 --- a/src/frontend/src/components/buttons/SSOButton.tsx +++ b/src/frontend/src/components/buttons/SSOButton.tsx @@ -1,4 +1,4 @@ -import { Button } from '@mantine/core'; +import { Button, Tooltip } from '@mantine/core'; import { IconBrandAzure, IconBrandBitbucket, @@ -14,6 +14,7 @@ import { IconLogin } from '@tabler/icons-react'; +import { t } from '@lingui/macro'; import { api } from '../../App'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { apiUrl } from '../../states/ApiState'; @@ -49,14 +50,23 @@ export function SsoButton({ provider }: Readonly<{ provider: Provider }>) { } return ( - + + ); } function getBrandIcon(provider: Provider) {