From 8b1b59d8ba05a189a4be811b209828d075f4fa7a Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 7 Jun 2025 20:20:48 +0200 Subject: [PATCH] New endpoint to update self account password --- modoboa/admin/api/v1/serializers.py | 33 ------------------------- modoboa/admin/api/v1/viewsets.py | 3 ++- modoboa/admin/api/v2/tests.py | 10 ++++++++ modoboa/core/api/v1/serializers.py | 37 +++++++++++++++++++++++++++++ modoboa/core/api/v2/viewsets.py | 14 +++++++++++ modoboa/core/tests/test_ldap.py | 7 +++--- 6 files changed, 66 insertions(+), 38 deletions(-) diff --git a/modoboa/admin/api/v1/serializers.py b/modoboa/admin/api/v1/serializers.py index ece2ab622..a7b0932b3 100644 --- a/modoboa/admin/api/v1/serializers.py +++ b/modoboa/admin/api/v1/serializers.py @@ -292,39 +292,6 @@ class AccountExistsSerializer(serializers.Serializer): exists = serializers.BooleanField() -class AccountPasswordSerializer(serializers.ModelSerializer): - """A serializer used to change a user password.""" - - new_password = serializers.CharField() - - class Meta: - model = core_models.User - fields = ( - "password", - "new_password", - ) - - def validate_password(self, value): - """Check password.""" - if not self.instance.check_password(value): - raise serializers.ValidationError("Password not correct") - return value - - def validate_new_password(self, value): - """Check new password.""" - try: - password_validation.validate_password(value, self.instance) - except ValidationError as exc: - raise serializers.ValidationError(exc.messages[0]) from None - return value - - def update(self, instance, validated_data): - """Set new password.""" - instance.set_password(validated_data["new_password"]) - instance.save() - return instance - - class WritableAccountSerializer(AccountSerializer): """Serializer to create account.""" diff --git a/modoboa/admin/api/v1/viewsets.py b/modoboa/admin/api/v1/viewsets.py index c97a2512c..9fa0d4dd6 100644 --- a/modoboa/admin/api/v1/viewsets.py +++ b/modoboa/admin/api/v1/viewsets.py @@ -14,6 +14,7 @@ from rest_framework.response import Response from modoboa.core import models as core_models from modoboa.core import sms_backends +from modoboa.core.api.v1 import serializers as core_serializers from modoboa.lib import renderers as lib_renderers from modoboa.lib import viewsets as lib_viewsets from modoboa.lib.throttle import GetThrottleViewsetMixin, PasswordResetRequestThrottle @@ -112,7 +113,7 @@ class AccountViewSet( action_dict = { "list": serializers.AccountSerializer, "retrieve": serializers.AccountSerializer, - "password": serializers.AccountPasswordSerializer, + "password": core_serializers.AccountPasswordSerializer, "reset_password": serializers.ResetPasswordSerializer, } return action_dict.get(self.action, serializers.WritableAccountSerializer) diff --git a/modoboa/admin/api/v2/tests.py b/modoboa/admin/api/v2/tests.py index 6d3f542bf..f495c0ebb 100644 --- a/modoboa/admin/api/v2/tests.py +++ b/modoboa/admin/api/v2/tests.py @@ -331,6 +331,16 @@ class AccountViewSetTestCase(ModoAPITestCase): resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) + def test_user_updates_password(self): + account = core_models.User.objects.get(username="user@test.com") + self.authenticate_user(account) + response = self.client.post( + reverse("v2:account-set-me-password"), + {"password": "toto", "new_password": "Toto1234"}, + format="json", + ) + self.assertEqual(response.status_code, 200) + def test_rename(self): account = core_models.User.objects.get(username="user@test.com") url = reverse("v2:account-detail", args=[account.pk]) diff --git a/modoboa/core/api/v1/serializers.py b/modoboa/core/api/v1/serializers.py index 6f322c9ca..5b38fcbba 100644 --- a/modoboa/core/api/v1/serializers.py +++ b/modoboa/core/api/v1/serializers.py @@ -2,14 +2,51 @@ import logging +from django.contrib.auth import password_validation +from django.core.exceptions import ValidationError from django.utils.html import escape from django.utils.translation import gettext as _ from rest_framework import serializers +from modoboa.core import models + logger = logging.getLogger("modoboa.auth") +class AccountPasswordSerializer(serializers.ModelSerializer): + """A serializer used to change a user password.""" + + new_password = serializers.CharField() + + class Meta: + model = models.User + fields = ( + "password", + "new_password", + ) + + def validate_password(self, value): + """Check password.""" + if not self.instance.check_password(value): + raise serializers.ValidationError("Password not correct") + return value + + def validate_new_password(self, value): + """Check new password.""" + try: + password_validation.validate_password(value, self.instance) + except ValidationError as exc: + raise serializers.ValidationError(exc.messages[0]) from None + return value + + def update(self, instance, validated_data): + """Set new password.""" + instance.set_password(validated_data["new_password"]) + instance.save() + return instance + + class CheckTFASetupSerializer(serializers.Serializer): """Serializer used to finalize 2FA setup.""" diff --git a/modoboa/core/api/v2/viewsets.py b/modoboa/core/api/v2/viewsets.py index 83abf419a..72b41756e 100644 --- a/modoboa/core/api/v2/viewsets.py +++ b/modoboa/core/api/v2/viewsets.py @@ -68,6 +68,20 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet): serializer.is_valid(raise_exception=True) return response.Response() + @action( + methods=["post"], + detail=False, + url_path="me/password/change", + serializer_class=core_v1_serializers.AccountPasswordSerializer, + ) + def set_me_password(self, request): + serializer = core_v1_serializers.AccountPasswordSerializer( + instance=request.user, data=request.data + ) + serializer.is_valid(raise_exception=True) + serializer.save() + return response.Response() + @extend_schema( description="Get current API token", responses={200: serializers.UserAPITokenSerializer}, diff --git a/modoboa/core/tests/test_ldap.py b/modoboa/core/tests/test_ldap.py index 69ab28fda..e1dbe8d42 100644 --- a/modoboa/core/tests/test_ldap.py +++ b/modoboa/core/tests/test_ldap.py @@ -160,10 +160,9 @@ class ProfileTestCase(LDAPTestCaseMixin, ModoAPITestCase): username = "testuser@example.com" self.authenticate(username, "test") - account = models.User.objects.get(username=username) - response = self.client.put( - reverse("v2:account-password", args=[account.id]), - {"password": "test", "newpassword": "Toto1234"}, + response = self.client.post( + reverse("v2:account-set-me-password"), + {"password": "test", "new_password": "Toto1234"}, format="json", ) self.assertEqual(response.status_code, 200)