From 02e00963e183dba5059b13ff58f5bf344b68087f Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 6 Feb 2026 00:20:21 -0500 Subject: [PATCH] feat: add password complexity validation Passwords must now contain at least one uppercase letter, one lowercase letter, and one digit (in addition to the existing 10-char minimum). Co-Authored-By: Claude Opus 4.6 --- backend/app/schemas/user.py | 14 +++++++++++++- backend/tests/test_auth.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 4179171d..b2f5d81e 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -1,7 +1,8 @@ from datetime import datetime from typing import Literal, Optional from uuid import UUID -from pydantic import BaseModel, EmailStr, Field +import re +from pydantic import BaseModel, EmailStr, Field, field_validator class UserBase(BaseModel): @@ -13,6 +14,17 @@ class UserCreate(UserBase): password: str = Field(..., min_length=10, description="Password must be at least 10 characters") invite_code: Optional[str] = Field(None, description="Invite code for registration (required when invite system is enabled)") + @field_validator('password') + @classmethod + def password_complexity(cls, v: str) -> str: + if not re.search(r'[A-Z]', v): + raise ValueError('Password must contain at least one uppercase letter') + if not re.search(r'[a-z]', v): + raise ValueError('Password must contain at least one lowercase letter') + if not re.search(r'[0-9]', v): + raise ValueError('Password must contain at least one digit') + return v + class UserUpdate(BaseModel): name: Optional[str] = Field(None, min_length=1, max_length=255) diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 9e0727dd..5e578900 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -121,3 +121,36 @@ class TestAuthentication: assert response.status_code == 201 assert response.json()["role"] == "engineer" + + @pytest.mark.asyncio + async def test_register_rejects_no_uppercase(self, client: AsyncClient): + """Test that password without uppercase is rejected.""" + user_data = { + "email": "weak1@example.com", + "password": "alllowercase123", + "name": "Weak User" + } + response = await client.post("/api/v1/auth/register", json=user_data) + assert response.status_code == 422 + + @pytest.mark.asyncio + async def test_register_rejects_no_lowercase(self, client: AsyncClient): + """Test that password without lowercase is rejected.""" + user_data = { + "email": "weak2@example.com", + "password": "ALLUPPERCASE123", + "name": "Weak User" + } + response = await client.post("/api/v1/auth/register", json=user_data) + assert response.status_code == 422 + + @pytest.mark.asyncio + async def test_register_rejects_no_digit(self, client: AsyncClient): + """Test that password without digit is rejected.""" + user_data = { + "email": "weak3@example.com", + "password": "NoDigitsHere!!", + "name": "Weak User" + } + response = await client.post("/api/v1/auth/register", json=user_data) + assert response.status_code == 422