Implement application setup and flow middleware
- Add SetupService for detecting application setup completion - Create ApplicationFlowMiddleware to enforce setup auth main flow - Add setup processing endpoints (/api/auth/setup, /api/auth/setup/status) - Add Pydantic models for setup requests and responses - Integrate middleware into FastAPI application - Fix logging paths to use ./logs consistently - All existing templates (setup.html, login.html) already working
This commit is contained in:
@@ -34,6 +34,10 @@ from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
# Import application flow services
|
||||
from src.server.middleware.application_flow_middleware import ApplicationFlowMiddleware
|
||||
from src.server.services.setup_service import SetupService
|
||||
|
||||
# Import our custom middleware - temporarily disabled due to file corruption
|
||||
# from src.server.web.middleware.fastapi_auth_middleware import AuthMiddleware
|
||||
# from src.server.web.middleware.fastapi_logging_middleware import (
|
||||
@@ -46,7 +50,7 @@ logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('./logs/aniworld.log'),
|
||||
logging.FileHandler('./logs/aniworld.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
@@ -138,6 +142,23 @@ class ErrorResponse(BaseModel):
|
||||
code: Optional[str] = None
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
|
||||
class SetupRequest(BaseModel):
|
||||
"""Setup request model."""
|
||||
password: str = Field(..., min_length=8, description="Master password (min 8 characters)")
|
||||
directory: str = Field(..., min_length=1, description="Anime directory path")
|
||||
|
||||
class SetupResponse(BaseModel):
|
||||
"""Setup response model."""
|
||||
status: str
|
||||
message: str
|
||||
redirect_url: Optional[str] = None
|
||||
|
||||
class SetupStatusResponse(BaseModel):
|
||||
"""Setup status response model."""
|
||||
setup_complete: bool
|
||||
requirements: Dict[str, bool]
|
||||
missing_requirements: List[str]
|
||||
|
||||
# Authentication utilities
|
||||
def hash_password(password: str) -> str:
|
||||
"""Hash password with salt using SHA-256."""
|
||||
@@ -311,6 +332,10 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Add application flow middleware
|
||||
setup_service = SetupService()
|
||||
app.add_middleware(ApplicationFlowMiddleware, setup_service=setup_service)
|
||||
|
||||
# Add custom middleware - temporarily disabled
|
||||
# app.add_middleware(EnhancedLoggingMiddleware)
|
||||
# app.add_middleware(AuthMiddleware)
|
||||
@@ -360,6 +385,144 @@ async def legacy_download(
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"Failed to start download: {str(e)}"}
|
||||
|
||||
# Setup endpoints
|
||||
@app.get("/api/auth/setup/status", response_model=SetupStatusResponse, tags=["Setup"])
|
||||
async def get_setup_status() -> SetupStatusResponse:
|
||||
"""
|
||||
Check the current setup status of the application.
|
||||
|
||||
Returns information about what setup requirements are met and which are missing.
|
||||
"""
|
||||
try:
|
||||
setup_service = SetupService()
|
||||
requirements = setup_service.get_setup_requirements()
|
||||
missing = setup_service.get_missing_requirements()
|
||||
|
||||
return SetupStatusResponse(
|
||||
setup_complete=setup_service.is_setup_complete(),
|
||||
requirements=requirements,
|
||||
missing_requirements=missing
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking setup status: {e}")
|
||||
return SetupStatusResponse(
|
||||
setup_complete=False,
|
||||
requirements={},
|
||||
missing_requirements=["Error checking setup status"]
|
||||
)
|
||||
|
||||
@app.post("/api/auth/setup", response_model=SetupResponse, tags=["Setup"])
|
||||
async def process_setup(request_data: SetupRequest) -> SetupResponse:
|
||||
"""
|
||||
Process the initial application setup.
|
||||
|
||||
- **password**: Master password (minimum 8 characters)
|
||||
- **directory**: Anime directory path
|
||||
"""
|
||||
try:
|
||||
setup_service = SetupService()
|
||||
|
||||
# Check if setup is already complete
|
||||
if setup_service.is_setup_complete():
|
||||
return SetupResponse(
|
||||
status="error",
|
||||
message="Setup has already been completed"
|
||||
)
|
||||
|
||||
# Validate directory path
|
||||
from pathlib import Path
|
||||
directory_path = Path(request_data.directory)
|
||||
if not directory_path.is_absolute():
|
||||
return SetupResponse(
|
||||
status="error",
|
||||
message="Please provide an absolute directory path"
|
||||
)
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
try:
|
||||
directory_path.mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create directory: {e}")
|
||||
return SetupResponse(
|
||||
status="error",
|
||||
message=f"Failed to create directory: {str(e)}"
|
||||
)
|
||||
|
||||
# Hash the password
|
||||
password_hash = hash_password(request_data.password)
|
||||
|
||||
# Prepare configuration updates
|
||||
config_updates = {
|
||||
"security": {
|
||||
"master_password_hash": password_hash,
|
||||
"salt": settings.password_salt,
|
||||
"session_timeout_hours": settings.token_expiry_hours,
|
||||
"max_failed_attempts": 5,
|
||||
"lockout_duration_minutes": 30
|
||||
},
|
||||
"anime": {
|
||||
"directory": str(directory_path),
|
||||
"download_threads": 3,
|
||||
"download_speed_limit": None,
|
||||
"auto_rescan_time": "03:00",
|
||||
"auto_download_after_rescan": False
|
||||
},
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"enable_console_logging": True,
|
||||
"enable_console_progress": False,
|
||||
"enable_fail2ban_logging": True,
|
||||
"log_file": "aniworld.log",
|
||||
"max_log_size_mb": 10,
|
||||
"log_backup_count": 5
|
||||
},
|
||||
"providers": {
|
||||
"default_provider": "aniworld.to",
|
||||
"preferred_language": "German Dub",
|
||||
"fallback_providers": ["aniworld.to"],
|
||||
"provider_timeout": 30,
|
||||
"retry_attempts": 3,
|
||||
"provider_settings": {
|
||||
"aniworld.to": {
|
||||
"enabled": True,
|
||||
"priority": 1,
|
||||
"quality_preference": "720p"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"max_concurrent_downloads": 3,
|
||||
"download_buffer_size": 8192,
|
||||
"connection_timeout": 30,
|
||||
"read_timeout": 300,
|
||||
"enable_debug_mode": False,
|
||||
"cache_duration_minutes": 60
|
||||
}
|
||||
}
|
||||
|
||||
# Mark setup as complete and save configuration
|
||||
success = setup_service.mark_setup_complete(config_updates)
|
||||
|
||||
if success:
|
||||
logger.info("Application setup completed successfully")
|
||||
return SetupResponse(
|
||||
status="success",
|
||||
message="Setup completed successfully",
|
||||
redirect_url="/login"
|
||||
)
|
||||
else:
|
||||
return SetupResponse(
|
||||
status="error",
|
||||
message="Failed to save configuration"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Setup processing error: {e}")
|
||||
return SetupResponse(
|
||||
status="error",
|
||||
message="Setup failed due to internal error"
|
||||
)
|
||||
|
||||
# Authentication endpoints
|
||||
@app.post("/auth/login", response_model=LoginResponse, tags=["Authentication"])
|
||||
async def login(request_data: LoginRequest, request: Request) -> LoginResponse:
|
||||
|
||||
248
src/server/middleware/application_flow_middleware.py
Normal file
248
src/server/middleware/application_flow_middleware.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
Application Flow Middleware for FastAPI.
|
||||
|
||||
This middleware enforces the application flow priorities:
|
||||
1. Setup page (if setup is not complete)
|
||||
2. Authentication page (if user is not authenticated)
|
||||
3. Main application (for authenticated users with completed setup)
|
||||
|
||||
The middleware redirects users to the appropriate page based on their current state
|
||||
and the state of the application setup.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
# Import the setup service
|
||||
try:
|
||||
from ..services.setup_service import SetupService
|
||||
except ImportError:
|
||||
# Handle case where service is not available
|
||||
class SetupService:
|
||||
def is_setup_complete(self):
|
||||
return True
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApplicationFlowMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware to enforce application flow: setup → auth → main application.
|
||||
|
||||
This middleware:
|
||||
1. Checks if setup is complete
|
||||
2. Validates authentication status
|
||||
3. Redirects to appropriate page based on state
|
||||
4. Allows API endpoints and static files to pass through
|
||||
"""
|
||||
|
||||
def __init__(self, app, setup_service: Optional[SetupService] = None):
|
||||
"""
|
||||
Initialize the application flow middleware.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
setup_service: Setup service instance (optional, will create if not provided)
|
||||
"""
|
||||
super().__init__(app)
|
||||
self.setup_service = setup_service or SetupService()
|
||||
|
||||
# Define paths that should bypass flow enforcement
|
||||
self.bypass_paths = {
|
||||
"/static", # Static files
|
||||
"/favicon.ico", # Browser favicon requests
|
||||
"/robots.txt", # Robots.txt
|
||||
"/health", # Health check endpoints
|
||||
"/docs", # OpenAPI documentation
|
||||
"/redoc", # ReDoc documentation
|
||||
"/openapi.json" # OpenAPI spec
|
||||
}
|
||||
|
||||
# API paths that should bypass flow but may require auth
|
||||
self.api_paths = {
|
||||
"/api",
|
||||
"/auth"
|
||||
}
|
||||
|
||||
# Pages that are part of the flow and should be accessible
|
||||
self.flow_pages = {
|
||||
"/setup",
|
||||
"/login",
|
||||
"/app"
|
||||
}
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
"""
|
||||
Process the request and enforce application flow.
|
||||
|
||||
Args:
|
||||
request: Incoming HTTP request
|
||||
call_next: Next middleware/handler in chain
|
||||
|
||||
Returns:
|
||||
Response: Either a redirect response or the result of call_next
|
||||
"""
|
||||
try:
|
||||
# Get the request path
|
||||
path = request.url.path
|
||||
|
||||
# Skip flow enforcement for certain paths
|
||||
if self._should_bypass_flow(path):
|
||||
return await call_next(request)
|
||||
|
||||
# Check application setup status
|
||||
setup_complete = self.setup_service.is_setup_complete()
|
||||
|
||||
# Check authentication status
|
||||
is_authenticated = await self._is_user_authenticated(request)
|
||||
|
||||
# Determine the appropriate action
|
||||
redirect_response = self._determine_redirect(path, setup_complete, is_authenticated)
|
||||
|
||||
if redirect_response:
|
||||
logger.info(f"Redirecting {path} to {redirect_response.headers.get('location')}")
|
||||
return redirect_response
|
||||
|
||||
# Continue with the request
|
||||
return await call_next(request)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in ApplicationFlowMiddleware: {e}", exc_info=True)
|
||||
# In case of error, allow the request to continue
|
||||
return await call_next(request)
|
||||
|
||||
def _should_bypass_flow(self, path: str) -> bool:
|
||||
"""
|
||||
Check if the given path should bypass flow enforcement.
|
||||
|
||||
Args:
|
||||
path: Request path
|
||||
|
||||
Returns:
|
||||
bool: True if path should bypass flow enforcement
|
||||
"""
|
||||
# Check exact bypass paths
|
||||
for bypass_path in self.bypass_paths:
|
||||
if path.startswith(bypass_path):
|
||||
return True
|
||||
|
||||
# API paths bypass flow enforcement (but may have their own auth)
|
||||
for api_path in self.api_paths:
|
||||
if path.startswith(api_path):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def _is_user_authenticated(self, request: Request) -> bool:
|
||||
"""
|
||||
Check if the user is authenticated by validating JWT token.
|
||||
|
||||
Args:
|
||||
request: HTTP request object
|
||||
|
||||
Returns:
|
||||
bool: True if user is authenticated, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Check for Authorization header
|
||||
auth_header = request.headers.get("authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
return False
|
||||
|
||||
# Extract and validate token
|
||||
token = auth_header.split(" ")[1]
|
||||
|
||||
# Import JWT validation function (avoid circular imports)
|
||||
try:
|
||||
from ..fastapi_app import verify_jwt_token
|
||||
payload = verify_jwt_token(token)
|
||||
return payload is not None
|
||||
except ImportError:
|
||||
# Fallback if import fails
|
||||
logger.warning("Could not import JWT verification function")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking authentication: {e}")
|
||||
return False
|
||||
|
||||
def _determine_redirect(self, path: str, setup_complete: bool, is_authenticated: bool) -> Optional[RedirectResponse]:
|
||||
"""
|
||||
Determine if a redirect is needed based on current state.
|
||||
|
||||
Args:
|
||||
path: Current request path
|
||||
setup_complete: Whether application setup is complete
|
||||
is_authenticated: Whether user is authenticated
|
||||
|
||||
Returns:
|
||||
Optional[RedirectResponse]: Redirect response if needed, None otherwise
|
||||
"""
|
||||
# If setup is not complete
|
||||
if not setup_complete:
|
||||
# Allow access to setup page
|
||||
if path == "/setup":
|
||||
return None
|
||||
# Redirect everything else to setup
|
||||
return RedirectResponse(url="/setup", status_code=302)
|
||||
|
||||
# Setup is complete, check authentication
|
||||
if not is_authenticated:
|
||||
# Allow access to login page
|
||||
if path == "/login":
|
||||
return None
|
||||
# Redirect unauthenticated users to login (except for specific pages)
|
||||
if path in self.flow_pages or path == "/":
|
||||
return RedirectResponse(url="/login", status_code=302)
|
||||
|
||||
# User is authenticated and setup is complete
|
||||
else:
|
||||
# Redirect from setup/login pages to main app
|
||||
if path in ["/setup", "/login", "/"]:
|
||||
return RedirectResponse(url="/app", status_code=302)
|
||||
|
||||
# No redirect needed
|
||||
return None
|
||||
|
||||
def get_flow_status(self, request: Request) -> dict:
|
||||
"""
|
||||
Get current flow status for debugging/monitoring.
|
||||
|
||||
Args:
|
||||
request: HTTP request object
|
||||
|
||||
Returns:
|
||||
dict: Current flow status information
|
||||
"""
|
||||
try:
|
||||
setup_complete = self.setup_service.is_setup_complete()
|
||||
is_authenticated = self._is_user_authenticated(request)
|
||||
|
||||
return {
|
||||
"setup_complete": setup_complete,
|
||||
"authenticated": is_authenticated,
|
||||
"path": request.url.path,
|
||||
"should_bypass": self._should_bypass_flow(request.url.path)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": str(e),
|
||||
"path": request.url.path
|
||||
}
|
||||
|
||||
|
||||
def create_application_flow_middleware(setup_service: Optional[SetupService] = None) -> ApplicationFlowMiddleware:
|
||||
"""
|
||||
Factory function to create application flow middleware.
|
||||
|
||||
Args:
|
||||
setup_service: Setup service instance (optional)
|
||||
|
||||
Returns:
|
||||
ApplicationFlowMiddleware: Configured middleware instance
|
||||
"""
|
||||
return ApplicationFlowMiddleware(app=None, setup_service=setup_service)
|
||||
268
src/server/services/setup_service.py
Normal file
268
src/server/services/setup_service.py
Normal file
@@ -0,0 +1,268 @@
|
||||
"""
|
||||
Setup service for detecting and managing application setup state.
|
||||
|
||||
This service determines if the application is properly configured and set up,
|
||||
following the application flow pattern: setup → auth → main application.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SetupService:
|
||||
"""Service for managing application setup detection and configuration."""
|
||||
|
||||
def __init__(self, config_path: str = "data/config.json", db_path: str = "data/aniworld.db"):
|
||||
"""Initialize the setup service with configuration and database paths."""
|
||||
self.config_path = Path(config_path)
|
||||
self.db_path = Path(db_path)
|
||||
self._config_cache: Optional[Dict[str, Any]] = None
|
||||
|
||||
def is_setup_complete(self) -> bool:
|
||||
"""
|
||||
Check if the application setup is complete.
|
||||
|
||||
Setup is considered complete if:
|
||||
1. Configuration file exists and is valid
|
||||
2. Database exists and is accessible
|
||||
3. Master password is configured
|
||||
4. Setup completion flag is set (if present)
|
||||
|
||||
Returns:
|
||||
bool: True if setup is complete, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Check if configuration file exists and is valid
|
||||
if not self._is_config_valid():
|
||||
logger.info("Setup incomplete: Configuration file is missing or invalid")
|
||||
return False
|
||||
|
||||
# Check if database exists and is accessible
|
||||
if not self._is_database_accessible():
|
||||
logger.info("Setup incomplete: Database is not accessible")
|
||||
return False
|
||||
|
||||
# Check if master password is configured
|
||||
if not self._is_master_password_configured():
|
||||
logger.info("Setup incomplete: Master password is not configured")
|
||||
return False
|
||||
|
||||
# Check for explicit setup completion flag
|
||||
config = self.get_config()
|
||||
if config and config.get("setup", {}).get("completed") is False:
|
||||
logger.info("Setup incomplete: Setup completion flag is False")
|
||||
return False
|
||||
|
||||
logger.debug("Setup validation complete: All checks passed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking setup completion: {e}")
|
||||
return False
|
||||
|
||||
def _is_config_valid(self) -> bool:
|
||||
"""Check if the configuration file exists and contains valid JSON."""
|
||||
try:
|
||||
if not self.config_path.exists():
|
||||
return False
|
||||
|
||||
config = self.get_config()
|
||||
return config is not None and isinstance(config, dict)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Configuration validation error: {e}")
|
||||
return False
|
||||
|
||||
def _is_database_accessible(self) -> bool:
|
||||
"""Check if the database exists and is accessible."""
|
||||
try:
|
||||
if not self.db_path.exists():
|
||||
return False
|
||||
|
||||
# Try to connect and perform a simple query
|
||||
with sqlite3.connect(str(self.db_path)) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database accessibility check failed: {e}")
|
||||
return False
|
||||
|
||||
def _is_master_password_configured(self) -> bool:
|
||||
"""Check if master password is properly configured."""
|
||||
try:
|
||||
config = self.get_config()
|
||||
if not config:
|
||||
return False
|
||||
|
||||
security_config = config.get("security", {})
|
||||
|
||||
# Check if password hash exists
|
||||
password_hash = security_config.get("master_password_hash")
|
||||
salt = security_config.get("salt")
|
||||
|
||||
return bool(password_hash and salt and len(password_hash) > 0 and len(salt) > 0)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Master password configuration check failed: {e}")
|
||||
return False
|
||||
|
||||
def get_config(self, force_reload: bool = False) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get the configuration data from the config file.
|
||||
|
||||
Args:
|
||||
force_reload: If True, reload config from file even if cached
|
||||
|
||||
Returns:
|
||||
dict: Configuration data or None if not accessible
|
||||
"""
|
||||
try:
|
||||
if self._config_cache is None or force_reload:
|
||||
if not self.config_path.exists():
|
||||
return None
|
||||
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
self._config_cache = json.load(f)
|
||||
|
||||
return self._config_cache
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading configuration: {e}")
|
||||
return None
|
||||
|
||||
def mark_setup_complete(self, config_updates: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""
|
||||
Mark the setup as completed and optionally update configuration.
|
||||
|
||||
Args:
|
||||
config_updates: Additional configuration updates to apply
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
config = self.get_config() or {}
|
||||
|
||||
# Update configuration with any provided updates
|
||||
if config_updates:
|
||||
config.update(config_updates)
|
||||
|
||||
# Set setup completion flag
|
||||
if "setup" not in config:
|
||||
config["setup"] = {}
|
||||
config["setup"]["completed"] = True
|
||||
config["setup"]["completed_at"] = str(datetime.utcnow())
|
||||
|
||||
# Save updated configuration
|
||||
return self._save_config(config)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking setup as complete: {e}")
|
||||
return False
|
||||
|
||||
def reset_setup(self) -> bool:
|
||||
"""
|
||||
Reset the setup completion status (for development/testing).
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
config = self.get_config()
|
||||
if not config:
|
||||
return False
|
||||
|
||||
# Remove or set setup completion flag to false
|
||||
if "setup" in config:
|
||||
config["setup"]["completed"] = False
|
||||
|
||||
return self._save_config(config)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting setup: {e}")
|
||||
return False
|
||||
|
||||
def _save_config(self, config: Dict[str, Any]) -> bool:
|
||||
"""Save configuration to file."""
|
||||
try:
|
||||
# Ensure directory exists
|
||||
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save configuration
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=4, ensure_ascii=False)
|
||||
|
||||
# Clear cache to force reload on next access
|
||||
self._config_cache = None
|
||||
|
||||
logger.info(f"Configuration saved to {self.config_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving configuration: {e}")
|
||||
return False
|
||||
|
||||
def get_setup_requirements(self) -> Dict[str, bool]:
|
||||
"""
|
||||
Get detailed breakdown of setup requirements and their status.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with requirement names and their completion status
|
||||
"""
|
||||
config = self.get_config()
|
||||
return {
|
||||
"config_file_exists": self.config_path.exists(),
|
||||
"config_file_valid": self._is_config_valid(),
|
||||
"database_exists": self.db_path.exists(),
|
||||
"database_accessible": self._is_database_accessible(),
|
||||
"master_password_configured": self._is_master_password_configured(),
|
||||
"setup_marked_complete": bool(config and config.get("setup", {}).get("completed", True))
|
||||
}
|
||||
|
||||
def get_missing_requirements(self) -> List[str]:
|
||||
"""
|
||||
Get list of missing setup requirements.
|
||||
|
||||
Returns:
|
||||
list: List of missing requirement descriptions
|
||||
"""
|
||||
requirements = self.get_setup_requirements()
|
||||
missing = []
|
||||
|
||||
if not requirements["config_file_exists"]:
|
||||
missing.append("Configuration file is missing")
|
||||
elif not requirements["config_file_valid"]:
|
||||
missing.append("Configuration file is invalid or corrupted")
|
||||
|
||||
if not requirements["database_exists"]:
|
||||
missing.append("Database file is missing")
|
||||
elif not requirements["database_accessible"]:
|
||||
missing.append("Database is not accessible or corrupted")
|
||||
|
||||
if not requirements["master_password_configured"]:
|
||||
missing.append("Master password is not configured")
|
||||
|
||||
if not requirements["setup_marked_complete"]:
|
||||
missing.append("Setup process was not completed")
|
||||
|
||||
return missing
|
||||
|
||||
|
||||
# Convenience functions for easy import
|
||||
def is_setup_complete() -> bool:
|
||||
"""Convenience function to check if setup is complete."""
|
||||
service = SetupService()
|
||||
return service.is_setup_complete()
|
||||
|
||||
|
||||
def get_setup_service() -> SetupService:
|
||||
"""Get a configured setup service instance."""
|
||||
return SetupService()
|
||||
Reference in New Issue
Block a user