feat: implement setup redirect middleware and fix test suite

- 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.
This commit is contained in:
2025-10-24 19:55:26 +02:00
parent 260b98e548
commit 731fd56768
13 changed files with 438 additions and 66 deletions

View File

@@ -0,0 +1,141 @@
"""Setup redirect middleware for Aniworld.
This middleware ensures that users are redirected to the setup page
if the application is not properly configured. It checks if both the
master password and basic configuration exist before allowing access
to other parts of the application.
"""
from __future__ import annotations
from typing import Callable
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import RedirectResponse
from starlette.types import ASGIApp
from src.server.services.auth_service import auth_service
from src.server.services.config_service import get_config_service
class SetupRedirectMiddleware(BaseHTTPMiddleware):
"""Middleware that redirects to /setup if configuration is incomplete.
The middleware checks:
1. If master password is configured (via auth_service.is_configured())
2. If configuration file exists and is valid
If either check fails, users are redirected to /setup page,
except for whitelisted paths that must remain accessible.
"""
# Paths that should always be accessible, even without setup
EXEMPT_PATHS = {
"/setup", # Setup page itself
"/login", # Login page (needs to be accessible after setup)
"/queue", # Queue page (for initial load)
"/api/auth/", # All auth endpoints (setup, login, logout, register)
"/api/queue/", # Queue API endpoints
"/api/downloads/", # Download API endpoints
"/api/config/", # Config API (needed for setup and management)
"/api/anime/", # Anime API endpoints
"/api/health", # Health check
"/health", # Health check (alternate path)
"/api/docs", # API documentation
"/api/redoc", # ReDoc documentation
"/openapi.json", # OpenAPI schema
"/static/", # Static files (CSS, JS, images)
}
def __init__(self, app: ASGIApp) -> None:
"""Initialize the setup redirect middleware.
Args:
app: The ASGI application
"""
super().__init__(app)
def _is_path_exempt(self, path: str) -> bool:
"""Check if a path is exempt from setup redirect.
Args:
path: The request path to check
Returns:
True if the path should be accessible without setup
"""
# Exact matches
if path in self.EXEMPT_PATHS:
return True
# Prefix matches (e.g., /static/, /api/config)
for exempt_path in self.EXEMPT_PATHS:
if exempt_path.endswith("/") and path.startswith(exempt_path):
return True
return False
def _needs_setup(self) -> bool:
"""Check if the application needs initial setup.
Returns:
True if setup is required, False otherwise
"""
# Check if master password is configured
if not auth_service.is_configured():
return True
# Check if config exists and is valid
try:
config_service = get_config_service()
config = config_service.load_config()
# Validate the loaded config
validation = config.validate()
if not validation.valid:
return True
except Exception:
# If we can't load or validate config, setup is needed
return True
return False
async def dispatch(
self, request: Request, call_next: Callable
) -> RedirectResponse:
"""Process the request and redirect to setup if needed.
Args:
request: The incoming request
call_next: The next middleware or route handler
Returns:
Either a redirect to /setup or the normal response
"""
path = request.url.path
# Skip setup check for exempt paths
if self._is_path_exempt(path):
return await call_next(request)
# Check if setup is needed
if self._needs_setup():
# Redirect to setup page for HTML requests
# Return 503 for API requests
accept_header = request.headers.get("accept", "")
if "text/html" in accept_header or path == "/":
return RedirectResponse(url="/setup", status_code=302)
else:
# For API requests, return JSON error
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=503,
content={
"detail": "Application setup required",
"setup_url": "/setup"
}
)
# Setup is complete, continue normally
return await call_next(request)