Skip to content
15 changes: 15 additions & 0 deletions apps/api/plane/app/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
118 changes: 118 additions & 0 deletions apps/api/plane/tests/unit/serializers/test_user.py
Original file line number Diff line number Diff line change
@@ -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 (_)"
)
5 changes: 5 additions & 0 deletions apps/web/core/components/onboarding/steps/profile/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ export const ProfileSetupStep: FC<Props> = 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;
},
Comment on lines +216 to +220
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Align rule with copy and block whitespace-only names

Message says “First name” though label is “Name”, and you allow spaces but don’t mention them; also accepts only-spaces. Trim and update message.

-              validate: (value) => {
-                if (!/^[a-zA-Z0-9 _-]+$/.test(value))
-                  return "First name can only contain letters, numbers, hyphens, and underscores.";
-                return true;
-              },
+              validate: (value) => {
+                const v = value.trim();
+                if (v.length === 0) return "Name can't be empty or only spaces.";
+                if (!/^[A-Za-z0-9 _-]+$/.test(v))
+                  return "Name can only contain letters, numbers, spaces, hyphens (-), and underscores (_).";
+                return true;
+              },

If the PR removes last name in onboarding, also delete it from:

  • TProfileSetupFormValues and defaultValues
  • handleSubmitUserDetail payload (Line 84)

🏁 Script executed:

#!/bin/bash
# Find remaining onboarding references to last_name
rg -nP -C2 '\blast_name\b' apps/web/core/components/onboarding

Length of output: 8338


Update name validation to trim whitespace and align message copy

apps/web/core/components/onboarding/steps/profile/root.tsx (lines 216–220):

- validate: (value) => {
-   if (!/^[a-zA-Z0-9 _-]+$/.test(value))
-     return "First name can only contain letters, numbers, hyphens, and underscores.";
-   return true;
- },
+ validate: (value) => {
+   const trimmed = value.trim();
+   if (trimmed.length === 0) return "Name can't be empty or only spaces.";
+   if (!/^[A-Za-z0-9 _-]+$/.test(trimmed))
+     return "Name can only contain letters, numbers, spaces, hyphens (-), and underscores (_).";
+   return true;
+ },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
validate: (value) => {
if (!/^[a-zA-Z0-9 _-]+$/.test(value))
return "First name can only contain letters, numbers, hyphens, and underscores.";
return true;
},
validate: (value) => {
const trimmed = value.trim();
if (trimmed.length === 0)
return "Name can't be empty or only spaces.";
if (!/^[A-Za-z0-9 _-]+$/.test(trimmed))
return "Name can only contain letters, numbers, spaces, hyphens (-), and underscores (_).";
return true;
},
🤖 Prompt for AI Agents
In apps/web/core/components/onboarding/steps/profile/root.tsx around lines 216
to 220, update the validation to trim leading/trailing whitespace before testing
and adjust the error copy to mention spaces explicitly: compute const v = (value
|| "").trim(); then run the regex test against v and if it fails return "First
name can only contain letters, numbers, spaces, hyphens, and underscores."
(optionally also check if v === "" and return a required-name message if
needed).

}}
render={({ field: { value, onChange, ref } }) => (
<input
Expand Down
14 changes: 14 additions & 0 deletions apps/web/core/components/profile/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ export const ProfileForm = observer((props: TProfileFormProps) => {
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 } }) => (
<Input
Expand All @@ -271,6 +276,14 @@ export const ProfileForm = observer((props: TProfileFormProps) => {
<Controller
control={control}
name="last_name"
rules={{
validate: (value) => {
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 } }) => (
<Input
id="last_name"
Expand All @@ -287,6 +300,7 @@ export const ProfileForm = observer((props: TProfileFormProps) => {
/>
)}
/>
{errors?.last_name && <span className="text-xs text-red-500">{errors?.last_name?.message}</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm font-medium text-custom-text-200">
Expand Down
Loading