2
0
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:
Matthias Mair
2025-06-19 01:14:59 +02:00
committed by GitHub
parent 8346318f7d
commit d5fa609275
13 changed files with 242 additions and 24 deletions

View File

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

View File

@ -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'),
]

View File

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

View File

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