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>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-08 06:02:21 +00:00
parent 5a4d21293b
commit c9e1de4845
4 changed files with 757 additions and 24 deletions
+5 -1
View File
@@ -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"):
+13 -1
View File
@@ -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")
+721 -11
View File
@@ -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,
},
}
+18 -11
View File
@@ -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