Spaces:
Paused
Paused
File size: 6,190 Bytes
d94d354 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
"""
User authentication models and validation for OpenManus
Mobile number + password based authentication system
"""
import hashlib
import re
import secrets
from datetime import datetime, timedelta
from typing import Optional
from dataclasses import dataclass
from pydantic import BaseModel, validator
class UserSignupRequest(BaseModel):
"""User signup request model"""
full_name: str
mobile_number: str
password: str
confirm_password: str
@validator("full_name")
def validate_full_name(cls, v):
if not v or len(v.strip()) < 2:
raise ValueError("Full name must be at least 2 characters long")
if len(v.strip()) > 100:
raise ValueError("Full name must be less than 100 characters")
return v.strip()
@validator("mobile_number")
def validate_mobile_number(cls, v):
# Remove all non-digit characters
digits_only = re.sub(r"\D", "", v)
# Check if it's a valid mobile number (10-15 digits)
if len(digits_only) < 10 or len(digits_only) > 15:
raise ValueError("Mobile number must be between 10-15 digits")
# Ensure it starts with country code or local format
if not re.match(r"^(\+?[1-9]\d{9,14})$", digits_only):
raise ValueError("Invalid mobile number format")
return digits_only
@validator("password")
def validate_password(cls, v):
if len(v) < 8:
raise ValueError("Password must be at least 8 characters long")
if len(v) > 128:
raise ValueError("Password must be less than 128 characters")
# Check for at least one uppercase, lowercase, and digit
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"\d", v):
raise ValueError("Password must contain at least one digit")
return v
@validator("confirm_password")
def validate_confirm_password(cls, v, values):
if "password" in values and v != values["password"]:
raise ValueError("Passwords do not match")
return v
class UserLoginRequest(BaseModel):
"""User login request model"""
mobile_number: str
password: str
@validator("mobile_number")
def validate_mobile_number(cls, v):
# Remove all non-digit characters
digits_only = re.sub(r"\D", "", v)
if len(digits_only) < 10 or len(digits_only) > 15:
raise ValueError("Invalid mobile number")
return digits_only
@dataclass
class User:
"""User model"""
id: str
mobile_number: str
full_name: str
password_hash: str
avatar_url: Optional[str] = None
preferences: Optional[str] = None
is_active: bool = True
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
@dataclass
class UserSession:
"""User session model"""
session_id: str
user_id: str
mobile_number: str
full_name: str
created_at: datetime
expires_at: datetime
@property
def is_valid(self) -> bool:
"""Check if session is still valid"""
return datetime.utcnow() < self.expires_at
class UserAuth:
"""User authentication utilities"""
@staticmethod
def hash_password(password: str) -> str:
"""Hash password using SHA-256 with salt"""
salt = secrets.token_hex(32)
password_hash = hashlib.sha256((password + salt).encode()).hexdigest()
return f"{salt}:{password_hash}"
@staticmethod
def verify_password(password: str, password_hash: str) -> bool:
"""Verify password against stored hash"""
try:
salt, stored_hash = password_hash.split(":")
password_hash_check = hashlib.sha256((password + salt).encode()).hexdigest()
return password_hash_check == stored_hash
except ValueError:
return False
@staticmethod
def generate_session_id() -> str:
"""Generate secure session ID"""
return secrets.token_urlsafe(32)
@staticmethod
def generate_user_id() -> str:
"""Generate unique user ID"""
return f"user_{secrets.token_hex(16)}"
@staticmethod
def format_mobile_number(mobile_number: str) -> str:
"""Format mobile number for consistent storage"""
# Remove all non-digit characters
digits_only = re.sub(r"\D", "", mobile_number)
# Add + prefix if not present and format consistently
if not digits_only.startswith("+"):
# Assume it's a local number, add default country code if needed
if len(digits_only) == 10: # US format
digits_only = f"1{digits_only}"
return f"+{digits_only}"
@staticmethod
def create_session(user: User, duration_hours: int = 24) -> UserSession:
"""Create a new user session"""
session_id = UserAuth.generate_session_id()
created_at = datetime.utcnow()
expires_at = created_at + timedelta(hours=duration_hours)
return UserSession(
session_id=session_id,
user_id=user.id,
mobile_number=user.mobile_number,
full_name=user.full_name,
created_at=created_at,
expires_at=expires_at,
)
# Response models
class AuthResponse(BaseModel):
"""Authentication response model"""
success: bool
message: str
session_id: Optional[str] = None
user_id: Optional[str] = None
full_name: Optional[str] = None
class UserProfile(BaseModel):
"""User profile response model"""
user_id: str
full_name: str
mobile_number: str # Masked for security
avatar_url: Optional[str] = None
created_at: Optional[str] = None
@staticmethod
def mask_mobile_number(mobile_number: str) -> str:
"""Mask mobile number for security (show only last 4 digits)"""
if len(mobile_number) <= 4:
return "*" * len(mobile_number)
return "*" * (len(mobile_number) - 4) + mobile_number[-4:]
|