from datetime import datetime from typing import Optional from pydantic import BaseModel, ConfigDict, Field, field_validator from models import BeerStyle, DrinkCategory, SpiritType, WineColor def validate_password_strength(v: str) -> str: if not any(c.isupper() for c in v): raise ValueError("Le mot de passe doit contenir au moins une majuscule") if not any(c.islower() for c in v): raise ValueError("Le mot de passe doit contenir au moins une minuscule") if not any(c.isdigit() for c in v): raise ValueError("Le mot de passe doit contenir au moins un chiffre") return v # ── Auth ── class UserCreate(BaseModel): username: str = Field(..., min_length=3, max_length=50) email: str = Field(..., max_length=255) password: str = Field(..., min_length=8, max_length=128) invite_token: str = Field(..., min_length=1, max_length=128) @field_validator("password") @classmethod def validate_password(cls, v: str) -> str: return validate_password_strength(v) @field_validator("email") @classmethod def validate_email(cls, v: str) -> str: if "@" not in v or "." not in v.split("@")[-1]: raise ValueError("Adresse email invalide") return v.lower().strip() class UserLogin(BaseModel): username: str = Field(..., min_length=1, max_length=50) password: str = Field(..., min_length=1, max_length=128) class UserResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: int username: str email: str is_admin: bool created_at: datetime class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" user: UserResponse class PasswordChange(BaseModel): current_password: str = Field(..., min_length=1, max_length=128) new_password: str = Field(..., min_length=8, max_length=128) @field_validator("new_password") @classmethod def validate_password(cls, v: str) -> str: return validate_password_strength(v) class InvitationCreate(BaseModel): pass # ── Admin ── class AdminResetPassword(BaseModel): new_password: str = Field(..., min_length=8, max_length=128) @field_validator("new_password") @classmethod def validate_password(cls, v: str) -> str: return validate_password_strength(v) class AdminToggleAdmin(BaseModel): is_admin: bool class InvitationResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: int token: str created_by: int used_by: Optional[int] = None created_at: datetime used_at: Optional[datetime] = None expires_at: datetime # ── Drinks ── class DrinkBase(BaseModel): name: str = Field(..., min_length=1, max_length=255) category: DrinkCategory rating: Optional[float] = Field(None, ge=0, le=5) notes: Optional[str] = Field(None, max_length=5000) grape_variety: Optional[str] = Field(None, max_length=255) vintage: Optional[int] = Field(None, ge=1900, le=2100) region: Optional[str] = Field(None, max_length=255) producer: Optional[str] = Field(None, max_length=255) wine_color: Optional[WineColor] = None brewery: Optional[str] = Field(None, max_length=255) beer_style: Optional[BeerStyle] = None ibu: Optional[float] = Field(None, ge=0, le=200) abv: Optional[float] = Field(None, ge=0, le=30) spirit_type: Optional[SpiritType] = None age_years: Optional[int] = Field(None, ge=0, le=200) distillery: Optional[str] = Field(None, max_length=255) country: Optional[str] = Field(None, max_length=255) class DrinkCreate(DrinkBase): pass class DrinkUpdate(BaseModel): name: Optional[str] = Field(None, min_length=1, max_length=255) category: Optional[DrinkCategory] = None rating: Optional[float] = Field(None, ge=0, le=5) notes: Optional[str] = Field(None, max_length=5000) grape_variety: Optional[str] = Field(None, max_length=255) vintage: Optional[int] = Field(None, ge=1900, le=2100) region: Optional[str] = Field(None, max_length=255) producer: Optional[str] = Field(None, max_length=255) wine_color: Optional[WineColor] = None brewery: Optional[str] = Field(None, max_length=255) beer_style: Optional[BeerStyle] = None ibu: Optional[float] = Field(None, ge=0, le=200) abv: Optional[float] = Field(None, ge=0, le=30) spirit_type: Optional[SpiritType] = None age_years: Optional[int] = Field(None, ge=0, le=200) distillery: Optional[str] = Field(None, max_length=255) country: Optional[str] = Field(None, max_length=255) class DrinkResponse(DrinkBase): model_config = ConfigDict(from_attributes=True) id: int owner_id: int image_path: Optional[str] = None created_at: datetime updated_at: datetime