diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 31451506b3..563cb55980 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,16 @@ """InvenTree API version information.""" # 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.""" 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 - Adds new fields to the supplier barcode API endpoints diff --git a/src/backend/InvenTree/InvenTree/auth_override_views.py b/src/backend/InvenTree/InvenTree/auth_override_views.py new file mode 100644 index 0000000000..3362c3bf07 --- /dev/null +++ b/src/backend/InvenTree/InvenTree/auth_override_views.py @@ -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 diff --git a/src/backend/InvenTree/InvenTree/forms.py b/src/backend/InvenTree/InvenTree/forms.py index f2e73b253e..f37eafbf6a 100644 --- a/src/backend/InvenTree/InvenTree/forms.py +++ b/src/backend/InvenTree/InvenTree/forms.py @@ -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.helper import FormHelper 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 import InvenTree.helpers_model @@ -385,16 +387,11 @@ class CustomSocialAccountAdapter( # override dj-rest-auth -class CustomRegisterSerializer(RegisterSerializer): - """Override of serializer to use dynamic settings.""" +class RegisterSerializer(DjRestRegisterSerializer): + """Registration requires email, password (twice) and username.""" 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): """Override to check if registration is open.""" if registration_enabled(): diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 08f7e1c42f..29d8211f90 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -620,12 +620,10 @@ REST_AUTH = { 'TOKEN_MODEL': 'users.models.ApiToken', 'TOKEN_CREATOR': 'users.models.default_create_token', 'USE_JWT': USE_JWT, + 'REGISTER_SERIALIZER': 'InvenTree.forms.RegisterSerializer', } OLD_PASSWORD_FIELD_ENABLED = True -REST_AUTH_REGISTER_SERIALIZERS = { - 'REGISTER_SERIALIZER': 'InvenTree.forms.CustomRegisterSerializer' -} # JWT settings - rest_framework_simplejwt if USE_JWT: diff --git a/src/backend/InvenTree/InvenTree/social_auth_urls.py b/src/backend/InvenTree/InvenTree/social_auth_urls.py index a7ad13dd6f..566809e645 100644 --- a/src/backend/InvenTree/InvenTree/social_auth_urls.py +++ b/src/backend/InvenTree/InvenTree/social_auth_urls.py @@ -18,6 +18,7 @@ from rest_framework.response import Response import InvenTree.sso from common.settings import get_global_setting +from InvenTree.forms import registration_enabled from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI from InvenTree.serializers import EmptySerializer, InvenTreeModelSerializer @@ -204,7 +205,7 @@ class SocialProviderListView(ListAPI): and get_global_setting('LOGIN_ENFORCE_MFA'), 'mfa_enabled': settings.MFA_ENABLED, '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'), } return Response(data) diff --git a/src/backend/InvenTree/InvenTree/test_sso.py b/src/backend/InvenTree/InvenTree/test_auth.py similarity index 63% rename from src/backend/InvenTree/InvenTree/test_sso.py rename to src/backend/InvenTree/InvenTree/test_auth.py index e20f11d3ae..1c734c2bad 100644 --- a/src/backend/InvenTree/InvenTree/test_sso.py +++ b/src/backend/InvenTree/InvenTree/test_auth.py @@ -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.core.exceptions import ValidationError from django.test import override_settings from django.test.testcases import TransactionTestCase @@ -9,6 +11,7 @@ from allauth.socialaccount.models import SocialAccount, SocialLogin from common.models import InvenTreeSetting from InvenTree import sso from InvenTree.forms import RegistratonMixin +from InvenTree.unit_test import InvenTreeAPITestCase class Dummy: @@ -119,3 +122,90 @@ class TestSsoGroupSync(TransactionTestCase): self.assertEqual(Group.objects.filter(name='inventree_group').count(), 0) sso.ensure_sso_groups(None, self.sociallogin) 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) diff --git a/src/backend/InvenTree/InvenTree/urls.py b/src/backend/InvenTree/InvenTree/urls.py index 43c16a14bf..4891c372a2 100644 --- a/src/backend/InvenTree/InvenTree/urls.py +++ b/src/backend/InvenTree/InvenTree/urls.py @@ -32,6 +32,7 @@ import users.api from build.urls import build_urls from common.urls import common_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 part.urls import part_urls from plugin.urls import get_plugin_urls @@ -202,6 +203,7 @@ apipatterns = [ ConfirmEmailView.as_view(), name='account_confirm_email', ), + path('registration/', CustomRegisterView.as_view(), name='rest_register'), path('registration/', include('dj_rest_auth.registration.urls')), path( 'providers/', SocialProviderListView.as_view(), name='social_providers' diff --git a/src/backend/InvenTree/templates/account/base.html b/src/backend/InvenTree/templates/account/base.html index 0e298e8cc0..14f92790f4 100644 --- a/src/backend/InvenTree/templates/account/base.html +++ b/src/backend/InvenTree/templates/account/base.html @@ -1,9 +1,48 @@ -{% extends "skeleton.html" %} +{% load static %} {% load i18n %} {% load inventree_extras %} -{% block body %} + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +