- Renamed `totalUsers` and `activeUsers` to `total_users` and `active_users` across frontend and backend for consistency. - Enhanced demo user creation by randomizing `created_at` dates for realistic charts. - Expanded demo data to include `is_active` for demo users, improving user status representation. - Refined admin dashboard statistics to support updated user growth data model.
202 lines
5.9 KiB
Python
Executable File
202 lines
5.9 KiB
Python
Executable File
# app/schemas/users.py
|
|
from datetime import datetime
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
|
|
|
|
from app.schemas.validators import validate_password_strength, validate_phone_number
|
|
|
|
|
|
class UserBase(BaseModel):
|
|
email: EmailStr
|
|
first_name: str
|
|
last_name: str | None = None
|
|
phone_number: str | None = None
|
|
|
|
@field_validator("phone_number")
|
|
@classmethod
|
|
def validate_phone(cls, v: str | None) -> str | None:
|
|
return validate_phone_number(v)
|
|
|
|
|
|
class UserCreate(UserBase):
|
|
password: str
|
|
is_superuser: bool = False
|
|
is_active: bool = True
|
|
|
|
@field_validator("password")
|
|
@classmethod
|
|
def password_strength(cls, v: str) -> str:
|
|
"""Enterprise-grade password strength validation"""
|
|
return validate_password_strength(v)
|
|
|
|
|
|
class UserUpdate(BaseModel):
|
|
first_name: str | None = None
|
|
last_name: str | None = None
|
|
phone_number: str | None = None
|
|
password: str | None = None
|
|
preferences: dict[str, Any] | None = None
|
|
locale: str | None = Field(
|
|
None,
|
|
max_length=10,
|
|
pattern=r"^[a-z]{2}(-[A-Z]{2})?$",
|
|
description="User's preferred locale (BCP 47 format: en, it, en-US, it-IT)",
|
|
examples=["en", "it", "en-US", "it-IT"],
|
|
)
|
|
is_active: bool | None = (
|
|
None # Changed default from True to None to avoid unintended updates
|
|
)
|
|
is_superuser: bool | None = None # Explicitly reject privilege escalation attempts
|
|
|
|
@field_validator("phone_number")
|
|
@classmethod
|
|
def validate_phone(cls, v: str | None) -> str | None:
|
|
return validate_phone_number(v)
|
|
|
|
@field_validator("password")
|
|
@classmethod
|
|
def password_strength(cls, v: str | None) -> str | None:
|
|
"""Enterprise-grade password strength validation"""
|
|
if v is None:
|
|
return v
|
|
return validate_password_strength(v)
|
|
|
|
@field_validator("locale")
|
|
@classmethod
|
|
def validate_locale(cls, v: str | None) -> str | None:
|
|
"""Validate locale against supported locales."""
|
|
if v is None:
|
|
return v
|
|
# Only support English and Italian for template showcase
|
|
# Note: Locales stored in lowercase for case-insensitive matching
|
|
supported_locales = {"en", "it", "en-us", "en-gb", "it-it"}
|
|
# Normalize to lowercase for comparison and storage
|
|
v_lower = v.lower()
|
|
if v_lower not in supported_locales:
|
|
raise ValueError(
|
|
f"Unsupported locale '{v}'. Supported locales: {sorted(supported_locales)}"
|
|
)
|
|
# Return normalized lowercase version for consistency
|
|
return v_lower
|
|
|
|
@field_validator("is_superuser")
|
|
@classmethod
|
|
def prevent_superuser_modification(cls, v: bool | None) -> bool | None:
|
|
"""Prevent users from modifying their superuser status via this schema."""
|
|
if v is not None:
|
|
raise ValueError("Cannot modify superuser status through user update")
|
|
return v
|
|
|
|
|
|
class UserInDB(UserBase):
|
|
id: UUID
|
|
is_active: bool
|
|
is_superuser: bool
|
|
created_at: datetime
|
|
updated_at: datetime | None = None
|
|
locale: str | None = None
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class UserResponse(UserBase):
|
|
id: UUID
|
|
is_active: bool
|
|
is_superuser: bool
|
|
created_at: datetime
|
|
updated_at: datetime | None = None
|
|
locale: str | None = None
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class Token(BaseModel):
|
|
access_token: str
|
|
refresh_token: str | None = None
|
|
token_type: str = "bearer"
|
|
user: "UserResponse" # Forward reference since UserResponse is defined above
|
|
expires_in: int | None = None # Token expiration in seconds
|
|
|
|
|
|
class TokenPayload(BaseModel):
|
|
sub: str # User ID
|
|
exp: int # Expiration time
|
|
iat: int | None = None # Issued at
|
|
jti: str | None = None # JWT ID
|
|
is_superuser: bool | None = False
|
|
first_name: str | None = None
|
|
email: str | None = None
|
|
type: str | None = None # Token type (access/refresh)
|
|
|
|
|
|
class TokenData(BaseModel):
|
|
user_id: UUID
|
|
is_superuser: bool = False
|
|
|
|
|
|
class PasswordChange(BaseModel):
|
|
"""Schema for changing password (requires current password)."""
|
|
|
|
current_password: str
|
|
new_password: str
|
|
|
|
@field_validator("new_password")
|
|
@classmethod
|
|
def password_strength(cls, v: str) -> str:
|
|
"""Enterprise-grade password strength validation"""
|
|
return validate_password_strength(v)
|
|
|
|
|
|
class PasswordReset(BaseModel):
|
|
"""Schema for resetting password (via email token)."""
|
|
|
|
token: str
|
|
new_password: str
|
|
|
|
@field_validator("new_password")
|
|
@classmethod
|
|
def password_strength(cls, v: str) -> str:
|
|
"""Enterprise-grade password strength validation"""
|
|
return validate_password_strength(v)
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
|
|
|
|
class RefreshTokenRequest(BaseModel):
|
|
refresh_token: str
|
|
|
|
|
|
class PasswordResetRequest(BaseModel):
|
|
"""Schema for requesting a password reset."""
|
|
|
|
email: EmailStr = Field(..., description="Email address of the account")
|
|
|
|
model_config = {"json_schema_extra": {"example": {"email": "user@example.com"}}}
|
|
|
|
|
|
class PasswordResetConfirm(BaseModel):
|
|
"""Schema for confirming a password reset with token."""
|
|
|
|
token: str = Field(..., description="Password reset token from email")
|
|
new_password: str = Field(..., min_length=8, description="New password")
|
|
|
|
@field_validator("new_password")
|
|
@classmethod
|
|
def password_strength(cls, v: str) -> str:
|
|
"""Enterprise-grade password strength validation"""
|
|
return validate_password_strength(v)
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"example": {
|
|
"token": "eyJwYXlsb2FkIjp7ImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImV4cCI6MTcxMjM0NTY3OH19",
|
|
"new_password": "NewSecurePassword123",
|
|
}
|
|
}
|
|
}
|