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:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user