diff --git a/apps/api/plane/app/serializers/user.py b/apps/api/plane/app/serializers/user.py index 7b545356884..0c01b1546ba 100644 --- a/apps/api/plane/app/serializers/user.py +++ b/apps/api/plane/app/serializers/user.py @@ -7,16 +7,31 @@ from .base import BaseSerializer +# Django import +import re + class UserSerializer(BaseSerializer): def validate_first_name(self, value): if contains_url(value): raise serializers.ValidationError("First name cannot contain a URL.") + + if not re.match(r"^[a-zA-Z0-9_\- ]+$", value): + raise serializers.ValidationError( + "first name can only contain letters, numbers, hyphens (-), and underscores (_)" + ) + return value def validate_last_name(self, value): if contains_url(value): raise serializers.ValidationError("Last name cannot contain a URL.") + + if not re.match(r"^[a-zA-Z0-9_\- ]+$", value) and not value == "": + raise serializers.ValidationError( + "last name can only contain letters, numbers, hyphens (-), and underscores (_)" + ) + return value class Meta: diff --git a/apps/api/plane/tests/unit/serializers/test_user.py b/apps/api/plane/tests/unit/serializers/test_user.py new file mode 100644 index 00000000000..bbe5f24cdfc --- /dev/null +++ b/apps/api/plane/tests/unit/serializers/test_user.py @@ -0,0 +1,118 @@ +import pytest +from rest_framework import serializers + +from plane.app.serializers.user import UserSerializer + + +@pytest.mark.unit +class TestUserSerializer: + """Test the UserSerializer""" + + def test_validate_first_name_valid(self): + """Test that valid first names are accepted""" + + serializer = UserSerializer() + valid_names = [ + "John", + "John Doe", + "John-Doe", + "John_Doe", + "John123", + ] + + for name in valid_names: + result = serializer.validate_first_name(name) + + assert result == name + + def test_validate_first_name_with_url(self): + """Test that first names containing URLs are rejected""" + + serializer = UserSerializer() + invalid_names = [ + "http://example.com", + "John https://test.com", + "www.test.com", + ] + + for name in invalid_names: + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_first_name(name) + + assert str(exc_info.value.detail[0]) == "First name cannot contain a URL." + + def test_validate_first_name_with_special_chars(self): + """Test that first names with special characters are rejected""" + + serializer = UserSerializer() + invalid_names = [ + "John@Doe", + "John#Doe", + "John$Doe", + "John!Doe", + "John&Doe", + ] + + for name in invalid_names: + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_first_name(name) + + assert str(exc_info.value.detail[0]) == ( + "first name can only contain letters, numbers, " + "hyphens (-), and underscores (_)" + ) + + def test_validate_last_name_valid(self): + """Test that valid last names are accepted""" + + serializer = UserSerializer() + valid_names = [ + "Smith", + "Smith Jr", + "Smith-Jr", + "Smith_Jr", + "Smith123", + "", + ] + + for name in valid_names: + result = serializer.validate_last_name(name) + + assert result == name + + def test_validate_last_name_with_url(self): + """Test that last names containing URLs are rejected""" + + serializer = UserSerializer() + invalid_names = [ + "http://example.com", + "Smith https://test.com", + "www.test.com", + ] + + for name in invalid_names: + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_last_name(name) + + assert str(exc_info.value.detail[0]) == "Last name cannot contain a URL." + + def test_validate_last_name_with_special_chars(self): + """Test that last names with special characters are rejected""" + + serializer = UserSerializer() + invalid_names = [ + "Smith@Jr", + "Smith#Jr", + "Smith$Jr", + "Smith!Jr", + "Smith&Jr", + ] + + for name in invalid_names: + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_last_name(name) + + assert str(exc_info.value.detail[0]) == ( + "last name can only contain letters, numbers, " + "hyphens (-), and underscores (_)" + ) diff --git a/apps/web/core/components/onboarding/steps/profile/root.tsx b/apps/web/core/components/onboarding/steps/profile/root.tsx index 90fc135d728..f589cd9346d 100644 --- a/apps/web/core/components/onboarding/steps/profile/root.tsx +++ b/apps/web/core/components/onboarding/steps/profile/root.tsx @@ -213,6 +213,11 @@ export const ProfileSetupStep: FC = observer(({ handleStepChange }) => { value: 24, message: "Name must be within 24 characters.", }, + validate: (value) => { + if (!/^[a-zA-Z0-9 _-]+$/.test(value)) + return "First name can only contain letters, numbers, hyphens, and underscores."; + return true; + }, }} render={({ field: { value, onChange, ref } }) => ( { name="first_name" rules={{ required: "Please enter first name", + validate: (value) => { + if (!/^[a-zA-Z0-9 _-]+$/.test(value)) + return "First name can only contain letters, numbers, hyphens, and underscores."; + return true; + }, }} render={({ field: { value, onChange, ref } }) => ( { { + if (value === "") return true; + if (!/^[a-zA-Z0-9 _-]+$/.test(value)) + return "Last name can only contain letters, numbers, hyphens, and underscores."; + return true; + }, + }} render={({ field: { value, onChange, ref } }) => ( { /> )} /> + {errors?.last_name && {errors?.last_name?.message}}