style: fix all flake8 linter errors across app/ directory
- Run Black formatter and isort on all app/ files - Remove unused imports (F401) across multiple files - Add # noqa: F401 for intentional re-exports in celery_worker.py, tasks/__init__.py, utils.py, frontend.py, views/base.py - Fix f-strings without placeholders (F541) in azure.py, notification.py, check_credentials.py, upload_to_onedrive.py, settings.py - Fix bare except (E722) in upload_to_sftp.py - Fix block comment format (E265) in models.py - Move imports to top of file to fix E402 in celery_app.py, celery_worker.py - Fix line-too-long (E501) by wrapping strings in multiple files - Remove unused variable (F841) in upload_to_nextcloud.py Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
"""
|
||||
Aggregated view routers for the application.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.views.dropbox import router as dropbox_router
|
||||
|
||||
# Import all the view routers
|
||||
from app.views.general import router as general_router
|
||||
from app.views.status import router as status_router
|
||||
from app.views.onedrive import router as onedrive_router
|
||||
from app.views.dropbox import router as dropbox_router
|
||||
from app.views.google_drive import router as google_drive_router
|
||||
from app.views.license_routes import router as license_router # Add the license router
|
||||
from app.views.onedrive import router as onedrive_router
|
||||
from app.views.settings import router as settings_router
|
||||
from app.views.status import router as status_router
|
||||
from app.views.wizard import router as wizard_router
|
||||
|
||||
# Create a main router that includes all the view routers
|
||||
|
||||
+12
-7
@@ -1,15 +1,17 @@
|
||||
"""
|
||||
Base setup for views, containing shared functionality and imports.
|
||||
"""
|
||||
from fastapi import APIRouter, Request, Depends, HTTPException
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pathlib import Path
|
||||
from sqlalchemy.orm import Session
|
||||
import logging
|
||||
|
||||
from app.auth import require_login
|
||||
from app.database import SessionLocal
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request # noqa: F401
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session # noqa: F401
|
||||
|
||||
from app.auth import require_login # noqa: F401
|
||||
from app.config import settings
|
||||
from app.database import SessionLocal
|
||||
|
||||
# Set up Jinja2 templates
|
||||
templates_dir = Path(__file__).parent.parent.parent / "frontend" / "templates"
|
||||
@@ -22,6 +24,7 @@ templates.env.globals["max"] = max
|
||||
# Customize Jinja2Templates to include app_version in all templates
|
||||
original_template_response = templates.TemplateResponse
|
||||
|
||||
|
||||
def template_response_with_version(*args, **kwargs):
|
||||
"""Wrapper for TemplateResponse to include version in all templates"""
|
||||
# If context dict is provided, add version to it
|
||||
@@ -31,11 +34,13 @@ def template_response_with_version(*args, **kwargs):
|
||||
kwargs["context"].setdefault("version", settings.version)
|
||||
return original_template_response(*args, **kwargs)
|
||||
|
||||
|
||||
templates.TemplateResponse = template_response_with_version
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_db():
|
||||
"""
|
||||
Dependency to get a database session.
|
||||
|
||||
+15
-18
@@ -1,12 +1,14 @@
|
||||
"""
|
||||
Dropbox integration views for setup and OAuth callback.
|
||||
"""
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from app.views.base import APIRouter, templates, require_login, settings
|
||||
from app.views.base import APIRouter, require_login, settings, templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/dropbox-setup")
|
||||
@require_login
|
||||
async def dropbox_setup_page(request: Request):
|
||||
@@ -15,10 +17,8 @@ async def dropbox_setup_page(request: Request):
|
||||
Shows configuration status and setup instructions.
|
||||
"""
|
||||
# Check Dropbox configuration
|
||||
is_configured = bool(settings.dropbox_app_key and
|
||||
settings.dropbox_app_secret and
|
||||
settings.dropbox_refresh_token)
|
||||
|
||||
is_configured = bool(settings.dropbox_app_key and settings.dropbox_app_secret and settings.dropbox_refresh_token)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"dropbox.html",
|
||||
{
|
||||
@@ -27,10 +27,11 @@ async def dropbox_setup_page(request: Request):
|
||||
"app_key_value": settings.dropbox_app_key or "",
|
||||
"app_secret_value": settings.dropbox_app_secret if settings.dropbox_app_secret else "",
|
||||
"refresh_token_value": settings.dropbox_refresh_token if settings.dropbox_refresh_token else "",
|
||||
"folder_path": settings.dropbox_folder or "/Documents/Uploads" # Default folder path
|
||||
}
|
||||
"folder_path": settings.dropbox_folder or "/Documents/Uploads", # Default folder path
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/dropbox-callback")
|
||||
@require_login
|
||||
async def dropbox_callback(request: Request, code: str = None, error: str = None):
|
||||
@@ -39,27 +40,23 @@ async def dropbox_callback(request: Request, code: str = None, error: str = None
|
||||
Automatically exchanges the code for a token and saves it to the configuration.
|
||||
"""
|
||||
if error:
|
||||
return templates.TemplateResponse(
|
||||
"dropbox_callback_error.html",
|
||||
{"request": request, "error": error}
|
||||
)
|
||||
|
||||
return templates.TemplateResponse("dropbox_callback_error.html", {"request": request, "error": error})
|
||||
|
||||
if not code:
|
||||
return templates.TemplateResponse(
|
||||
"dropbox_callback_error.html",
|
||||
{"request": request, "error": "No authorization code received from Dropbox"}
|
||||
"dropbox_callback_error.html", {"request": request, "error": "No authorization code received from Dropbox"}
|
||||
)
|
||||
|
||||
|
||||
# Display the processing page with automatic token exchange
|
||||
# Note: We provide empty strings for app_key_value and app_secret_value
|
||||
# to prevent overriding what's in sessionStorage
|
||||
return templates.TemplateResponse(
|
||||
"dropbox_callback.html",
|
||||
{
|
||||
"request": request,
|
||||
"request": request,
|
||||
"code": code,
|
||||
"app_key_value": "", # The callback will prioritize sessionStorage values
|
||||
"app_secret_value": "", # The callback will prioritize sessionStorage values
|
||||
"folder_path": "" # The callback will prioritize sessionStorage values
|
||||
}
|
||||
"folder_path": "", # The callback will prioritize sessionStorage values
|
||||
},
|
||||
)
|
||||
|
||||
+9
-6
@@ -2,13 +2,14 @@
|
||||
File management views for displaying and managing files.
|
||||
"""
|
||||
|
||||
from fastapi import Request, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from app.views.base import APIRouter, templates, require_login, get_db, logger
|
||||
from app.utils.file_status import get_files_processing_status
|
||||
from fastapi import Depends, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import settings
|
||||
from app.utils.file_status import get_files_processing_status
|
||||
from app.views.base import APIRouter, get_db, logger, require_login, templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -31,8 +32,9 @@ def files_page(
|
||||
"""
|
||||
try:
|
||||
# Import the model here to avoid circular imports
|
||||
from sqlalchemy import asc, desc, or_
|
||||
|
||||
from app.models import FileRecord, ProcessingLog
|
||||
from sqlalchemy import desc, asc, or_
|
||||
|
||||
# Start with base query
|
||||
query = db.query(FileRecord)
|
||||
@@ -159,9 +161,10 @@ def file_detail_page(request: Request, file_id: int, db: Session = Depends(get_d
|
||||
Return the file detail page showing processing history and file information
|
||||
"""
|
||||
try:
|
||||
from app.models import FileRecord, ProcessingLog
|
||||
import os
|
||||
|
||||
from app.models import FileRecord, ProcessingLog
|
||||
|
||||
# Find the file record
|
||||
file_record = db.query(FileRecord).filter(FileRecord.id == file_id).first()
|
||||
|
||||
|
||||
+38
-30
@@ -1,75 +1,83 @@
|
||||
"""
|
||||
General routes for the application homepage and basic pages.
|
||||
"""
|
||||
from fastapi import Request, HTTPException, Depends
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from pathlib import Path
|
||||
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.views.base import APIRouter, templates, require_login, get_db, logger
|
||||
from app.utils.config_validator import get_provider_status, validate_storage_configs
|
||||
from app.views.base import APIRouter, get_db, logger, require_login, templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", include_in_schema=False)
|
||||
async def serve_index(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Serve the index/home page.
|
||||
|
||||
|
||||
If the system requires initial setup, redirect to the setup wizard.
|
||||
"""
|
||||
# Check if setup wizard is needed
|
||||
from app.utils.setup_wizard import is_setup_required
|
||||
from app.utils.settings_service import get_setting_from_db
|
||||
|
||||
from app.utils.setup_wizard import is_setup_required
|
||||
|
||||
# Check if setup was explicitly skipped
|
||||
setup_skipped = get_setting_from_db(db, "_setup_wizard_skipped")
|
||||
|
||||
|
||||
# Check setup completion query param
|
||||
setup_complete = request.query_params.get("setup") == "complete"
|
||||
|
||||
|
||||
if not setup_skipped and not setup_complete and is_setup_required():
|
||||
logger.info("System requires initial setup, redirecting to wizard")
|
||||
return RedirectResponse(url="/setup?step=1", status_code=303)
|
||||
|
||||
|
||||
# Get provider information from config validator
|
||||
providers = get_provider_status()
|
||||
|
||||
|
||||
# Count configured providers
|
||||
configured_providers = sum(1 for provider in providers.values() if provider['configured'])
|
||||
|
||||
configured_providers = sum(1 for provider in providers.values() if provider["configured"])
|
||||
|
||||
# Count different types of storage targets
|
||||
storage_issues = validate_storage_configs()
|
||||
configured_storage_targets = sum(1 for provider, issues in storage_issues.items()
|
||||
if not issues and provider in ['dropbox', 'nextcloud', 'sftp',
|
||||
's3', 'ftp', 'webdav',
|
||||
'google_drive', 'onedrive'])
|
||||
|
||||
configured_storage_targets = sum(
|
||||
1
|
||||
for provider, issues in storage_issues.items()
|
||||
if not issues
|
||||
and provider in ["dropbox", "nextcloud", "sftp", "s3", "ftp", "webdav", "google_drive", "onedrive"]
|
||||
)
|
||||
|
||||
# Query the actual file count from the database
|
||||
processed_files = 0
|
||||
try:
|
||||
# Import the model here to avoid circular imports
|
||||
from app.models import FileRecord
|
||||
|
||||
processed_files = db.query(FileRecord).count()
|
||||
except Exception as e:
|
||||
# Log error but continue (don't break the page if DB query fails)
|
||||
logger.error(f"Error counting files: {str(e)}")
|
||||
|
||||
|
||||
# Create stats object to pass to the template
|
||||
stats = {
|
||||
"processed_files": processed_files,
|
||||
"active_integrations": configured_providers,
|
||||
"storage_targets": configured_storage_targets
|
||||
"storage_targets": configured_storage_targets,
|
||||
}
|
||||
|
||||
|
||||
return templates.TemplateResponse("index.html", {"request": request, "stats": stats})
|
||||
|
||||
|
||||
@router.get("/about", include_in_schema=False)
|
||||
async def serve_about(request: Request):
|
||||
"""Serve the about page."""
|
||||
return templates.TemplateResponse("about.html", {"request": request})
|
||||
|
||||
|
||||
@router.get("/privacy", include_in_schema=False)
|
||||
async def serve_privacy(request: Request):
|
||||
"""Serve the privacy policy page."""
|
||||
@@ -77,17 +85,20 @@ async def serve_privacy(request: Request):
|
||||
current_date = date.today().strftime("%B %d, %Y")
|
||||
return templates.TemplateResponse("privacy.html", {"request": request, "current_date": current_date})
|
||||
|
||||
|
||||
@router.get("/imprint", include_in_schema=False)
|
||||
async def serve_imprint(request: Request):
|
||||
"""Serve the imprint/impressum page."""
|
||||
return templates.TemplateResponse("imprint.html", {"request": request})
|
||||
|
||||
|
||||
@router.get("/upload", include_in_schema=False)
|
||||
@require_login
|
||||
async def serve_upload(request: Request):
|
||||
"""Serve the upload page."""
|
||||
return templates.TemplateResponse("upload.html", {"request": request})
|
||||
|
||||
|
||||
@router.get("/favicon.ico", include_in_schema=False)
|
||||
def favicon():
|
||||
"""Serve the favicon."""
|
||||
@@ -97,6 +108,7 @@ def favicon():
|
||||
raise HTTPException(status_code=404, detail="Favicon not found")
|
||||
return FileResponse(favicon_path)
|
||||
|
||||
|
||||
@router.get("/license", include_in_schema=False)
|
||||
async def serve_license(request: Request):
|
||||
"""Serve the license page."""
|
||||
@@ -108,7 +120,7 @@ async def serve_license(request: Request):
|
||||
]
|
||||
|
||||
license_text = None
|
||||
|
||||
|
||||
# Try to read from any of the possible locations
|
||||
for path in possible_locations:
|
||||
try:
|
||||
@@ -117,7 +129,7 @@ async def serve_license(request: Request):
|
||||
break # File found and read, exit loop
|
||||
except (FileNotFoundError, PermissionError):
|
||||
continue # Try next location
|
||||
|
||||
|
||||
# If license text is still None, use embedded text
|
||||
if license_text is None:
|
||||
license_text = """
|
||||
@@ -130,13 +142,8 @@ The full license text could not be located on this system.
|
||||
Please visit http://www.apache.org/licenses/LICENSE-2.0 for the complete license text.
|
||||
"""
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"license.html",
|
||||
{
|
||||
"request": request,
|
||||
"license_text": license_text
|
||||
}
|
||||
)
|
||||
return templates.TemplateResponse("license.html", {"request": request, "license_text": license_text})
|
||||
|
||||
|
||||
@router.get("/cookies", include_in_schema=False)
|
||||
async def serve_cookies(request: Request):
|
||||
@@ -144,6 +151,7 @@ async def serve_cookies(request: Request):
|
||||
current_date = date.today().strftime("%B %d, %Y")
|
||||
return templates.TemplateResponse("cookies.html", {"request": request, "current_date": current_date})
|
||||
|
||||
|
||||
@router.get("/terms", include_in_schema=False)
|
||||
async def serve_terms(request: Request):
|
||||
"""Serve the terms of service page."""
|
||||
|
||||
+31
-42
@@ -1,14 +1,17 @@
|
||||
"""
|
||||
Google Drive integration views for setup and OAuth callback.
|
||||
"""
|
||||
from fastapi import Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
import urllib.parse
|
||||
|
||||
from app.views.base import APIRouter, templates, require_login, settings
|
||||
from fastapi import Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from app.views.base import APIRouter, require_login, settings, templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/google-drive-setup")
|
||||
@require_login
|
||||
async def google_drive_setup_page(request: Request):
|
||||
@@ -17,24 +20,24 @@ async def google_drive_setup_page(request: Request):
|
||||
Shows configuration status and setup instructions.
|
||||
"""
|
||||
# Check if using OAuth
|
||||
use_oauth = getattr(settings, 'google_drive_use_oauth', False)
|
||||
|
||||
use_oauth = getattr(settings, "google_drive_use_oauth", False)
|
||||
|
||||
# Check Google Drive OAuth configuration
|
||||
oauth_configured = bool(settings.google_drive_client_id and
|
||||
settings.google_drive_client_secret and
|
||||
settings.google_drive_refresh_token)
|
||||
|
||||
oauth_configured = bool(
|
||||
settings.google_drive_client_id and settings.google_drive_client_secret and settings.google_drive_refresh_token
|
||||
)
|
||||
|
||||
# Check Google Drive service account configuration
|
||||
sa_configured = bool(settings.google_drive_credentials_json)
|
||||
|
||||
|
||||
# Overall configuration status
|
||||
is_configured = (use_oauth and oauth_configured) or (not use_oauth and sa_configured)
|
||||
|
||||
|
||||
if settings.google_drive_folder_id:
|
||||
is_configured = is_configured and True
|
||||
else:
|
||||
is_configured = False
|
||||
|
||||
|
||||
# Get configuration values to display status (hide sensitive values)
|
||||
return templates.TemplateResponse(
|
||||
"google_drive.html",
|
||||
@@ -51,10 +54,11 @@ async def google_drive_setup_page(request: Request):
|
||||
"refresh_token": bool(settings.google_drive_refresh_token),
|
||||
"refresh_token_value": settings.google_drive_refresh_token or "",
|
||||
"folder_id": settings.google_drive_folder_id or "",
|
||||
"has_credentials_json": bool(settings.google_drive_credentials_json)
|
||||
}
|
||||
"has_credentials_json": bool(settings.google_drive_credentials_json),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/google-drive-callback")
|
||||
@require_login
|
||||
async def google_drive_callback(request: Request, code: str = None, error: str = None, state: str = None):
|
||||
@@ -63,48 +67,33 @@ async def google_drive_callback(request: Request, code: str = None, error: str =
|
||||
Now automatically exchanges the code for a token and saves it to the configuration.
|
||||
"""
|
||||
if error:
|
||||
return templates.TemplateResponse(
|
||||
"google_drive_callback_error.html",
|
||||
{"request": request, "error": error}
|
||||
)
|
||||
|
||||
return templates.TemplateResponse("google_drive_callback_error.html", {"request": request, "error": error})
|
||||
|
||||
if not code:
|
||||
return templates.TemplateResponse(
|
||||
"google_drive_callback_error.html",
|
||||
{"request": request, "error": "No authorization code received from Google"}
|
||||
{"request": request, "error": "No authorization code received from Google"},
|
||||
)
|
||||
|
||||
|
||||
# Display the processing page with automatic token exchange
|
||||
return templates.TemplateResponse(
|
||||
"google_drive_callback.html",
|
||||
{
|
||||
"request": request,
|
||||
"code": code,
|
||||
"state": state
|
||||
}
|
||||
)
|
||||
return templates.TemplateResponse("google_drive_callback.html", {"request": request, "code": code, "state": state})
|
||||
|
||||
|
||||
@router.get("/google-drive-auth-start")
|
||||
@require_login
|
||||
async def google_drive_auth_start(
|
||||
request: Request,
|
||||
client_id: str,
|
||||
redirect_uri: str = None
|
||||
):
|
||||
async def google_drive_auth_start(request: Request, client_id: str, redirect_uri: str = None):
|
||||
"""
|
||||
Start the Google Drive OAuth flow by redirecting to Google's authorization page.
|
||||
"""
|
||||
if not redirect_uri:
|
||||
redirect_uri = f"{request.url.scheme}://{request.url.netloc}/google-drive-callback"
|
||||
|
||||
|
||||
# Create the authorization URL with required scopes
|
||||
# Use only drive.file scope to minimize required permissions
|
||||
scopes = [
|
||||
"https://www.googleapis.com/auth/drive.file" # Access to files created or opened by the app
|
||||
]
|
||||
|
||||
scope_str = urllib.parse.quote(' '.join(scopes))
|
||||
|
||||
scopes = ["https://www.googleapis.com/auth/drive.file"] # Access to files created or opened by the app
|
||||
|
||||
scope_str = urllib.parse.quote(" ".join(scopes))
|
||||
|
||||
auth_url = (
|
||||
f"https://accounts.google.com/o/oauth2/auth"
|
||||
f"?client_id={client_id}"
|
||||
@@ -114,5 +103,5 @@ async def google_drive_auth_start(
|
||||
f"&access_type=offline"
|
||||
f"&prompt=consent" # Force to show consent screen to get refresh token
|
||||
)
|
||||
|
||||
|
||||
return RedirectResponse(url=auth_url)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import PlainTextResponse, HTMLResponse
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
from app.views.base import templates, require_login
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse
|
||||
|
||||
from app.views.base import templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/licenses/lgpl.txt", response_class=PlainTextResponse)
|
||||
async def get_lgpl_license():
|
||||
"""
|
||||
@@ -15,10 +16,11 @@ async def get_lgpl_license():
|
||||
license_path = Path("frontend/static/licenses/lgpl.txt")
|
||||
if not license_path.exists():
|
||||
raise HTTPException(status_code=404, detail="License file not found")
|
||||
|
||||
|
||||
with open(license_path, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@router.get("/attribution", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def serve_attribution(request: Request):
|
||||
"""
|
||||
|
||||
+17
-17
@@ -1,12 +1,14 @@
|
||||
"""
|
||||
OneDrive integration views for setup and OAuth callback.
|
||||
"""
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from app.views.base import APIRouter, templates, require_login, settings
|
||||
from app.views.base import APIRouter, require_login, settings, templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/onedrive-setup")
|
||||
@require_login
|
||||
async def onedrive_setup_page(request: Request):
|
||||
@@ -15,10 +17,10 @@ async def onedrive_setup_page(request: Request):
|
||||
Shows configuration status and setup instructions.
|
||||
"""
|
||||
# Check OneDrive configuration
|
||||
is_configured = bool(settings.onedrive_client_id and
|
||||
settings.onedrive_client_secret and
|
||||
settings.onedrive_refresh_token)
|
||||
|
||||
is_configured = bool(
|
||||
settings.onedrive_client_id and settings.onedrive_client_secret and settings.onedrive_refresh_token
|
||||
)
|
||||
|
||||
# Get configuration values to display status (hide sensitive values)
|
||||
return templates.TemplateResponse(
|
||||
"onedrive.html",
|
||||
@@ -32,10 +34,11 @@ async def onedrive_setup_page(request: Request):
|
||||
"tenant_id": settings.onedrive_tenant_id,
|
||||
"refresh_token": bool(settings.onedrive_refresh_token),
|
||||
"refresh_token_value": settings.onedrive_refresh_token if settings.onedrive_refresh_token else "",
|
||||
"folder_path": settings.onedrive_folder_path or "Documents/Uploads" # Default folder path
|
||||
}
|
||||
"folder_path": settings.onedrive_folder_path or "Documents/Uploads", # Default folder path
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/onedrive-callback")
|
||||
@require_login
|
||||
async def onedrive_callback(request: Request, code: str = None, error: str = None):
|
||||
@@ -44,25 +47,22 @@ async def onedrive_callback(request: Request, code: str = None, error: str = Non
|
||||
Now automatically exchanges the code for a token and saves it to the configuration.
|
||||
"""
|
||||
if error:
|
||||
return templates.TemplateResponse(
|
||||
"onedrive_callback_error.html",
|
||||
{"request": request, "error": error}
|
||||
)
|
||||
|
||||
return templates.TemplateResponse("onedrive_callback_error.html", {"request": request, "error": error})
|
||||
|
||||
if not code:
|
||||
return templates.TemplateResponse(
|
||||
"onedrive_callback_error.html",
|
||||
{"request": request, "error": "No authorization code received from Microsoft"}
|
||||
{"request": request, "error": "No authorization code received from Microsoft"},
|
||||
)
|
||||
|
||||
|
||||
# Display the processing page with automatic token exchange
|
||||
return templates.TemplateResponse(
|
||||
"onedrive_callback.html",
|
||||
{
|
||||
"request": request,
|
||||
"request": request,
|
||||
"code": code,
|
||||
"client_id_value": settings.onedrive_client_id or "",
|
||||
"client_secret_value": settings.onedrive_client_secret or "",
|
||||
"tenant_id": settings.onedrive_tenant_id or "common"
|
||||
}
|
||||
"tenant_id": settings.onedrive_tenant_id or "common",
|
||||
},
|
||||
)
|
||||
|
||||
+33
-35
@@ -2,17 +2,18 @@
|
||||
Settings management views for the application.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
from functools import wraps
|
||||
from fastapi import Request, Depends, HTTPException, status
|
||||
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.views.base import APIRouter, templates, require_login, settings, get_db
|
||||
from app.utils.settings_service import get_settings_by_category, get_setting_metadata, SETTING_METADATA
|
||||
from app.utils.config_validator.masking import mask_sensitive_value
|
||||
from app.utils.settings_service import get_setting_metadata, get_settings_by_category
|
||||
from app.views.base import APIRouter, get_db, require_login, settings, templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -21,23 +22,25 @@ router = APIRouter()
|
||||
def require_admin_access(func):
|
||||
"""
|
||||
Decorator to require admin access for a route.
|
||||
|
||||
|
||||
This decorator checks if the user in the session has admin privileges.
|
||||
If not, redirects to the home page. Works with both sync and async functions,
|
||||
though FastAPI route handlers should always be async.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(request: Request, *args, **kwargs):
|
||||
user = request.session.get("user")
|
||||
if not user or not user.get("is_admin"):
|
||||
logger.warning(f"Non-admin user attempted to access admin-only route")
|
||||
logger.warning("Non-admin user attempted to access admin-only route")
|
||||
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
# FastAPI route handlers are async, but we support sync for flexibility
|
||||
if inspect.iscoroutinefunction(func):
|
||||
return await func(request, *args, **kwargs)
|
||||
else:
|
||||
return func(request, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -47,19 +50,20 @@ def require_admin_access(func):
|
||||
async def settings_page(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Settings management page - admin only.
|
||||
|
||||
|
||||
This page is a convenience feature to view and edit settings.
|
||||
Values are displayed in precedence order: Database > Environment > Defaults
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
# Get settings from database
|
||||
from app.utils.settings_service import get_all_settings_from_db
|
||||
|
||||
db_settings = get_all_settings_from_db(db)
|
||||
|
||||
|
||||
# Get settings organized by category
|
||||
categories = get_settings_by_category()
|
||||
|
||||
|
||||
# Build settings data for display
|
||||
settings_data = {}
|
||||
for category, keys in categories.items():
|
||||
@@ -67,7 +71,7 @@ async def settings_page(request: Request, db: Session = Depends(get_db)):
|
||||
for key in keys:
|
||||
# Get current value from settings (already has precedence applied)
|
||||
value = getattr(settings, key, None)
|
||||
|
||||
|
||||
# Determine the source of this setting
|
||||
# Check if it's in the database
|
||||
if key in db_settings:
|
||||
@@ -84,35 +88,29 @@ async def settings_page(request: Request, db: Session = Depends(get_db)):
|
||||
source = "default"
|
||||
source_label = "DEFAULT"
|
||||
source_color = "gray"
|
||||
|
||||
|
||||
# Get metadata
|
||||
metadata = get_setting_metadata(key)
|
||||
|
||||
|
||||
# Mask sensitive values
|
||||
display_value = value
|
||||
if metadata.get("sensitive") and value:
|
||||
display_value = mask_sensitive_value(value)
|
||||
|
||||
settings_data[category].append({
|
||||
"key": key,
|
||||
"display_value": display_value if display_value is not None else "",
|
||||
"metadata": metadata,
|
||||
"source": source,
|
||||
"source_label": source_label,
|
||||
"source_color": source_color
|
||||
})
|
||||
|
||||
|
||||
settings_data[category].append(
|
||||
{
|
||||
"key": key,
|
||||
"display_value": display_value if display_value is not None else "",
|
||||
"metadata": metadata,
|
||||
"source": source,
|
||||
"source_label": source_label,
|
||||
"source_color": source_color,
|
||||
}
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"settings.html",
|
||||
{
|
||||
"request": request,
|
||||
"settings_data": settings_data,
|
||||
"app_version": settings.version
|
||||
}
|
||||
"settings.html", {"request": request, "settings_data": settings_data, "app_version": settings.version}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading settings page: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to load settings page"
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to load settings page")
|
||||
|
||||
+44
-42
@@ -1,17 +1,19 @@
|
||||
"""
|
||||
Status and configuration views for the application.
|
||||
"""
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from datetime import datetime
|
||||
import os
|
||||
import logging
|
||||
|
||||
from app.views.base import APIRouter, templates, require_login, settings
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from app.views.base import APIRouter, require_login, settings, templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
@require_login
|
||||
async def status_dashboard(request: Request):
|
||||
@@ -19,75 +21,74 @@ async def status_dashboard(request: Request):
|
||||
Status dashboard showing all configured integration targets
|
||||
"""
|
||||
from app.utils.config_validator import get_provider_status
|
||||
|
||||
|
||||
# Get provider status
|
||||
providers = get_provider_status()
|
||||
|
||||
|
||||
# Get build date from settings
|
||||
build_date = getattr(settings, 'build_date', 'Unknown')
|
||||
|
||||
build_date = getattr(settings, "build_date", "Unknown")
|
||||
|
||||
# Try to get container information
|
||||
container_info = {}
|
||||
try:
|
||||
# Check for Docker environment
|
||||
if os.path.exists('/.dockerenv'):
|
||||
if os.path.exists("/.dockerenv"):
|
||||
# We're inside a Docker container
|
||||
container_info['is_docker'] = True
|
||||
|
||||
container_info["is_docker"] = True
|
||||
|
||||
# Try to get container ID
|
||||
try:
|
||||
with open('/proc/self/cgroup', 'r') as f:
|
||||
with open("/proc/self/cgroup", "r") as f:
|
||||
for line in f:
|
||||
if 'docker' in line:
|
||||
container_id = line.split('/')[-1].strip()
|
||||
container_info['id'] = container_id[:12] # Short ID format
|
||||
if "docker" in line:
|
||||
container_id = line.split("/")[-1].strip()
|
||||
container_info["id"] = container_id[:12] # Short ID format
|
||||
break
|
||||
except Exception:
|
||||
container_info['id'] = 'Unknown'
|
||||
|
||||
container_info["id"] = "Unknown"
|
||||
|
||||
# Get Git commit SHA from settings
|
||||
try:
|
||||
git_sha = settings.git_sha
|
||||
container_info['git_sha'] = git_sha[:7] if git_sha and git_sha != 'unknown' else 'Unknown'
|
||||
container_info["git_sha"] = git_sha[:7] if git_sha and git_sha != "unknown" else "Unknown"
|
||||
except Exception:
|
||||
container_info['git_sha'] = 'Unknown'
|
||||
|
||||
container_info["git_sha"] = "Unknown"
|
||||
|
||||
# Try to get runtime information
|
||||
try:
|
||||
container_info['runtime_info'] = settings.runtime_info
|
||||
container_info["runtime_info"] = settings.runtime_info
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
container_info['is_docker'] = False
|
||||
|
||||
container_info["is_docker"] = False
|
||||
|
||||
# If not in Docker, get Git info from settings
|
||||
try:
|
||||
git_sha = settings.git_sha
|
||||
container_info['git_sha'] = git_sha[:7] if git_sha and git_sha != 'unknown' else 'Unknown'
|
||||
container_info["git_sha"] = git_sha[:7] if git_sha and git_sha != "unknown" else "Unknown"
|
||||
except Exception:
|
||||
container_info['git_sha'] = 'Unknown'
|
||||
container_info["git_sha"] = "Unknown"
|
||||
except Exception:
|
||||
container_info = {'is_docker': False, 'id': 'Unknown', 'git_sha': 'Unknown'}
|
||||
|
||||
container_info = {"is_docker": False, "id": "Unknown", "git_sha": "Unknown"}
|
||||
|
||||
# Get notification URLs for the notification box
|
||||
notification_urls = getattr(settings, 'notification_urls', [])
|
||||
|
||||
notification_urls = getattr(settings, "notification_urls", [])
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"status_dashboard.html",
|
||||
{
|
||||
"request": request,
|
||||
"request": request,
|
||||
"providers": providers,
|
||||
"app_version": settings.version,
|
||||
"build_date": build_date,
|
||||
"debug_enabled": getattr(settings, 'debug', False),
|
||||
"debug_enabled": getattr(settings, "debug", False),
|
||||
"last_check": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"container_info": container_info,
|
||||
"settings": {
|
||||
"notification_urls": notification_urls
|
||||
}
|
||||
}
|
||||
"settings": {"notification_urls": notification_urls},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/env")
|
||||
@require_login
|
||||
async def env_debug(request: Request):
|
||||
@@ -97,17 +98,18 @@ async def env_debug(request: Request):
|
||||
"""
|
||||
# Use the actual debug setting from configuration
|
||||
debug_enabled = settings.debug
|
||||
|
||||
|
||||
# Get settings data
|
||||
from app.utils.config_validator import get_settings_for_display
|
||||
|
||||
settings_data = get_settings_for_display(show_values=debug_enabled)
|
||||
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"env_debug.html",
|
||||
{
|
||||
"request": request,
|
||||
"request": request,
|
||||
"settings": settings_data,
|
||||
"debug_enabled": debug_enabled,
|
||||
"app_version": settings.version
|
||||
}
|
||||
"app_version": settings.version,
|
||||
},
|
||||
)
|
||||
|
||||
+21
-30
@@ -2,21 +2,16 @@
|
||||
Setup wizard views for initial system configuration.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import secrets
|
||||
from fastapi import Request, Depends, Form
|
||||
|
||||
from fastapi import Depends, Form, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.views.base import APIRouter, templates, get_db
|
||||
from app.utils.setup_wizard import (
|
||||
is_setup_required,
|
||||
get_required_settings,
|
||||
get_wizard_steps,
|
||||
get_missing_required_settings
|
||||
)
|
||||
from app.utils.settings_service import save_setting_to_db
|
||||
from app.utils.setup_wizard import get_wizard_steps
|
||||
from app.views.base import APIRouter, get_db, templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -26,26 +21,26 @@ router = APIRouter()
|
||||
async def setup_wizard(request: Request, step: int = 1):
|
||||
"""
|
||||
Setup wizard for first-time configuration.
|
||||
|
||||
|
||||
This wizard guides users through configuring essential settings
|
||||
needed for the system to operate properly.
|
||||
"""
|
||||
# Get wizard steps
|
||||
wizard_steps = get_wizard_steps()
|
||||
max_step = max(wizard_steps.keys())
|
||||
|
||||
|
||||
# Validate step number
|
||||
if step < 1:
|
||||
step = 1
|
||||
elif step > max_step:
|
||||
step = max_step
|
||||
|
||||
|
||||
# Get settings for current step
|
||||
current_settings = wizard_steps.get(step, [])
|
||||
|
||||
|
||||
# Get step category (all settings in a step should have same category)
|
||||
step_category = current_settings[0].get("wizard_category", "Configuration") if current_settings else "Configuration"
|
||||
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"setup_wizard.html",
|
||||
{
|
||||
@@ -54,59 +49,55 @@ async def setup_wizard(request: Request, step: int = 1):
|
||||
"max_step": max_step,
|
||||
"settings": current_settings,
|
||||
"step_category": step_category,
|
||||
"progress_percent": int((step / max_step) * 100)
|
||||
}
|
||||
"progress_percent": int((step / max_step) * 100),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/setup")
|
||||
async def setup_wizard_save(
|
||||
request: Request,
|
||||
step: int = Form(...),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
async def setup_wizard_save(request: Request, step: int = Form(...), db: Session = Depends(get_db)):
|
||||
"""
|
||||
Save settings from the current wizard step.
|
||||
"""
|
||||
try:
|
||||
# Get form data
|
||||
form_data = await request.form()
|
||||
|
||||
|
||||
# Get settings for current step
|
||||
wizard_steps = get_wizard_steps()
|
||||
current_settings = wizard_steps.get(step, [])
|
||||
|
||||
|
||||
# Save each setting from the form
|
||||
saved_count = 0
|
||||
for setting in current_settings:
|
||||
key = setting["key"]
|
||||
value = form_data.get(key)
|
||||
|
||||
|
||||
# Skip empty values unless it's explicitly allowed
|
||||
if value and value.strip():
|
||||
# Auto-generate session_secret if needed
|
||||
if key == "session_secret" and value == "auto-generate":
|
||||
value = secrets.token_hex(32)
|
||||
logger.info("Auto-generated session secret")
|
||||
|
||||
|
||||
# Save to database
|
||||
if save_setting_to_db(db, key, value):
|
||||
saved_count += 1
|
||||
logger.info(f"Setup wizard: Saved {key}")
|
||||
|
||||
|
||||
logger.info(f"Setup wizard step {step}: Saved {saved_count} settings")
|
||||
|
||||
|
||||
# Determine next step
|
||||
max_step = max(wizard_steps.keys())
|
||||
next_step = step + 1
|
||||
|
||||
|
||||
if next_step > max_step:
|
||||
# Setup complete, redirect to home
|
||||
return RedirectResponse(url="/?setup=complete", status_code=303)
|
||||
else:
|
||||
# Go to next step
|
||||
return RedirectResponse(url=f"/setup?step={next_step}", status_code=303)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving wizard settings: {e}")
|
||||
return RedirectResponse(url=f"/setup?step={step}&error=save_failed", status_code=303)
|
||||
@@ -116,7 +107,7 @@ async def setup_wizard_save(
|
||||
async def setup_wizard_skip(request: Request):
|
||||
"""
|
||||
Skip the setup wizard (for advanced users).
|
||||
|
||||
|
||||
Creates a marker to indicate setup was skipped.
|
||||
"""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user