mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-01 03:00:54 +00:00
feat: improve user/group management actions (#9602)
* feat: improve user management actions add "open profile" actions * add lock / unlock action * add actions for password reset * submit coverage info to codecov no idea why this was turned off * bump api version * add frontend test * add backend test * fix test state * move test * fix style * fix name * hide password change if not superuser * bump playwright see https://github.com/microsoft/playwright/issues/35183 * fix test * fix test order * simplify test --------- Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
@ -1,11 +1,15 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 350
|
||||
INVENTREE_API_VERSION = 351
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v351 -> 2025-06-18 : https://github.com/inventree/InvenTree/pull/9602
|
||||
- Adds passwort reset API endpoint for admin users
|
||||
|
||||
v350 -> 2025-06-17 : https://github.com/inventree/InvenTree/pull/9798
|
||||
- Adds "can_build" field to the part requirements API endpoint
|
||||
- Remove "allocated" and "required" fields from the part requirements API endpoint
|
||||
|
@ -4,6 +4,8 @@ import datetime
|
||||
|
||||
from django.contrib.auth import get_user, login
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.auth.password_validation import password_changed, validate_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import include, path
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.generic.base import RedirectView
|
||||
@ -23,6 +25,7 @@ from InvenTree.mixins import (
|
||||
RetrieveAPI,
|
||||
RetrieveUpdateAPI,
|
||||
RetrieveUpdateDestroyAPI,
|
||||
UpdateAPI,
|
||||
)
|
||||
from InvenTree.settings import FRONTEND_URL_BASE
|
||||
from users.models import ApiToken, Owner, RuleSet, UserProfile
|
||||
@ -37,6 +40,7 @@ from users.serializers import (
|
||||
RuleSetSerializer,
|
||||
UserCreateSerializer,
|
||||
UserProfileSerializer,
|
||||
UserSetPasswordSerializer,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
@ -141,6 +145,36 @@ class UserDetail(RetrieveUpdateDestroyAPI):
|
||||
permission_classes = [InvenTree.permissions.StaffRolePermissionOrReadOnly]
|
||||
|
||||
|
||||
class UserDetailSetPassword(UpdateAPI):
|
||||
"""Allows superusers to set the password for a user."""
|
||||
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSetPasswordSerializer
|
||||
permission_classes = [InvenTree.permissions.IsSuperuserOrSuperScope]
|
||||
|
||||
def get_object(self):
|
||||
"""Return the user object for this endpoint."""
|
||||
return self.get_queryset().get(pk=self.kwargs['pk'])
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""Set the password for the user."""
|
||||
user: User = serializer.instance
|
||||
|
||||
password: str = serializer.validated_data.get('password', None)
|
||||
overwrite: bool = serializer.validated_data.get('override_warning', False)
|
||||
|
||||
if password:
|
||||
if not overwrite:
|
||||
try:
|
||||
validate_password(password=password, user=user)
|
||||
except ValidationError as e:
|
||||
raise exceptions.ValidationError({'password': str(e)})
|
||||
|
||||
user.set_password(password)
|
||||
password_changed(password=password, user=user)
|
||||
user.save()
|
||||
|
||||
|
||||
class MeUserDetail(RetrieveUpdateAPI, UserDetail):
|
||||
"""Detail endpoint for current user.
|
||||
|
||||
@ -467,6 +501,16 @@ user_urls = [
|
||||
path('', RuleSetList.as_view(), name='api-ruleset-list'),
|
||||
]),
|
||||
),
|
||||
path('<int:pk>/', UserDetail.as_view(), name='api-user-detail'),
|
||||
path(
|
||||
'<int:pk>/',
|
||||
include([
|
||||
path(
|
||||
'set-password/',
|
||||
UserDetailSetPassword.as_view(),
|
||||
name='api-user-set-password',
|
||||
),
|
||||
path('', UserDetail.as_view(), name='api-user-detail'),
|
||||
]),
|
||||
),
|
||||
path('', UserList.as_view(), name='api-user-list'),
|
||||
]
|
||||
|
@ -360,6 +360,30 @@ class ExtendedUserSerializer(UserSerializer):
|
||||
return instance
|
||||
|
||||
|
||||
class UserSetPasswordSerializer(serializers.Serializer):
|
||||
"""Serializer for setting a password for a user."""
|
||||
|
||||
class Meta:
|
||||
"""Meta options for UserSetPasswordSerializer."""
|
||||
|
||||
model = User
|
||||
fields = ['password', 'override_warning']
|
||||
|
||||
password = serializers.CharField(
|
||||
label=_('Password'),
|
||||
help_text=_('Password for the user'),
|
||||
write_only=True,
|
||||
required=True,
|
||||
style={'input_type': 'password'},
|
||||
)
|
||||
override_warning = serializers.BooleanField(
|
||||
label=_('Override warning'),
|
||||
help_text=_('Override the warning about password rules'),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class MeUserSerializer(ExtendedUserSerializer):
|
||||
"""API serializer specifically for the 'me' endpoint."""
|
||||
|
||||
|
@ -209,6 +209,32 @@ class UserAPITests(InvenTreeAPITestCase):
|
||||
self.assertEqual(len(data['permissions']), len(perms) + len(build_perms))
|
||||
|
||||
|
||||
class SuperuserAPITests(InvenTreeAPITestCase):
|
||||
"""Tests for user API endpoints that require superuser rights."""
|
||||
|
||||
fixtures = ['users']
|
||||
superuser = True
|
||||
|
||||
def test_user_password_set(self):
|
||||
"""Test the set-password/ endpoint."""
|
||||
user = User.objects.get(pk=2)
|
||||
url = reverse('api-user-set-password', kwargs={'pk': user.pk})
|
||||
|
||||
# to simple password
|
||||
resp = self.put(url, {'password': 1}, expected_code=400)
|
||||
self.assertContains(resp, 'This password is too short', status_code=400)
|
||||
|
||||
# now with overwerite
|
||||
resp = self.put(
|
||||
url, {'password': 1, 'override_warning': True}, expected_code=200
|
||||
)
|
||||
self.assertEqual(resp.data, {})
|
||||
|
||||
# complex enough pwd
|
||||
resp = self.put(url, {'password': 'inventree'}, expected_code=200)
|
||||
self.assertEqual(resp.data, {})
|
||||
|
||||
|
||||
class UserTokenTests(InvenTreeAPITestCase):
|
||||
"""Tests for user token functionality."""
|
||||
|
||||
|
Reference in New Issue
Block a user