2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00

[0.17.x] Fix REST registration endpoint (#8738) (#8763)

* 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:
Matthias Mair 2024-12-25 01:38:02 +01:00 committed by GitHub
parent 3cb806d20a
commit 40245a6c4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 216 additions and 19 deletions

View File

@ -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

View 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

View File

@ -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():

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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>&nbsp;`);
}
});
});
</script>
</body>
</html>

View File

@ -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

View File

@ -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`,