From 170ec90ec12d18ecb6662d2226373306d49d6bc7 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Sun, 7 Jun 2026 00:50:42 +0330 Subject: [PATCH] fix(demo): block external account actions --- apps/users/api/serializers.py | 4 +++- apps/users/api/views.py | 25 +++++++++++++++++++++++++ apps/users/services/auth.py | 6 +++++- apps/workspaces/api/views.py | 15 ++++++++++++++- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/apps/users/api/serializers.py b/apps/users/api/serializers.py index ea45fcb..6b16be8 100644 --- a/apps/users/api/serializers.py +++ b/apps/users/api/serializers.py @@ -212,10 +212,12 @@ class UserProfileSerializer(BaseModelSerializer): "profile_picture", "birth_date", "is_verified", + "is_demo", + "demo_expires_at", "full_name", "age", ) - read_only_fields = BaseModelSerializer.Meta.fields + ("mobile", "is_verified") + read_only_fields = BaseModelSerializer.Meta.fields + ("mobile", "is_verified", "is_demo", "demo_expires_at") class UserSearchSerializer(serializers.ModelSerializer): diff --git a/apps/users/api/views.py b/apps/users/api/views.py index 509f715..bcc505b 100644 --- a/apps/users/api/views.py +++ b/apps/users/api/views.py @@ -293,6 +293,11 @@ class ChangePasswordView(APIView): @extend_schema(request=ChangePasswordSerializer) def patch(self, request, *args, **kwargs): + if getattr(request.user, "is_demo", False): + return Response( + {"detail": "Demo accounts cannot change passwords."}, + status=status.HTTP_403_FORBIDDEN, + ) serializer = ChangePasswordSerializer(data=request.data, context={"request": request}) serializer.is_valid(raise_exception=True) @@ -327,6 +332,11 @@ class SetPasswordView(UpdateAPIView): @extend_schema(request=ChangePasswordSerializer, responses=None) def patch(self, request, *args, **kwargs): + if getattr(request.user, "is_demo", False): + return Response( + {"detail": "Demo accounts cannot change passwords."}, + status=status.HTTP_403_FORBIDDEN, + ) return super().patch(request, *args, **kwargs) def get_object(self): @@ -347,6 +357,11 @@ class ProfilePictureView(APIView): operation_id="users_profile_picture_self_create", ) def post(self, request): + if getattr(request.user, "is_demo", False): + return Response( + {"detail": "Demo accounts cannot upload profile pictures."}, + status=status.HTTP_403_FORBIDDEN, + ) serializer = UserProfilePictureSerializer( instance=request.user, data=request.data, @@ -362,6 +377,11 @@ class ProfilePictureView(APIView): operation_id="users_profile_picture_self_delete", ) def delete(self, request): + if getattr(request.user, "is_demo", False): + return Response( + {"detail": "Demo accounts cannot remove profile pictures."}, + status=status.HTTP_403_FORBIDDEN, + ) request.user.profile_picture.delete(save=False) request.user.profile_picture = None request.user.save(update_fields=["profile_picture", "updated_at"]) @@ -401,6 +421,11 @@ class UserSearchAPIView(APIView): permission_classes = [IsAuthenticated] def get(self, request): + if getattr(request.user, "is_demo", False): + return Response( + {"detail": "Demo accounts cannot search external users."}, + status=status.HTTP_403_FORBIDDEN, + ) mobile = request.query_params.get('mobile') if not mobile: return Response( diff --git a/apps/users/services/auth.py b/apps/users/services/auth.py index f0939ad..7ceb7a3 100644 --- a/apps/users/services/auth.py +++ b/apps/users/services/auth.py @@ -81,7 +81,11 @@ def register_user_with_otp(mobile, code, password, first_name="", last_name=""): def generate_and_send_otp(mobile, mode): """Business logic for generating OTP, checking existence rules, and sending SMS.""" - user_exists = User.objects.filter(mobile=mobile).exists() + user = User.objects.filter(mobile=mobile).only("is_demo").first() + user_exists = user is not None + + if user and user.is_demo: + raise ValidationError({"mobile": "Demo accounts cannot use SMS verification."}) if mode == "register" and user_exists: raise ValidationError({"mobile": "این شماره قبلاً ثبت‌نام شده است."}) diff --git a/apps/workspaces/api/views.py b/apps/workspaces/api/views.py index 6ec367b..6250666 100644 --- a/apps/workspaces/api/views.py +++ b/apps/workspaces/api/views.py @@ -94,6 +94,14 @@ class WorkspaceViewSet(ModelViewSet): def perform_create(self, serializer): serializer.save(owner=self.request.user) + def create(self, request, *args, **kwargs): + if getattr(request.user, "is_demo", False): + return Response( + {"detail": "Demo accounts cannot create additional workspaces."}, + status=status.HTTP_403_FORBIDDEN, + ) + return super().create(request, *args, **kwargs) + @action(detail=True, methods=["get"], url_path="my-rates") def my_rates(self, request, pk=None): workspace = self.get_object() @@ -246,7 +254,12 @@ class WorkspaceMembershipViewSet(ModelViewSet): status=status.HTTP_400_BAD_REQUEST ) - workspace = get_object_or_404(Workspace, id=workspace_id) + workspace = get_object_or_404(Workspace, id=workspace_id) + if getattr(request.user, "is_demo", False): + return Response( + {"detail": "Demo accounts cannot add workspace members."}, + status=status.HTTP_403_FORBIDDEN, + ) permission = IsWorkspaceAdmin() if not permission.has_object_permission(request, self, workspace):