Bruno Charest 88742892d0
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
refactorisation pour correction de sécurité
2026-03-03 08:29:52 -05:00

129 lines
4.1 KiB
Python

"""Authentication schemas for login, token, and user management."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field, field_validator
def _validate_password_strength(password: str) -> str:
"""Shared password strength validator.
Requirements:
- Minimum 8 characters
- At least 1 uppercase letter
- At least 1 lowercase letter
- At least 1 digit
- At least 1 special character
"""
if len(password) < 8:
raise ValueError("Le mot de passe doit contenir au moins 8 caractères")
if not any(c.isupper() for c in password):
raise ValueError("Le mot de passe doit contenir au moins une majuscule")
if not any(c.islower() for c in password):
raise ValueError("Le mot de passe doit contenir au moins une minuscule")
if not any(c.isdigit() for c in password):
raise ValueError("Le mot de passe doit contenir au moins un chiffre")
if not any(c in "!@#$%^&*()_+-=[]{}|;:',.<>?/~`" for c in password):
raise ValueError("Le mot de passe doit contenir au moins un caractère spécial")
return password
class LoginRequest(BaseModel):
"""Request schema for user login."""
username: str = Field(..., min_length=3, max_length=50, description="Username")
password: str = Field(..., min_length=1, description="Password")
class Token(BaseModel):
"""JWT token response."""
access_token: str
token_type: str = "bearer"
expires_in: int = Field(description="Token expiration time in seconds")
class TokenData(BaseModel):
"""Data extracted from JWT token."""
username: Optional[str] = None
user_id: Optional[int] = None
role: Optional[str] = None
# User schemas
class UserBase(BaseModel):
"""Base user schema with common fields."""
username: str = Field(..., min_length=3, max_length=50)
email: Optional[EmailStr] = None
display_name: Optional[str] = Field(None, max_length=100)
role: str = Field(default="admin", description="User role: admin, operator, viewer")
is_active: bool = True
class UserCreate(UserBase):
"""Schema for creating a new user."""
password: str = Field(..., min_length=8, max_length=128, description="Password (min 8 chars, requires uppercase, lowercase, digit, special char)")
@field_validator('password')
@classmethod
def password_strength(cls, v: str) -> str:
return _validate_password_strength(v)
class UserUpdate(BaseModel):
"""Schema for updating user (all fields optional)."""
email: Optional[EmailStr] = None
display_name: Optional[str] = Field(None, max_length=100)
role: Optional[str] = None
is_active: Optional[bool] = None
class PasswordChange(BaseModel):
"""Schema for changing password."""
current_password: str = Field(..., description="Current password")
new_password: str = Field(..., min_length=8, max_length=128, description="New password")
@field_validator('new_password')
@classmethod
def password_strength(cls, v: str) -> str:
return _validate_password_strength(v)
class UserOut(BaseModel):
"""Schema for user output (without password)."""
id: int
username: str
email: Optional[str] = None
display_name: Optional[str] = None
role: str
is_active: bool
is_superuser: bool
created_at: datetime
last_login: Optional[datetime] = None
class Config:
from_attributes = True
class UserSetup(BaseModel):
"""Schema for initial admin setup (first user creation)."""
username: str = Field(..., min_length=3, max_length=50)
password: str = Field(..., min_length=8, max_length=128)
email: Optional[EmailStr] = None
display_name: Optional[str] = None
@field_validator('password')
@classmethod
def password_strength(cls, v: str) -> str:
return _validate_password_strength(v)
class AuthStatus(BaseModel):
"""Response for auth status check."""
authenticated: bool
user: Optional[UserOut] = None
setup_required: bool = Field(
default=False,
description="True if no users exist and setup is needed"
)