feat(schemas): T17 add Pydantic auth schemas

Add authentication schemas for user registration and login:
- UserRegister: email, password (with strength validation), password_confirm
- UserLogin: email, password
- UserResponse: id, email, created_at, is_active (orm_mode=True)
- TokenResponse: access_token, token_type, expires_in
- TokenData: user_id, exp

Includes field validators for password strength and password confirmation matching.

Test coverage: 19 tests for all schemas
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 13:52:33 +02:00
parent a698d09a77
commit 02473bc39e
7 changed files with 1341 additions and 5 deletions

View File

@@ -0,0 +1,16 @@
"""Schemas package for OpenRouter Monitor."""
from openrouter_monitor.schemas.auth import (
TokenData,
TokenResponse,
UserLogin,
UserRegister,
UserResponse,
)
__all__ = [
"UserRegister",
"UserLogin",
"UserResponse",
"TokenResponse",
"TokenData",
]

View File

@@ -0,0 +1,173 @@
"""Authentication Pydantic schemas.
T17: Pydantic schemas for user registration, login, and token management.
"""
from datetime import datetime
from typing import Optional, Union
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator
from openrouter_monitor.services.password import validate_password_strength
class UserRegister(BaseModel):
"""Schema for user registration.
Attributes:
email: User email address (must be valid email format)
password: User password (min 12 chars, must pass strength validation)
password_confirm: Password confirmation (must match password)
"""
email: EmailStr = Field(
..., # Required field
description="User email address",
examples=["user@example.com"]
)
password: str = Field(
...,
min_length=12,
description="User password (min 12 characters)",
examples=["SecurePass123!"]
)
password_confirm: str = Field(
...,
description="Password confirmation",
examples=["SecurePass123!"]
)
@field_validator('password')
@classmethod
def validate_password(cls, v: str) -> str:
"""Validate password strength.
Args:
v: The password value to validate
Returns:
The password if valid
Raises:
ValueError: If password doesn't meet strength requirements
"""
if not validate_password_strength(v):
raise ValueError(
"Password must be at least 12 characters long and contain "
"at least one uppercase letter, one lowercase letter, "
"one digit, and one special character"
)
return v
@model_validator(mode='after')
def check_passwords_match(self) -> 'UserRegister':
"""Verify that password and password_confirm match.
Returns:
The validated model instance
Raises:
ValueError: If passwords don't match
"""
if self.password != self.password_confirm:
raise ValueError("Passwords do not match")
return self
class UserLogin(BaseModel):
"""Schema for user login.
Attributes:
email: User email address
password: User password
"""
email: EmailStr = Field(
...,
description="User email address",
examples=["user@example.com"]
)
password: str = Field(
...,
description="User password",
examples=["your-password"]
)
class UserResponse(BaseModel):
"""Schema for user response (returned to client).
Attributes:
id: User ID
email: User email address
created_at: User creation timestamp
is_active: Whether the user account is active
"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(
...,
description="User ID",
examples=[1]
)
email: EmailStr = Field(
...,
description="User email address",
examples=["user@example.com"]
)
created_at: datetime = Field(
...,
description="User creation timestamp",
examples=["2024-01-01T12:00:00"]
)
is_active: bool = Field(
...,
description="Whether the user account is active",
examples=[True]
)
class TokenResponse(BaseModel):
"""Schema for token response (returned after login).
Attributes:
access_token: The JWT access token
token_type: Token type (always 'bearer')
expires_in: Token expiration time in seconds
"""
access_token: str = Field(
...,
description="JWT access token",
examples=["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."]
)
token_type: str = Field(
default="bearer",
description="Token type",
examples=["bearer"]
)
expires_in: int = Field(
...,
description="Token expiration time in seconds",
examples=[86400]
)
class TokenData(BaseModel):
"""Schema for token payload data.
Attributes:
user_id: User ID (from 'sub' claim in JWT)
exp: Token expiration timestamp
"""
user_id: Union[str, int] = Field(
...,
description="User ID (from JWT 'sub' claim)",
examples=["123"]
)
exp: datetime = Field(
...,
description="Token expiration timestamp",
examples=["2024-01-02T12:00:00"]
)