Files
Aniworld/src/server/models/auth.py
Lukas 4e29c4ed80 Enhanced setup and settings pages with full configuration
- 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
2026-01-17 18:01:15 +01:00

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