"""
Input validation utilities for MUD client.
This module provides validation functions for user input across all client
operations. All validators return a tuple of (is_valid: bool, error_message: str).
Validation is separated from API logic to enable:
- Reusability across different UI contexts
- Easy testing of validation rules
- Consistent error messages
- Client-side validation before API calls
Password Validation:
This module integrates with the password_policy module for comprehensive
password strength validation. The validate_password_strength() function
provides detailed feedback including:
- Policy compliance checking (length, character classes)
- Common password rejection
- Sequential/repeated character detection
- Strength scoring and entropy estimation
Common Patterns:
All validators return (bool, str) tuples:
- (True, "") for valid input
- (False, "Error message") for invalid input
See Also:
mud_server.api.password_policy: Comprehensive password policy enforcement.
"""
from mud_server.api.password_policy import (
PolicyLevel,
ValidationResult,
get_password_requirements,
)
from mud_server.api.password_policy import (
validate_password_strength as _validate_strength,
)
[docs]
def validate_username(username: str | None) -> tuple[bool, str]:
"""
Validate username meets requirements.
Requirements:
- Must not be None or empty
- Must be at least 2 characters after stripping whitespace
Args:
username: Username to validate
Returns:
Tuple of (is_valid, error_message)
- (True, "") if valid
- (False, error_message) if invalid
Examples:
>>> validate_username("alice")
(True, "")
>>> validate_username("a")
(False, "Username must be at least 2 characters.")
>>> validate_username(" ")
(False, "Username must be at least 2 characters.")
>>> validate_username(None)
(False, "Username must be at least 2 characters.")
"""
if not username or len(username.strip()) < 2:
return False, "Username must be at least 2 characters."
return True, ""
[docs]
def validate_password(password: str | None, min_length: int = 8) -> tuple[bool, str]:
"""
Validate password meets basic length requirements.
This is a simple length-only validator for backward compatibility.
For comprehensive password strength validation including common password
checking, sequential/repeated character detection, and strength scoring,
use validate_password_with_policy() instead.
Requirements:
- Must not be None or empty
- Must be at least min_length characters (default: 8)
Args:
password: Password to validate
min_length: Minimum required length (default: 8)
Returns:
Tuple of (is_valid, error_message)
Examples:
>>> validate_password("password123")
(True, "")
>>> validate_password("short")
(False, "Password must be at least 8 characters.")
>>> validate_password(None)
(False, "Password is required.")
See Also:
validate_password_with_policy: Comprehensive password validation.
"""
if password is None or password == "":
return False, "Password is required."
if len(password) < min_length:
return False, f"Password must be at least {min_length} characters."
return True, ""
[docs]
def validate_password_with_policy(
password: str | None,
level: PolicyLevel = PolicyLevel.STANDARD,
) -> tuple[bool, str, ValidationResult | None]:
"""
Validate password against comprehensive security policy.
This function performs thorough password validation using the password
policy module. It checks for:
- Minimum length requirements (12 chars for STANDARD policy)
- Common/compromised password rejection
- Sequential character patterns (abc, 123)
- Repeated character patterns (aaa, 111)
- Character class diversity (uppercase, lowercase, digits, special)
The function returns detailed feedback including a strength score and
specific error messages to help users create stronger passwords.
Args:
password: Password to validate. Must not be None or empty.
level: Security policy level to enforce. Options:
- PolicyLevel.BASIC: 8 char minimum, basic checks
- PolicyLevel.STANDARD: 12 char minimum, comprehensive checks (default)
- PolicyLevel.STRICT: 16 char minimum, all character classes required
Returns:
Tuple of (is_valid, error_message, validation_result):
- is_valid: True if password meets all policy requirements
- error_message: Combined error messages or empty string if valid
- validation_result: Full ValidationResult object with score, warnings, etc.
None if password was None/empty.
Examples:
>>> is_valid, msg, result = validate_password_with_policy("MyStr0ng!Pass#2024")
>>> is_valid
True
>>> result.score > 70
True
>>> is_valid, msg, result = validate_password_with_policy("password123")
>>> is_valid
False
>>> "common" in msg.lower()
True
>>> is_valid, msg, result = validate_password_with_policy("short")
>>> is_valid
False
>>> "12 characters" in msg
True
See Also:
mud_server.api.password_policy: Full policy configuration options.
get_password_requirements_text: Get human-readable requirements.
"""
if password is None or password == "":
return False, "Password is required.", None
result = _validate_strength(password, level=level)
if result.is_valid:
return True, "", result
# Combine all errors into a single message
error_message = " ".join(result.errors)
return False, error_message, result
[docs]
def get_password_requirements_text(
level: PolicyLevel = PolicyLevel.STANDARD,
) -> str:
"""
Get human-readable password requirements for display to users.
This function returns a formatted string describing all password
requirements for the specified policy level. Useful for displaying
in registration forms, password change dialogs, or help text.
Args:
level: Policy level to describe. Options:
- PolicyLevel.BASIC: Minimal requirements
- PolicyLevel.STANDARD: Recommended requirements (default)
- PolicyLevel.STRICT: Maximum security requirements
Returns:
Multi-line string describing all password requirements.
Examples:
>>> requirements = get_password_requirements_text()
>>> "12 characters" in requirements
True
>>> "common" in requirements.lower()
True
>>> strict_req = get_password_requirements_text(PolicyLevel.STRICT)
>>> "16 characters" in strict_req
True
>>> "uppercase" in strict_req.lower()
True
"""
return get_password_requirements(level)
[docs]
def validate_password_confirmation(
password: str | None,
password_confirm: str | None,
) -> tuple[bool, str]:
"""
Validate that password and confirmation match.
This should be called AFTER validate_password() has confirmed the
password meets basic requirements.
Args:
password: Original password
password_confirm: Confirmation password
Returns:
Tuple of (is_valid, error_message)
Examples:
>>> validate_password_confirmation("password123", "password123")
(True, "")
>>> validate_password_confirmation("password123", "different")
(False, "Passwords do not match.")
"""
if password != password_confirm:
return False, "Passwords do not match."
return True, ""
[docs]
def validate_password_different(
old_password: str | None,
new_password: str | None,
) -> tuple[bool, str]:
"""
Validate that new password is different from old password.
Args:
old_password: Current/old password
new_password: New password to set
Returns:
Tuple of (is_valid, error_message)
Examples:
>>> validate_password_different("old123", "new456")
(True, "")
>>> validate_password_different("same123", "same123")
(False, "New password must be different from current password.")
"""
if old_password == new_password:
return False, "New password must be different from current password."
return True, ""
[docs]
def validate_required_field(value: str | None, field_name: str) -> tuple[bool, str]:
"""
Validate that a required field has a value.
Args:
value: Field value to check
field_name: Name of field (for error message)
Returns:
Tuple of (is_valid, error_message)
Examples:
>>> validate_required_field("something", "username")
(True, "")
>>> validate_required_field("", "password")
(False, "Password is required.")
>>> validate_required_field(None, "email")
(False, "Email is required.")
"""
if not value or not value.strip():
return False, f"{field_name.capitalize()} is required."
return True, ""
[docs]
def validate_session_state(session_state: dict) -> tuple[bool, str]:
"""
Validate that user has an active session.
Args:
session_state: Session state dictionary from Gradio
Returns:
Tuple of (is_valid, error_message)
Examples:
>>> validate_session_state({"logged_in": True})
(True, "")
>>> validate_session_state({"logged_in": False})
(False, "You are not logged in.")
>>> validate_session_state({})
(False, "You are not logged in.")
"""
if not session_state.get("logged_in"):
return False, "You are not logged in."
return True, ""
[docs]
def validate_admin_role(session_state: dict) -> tuple[bool, str]:
"""
Validate that user has admin or superuser role.
This should be called AFTER validate_session_state() has confirmed
the user is logged in.
Args:
session_state: Session state dictionary from Gradio
Returns:
Tuple of (is_valid, error_message)
Examples:
>>> validate_admin_role({"role": "admin"})
(True, "")
>>> validate_admin_role({"role": "superuser"})
(True, "")
>>> validate_admin_role({"role": "player"})
(False, "Access Denied: Admin or Superuser role required.")
"""
role = session_state.get("role", "player")
if role not in ["admin", "superuser"]:
return False, "Access Denied: Admin or Superuser role required."
return True, ""