Initial commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Todd
2026-03-29 22:42:55 -04:00
commit 0d7b2b1aab
389 changed files with 280296 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
"""
Shared Dependencies
Provides dependency injection for FastAPI routes.
All authentication, database access, and common dependencies are defined here.
"""
import sys
from pathlib import Path
from typing import Dict, Optional, List
from datetime import datetime
from fastapi import Depends, HTTPException, Query, Request, WebSocket
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from modules.universal_logger import get_logger
logger = get_logger('API')
# Security
security = HTTPBearer(auto_error=False)
# ============================================================================
# GLOBAL STATE (imported from main app)
# ============================================================================
class AppState:
"""Global application state - singleton pattern"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.db = None
self.config: Dict = {}
self.scheduler = None
self.websocket_manager = None # ConnectionManager instance for broadcasts
self.scraper_event_emitter = None # ScraperEventEmitter for real-time scraping monitor
self.active_scraper_sessions: Dict = {} # Track active scraping sessions for monitor page
self.running_platform_downloads: Dict = {} # Track running platform downloads {platform: process}
self.download_tasks: Dict = {}
self.auth = None
self.settings = None
self.face_recognition_semaphore = None
self.indexing_running: bool = False
self.indexing_start_time = None
self.review_rescan_running: bool = False
self.review_rescan_progress: Dict = {"current": 0, "total": 0, "current_file": ""}
self._initialized = True
def get_app_state() -> AppState:
"""Get the global application state"""
return AppState()
# ============================================================================
# AUTHENTICATION DEPENDENCIES
# ============================================================================
async def get_current_user(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> Dict:
"""
Dependency to get current authenticated user from JWT token.
Supports both Authorization header and cookie-based authentication.
"""
app_state = get_app_state()
auth_token = None
# Try Authorization header first
if credentials:
auth_token = credentials.credentials
# Try cookie second
elif 'auth_token' in request.cookies:
auth_token = request.cookies.get('auth_token')
if not auth_token:
raise HTTPException(status_code=401, detail="Not authenticated")
if not app_state.auth:
raise HTTPException(status_code=500, detail="Authentication not initialized")
payload = app_state.auth.verify_session(auth_token)
if not payload:
raise HTTPException(status_code=401, detail="Invalid or expired token")
return payload
async def get_current_user_optional(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> Optional[Dict]:
"""Optional authentication dependency - returns None if not authenticated"""
try:
return await get_current_user(request, credentials)
except HTTPException:
return None
async def get_current_user_media(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
token: Optional[str] = Query(None)
) -> Dict:
"""
Authentication for media endpoints.
Supports header, cookie, and query parameter tokens (for img/video tags).
Priority: 1) Authorization header, 2) Cookie, 3) Query parameter
"""
app_state = get_app_state()
auth_token = None
# Try to get token from Authorization header first (preferred)
if credentials:
auth_token = credentials.credentials
# Try cookie second (for <img> and <video> tags)
elif 'auth_token' in request.cookies:
auth_token = request.cookies.get('auth_token')
# Fall back to query parameter (for backward compatibility)
elif token:
auth_token = token
if not auth_token:
raise HTTPException(status_code=401, detail="Not authenticated")
if not app_state.auth:
raise HTTPException(status_code=500, detail="Authentication not initialized")
payload = app_state.auth.verify_session(auth_token)
if not payload:
raise HTTPException(status_code=401, detail="Invalid or expired token")
return payload
async def require_admin(
request: Request,
current_user: Dict = Depends(get_current_user)
) -> Dict:
"""Dependency to require admin role for sensitive operations"""
app_state = get_app_state()
username = current_user.get('sub')
if not username:
raise HTTPException(status_code=401, detail="Invalid user session")
# Get user info to check role
user_info = app_state.auth.get_user(username)
if not user_info:
raise HTTPException(status_code=401, detail="User not found")
if user_info.get('role') != 'admin':
logger.warning(f"User {username} attempted admin operation without admin role", module="Auth")
raise HTTPException(status_code=403, detail="Admin role required")
return current_user
# ============================================================================
# DATABASE DEPENDENCIES
# ============================================================================
def get_database():
"""Get the database connection"""
app_state = get_app_state()
if not app_state.db:
raise HTTPException(status_code=500, detail="Database not initialized")
return app_state.db
def get_settings_manager():
"""Get the settings manager"""
app_state = get_app_state()
if not app_state.settings:
raise HTTPException(status_code=500, detail="Settings manager not initialized")
return app_state.settings
# ============================================================================
# UTILITY DEPENDENCIES
# ============================================================================
def get_scheduler():
"""Get the scheduler instance"""
app_state = get_app_state()
return app_state.scheduler
def get_face_semaphore():
"""Get the face recognition semaphore for limiting concurrent operations"""
app_state = get_app_state()
return app_state.face_recognition_semaphore