Coverage for server / validators / accounts_validator.py: 80%
74 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
1import re
2from fastapi import HTTPException, status
5def validate_fields(cls, values):
6 try:
7 values = is_not_empty(cls, values)
8 values = validate_field_lengths(cls, values)
9 values["email"] = check_email(cls, values["email"])
10 values = password_must_be_valid(cls, values)
11 return values
12 except Exception as e:
13 raise
16def validate_update_fields(cls, values):
17 try:
18 values = is_not_empty(cls, values)
19 values = validate_field_lengths(cls, values)
20 values["email"] = check_email(cls, values["email"])
21 return values
22 except ValueError as e:
23 raise
26def validate_field_lengths(cls, values):
27 """
28 Validates that fields don't exceed maximum character limits.
30 Field length limits:
31 - first_name: 50 characters
32 - middle_name: 50 characters
33 - last_name: 50 characters
34 - email: 100 characters
35 - school: 100 characters
36 # - created_by: 100 characters
37 # - updated_by: 100 characters
39 Args:
40 cls: The class instance
41 values (dict): Dictionary containing field values
43 Returns:
44 dict: The validated values dictionary
46 Raises:
47 ValueError: If any field exceeds its maximum length
48 """
49 field_limits = {
50 "first_name": 50,
51 "middle_name": 50,
52 "last_name": 50,
53 "email": 100,
54 "school": 100,
55 # "created_by": 100,
56 # "updated_by": 100
57 }
59 for field, limit in field_limits.items():
60 if field in values and values[field] is not None:
61 if len(str(values[field])) > limit:
62 raise ValueError(f"{field} field exceeds maximum length of {limit} characters")
64 return values
67def is_not_empty(cls, values):
68 for attr, value in values.items():
69 if attr in ["middle_name","created_by", "updated_by", "organization", "profile_picture"]:
70 continue
71 else:
72 if str(value).strip() == "" and attr != "school":
73 raise ValueError(f"{attr} field should not be empty")
75 if (
76 attr == "school"
77 and "role" in values.keys()
78 and values["role"] == "subscriber"
79 ):
80 if str(value).strip() == "":
81 raise ValueError(f"{attr} field should not be empty")
82 if ("role" not in values):
83 raise ValueError("Role field should not be empty")
84 if ("email" not in values):
85 raise ValueError("Email field is required")
87 return values
90def password_must_be_valid(cls, values):
91 """
92 Validates password fields according to security requirements.
94 Password requirements:
95 - Length: 10-30 characters
96 - Must contain at least one lowercase letter
97 - Must contain at least one uppercase letter
98 - Must contain at least one number
99 - Must contain at least one special character (@$!%*#?&.)
100 - New password must differ from old password
101 - New password must match the confirmation password
103 Args:
104 cls: The class instance
105 values (dict): Dictionary containing password fields:
106 - 'password': For initial password setting
107 - 'new_password': For password changes
108 - 'old_password': Current password (required for changes)
109 - 'repeat_new_password': Password confirmation
111 Returns:
112 dict: The validated values dictionary
114 Raises:
115 ValueError: If any password validation fails
116 """
117 # Define regex pattern for password requirements
118 password_requirements = (
119 r"^(?=.*[a-z])" # At least one lowercase letter
120 r"(?=.*[A-Z])" # At least one uppercase letter
121 r"(?=.*\d)" # At least one digit
122 r"(?=.*[@$!%*#?&.])" # At least one special character
123 r"[A-Za-z\d@$!%*#?&.]" # Only allow these characters
124 r"{10,25}$" # Length between 10-25 characters (canonical)
125 )
126 password_pattern = re.compile(password_requirements)
128 # Determine which password fields to use
129 if "new_password" in values:
130 password_field = "new_password"
131 repeat_field = "repeat_new_password"
132 password = values[password_field]
134 # Validate password complexity
135 if not password_pattern.match(password):
136 raise ValueError("New password is invalid.")
138 # Ensure new password is different from old password
139 if "old_password" in values and password == values["old_password"]:
140 raise ValueError("New password must be different from the old password.")
142 # Verify password confirmation matches
143 if values.get(repeat_field) != password:
144 raise HTTPException(
145 status_code=status.HTTP_400_BAD_REQUEST,
146 detail="Passwords do not match"
147 )
149 # Handle initial password setting scenario
150 elif "password" in values:
151 password_field = "password"
152 repeat_field = "repeat_password"
154 if password_field not in values or repeat_field not in values:
155 raise HTTPException(
156 status_code=status.HTTP_400_BAD_REQUEST,
157 detail="Field required"
158 )
160 password = values[password_field]
161 repeat_password = values[repeat_field]
164 if not password_pattern.match(password):
165 raise HTTPException(
166 status_code=status.HTTP_400_BAD_REQUEST,
167 detail="Invalid password"
168 )
170 if repeat_password != password:
171 raise HTTPException(
172 status_code=status.HTTP_400_BAD_REQUEST,
173 detail="Passwords do not match"
174 )
176 return values
179def check_email(cls, email_value):
180 if not email_value or email_value is None:
181 raise ValueError("Email field should not be empty.")
183 # Convert email to lowercase for case insensitivity
184 email_value = email_value
186 # Define the email validation regex
187 email_pattern = (
188 r"^([a-zA-Z][a-zA-Z0-9_\.]{7,29})" # Username: 8-30 alphanumeric chars, starts with a letter, allows underscores
189 r"@eruditiontx\.com$" # Domain: exactly "@eruditiontx.com"
190 )
191 pattern = re.compile(email_pattern)
193 # Validate the email
194 if not pattern.match(email_value):
195 raise ValueError("Email is invalid.")
197 return email_value