mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 21:25:42 +00:00 
			
		
		
		
	* Fix REST registration endpoint (#8738) * Re-add html account base Fixes #8690 * fix base template * override dj-rest-auth pattern to fix fixed token model reference * pin req * fix urls.py * move definition out to separate file * fix possible issues where email is not enabled but UI shows that registration is enabled * fix import order * fix token recovery * make sure registration redirects * fix name change * fix import name * adjust description * cleanup * bum api version * add test for registration * add test for registration requirements * fix merge issues * fix merge from https://github.com/inventree/InvenTree/pull/8724
This commit is contained in:
		| @@ -1,13 +1,16 @@ | |||||||
| """InvenTree API version information.""" | """InvenTree API version information.""" | ||||||
|  |  | ||||||
| # InvenTree API version | # InvenTree API version | ||||||
| INVENTREE_API_VERSION = 293 | INVENTREE_API_VERSION = 294 | ||||||
|  |  | ||||||
| """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" | """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" | ||||||
|  |  | ||||||
|  |  | ||||||
| INVENTREE_API_TEXT = """ | INVENTREE_API_TEXT = """ | ||||||
|  |  | ||||||
|  | v294 - 2024-12-23 : https://github.com/inventree/InvenTree/pull/8738 | ||||||
|  |     - Extends registration API documentation | ||||||
|  |  | ||||||
| v293 - 2024-12-14 : https://github.com/inventree/InvenTree/pull/8658 | v293 - 2024-12-14 : https://github.com/inventree/InvenTree/pull/8658 | ||||||
|     - Adds new fields to the supplier barcode API endpoints |     - Adds new fields to the supplier barcode API endpoints | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								src/backend/InvenTree/InvenTree/auth_override_views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/backend/InvenTree/InvenTree/auth_override_views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | """Overrides for registration view.""" | ||||||
|  |  | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
|  | from allauth.account import app_settings as allauth_account_settings | ||||||
|  | from dj_rest_auth.app_settings import api_settings | ||||||
|  | from dj_rest_auth.registration.views import RegisterView | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomRegisterView(RegisterView): | ||||||
|  |     """Registers a new user. | ||||||
|  |  | ||||||
|  |     Accepts the following POST parameters: username, email, password1, password2. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     # Fixes https://github.com/inventree/InvenTree/issues/8707 | ||||||
|  |     # This contains code from dj-rest-auth 7.0 - therefore the version was pinned | ||||||
|  |     def get_response_data(self, user): | ||||||
|  |         """Override to fix check for auth_model.""" | ||||||
|  |         if ( | ||||||
|  |             allauth_account_settings.EMAIL_VERIFICATION | ||||||
|  |             == allauth_account_settings.EmailVerificationMethod.MANDATORY | ||||||
|  |         ): | ||||||
|  |             return {'detail': _('Verification e-mail sent.')} | ||||||
|  |  | ||||||
|  |         if api_settings.USE_JWT: | ||||||
|  |             data = { | ||||||
|  |                 'user': user, | ||||||
|  |                 'access': self.access_token, | ||||||
|  |                 'refresh': self.refresh_token, | ||||||
|  |             } | ||||||
|  |             return api_settings.JWT_SERIALIZER( | ||||||
|  |                 data, context=self.get_serializer_context() | ||||||
|  |             ).data | ||||||
|  |         elif self.token_model: | ||||||
|  |             # Only change in this block is below | ||||||
|  |             return api_settings.TOKEN_SERIALIZER( | ||||||
|  |                 user.api_tokens.last(), context=self.get_serializer_context() | ||||||
|  |             ).data | ||||||
|  |         return None | ||||||
| @@ -20,7 +20,9 @@ from allauth_2fa.utils import user_has_valid_totp_device | |||||||
| from crispy_forms.bootstrap import AppendedText, PrependedAppendedText, PrependedText | from crispy_forms.bootstrap import AppendedText, PrependedAppendedText, PrependedText | ||||||
| from crispy_forms.helper import FormHelper | from crispy_forms.helper import FormHelper | ||||||
| from crispy_forms.layout import Field, Layout | from crispy_forms.layout import Field, Layout | ||||||
| from dj_rest_auth.registration.serializers import RegisterSerializer | from dj_rest_auth.registration.serializers import ( | ||||||
|  |     RegisterSerializer as DjRestRegisterSerializer, | ||||||
|  | ) | ||||||
| from rest_framework import serializers | from rest_framework import serializers | ||||||
|  |  | ||||||
| import InvenTree.helpers_model | import InvenTree.helpers_model | ||||||
| @@ -385,16 +387,11 @@ class CustomSocialAccountAdapter( | |||||||
|  |  | ||||||
|  |  | ||||||
| # override dj-rest-auth | # override dj-rest-auth | ||||||
| class CustomRegisterSerializer(RegisterSerializer): | class RegisterSerializer(DjRestRegisterSerializer): | ||||||
|     """Override of serializer to use dynamic settings.""" |     """Registration requires email, password (twice) and username.""" | ||||||
|  |  | ||||||
|     email = serializers.EmailField() |     email = serializers.EmailField() | ||||||
|  |  | ||||||
|     def __init__(self, instance=None, data=..., **kwargs): |  | ||||||
|         """Check settings to influence which fields are needed.""" |  | ||||||
|         kwargs['email_required'] = get_global_setting('LOGIN_MAIL_REQUIRED') |  | ||||||
|         super().__init__(instance, data, **kwargs) |  | ||||||
|  |  | ||||||
|     def save(self, request): |     def save(self, request): | ||||||
|         """Override to check if registration is open.""" |         """Override to check if registration is open.""" | ||||||
|         if registration_enabled(): |         if registration_enabled(): | ||||||
|   | |||||||
| @@ -620,12 +620,10 @@ REST_AUTH = { | |||||||
|     'TOKEN_MODEL': 'users.models.ApiToken', |     'TOKEN_MODEL': 'users.models.ApiToken', | ||||||
|     'TOKEN_CREATOR': 'users.models.default_create_token', |     'TOKEN_CREATOR': 'users.models.default_create_token', | ||||||
|     'USE_JWT': USE_JWT, |     'USE_JWT': USE_JWT, | ||||||
|  |     'REGISTER_SERIALIZER': 'InvenTree.forms.RegisterSerializer', | ||||||
| } | } | ||||||
|  |  | ||||||
| OLD_PASSWORD_FIELD_ENABLED = True | OLD_PASSWORD_FIELD_ENABLED = True | ||||||
| REST_AUTH_REGISTER_SERIALIZERS = { |  | ||||||
|     'REGISTER_SERIALIZER': 'InvenTree.forms.CustomRegisterSerializer' |  | ||||||
| } |  | ||||||
|  |  | ||||||
| # JWT settings - rest_framework_simplejwt | # JWT settings - rest_framework_simplejwt | ||||||
| if USE_JWT: | if USE_JWT: | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ from rest_framework.response import Response | |||||||
|  |  | ||||||
| import InvenTree.sso | import InvenTree.sso | ||||||
| from common.settings import get_global_setting | from common.settings import get_global_setting | ||||||
|  | from InvenTree.forms import registration_enabled | ||||||
| from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI | from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI | ||||||
| from InvenTree.serializers import EmptySerializer, InvenTreeModelSerializer | from InvenTree.serializers import EmptySerializer, InvenTreeModelSerializer | ||||||
|  |  | ||||||
| @@ -204,7 +205,7 @@ class SocialProviderListView(ListAPI): | |||||||
|             and get_global_setting('LOGIN_ENFORCE_MFA'), |             and get_global_setting('LOGIN_ENFORCE_MFA'), | ||||||
|             'mfa_enabled': settings.MFA_ENABLED, |             'mfa_enabled': settings.MFA_ENABLED, | ||||||
|             'providers': provider_list, |             'providers': provider_list, | ||||||
|             'registration_enabled': get_global_setting('LOGIN_ENABLE_REG'), |             'registration_enabled': registration_enabled(), | ||||||
|             'password_forgotten_enabled': get_global_setting('LOGIN_ENABLE_PWD_FORGOT'), |             'password_forgotten_enabled': get_global_setting('LOGIN_ENABLE_PWD_FORGOT'), | ||||||
|         } |         } | ||||||
|         return Response(data) |         return Response(data) | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| """Test the sso module functionality.""" | """Test the sso and auth module functionality.""" | ||||||
| 
 | 
 | ||||||
|  | from django.conf import settings | ||||||
| from django.contrib.auth.models import Group, User | from django.contrib.auth.models import Group, User | ||||||
|  | from django.core.exceptions import ValidationError | ||||||
| from django.test import override_settings | from django.test import override_settings | ||||||
| from django.test.testcases import TransactionTestCase | from django.test.testcases import TransactionTestCase | ||||||
| 
 | 
 | ||||||
| @@ -9,6 +11,7 @@ from allauth.socialaccount.models import SocialAccount, SocialLogin | |||||||
| from common.models import InvenTreeSetting | from common.models import InvenTreeSetting | ||||||
| from InvenTree import sso | from InvenTree import sso | ||||||
| from InvenTree.forms import RegistratonMixin | from InvenTree.forms import RegistratonMixin | ||||||
|  | from InvenTree.unit_test import InvenTreeAPITestCase | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Dummy: | class Dummy: | ||||||
| @@ -119,3 +122,90 @@ class TestSsoGroupSync(TransactionTestCase): | |||||||
|         self.assertEqual(Group.objects.filter(name='inventree_group').count(), 0) |         self.assertEqual(Group.objects.filter(name='inventree_group').count(), 0) | ||||||
|         sso.ensure_sso_groups(None, self.sociallogin) |         sso.ensure_sso_groups(None, self.sociallogin) | ||||||
|         self.assertEqual(Group.objects.filter(name='inventree_group').count(), 1) |         self.assertEqual(Group.objects.filter(name='inventree_group').count(), 1) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class EmailSettingsContext: | ||||||
|  |     """Context manager to enable email settings for tests.""" | ||||||
|  | 
 | ||||||
|  |     def __enter__(self): | ||||||
|  |         """Enable stuff.""" | ||||||
|  |         InvenTreeSetting.set_setting('LOGIN_ENABLE_REG', True) | ||||||
|  |         settings.EMAIL_HOST = 'localhost' | ||||||
|  | 
 | ||||||
|  |     def __exit__(self, type, value, traceback): | ||||||
|  |         """Exit stuff.""" | ||||||
|  |         InvenTreeSetting.set_setting('LOGIN_ENABLE_REG', False) | ||||||
|  |         settings.EMAIL_HOST = '' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestAuth(InvenTreeAPITestCase): | ||||||
|  |     """Test authentication functionality.""" | ||||||
|  | 
 | ||||||
|  |     def email_args(self, user=None, email=None): | ||||||
|  |         """Generate registration arguments.""" | ||||||
|  |         return { | ||||||
|  |             'username': user or 'user1', | ||||||
|  |             'email': email or 'test@example.com', | ||||||
|  |             'password1': '#asdf1234', | ||||||
|  |             'password2': '#asdf1234', | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     def test_registration(self): | ||||||
|  |         """Test the registration process.""" | ||||||
|  |         self.logout() | ||||||
|  | 
 | ||||||
|  |         # Duplicate username | ||||||
|  |         resp = self.post( | ||||||
|  |             '/api/auth/registration/', | ||||||
|  |             self.email_args(user='testuser'), | ||||||
|  |             expected_code=400, | ||||||
|  |         ) | ||||||
|  |         self.assertIn( | ||||||
|  |             'A user with that username already exists.', resp.data['username'] | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         # Registration is disabled | ||||||
|  |         resp = self.post( | ||||||
|  |             '/api/auth/registration/', self.email_args(), expected_code=400 | ||||||
|  |         ) | ||||||
|  |         self.assertIn('Registration is disabled.', resp.data['non_field_errors']) | ||||||
|  | 
 | ||||||
|  |         # Enable registration - now it should work | ||||||
|  |         with EmailSettingsContext(): | ||||||
|  |             resp = self.post( | ||||||
|  |                 '/api/auth/registration/', self.email_args(), expected_code=201 | ||||||
|  |             ) | ||||||
|  |             self.assertIn('key', resp.data) | ||||||
|  | 
 | ||||||
|  |     def test_registration_email(self): | ||||||
|  |         """Test that LOGIN_SIGNUP_MAIL_RESTRICTION works.""" | ||||||
|  |         self.logout() | ||||||
|  | 
 | ||||||
|  |         # Check the setting validation is working | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             InvenTreeSetting.set_setting( | ||||||
|  |                 'LOGIN_SIGNUP_MAIL_RESTRICTION', 'example.com,inventree.org' | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         # Setting setting correctly | ||||||
|  |         correct_setting = '@example.com,@inventree.org' | ||||||
|  |         InvenTreeSetting.set_setting('LOGIN_SIGNUP_MAIL_RESTRICTION', correct_setting) | ||||||
|  |         self.assertEqual( | ||||||
|  |             InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_RESTRICTION'), | ||||||
|  |             correct_setting, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         # Wrong email format | ||||||
|  |         resp = self.post( | ||||||
|  |             '/api/auth/registration/', | ||||||
|  |             self.email_args(email='admin@invenhost.com'), | ||||||
|  |             expected_code=400, | ||||||
|  |         ) | ||||||
|  |         self.assertIn('The provided email domain is not approved.', resp.data['email']) | ||||||
|  | 
 | ||||||
|  |         # Right format should work | ||||||
|  |         with EmailSettingsContext(): | ||||||
|  |             resp = self.post( | ||||||
|  |                 '/api/auth/registration/', self.email_args(), expected_code=201 | ||||||
|  |             ) | ||||||
|  |             self.assertIn('key', resp.data) | ||||||
| @@ -32,6 +32,7 @@ import users.api | |||||||
| from build.urls import build_urls | from build.urls import build_urls | ||||||
| from common.urls import common_urls | from common.urls import common_urls | ||||||
| from company.urls import company_urls, manufacturer_part_urls, supplier_part_urls | from company.urls import company_urls, manufacturer_part_urls, supplier_part_urls | ||||||
|  | from InvenTree.auth_override_views import CustomRegisterView | ||||||
| from order.urls import order_urls | from order.urls import order_urls | ||||||
| from part.urls import part_urls | from part.urls import part_urls | ||||||
| from plugin.urls import get_plugin_urls | from plugin.urls import get_plugin_urls | ||||||
| @@ -202,6 +203,7 @@ apipatterns = [ | |||||||
|                 ConfirmEmailView.as_view(), |                 ConfirmEmailView.as_view(), | ||||||
|                 name='account_confirm_email', |                 name='account_confirm_email', | ||||||
|             ), |             ), | ||||||
|  |             path('registration/', CustomRegisterView.as_view(), name='rest_register'), | ||||||
|             path('registration/', include('dj_rest_auth.registration.urls')), |             path('registration/', include('dj_rest_auth.registration.urls')), | ||||||
|             path( |             path( | ||||||
|                 'providers/', SocialProviderListView.as_view(), name='social_providers' |                 'providers/', SocialProviderListView.as_view(), name='social_providers' | ||||||
|   | |||||||
| @@ -1,9 +1,48 @@ | |||||||
| {% extends "skeleton.html" %} | {% load static %} | ||||||
| {% load i18n %} | {% load i18n %} | ||||||
| {% load inventree_extras %} | {% load inventree_extras %} | ||||||
|  |  | ||||||
| {% block body %} | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  | <!-- Required meta tags --> | ||||||
|  | <meta charset="utf-8"> | ||||||
|  | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||||
|  | <!-- Favicon --> | ||||||
|  | <link rel="apple-touch-icon" sizes="57x57" href="{% static 'img/favicon/apple-icon-57x57.png' %}"> | ||||||
|  | <link rel="apple-touch-icon" sizes="60x60" href="{% static 'img/favicon/apple-icon-60x60.png' %}"> | ||||||
|  | <link rel="apple-touch-icon" sizes="72x72" href="{% static 'img/favicon/apple-icon-72x72.png' %}"> | ||||||
|  | <link rel="apple-touch-icon" sizes="76x76" href="{% static 'img/favicon/apple-icon-76x76.png' %}"> | ||||||
|  | <link rel="apple-touch-icon" sizes="114x114" href="{% static 'img/favicon/apple-icon-114x114.png' %}"> | ||||||
|  | <link rel="apple-touch-icon" sizes="120x120" href="{% static 'img/favicon/apple-icon-120x120.png' %}"> | ||||||
|  | <link rel="apple-touch-icon" sizes="144x144" href="{% static 'img/favicon/apple-icon-144x144.png' %}"> | ||||||
|  | <link rel="apple-touch-icon" sizes="152x152" href="{% static 'img/favicon/apple-icon-152x152.png' %}"> | ||||||
|  | <link rel="apple-touch-icon" sizes="180x180" href="{% static 'img/favicon/apple-icon-180x180.png' %}"> | ||||||
|  | <link rel="icon" type="image/png" sizes="192x192"  href="{% static 'img/favicon/android-icon-192x192.png' %}"> | ||||||
|  | <link rel="icon" type="image/png" sizes="32x32" href="{% static 'img/favicon/favicon-32x32.png' %}"> | ||||||
|  | <link rel="icon" type="image/png" sizes="96x96" href="{% static 'img/favicon/favicon-96x96.png' %}"> | ||||||
|  | <link rel="icon" type="image/png" sizes="16x16" href="{% static 'img/favicon/favicon-16x16.png' %}"> | ||||||
|  | <link rel="manifest" href="{% static 'img/favicon/manifest.json' %}"> | ||||||
|  | <meta name="msapplication-TileColor" content="#ffffff"> | ||||||
|  | <meta name="msapplication-TileImage" content="{% static 'img/favicon/ms-icon-144x144.png' %}"> | ||||||
|  | <meta name="theme-color" content="#ffffff"> | ||||||
|  | <!-- CSS --> | ||||||
|  | <link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}"> | ||||||
|  | <link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}"> | ||||||
|  | <link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}"> | ||||||
|  | <link rel="stylesheet" href="{% static 'select2/css/select2.css' %}"> | ||||||
|  | <link rel="stylesheet" href="{% static 'select2/css/select2-bootstrap-5-theme.css' %}"> | ||||||
|  | <link rel="stylesheet" href="{% static 'css/inventree.css' %}"> | ||||||
|  | <link rel="stylesheet" href="{% get_color_theme_css request.user %}"> | ||||||
|  | <title> | ||||||
|  |     {% inventree_title %} | {% block head_title %}{% endblock head_title %} | ||||||
|  | </title> | ||||||
|  | {% block extra_head %} | ||||||
|  | {% endblock extra_head %} | ||||||
|  | </head> | ||||||
|  |  | ||||||
| <body class='login-screen' style='background: url({% inventree_splash %}); background-size: cover;'> | <body class='login-screen' style='background: url({% inventree_splash %}); background-size: cover;'> | ||||||
|  |  | ||||||
|     <div class='container-fluid'> |     <div class='container-fluid'> | ||||||
|         <div class='notification-area' id='alerts'> |         <div class='notification-area' id='alerts'> | ||||||
|             <!-- Div for displayed alerts --> |             <!-- Div for displayed alerts --> | ||||||
| @@ -34,4 +73,31 @@ | |||||||
|         {% block extra_body %} |         {% block extra_body %} | ||||||
|         {% endblock extra_body %} |         {% endblock extra_body %} | ||||||
|     </div> |     </div> | ||||||
| {% endblock body %} |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | <!-- general JS --> | ||||||
|  | {% include "third_party_js.html" %} | ||||||
|  | <script type='text/javascript' src='{% static "script/inventree/inventree.js" %}'></script> | ||||||
|  | <script type='text/javascript' src='{% static "script/inventree/message.js" %}'></script> | ||||||
|  | <script type='text/javascript'> | ||||||
|  | $(document).ready(function () { | ||||||
|  |     {% if messages %} | ||||||
|  |     {% for message in messages %} | ||||||
|  |     showMessage("{{ message }}"); | ||||||
|  |     {% endfor %} | ||||||
|  |     {% endif %} | ||||||
|  |     showCachedAlerts(); | ||||||
|  |     // Add brand icons for SSO providers, if available | ||||||
|  |     $('.socialaccount_provider').each(function(i, obj) { | ||||||
|  |         var el = $(this); | ||||||
|  |         var tag = el.attr('brand_name'); | ||||||
|  |         var icon = window.FontAwesome.icon({prefix: 'fab', iconName: tag}); | ||||||
|  |         if (icon) { | ||||||
|  |             el.prepend(`<span class='fab fa-${tag}'></span> `); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ django-weasyprint                       # django weasyprint integration | |||||||
| djangorestframework<3.15                # DRF framework  # FIXED 2024-06-26 see https://github.com/inventree/InvenTree/pull/7521 | djangorestframework<3.15                # DRF framework  # FIXED 2024-06-26 see https://github.com/inventree/InvenTree/pull/7521 | ||||||
| djangorestframework-simplejwt[crypto]   # JWT authentication | djangorestframework-simplejwt[crypto]   # JWT authentication | ||||||
| django-xforwardedfor-middleware         # IP forwarding metadata | django-xforwardedfor-middleware         # IP forwarding metadata | ||||||
| dj-rest-auth                            # Authentication API endpoints | dj-rest-auth==7.0.0                     # Authentication API endpoints # FIXED 2024-12-22 due to https://github.com/inventree/InvenTree/issues/8707 | ||||||
| dulwich                                 # pure Python git integration | dulwich                                 # pure Python git integration | ||||||
| drf-spectacular                         # DRF API documentation | drf-spectacular                         # DRF API documentation | ||||||
| feedparser                              # RSS newsfeed parser | feedparser                              # RSS newsfeed parser | ||||||
|   | |||||||
| @@ -192,7 +192,7 @@ export function RegistrationForm() { | |||||||
|         headers: { Authorization: '' } |         headers: { Authorization: '' } | ||||||
|       }) |       }) | ||||||
|       .then((ret) => { |       .then((ret) => { | ||||||
|         if (ret?.status === 204) { |         if (ret?.status === 204 || ret?.status === 201) { | ||||||
|           setIsRegistering(false); |           setIsRegistering(false); | ||||||
|           showLoginNotification({ |           showLoginNotification({ | ||||||
|             title: t`Registration successful`, |             title: t`Registration successful`, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user