- Created SetupRedirectMiddleware to redirect unconfigured apps to /setup - Enhanced /api/auth/setup endpoint to save anime_directory to config - Updated SetupRequest model to accept optional anime_directory parameter - Modified setup.html to send anime_directory in setup API call - Added @pytest.mark.requires_clean_auth marker for tests needing unconfigured state - Modified conftest.py to conditionally setup auth based on test marker - Fixed all test failures (846/846 tests now passing) - Updated instructions.md to mark setup tasks as complete This implementation ensures users are guided through initial setup before accessing the application, while maintaining test isolation and preventing auth state leakage between tests.
101 lines
3.2 KiB
Python
101 lines
3.2 KiB
Python
"""Authentication Pydantic models for the Aniworld web application.
|
|
|
|
This module defines simple request/response shapes used by the auth API and
|
|
by the authentication service. Keep models small and focused so they are
|
|
easy to validate and test.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
"""Request body for a login attempt.
|
|
|
|
Fields:
|
|
- password: master password string (minimum 8 chars recommended)
|
|
- remember: optional flag to request a long-lived session
|
|
"""
|
|
|
|
password: str = Field(..., min_length=1, description="Master password")
|
|
remember: Optional[bool] = Field(
|
|
False, description="Keep session alive"
|
|
)
|
|
|
|
|
|
class LoginResponse(BaseModel):
|
|
"""Response returned after a successful login."""
|
|
|
|
access_token: str = Field(..., description="JWT access token")
|
|
token_type: str = Field("bearer", description="Token type")
|
|
expires_at: Optional[datetime] = Field(None, description="Optional expiry timestamp")
|
|
|
|
|
|
class SetupRequest(BaseModel):
|
|
"""Request to initialize the master password during first-time setup."""
|
|
|
|
master_password: str = Field(
|
|
..., min_length=8, description="New master password"
|
|
)
|
|
anime_directory: Optional[str] = Field(
|
|
None, description="Optional anime directory path"
|
|
)
|
|
|
|
|
|
class AuthStatus(BaseModel):
|
|
"""Public status about whether auth is configured and the current user state."""
|
|
|
|
configured: bool = Field(..., description="Whether a master password is set")
|
|
authenticated: bool = Field(False, description="Whether the caller is authenticated")
|
|
|
|
|
|
class SessionModel(BaseModel):
|
|
"""Lightweight session representation stored/returned by the auth service.
|
|
|
|
This model can be persisted if a persistent session store is used.
|
|
"""
|
|
|
|
session_id: str = Field(..., description="Unique session identifier")
|
|
user: Optional[str] = Field(None, description="Username or identifier")
|
|
created_at: datetime = Field(
|
|
default_factory=lambda: datetime.now(timezone.utc)
|
|
)
|
|
expires_at: Optional[datetime] = Field(None)
|
|
|
|
|
|
class RegisterRequest(BaseModel):
|
|
"""Request to register a new user (for testing purposes)."""
|
|
|
|
username: str = Field(
|
|
..., min_length=3, max_length=50, description="Username"
|
|
)
|
|
password: str = Field(..., min_length=8, description="Password")
|
|
email: str = Field(..., description="Email address")
|
|
|
|
@field_validator("email")
|
|
@classmethod
|
|
def validate_email(cls, v: str) -> str:
|
|
"""Validate email format."""
|
|
# Basic email validation
|
|
pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
|
|
if not re.match(pattern, v):
|
|
raise ValueError("Invalid email address")
|
|
return v
|
|
|
|
@field_validator("username")
|
|
@classmethod
|
|
def validate_username(cls, v: str) -> str:
|
|
"""Validate username contains no special characters."""
|
|
if not re.match(r"^[a-zA-Z0-9_-]+$", v):
|
|
raise ValueError(
|
|
"Username can only contain letters, numbers, underscore, "
|
|
"and hyphen"
|
|
)
|
|
return v
|
|
|
|
|