Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 576d9f7a7b | |||
| af93daeddc | |||
| a05795bb35 | |||
| d22df947e4 | |||
| 8bb8c6aa64 | |||
| 109d3c8ac9 | |||
| 6a934db8ac | |||
| ac7302b1dd | |||
| ac5ee3bb27 | |||
| a9084202e3 |
@@ -1 +1 @@
|
|||||||
v1.4.5
|
v1.4.10
|
||||||
|
|||||||
174
docs/NAVIGATION.md
Normal file
174
docs/NAVIGATION.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Navigation & Redirect Logic
|
||||||
|
|
||||||
|
This document describes the setup flow navigation, covering how users progress from initial setup through to the main application.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The application uses a middleware-based redirect system to ensure users complete setup before accessing the main app. The flow involves multiple pages handling setup completion, unresolved folder detection, and initialization.
|
||||||
|
|
||||||
|
## Setup Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ SETUP FLOW │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ /setup ──► /loading ──┬──► /setup/unresolved ──► /loading │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ ▼ │
|
||||||
|
│ (first time) (WebSocket) (has folders) (all resolved) │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ │ │
|
||||||
|
│ /login ◄───────────────────┴──────────────────────┤
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Middleware: SetupRedirectMiddleware
|
||||||
|
|
||||||
|
**File:** `src/server/middleware/setup_redirect.py`
|
||||||
|
|
||||||
|
The middleware intercepts all requests and redirects to `/setup` if:
|
||||||
|
- No master password is configured
|
||||||
|
- Configuration file is missing or invalid
|
||||||
|
|
||||||
|
### Exempt Paths (always accessible)
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `/setup` | Initial setup page |
|
||||||
|
| `/setup/unresolved` | Unresolved folder resolution |
|
||||||
|
| `/loading` | Initialization progress page |
|
||||||
|
| `/login` | Authentication |
|
||||||
|
| `/api/auth/*` | Auth endpoints |
|
||||||
|
| `/api/config/*` | Config API |
|
||||||
|
| `/api/health` | Health check |
|
||||||
|
| `/static/*` | Static assets |
|
||||||
|
|
||||||
|
### Middleware Logic
|
||||||
|
|
||||||
|
1. **Setup incomplete** → Redirect to `/setup`
|
||||||
|
2. **Setup complete, accessing `/setup`** → Redirect to `/loading`
|
||||||
|
3. **Setup complete, accessing `/loading`** → Allow access (page handles its own redirect)
|
||||||
|
4. **API requests during setup** → Return 503 with `setup_url`
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
### 1. Setup Page (`/setup`)
|
||||||
|
|
||||||
|
**File:** `src/server/web/templates/setup.html`
|
||||||
|
|
||||||
|
Handles initial configuration:
|
||||||
|
- Master password creation
|
||||||
|
- Anime directory selection
|
||||||
|
- Database initialization
|
||||||
|
|
||||||
|
**Post-completion flow:**
|
||||||
|
- Redirects to `/loading` to begin initialization
|
||||||
|
|
||||||
|
### 2. Loading Page (`/loading`)
|
||||||
|
|
||||||
|
**File:** `src/server/web/templates/loading.html`
|
||||||
|
|
||||||
|
Shows initialization progress via WebSocket:
|
||||||
|
- Series scanning
|
||||||
|
- Database population
|
||||||
|
- Logo/image loading
|
||||||
|
|
||||||
|
**Post-initialization flow:**
|
||||||
|
```javascript
|
||||||
|
async function checkUnresolvedAndProceed() {
|
||||||
|
// Fetch unresolved folders via API
|
||||||
|
const res = await fetch('/api/setup/unresolved', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const folders = await res.json();
|
||||||
|
|
||||||
|
if (folders.length > 0) {
|
||||||
|
// Has unresolved folders → go to resolution page
|
||||||
|
window.location.href = '/setup/unresolved';
|
||||||
|
} else {
|
||||||
|
// No unresolved folders → go to login
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Unresolved Folders Page (`/setup/unresolved`)
|
||||||
|
|
||||||
|
**File:** `src/server/web/templates/unresolved.html`
|
||||||
|
|
||||||
|
Allows manual resolution of folders that couldn't be auto-matched:
|
||||||
|
- Shows list of unresolved folders
|
||||||
|
- Provides search suggestions
|
||||||
|
- Input field for entering provider key
|
||||||
|
- Resolve/delete actions
|
||||||
|
|
||||||
|
**Post-resolution flow:**
|
||||||
|
```javascript
|
||||||
|
function checkEmptyList() {
|
||||||
|
if (listEl.children.length === 0) {
|
||||||
|
// All folders resolved → return to loading
|
||||||
|
setTimeout(() => { window.location.href = '/loading'; }, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Login Page (`/login`)
|
||||||
|
|
||||||
|
**File:** `src/server/web/templates/login.html`
|
||||||
|
|
||||||
|
Authentication page. After successful login → redirect to `/` (main app).
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Unresolved Folders API
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| `GET` | `/api/setup/unresolved` | List all unresolved folders |
|
||||||
|
| `GET` | `/api/setup/unresolved/{folder_name}` | Get specific folder details |
|
||||||
|
| `POST` | `/api/setup/unresolved/{folder_name}/resolve` | Resolve with provider key |
|
||||||
|
| `POST` | `/api/setup/unresolved/{folder_name}/search` | Re-search for matches |
|
||||||
|
| `DELETE` | `/api/setup/unresolved/{folder_name}` | Remove folder from tracking |
|
||||||
|
|
||||||
|
### Auth API
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| `POST` | `/api/auth/setup` | Create master password |
|
||||||
|
| `POST` | `/api/auth/login` | Authenticate |
|
||||||
|
| `POST` | `/api/auth/logout` | End session |
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/server/middleware/setup_redirect.py` | Redirect middleware |
|
||||||
|
| `src/server/controllers/page_controller.py` | Page route handlers |
|
||||||
|
| `src/server/web/templates/setup.html` | Setup template |
|
||||||
|
| `src/server/web/templates/loading.html` | Loading template |
|
||||||
|
| `src/server/web/templates/unresolved.html` | Unresolved folders template |
|
||||||
|
| `src/server/api/setup_endpoints.py` | Unresolved folders API |
|
||||||
|
| `src/server/database/service.py` | UnresolvedFolderService |
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### Redirect Loop
|
||||||
|
|
||||||
|
**Symptom:** Browser keeps redirecting between pages.
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. `loading.html` always redirected to `/setup/unresolved` without checking if any exist
|
||||||
|
2. `unresolved.html` redirected to `/` which middleware redirected back to `/login`
|
||||||
|
|
||||||
|
**Fix:** See the navigation logic updates in loading.html and unresolved.html.
|
||||||
|
|
||||||
|
### Can't Access Unresolved Page After Setup
|
||||||
|
|
||||||
|
**Symptom:** Middleware redirects to `/login` instead of allowing access to `/setup/unresolved`.
|
||||||
|
|
||||||
|
**Cause:** `/setup/unresolved` is in the exempt paths but the request may not be reaching it due to completion check timing.
|
||||||
|
|
||||||
|
**Fix:** The middleware allows access to `/loading` which handles the redirect to `/setup/unresolved` after initialization.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aniworld-web",
|
"name": "aniworld-web",
|
||||||
"version": "1.4.5",
|
"version": "1.4.10",
|
||||||
"description": "Aniworld Anime Download Manager - Web Frontend",
|
"description": "Aniworld Anime Download Manager - Web Frontend",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Authentication API endpoints for Aniworld."""
|
"""Authentication API endpoints for Aniworld."""
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import structlog
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fastapi import status as http_status
|
from fastapi import status as http_status
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
@@ -16,6 +17,8 @@ from src.server.models.config import AppConfig
|
|||||||
from src.server.services.auth_service import AuthError, LockedOutError, auth_service
|
from src.server.services.auth_service import AuthError, LockedOutError, auth_service
|
||||||
from src.server.services.config_service import get_config_service
|
from src.server.services.config_service import get_config_service
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
# NOTE: import dependencies (optional_auth, security) lazily inside handlers
|
# NOTE: import dependencies (optional_auth, security) lazily inside handlers
|
||||||
# to avoid importing heavyweight modules (e.g. sqlalchemy) at import time.
|
# to avoid importing heavyweight modules (e.g. sqlalchemy) at import time.
|
||||||
|
|
||||||
@@ -144,10 +147,7 @@ async def setup_auth(req: SetupRequest):
|
|||||||
# Trigger initialization in background task
|
# Trigger initialization in background task
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from src.server.services.initialization_service import (
|
from src.server.services.initialization_service import perform_initial_setup
|
||||||
perform_initial_setup,
|
|
||||||
perform_nfo_scan_if_needed,
|
|
||||||
)
|
|
||||||
from src.server.services.progress_service import get_progress_service
|
from src.server.services.progress_service import get_progress_service
|
||||||
|
|
||||||
progress_service = get_progress_service()
|
progress_service = get_progress_service()
|
||||||
@@ -158,9 +158,6 @@ async def setup_auth(req: SetupRequest):
|
|||||||
# Perform the initial series sync and mark as completed
|
# Perform the initial series sync and mark as completed
|
||||||
await perform_initial_setup(progress_service)
|
await perform_initial_setup(progress_service)
|
||||||
|
|
||||||
# Perform NFO scan if configured
|
|
||||||
await perform_nfo_scan_if_needed(progress_service)
|
|
||||||
|
|
||||||
# Start scheduler if anime_directory is now set
|
# Start scheduler if anime_directory is now set
|
||||||
try:
|
try:
|
||||||
from src.server.services.scheduler.scheduler_service import (
|
from src.server.services.scheduler.scheduler_service import (
|
||||||
|
|||||||
@@ -344,7 +344,6 @@ async def lifespan(_application: FastAPI):
|
|||||||
from src.server.services.initialization_service import (
|
from src.server.services.initialization_service import (
|
||||||
perform_initial_setup,
|
perform_initial_setup,
|
||||||
perform_media_scan_if_needed,
|
perform_media_scan_if_needed,
|
||||||
perform_nfo_scan_if_needed,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -373,9 +372,6 @@ async def lifespan(_application: FastAPI):
|
|||||||
"exist yet): %s", e
|
"exist yet): %s", e
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run NFO scan only on first run (if configured)
|
|
||||||
await perform_nfo_scan_if_needed()
|
|
||||||
|
|
||||||
# Initialize download service
|
# Initialize download service
|
||||||
try:
|
try:
|
||||||
from src.server.utils.dependencies import get_download_service
|
from src.server.utils.dependencies import get_download_service
|
||||||
|
|||||||
@@ -32,10 +32,12 @@ class SetupRedirectMiddleware(BaseHTTPMiddleware):
|
|||||||
# Paths that should always be accessible, even without setup
|
# Paths that should always be accessible, even without setup
|
||||||
EXEMPT_PATHS = {
|
EXEMPT_PATHS = {
|
||||||
"/setup", # Setup page itself
|
"/setup", # Setup page itself
|
||||||
|
"/setup/unresolved", # Unresolved folders page (after setup)
|
||||||
"/loading", # Loading page (initialization progress)
|
"/loading", # Loading page (initialization progress)
|
||||||
"/login", # Login page (needs to be accessible after setup)
|
"/login", # Login page (needs to be accessible after setup)
|
||||||
"/queue", # Queue page (for initial load)
|
"/queue", # Queue page (for initial load)
|
||||||
"/api/auth/", # All auth endpoints (setup, login, logout, register)
|
"/api/auth/", # All auth endpoints (setup, login, logout, register)
|
||||||
|
"/api/setup/", # Setup API (unresolved folders, etc.)
|
||||||
"/ws/connect", # WebSocket connection (needed for loading page)
|
"/ws/connect", # WebSocket connection (needed for loading page)
|
||||||
"/api/queue/", # Queue API endpoints
|
"/api/queue/", # Queue API endpoints
|
||||||
"/api/downloads/", # Download API endpoints
|
"/api/downloads/", # Download API endpoints
|
||||||
@@ -126,20 +128,9 @@ class SetupRedirectMiddleware(BaseHTTPMiddleware):
|
|||||||
# Otherwise redirect to login
|
# Otherwise redirect to login
|
||||||
return RedirectResponse(url="/login", status_code=302)
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
elif path == "/loading":
|
elif path == "/loading":
|
||||||
# Check if initialization is complete
|
# Always allow access to loading page - it handles its own
|
||||||
try:
|
# redirect flow via WebSocket events (initialization_complete
|
||||||
from src.server.database.connection import get_db_session
|
# event triggers redirect to /setup/unresolved)
|
||||||
from src.server.database.system_settings_service import (
|
|
||||||
SystemSettingsService,
|
|
||||||
)
|
|
||||||
|
|
||||||
async with get_db_session() as db:
|
|
||||||
is_complete = await SystemSettingsService.is_initial_scan_completed(db)
|
|
||||||
if is_complete:
|
|
||||||
# Initialization complete, redirect to login
|
|
||||||
return RedirectResponse(url="/login", status_code=302)
|
|
||||||
except Exception:
|
|
||||||
# If we can't check, allow access to loading page
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Skip setup check for exempt paths
|
# Skip setup check for exempt paths
|
||||||
|
|||||||
@@ -281,15 +281,11 @@
|
|||||||
let isComplete = false;
|
let isComplete = false;
|
||||||
|
|
||||||
const stepOrder = [
|
const stepOrder = [
|
||||||
'series_sync',
|
'series_sync'
|
||||||
'nfo_scan',
|
|
||||||
'media_scan'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const stepTitles = {
|
const stepTitles = {
|
||||||
'series_sync': 'Syncing Series Database',
|
'series_sync': 'Syncing Series Database'
|
||||||
'nfo_scan': 'Processing NFO Metadata',
|
|
||||||
'media_scan': 'Scanning Media Files'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
@@ -479,32 +475,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkUnresolvedAndProceed() {
|
async function checkUnresolvedAndProceed() {
|
||||||
|
// Fetch unresolved folders and only redirect if there are any
|
||||||
|
// Otherwise go directly to login
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
if (!token) {
|
|
||||||
// No token, go to login
|
|
||||||
document.getElementById('completionMessage').style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch('/api/setup/unresolved', {
|
const res = await fetch('/api/setup/unresolved', {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const unresolved = await res.json();
|
const folders = await res.json();
|
||||||
if (unresolved && unresolved.length > 0) {
|
if (folders && folders.length > 0) {
|
||||||
// Has unresolved folders - redirect to unresolved page
|
// Has unresolved folders - go to resolution page
|
||||||
window.location.href = '/setup/unresolved';
|
window.location.href = '/setup/unresolved';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
console.error('Error checking unresolved folders:', e);
|
console.error('Failed to check unresolved folders:', err);
|
||||||
}
|
}
|
||||||
|
// No unresolved folders or error - go to login
|
||||||
// No unresolved folders or error - show completion message
|
window.location.href = '/login';
|
||||||
document.getElementById('completionMessage').style.display = 'block';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(message) {
|
function showError(message) {
|
||||||
|
|||||||
@@ -443,15 +443,13 @@
|
|||||||
|
|
||||||
// API client helpers
|
// API client helpers
|
||||||
async function fetchUnresolved() {
|
async function fetchUnresolved() {
|
||||||
|
// Note: /api/setup/unresolved does not require auth
|
||||||
|
// It's accessible during the initial setup flow
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
if (!token) {
|
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||||
window.location.href = '/login';
|
const res = await fetch('/api/setup/unresolved', { headers });
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const res = await fetch('/api/setup/unresolved', {
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
|
||||||
});
|
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
|
// Redirect to login only if we had a token but it expired
|
||||||
localStorage.removeItem('auth_token');
|
localStorage.removeItem('auth_token');
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
return null;
|
return null;
|
||||||
@@ -552,7 +550,7 @@
|
|||||||
listEl.style.display = 'none';
|
listEl.style.display = 'none';
|
||||||
emptyEl.style.display = 'block';
|
emptyEl.style.display = 'block';
|
||||||
document.getElementById('skip-link').style.display = 'block';
|
document.getElementById('skip-link').style.display = 'block';
|
||||||
setTimeout(() => { window.location.href = '/'; }, 2000);
|
setTimeout(() => { window.location.href = '/loading'; }, 2000);
|
||||||
} else {
|
} else {
|
||||||
listEl.style.display = 'flex';
|
listEl.style.display = 'flex';
|
||||||
emptyEl.style.display = 'none';
|
emptyEl.style.display = 'none';
|
||||||
@@ -690,7 +688,7 @@
|
|||||||
emptyEl.style.display = 'block';
|
emptyEl.style.display = 'block';
|
||||||
skipLink.style.display = 'block';
|
skipLink.style.display = 'block';
|
||||||
showToast('All series configured!', 'success');
|
showToast('All series configured!', 'success');
|
||||||
setTimeout(() => { window.location.href = '/'; }, 2000);
|
setTimeout(() => { window.location.href = '/loading'; }, 2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user