diff --git a/app/frontend.py b/app/frontend.py index b38befc1..35954cb7 100644 --- a/app/frontend.py +++ b/app/frontend.py @@ -70,6 +70,7 @@ async def status_dashboard(request: Request): { "request": request, "providers": providers, + "app_version": settings.version, # Add app version to the context "debug_enabled": getattr(settings, 'debug', False), "last_check": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } diff --git a/app/utils/config_validator.py b/app/utils/config_validator.py index 6bc87ea3..767a96aa 100644 --- a/app/utils/config_validator.py +++ b/app/utils/config_validator.py @@ -137,125 +137,223 @@ def validate_storage_configs(): return issues +def mask_sensitive_value(value): + """Helper function to mask sensitive values consistently""" + if not value: + return "Not set" + + if isinstance(value, str): + if len(value) > 10: + visible_start = max(1, len(value) // 3) + visible_end = max(1, len(value) // 4) + return f"{value[:visible_start]}{'*' * (len(value) - visible_start - visible_end)}{value[-visible_end:]}" + else: + return f"{value[:2]}{'*' * (len(value) - 4)}{value[-2:]}" if len(value) > 4 else "****" + elif not isinstance(value, (bool, int, float)): + return "**Configured Value**" + return str(value) + def get_provider_status(): """Returns status information for all configured providers""" providers = {} - # Check Dropbox configuration + # Add Dropbox configuration - alphabetically ordered providers providers["Dropbox"] = { "name": "Dropbox", + "icon": "dropbox", "configured": bool(getattr(settings, 'dropbox_app_key', None) and getattr(settings, 'dropbox_app_secret', None) and getattr(settings, 'dropbox_refresh_token', None)), "enabled": True, + "description": "Upload files to Dropbox cloud storage", "details": { "folder": getattr(settings, 'dropbox_folder', 'Not set'), "app_key": getattr(settings, 'dropbox_app_key', 'Not set'), - "app_secret": getattr(settings, 'dropbox_app_secret', None) and "Configured" or "Not set", - "refresh_token": getattr(settings, 'dropbox_refresh_token', None) and "Configured" or "Not set" + "app_secret": mask_sensitive_value(getattr(settings, 'dropbox_app_secret', None)), + "refresh_token": mask_sensitive_value(getattr(settings, 'dropbox_refresh_token', None)) } } - # Check Paperless configuration - providers["Paperless-ngx"] = { - "name": "Paperless-ngx", - "configured": bool(getattr(settings, 'paperless_host', None) and - getattr(settings, 'paperless_ngx_api_token', None)), + # Add Email configuration + providers["Email"] = { + "name": "Email", + "icon": "mail", + "configured": bool(getattr(settings, 'email_host', None) and + getattr(settings, 'email_default_recipient', None)), "enabled": True, + "description": "Send documents via email", "details": { - "host": getattr(settings, 'paperless_host', 'Not set') + "host": getattr(settings, 'email_host', 'Not set'), + "port": getattr(settings, 'email_port', 'Not set'), + "username": getattr(settings, 'email_username', 'Not set'), + "password": mask_sensitive_value(getattr(settings, 'email_password', None)), + "use_tls": getattr(settings, 'email_use_tls', 'Not set'), + "sender": getattr(settings, 'email_sender', 'Not set'), + "default_recipient": getattr(settings, 'email_default_recipient', 'Not set') } } - # Check NextCloud configuration - providers["NextCloud"] = { - "name": "NextCloud", - "configured": bool(getattr(settings, 'nextcloud_upload_url', None) and - getattr(settings, 'nextcloud_username', None) and - getattr(settings, 'nextcloud_password', None)), + # Add FTP configuration to providers + providers["FTP Storage"] = { + "name": "FTP Storage", + "icon": "ftp", + "configured": bool(getattr(settings, 'ftp_host', None) and + getattr(settings, 'ftp_username', None) and + getattr(settings, 'ftp_password', None)), "enabled": True, + "description": "Upload files to FTP server", "details": { - "url": getattr(settings, 'nextcloud_upload_url', 'Not set'), - "folder": getattr(settings, 'nextcloud_folder', 'Not set') - } - } - - # Check SFTP configuration - providers["SFTP Storage"] = { - "name": "SFTP Storage", - "configured": bool(getattr(settings, 'sftp_host', None) and - getattr(settings, 'sftp_username', None) and - (getattr(settings, 'sftp_password', None) or - getattr(settings, 'sftp_private_key', None))), - "enabled": True, - "details": { - "host": getattr(settings, 'sftp_host', 'Not set'), - "folder": getattr(settings, 'sftp_folder', 'Not set') - } - } - - # Check S3 configuration - providers["S3 Storage"] = { - "name": "S3 Storage", - "configured": bool(getattr(settings, 's3_bucket_name', None) and - getattr(settings, 'aws_access_key_id', None) and - getattr(settings, 'aws_secret_access_key', None)), - "enabled": True, - "details": { - "bucket": getattr(settings, 's3_bucket_name', 'Not set'), - "region": getattr(settings, 'aws_region', 'Not set') + "host": getattr(settings, 'ftp_host', 'Not set'), + "port": getattr(settings, 'ftp_port', 'Not set'), + "username": getattr(settings, 'ftp_username', 'Not set'), + "password": mask_sensitive_value(getattr(settings, 'ftp_password', None)), + "folder": getattr(settings, 'ftp_folder', 'Not set'), + "tls": getattr(settings, 'ftp_use_tls', True), + "allow_plaintext": getattr(settings, 'ftp_allow_plaintext', True) } } # Check Google Drive configuration providers["Google Drive"] = { "name": "Google Drive", + "icon": "google", "configured": bool(getattr(settings, 'google_drive_credentials_json', None) and getattr(settings, 'google_drive_folder_id', None)), "enabled": True, + "description": "Store documents in Google Drive", "details": { + "credentials_json": mask_sensitive_value(getattr(settings, 'google_drive_credentials_json', None)), "folder_id": getattr(settings, 'google_drive_folder_id', 'Not set'), "delegate": getattr(settings, 'google_drive_delegate_to', 'Not set') } } + # Check NextCloud configuration + nextcloud_url = getattr(settings, 'nextcloud_upload_url', 'Not set') + # Extract base URL from WebDAV URL (remove the /remote.php part and everything after it) + if nextcloud_url != 'Not set' and '/remote.php' in nextcloud_url: + nextcloud_base_url = nextcloud_url.split('/remote.php')[0] + else: + nextcloud_base_url = nextcloud_url + + providers["NextCloud"] = { + "name": "NextCloud", + "icon": "cloud", + "configured": bool(getattr(settings, 'nextcloud_upload_url', None) and + getattr(settings, 'nextcloud_username', None) and + getattr(settings, 'nextcloud_password', None)), + "enabled": True, + "description": "Store documents in NextCloud", + "details": { + "url": getattr(settings, 'nextcloud_upload_url', 'Not set'), + "base_url": nextcloud_base_url, + "username": getattr(settings, 'nextcloud_username', 'Not set'), + "password": mask_sensitive_value(getattr(settings, 'nextcloud_password', None)), + "folder": getattr(settings, 'nextcloud_folder', 'Not set') + } + } + # Check OneDrive configuration providers["OneDrive"] = { "name": "OneDrive", + "icon": "microsoft", "configured": bool(getattr(settings, 'onedrive_client_id', None) and getattr(settings, 'onedrive_client_secret', None) and getattr(settings, 'onedrive_refresh_token', None)), "enabled": True, + "description": "Store documents in Microsoft OneDrive", "details": { + "client_id": getattr(settings, 'onedrive_client_id', 'Not set'), + "client_secret": mask_sensitive_value(getattr(settings, 'onedrive_client_secret', None)), + "tenant_id": getattr(settings, 'onedrive_tenant_id', 'Not set'), + "refresh_token": mask_sensitive_value(getattr(settings, 'onedrive_refresh_token', None)), "folder": getattr(settings, 'onedrive_folder_path', 'Not set') } } + + # Check Paperless configuration + providers["Paperless-ngx"] = { + "name": "Paperless-ngx", + "icon": "file-text", + "configured": bool(getattr(settings, 'paperless_host', None) and + getattr(settings, 'paperless_ngx_api_token', None)), + "enabled": True, + "description": "Document management system for digital archives", + "details": { + "host": getattr(settings, 'paperless_host', 'Not set'), + "api_token": mask_sensitive_value(getattr(settings, 'paperless_ngx_api_token', None)) + } + } + + # Check S3 configuration + providers["S3 Storage"] = { + "name": "S3 Storage", + "icon": "database", + "configured": bool(getattr(settings, 's3_bucket_name', None) and + getattr(settings, 'aws_access_key_id', None) and + getattr(settings, 'aws_secret_access_key', None)), + "enabled": True, + "description": "Store documents in S3-compatible object storage", + "details": { + "bucket": getattr(settings, 's3_bucket_name', 'Not set'), + "region": getattr(settings, 'aws_region', 'Not set'), + "access_key_id": getattr(settings, 'aws_access_key_id', 'Not set'), + "secret_access_key": mask_sensitive_value(getattr(settings, 'aws_secret_access_key', None)), + "folder_prefix": getattr(settings, 's3_folder_prefix', 'Not set'), + "storage_class": getattr(settings, 's3_storage_class', 'Not set'), + "acl": getattr(settings, 's3_acl', 'Not set') + } + } + + # Check SFTP configuration + providers["SFTP Storage"] = { + "name": "SFTP Storage", + "icon": "server", + "configured": bool(getattr(settings, 'sftp_host', None) and + getattr(settings, 'sftp_username', None) and + (getattr(settings, 'sftp_password', None) or + getattr(settings, 'sftp_private_key', None))), + "enabled": True, + "description": "Upload files to SFTP server", + "details": { + "host": getattr(settings, 'sftp_host', 'Not set'), + "port": getattr(settings, 'sftp_port', 'Not set'), + "username": getattr(settings, 'sftp_username', 'Not set'), + "password": mask_sensitive_value(getattr(settings, 'sftp_password', None)), + "private_key": getattr(settings, 'sftp_private_key', 'Not set'), + "private_key_passphrase": mask_sensitive_value(getattr(settings, 'sftp_private_key_passphrase', None)), + "folder": getattr(settings, 'sftp_folder', 'Not set') + } + } + + # Add Uptime Kuma configuration + providers["Uptime Kuma"] = { + "name": "Uptime Kuma", + "icon": "activity", + "configured": bool(getattr(settings, 'uptime_kuma_url', None)), + "enabled": True, + "description": "Server monitoring and status page", + "details": { + "url": getattr(settings, 'uptime_kuma_url', 'Not set'), + "ping_interval": getattr(settings, 'uptime_kuma_ping_interval', 'Not set') + } + } # Check WebDAV configuration providers["WebDAV"] = { "name": "WebDAV", + "icon": "globe", "configured": bool(getattr(settings, 'webdav_url', None) and getattr(settings, 'webdav_username', None) and getattr(settings, 'webdav_password', None)), "enabled": True, + "description": "Store documents on WebDAV servers", "details": { "url": getattr(settings, 'webdav_url', 'Not set'), - "folder": getattr(settings, 'webdav_folder', 'Not set') - } - } - - # Add FTP configuration to providers - providers["FTP Storage"] = { - "name": "FTP Storage", - "configured": bool(getattr(settings, 'ftp_host', None) and - getattr(settings, 'ftp_username', None) and - getattr(settings, 'ftp_password', None)), - "enabled": True, - "details": { - "host": getattr(settings, 'ftp_host', 'Not set'), - "folder": getattr(settings, 'ftp_folder', 'Not set'), - "tls": getattr(settings, 'ftp_use_tls', True), - "allow_plaintext": getattr(settings, 'ftp_allow_plaintext', True) + "username": getattr(settings, 'webdav_username', 'Not set'), + "password": mask_sensitive_value(getattr(settings, 'webdav_password', None)), + "folder": getattr(settings, 'webdav_folder', 'Not set'), + "verify_ssl": getattr(settings, 'webdav_verify_ssl', 'Not set') } } @@ -270,7 +368,12 @@ def dump_all_settings(): # Mask sensitive values in logs if key.lower().find('password') >= 0 or key.lower().find('secret') >= 0 or key.lower().find('token') >= 0 or key.lower().find('key') >= 0: if value: - value = "********" + if isinstance(value, str) and len(value) > 10: + visible_start = max(1, len(value) // 3) + visible_end = max(1, len(value) // 4) + value = f"{value[:visible_start]}{'*' * (len(value) - visible_start - visible_end)}{value[-visible_end:]}" + else: + value = f"{value[:2]}{'*' * (len(value) - 4)}{value[-2:]}" if isinstance(value, str) and len(value) > 4 else "****" logger.info(f"{key}: {value}") logger.info("--- END OF SETTINGS DUMP ---") @@ -439,17 +542,26 @@ def get_settings_for_display(show_values=False): # List of patterns that indicate sensitive values sensitive_patterns = [ 'password', 'secret', 'token', 'api_key', 'private_key', - 'credentials', 'access_key', 'auth' + 'credentials', 'access_key', 'ai_key' ] # Check if this is a sensitive value that should be masked - is_sensitive = any(pattern in key.lower() for pattern in sensitive_patterns) + is_sensitive = any( + pattern in key.lower() for pattern in sensitive_patterns + ) + + # Special handling for "auth" to avoid matching prefixes like "authentik" + if not is_sensitive and "auth" in key.lower(): + # Only mark as sensitive if "auth" is a standalone word or at the end + # This avoids matching "authentik" as sensitive + parts = key.lower().split('_') + is_sensitive = any(part == "auth" for part in parts) or key.lower().endswith("auth") # Mask sensitive values regardless of debug mode # Other values are only hidden if debug mode is off AND show_values is False if (is_sensitive or not show_values) and value: if is_sensitive: - value = "********" + value = mask_sensitive_value(value) # Check if the setting is configured (has a non-None value) # For boolean settings, consider them configured even if False diff --git a/frontend/templates/base.html b/frontend/templates/base.html index 0bfa04f0..9122034c 100644 --- a/frontend/templates/base.html +++ b/frontend/templates/base.html @@ -36,35 +36,6 @@ Upload Files Status - - -
- - -
- -
-
- About diff --git a/frontend/templates/onedrive.html b/frontend/templates/onedrive.html index cdd8ff9f..d3d6d604 100644 --- a/frontend/templates/onedrive.html +++ b/frontend/templates/onedrive.html @@ -112,6 +112,12 @@

Use "common" for personal accounts or your organization's Tenant ID for corporate accounts

+
+ + +

Enter the folder path where files should be uploaded (e.g., Documents/Uploads)

+
+
+ + + {% endblock %} @@ -216,25 +244,78 @@ document.addEventListener('DOMContentLoaded', function() { const tokenStatus = document.getElementById('token-status'); const clientSecretInput = document.getElementById('client-secret'); + // Modal elements + const resultModal = document.getElementById('resultModal'); + const modalTitle = document.getElementById('modalTitle'); + const modalMessage = document.getElementById('modalMessage'); + const modalIcon = document.getElementById('modalIcon'); + const modalClose = document.getElementById('modalClose'); + + // Modal functions + function showModal(status, title, message) { + modalTitle.textContent = title; + modalMessage.textContent = message; + + // Set the appropriate icon + if (status === 'success') { + modalIcon.innerHTML = ` + + + + `; + modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4'; + } else { + modalIcon.innerHTML = ` + + + + `; + modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4'; + } + + resultModal.classList.remove('hidden'); + } + + function hideModal() { + resultModal.classList.add('hidden'); + } + + // Close modal when clicking the close button + modalClose.addEventListener('click', hideModal); + + // Close modal when clicking outside of it + resultModal.addEventListener('click', function(e) { + if (e.target === resultModal) { + hideModal(); + } + }); + // Start Authentication Flow button click startAuthFlowBtn.addEventListener('click', function() { const clientId = document.getElementById('client-id').value.trim(); const clientSecret = clientSecretInput.value.trim(); const redirectUri = window.location.origin + "/onedrive-callback"; const tenantId = document.getElementById('tenant-id').value.trim() || 'common'; + const folderPath = document.getElementById('folder-path') ? document.getElementById('folder-path').value.trim() : ''; if (!clientId) { - alert('Please enter your Client ID'); + showModal('error', 'Validation Error', 'Please enter your Client ID'); return; } if (!clientSecret) { - alert('Please enter your Client Secret'); + showModal('error', 'Validation Error', 'Please enter your Client Secret'); return; } - // Save client secret to session storage temporarily + // Save all entered values to session storage temporarily + sessionStorage.setItem('onedrive_client_id', clientId); sessionStorage.setItem('onedrive_client_secret', clientSecret); + sessionStorage.setItem('onedrive_tenant_id', tenantId); + + if (folderPath) { + sessionStorage.setItem('onedrive_folder_path', folderPath); + } // Generate the authorization URL with .default scope const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?client_id=${encodeURIComponent(clientId)}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&response_mode=query&scope=${encodeURIComponent('https://graph.microsoft.com/.default offline_access')}&prompt=consent`; @@ -253,19 +334,25 @@ document.addEventListener('DOMContentLoaded', function() { .then(response => response.json()) .then(data => { if (data.status === 'success') { - alert('Token is valid! Successfully connected to OneDrive.'); + showModal('success', 'Token Valid', 'Successfully connected to OneDrive!'); } else { if (data.message && data.message.includes('invalid_grant')) { - if (confirm('Your OneDrive token has expired or is invalid. Would you like to obtain a new token now?')) { + showModal('error', 'Token Invalid', 'Your OneDrive token has expired or is invalid. Please obtain a new token.'); + // Give option to start new auth flow + modalClose.textContent = "Get New Token"; + modalClose.addEventListener('click', function onGetNewToken() { startAuthFlowBtn.click(); - } + // Remove this special event handler after use + modalClose.removeEventListener('click', onGetNewToken); + modalClose.textContent = "Close"; + }, { once: true }); } else { - alert('Token validation failed: ' + data.message); + showModal('error', 'Token Test Failed', data.message); } } }) .catch(error => { - alert('Error testing token: ' + error.message); + showModal('error', 'Connection Error', 'Error testing token: ' + error.message); }) .finally(() => { testTokenBtn.innerHTML = 'Test Token'; @@ -277,9 +364,31 @@ document.addEventListener('DOMContentLoaded', function() { // Refresh Token button click if (refreshTokenBtn) { refreshTokenBtn.addEventListener('click', function() { - if (confirm('This will start a new authentication flow to obtain a fresh token from Microsoft. Continue?')) { + showModal('info', 'Confirm', 'This will start a new authentication flow to obtain a fresh token from Microsoft. Continue?'); + modalClose.textContent = "Cancel"; + + // Add a confirm button + const confirmBtn = document.createElement('button'); + confirmBtn.className = 'ml-2 px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-300'; + confirmBtn.textContent = 'Continue'; + confirmBtn.addEventListener('click', function() { + hideModal(); startAuthFlowBtn.click(); - } + }); + + // Add to modal + modalClose.parentNode.appendChild(confirmBtn); + + // Make sure to remove the confirm button when modal is closed + const removeConfirmBtn = function() { + if (confirmBtn.parentNode) { + confirmBtn.parentNode.removeChild(confirmBtn); + } + modalClose.textContent = "Close"; + modalClose.removeEventListener('click', removeConfirmBtn); + }; + + modalClose.addEventListener('click', removeConfirmBtn, { once: true }); }); } @@ -300,16 +409,32 @@ document.addEventListener('DOMContentLoaded', function() { }) .catch(err => { console.error('Failed to copy: ', err); - alert('Failed to copy text to clipboard'); + showModal('error', 'Copy Failed', 'Failed to copy text to clipboard'); }); }); } - // Try to retrieve client secret from session storage (if coming back from auth) + // Try to retrieve values from session storage (if coming back from auth or browser refresh) if (clientSecretInput && !clientSecretInput.value && sessionStorage.getItem('onedrive_client_secret')) { clientSecretInput.value = sessionStorage.getItem('onedrive_client_secret'); - // Clear it after use - sessionStorage.removeItem('onedrive_client_secret'); + } + + // Also check for client ID in session storage + const clientIdInput = document.getElementById('client-id'); + if (clientIdInput && !clientIdInput.value && sessionStorage.getItem('onedrive_client_id')) { + clientIdInput.value = sessionStorage.getItem('onedrive_client_id'); + } + + // Check for tenant ID in session storage + const tenantIdInput = document.getElementById('tenant-id'); + if (tenantIdInput && !tenantIdInput.value && sessionStorage.getItem('onedrive_tenant_id')) { + tenantIdInput.value = sessionStorage.getItem('onedrive_tenant_id'); + } + + // Check for folder path in session storage + const folderPathInput = document.getElementById('folder-path'); + if (folderPathInput && !folderPathInput.value && sessionStorage.getItem('onedrive_folder_path')) { + folderPathInput.value = sessionStorage.getItem('onedrive_folder_path'); } // If token is not configured but we have a client ID, show the token status section diff --git a/frontend/templates/onedrive_callback.html b/frontend/templates/onedrive_callback.html index 4f827487..468d2ba9 100644 --- a/frontend/templates/onedrive_callback.html +++ b/frontend/templates/onedrive_callback.html @@ -92,24 +92,34 @@ +{% endblock %}