Skip to content

Commit

Permalink
[0.17.x] Fix REST registration endpoint (#8738) (#8763)
Browse files Browse the repository at this point in the history
* 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 #8724
  • Loading branch information
matmair authored Dec 25, 2024
1 parent 3cb806d commit 40245a6
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 19 deletions.
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
40 changes: 40 additions & 0 deletions src/backend/InvenTree/InvenTree/auth_override_views.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 5 additions & 8 deletions src/backend/InvenTree/InvenTree/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
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
Expand Down Expand Up @@ -385,16 +387,11 @@ def authentication_error(


# 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():
Expand Down
4 changes: 1 addition & 3 deletions src/backend/InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,12 +620,10 @@
'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:
Expand Down
3 changes: 2 additions & 1 deletion src/backend/InvenTree/InvenTree/social_auth_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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

Expand Down Expand Up @@ -204,7 +205,7 @@ def get(self, request, *args, **kwargs):
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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -9,6 +11,7 @@
from common.models import InvenTreeSetting
from InvenTree import sso
from InvenTree.forms import RegistratonMixin
from InvenTree.unit_test import InvenTreeAPITestCase


class Dummy:
Expand Down Expand Up @@ -119,3 +122,90 @@ def test_sso_group_created_if_not_exists(self):
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 '[email protected]',
'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='[email protected]'),
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)
2 changes: 2 additions & 0 deletions src/backend/InvenTree/InvenTree/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
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
Expand Down Expand Up @@ -202,6 +203,7 @@
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'
Expand Down
72 changes: 69 additions & 3 deletions src/backend/InvenTree/templates/account/base.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,48 @@
{% extends "skeleton.html" %}
{% load static %}
{% load i18n %}
{% 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;'>

<div class='container-fluid'>
<div class='notification-area' id='alerts'>
<!-- Div for displayed alerts -->
Expand Down Expand Up @@ -34,4 +73,31 @@
{% block extra_body %}
{% endblock extra_body %}
</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>&nbsp;`);
}
});
});
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion src/backend/requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -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-simplejwt[crypto] # JWT authentication
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
drf-spectacular # DRF API documentation
feedparser # RSS newsfeed parser
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/components/forms/AuthenticationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export function RegistrationForm() {
headers: { Authorization: '' }
})
.then((ret) => {
if (ret?.status === 204) {
if (ret?.status === 204 || ret?.status === 201) {
setIsRegistering(false);
showLoginNotification({
title: t`Registration successful`,
Expand Down

0 comments on commit 40245a6

Please sign in to comment.