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 datetime import datetime
from typing import Literal, Optional from typing import Literal, Optional
from uuid import UUID from uuid import UUID
from pydantic import BaseModel, EmailStr, Field import re
from pydantic import BaseModel, EmailStr, Field, field_validator
class UserBase(BaseModel): class UserBase(BaseModel):
@@ -13,6 +14,17 @@ class UserCreate(UserBase):
password: str = Field(..., min_length=10, description="Password must be at least 10 characters") 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)") 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): class UserUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=255) 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.status_code == 201
assert response.json()["role"] == "engineer" 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