86 lines
2.0 KiB
Python
86 lines
2.0 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Annotated, Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, StrictStr, StringConstraints, field_validator
|
|
|
|
from app.models.user import RoleEnum
|
|
|
|
UsernameField = Annotated[
|
|
StrictStr,
|
|
StringConstraints(
|
|
strip_whitespace=True,
|
|
min_length=3,
|
|
max_length=50,
|
|
),
|
|
]
|
|
PasswordField = Annotated[
|
|
StrictStr,
|
|
StringConstraints(
|
|
min_length=8,
|
|
max_length=72,
|
|
),
|
|
]
|
|
StrongPasswordField = Annotated[
|
|
StrictStr,
|
|
StringConstraints(
|
|
min_length=12,
|
|
max_length=72,
|
|
),
|
|
]
|
|
|
|
|
|
class UserCreate(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
username: UsernameField
|
|
password: StrongPasswordField
|
|
|
|
@field_validator("username")
|
|
@classmethod
|
|
def normalize_username(cls, value: str) -> str:
|
|
return value.lower()
|
|
|
|
@field_validator("password")
|
|
@classmethod
|
|
def validate_password_strength(cls, value: str) -> str:
|
|
has_upper = any(char.isupper() for char in value)
|
|
has_lower = any(char.islower() for char in value)
|
|
has_digit = any(char.isdigit() for char in value)
|
|
has_special = any(not char.isalnum() for char in value)
|
|
if not all((has_upper, has_lower, has_digit, has_special)):
|
|
raise ValueError(
|
|
"Password must include upper, lower, digit, and special characters"
|
|
)
|
|
return value
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
username: UsernameField
|
|
password: PasswordField
|
|
|
|
@field_validator("username")
|
|
@classmethod
|
|
def normalize_username(cls, value: str) -> str:
|
|
return value.lower()
|
|
|
|
|
|
class UserResponse(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True, extra="forbid")
|
|
|
|
id: int
|
|
username: str
|
|
role: RoleEnum
|
|
is_active: bool
|
|
created_at: datetime
|
|
|
|
|
|
class TokenResponse(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
access_token: str
|
|
token_type: Literal["bearer"] = "bearer"
|