mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +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:
parent
3cb806d20a
commit
40245a6c4a
@ -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`,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user