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 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-06 00:20:21 -05:00
parent 741938cf1f
commit 02e00963e1
2 changed files with 46 additions and 1 deletions

View File

@@ -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)

View File

@@ -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