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:
16
src/openrouter_monitor/schemas/__init__.py
Normal file
16
src/openrouter_monitor/schemas/__init__.py
Normal 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",
|
||||
]
|
||||
173
src/openrouter_monitor/schemas/auth.py
Normal file
173
src/openrouter_monitor/schemas/auth.py
Normal 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"]
|
||||
)
|
||||
Reference in New Issue
Block a user