From c9e1de48459a71ac0d18641afda6c0cbfe6dfa5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 06:02:21 +0000 Subject: [PATCH] Fix /settings redirect issue and add OAuth admin support - Convert require_admin_access to proper decorator pattern - Fix redirect loop that was sending all users to / - Add is_admin flag handling for OAuth users (checks groups) - Update SETTING_METADATA with all 102 settings from config.py - Improve API admin check with type hints Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/api/settings.py | 6 +- app/auth.py | 14 +- app/utils/settings_service.py | 732 +++++++++++++++++++++++++++++++++- app/views/settings.py | 29 +- 4 files changed, 757 insertions(+), 24 deletions(-) diff --git a/app/api/settings.py b/app/api/settings.py index d35c7c95..84aa5c8c 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -24,9 +24,13 @@ logger = logging.getLogger(__name__) router = APIRouter(prefix="/settings", tags=["settings"]) -def require_admin(request: Request): +def require_admin(request: Request) -> dict: """ Dependency to ensure the user is an admin. + Raises HTTPException if not admin. + + Returns: + User dict from session """ user = request.session.get("user") if not user or not user.get("is_admin"): diff --git a/app/auth.py b/app/auth.py index 40cebc06..5f4054da 100644 --- a/app/auth.py +++ b/app/auth.py @@ -114,10 +114,22 @@ if AUTH_ENABLED: if not user_data.get("picture") and user_data.get("email"): user_data["picture"] = get_gravatar_url(user_data["email"]) + # Check if user is admin based on OAuth groups or specific email + # You can customize this logic based on your OAuth provider's attributes + # For example, check if user has an "admin" group or specific email domain + is_admin = False + if "groups" in user_data: + # Check if user is in admin group + groups = user_data.get("groups", []) + is_admin = "admin" in groups or "administrators" in groups + + # Set is_admin flag (defaults to False for OAuth users unless they're in admin group) + user_data["is_admin"] = is_admin + request.session["user"] = user_data # Log the successful authentication - print(f"User authenticated via OAuth: {user_data.get('email', 'No email')}") + print(f"User authenticated via OAuth: {user_data.get('email', 'No email')} (admin: {is_admin})") # Redirect to original destination or default redirect_url = request.session.pop("redirect_after_login", "/upload") diff --git a/app/utils/settings_service.py b/app/utils/settings_service.py index 0a8bff19..581b12bd 100644 --- a/app/utils/settings_service.py +++ b/app/utils/settings_service.py @@ -48,7 +48,7 @@ SETTING_METADATA = { "description": "External hostname for the application (e.g., docuelevate.example.com)", "type": "string", "sensitive": False, - "required": True, + "required": False, "restart_required": True, }, "debug": { @@ -59,14 +59,6 @@ SETTING_METADATA = { "required": False, "restart_required": True, }, - "allow_file_delete": { - "category": "Core", - "description": "Allow deleting files from the database", - "type": "boolean", - "sensitive": False, - "required": False, - "restart_required": False, - }, "gotenberg_url": { "category": "Core", "description": "Gotenberg service URL for document conversion", @@ -90,7 +82,7 @@ SETTING_METADATA = { "description": "Secret key for session encryption (min 32 characters)", "type": "string", "sensitive": True, - "required": True, + "required": False, "restart_required": True, }, "admin_username": { @@ -109,6 +101,38 @@ SETTING_METADATA = { "required": False, "restart_required": True, }, + "authentik_client_id": { + "category": "Authentication", + "description": "Authentik OAuth2 client ID", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": True, + }, + "authentik_client_secret": { + "category": "Authentication", + "description": "Authentik OAuth2 client secret", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": True, + }, + "authentik_config_url": { + "category": "Authentication", + "description": "Authentik OpenID Connect configuration URL", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": True, + }, + "oauth_provider_name": { + "category": "Authentication", + "description": "Display name for OAuth provider (e.g., 'Authentik', 'Keycloak')", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": True, + }, # AI Services "openai_api_key": { @@ -160,7 +184,693 @@ SETTING_METADATA = { "restart_required": False, }, - # Add more settings metadata as needed... + # Storage Providers - Dropbox + "dropbox_app_key": { + "category": "Storage Providers", + "description": "Dropbox app key for OAuth authentication", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "dropbox_app_secret": { + "category": "Storage Providers", + "description": "Dropbox app secret for OAuth authentication", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "dropbox_folder": { + "category": "Storage Providers", + "description": "Dropbox folder path for document storage", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "dropbox_refresh_token": { + "category": "Storage Providers", + "description": "Dropbox OAuth refresh token", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + + # Storage Providers - Nextcloud + "nextcloud_upload_url": { + "category": "Storage Providers", + "description": "Nextcloud WebDAV upload URL", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "nextcloud_username": { + "category": "Storage Providers", + "description": "Nextcloud username for authentication", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "nextcloud_password": { + "category": "Storage Providers", + "description": "Nextcloud password or app password", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "nextcloud_folder": { + "category": "Storage Providers", + "description": "Nextcloud folder path for document storage", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # Storage Providers - Paperless-ngx + "paperless_ngx_api_token": { + "category": "Storage Providers", + "description": "Paperless-ngx API authentication token", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "paperless_host": { + "category": "Storage Providers", + "description": "Paperless-ngx host URL (e.g., https://paperless.example.com)", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # Storage Providers - Google Drive + "google_drive_credentials_json": { + "category": "Storage Providers", + "description": "Google Drive service account credentials JSON", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "google_drive_folder_id": { + "category": "Storage Providers", + "description": "Google Drive folder ID for document storage", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "google_drive_delegate_to": { + "category": "Storage Providers", + "description": "Optional delegated user email for Google Drive service account", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "google_drive_use_oauth": { + "category": "Storage Providers", + "description": "Use OAuth instead of service account for Google Drive", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "google_drive_client_id": { + "category": "Storage Providers", + "description": "Google Drive OAuth client ID", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "google_drive_client_secret": { + "category": "Storage Providers", + "description": "Google Drive OAuth client secret", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "google_drive_refresh_token": { + "category": "Storage Providers", + "description": "Google Drive OAuth refresh token", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + + # Storage Providers - OneDrive + "onedrive_client_id": { + "category": "Storage Providers", + "description": "OneDrive OAuth client ID", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "onedrive_client_secret": { + "category": "Storage Providers", + "description": "OneDrive OAuth client secret", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "onedrive_tenant_id": { + "category": "Storage Providers", + "description": "OneDrive tenant ID (use 'common' for personal accounts)", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "onedrive_refresh_token": { + "category": "Storage Providers", + "description": "OneDrive OAuth refresh token", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "onedrive_folder_path": { + "category": "Storage Providers", + "description": "OneDrive folder path for document storage", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # Storage Providers - WebDAV + "webdav_url": { + "category": "Storage Providers", + "description": "WebDAV server URL", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "webdav_username": { + "category": "Storage Providers", + "description": "WebDAV username for authentication", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "webdav_password": { + "category": "Storage Providers", + "description": "WebDAV password for authentication", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "webdav_folder": { + "category": "Storage Providers", + "description": "WebDAV folder path for document storage", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "webdav_verify_ssl": { + "category": "Storage Providers", + "description": "Verify SSL certificates for WebDAV connections", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # Storage Providers - FTP + "ftp_host": { + "category": "Storage Providers", + "description": "FTP server hostname or IP address", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "ftp_port": { + "category": "Storage Providers", + "description": "FTP server port (default: 21)", + "type": "integer", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "ftp_username": { + "category": "Storage Providers", + "description": "FTP username for authentication", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "ftp_password": { + "category": "Storage Providers", + "description": "FTP password for authentication", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "ftp_folder": { + "category": "Storage Providers", + "description": "FTP folder path for document storage", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "ftp_use_tls": { + "category": "Storage Providers", + "description": "Use TLS encryption for FTP connections (FTPS)", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "ftp_allow_plaintext": { + "category": "Storage Providers", + "description": "Allow fallback to plaintext FTP if TLS fails", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # Storage Providers - SFTP + "sftp_host": { + "category": "Storage Providers", + "description": "SFTP server hostname or IP address", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "sftp_port": { + "category": "Storage Providers", + "description": "SFTP server port (default: 22)", + "type": "integer", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "sftp_username": { + "category": "Storage Providers", + "description": "SFTP username for authentication", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "sftp_password": { + "category": "Storage Providers", + "description": "SFTP password for authentication", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "sftp_folder": { + "category": "Storage Providers", + "description": "SFTP folder path for document storage", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "sftp_private_key": { + "category": "Storage Providers", + "description": "SFTP private key for key-based authentication", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "sftp_private_key_passphrase": { + "category": "Storage Providers", + "description": "Passphrase for encrypted SFTP private key", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "sftp_disable_host_key_verification": { + "category": "Storage Providers", + "description": "Disable host key verification (not recommended for production)", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # Storage Providers - AWS S3 + "aws_access_key_id": { + "category": "Storage Providers", + "description": "AWS access key ID for S3", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "aws_secret_access_key": { + "category": "Storage Providers", + "description": "AWS secret access key for S3", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "aws_region": { + "category": "Storage Providers", + "description": "AWS region for S3 bucket (default: us-east-1)", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "s3_bucket_name": { + "category": "Storage Providers", + "description": "S3 bucket name for document storage", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "s3_folder_prefix": { + "category": "Storage Providers", + "description": "Optional folder prefix in S3 bucket (e.g., 'uploads/')", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "s3_storage_class": { + "category": "Storage Providers", + "description": "S3 storage class (e.g., STANDARD, INTELLIGENT_TIERING)", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "s3_acl": { + "category": "Storage Providers", + "description": "S3 object ACL (e.g., private, public-read)", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # Email Settings + "email_host": { + "category": "Email", + "description": "SMTP server hostname", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "email_port": { + "category": "Email", + "description": "SMTP server port (default: 587)", + "type": "integer", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "email_username": { + "category": "Email", + "description": "SMTP username for authentication", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "email_password": { + "category": "Email", + "description": "SMTP password for authentication", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "email_use_tls": { + "category": "Email", + "description": "Use TLS encryption for SMTP", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "email_sender": { + "category": "Email", + "description": "From address for outgoing emails (defaults to email_username)", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "email_default_recipient": { + "category": "Email", + "description": "Default recipient email address", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # IMAP Settings - Account 1 + "imap1_host": { + "category": "IMAP", + "description": "IMAP server hostname for account 1", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "imap1_port": { + "category": "IMAP", + "description": "IMAP server port for account 1 (default: 993)", + "type": "integer", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "imap1_username": { + "category": "IMAP", + "description": "IMAP username for account 1", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "imap1_password": { + "category": "IMAP", + "description": "IMAP password for account 1", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "imap1_ssl": { + "category": "IMAP", + "description": "Use SSL for IMAP account 1", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "imap1_poll_interval_minutes": { + "category": "IMAP", + "description": "Poll interval in minutes for IMAP account 1 (default: 5)", + "type": "integer", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "imap1_delete_after_process": { + "category": "IMAP", + "description": "Delete emails after processing for IMAP account 1", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # IMAP Settings - Account 2 + "imap2_host": { + "category": "IMAP", + "description": "IMAP server hostname for account 2", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "imap2_port": { + "category": "IMAP", + "description": "IMAP server port for account 2 (default: 993)", + "type": "integer", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "imap2_username": { + "category": "IMAP", + "description": "IMAP username for account 2", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "imap2_password": { + "category": "IMAP", + "description": "IMAP password for account 2", + "type": "string", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "imap2_ssl": { + "category": "IMAP", + "description": "Use SSL for IMAP account 2", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "imap2_poll_interval_minutes": { + "category": "IMAP", + "description": "Poll interval in minutes for IMAP account 2 (default: 10)", + "type": "integer", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "imap2_delete_after_process": { + "category": "IMAP", + "description": "Delete emails after processing for IMAP account 2", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # Monitoring - Uptime Kuma + "uptime_kuma_url": { + "category": "Monitoring", + "description": "Uptime Kuma push monitor URL", + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "uptime_kuma_ping_interval": { + "category": "Monitoring", + "description": "Uptime Kuma ping interval in minutes (default: 5)", + "type": "integer", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # Processing Settings + "http_request_timeout": { + "category": "Processing", + "description": "Timeout for HTTP requests in seconds (default: 120)", + "type": "integer", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "processall_throttle_threshold": { + "category": "Processing", + "description": "Number of files above which throttling is applied in /processall", + "type": "integer", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "processall_throttle_delay": { + "category": "Processing", + "description": "Delay in seconds between task submissions when throttling in /processall", + "type": "integer", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # Notifications Settings + "notification_urls": { + "category": "Notifications", + "description": "Comma-separated list of Apprise notification URLs (e.g., discord://, telegram://)", + "type": "list", + "sensitive": True, + "required": False, + "restart_required": False, + }, + "notify_on_task_failure": { + "category": "Notifications", + "description": "Send notifications when Celery tasks fail", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "notify_on_credential_failure": { + "category": "Notifications", + "description": "Send notifications when credential checks fail", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "notify_on_startup": { + "category": "Notifications", + "description": "Send notifications when application starts", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "notify_on_shutdown": { + "category": "Notifications", + "description": "Send notifications when application shuts down", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + "notify_on_file_processed": { + "category": "Notifications", + "description": "Send notifications when files are successfully processed", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, + + # Feature Flags + "allow_file_delete": { + "category": "Feature Flags", + "description": "Allow deleting files from the database", + "type": "boolean", + "sensitive": False, + "required": False, + "restart_required": False, + }, } diff --git a/app/views/settings.py b/app/views/settings.py index ecb9d799..1a4592f5 100644 --- a/app/views/settings.py +++ b/app/views/settings.py @@ -3,6 +3,8 @@ Settings management views for the application. """ import logging +import inspect +from functools import wraps from fastapi import Request, Depends, HTTPException, status from fastapi.responses import RedirectResponse from sqlalchemy.orm import Session @@ -15,25 +17,30 @@ logger = logging.getLogger(__name__) router = APIRouter() -def require_admin_access(request: Request): - """Check if user is admin and redirect if not""" - user = request.session.get("user") - if not user or not user.get("is_admin"): - logger.warning(f"Non-admin user attempted to access settings page") - return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) - return None +def require_admin_access(func): + """Decorator to require admin access for a route""" + @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 settings page") + return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) + + # Check if the wrapped function is a coroutine function + if inspect.iscoroutinefunction(func): + return await func(request, *args, **kwargs) + else: + return func(request, *args, **kwargs) + return wrapper @router.get("/settings") @require_login +@require_admin_access async def settings_page(request: Request, db: Session = Depends(get_db)): """ Settings management page - admin only. """ - # Check admin access - redirect = require_admin_access(request) - if redirect: - return redirect try: # Get settings organized by category