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:
141
src/server/middleware/setup_redirect.py
Normal file
141
src/server/middleware/setup_redirect.py
Normal 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)
|
||||
Reference in New Issue
Block a user