- Extended SetupRequest model to include all configuration fields - Updated setup API endpoint to handle comprehensive configuration - Created new setup.html with organized configuration sections - Enhanced config modal in index.html with all settings - Updated JavaScript modules to use unified config API - Added backup configuration section - Documented new features in features.md and instructions.md
193 lines
6.3 KiB
Python
193 lines
6.3 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.
|
|
|
|
This request includes all configuration fields needed to set up the application.
|
|
"""
|
|
|
|
# Required fields
|
|
master_password: str = Field(
|
|
..., min_length=8, description="New master password"
|
|
)
|
|
anime_directory: Optional[str] = Field(
|
|
None, description="Optional anime directory path"
|
|
)
|
|
|
|
# Application settings
|
|
name: Optional[str] = Field(
|
|
default="Aniworld", description="Application name"
|
|
)
|
|
data_dir: Optional[str] = Field(
|
|
default="data", description="Data directory path"
|
|
)
|
|
|
|
# Scheduler configuration
|
|
scheduler_enabled: Optional[bool] = Field(
|
|
default=True, description="Enable/disable scheduler"
|
|
)
|
|
scheduler_interval_minutes: Optional[int] = Field(
|
|
default=60, ge=1, description="Scheduler interval in minutes"
|
|
)
|
|
|
|
# Logging configuration
|
|
logging_level: Optional[str] = Field(
|
|
default="INFO", description="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)"
|
|
)
|
|
logging_file: Optional[str] = Field(
|
|
default=None, description="Log file path"
|
|
)
|
|
logging_max_bytes: Optional[int] = Field(
|
|
default=None, ge=0, description="Max log file size in bytes"
|
|
)
|
|
logging_backup_count: Optional[int] = Field(
|
|
default=3, ge=0, description="Number of backup log files"
|
|
)
|
|
|
|
# Backup configuration
|
|
backup_enabled: Optional[bool] = Field(
|
|
default=False, description="Enable/disable backups"
|
|
)
|
|
backup_path: Optional[str] = Field(
|
|
default="data/backups", description="Backup directory path"
|
|
)
|
|
backup_keep_days: Optional[int] = Field(
|
|
default=30, ge=0, description="Days to keep backups"
|
|
)
|
|
|
|
# NFO configuration
|
|
nfo_tmdb_api_key: Optional[str] = Field(
|
|
default=None, description="TMDB API key"
|
|
)
|
|
nfo_auto_create: Optional[bool] = Field(
|
|
default=True, description="Auto-create NFO files"
|
|
)
|
|
nfo_update_on_scan: Optional[bool] = Field(
|
|
default=True, description="Update NFO on scan"
|
|
)
|
|
nfo_download_poster: Optional[bool] = Field(
|
|
default=True, description="Download poster images"
|
|
)
|
|
nfo_download_logo: Optional[bool] = Field(
|
|
default=True, description="Download logo images"
|
|
)
|
|
nfo_download_fanart: Optional[bool] = Field(
|
|
default=True, description="Download fanart images"
|
|
)
|
|
nfo_image_size: Optional[str] = Field(
|
|
default="original", description="Image size preference (original or w500)"
|
|
)
|
|
|
|
@field_validator("logging_level")
|
|
@classmethod
|
|
def validate_logging_level(cls, v: Optional[str]) -> Optional[str]:
|
|
"""Validate logging level."""
|
|
if v is None:
|
|
return v
|
|
allowed = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
|
lvl = v.upper()
|
|
if lvl not in allowed:
|
|
raise ValueError(f"Invalid logging level: {v}. Must be one of {allowed}")
|
|
return lvl
|
|
|
|
@field_validator("nfo_image_size")
|
|
@classmethod
|
|
def validate_image_size(cls, v: Optional[str]) -> Optional[str]:
|
|
"""Validate image size."""
|
|
if v is None:
|
|
return v
|
|
allowed = {"original", "w500"}
|
|
size = v.lower()
|
|
if size not in allowed:
|
|
raise ValueError(f"Invalid image size: {v}. Must be 'original' or 'w500'")
|
|
return size
|
|
|
|
|
|
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
|
|
|
|
|