core/auth/validations.py

117 lines
3.7 KiB
Python
Raw Permalink Normal View History

2025-02-09 19:26:50 +00:00
import re
from datetime import datetime
from typing import Dict, List, Optional, Union
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
from pydantic import BaseModel, Field, field_validator
# RFC 5322 compliant email regex pattern
EMAIL_PATTERN = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
class AuthInput(BaseModel):
"""Base model for authentication input validation"""
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
user_id: str = Field(description="Unique user identifier")
username: str = Field(min_length=2, max_length=50)
token: str = Field(min_length=32)
2025-02-11 09:00:35 +00:00
@field_validator("user_id")
2025-02-09 19:26:50 +00:00
@classmethod
def validate_user_id(cls, v: str) -> str:
if not v.strip():
raise ValueError("user_id cannot be empty")
return v
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
class UserRegistrationInput(BaseModel):
"""Validation model for user registration"""
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
email: str = Field(max_length=254) # Max email length per RFC 5321
password: str = Field(min_length=8, max_length=100)
name: str = Field(min_length=2, max_length=50)
2025-02-11 09:00:35 +00:00
@field_validator("email")
2025-02-09 19:26:50 +00:00
@classmethod
def validate_email(cls, v: str) -> str:
"""Validate email format"""
if not re.match(EMAIL_PATTERN, v):
raise ValueError("Invalid email format")
return v.lower()
2025-02-11 09:00:35 +00:00
@field_validator("password")
2025-02-09 19:26:50 +00:00
@classmethod
def validate_password_strength(cls, v: str) -> str:
"""Validate password meets security requirements"""
if not any(c.isupper() for c in v):
raise ValueError("Password must contain at least one uppercase letter")
if not any(c.islower() for c in v):
raise ValueError("Password must contain at least one lowercase letter")
if not any(c.isdigit() for c in v):
raise ValueError("Password must contain at least one number")
if not any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in v):
raise ValueError("Password must contain at least one special character")
return v
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
class UserLoginInput(BaseModel):
"""Validation model for user login"""
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
email: str = Field(max_length=254)
password: str = Field(min_length=8, max_length=100)
2025-02-11 09:00:35 +00:00
@field_validator("email")
2025-02-09 19:26:50 +00:00
@classmethod
def validate_email(cls, v: str) -> str:
if not re.match(EMAIL_PATTERN, v):
raise ValueError("Invalid email format")
return v.lower()
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
class TokenPayload(BaseModel):
"""Validation model for JWT token payload"""
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
user_id: str
username: str
exp: datetime
iat: datetime
scopes: Optional[List[str]] = []
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
class OAuthInput(BaseModel):
"""Validation model for OAuth input"""
2025-02-11 09:00:35 +00:00
provider: str = Field(pattern="^(google|github|facebook)$")
2025-02-09 19:26:50 +00:00
code: str
redirect_uri: Optional[str] = None
2025-02-11 09:00:35 +00:00
@field_validator("provider")
2025-02-09 19:26:50 +00:00
@classmethod
def validate_provider(cls, v: str) -> str:
2025-02-11 09:00:35 +00:00
valid_providers = ["google", "github", "facebook"]
2025-02-09 19:26:50 +00:00
if v.lower() not in valid_providers:
raise ValueError(f"Provider must be one of: {', '.join(valid_providers)}")
return v.lower()
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
class AuthResponse(BaseModel):
"""Validation model for authentication responses"""
2025-02-11 09:00:35 +00:00
2025-02-09 19:26:50 +00:00
success: bool
token: Optional[str] = None
error: Optional[str] = None
user: Optional[Dict[str, Union[str, int, bool]]] = None
2025-02-11 09:00:35 +00:00
@field_validator("error")
2025-02-09 19:26:50 +00:00
@classmethod
def validate_error_if_not_success(cls, v: Optional[str], info) -> Optional[str]:
2025-02-11 09:00:35 +00:00
if not info.data.get("success") and not v:
2025-02-09 19:26:50 +00:00
raise ValueError("Error message required when success is False")
return v
2025-02-11 09:00:35 +00:00
@field_validator("token")
2025-02-09 19:26:50 +00:00
@classmethod
def validate_token_if_success(cls, v: Optional[str], info) -> Optional[str]:
2025-02-11 09:00:35 +00:00
if info.data.get("success") and not v:
2025-02-09 19:26:50 +00:00
raise ValueError("Token required when success is True")
2025-02-11 09:00:35 +00:00
return v